[
  {
    "path": ".circleci/config.yml",
    "content": "# This config and the the Gradle flags/opts are based on: https://circleci.com/docs/2.0/language-android/\n# and https://support.circleci.com/hc/en-us/articles/360021812453\n\nversion: 2\n\nreferences:\n  android_config_small: &android_config_small\n    working_directory: ~/work\n    docker:\n      - image: cimg/android:2026.02\n    resource_class: small\n\n  android_config: &android_config\n    working_directory: ~/work\n    docker:\n      - image: cimg/android:2026.02\n    resource_class: large\n\n  android_config_large: &android_config_large\n    working_directory: ~/work\n    docker:\n      - image: cimg/android:2026.02\n    resource_class: xlarge\n\n  gcloud_config: &gcloud_config\n    working_directory: ~/work\n    docker:\n      - image: cimg/gcp:2025.01\n    resource_class: small\n\njobs:\n  compile:\n    <<: *android_config\n    steps:\n      - checkout\n      - restore_cache:\n          keys:\n            - compile-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n            - compile-\n      - run:\n          name: Copy gradle config\n          command: mkdir -p ~/.gradle && cp .circleci/gradle.properties ~/.gradle/gradle.properties\n      - run:\n          name: Download Robolectric deps\n          command: ./download-robolectric-deps.sh\n\n      - run:\n          name: Compile code\n          command: ./gradlew assembleDebug\n\n      - save_cache:\n          paths:\n            - ~/.gradle/caches\n            - ~/.gradle/wrapper\n          key: compile-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n      - save_cache:\n          paths:\n            - ~/work\n          key: workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}\n\n      - persist_to_workspace:\n          root: ~/work\n          paths:\n            - collect_app/build/outputs/apk\n\n  create_dependency_backup:\n    <<: *android_config_small\n    steps:\n      - checkout\n      - restore_cache:\n          keys:\n            - workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}\n      - restore_cache:\n          keys:\n            - compile-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n            - compile-\n\n      - run:\n          name: Create Maven repo from dependencies\n          command: ./gradlew cacheToMavenLocal\n      - run:\n          name: Compress Maven repo\n          command: tar -cvzf maven.tar .local-m2\n      - store_artifacts:\n          path: maven.tar\n\n  check_quality:\n    <<: *android_config\n    steps:\n      - checkout\n      - restore_cache:\n          keys:\n            - workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}\n      - restore_cache:\n          keys:\n            - compile-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n            - compile-\n\n      - run:\n          name: Copy gradle config\n          command: mkdir -p ~/.gradle && cp .circleci/gradle.properties ~/.gradle/gradle.properties\n\n      - run:\n          name: Run code quality checks\n          command: ./gradlew pmd ktlintCheck checkstyle\n\n      - run:\n          name: Run Android lint\n          command: ./gradlew lintDebug\n\n  test_modules:\n    <<: *android_config\n    parallelism: 4\n    steps:\n      - checkout\n      - restore_cache:\n          keys:\n            - workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}\n      - restore_cache:\n          keys:\n            - test_modules-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n            - test_modules\n            - compile-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n            - compile-\n\n      - run:\n          name: Copy gradle config\n          command: mkdir -p ~/.gradle && cp .circleci/gradle.properties ~/.gradle/gradle.properties\n\n      - run:\n          name: Generate list of modules for this fork\n          command: |\n            cat .circleci/test_modules.txt | circleci tests split > .circleci/fork_test_modules.txt && \\\n            echo \"Modules for this fork:\" && \\\n            cat .circleci/fork_test_modules.txt\n\n      - run:\n          name: Run module unit tests\n          command: |\n            ./gradlew $(cat .circleci/fork_test_modules.txt | awk '{for (i=1; i<=NF; i++) printf \"%s:testDebug \",$i}')\n\n      - store_test_results:\n          path: collect_app/build/test-results\n\n      - save_cache:\n          paths:\n            - ~/.gradle/caches\n            - ~/.gradle/wrapper\n          key: test_modules-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n\n  test_app:\n    <<: *android_config\n    parallelism: 4\n    steps:\n      - checkout\n      - restore_cache:\n          keys:\n            - workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}\n      - restore_cache:\n          keys:\n            - test_app-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n            - test_app-\n            - compile-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n            - compile-\n\n      - run:\n          name: Copy gradle config\n          command: mkdir -p ~/.gradle && cp .circleci/gradle.properties ~/.gradle/gradle.properties\n\n      - run:\n          name: Generate list of test classes\n          command: .circleci/generate-app-test-list.sh\n\n      - run:\n          name: Generate list of tests for this fork\n          command: |\n            cat .circleci/collect_app_test_classes.txt | circleci tests split > .circleci/fork_test_classes.txt && \\\n            echo \"Tests for this fork:\" && \\\n            cat .circleci/fork_test_classes.txt && \\\n            echo \"\" && \\\n            echo \"Will run command:\" && \\\n            echo \"./gradlew collect_app:testDebug $(cat .circleci/fork_test_classes.txt | awk '{for (i=1; i<=NF; i++) printf \"--tests %s \",$i}')\"\n\n      - run:\n          name: Run app unit tests\n          command: |\n            ./gradlew collect_app:testDebug $(cat .circleci/fork_test_classes.txt | awk '{for (i=1; i<=NF; i++) printf \"--tests %s \",$i}')\n\n      - store_artifacts:\n          path: collect_app/build/reports\n          destination: reports\n      - store_test_results:\n          path: collect_app/build/test-results\n\n      - save_cache:\n          paths:\n            - ~/.gradle/caches\n            - ~/.gradle/wrapper\n          key: test_app-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n\n  build_instrumented:\n    <<: *android_config_large\n    steps:\n      - checkout\n      - restore_cache:\n          keys:\n            - workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}\n      - restore_cache:\n          keys:\n            - build_instrumented-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n            - build_instrumented-\n            - compile-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n            - compile-\n\n      - run:\n          name: Copy gradle config\n          command: mkdir -p ~/.gradle && cp .circleci/gradle-large.properties ~/.gradle/gradle.properties\n\n      - run:\n          name: Assemble connected test build\n          command: ./gradlew assembleDebugAndroidTest\n\n      - save_cache:\n          paths:\n            - ~/.gradle/caches\n            - ~/.gradle/wrapper\n          key: build_instrumented-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n\n      - persist_to_workspace:\n          root: ~/work\n          paths:\n            - collect_app/build/outputs/apk\n\n  build_release:\n    <<: *android_config\n    steps:\n      - checkout\n      - restore_cache:\n          keys:\n            - workflow-{{ .Environment.CIRCLE_WORKFLOW_ID }}\n      - restore_cache:\n          keys:\n            - compile-{{ checksum \"gradle/libs.versions.toml\" }}-{{ checksum \"gradle/wrapper/gradle-wrapper.properties\" }}\n            - compile-\n\n      - run:\n          name: Copy gradle config\n          command: mkdir -p ~/.gradle && cp .circleci/gradle.properties ~/.gradle/gradle.properties\n\n      - run:\n          name: Set up secrets.properties for Maps frameworks\n          command: |\n            if [[ -n \"$GOOGLE_MAPS_API_KEY\" ]]; then \\\n              echo \"GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY\" >> secrets.properties\n              echo \"MAPBOX_ACCESS_TOKEN=$MAPBOX_ACCESS_TOKEN\" >> secrets.properties\n              echo \"MAPBOX_DOWNLOADS_TOKEN=$MAPBOX_DOWNLOADS_TOKEN\" >> secrets.properties\n            fi\n\n      - run:\n          name: Assemble self signed release build\n          command: ./gradlew assembleSelfSignedRelease\n\n      - run:\n          name: Check APK size hasn't increased\n          command: |\n            if [[ -n \"$GOOGLE_MAPS_API_KEY\" ]]; then \\\n              ./check-size.sh 20447232\n            else\n              ./check-size.sh 11010048\n            fi\n\n      - run:\n          name: Copy APK to predictable path for artifact storage\n          command: cp collect_app/build/outputs/apk/selfSignedRelease/*.apk selfSignedRelease.apk\n      - store_artifacts:\n          path: selfSignedRelease.apk\n\n  test_smoke_instrumented:\n    <<: *gcloud_config\n    steps:\n      - attach_workspace:\n          at: /tmp/workspace\n\n      - run:\n          name: Authorize gcloud\n          command: |\n            if [[ \"$CIRCLE_PROJECT_USERNAME\" == \"getodk\" ]]; then \\\n              gcloud config set project api-project-322300403941\n              echo $GCLOUD_SERVICE_KEY | base64 --decode > client-secret.json\n              gcloud auth activate-service-account --key-file client-secret.json\n            fi\n      - run:\n          name: Run integration tests\n          command: |\n            if [[ \"$CIRCLE_PROJECT_USERNAME\" == \"getodk\" ]]; then \\\n              echo \"y\" | gcloud beta firebase test android run \\\n              --type instrumentation \\\n              --app /tmp/workspace/collect_app/build/outputs/apk/debug/ODK-Collect-debug.apk \\\n              --test /tmp/workspace/collect_app/build/outputs/apk/androidTest/debug/ODK-Collect-debug-androidTest.apk \\\n              --device model=MediumPhone.arm,version=34,locale=en,orientation=portrait \\\n              --results-bucket opendatakit-collect-test-results \\\n              --directories-to-pull /sdcard --timeout 20m \\\n              --test-targets \"package org.odk.collect.android.feature.smoke\"\n            fi\n          no_output_timeout: 25m\n\n  test_instrumented:\n    <<: *gcloud_config\n    steps:\n      - attach_workspace:\n          at: /tmp/workspace\n\n      - run:\n          name: Authorize gcloud\n          command: |\n            if [[ -n \"$GCLOUD_SERVICE_KEY\" ]]; then \\\n              gcloud config set project api-project-322300403941\n              echo $GCLOUD_SERVICE_KEY | base64 --decode > client-secret.json\n              gcloud auth activate-service-account --key-file client-secret.json\n            fi\n      - run:\n          name: Run integration tests\n          command: |\n            echo \"y\" | gcloud beta firebase test android run \\\n              --type instrumentation \\\n              --num-uniform-shards=25 \\\n              --app /tmp/workspace/collect_app/build/outputs/apk/debug/ODK-Collect-debug.apk \\\n              --test /tmp/workspace/collect_app/build/outputs/apk/androidTest/debug/ODK-Collect-debug-androidTest.apk \\\n              --device model=MediumPhone.arm,version=34,locale=en,orientation=portrait \\\n              --results-bucket opendatakit-collect-test-results \\\n              --directories-to-pull /sdcard --timeout 20m \\\n              --test-targets \"notPackage org.odk.collect.android.regression\" \\\n              --test-targets \"notPackage org.odk.collect.android.benchmark\"\n          no_output_timeout: 25m\n\nworkflows:\n  version: 2\n  pr:\n    jobs:\n      - hold_pr:\n          type: approval\n          filters:\n            branches:\n              ignore:\n                - master\n                - ^v((20)[0-9]{2})\\.\\d+\\.x$\n      - compile:\n          requires:\n            - hold_pr\n      - check_quality:\n          requires:\n            - compile\n      - test_modules:\n          requires:\n            - compile\n      - test_app:\n          requires:\n            - compile\n      - build_release:\n          requires:\n            - compile\n      - build_instrumented:\n          requires:\n            - compile\n\n  master:\n    jobs:\n      - compile:\n          filters:\n            branches:\n              only:\n                - master\n                - ^v((20)[0-9]{2})\\.\\d+\\.x$\n      - check_quality:\n          requires:\n            - compile\n      - test_modules:\n          requires:\n            - compile\n      - test_app:\n          requires:\n            - compile\n      - build_release:\n          requires:\n            - compile\n      - build_instrumented:\n          requires:\n            - compile\n      - test_smoke_instrumented:\n          requires:\n            - build_instrumented\n\n  nightly:\n    triggers:\n      - schedule:\n          cron: \"0 0 * * 1-5\"\n          filters:\n            branches:\n              only:\n                - master\n    jobs:\n      - compile\n      - build_instrumented:\n          requires:\n            - compile\n      - test_instrumented:\n          requires:\n            - build_instrumented\n\n  release:\n    jobs:\n      - compile:\n          filters:\n            tags:\n              only: /^v((20)[0-9]{2})\\.\\d+\\.\\d+$/ # matches semvers like v1.2.3\n            branches:\n              ignore: /.*/\n      - create_dependency_backup:\n          requires:\n            - compile\n          filters:\n            tags:\n              only: /^v((20)[0-9]{2})\\.\\d+\\.\\d+$/ # matches semvers like v1.2.3\n            branches:\n              ignore: /.*/\n\n"
  },
  {
    "path": ".circleci/generate-app-test-list.sh",
    "content": "ls -R collect_app/src/test/java/ | grep Test.java > .circleci/collect_app_test_files.txt\nls -R collect_app/src/test/java/ | grep Test.kt >> .circleci/collect_app_test_files.txt\ncat .circleci/collect_app_test_files.txt | sed \"s/\\.java//\" | sed \"s/\\.kt//\" > .circleci/collect_app_test_classes.txt\n"
  },
  {
    "path": ".circleci/gradle-large.properties",
    "content": "# Gradle config for \"X-Large\" Circle CI resource (https://circleci.com/pricing/price-list/)\n\norg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options=-Xmx2g\norg.gradle.daemon=false\norg.gradle.parallel=true\norg.gradle.workers.max=8\ntest.heap.max=1g\n"
  },
  {
    "path": ".circleci/gradle.properties",
    "content": "# Gradle config for \"Large\" Circle CI resource (https://circleci.com/pricing/price-list/)\n\norg.gradle.jvmargs=-Xmx2560m -XX:MaxMetaspaceSize=1g\norg.gradle.daemon=false\norg.gradle.parallel=true\norg.gradle.workers.max=4\ntest.heap.max=1g\n"
  },
  {
    "path": ".circleci/test_modules.txt",
    "content": "shared\nforms-test\nandroidshared\nasync\nstrings\naudio-clips\naudio-recorder\nprojects\nlocation\ngeo\nupgrade\npermissions\nsettings\nmaps\nerrors\ncrash-handler\nentities\nqr-code\nshadows\nmetadata\nselfie-camera\ndraw\nprinter\nlists\nweb-page\nopen-rosa\nmobile-device-management\nmaterial\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*.{kt,kts}]\nktlint_standard_no-blank-lines-in-chained-method-calls = disabled\nktlint_standard_trailing-comma-on-call-site = disabled\nktlint_standard_trailing-comma-on-declaration-site = disabled\nktlint_standard_function-signature = disabled\nktlint_standard_no-empty-first-line-in-class-body = disabled\nktlint_standard_argument-list-wrapping = disabled\nktlint_standard_parameter-list-wrapping = disabled\nktlint_standard_multiline-expression-wrapping = disabled\nktlint_standard_max-line-length = disabled\nktlint_standard_string-template-indent = disabled\nktlint_standard_annotation = disabled\nktlint_standard_value-parameter-comment = disabled\nktlint_standard_property-naming = disabled\nktlint_standard_value-argument-comment = disabled\nktlint_standard_blank-line-before-declaration = disabled\nktlint_standard_no-consecutive-comments = disabled\nktlint_standard_enum-wrapping = disabled\nktlint_standard_statement-wrapping = disabled\nktlint_standard_try-catch-finally-spacing = disabled\nktlint_standard_wrapping = disabled\nktlint_standard_chain-method-continuation = disabled\nktlint_standard_function-expression-body = disabled\nktlint_standard_class-signature = disabled\nktlint_standard_binary-expression-wrapping = disabled\nktlint_standard_condition-wrapping = disabled\nktlint_standard_function-literal = disabled\nktlint_standard_backing-property-naming = disabled\nktlint_function_naming_ignore_when_annotated_with = Composable\nktlint_standard_no-unused-imports=enabled\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=lf\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "Please refer to the project-wide [ODK Code of Conduct](https://github.com/getodk/governance/blob/master/CODE-OF-CONDUCT.md)."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links: \n  - name: \"Report an issue\"\n    about: \"For when Collect is behaving in an unexpected way\"\n    url: \"https://forum.getodk.org/c/support/6\"\n  - name: \"Request a feature\"\n    about: \"For when Collect is missing functionality\"\n    url: \"https://forum.getodk.org/c/features/9\"\n  - name: \"Everything else\"\n    about: \"For everything else\"\n    url: \"https://forum.getodk.org/c/support/6\""
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!-- \n\nThank you for taking the time to report an ODK Collect issue!\n\nThis space is for bugs that have clear reproduction steps and expected behavior. For unexpected behavior that is unclear how to address, general usage questions, form design questions, and to ask about the source code, please visit the ODK forum: https://forum.getodk.org\n\nBefore filling the template below, visit https://github.com/getodk/collect/issues?q=is%3Aissue and search to see whether your issue was already reported or fixed. If you find a match, comment on it or add a +1 rather than posting a new issue. If you find a problem you know how to fix, submit a pull request. 🎉\n\nFeature 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.\n\n-->\n\n#### ODK Collect version\n\n#### Android version\n\n#### Device used\n\n#### Problem description\n\n#### Steps to reproduce the problem\n\n#### Expected behavior\n\n#### Other information \n\nThings you tried, stack traces, related issues, suggestions on how to fix it...\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "Closes #\n\n<!-- \nThank you for contributing to ODK Collect!\n\nBefore sending this PR, please read\nhttps://github.com/getodk/collect/blob/master/docs/CONTRIBUTING.md\n-->\n\n#### Why is this the best possible solution? Were any other approaches considered?\n\n#### 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?\n\n#### Do we need any specific form for testing your changes? If so, please attach one.\n\n#### 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.\n\n#### Before submitting this PR, please make sure you have:\n- [ ] added or modified tests for any new or changed behavior\n- [ ] run `./gradlew connectedAndroidTest` (or `./gradlew testLab`) and confirmed all checks still pass\n- [ ] added a comment above any new strings describing it for translators\n- [ ] added any new strings with date formatting to `DateFormatsTest`\n- [ ] 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).\n- [ ] 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)\n"
  },
  {
    "path": ".github/TESTING_RESULT_TEMPLATES.md",
    "content": "# Testing result templates\n\n## Tested with success!\n#### Verified on: [List of devices/os versions]\n#### Verified cases: [optional]\n\n***\n\n## Bug[s] has[have] been found! / Regression[s] has[have] been found!\n\n#### Verified on: [List of devices/os versions]\n\n#### Problem visible on: [only if it's related to some specific devices otherwise it’s the same as above]\n\n#### Steps to reproduce:\n\n#### Current behavior:\n\n#### Expected behavior:\n\n#### Other information: [optional]\n"
  },
  {
    "path": ".gitignore",
    "content": "build\n.gradle\n.idea\n.kotlin\nlocal.properties\n*.iml\n.DS_Store\n*.sublime-project\n\n# emacs backup files\n*.*~\n\n# gradle env props\nsecrets.properties\n\n# built application files\n*.apk\n*.ap_\n\n# files for the Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n\n# Android Studio files\n.externalNativeBuild/\n.navigation/\ncaptures/\n\n# Visual Studio Code files\n.project\n.settings/\ncollect_app/.classpath\ncollect_app/.project\ncollect_app/.settings/\n\n# Config for the official ODK Collect build\ncollect_app/src/odkCollectRelease/\n\n# Files generated during CI runs\n.circleci/collect_app_test_files.txt\n.circleci/collect_app_test_classes.txt\n.circleci/fork_test_classes.txt\n\n# Robolectric dependencies\nrobolectric-deps/\n**/robolectric-deps.properties\n\n.local-m2\napks\n"
  },
  {
    "path": ".hgtags",
    "content": "5836a17a0e6f28b4e5b7a7abc9152446f4806ee2 v1.0.0\n30d9314729db181177bb7209d1c16cd65a5095eb v1.1.0\n827cf67bd902b80c37dce8d4ee8d2d29437408a5 v1.1.1\n82798e410f5ab043721b3998e23861fd365a9801 v1.1.2\n78fc6b2df0575ec79b94c0196babacbafcb7f958 v1.1.3\nc6308d8be8cd99d060a06f3b10c87f5722776098 v1.1.4\nf85cab690aa86dc86c2d53f12def22f16d9c0c5b v1.1.7-beta1\n165ee1feda811cccefda77ae83896b7cb3209b6c v1.1.7-rc1\n0c06023f4bae172a79a5aad27ca8d66798f2109e v1.1.7-rc2\ncaeb88584771e1e569c87eed7cca90738c867189 v1.1.7\n96568099c2ec1408508178568fc5ca2503ec670b v1.2 RC1\n96568099c2ec1408508178568fc5ca2503ec670b v1.2 RC1\n15185704d55c220b02abcf5de6170a106f387f5b v1.2 RC1\n88476e4dc9845a0a276bb57f93911d8aacc68912 v1.2.0\n88476e4dc9845a0a276bb57f93911d8aacc68912 v1.2.0\nccb4a2556950af32ac153c26ca1f71faebbf8a9b v1.2.0\nccb4a2556950af32ac153c26ca1f71faebbf8a9b v1.2.0\n2a9b44755f0a5af2558b7e22ac9948377b6e9597 v1.2.0\n2a9b44755f0a5af2558b7e22ac9948377b6e9597 v1.2.0\na5faa6055d4bdbc5db786673c28d9872142120c7 v1.2.0\na5faa6055d4bdbc5db786673c28d9872142120c7 v1.2.0\n0000000000000000000000000000000000000000 v1.2.0\nb0ac9ab2b5bccea1b160e774b28a1f0c8af9a9e8 v1.2.0 rev 1012\n3e7860308f3d7520ff91b8fb27bc5123450cfaa4 v1.2.0 rev 1013\n3e7860308f3d7520ff91b8fb27bc5123450cfaa4 v1.2.0 rev 1013\nb7d1fccb1a29ae62185bef1a123e80dbcdbc171c v1.2.0 rev 1013\n29d29a54a208785e93d8ed2024e2222115ede489 v1.2.0 rev 1014\n29d29a54a208785e93d8ed2024e2222115ede489 v1.2.0 rev 1014\n0000000000000000000000000000000000000000 v1.2.0 rev 1014\nabbf6ad98f25f0f33216ed11125a3a9cee8f90bc v1.2.1 rev 1014\n6b017ddf90f21ab24a9b0f219657d48b1bdfc204 v1.2.1 rev 1015\nfbc9d90492a631209d8587d299d08b0d005c681a v1.2.1 rev 1016\nfd9ea977734c1bdbb37f40217becb4ec50d356d2 v1.2.1 rev 1017\nb25021b3a495e039ef4a2a932a95df705648dc3c v1.2.0 rev 1010 prerelease\n797ac8701d2c93b13c5cc585d39ff0a1a74d70d2 v1.2.0 rev 1011 prerelease\n6e3dcf4780ac5e8a62b8d92f4111638cbc9a53e9 v1.2.0 rev 1009 prerelease\ndd3c2816eb879b2b922d2e4c52bc95c8996a8f25 v1.2.0 rev 1008 RC2\n06063863a95919cd18dc36841febbf21ca0fa60e v1.2.0 rev 1008 RC1\n7f9a9e162ff5de00b4384de5a6bd7b0132ad1e9c v1.2.0 rev 1007 beta\na5a35a6eaf724a9c080a8ab85a2190f8fa6470fa v1.2.0 rev 1006 beta\neca30a5719a2d603c364d470bedfbd7c90c89e35 v1.2.0 rev 1005 beta\n820c7e389d341953d0158290f880c9226d144870 v1.2.0 rev 1004 alpha\n5630fb8134d1e8169517b5586b05cc4183370e56 v1.2.0 rev 1003 alpha\nf628ef1bc45293e3d5fea77ac881a7b7652af554 v1.2.0 rev 1001 alpha\n8c5544b1b0f4d469f35832c730d02a3bfbdb21ea v1.2.0 rev 1000 alpha\n7f9a9e162ff5de00b4384de5a6bd7b0132ad1e9c v1.2.0 rev 1007 RC1\n7f9a9e162ff5de00b4384de5a6bd7b0132ad1e9c v1.2.0 rev 1007 beta\n0000000000000000000000000000000000000000 v1.2.0 rev 1007 beta\na5a35a6eaf724a9c080a8ab85a2190f8fa6470fa v1.2.0 rev 1006 RC1\na5a35a6eaf724a9c080a8ab85a2190f8fa6470fa v1.2.0 rev 1006 beta\n0000000000000000000000000000000000000000 v1.2.0 rev 1006 beta\neca30a5719a2d603c364d470bedfbd7c90c89e35 v1.2.0 rev 1005 RC1\neca30a5719a2d603c364d470bedfbd7c90c89e35 v1.2.0 rev 1005 beta\n0000000000000000000000000000000000000000 v1.2.0 rev 1005 beta\n820c7e389d341953d0158290f880c9226d144870 v1.2.0 rev 1004 RC1\n820c7e389d341953d0158290f880c9226d144870 v1.2.0 rev 1004 alpha\n0000000000000000000000000000000000000000 v1.2.0 rev 1004 alpha\n5630fb8134d1e8169517b5586b05cc4183370e56 v1.2.0 rev 1003 RC1\n5630fb8134d1e8169517b5586b05cc4183370e56 v1.2.0 rev 1003 alpha\n0000000000000000000000000000000000000000 v1.2.0 rev 1003 alpha\nf628ef1bc45293e3d5fea77ac881a7b7652af554 v1.2.0 rev 1001 RC1\nf628ef1bc45293e3d5fea77ac881a7b7652af554 v1.2.0 rev 1001 alpha\n0000000000000000000000000000000000000000 v1.2.0 rev 1001 alpha\n8c5544b1b0f4d469f35832c730d02a3bfbdb21ea v1.2.0 rev 1000 RC1\n8c5544b1b0f4d469f35832c730d02a3bfbdb21ea v1.2.0 rev 1000 alpha\n0000000000000000000000000000000000000000 v1.2.0 rev 1000 alpha\n7be1f7e0a0dc36b67f8a584c7f420c240eefd5c6 v1.2.1 rev 1018 (plus swahili)\nc11fe1aebaca2e213f71e61ad2337791d02723e0 v1.2.1 rev 1019\n64e490522c2ef5ebc9af8a1a2c0dbf78bb213c06 v1.2.1 rev 1020\nea230f4ee12c303640c30090c30a9ac386324d5a v1.2.2 rev 1021\nea230f4ee12c303640c30090c30a9ac386324d5a v1.2.2 rev 1021\n3b8a3847d1bea472176a27548d602b119d0e695d v1.2.2 rev 1021\n3b8a3847d1bea472176a27548d602b119d0e695d v1.2.2 rev 1021\nf714913194222fb579c5ef6e59092c788eeea300 v1.2.2 rev 1021\n1759bdc402730ed7a16d26e7aba44aa43f822355 v1.2.2 rev 1023\ncef8219f74248bd4d295e5c105b9f0ced3ac7f8d v1.3 rev 1025\na3d74ab96cdee3e972f7e507776a3834f497fbe0 v1.3 rev 1027\n5c2ce96c94c6daf5277befaabf54ab74f088437b v1.3 rev 1029\n1c8b890c69fbccc47de596c8592d21c7439646ba v1.3 rev 1030\ne1e6c70eef9913497c65de897e229a6e0d871ad8 v1.4 rev 1033\n68616e24e60d963334c762daba5784c7af763d76 v1.4 rev 1034\naa63d3a1f3295cf5a4a6bc74d8f1a4d4939fdd8b v1.4 rev 1035\ndb03f3497da8c76883e475a14a2b034e61e29c37 v1.4 rev 1036\ndb03f3497da8c76883e475a14a2b034e61e29c37 v1.4 rev 1036\n8f77433258b376de5a6a7267df03ecc7cdecc1a4 v1.4 rev 1036\n86afc4615411eaeee61271790b2bae5de7b94176 v1.4 rev 1037\n3254b74a70d9c4f4088ba710fd6d7f542ffe166f v1.4 rev 1038\nbc69898dc4ccb061079ee82f9b52da34de8400b7 v1.4.3 rev 1040\ncd78ad21a3c8b1f106cdbb35a55ddfd1c2cf021f v1.4.3 rev 1041\n7bf86c85d2dac5d6353bf2007a5c817654bbfdfc v1.4.3 rev 1042\n753b33ca1fa800b517fcb6156d55370b986cedeb v1.4.4 rev 1044\n753b33ca1fa800b517fcb6156d55370b986cedeb v1.4.4 rev 1044\nc0d79cc86e2da93dfb03165a520de2c42b5cf061 v1.4.4 rev 1044\n72d2e66877f36e0d3b607a6dbc4207c96dcc4c0c v1.4.4 rev 1045\n712f8174b16bd95ece6bac433f871a19d59ea46c v1.4.4 rev 1046\n5a92ad65ed775aa744b0abfe1aaa3ef756bd82f3 v1.4.5 rev 1048\n5a92ad65ed775aa744b0abfe1aaa3ef756bd82f3 v1.4.5 rev 1048\n1a6028ae2ce23b2eb50bd7ee3b2674effd2620af v1.4.5 rev 1048\naacc424db2a79c201abd6dcb481af830d017477f v1.4.6 rev 1049 for testing\naacc424db2a79c201abd6dcb481af830d017477f v1.4.6 rev 1049 for testing\n1fced08f31e9e4f10fcdf067debdbad3557899de v1.4.6 rev 1049 for testing\n"
  },
  {
    "path": "LICENSE.md",
    "content": "Apache License\n==============\n\n_Version 2.0, January 2004_  \n_&lt;<http://www.apache.org/licenses/>&gt;_\n\n### Terms and Conditions for use, reproduction, and distribution\n\n#### 1. Definitions\n\n“License” shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n“Licensor” shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n“Legal Entity” shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, “control” means **(i)** the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the\noutstanding shares, or **(iii)** beneficial ownership of such entity.\n\n“You” (or “Your”) shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n“Source” form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n“Object” form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n“Work” shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n“Derivative Works” shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n“Contribution” shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n“submitted” means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as “Not a Contribution.”\n\n“Contributor” shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n#### 2. Grant of Copyright License\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n#### 3. Grant of Patent License\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n#### 4. Redistribution\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\n* **(a)** You must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\n* **(b)** You must cause any modified files to carry prominent notices stating that You\nchanged the files; and\n* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\n* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\n\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n#### 5. Submission of Contributions\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n#### 6. Trademarks\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n#### 7. Disclaimer of Warranty\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an “AS IS” BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n#### 8. Limitation of Liability\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n#### 9. Accepting Warranty or Additional Liability\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\n_END OF TERMS AND CONDITIONS_\n\n### APPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets `[]` replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same “printed page” as the copyright notice for easier identification within\nthird-party archives.\n\n    Copyright [yyyy] [name of copyright owner]\n    \n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n    \n      http://www.apache.org/licenses/LICENSE-2.0\n    \n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# ODK Collect\n\n![Platform](https://img.shields.io/badge/platform-Android-blue.svg)\n[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Build status](https://circleci.com/gh/getodk/collect.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/getodk/collect)\n[![Slack](https://img.shields.io/badge/chat-on%20slack-brightgreen)](https://slack.getodk.org)\n\nODK 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).\n\nODK 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.\n\nPlease note that the `master` branch reflects ongoing development and is not production-ready.\n\n## Table of Contents\n* [Learn more about ODK Collect](#learn-more-about-odk-collect)\n* [Release cycle](#release-cycle)\n* [Downloading builds](#downloading-builds)\n* [Suggesting new features](#suggesting-new-features)\n* Contributing\n  * [Contributing code](#contributing-code)\n  * [Contributing translations](#contributing-translations)\n* Developing\n  * [Setting up your development environment](#setting-up-your-development-environment)\n  * [Testing a form without a server](#testing-a-form-without-a-server)\n  * [Using APIs for local development](#using-apis-for-local-development)\n  * [Debugging JavaRosa](#debugging-javarosa)\n  * [Troubleshooting](#troubleshooting)\n  * [Test devices](#test-devices)\n* [Creating signed releases for Google Play Store](#creating-signed-releases-for-google-play-store)\n\n## Learn more about ODK Collect\n* ODK website: [https://getodk.org](https://getodk.org)\n* ODK Collect usage documentation: [https://docs.getodk.org/collect-intro/](https://docs.getodk.org/collect-intro/)\n* ODK forum: [https://forum.getodk.org](https://forum.getodk.org)\n* ODK developer Slack chat: [https://slack.getodk.org](https://slack.getodk.org)\n\n## Release cycle\n\nThe 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.\n\nSometimes 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.\n\nOnce 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).\n\nFixes 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.\n\nAt 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.\n\n## Downloading builds\nPer-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.\n\nIf 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).\n\n## Suggesting new features\nWe 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).\n\n## Contributing code\nAny 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!\n\nIssues 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.\n\nIf you're ready to contribute code, see [the contribution guide](docs/CONTRIBUTING.md).\n\n## Contributing translations\nIf you know a language other than English, consider contributing translations through [Transifex](https://explore.transifex.com/getodk/collect/).\n\nTranslations 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.\n\n## Setting up your development environment\n\n1. Download and install [Git](https://git-scm.com/downloads) and add it to your PATH\n\n1. Download and install [Android Studio](https://developer.android.com/studio/index.html) \n\n1. Fork the collect project ([why and how to fork](https://help.github.com/articles/fork-a-repo/))\n\n1. Clone your fork of the project locally. At the command line:\n\n        git clone https://github.com/YOUR-GITHUB-USERNAME/collect\n\n    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`.\n\n1. 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.\n\n1. Windows developers: continue configuring Android Studio with the steps in this document: [Developing ODK Collect on Windows](docs/WINDOWS-DEV-SETUP.md).\n\n1. Make sure you can run unit tests by running everything under `collect_app/src/test/java` in Android Studio or on the command line:\n\n    ```\n    ./gradlew testDebug\n    ```\n\n1. Make sure you can run instrumented tests by running everything under `collect_app/src/androidTest/java` in Android Studio or on the command line:\n\n    ```\n    ./gradlew connectedAndroidTest\n    ```\n    **Note:** You can see the emulator setup used on CI in  `.circleci/config.yml`.\n\n## Customizing the development environment\n\n### Changing JVM heap size\n\nYou 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:\n\n```\norg.gradle.jvmargs=-Xmx4096m\n```\n\n## Testing a form without a server\nWhen 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:\n\n1. 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).\n\n1. 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).\n\n1. 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.\n\t```\n\tadb push my_form.xml /sdcard/Android/data/org.odk.collect.android/files/projects/{project-id}/forms\n\t```\n\nIf you are using the demo project, kindly replace `{project_id}` with `DEMO`\n\n4. Launch ODK Collect and tap `+ Start new form`. The new form will be there.\n\nMore information about using Android Debug Bridge with Collect can be found [here](https://docs.getodk.org/collect-adb/).\n\n## Using APIs for local development\n\nCertain 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.\n\n**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:\n  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.\n  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:\n    ```\n    GOOGLE_MAPS_API_KEY=AIbzvW8e0ub...\n    ```\n\n**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:\n  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.\n  2. Find your access token on your [account page](https://account.mapbox.com/) - it should be in \"Tokens\" as \"Default public token\".\n  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:\n    ```\n    MAPBOX_ACCESS_TOKEN=pk.eyJk3bumVp4i...\n    ```\n  4. Create a new secret token with the \"DOWNLOADS:READ\" secret scope and then add it to `secrets.properties` as `MAPBOX_DOWNLOADS_TOKEN`.\n\n*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`.*\n\n## Debugging JavaRosa\n\nJavaRosa 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):\n\n1. Build and install your changes of JavaRosa (into your local Maven repo):\n\n```bash\n./gradlew publishToMavenLocal\n```\n\n1. Change `const val javarosa = javarosa_online` in `Dependencies.kt` to `const val javarosa = javarosa_local`\n\n## Troubleshooting\n\n#### Error when running Robolectric tests from Android Studio on macOS: `build/intermediates/bundles/debug/AndroidManifest.xml (No such file or directory)`\n> 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$.\n\n> Source: [Robolectric Wiki](https://github.com/robolectric/robolectric/wiki/Running-tests-in-Android-Studio#notes-for-mac).\n\n#### Android Studio Error: `SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable.`\nWhen cloning the project from Android Studio, click \"No\" when prompted to open the `build.gradle` file and then open project.\n\n#### Execution failed for task ':collect_app:transformClassesWithInstantRunForDebug'.\n\nWe 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.\n\n#### Moving to the main view if user minimizes the app\nIf 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).\n\nThis problem occurs building other apps as well.\n\n#### gradlew Failure: `FAILURE: Build failed with an exception.`\n\nIf you encounter an error similar to this when running `gradlew`:\n\n```\nFAILURE: Build failed with an exception\n\nWhat went wrong:\nA problem occurred configuring project ':collect_app'.\n> Failed to notify project evaluation listener.\n   > Could not initialize class com.android.sdklib.repository.AndroidSdkHandler\n```\n\nYou 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:\n\n`export JAVA_HOME=\"/Applications/Android\\ Studio.app/Contents/jre/Contents/Home/\"\n`\n\nNote that this change might cause problems with other Java-based applications (e.g., if you uninstall Android Studio).\n\n#### gradlew Failure: `java.lang.NullPointerException (no error message).`\nIf you encounter the `java.lang.NullPointerException (no error message).` when running `gradlew`, please make sure your Java version for this project is Java 17.\n\nThis 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.\n\n#### `Unable to resolve artifact: Missing` while running tests\n\nThis 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:\n\n```\n./download-robolectric-deps.sh\n```\n\n## Test devices\n\nDevices that @getodk/testers have available for testing are as follows:\n\n* Xiaomi Redmi 9T 4GB - Android 10\n* Pixel 7a 8GB - Android 14\n* LG Nexus 5X 2GB - Android 8.1\n* Samsung Galaxy M12 4GB - Android 11\n* Samsung Galaxy M23 4GB - Android 14\n* Xiaomi Redmi 7 3GB - Android 10\n* Pixel 6a 6GB - Android 13\n* Pixel 3a 4GB - Android 12\n* Huawei Y560-L01 1GB - Android 5.1\n\n## Creating signed releases for Google Play Store\nMaintainers 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.\n\n### Release prerequisites:\n\n- a`local.properties` file in the root folder with the following:\n  ```\n  sdk.dir=/path/to/android/sdk\n  ```\n\n- the keystore file and passwords\n\n- a `secrets.properties` file in the root project folder folder with the following:\n  ```\n  // secrets.properties\n  RELEASE_STORE_FILE=/path/to/collect.keystore\n  RELEASE_STORE_PASSWORD=secure-store-password\n  RELEASE_KEY_ALIAS=key-alias\n  RELEASE_KEY_PASSWORD=secure-alias-password\n  ```\n\n- 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`.\n\n### Release checklist:\n\n- update translations\n- make sure CI is green for the chosen commit\n- run `./gradlew releaseCheck`. If successful, a signed release will be at `collect_app/build/outputs/apk` (with an old version name)\n- 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\n- run `./benchmark.sh` with a real device connected to verify performance\n  - 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).\n- verify new APK can be installed as update to previous version and that above \"happy path\" works in that case also\n- create and publish scheduled forum post with release description\n- write Play Store release notes, include link to forum post\n- when creating a major release:\n  - Tag the commit for the release (`vX.X.0`)\n  - Run `./create-release.sh <last release version code> <release tag>`\n- when creating a patch release:\n  - Tag the commit for the patch release (`vX.X.X`)\n  - (If beta has started for next release) Tag the commit for the beta release (`vX.X.X-beta.X`)\n  - Run `./create-release.sh <last release version code> <patch release tag> <beta release tag>`\n- when creating a beta release:\n  - Tag the commit for the beta release (`vX.X.X-beta.X`)\n  - Run `./create-release.sh <last release version code> <beta release tag>`\n- add a release to Github [here](https://github.com/getodk/collect/releases), generate release notes and attach the APK\n- upload APK(s) to Play Store\n  - When creating a hotfix, the beta APK should be uploaded second as it will have a higher version code\n- 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)\n- 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)\n\n## Compiling a previous release using backed-up dependencies\n\n1. Download the `.tar` for relevant release tag\n2. Extract `.local-m2` into the project directory:\n    ```bash\n    tar -xf maven.tar -C <collect project directory>\n    ```\n   \nThe 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.\n\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nSee our [Vulnerability Disclosure Policy](https://getodk.org/vdp).\n"
  },
  {
    "path": "analytics/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "analytics/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\")\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    namespace = \"org.odk.collect.analytics\"\n}\n\ndependencies {\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.firebaseCrashlytics)\n    implementation(libs.firebaseAnalytics)\n}\n"
  },
  {
    "path": "analytics/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "analytics/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n</manifest>\n"
  },
  {
    "path": "analytics/src/main/java/org/odk/collect/analytics/Analytics.kt",
    "content": "package org.odk.collect.analytics\n\ninterface Analytics {\n    fun logEvent(event: String)\n    fun logEventWithParam(event: String, key: String, value: String)\n    fun logNonFatal(throwable: Throwable)\n    fun logMessage(message: String)\n    fun setAnalyticsCollectionEnabled(isAnalyticsEnabled: Boolean)\n    fun setUserProperty(name: String, value: String)\n\n    companion object {\n        private var instance: Analytics = NoopAnalytics()\n        private val params = mutableMapOf<String, String>()\n\n        fun setInstance(analytics: Analytics) {\n            this.instance = analytics\n        }\n\n        @JvmStatic\n        fun log(event: String) {\n            instance.logEvent(event)\n        }\n\n        @JvmStatic\n        fun log(event: String, key: String) {\n            val paramValue = params[key]\n\n            if (paramValue != null) {\n                log(event, key, paramValue)\n            } else {\n                log(event)\n            }\n        }\n\n        @JvmStatic\n        fun log(event: String, key: String, value: String) {\n            instance.logEventWithParam(event, key, value)\n        }\n\n        @JvmStatic\n        fun setParam(key: String, value: String) {\n            params[key] = value\n        }\n\n        @JvmStatic\n        fun getParamValue(key: String): String? {\n            return params[key]\n        }\n\n        fun setUserProperty(name: String, value: String) {\n            instance.setUserProperty(name, value)\n        }\n\n        fun logNonFatal(throwable: Throwable) {\n            instance.logNonFatal(throwable)\n        }\n    }\n}\n"
  },
  {
    "path": "analytics/src/main/java/org/odk/collect/analytics/BlockableFirebaseAnalytics.kt",
    "content": "package org.odk.collect.analytics\n\nimport android.app.Application\nimport android.os.Bundle\nimport com.google.firebase.analytics.FirebaseAnalytics\nimport com.google.firebase.crashlytics.FirebaseCrashlytics\n\nclass BlockableFirebaseAnalytics(application: Application) : Analytics {\n    private val firebaseAnalytics = FirebaseAnalytics.getInstance(application)\n    private val crashlytics = FirebaseCrashlytics.getInstance()\n\n    override fun logEvent(event: String) {\n        firebaseAnalytics.logEvent(event, null)\n    }\n\n    override fun logEventWithParam(event: String, key: String, value: String) {\n        val bundle = Bundle()\n        bundle.putString(key, value)\n        firebaseAnalytics.logEvent(event, bundle)\n    }\n\n    override fun logNonFatal(throwable: Throwable) {\n        crashlytics.recordException(throwable)\n    }\n\n    override fun logMessage(message: String) {\n        crashlytics.log(message)\n    }\n\n    override fun setAnalyticsCollectionEnabled(isAnalyticsEnabled: Boolean) {\n        firebaseAnalytics.setAnalyticsCollectionEnabled(isAnalyticsEnabled)\n        crashlytics.setCrashlyticsCollectionEnabled(isAnalyticsEnabled)\n    }\n\n    override fun setUserProperty(name: String, value: String) {\n        firebaseAnalytics.setUserProperty(name, value)\n    }\n}\n"
  },
  {
    "path": "analytics/src/main/java/org/odk/collect/analytics/NoopAnalytics.kt",
    "content": "package org.odk.collect.analytics\n\nclass NoopAnalytics : Analytics {\n    override fun logEvent(event: String) {}\n    override fun logEventWithParam(event: String, key: String, value: String) {}\n    override fun logNonFatal(throwable: Throwable) {}\n    override fun logMessage(message: String) {}\n    override fun setAnalyticsCollectionEnabled(isAnalyticsEnabled: Boolean) {}\n    override fun setUserProperty(name: String, value: String) {}\n}\n"
  },
  {
    "path": "androidshared/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "androidshared/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n    alias(libs.plugins.composeCompiler)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    buildFeatures {\n        viewBinding = true\n        compose = true\n    }\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests.isIncludeAndroidResources = true\n    }\n    namespace = \"org.odk.collect.androidshared\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":icons\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":shared\"))\n    implementation(project(\":async\"))\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.androidxLifecycleLivedataKtx)\n    implementation(libs.androidMaterial)\n    implementation(libs.androidxFragmentKtx)\n    implementation(libs.androidxPreferenceKtx)\n    implementation(libs.timber)\n    implementation(libs.androidxExinterface)\n    implementation(libs.playServicesLocation)\n\n    val composeBom = platform(libs.androidxComposeBom)\n    implementation(composeBom)\n    implementation(libs.androidXComposeMaterial)\n\n    testImplementation(project(\":test-shared\"))\n    testImplementation(project(\":androidtest\"))\n    testImplementation(libs.junit)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.androidxTestEspressoCore)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.androidxArchCoreTesting)\n\n    androidTestImplementation(libs.androidxTestExtJunit)\n    androidTestImplementation(libs.junit)\n\n    debugImplementation(project(\":fragments-test\"))\n}\n"
  },
  {
    "path": "androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap/ImageCompressorTest.kt",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.androidshared.bitmap\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport androidx.exifinterface.media.ExifInterface\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass ImageCompressorTest {\n    private lateinit var testImagePath: String\n    private val imageCompressor = ImageCompressor\n\n    @Test\n    fun imageShouldNotBeChangedIfMaxPixelsIsZero() {\n        saveTestBitmap(3000, 2000)\n        imageCompressor.execute(testImagePath, 0)\n\n        val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!\n\n        assertThat(3000, equalTo(image.width))\n        assertThat(2000, equalTo(image.height))\n    }\n\n    @Test\n    fun imageShouldNotBeChangedIfMaxPixelsIsSmallerThanZero() {\n        saveTestBitmap(3000, 2000)\n        imageCompressor.execute(testImagePath, -10)\n\n        val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!\n\n        assertThat(3000, equalTo(image.width))\n        assertThat(2000, equalTo(image.height))\n    }\n\n    @Test\n    fun imageShouldNotBeChangedIfMaxPixelsIsNotSmallerThanTheEdgeWhenWidthIsBiggerThanHeight() {\n        saveTestBitmap(3000, 2000)\n        imageCompressor.execute(testImagePath, 3000)\n\n        val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!\n\n        assertThat(3000, equalTo(image.width))\n        assertThat(2000, equalTo(image.height))\n    }\n\n    @Test\n    fun imageShouldNotBeChangedIfMaxPixelsIsNotSmallerThanTheLongEdgeWhenWidthIsSmallerThanHeight() {\n        saveTestBitmap(2000, 3000)\n        imageCompressor.execute(testImagePath, 4000)\n\n        val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!\n\n        assertThat(2000, equalTo(image.width))\n        assertThat(3000, equalTo(image.height))\n    }\n\n    @Test\n    fun imageShouldNotBeChangedIfMaxPixelsIsNotSmallerThanTheLongEdgeWhenWidthEqualsHeight() {\n        saveTestBitmap(3000, 3000)\n        imageCompressor.execute(testImagePath, 3000)\n\n        val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!\n\n        assertThat(3000, equalTo(image.width))\n        assertThat(3000, equalTo(image.height))\n    }\n\n    @Test\n    fun imageShouldBeCompressedIfMaxPixelsIsSmallerThanTheLongEdgeWhenWidthIsBiggerThanHeight() {\n        saveTestBitmap(4000, 3000)\n        imageCompressor.execute(testImagePath, 2000)\n\n        val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!\n\n        assertThat(2000, equalTo(image.width))\n        assertThat(1500, equalTo(image.height))\n    }\n\n    @Test\n    fun imageShouldBeCompressedIfMaxPixelsIsSmallerThanTheLongEdgeWhenWidthIsSmallerThanHeight() {\n        saveTestBitmap(3000, 4000)\n        imageCompressor.execute(testImagePath, 2000)\n\n        val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!\n\n        assertThat(1500, equalTo(image.width))\n        assertThat(2000, equalTo(image.height))\n    }\n\n    @Test\n    fun imageShouldBeCompressedIfMaxPixelsIsSmallerThanTheLongEdgeWhenWidthEqualsHeight() {\n        saveTestBitmap(3000, 3000)\n        imageCompressor.execute(testImagePath, 2000)\n\n        val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!\n\n        assertThat(2000, equalTo(image.width))\n        assertThat(2000, equalTo(image.height))\n    }\n\n    @Test\n    fun keepExifAfterScaling() {\n        val attributes = mutableMapOf(\n            // supported exif tags\n            ExifInterface.TAG_DATETIME to \"2014:01:23 14:57:18\",\n            ExifInterface.TAG_DATETIME_ORIGINAL to \"2014:01:23 14:57:18\",\n            ExifInterface.TAG_DATETIME_DIGITIZED to \"2014:01:23 14:57:18\",\n            ExifInterface.TAG_OFFSET_TIME to \"+1:00\",\n            ExifInterface.TAG_OFFSET_TIME_ORIGINAL to \"+1:00\",\n            ExifInterface.TAG_OFFSET_TIME_DIGITIZED to \"+1:00\",\n            ExifInterface.TAG_SUBSEC_TIME to \"First photo\",\n            ExifInterface.TAG_SUBSEC_TIME_ORIGINAL to \"0\",\n            ExifInterface.TAG_SUBSEC_TIME_DIGITIZED to \"0\",\n            ExifInterface.TAG_IMAGE_DESCRIPTION to \"Photo from Poland\",\n            ExifInterface.TAG_MAKE to \"OLYMPUS IMAGING CORP\",\n            ExifInterface.TAG_MODEL to \"STYLUS1\",\n            ExifInterface.TAG_SOFTWARE to \"Version 1.0\",\n            ExifInterface.TAG_ARTIST to \"Grzegorz\",\n            ExifInterface.TAG_COPYRIGHT to \"G\",\n            ExifInterface.TAG_MAKER_NOTE to \"OLYMPUS\",\n            ExifInterface.TAG_USER_COMMENT to \"First photo\",\n            ExifInterface.TAG_IMAGE_UNIQUE_ID to \"123456789\",\n            ExifInterface.TAG_CAMERA_OWNER_NAME to \"John\",\n            ExifInterface.TAG_BODY_SERIAL_NUMBER to \"987654321\",\n            ExifInterface.TAG_GPS_ALTITUDE to \"41/1\",\n            ExifInterface.TAG_GPS_ALTITUDE_REF to \"0\",\n            ExifInterface.TAG_GPS_DATESTAMP to \"2014:01:23\",\n            ExifInterface.TAG_GPS_TIMESTAMP to \"14:57:18\",\n            ExifInterface.TAG_GPS_LATITUDE to \"50/1,49/1,8592/1000\",\n            ExifInterface.TAG_GPS_LATITUDE_REF to \"N\",\n            ExifInterface.TAG_GPS_LONGITUDE to \"0/1,8/1,12450/1000\",\n            ExifInterface.TAG_GPS_LONGITUDE_REF to \"W\",\n            ExifInterface.TAG_GPS_SATELLITES to \"8\",\n            ExifInterface.TAG_GPS_STATUS to \"A\",\n            ExifInterface.TAG_ORIENTATION to \"1\",\n            // unsupported exif tags\n            ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH to \"5\",\n            ExifInterface.TAG_DNG_VERSION to \"100\"\n        )\n\n        saveTestBitmap(3000, 4000, attributes)\n        imageCompressor.execute(testImagePath, 2000)\n\n        val exifData = ExifInterface(testImagePath)\n        for (attributeName in attributes.keys) {\n            if (attributeName == ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH ||\n                attributeName == ExifInterface.TAG_DNG_VERSION\n            ) {\n                assertThat(exifData.getAttribute(attributeName), equalTo(null))\n            } else {\n                assertThat(exifData.getAttribute(attributeName), equalTo(attributes[attributeName]))\n            }\n        }\n    }\n\n    @Test\n    fun verifyNoRotationAppliedForExifRotation() {\n        val attributes = mapOf(ExifInterface.TAG_ORIENTATION to ExifInterface.ORIENTATION_ROTATE_90.toString())\n        saveTestBitmap(3000, 4000, attributes)\n        imageCompressor.execute(testImagePath, 4000)\n\n        val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!!\n\n        assertThat(3000, equalTo(image.width))\n        assertThat(4000, equalTo(image.height))\n    }\n\n    private fun saveTestBitmap(width: Int, height: Int, attributes: Map<String, String> = emptyMap()) {\n        testImagePath = File.createTempFile(\"test\", \".jpg\").absolutePath\n\n        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)\n        ImageFileUtils.saveBitmapToFile(bitmap, testImagePath)\n        val exifInterface = ExifInterface(testImagePath)\n        for ((key, value) in attributes) {\n            exifInterface.setAttribute(key, value)\n        }\n        exifInterface.saveAttributes()\n    }\n}\n"
  },
  {
    "path": "androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap/ImageFileUtilsTest.kt",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.androidshared.bitmap\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.Color\nimport androidx.exifinterface.media.ExifInterface\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Assert.assertEquals\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport timber.log.Timber\nimport java.io.File\nimport java.io.IOException\n\n@RunWith(AndroidJUnit4::class)\nclass ImageFileUtilsTest {\n\n    private lateinit var sourceFile: File\n    private lateinit var destinationFile: File\n    private lateinit var attributes: MutableMap<String, String>\n\n    @Before\n    fun setup() {\n        sourceFile = createTempImageFile(\"source\")\n        destinationFile = createTempImageFile(\"destination\")\n        attributes = HashMap()\n    }\n\n    @Test\n    fun copyAndRotateImageNinety() {\n        attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_ROTATE_90.toString()\n        saveTestBitmapToFile(sourceFile.absolutePath, attributes)\n        ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile)\n        val image = ImageFileUtils.getBitmap(\n            destinationFile.absolutePath,\n            BitmapFactory.Options()\n        )!!\n\n        assertEquals(2, image.width)\n        assertEquals(1, image.height)\n        assertEquals(Color.GREEN, image.getPixel(0, 0))\n        assertEquals(Color.RED, image.getPixel(1, 0))\n        verifyNoExifOrientationInDestinationFile(destinationFile.absolutePath)\n    }\n\n    @Test\n    fun copyAndRotateImageTwoSeventy() {\n        attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_ROTATE_270.toString()\n        saveTestBitmapToFile(sourceFile.absolutePath, attributes)\n        ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile)\n        val image = ImageFileUtils.getBitmap(\n            destinationFile.absolutePath,\n            BitmapFactory.Options()\n        )!!\n\n        assertEquals(2, image.width)\n        assertEquals(1, image.height)\n        assertEquals(Color.RED, image.getPixel(0, 0))\n        assertEquals(Color.GREEN, image.getPixel(1, 0))\n        verifyNoExifOrientationInDestinationFile(destinationFile.absolutePath)\n    }\n\n    @Test\n    fun copyAndRotateImageOneEighty() {\n        attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_ROTATE_180.toString()\n        saveTestBitmapToFile(sourceFile.absolutePath, attributes)\n        ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile)\n        val image = ImageFileUtils.getBitmap(\n            destinationFile.absolutePath,\n            BitmapFactory.Options()\n        )!!\n\n        assertEquals(1, image.width)\n        assertEquals(2, image.height)\n        assertEquals(Color.GREEN, image.getPixel(0, 0))\n        assertEquals(Color.RED, image.getPixel(0, 1))\n        verifyNoExifOrientationInDestinationFile(destinationFile.absolutePath)\n    }\n\n    @Test\n    fun copyAndRotateImageUndefined() {\n        attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_UNDEFINED.toString()\n        saveTestBitmapToFile(sourceFile.absolutePath, attributes)\n        ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile)\n        val image = ImageFileUtils.getBitmap(\n            destinationFile.absolutePath,\n            BitmapFactory.Options()\n        )!!\n\n        assertEquals(1, image.width)\n        assertEquals(2, image.height)\n        assertEquals(Color.RED, image.getPixel(0, 0))\n        assertEquals(Color.GREEN, image.getPixel(0, 1))\n        verifyNoExifOrientationInDestinationFile(destinationFile.absolutePath)\n    }\n\n    @Test\n    fun copyAndRotateImageNoExif() {\n        saveTestBitmapToFile(sourceFile.absolutePath, null)\n        ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile)\n        val image = ImageFileUtils.getBitmap(\n            destinationFile.absolutePath,\n            BitmapFactory.Options()\n        )!!\n\n        assertEquals(1, image.width)\n        assertEquals(2, image.height)\n        assertEquals(Color.RED, image.getPixel(0, 0))\n        assertEquals(Color.GREEN, image.getPixel(0, 1))\n        verifyNoExifOrientationInDestinationFile(destinationFile.absolutePath)\n    }\n\n    /**\n     * These cases all have a window smaller than the image so the image should be scaled down.\n     * Note that the scaling isn't exact -- the factor is the closest power of 2 to the exact one.\n     */\n    @Test\n    fun scaleDownBitmapWhenNeeded() {\n        runScaleTest(1000, 1000, 500, 500, 500, 500, false)\n        runScaleTest(600, 800, 600, 200, 150, 200, false)\n        runScaleTest(500, 400, 250, 200, 250, 200, false)\n    }\n\n    @Test\n    fun doNotScaleDownBitmapWhenNotNeeded() {\n        runScaleTest(1000, 1000, 2000, 2000, 1000, 1000, false)\n        runScaleTest(600, 800, 600, 800, 600, 800, false)\n        runScaleTest(500, 400, 600, 600, 500, 400, false)\n        runScaleTest(2000, 800, 4000, 2000, 2000, 800, false)\n    }\n\n    @Test\n    fun accuratelyScaleBitmapToDisplay() {\n        runScaleTest(1000, 1000, 500, 500, 500, 500, true)\n        runScaleTest(600, 800, 600, 200, 150, 200, true)\n        runScaleTest(500, 400, 250, 200, 250, 200, true)\n        runScaleTest(2000, 800, 300, 400, 300, 120, true)\n        runScaleTest(1000, 1000, 2000, 2000, 2000, 2000, true)\n        runScaleTest(600, 800, 600, 800, 600, 800, true)\n        runScaleTest(500, 400, 600, 600, 600, 480, true)\n        runScaleTest(2000, 800, 4000, 2000, 4000, 1600, true)\n    }\n\n    private fun runScaleTest(\n        imageHeight: Int,\n        imageWidth: Int,\n        windowHeight: Int,\n        windowWidth: Int,\n        expectedHeight: Int,\n        expectedWidth: Int,\n        shouldScaleAccurately: Boolean\n    ) {\n        ScaleImageTest()\n            .createBitmap(imageHeight, imageWidth)\n            .scaleBitmapToDisplay(windowHeight, windowWidth, shouldScaleAccurately)\n            .assertScaledBitmapDimensions(expectedHeight, expectedWidth)\n    }\n\n    private fun verifyNoExifOrientationInDestinationFile(destinationFilePath: String) {\n        val exifData = getTestImageExif(destinationFilePath)\n        if (exifData != null) {\n            assertEquals(\n                ExifInterface.ORIENTATION_UNDEFINED,\n                exifData.getAttributeInt(\n                    ExifInterface.TAG_ORIENTATION,\n                    ExifInterface.ORIENTATION_UNDEFINED\n                )\n            )\n        }\n    }\n\n    private fun saveTestBitmapToFile(\n        filePath: String,\n        attributes: Map<String, String>?\n    ) {\n        val bitmap = Bitmap.createBitmap(1, 2, Bitmap.Config.ARGB_8888)\n        bitmap.setPixel(0, 0, Color.RED)\n        bitmap.setPixel(0, 1, Color.GREEN)\n\n        ImageFileUtils.saveBitmapToFile(bitmap, filePath)\n        if (attributes != null) {\n            try {\n                val exifInterface = ExifInterface(filePath)\n                for (attributeName in attributes.keys) {\n                    exifInterface.setAttribute(attributeName, attributes[attributeName])\n                }\n                exifInterface.saveAttributes()\n            } catch (e: IOException) {\n                Timber.w(e)\n            }\n        }\n    }\n\n    private fun getTestImageExif(imagePath: String): ExifInterface? {\n        try {\n            return ExifInterface(imagePath)\n        } catch (e: Exception) {\n            Timber.w(e)\n        }\n        return null\n    }\n\n    private fun createTempImageFile(imageName: String): File {\n        val temp = File.createTempFile(imageName, \".png\")\n        temp.deleteOnExit()\n        return temp\n    }\n\n    private class ScaleImageTest {\n        private val cache = ApplicationProvider.getApplicationContext<Context>().externalCacheDir\n        private val imageFile = File(cache, \"testImage.jpeg\")\n        private var scaledBitmap: Bitmap? = null\n\n        fun createBitmap(imageHeight: Int, imageWidth: Int): ScaleImageTest {\n            val bitmap = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888)\n            ImageFileUtils.saveBitmapToFile(bitmap, imageFile.absolutePath)\n            return this\n        }\n\n        fun scaleBitmapToDisplay(\n            windowHeight: Int,\n            windowWidth: Int,\n            shouldScaleAccurately: Boolean\n        ): ScaleImageTest {\n            scaledBitmap = ImageFileUtils.getBitmapScaledToDisplay(\n                imageFile,\n                windowHeight,\n                windowWidth,\n                shouldScaleAccurately\n            )\n            return this\n        }\n\n        fun assertScaledBitmapDimensions(expectedHeight: Int, expectedWidth: Int) {\n            assertEquals(expectedHeight.toLong(), scaledBitmap!!.height.toLong())\n            assertEquals(expectedWidth.toLong(), scaledBitmap!!.width.toLong())\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n\n    <application>\n        <activity android:name=\"org.odk.collect.androidshared.ui.ReturnToAppActivity\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/async/TrackableWorker.kt",
    "content": "package org.odk.collect.androidshared.async\n\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\nimport org.odk.collect.async.Scheduler\nimport java.util.concurrent.atomic.AtomicInteger\nimport java.util.function.Consumer\nimport java.util.function.Supplier\n\nclass TrackableWorker(private val scheduler: Scheduler) {\n\n    private val _isWorking = MutableNonNullLiveData(false)\n    val isWorking: NonNullLiveData<Boolean> = _isWorking\n\n    private var activeBackgroundJobsCounter = AtomicInteger(0)\n\n    fun <T> immediate(background: Supplier<T>, foreground: Consumer<T>) {\n        activeBackgroundJobsCounter.incrementAndGet()\n        _isWorking.value = true\n        scheduler.immediate(background) { result ->\n            if (activeBackgroundJobsCounter.decrementAndGet() == 0) {\n                _isWorking.value = false\n            }\n            foreground.accept(result)\n        }\n    }\n\n    fun immediate(background: Runnable) {\n        immediate(\n            background = {\n                background.run()\n            },\n            foreground = {}\n        )\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/bitmap/ImageCompressor.kt",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.androidshared.bitmap\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport androidx.exifinterface.media.ExifInterface\nimport timber.log.Timber\n\nobject ImageCompressor {\n    /**\n     * Before proceed with scaling or rotating, make sure existing exif information is stored/restored.\n     * @author Khuong Ninh (khuong.ninh@it-development.com)\n     */\n    fun execute(imagePath: String, maxPixels: Int) {\n        backupExifData(imagePath)\n        scaleDownImage(imagePath, maxPixels)\n        restoreExifData(imagePath)\n    }\n\n    /**\n     * This method is used to reduce an original picture size.\n     * maxPixels refers to the max pixels of the long edge, the short edge is scaled proportionately.\n     */\n    private fun scaleDownImage(imagePath: String, maxPixels: Int) {\n        if (maxPixels <= 0) {\n            return\n        }\n\n        var image = ImageFileUtils.getBitmap(imagePath, BitmapFactory.Options())\n        if (image != null) {\n            val originalWidth = image.width.toDouble()\n            val originalHeight = image.height.toDouble()\n            if (originalWidth > originalHeight && originalWidth > maxPixels) {\n                val newHeight = (originalHeight / (originalWidth / maxPixels)).toInt()\n                image = Bitmap.createScaledBitmap(image, maxPixels, newHeight, false)\n                ImageFileUtils.saveBitmapToFile(image, imagePath)\n            } else if (originalHeight > maxPixels) {\n                val newWidth = (originalWidth / (originalHeight / maxPixels)).toInt()\n                image = Bitmap.createScaledBitmap(image, newWidth, maxPixels, false)\n                ImageFileUtils.saveBitmapToFile(image, imagePath)\n            }\n        }\n    }\n\n    private fun backupExifData(imagePath: String) {\n        try {\n            val exif = ExifInterface(imagePath)\n            for ((key, _) in exifDataBackup) {\n                exifDataBackup[key] = exif.getAttribute(key)\n            }\n        } catch (e: Throwable) {\n            Timber.w(e)\n        }\n    }\n\n    private fun restoreExifData(imagePath: String) {\n        try {\n            val exif = ExifInterface(imagePath)\n            for ((key, value) in exifDataBackup) {\n                exif.setAttribute(key, value)\n            }\n            exif.saveAttributes()\n        } catch (e: Throwable) {\n            Timber.w(e)\n        }\n    }\n\n    private val exifDataBackup = mutableMapOf<String, String?>(\n        ExifInterface.TAG_DATETIME to null,\n        ExifInterface.TAG_DATETIME_ORIGINAL to null,\n        ExifInterface.TAG_DATETIME_DIGITIZED to null,\n        ExifInterface.TAG_OFFSET_TIME to null,\n        ExifInterface.TAG_OFFSET_TIME_ORIGINAL to null,\n        ExifInterface.TAG_OFFSET_TIME_DIGITIZED to null,\n        ExifInterface.TAG_SUBSEC_TIME to null,\n        ExifInterface.TAG_SUBSEC_TIME_ORIGINAL to null,\n        ExifInterface.TAG_SUBSEC_TIME_DIGITIZED to null,\n        ExifInterface.TAG_IMAGE_DESCRIPTION to null,\n        ExifInterface.TAG_MAKE to null,\n        ExifInterface.TAG_MODEL to null,\n        ExifInterface.TAG_SOFTWARE to null,\n        ExifInterface.TAG_ARTIST to null,\n        ExifInterface.TAG_COPYRIGHT to null,\n        ExifInterface.TAG_MAKER_NOTE to null,\n        ExifInterface.TAG_USER_COMMENT to null,\n        ExifInterface.TAG_IMAGE_UNIQUE_ID to null,\n        ExifInterface.TAG_CAMERA_OWNER_NAME to null,\n        ExifInterface.TAG_BODY_SERIAL_NUMBER to null,\n        ExifInterface.TAG_GPS_ALTITUDE to null,\n        ExifInterface.TAG_GPS_ALTITUDE_REF to null,\n        ExifInterface.TAG_GPS_DATESTAMP to null,\n        ExifInterface.TAG_GPS_TIMESTAMP to null,\n        ExifInterface.TAG_GPS_LATITUDE to null,\n        ExifInterface.TAG_GPS_LATITUDE_REF to null,\n        ExifInterface.TAG_GPS_LONGITUDE to null,\n        ExifInterface.TAG_GPS_LONGITUDE_REF to null,\n        ExifInterface.TAG_GPS_SATELLITES to null,\n        ExifInterface.TAG_GPS_STATUS to null,\n        ExifInterface.TAG_ORIENTATION to null\n    )\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/bitmap/ImageFileUtils.kt",
    "content": "package org.odk.collect.androidshared.bitmap\n\nimport android.graphics.Bitmap\nimport android.graphics.Bitmap.CompressFormat\nimport android.graphics.BitmapFactory\nimport android.graphics.Matrix\nimport androidx.exifinterface.media.ExifInterface\nimport timber.log.Timber\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.io.IOException\nimport java.lang.Exception\nimport java.util.Locale\nimport kotlin.math.ceil\n\nobject ImageFileUtils {\n    // 80% JPEG quality gives a greater file size reduction with almost no loss in quality\n    private const val IMAGE_COMPRESS_QUALITY = 80\n\n    private val EXIF_ORIENTATION_ROTATIONS = arrayOf(\n        ExifInterface.ORIENTATION_ROTATE_90,\n        ExifInterface.ORIENTATION_ROTATE_180,\n        ExifInterface.ORIENTATION_ROTATE_270\n    )\n\n    @JvmStatic\n    fun saveBitmapToFile(bitmap: Bitmap?, path: String) {\n        val compressFormat =\n            if (path.lowercase(Locale.getDefault()).endsWith(\".png\")) {\n                CompressFormat.PNG\n            } else {\n                CompressFormat.JPEG\n            }\n        try {\n            if (bitmap != null) {\n                FileOutputStream(path).use { out -> bitmap.compress(compressFormat, IMAGE_COMPRESS_QUALITY, out) }\n            }\n        } catch (e: Exception) {\n            Timber.e(e)\n        }\n    }\n\n    /*\n    This method is used to avoid OutOfMemoryError exception during loading an image.\n    If the exception occurs we catch it and try to load a smaller image.\n     */\n    @JvmStatic\n    fun getBitmap(path: String?, originalOptions: BitmapFactory.Options): Bitmap? {\n        val newOptions = BitmapFactory.Options()\n        newOptions.inSampleSize = originalOptions.inSampleSize\n        if (newOptions.inSampleSize <= 0) {\n            newOptions.inSampleSize = 1\n        }\n        val bitmap: Bitmap? = try {\n            BitmapFactory.decodeFile(path, originalOptions)\n        } catch (e: OutOfMemoryError) {\n            Timber.i(e)\n            newOptions.inSampleSize++\n            return getBitmap(path, newOptions)\n        }\n        return bitmap\n    }\n\n    @JvmStatic\n    fun getBitmapScaledToDisplay(file: File, screenHeight: Int, screenWidth: Int): Bitmap? {\n        return getBitmapScaledToDisplay(file, screenHeight, screenWidth, false)\n    }\n\n    /**\n     * Scales image according to the given display\n     *\n     * @param file           containing the image\n     * @param screenHeight   height of the display\n     * @param screenWidth    width of the display\n     * @param upscaleEnabled determines whether the image should be up-scaled or not\n     * if the window size is greater than the image size\n     * @return scaled bitmap\n     */\n    @JvmStatic\n    fun getBitmapScaledToDisplay(\n        file: File,\n        screenHeight: Int,\n        screenWidth: Int,\n        upscaleEnabled: Boolean\n    ): Bitmap? {\n        // Determine image size of file\n        var options = BitmapFactory.Options()\n        options.inJustDecodeBounds = true\n        getBitmap(file.absolutePath, options)\n        var bitmap: Bitmap?\n        val scale: Double\n        if (upscaleEnabled) {\n            // Load full size bitmap image\n            options = BitmapFactory.Options()\n            options.inInputShareable = true\n            options.inPurgeable = true\n            bitmap = getBitmap(file.absolutePath, options)\n            val heightScale = options.outHeight.toDouble() / screenHeight\n            val widthScale = options.outWidth.toDouble() / screenWidth\n            scale = widthScale.coerceAtLeast(heightScale)\n            val newHeight = ceil(options.outHeight / scale)\n            val newWidth = ceil(options.outWidth / scale)\n            if (bitmap != null) {\n                bitmap = Bitmap.createScaledBitmap(\n                    bitmap,\n                    newWidth.toInt(),\n                    newHeight.toInt(),\n                    false\n                )\n            }\n        } else {\n            val heightScale = options.outHeight / screenHeight\n            val widthScale = options.outWidth / screenWidth\n\n            // Powers of 2 work faster, sometimes, according to the doc.\n            // We're just doing closest size that still fills the screen.\n            scale = widthScale.coerceAtLeast(heightScale).toDouble()\n\n            // get bitmap with scale ( < 1 is the same as 1)\n            options = BitmapFactory.Options()\n            options.inInputShareable = true\n            options.inPurgeable = true\n            options.inSampleSize = scale.toInt()\n            bitmap = getBitmap(file.absolutePath, options)\n        }\n        if (bitmap != null) {\n            Timber.i(\n                \"Screen is %dx%d.  Image has been scaled down by %f to %dx%d\",\n                screenHeight,\n                screenWidth,\n                scale,\n                bitmap.height,\n                bitmap.width\n            )\n        }\n        return bitmap\n    }\n\n    /**\n     * While copying the file, apply the exif rotation of sourceFile to destinationFile\n     * so that sourceFile with EXIF has same orientation as destinationFile without EXIF\n     */\n    @JvmStatic\n    fun copyImageAndApplyExifRotation(sourceFile: File, destFile: File) {\n        var sourceFileExif: ExifInterface? = null\n        try {\n            sourceFileExif = ExifInterface(sourceFile)\n        } catch (e: IOException) {\n            Timber.w(e)\n        }\n        if (sourceFileExif == null ||\n            !EXIF_ORIENTATION_ROTATIONS.contains(\n                sourceFileExif\n                    .getAttributeInt(\n                        ExifInterface.TAG_ORIENTATION,\n                        ExifInterface.ORIENTATION_UNDEFINED\n                    )\n            )\n        ) {\n            // Source Image doesn't have any EXIF Rotations, so a normal file copy will suffice\n            sourceFile.copyTo(destFile, true)\n        } else {\n            val sourceImage = getBitmap(sourceFile.absolutePath, BitmapFactory.Options())\n            val orientation = sourceFileExif.getAttributeInt(\n                ExifInterface.TAG_ORIENTATION,\n                ExifInterface.ORIENTATION_UNDEFINED\n            )\n            when (orientation) {\n                ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmapAndSaveToFile(\n                    sourceImage,\n                    90,\n                    destFile.absolutePath\n                )\n                ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmapAndSaveToFile(\n                    sourceImage,\n                    180,\n                    destFile.absolutePath\n                )\n                ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmapAndSaveToFile(\n                    sourceImage,\n                    270,\n                    destFile.absolutePath\n                )\n            }\n        }\n    }\n\n    private fun rotateBitmapAndSaveToFile(image: Bitmap?, degrees: Int, filePath: String) {\n        var imageToSave = image\n        try {\n            val matrix = Matrix()\n            matrix.postRotate(degrees.toFloat())\n            if (image != null) {\n                imageToSave = Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, true)\n            }\n        } catch (e: OutOfMemoryError) {\n            Timber.w(e)\n        }\n        saveBitmapToFile(imageToSave, filePath)\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/data/AppState.kt",
    "content": "package org.odk.collect.androidshared.data\n\nimport android.app.Activity\nimport android.app.Application\nimport android.app.Service\nimport android.content.Context\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.ViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\n/**\n * [AppState] can be used as a shared store of state that lives at an \"app\"/\"in-memory\" level\n * rather than being tied to a specific component. This could be shared state between different\n * [Activity] objects or a way of communicating between a [Service] and other components.\n * [AppState] can be used as an alternative to Dagger singleton objects or static fields.\n *\n * [AppState] should not be used to share state between views or components on the same screen or make\n * up part of the same flow. For this, using Jetpack's [ViewModel] at either a [Fragment] or [Activity]\n * level is more appropriate.\n *\n * The easiest way to use [AppState] is have an instance owned by your app's [Application] object\n * and implement the [StateStore] interface:\n *\n * ```\n * class MyApplication : Application(), StateStore {\n *     private val appState = AppState()\n * }\n * ```\n *\n * The [AppState] instance can then be accessed anywhere the [Application] is available using the\n * [getState] extension function.\n *\n */\nclass AppState {\n\n    private val map = mutableMapOf<String, Any?>()\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <T> get(key: String, default: T): T {\n        return map.getOrPut(key) { default } as T\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <T> get(key: String): T? {\n        return map[key] as T?\n    }\n\n    fun <T> getFlow(key: String, default: T): StateFlow<T> {\n        return get(key, MutableStateFlow(default))\n    }\n\n    fun set(key: String, value: Any?) {\n        map[key] = value\n    }\n\n    fun <T> setFlow(key: String, value: T) {\n        get<MutableStateFlow<T>>(key).let {\n            if (it != null) {\n                it.value = value\n            } else {\n                map[key] = MutableStateFlow(value)\n            }\n        }\n    }\n\n    fun clear() {\n        map.clear()\n    }\n\n    fun clear(key: String) {\n        map.remove(key)\n    }\n}\n\ninterface StateStore {\n    fun getState(): AppState\n}\n\nfun Application.getState(): AppState {\n    try {\n        val stateStore = this as StateStore\n        return stateStore.getState()\n    } catch (e: ClassCastException) {\n        throw ClassCastException(\"${this.javaClass} cannot be cast to StateStore\")\n    }\n}\n\nfun Context.getState(): AppState {\n    return (applicationContext as Application).getState()\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/data/Consumable.kt",
    "content": "package org.odk.collect.androidshared.data\n\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.LiveData\n\n/**\n * Useful for values that are read multiple times but only used\n * once (like an error that shows a dialog once).\n */\ndata class Consumable<T>(val value: T) {\n\n    private var consumed = false\n\n    fun isConsumed(): Boolean {\n        return consumed\n    }\n\n    fun consume() {\n        consumed = true\n    }\n}\n\nfun <T> LiveData<out Consumable<T>?>.consume(lifecycleOwner: LifecycleOwner, consumer: (T) -> Unit) {\n    observe(lifecycleOwner) { consumable ->\n        if (consumable != null && !consumable.isConsumed()) {\n            consumable.consume()\n            consumer(consumable.value)\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/data/Data.kt",
    "content": "package org.odk.collect.androidshared.data\n\nimport kotlinx.coroutines.flow.StateFlow\nimport org.odk.collect.androidshared.data.Updatable.Data\nimport org.odk.collect.androidshared.data.Updatable.QualifiedData\nimport kotlin.reflect.KProperty\n\nsealed interface Updatable<T> {\n    class QualifiedData<T>(\n        private val appState: AppState,\n        private val key: String,\n        private val default: T\n    ) : Updatable<T> {\n        fun flow(qualifier: String): StateFlow<T> {\n            return appState.getFlow(\"$qualifier:$key\", default)\n        }\n\n        fun set(qualifier: String?, value: T) {\n            appState.setFlow(\"$qualifier:$key\", value)\n        }\n    }\n\n    class Data<T>(private val appState: AppState, private val key: String, private val default: T) :\n        Updatable<T> {\n        fun flow(): StateFlow<T> {\n            return appState.getFlow(key, default)\n        }\n\n        fun set(value: T) {\n            appState.setFlow(key, value)\n        }\n    }\n}\n\nabstract class DataService(\n    private val appState: AppState,\n    private val onUpdate: (() -> Unit)? = null\n) {\n\n    private val updaters = mutableListOf<Updater<*>>()\n\n    fun update(qualifier: String? = null) {\n        updaters.forEach { it.update(qualifier) }\n        onUpdate?.invoke()\n    }\n\n    protected fun <T> data(key: String, default: T): DataDelegate<T> {\n        val data = Data(appState, key, default)\n        return DataDelegate(data)\n    }\n\n    protected fun <T> data(key: String, default: T, updater: () -> T): DataDelegate<T> {\n        val data = Data(appState, key, default)\n        updaters.add(Updater(data) { updater() })\n        return DataDelegate(data)\n    }\n\n    protected fun <T> qualifiedData(\n        key: String,\n        default: T\n    ): QualifiedDataDelegate<T> {\n        val data = QualifiedData(appState, key, default)\n        return QualifiedDataDelegate(data)\n    }\n\n    protected fun <T> qualifiedData(\n        key: String,\n        default: T,\n        updater: (String) -> T\n    ): QualifiedDataDelegate<T> {\n        val data = QualifiedData(appState, key, default)\n        updaters.add(Updater(data) { it: String? -> updater(it!!) })\n        return QualifiedDataDelegate(data)\n    }\n\n    class QualifiedDataDelegate<T>(private val data: QualifiedData<T>) {\n        operator fun getValue(thisRef: Any?, property: KProperty<*>): QualifiedData<T> {\n            return data\n        }\n    }\n\n    class DataDelegate<T>(private val data: Data<T>) {\n        operator fun getValue(thisRef: Any?, property: KProperty<*>): Data<T> {\n            return data\n        }\n    }\n\n    private class Updater<T>(\n        private val updatable: Updatable<T>,\n        private val updater: (String?) -> T\n    ) {\n        fun update(qualifier: String? = null) {\n            when (updatable) {\n                is Data -> updatable.set(updater(qualifier))\n                is QualifiedData -> updatable.set(qualifier, updater(qualifier))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/livedata/LiveDataExt.kt",
    "content": "package org.odk.collect.androidshared.livedata\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MediatorLiveData\n\nobject LiveDataExt {\n    fun <T, U> LiveData<T>.combine(other: LiveData<U>): LiveData<Pair<T?, U?>> {\n        return LiveDataUtils.combine(this, other)\n    }\n\n    fun <T, U> LiveData<T>.runningFold(initial: U, operation: (U, T) -> U): LiveData<U> {\n        val mediator = MediatorLiveData<U>()\n\n        var accum = initial\n        mediator.addSource(this) {\n            accum = operation(accum, it)\n            mediator.value = accum\n        }\n\n        return mediator\n    }\n\n    /**\n     * Returns a [LiveData] where each value is a [Pair] made up of the latest value and the\n     * previous value.\n     */\n    fun <T : Any?> LiveData<T>.withLast(): LiveData<Pair<T?, T?>> {\n        return this.runningFold(Pair(null, null) as Pair<T?, T?>) { last, current ->\n            Pair(last.second, current)\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/livedata/LiveDataUtils.java",
    "content": "package org.odk.collect.androidshared.livedata;\n\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.MediatorLiveData;\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.lifecycle.Observer;\n\nimport org.odk.collect.async.Cancellable;\n\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\nimport kotlin.Pair;\nimport kotlin.Triple;\n\npublic class LiveDataUtils {\n\n    private LiveDataUtils() {\n\n    }\n\n    public static <T> Cancellable observe(LiveData<T> liveData, Consumer<T> consumer) {\n        Observer<T> observer = value -> {\n            if (value != null) {\n                consumer.accept(value);\n            }\n        };\n\n        liveData.observeForever(observer);\n        return () -> {\n            liveData.removeObserver(observer);\n            return true;\n        };\n    }\n\n    public static <T> LiveData<T> liveDataOf(T value) {\n        return new MutableLiveData<>(value);\n    }\n\n    public static <T, U> LiveData<Pair<T, U>> combine(LiveData<T> one, LiveData<U> two) {\n        return new CombinedLiveData<>(\n                new LiveData[]{one, two},\n                values -> new Pair<>((T) values[0], (U) values[1])\n        );\n    }\n\n    public static <T, U, V> LiveData<Triple<T, U, V>> combine3(LiveData<T> one, LiveData<U> two, LiveData<V> three) {\n        return new CombinedLiveData<>(\n                new LiveData[]{one, two, three},\n                values -> new Triple<>((T) values[0], (U) values[1], (V) values[2])\n        );\n    }\n\n    public static <T, U, V, W> LiveData<Quad<T, U, V, W>> combine4(LiveData<T> one, LiveData<U> two, LiveData<V> three, LiveData<W> four) {\n        return new CombinedLiveData<>(\n                new LiveData[]{one, two, three, four},\n                values -> new Quad<>((T) values[0], (U) values[1], (V) values[2], (W) values[3])\n        );\n    }\n\n    private static class CombinedLiveData<T> extends MediatorLiveData<T> {\n\n        private final Object[] values;\n        private final Function<Object[], T> map;\n        private T lastEmitted;\n\n        CombinedLiveData(LiveData<?>[] sources, Function<Object[], T> map) {\n            this.map = map;\n            values = new Object[sources.length];\n\n            for (int i = 0; i < sources.length; i++) {\n                int index = i;\n                addSource(sources[i], value -> {\n                    values[index] = value;\n                    update();\n                });\n            }\n\n            update();\n        }\n\n        private void update() {\n            T newValue = map.apply(values);\n\n            if (lastEmitted == null || !lastEmitted.equals(newValue)) {\n                lastEmitted = newValue;\n                setValue(newValue);\n            }\n        }\n    }\n\n    public static class Quad<T, U, V, W> {\n\n        public final T first;\n        public final U second;\n        public final V third;\n        public final W fourth;\n\n        public Quad(T first, U second, V third, W fourth) {\n            this.first = first;\n            this.second = second;\n            this.third = third;\n            this.fourth = fourth;\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/livedata/NonNullLiveData.kt",
    "content": "package org.odk.collect.androidshared.livedata\n\nimport androidx.lifecycle.LiveData\n\n/**\n * Allows creating LiveData values that can be used without null checks\n */\nabstract class NonNullLiveData<T : Any>(value: T) : LiveData<T>(value) {\n\n    override fun getValue(): T {\n        return super.getValue() as T\n    }\n}\n\nclass MutableNonNullLiveData<T : Any>(value: T) : NonNullLiveData<T>(value) {\n\n    public override fun postValue(value: T) {\n        super.postValue(value)\n    }\n\n    public override fun setValue(value: T) {\n        super.setValue(value)\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/system/BroadcastReceiverRegister.kt",
    "content": "package org.odk.collect.androidshared.system\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.IntentFilter\n\ninterface BroadcastReceiverRegister {\n    fun registerReceiver(receiver: BroadcastReceiver, filter: IntentFilter)\n\n    fun unregisterReceiver(receiver: BroadcastReceiver)\n}\n\nclass BroadcastReceiverRegisterImpl(private val context: Context) : BroadcastReceiverRegister {\n    override fun registerReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {\n        context.registerReceiver(receiver, filter)\n    }\n\n    override fun unregisterReceiver(receiver: BroadcastReceiver) {\n        context.unregisterReceiver(receiver)\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/system/CameraUtils.java",
    "content": "package org.odk.collect.androidshared.system;\n\n/*\nCopyright 2018 Theodoros Tyrovouzis\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n */\n\nimport android.content.Context;\nimport android.hardware.Camera;\nimport android.hardware.camera2.CameraAccessException;\nimport android.hardware.camera2.CameraCharacteristics;\nimport android.hardware.camera2.CameraManager;\n\nimport timber.log.Timber;\n\npublic class CameraUtils {\n    public static int getFrontCameraId() {\n        for (int camNo = 0; camNo < Camera.getNumberOfCameras(); camNo++) {\n            Camera.CameraInfo camInfo = new Camera.CameraInfo();\n            Camera.getCameraInfo(camNo, camInfo);\n\n            if (camInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {\n                return camNo;\n            }\n        }\n        Timber.w(\"No Available front camera\");\n        return -1;\n    }\n\n    public boolean isFrontCameraAvailable(Context context) {\n        try {\n            //https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics.html\n            CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);\n            if (cameraManager != null) {\n                String[] cameraId = cameraManager.getCameraIdList();\n                for (String id : cameraId) {\n                    CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);\n                    Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);\n                    if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {\n                        return true;\n                    }\n                }\n            }\n        } catch (CameraAccessException | NullPointerException e) {\n            Timber.e(e);\n        }\n        return false; // No front-facing camera found\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/system/ContextExt.kt",
    "content": "package org.odk.collect.androidshared.system\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.res.Configuration\nimport android.util.TypedValue\nimport androidx.annotation.AttrRes\n\nobject ContextExt {\n\n    /**\n     * Be careful when using this method to retrieve colors, especially for those defined\n     * using selectors as it might not work well.\n     * In such cases consider using [com.google.android.material.color.MaterialColors.getColor] instead.\n     */\n    @JvmStatic\n    fun getThemeAttributeValue(context: Context, @AttrRes resId: Int): Int {\n        val outValue = TypedValue()\n        context.theme.resolveAttribute(resId, outValue, true)\n        return outValue.data\n    }\n\n    @JvmStatic\n    fun Context.isDarkTheme(): Boolean {\n        val uiMode: Int = this.resources.configuration.uiMode\n        return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/system/ExternalFilesUtils.kt",
    "content": "package org.odk.collect.androidshared.system\n\nimport android.content.Context\nimport java.io.File\nimport java.io.IOException\n\nobject ExternalFilesUtils {\n\n    @JvmStatic\n    fun testExternalFilesAccess(context: Context) {\n        val externalFilesDir = context.getExternalFilesDir(null)\n\n        if (externalFilesDir == null) {\n            throw IllegalStateException(\"External files dir is null!\")\n        } else {\n            try {\n                val testFile = File(externalFilesDir, \".test\")\n                testFile.createNewFile()\n                testFile.delete()\n            } catch (e: IOException) {\n                if (!externalFilesDir.exists()) {\n                    throw IllegalStateException(\n                        \"External files dir does not exist: ${externalFilesDir.absolutePath}\"\n                    )\n                } else {\n                    throw IllegalStateException(\n                        \"App can't write to external files dir: ${externalFilesDir.absolutePath}\",\n                        e\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/system/IntentLauncher.kt",
    "content": "package org.odk.collect.androidshared.system\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport androidx.activity.result.ActivityResultLauncher\n\nobject IntentLauncherImpl : IntentLauncher {\n    override fun launch(context: Context, intent: Intent?, onError: () -> Unit) {\n        try {\n            context.startActivity(intent)\n        } catch (e: Exception) {\n            onError()\n        } catch (e: Error) {\n            onError()\n        }\n    }\n\n    override fun launchForResult(\n        activity: Activity,\n        intent: Intent?,\n        requestCode: Int,\n        onError: () -> Unit\n    ) {\n        try {\n            activity.startActivityForResult(intent, requestCode)\n        } catch (e: Exception) {\n            onError()\n        } catch (e: Error) {\n            onError()\n        }\n    }\n\n    override fun launchForResult(\n        resultLauncher: ActivityResultLauncher<Intent>,\n        intent: Intent?,\n        onError: () -> Unit\n    ) {\n        try {\n            resultLauncher.launch(intent)\n        } catch (e: Exception) {\n            onError()\n        } catch (e: Error) {\n            onError()\n        }\n    }\n}\n\ninterface IntentLauncher {\n    fun launch(context: Context, intent: Intent?, onError: () -> Unit)\n\n    fun launchForResult(activity: Activity, intent: Intent?, requestCode: Int, onError: () -> Unit)\n\n    fun launchForResult(\n        resultLauncher: ActivityResultLauncher<Intent>,\n        intent: Intent?,\n        onError: () -> Unit\n    )\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/system/OpenGLVersionChecker.kt",
    "content": "package org.odk.collect.androidshared.system\n\nimport android.app.ActivityManager\nimport android.content.Context\n\n/**\n * Checks if the device supports the given OpenGL ES version.\n *\n * Note: This approach may not be 100% reliable because `reqGlEsVersion` indicates\n * the highest version of OpenGL ES that the device's hardware is guaranteed to support\n * at runtime. However, it might not always reflect the actual version available.\n *\n * For a more reliable method, refer to https://developer.android.com/develop/ui/views/graphics/opengl/about-opengl#version-check.\n * This recommended approach is more complex to implement but offers better accuracy.\n */\nobject OpenGLVersionChecker {\n    @JvmStatic\n    fun isOpenGLv2Supported(context: Context): Boolean {\n        return (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)\n            .deviceConfigurationInfo.reqGlEsVersion >= 0x20000\n    }\n\n    @JvmStatic\n    fun isOpenGLv3Supported(context: Context): Boolean {\n        return (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)\n            .deviceConfigurationInfo.reqGlEsVersion >= 0x30000\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/system/PlayServicesChecker.java",
    "content": "package org.odk.collect.androidshared.system;\n\nimport android.app.Activity;\nimport android.content.Context;\n\nimport com.google.android.gms.common.ConnectionResult;\nimport com.google.android.gms.common.GoogleApiAvailability;\n\n/** Created by Divya on 3/2/2017. */\n\npublic class PlayServicesChecker {\n    private static final int PLAY_SERVICE_ERROR_REQUEST_CODE = 1000;\n    private static int lastResultCode = ConnectionResult.SUCCESS;\n\n    /** Returns true if Google Play Services is installed and up to date. */\n    public boolean isGooglePlayServicesAvailable(Context context) {\n        lastResultCode = GoogleApiAvailability.getInstance()\n            .isGooglePlayServicesAvailable(context);\n        return lastResultCode == ConnectionResult.SUCCESS;\n    }\n\n    /** Shows an error dialog for the last call to isGooglePlayServicesAvailable(). */\n    public void showGooglePlayServicesAvailabilityErrorDialog(Context context) {\n        GoogleApiAvailability.getInstance().getErrorDialog(\n            (Activity) context, lastResultCode, PLAY_SERVICE_ERROR_REQUEST_CODE).show();\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/system/ProcessRestoreDetector.kt",
    "content": "package org.odk.collect.androidshared.system\n\nimport android.content.Context\nimport android.os.Bundle\nimport org.odk.collect.androidshared.data.getState\nimport org.odk.collect.shared.strings.UUIDGenerator\n\nobject ProcessRestoreDetector {\n\n    @JvmStatic\n    fun registerOnSaveInstanceState(context: Context, outState: Bundle) {\n        val uuid = UUIDGenerator().generateUUID()\n        context.getState().set(\"${getKey()}:$uuid\", Any())\n        outState.putString(getKey(), uuid)\n    }\n\n    @JvmStatic\n    fun isProcessRestoring(context: Context, savedInstanceState: Bundle?): Boolean {\n        return if (savedInstanceState != null) {\n            val bundleUuid = savedInstanceState.getString(getKey())\n            context.getState().get<Any>(\"${getKey()}:$bundleUuid\") == null\n        } else {\n            false\n        }\n    }\n\n    private fun getKey() = this::class.qualifiedName\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt",
    "content": "package org.odk.collect.androidshared.system\n\nimport android.content.ContentResolver\nimport android.content.Context\nimport android.net.Uri\nimport android.provider.OpenableColumns\nimport android.webkit.MimeTypeMap\nimport androidx.core.net.toFile\nimport java.io.File\nimport java.io.FileOutputStream\n\nfun Uri.copyToFile(context: Context, dest: File) {\n    try {\n        context.contentResolver.openInputStream(this)?.use { inputStream ->\n            FileOutputStream(dest).use { outputStream ->\n                inputStream.copyTo(outputStream)\n            }\n        }\n    } catch (e: Exception) {\n        // ignore\n    }\n}\n\nfun Uri.getFileExtension(context: Context): String? {\n    var extension = getFileName(context)?.substringAfterLast(\".\", \"\")\n\n    if (extension.isNullOrEmpty()) {\n        val mimeType = context.contentResolver.getType(this)\n\n        extension = if (scheme == ContentResolver.SCHEME_CONTENT) {\n            MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)\n        } else {\n            MimeTypeMap.getFileExtensionFromUrl(toString())\n        }\n\n        if (extension.isNullOrEmpty()) {\n            extension = mimeType?.substringAfterLast(\"/\", \"\")\n        }\n    }\n\n    return if (extension.isNullOrEmpty()) {\n        null\n    } else {\n        \".$extension\"\n    }\n}\n\nfun Uri.getFileName(context: Context): String? {\n    var fileName: String? = null\n\n    try {\n        when (scheme) {\n            ContentResolver.SCHEME_FILE -> fileName = toFile().name\n            ContentResolver.SCHEME_CONTENT -> {\n                val cursor = context.contentResolver.query(this, null, null, null, null)\n                cursor?.use {\n                    if (it.moveToFirst()) {\n                        val fileNameColumnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)\n                        if (fileNameColumnIndex != -1) {\n                            fileName = it.getString(fileNameColumnIndex)\n                        }\n                    }\n                }\n            }\n            ContentResolver.SCHEME_ANDROID_RESOURCE -> {\n                // for uris like [android.resource://com.example.app/1234567890]\n                val resourceId = lastPathSegment?.toIntOrNull()\n                if (resourceId != null) {\n                    fileName = context.resources.getResourceName(resourceId)\n                } else {\n                    // for uris like [android.resource://com.example.app/raw/sample]\n                    val packageName = authority\n                    if (pathSegments.size >= 2) {\n                        val resourceType = pathSegments[0]\n                        val resourceEntryName = pathSegments[1]\n                        val resId = context.resources.getIdentifier(resourceEntryName, resourceType, packageName)\n                        if (resId != 0) {\n                            fileName = context.resources.getResourceName(resId)\n                        }\n                    }\n                }\n            }\n        }\n\n        if (fileName == null) {\n            fileName = path?.substringAfterLast(\"/\")\n        }\n    } catch (e: Exception) {\n        // ignore\n    }\n\n    return fileName\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/AlertStore.kt",
    "content": "package org.odk.collect.androidshared.ui\n\n/**\n * Component for recording \"alerts\". This is useful for testing transient UI elements like toasts,\n * flashes or snackbars that are susceptible to flakiness with assertions running after they have\n * disappeared.\n */\nclass AlertStore {\n\n    var enabled = false\n    private var recordedAlerts = mutableListOf<String>()\n\n    fun register(alert: String) {\n        if (enabled) {\n            recordedAlerts.add(alert)\n        }\n    }\n\n    fun popAll(): List<String> {\n        val copy = recordedAlerts.toList()\n        recordedAlerts.clear()\n\n        return copy\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/Animations.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.animation.Animator\nimport android.animation.AnimatorSet\nimport android.animation.ValueAnimator\nimport android.view.View\nimport org.odk.collect.androidshared.ui.Animations.DISABLE_ANIMATIONS\n\n/**\n * Helpers/extensions for running animations. These are \"test safe\" in that animations can be disabled\n * using [DISABLE_ANIMATIONS] - this should be set to `true` in Robolectric tests to avoid\n * infinite loops.\n */\nobject Animations {\n\n    var DISABLE_ANIMATIONS = false\n\n    @JvmStatic\n    fun View.createAlphaAnimation(\n        startValue: Float,\n        endValue: Float,\n        duration: Long\n    ): DisableableAnimatorWrapper {\n        val animation = ValueAnimator.ofFloat(startValue, endValue)\n        animation.duration = duration\n        animation.addUpdateListener {\n            this.alpha = it.animatedValue as Float\n        }\n\n        return DisableableAnimatorWrapper(animation)\n    }\n}\n\nclass DisableableAnimatorWrapper(private val wrapped: Animator) {\n\n    fun onEnd(onEnd: () -> Unit): DisableableAnimatorWrapper {\n        wrapped.addListener(object : Animator.AnimatorListener {\n            override fun onAnimationStart(animation: Animator) {\n            }\n\n            override fun onAnimationEnd(animation: Animator) {\n                onEnd()\n            }\n\n            override fun onAnimationCancel(animation: Animator) {\n            }\n\n            override fun onAnimationRepeat(animation: Animator) {\n            }\n        })\n\n        return this\n    }\n\n    fun then(other: DisableableAnimatorWrapper): DisableableAnimatorWrapper {\n        val set = AnimatorSet()\n        set.playSequentially(this.wrapped, other.wrapped)\n\n        return DisableableAnimatorWrapper(set)\n    }\n\n    fun start() {\n        if (!DISABLE_ANIMATIONS) {\n            wrapped.start()\n        } else {\n            // Just run listeners immediately if we're not running the actual animations\n            if (wrapped is AnimatorSet) {\n                (wrapped.childAnimations + wrapped).forEach { anim ->\n                    anim.listeners?.forEach {\n                        it.onAnimationStart(wrapped)\n                        it.onAnimationEnd(wrapped)\n                    }\n                }\n            } else {\n                wrapped.listeners?.forEach {\n                    it.onAnimationStart(wrapped)\n                    it.onAnimationEnd(wrapped)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/ColorPickerDialog.kt",
    "content": "package org.odk.collect.androidshared\n\nimport android.app.Dialog\nimport android.graphics.Color\nimport android.graphics.drawable.GradientDrawable\nimport android.os.Bundle\nimport android.view.Gravity\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport android.widget.LinearLayout\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.widget.AppCompatTextView\nimport androidx.core.content.ContextCompat\nimport androidx.core.widget.doOnTextChanged\nimport androidx.fragment.app.DialogFragment\nimport androidx.fragment.app.activityViewModels\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.androidshared.databinding.ColorPickerDialogLayoutBinding\nimport java.lang.Exception\n\nclass ColorPickerDialog : DialogFragment() {\n    lateinit var binding: ColorPickerDialogLayoutBinding\n\n    val model: ColorPickerViewModel by activityViewModels()\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        binding = ColorPickerDialogLayoutBinding.inflate(LayoutInflater.from(context))\n        binding.hexColor.doOnTextChanged { color, _, _, _ ->\n            updateCurrentColorCircle(\"#$color\")\n        }\n        binding.currentColor.text = requireArguments().getString(CURRENT_ICON)!!\n\n        fixHexColorPrefix()\n        setListeners()\n        setCurrentColor(requireArguments().getString(CURRENT_COLOR)!!)\n\n        return MaterialAlertDialogBuilder(requireContext())\n            .setView(binding.root)\n            .setTitle(org.odk.collect.strings.R.string.project_color)\n            .setNegativeButton(org.odk.collect.strings.R.string.cancel) { _, _ -> dismiss() }\n            .setPositiveButton(org.odk.collect.strings.R.string.ok) { _, _ -> model.pickColor(\"#${binding.hexColor.text}\") }\n            .create()\n    }\n\n    private fun setListeners() {\n        binding.color1.setOnClickListener { setCurrentColor(R.color.color1) }\n        binding.color2.setOnClickListener { setCurrentColor(R.color.color2) }\n        binding.color3.setOnClickListener { setCurrentColor(R.color.color3) }\n        binding.color4.setOnClickListener { setCurrentColor(R.color.color4) }\n        binding.color5.setOnClickListener { setCurrentColor(R.color.color5) }\n        binding.color6.setOnClickListener { setCurrentColor(R.color.color6) }\n        binding.color7.setOnClickListener { setCurrentColor(R.color.color7) }\n        binding.color8.setOnClickListener { setCurrentColor(R.color.color8) }\n        binding.color9.setOnClickListener { setCurrentColor(R.color.color9) }\n        binding.color10.setOnClickListener { setCurrentColor(R.color.color10) }\n        binding.color11.setOnClickListener { setCurrentColor(R.color.color11) }\n        binding.color12.setOnClickListener { setCurrentColor(R.color.color12) }\n        binding.color13.setOnClickListener { setCurrentColor(R.color.color13) }\n        binding.color14.setOnClickListener { setCurrentColor(R.color.color14) }\n        binding.color15.setOnClickListener { setCurrentColor(R.color.color15) }\n    }\n\n    private fun setCurrentColor(color: Int) {\n        setCurrentColor(\"#${Integer.toHexString(ContextCompat.getColor(requireContext(), color)).substring(2)}\")\n    }\n\n    private fun setCurrentColor(color: String) {\n        binding.hexColor.setText(color.substring(1))\n    }\n\n    private fun updateCurrentColorCircle(color: String) {\n        try {\n            (binding.currentColor.background as GradientDrawable).setColor(Color.parseColor(color))\n            binding.hexColor.error = null\n            (dialog as? AlertDialog)?.also {\n                it.getButton(AlertDialog.BUTTON_POSITIVE).alpha = 1f\n                it.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true\n            }\n        } catch (e: Exception) {\n            binding.hexColor.error = getString(org.odk.collect.strings.R.string.invalid_hex_code)\n            (dialog as? AlertDialog)?.also {\n                it.getButton(AlertDialog.BUTTON_POSITIVE).alpha = 0.3f\n                it.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false\n            }\n        }\n    }\n\n    // https://github.com/material-components/material-components-android/issues/773#issuecomment-603759649\n    private fun fixHexColorPrefix() {\n        val prefixView = binding.hexColorLayout.findViewById<AppCompatTextView>(com.google.android.material.R.id.textinput_prefix_text)\n        prefixView.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)\n        prefixView.gravity = Gravity.CENTER\n    }\n\n    companion object {\n        const val CURRENT_COLOR = \"CURRENT_COLOR\"\n        const val CURRENT_ICON = \"CURRENT_ICON\"\n    }\n}\n\nclass ColorPickerViewModel : ViewModel() {\n    private val _pickedColor = MutableLiveData<String>()\n    val pickedColor: LiveData<String> = _pickedColor\n\n    fun pickColor(color: String) {\n        _pickedColor.value = color\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/ComposeThemeProvider.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.ComposeView\nimport androidx.compose.ui.platform.ViewCompositionStrategy\n\n/**\n * Interface that allows a [android.content.Context] (such as an [android.app.Activity]) to\n * provide a Compose Theme to any child [ComposeView] instances:\n *\n * ```kotlin\n * class MyActivity : AppCompatActivity(), ComposeThemeProvider {\n *\n *     override fun onCreate(savedInstanceState: Bundle?) {\n *         super.onCreate(savedInstanceState)\n *         setContentView(R.layout.my_activity_layout)\n *         findViewById<ComposeView>(R.id.compose_view).setContextThemedContent {\n *             Text(\"Hello, world!\")\n *         }\n *     }\n *\n *     @Composable\n *     override fun Theme(content: @Composable (() -> Unit)) {\n *         MyTheme { content() }\n *     }\n * }\n */\ninterface ComposeThemeProvider {\n    @Composable\n    fun Theme(content: @Composable () -> Unit)\n\n    companion object {\n        fun ComposeView.setContextThemedContent(\n            viewCompositionStrategy: ViewCompositionStrategy,\n            content: @Composable () -> Unit\n        ) {\n            setViewCompositionStrategy(viewCompositionStrategy)\n            setContent {\n                val themeProvider = context as? ComposeThemeProvider\n                if (themeProvider != null) {\n                    themeProvider.Theme {\n                        content()\n                    }\n                } else {\n                    content()\n                }\n            }\n        }\n\n        @Deprecated(\"Use overload instead\")\n        fun ComposeView.setContextThemedContent(content: @Composable () -> Unit) {\n            setContextThemedContent(ViewCompositionStrategy.Default, content)\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/DialogFragmentUtils.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.os.Bundle\nimport androidx.fragment.app.DialogFragment\nimport androidx.fragment.app.FragmentManager\nimport timber.log.Timber\nimport kotlin.reflect.KClass\n\nobject DialogFragmentUtils {\n\n    @JvmStatic\n    fun <T : DialogFragment> showIfNotShowing(\n        dialogClass: Class<T>,\n        fragmentManager: FragmentManager\n    ) {\n        showIfNotShowing(dialogClass, null, fragmentManager)\n    }\n\n    @JvmStatic\n    fun <T : DialogFragment> showIfNotShowing(\n        dialogClass: Class<T>,\n        args: Bundle?,\n        fragmentManager: FragmentManager\n    ) {\n        if (fragmentManager.isDestroyed) {\n            return\n        }\n\n        val fragmentFactory = fragmentManager.fragmentFactory\n        val instance = fragmentFactory.instantiate(dialogClass.classLoader, dialogClass.name) as T\n        instance.arguments = args\n        showIfNotShowing(instance, dialogClass, fragmentManager)\n    }\n\n    @JvmStatic\n    fun <T : DialogFragment> showIfNotShowing(\n        newDialog: T,\n        dialogClass: Class<T>,\n        fragmentManager: FragmentManager\n    ) {\n        showIfNotShowing(newDialog, dialogClass.name, fragmentManager)\n    }\n\n    @JvmStatic\n    fun <T : DialogFragment> showIfNotShowing(\n        newDialog: T,\n        tag: String,\n        fragmentManager: FragmentManager\n    ) {\n        if (fragmentManager.isStateSaved) {\n            return\n        }\n        val existingDialog = fragmentManager.findFragmentByTag(tag) as T?\n        if (existingDialog == null) {\n            newDialog.show(fragmentManager.beginTransaction(), tag)\n\n            // We need to execute this transaction. Otherwise a follow up call to this method\n            // could happen before the Fragment exists in the Fragment Manager and so the\n            // call to findFragmentByTag would return null and result in second dialog being show.\n            try {\n                fragmentManager.executePendingTransactions()\n            } catch (e: IllegalStateException) {\n                Timber.w(e)\n            }\n        }\n    }\n\n    @JvmStatic\n    fun dismissDialog(dialogClazz: Class<*>, fragmentManager: FragmentManager) {\n        dismissDialog(dialogClazz.name, fragmentManager)\n    }\n\n    @JvmStatic\n    fun dismissDialog(tag: String, fragmentManager: FragmentManager) {\n        val existingDialog = fragmentManager.findFragmentByTag(tag) as DialogFragment?\n        if (existingDialog != null) {\n            existingDialog.dismissAllowingStateLoss()\n\n            // We need to execute this transaction. Otherwise a next attempt to display a dialog\n            // could happen before the Fragment is dismissed in Fragment Manager and so the\n            // call to findFragmentByTag would return something (not null) and as a result the\n            // next dialog won't be displayed.\n            try {\n                fragmentManager.executePendingTransactions()\n            } catch (e: IllegalStateException) {\n                Timber.w(e)\n            }\n        }\n    }\n\n    fun <T : DialogFragment> FragmentManager.showIfNotShowing(dialogClass: KClass<T>) {\n        showIfNotShowing(dialogClass.java, this)\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/DialogUtils.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.content.Context\nimport androidx.annotation.StringRes\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\n\nobject DialogUtils {\n    @JvmStatic\n    fun show(\n        context: Context,\n        @StringRes titleRes: Int,\n        @StringRes messageRes: Int,\n    ) {\n        MaterialAlertDialogBuilder(context)\n            .setTitle(titleRes)\n            .setMessage(messageRes)\n            .setPositiveButton(org.odk.collect.strings.R.string.ok, null)\n            .show()\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/DisplayString.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.content.Context\n\nsealed class DisplayString {\n\n    data class Raw(val value: String) : DisplayString()\n    data class Resource(val resource: Int) : DisplayString()\n\n    fun getString(context: Context): String {\n        return when (this) {\n            is Raw -> value\n            is Resource -> context.getString(resource)\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/EdgeToEdge.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.app.Activity\nimport android.content.Context\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.Window\nimport androidx.annotation.LayoutRes\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.WindowCompat\nimport androidx.core.view.WindowInsetsCompat\nimport androidx.core.view.updateLayoutParams\nimport androidx.core.view.updatePadding\nimport org.odk.collect.androidshared.system.ContextExt.isDarkTheme\n\nobject EdgeToEdge {\n\n    @JvmStatic\n    fun Activity.setView(@LayoutRes layout: Int, edgeToEdge: Boolean) {\n        window.handleEdgeToEdge(this, edgeToEdge)\n        setContentView(layout)\n    }\n\n    @JvmStatic\n    fun Activity.setView(view: View, edgeToEdge: Boolean) {\n        window.handleEdgeToEdge(this, edgeToEdge)\n        setContentView(view)\n    }\n\n    fun Window.handleEdgeToEdge(context: Context, edgeToEdge: Boolean = false) {\n        WindowCompat.enableEdgeToEdge(this)\n        WindowCompat.getInsetsController(this, this.decorView).let {\n            val darkTheme = context.isDarkTheme()\n            it.isAppearanceLightStatusBars = !darkTheme\n            it.isAppearanceLightNavigationBars = !darkTheme\n        }\n\n        if (!edgeToEdge) {\n            avoidEdgeToEdge()\n        }\n    }\n\n    fun View.applyBottomBarInsetMargins() {\n        ViewCompat.setOnApplyWindowInsetsListener(this) { v, windowInsets ->\n            val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())\n            val keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())\n\n            v.updatePadding(\n                bottom = maxOf(0, keyboardInsets.bottom - systemBarsInsets.bottom)\n            )\n\n            windowInsets\n        }\n    }\n\n    private fun Window.avoidEdgeToEdge() {\n        val contentView = decorView.findViewById<View>(android.R.id.content)\n        contentView.addSystemBarInsetMargins()\n    }\n\n    private fun View.addSystemBarInsetMargins() {\n        ViewCompat.setOnApplyWindowInsetsListener(this) { v, windowInsets ->\n            val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())\n            v.updateLayoutParams<ViewGroup.MarginLayoutParams> {\n                topMargin = insets.top\n                bottomMargin = insets.bottom\n\n                leftMargin = insets.left\n                rightMargin = insets.right\n            }\n\n            windowInsets\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/FragmentFactoryBuilder.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentFactory\nimport kotlin.reflect.KClass\n\n/**\n * Convenience object for creating [FragmentFactory] instances without needing to use an inner,\n * private or anonymous class.\n */\nclass FragmentFactoryBuilder {\n\n    private val classesAndFactories = mutableListOf<Pair<Class<*>, () -> Fragment>>()\n\n    fun forClass(fragmentClass: KClass<*>, factory: () -> Fragment): FragmentFactoryBuilder {\n        return forClass(fragmentClass.java, factory)\n    }\n\n    fun forClass(fragmentClass: Class<*>, factory: () -> Fragment): FragmentFactoryBuilder {\n        classesAndFactories.add(Pair(fragmentClass, factory))\n        return this\n    }\n\n    fun build(): FragmentFactory {\n        return object : androidx.fragment.app.FragmentFactory() {\n            override fun instantiate(classLoader: ClassLoader, className: String): Fragment {\n                val fragmentClass = loadFragmentClass(classLoader, className)\n\n                val factory =\n                    classesAndFactories.find { it.first.isAssignableFrom(fragmentClass) }?.second\n\n                return if (factory != null) {\n                    factory()\n                } else {\n                    super.instantiate(classLoader, className)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/GroupClickListener.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.view.View\nimport androidx.constraintlayout.widget.Group\n\n// https://stackoverflow.com/questions/59020818/group-multiple-views-in-constraint-layout-to-set-only-one-click-listener\nfun Group.addOnClickListener(listener: (view: View) -> Unit) {\n    referencedIds.forEach { id ->\n        rootView.findViewById<View>(id).setOnClickListener(listener)\n    }\n}\n\nfun List<View>.addOnClickListener(listener: (view: View) -> Unit) {\n    forEach {\n        it.setOnClickListener(listener)\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/ListFragmentStateAdapter.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentActivity\nimport androidx.viewpager2.adapter.FragmentStateAdapter\n\nclass ListFragmentStateAdapter(\n    activity: FragmentActivity,\n    private val fragments: List<Class<out Fragment>>\n) : FragmentStateAdapter(activity) {\n\n    private val fragmentFactory = activity.supportFragmentManager.fragmentFactory\n\n    override fun createFragment(position: Int): Fragment {\n        return fragmentFactory.instantiate(\n            Thread.currentThread().contextClassLoader,\n            fragments[position].name\n        )\n    }\n\n    override fun getItemCount(): Int {\n        return fragments.size\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/MenuExt.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.annotation.SuppressLint\nimport android.view.Menu\nimport androidx.appcompat.view.menu.MenuBuilder\n\n/**\n * Currently, there is no public API to add icons to popup menus.\n * The following workaround is for API 21+, and uses library-only APIs,\n * and is not guaranteed to work in future versions.\n */\n@SuppressLint(\"RestrictedApi\")\nfun Menu.enableIconsVisibility() {\n    if (this is MenuBuilder) {\n        this.setOptionalIconsVisible(true)\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/ObviousProgressBar.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.content.Context\nimport android.os.Handler\nimport android.util.AttributeSet\nimport com.google.android.material.progressindicator.LinearProgressIndicator\n\n/**\n * A progress bar that shows for a minimum amount fo time so it's obvious to the user that\n * something has happened.\n */\nclass ObviousProgressBar(\n    context: Context,\n    attrs: AttributeSet?\n) : LinearProgressIndicator(context, attrs) {\n    private val handler = Handler()\n    private var shownAt: Long? = null\n\n    init {\n        super.setVisibility(GONE)\n        super.setIndeterminate(true)\n    }\n\n    override fun show() {\n        handler.removeCallbacksAndMessages(null)\n        shownAt = System.currentTimeMillis()\n        super.setVisibility(VISIBLE)\n    }\n\n    override fun hide() {\n        if (shownAt != null) {\n            val timeShown = System.currentTimeMillis() - shownAt!!\n\n            if (timeShown < MINIMUM_SHOW_TIME) {\n                val delay = MINIMUM_SHOW_TIME - timeShown\n\n                handler.removeCallbacksAndMessages(null)\n                handler.postDelayed({ this.makeGone() }, delay)\n            } else {\n                makeGone()\n            }\n        } else {\n            makeGone()\n        }\n    }\n\n    private fun makeGone() {\n        super.setVisibility(GONE)\n        shownAt = null\n    }\n\n    companion object {\n        private const val MINIMUM_SHOW_TIME = 750\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/OneSignTextWatcher.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.text.Editable\nimport android.text.TextWatcher\nimport android.widget.EditText\nimport org.odk.collect.shared.strings.StringUtils\n\nclass OneSignTextWatcher(private val editText: EditText) : TextWatcher {\n    lateinit var oldTextString: String\n\n    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {\n        oldTextString = s.toString()\n    }\n\n    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {\n    }\n\n    override fun afterTextChanged(editable: Editable?) {\n        editable.toString().let {\n            if (it != oldTextString) {\n                val trimmedString = StringUtils.firstCharacterOrEmoji(it)\n                editText.setText(trimmedString)\n                editText.setSelection(trimmedString.length)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/PrefUtils.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.content.Context\nimport androidx.preference.ListPreference\nimport org.odk.collect.shared.settings.Settings\n\nobject PrefUtils {\n\n    @JvmStatic\n    fun createListPref(\n        context: Context,\n        key: String,\n        title: String,\n        labelIds: IntArray,\n        values: Array<String>,\n        settings: Settings\n    ): ListPreference {\n        val labels: Array<String?> = labelIds.map { context.getString(it) }.toTypedArray()\n        return createListPref(context, key, title, labels, values, settings)\n    }\n\n    /**\n     * Gets an integer value from the shared preferences. If the preference has\n     * a string value, attempts to convert it to an integer. If the preference\n     * is not found or is not a valid integer, returns the defaultValue.\n     */\n    @JvmStatic\n    fun getInt(key: String?, defaultValue: Int, settings: Settings): Int {\n        val value: Any? = settings.getAll()[key]\n        if (value is Int) {\n            return value\n        }\n\n        if (value is String) {\n            try {\n                return Integer.parseInt(value)\n            } catch (e: NumberFormatException) {\n                // ignore\n            }\n        }\n\n        return defaultValue\n    }\n\n    private fun createListPref(\n        context: Context,\n        key: String,\n        title: String,\n        labels: Array<String?>,\n        values: Array<String>,\n        settings: Settings\n    ): ListPreference {\n        ensurePrefHasValidValue(key, values, settings)\n        return ListPreference(context).also {\n            it.key = key\n            it.isPersistent = true\n            it.title = title\n            it.dialogTitle = title\n            it.entries = labels\n            it.entryValues = values\n            it.summary = \"%s\"\n        }\n    }\n\n    private fun ensurePrefHasValidValue(\n        key: String,\n        validValues: Array<String>,\n        settings: Settings\n    ) {\n        val value = settings.getString(key)\n        if (validValues.indexOf(value) < 0) {\n            if (validValues.isNotEmpty()) {\n                settings.save(key, validValues[0])\n            } else {\n                settings.remove(key)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/ReturnToAppActivity.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.app.Activity\nimport android.os.Bundle\n\n/**\n * This Activity will close as soon as it is started. This means it can be used as the content\n * intent of a notification so clicking it will effectively return to the screen the user\n * was last on (knowing what that Activity was).\n */\nclass ReturnToAppActivity : Activity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        finish()\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt",
    "content": "/*\n\nCopyright 2018 Shobhit\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\nhttp://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage org.odk.collect.androidshared.ui\n\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.Button\nimport android.widget.ImageView\nimport android.widget.LinearLayout\nimport android.widget.TextView\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.Observer\nimport com.google.android.material.R\nimport com.google.android.material.snackbar.BaseTransientBottomBar\nimport com.google.android.material.snackbar.Snackbar\nimport org.odk.collect.androidshared.data.Consumable\n\n/**\n * Convenience wrapper around Android's [Snackbar] API.\n */\nobject SnackbarUtils {\n\n    @JvmStatic\n    val alertStore = AlertStore()\n\n    const val DURATION_SHORT = 3500\n    const val DURATION_LONG = 5500\n\n    private var lastSnackbar: Snackbar? = null\n\n    /**\n     * Displays snackbar with {@param message} and multi-line message enabled.\n     *\n     * @param parentView            The view to find a parent from.\n     * @param anchorView            The view this snackbar should be anchored above.\n     * @param message               The text to show. Can be formatted text.\n     * @param displayDismissButton  True if the dismiss button should be displayed, false otherwise.\n     */\n    @JvmStatic\n    @JvmOverloads\n    fun showSnackbar(\n        parentView: View,\n        message: String,\n        duration: Int,\n        anchorView: View? = null,\n        action: Action? = null,\n        displayDismissButton: Boolean = false,\n        onDismiss: () -> Unit = {}\n    ) {\n        if (message.isBlank()) {\n            return\n        }\n\n        val snackbar = make(\n            parentView,\n            message,\n            duration,\n            anchorView,\n            action,\n            displayDismissButton,\n            onDismiss\n        )\n\n        show(snackbar)\n    }\n\n    fun show(snackbar: Snackbar) {\n        if (snackbar != lastSnackbar) {\n            lastSnackbar?.dismiss()\n        }\n\n        snackbar.show()\n\n        lastSnackbar = snackbar\n        val message = snackbar.view.findViewById<TextView>(R.id.snackbar_text).text.toString()\n        alertStore.register(message)\n    }\n\n    fun make(\n        parentView: View,\n        message: String,\n        duration: Int,\n        anchorView: View? = null,\n        action: Action? = null,\n        displayDismissButton: Boolean = false,\n        onDismiss: () -> Unit = {}\n    ): Snackbar = Snackbar.make(parentView, message.trim(), duration).apply {\n        val textView =\n            view.findViewById<TextView>(R.id.snackbar_text)\n        textView.isSingleLine = false\n\n        if (anchorView?.visibility != View.GONE) {\n            this.anchorView = anchorView\n        }\n\n        if (displayDismissButton) {\n            view.findViewById<Button>(R.id.snackbar_action).let {\n                val dismissButton = ImageView(view.context).apply {\n                    setImageResource(org.odk.collect.androidshared.R.drawable.ic_close_24)\n                    setOnClickListener {\n                        dismiss()\n                    }\n                    contentDescription =\n                        context.getString(org.odk.collect.strings.R.string.close_snackbar)\n                }\n\n                val params = LinearLayout.LayoutParams(\n                    LinearLayout.LayoutParams.WRAP_CONTENT,\n                    LinearLayout.LayoutParams.MATCH_PARENT\n                )\n                params.setMargins(16, 0, 0, 0)\n\n                (it.parent as ViewGroup).addView(dismissButton, params)\n            }\n        }\n\n        if (action != null) {\n            setAction(action.text) {\n                action.beforeDismiss.invoke()\n                dismiss()\n            }\n        }\n    }.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {\n        override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {\n            super.onDismissed(transientBottomBar, event)\n            onDismiss()\n            lastSnackbar = null\n        }\n    })\n\n    data class SnackbarDetails @JvmOverloads constructor(\n        val text: String,\n        val action: Action? = null\n    )\n\n    data class Action(val text: String, val beforeDismiss: () -> Unit = {})\n\n    abstract class SnackbarPresenterObserver<T : Any?>(private val parentView: View) :\n        Observer<Consumable<T>?> {\n\n        abstract fun getSnackbarDetails(value: T): SnackbarDetails\n\n        override fun onChanged(consumable: Consumable<T>?) {\n            if (consumable != null && !consumable.isConsumed()) {\n                showSnackbar(\n                    parentView,\n                    getSnackbarDetails(consumable.value).text,\n                    DURATION_LONG,\n                    action = getSnackbarDetails(consumable.value).action\n                )\n                consumable.consume()\n            }\n        }\n    }\n\n    fun <T : Any?> LiveData<Consumable<T>?>.showSnackbar(lifecycleOwner: LifecycleOwner, parentView: View, details: (T) -> SnackbarDetails) {\n        observe(lifecycleOwner, object : SnackbarPresenterObserver<T>(parentView) {\n            override fun getSnackbarDetails(value: T): SnackbarDetails {\n                return details(value)\n            }\n        })\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/ToastUtils.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.app.Activity\nimport android.app.Application\nimport android.os.Build\nimport android.view.Gravity\nimport android.view.ViewGroup\nimport android.widget.TextView\nimport android.widget.Toast\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.strings.localization.getLocalizedString\n\n/**\n * Convenience wrapper around Android's [Toast] API.\n */\nobject ToastUtils {\n\n    @JvmStatic\n    val alertStore = AlertStore()\n\n    private lateinit var lastToast: Toast\n    private lateinit var application: Application\n\n    @JvmStatic\n    fun showShortToast(message: String) {\n        showToast(message)\n    }\n\n    @JvmStatic\n    fun showShortToast(messageResource: Int) {\n        showToast(\n            application.getLocalizedString(messageResource)\n        )\n    }\n\n    @JvmStatic\n    fun showLongToast(message: String) {\n        showToast(message, Toast.LENGTH_LONG)\n    }\n\n    @JvmStatic\n    fun showLongToast(messageResource: Int) {\n        showToast(\n            application.getLocalizedString(messageResource),\n            Toast.LENGTH_LONG\n        )\n    }\n\n    @JvmStatic\n    @Deprecated(\"Toast position cannot be customized on API 30 and above. A dialog is shown instead for this API levels.\")\n    fun showShortToastInMiddle(activity: Activity, message: String) {\n        showToastInMiddle(activity, message)\n    }\n\n    @JvmStatic\n    fun setApplication(application: Application) {\n        this.application = application\n    }\n\n    private fun showToast(\n        message: String,\n        duration: Int = Toast.LENGTH_SHORT\n    ) {\n        hideLastToast()\n        lastToast = Toast.makeText(application, message, duration)\n        lastToast.show()\n\n        alertStore.register(message)\n    }\n\n    private fun showToastInMiddle(\n        activity: Activity,\n        message: String,\n        duration: Int = Toast.LENGTH_SHORT\n    ) {\n        if (Build.VERSION.SDK_INT < 30) {\n            hideLastToast()\n            lastToast = Toast.makeText(activity.applicationContext, message, duration)\n            try {\n                val group = lastToast.view as ViewGroup?\n                val messageTextView = group!!.getChildAt(0) as TextView\n                messageTextView.textSize = 21f\n                messageTextView.gravity = Gravity.CENTER\n            } catch (ignored: Exception) {\n                // ignored\n            }\n            lastToast.setGravity(Gravity.CENTER, 0, 0)\n            lastToast.show()\n\n            alertStore.register(message)\n        } else {\n            MaterialAlertDialogBuilder(activity)\n                .setMessage(message)\n                .setPositiveButton(org.odk.collect.strings.R.string.ok, null)\n                .create()\n                .show()\n        }\n    }\n\n    private fun hideLastToast() {\n        if (ToastUtils::lastToast.isInitialized) {\n            lastToast.cancel()\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/compose/Margins.kt",
    "content": "package org.odk.collect.androidshared.ui.compose\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.unit.Dp\nimport org.odk.collect.androidshared.R.dimen\n\n@Composable\nfun marginStandard(): Dp {\n    return dimensionResource(id = dimen.margin_standard)\n}\n\n@Composable\nfun marginSmall(): Dp {\n    return dimensionResource(id = dimen.margin_small)\n}\n\n@Composable\nfun marginExtraSmall(): Dp {\n    return dimensionResource(id = dimen.margin_extra_small)\n}"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/DoubleClickSafeMaterialButton.kt",
    "content": "package org.odk.collect.androidshared.ui.multiclicksafe\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.google.android.material.button.MaterialButton\n\nclass DoubleClickSafeMaterialButton @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0\n) : MaterialButton(context, attrs, defStyleAttr) {\n\n    override fun performClick(): Boolean {\n        return allowClick() && super.performClick()\n    }\n\n    /**\n     * Use [MultiClickGuard] with a scope unique to this object (class name + hash).\n     */\n    private fun allowClick(): Boolean {\n        val scope = javaClass.name + hashCode()\n        return MultiClickGuard.allowClick(scope)\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/MultiClickGuard.kt",
    "content": "package org.odk.collect.androidshared.ui.multiclicksafe\n\nimport android.os.SystemClock\n\nobject MultiClickGuard {\n    @JvmField\n    var test = false\n\n    private var lastClickTime: Long = 0\n    private var lastClickName: String = javaClass.name\n\n    @JvmStatic\n    fun allowClickFast(className: String = javaClass.name): Boolean {\n        return allowClick(className, 500)\n    }\n\n    /**\n     * Debounce multiple clicks within the same scope\n     *\n     * @param scope If not provided, the Java class name of the element\n     * is used. However, this approach is imperfect, as elements on the same screen might belong to\n     * different classes. Consequently, clicks on these elements are treated as interactions occurring\n     * on two distinct screens, not protecting from rapid clicking.\n     */\n    @JvmStatic\n    @JvmOverloads\n    fun allowClick(scope: String = javaClass.name, clickDebounceMs: Long = 1000): Boolean {\n        if (test) {\n            return true\n        }\n        val elapsedRealtime = SystemClock.elapsedRealtime()\n        val isSameClass = scope == lastClickName\n        val isBeyondThreshold = elapsedRealtime - lastClickTime > clickDebounceMs\n        val isBeyondTestThreshold =\n            lastClickTime == 0L || lastClickTime == elapsedRealtime // just for tests\n\n        val allowClick = !isSameClass || isBeyondThreshold || isBeyondTestThreshold\n\n        if (allowClick) {\n            lastClickTime = elapsedRealtime\n            lastClickName = scope\n        }\n        return allowClick\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/MultiClickSafeMaterialButton.kt",
    "content": "package org.odk.collect.androidshared.ui.multiclicksafe\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport androidx.core.content.withStyledAttributes\nimport com.google.android.material.button.MaterialButton\nimport org.odk.collect.androidshared.R\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard.allowClick\n\nopen class MultiClickSafeMaterialButton @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0\n) : MaterialButton(context, attrs, defStyleAttr) {\n    private lateinit var screenName: String\n\n    init {\n        context.withStyledAttributes(attrs, R.styleable.MultiClickSafeMaterialButton) {\n            screenName = getString(R.styleable.MultiClickSafeMaterialButton_screenName) ?: javaClass.name\n        }\n    }\n\n    override fun performClick(): Boolean {\n        return allowClick(screenName) && super.performClick()\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/MultiClickSafeTextInputEditText.kt",
    "content": "package org.odk.collect.androidshared.ui.multiclicksafe\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.google.android.material.textfield.TextInputEditText\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard.allowClick\n\nclass MultiClickSafeTextInputEditText : TextInputEditText {\n    constructor(context: Context) : super(context)\n\n    constructor(context: Context, attrs: AttributeSet?) : super(\n        context,\n        attrs\n    )\n\n    override fun performClick(): Boolean {\n        return allowClick() && super.performClick()\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/ui/multiclicksafe/MultiClickSaveOnClickListener.kt",
    "content": "package org.odk.collect.androidshared.ui.multiclicksafe\n\nimport android.view.View\n\nabstract class MultiClickSafeOnClickListener : View.OnClickListener {\n\n    abstract fun onSafeClick(v: View)\n\n    override fun onClick(v: View) {\n        if (MultiClickGuard.allowClick()) {\n            onSafeClick(v)\n        }\n    }\n}\n\nfun View.setMultiClickSafeOnClickListener(listener: (View) -> Unit) {\n    setOnClickListener(object : MultiClickSafeOnClickListener() {\n        override fun onSafeClick(v: View) {\n            listener(v)\n        }\n    })\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/AppBarUtils.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport android.app.Activity\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.appcompat.widget.Toolbar\n\nobject AppBarUtils {\n\n    @JvmStatic\n    fun setupAppBarLayout(activity: Activity, title: CharSequence) {\n        val toolbar = activity.findViewById<Toolbar>(org.odk.collect.androidshared.R.id.toolbar)\n        if (toolbar != null && activity is AppCompatActivity) {\n            toolbar.title = title\n            activity.setSupportActionBar(toolbar)\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/ColorUtils.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport androidx.annotation.ColorInt\nimport androidx.core.graphics.ColorUtils\nimport androidx.core.graphics.toColorInt\n\n@ColorInt\nfun String.sanitizeToColorInt() = try {\n    var sanitizedColor = if (this.startsWith(\"#\")) {\n        this\n    } else {\n        \"#$this\"\n    }\n\n    if (sanitizedColor.length == 4) {\n        sanitizedColor = shorthandToLonghandHexColor(sanitizedColor)\n    }\n\n    sanitizedColor.toColorInt()\n} catch (e: IllegalArgumentException) {\n    null\n}\n\nfun Int.opaque(): Int {\n    return ColorUtils.setAlphaComponent(this, 100)\n}\n\nprivate fun shorthandToLonghandHexColor(shorthandColor: String): String {\n    return shorthandColor.substring(1).fold(\"#\") { accum, char ->\n        \"$accum$char$char\"\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/CompressionUtils.kt",
    "content": "/*\n * Copyright (C) 2017 Shobhit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.androidshared.utils\n\nimport android.util.Base64\nimport java.io.ByteArrayOutputStream\nimport java.io.IOException\nimport java.lang.IllegalArgumentException\nimport java.util.zip.DataFormatException\nimport java.util.zip.Deflater\nimport java.util.zip.Inflater\n\nobject CompressionUtils {\n\n    @Throws(IOException::class)\n    fun compress(data: String?): String {\n        if (data == null || data.isEmpty()) {\n            return \"\"\n        }\n\n        // Encode string into bytes\n        val input = data.toByteArray(charset(\"UTF-8\"))\n        val deflater = Deflater()\n        deflater.setInput(input)\n\n        // Compress the bytes\n        val outputStream = ByteArrayOutputStream(data.length)\n        deflater.finish()\n        val buffer = ByteArray(1024)\n        while (!deflater.finished()) {\n            val count = deflater.deflate(buffer) // returns the generated code... index\n            outputStream.write(buffer, 0, count)\n        }\n        outputStream.close()\n        val output = outputStream.toByteArray()\n\n        // Encode to base64\n        return Base64.encodeToString(output, Base64.NO_WRAP)\n    }\n\n    @Throws(\n        IOException::class,\n        DataFormatException::class,\n        IllegalArgumentException::class\n    )\n    fun decompress(compressedString: String?): String {\n        if (compressedString == null || compressedString.isEmpty()) {\n            return \"\"\n        }\n\n        // Decode from base64\n        val output = Base64.decode(compressedString, Base64.NO_WRAP)\n        val inflater = Inflater()\n        inflater.setInput(output)\n\n        // Decompresses the bytes\n        val outputStream = ByteArrayOutputStream(output.size)\n        val buffer = ByteArray(1024)\n        while (!inflater.finished()) {\n            val count = inflater.inflate(buffer)\n            outputStream.write(buffer, 0, count)\n        }\n        outputStream.close()\n        inflater.end()\n        val result = outputStream.toByteArray()\n\n        // Decode the bytes into a String\n        return String(result, charset(\"UTF-8\"))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/FileExt.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.media.MediaMetadataRetriever\nimport androidx.core.net.toUri\nimport timber.log.Timber\nimport java.io.File\n\nfun File.getVideoThumbnail(context: Context): Bitmap? {\n    val retriever = MediaMetadataRetriever()\n    return try {\n        retriever.setDataSource(context, this.toUri())\n        retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)\n    } catch (e: Exception) {\n        Timber.w(e)\n        null\n    } finally {\n        retriever.release()\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/InMemUniqueIdGenerator.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nclass InMemUniqueIdGenerator() : UniqueIdGenerator {\n\n    private val ids = mutableMapOf<String, Int>()\n    private var next = AtomicInteger(1)\n\n    override fun getInt(identifier: String): Int {\n        return ids.getOrPut(identifier) { next.getAndIncrement() }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/PathUtils.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport org.odk.collect.shared.files.FileExt.sanitizedCanonicalPath\nimport timber.log.Timber\nimport java.io.File\nimport java.io.IOException\n\nobject PathUtils {\n    @JvmStatic\n    fun getAbsoluteFilePath(dirPath: String, filePath: String): String {\n        val absoluteFilePath =\n            if (filePath.startsWith(dirPath)) filePath else dirPath + File.separator + filePath\n\n        val absoluteFile = File(absoluteFilePath)\n        return try {\n            if (absoluteFile.sanitizedCanonicalPath().startsWith(File(dirPath).sanitizedCanonicalPath())) {\n                absoluteFilePath\n            } else {\n                throw SecurityException(\"Contact support@getodk.org. Attempt to access file outside of Collect directory: $absoluteFilePath\")\n            }\n        } catch (e: IOException) {\n            Timber.e(\n                \"Failed attempt to access canonicalPath:\\n\" +\n                    \"dirPath: $dirPath\\n\" +\n                    \"filePath: $filePath\\n\" +\n                    \"absoluteFilePath: $absoluteFilePath\\n\" +\n                    \"absoluteFilePath exists: ${absoluteFile.exists()}\\n\"\n            )\n            absoluteFilePath\n        }\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/PreferenceFragmentCompatUtils.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceFragmentCompat\n\nfun <T : Preference> PreferenceFragmentCompat.getPreference(key: String): T {\n    return this.findPreference(key)!!\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/ScreenUtils.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.androidshared.utils;\n\nimport android.content.Context;\nimport android.content.res.Configuration;\nimport android.util.DisplayMetrics;\n\npublic class ScreenUtils {\n\n    private final Context context;\n\n    public ScreenUtils(Context context) {\n        this.context = context;\n    }\n\n    public static int getScreenWidth(Context context) {\n        return getDisplayMetrics(context).widthPixels;\n    }\n\n    public static int getScreenHeight(Context context) {\n        return getDisplayMetrics(context).heightPixels;\n    }\n\n    public static float xdpi(Context context) {\n        return getDisplayMetrics(context).xdpi;\n    }\n\n    public static float ydpi(Context context) {\n        return getDisplayMetrics(context).ydpi;\n    }\n\n    private static DisplayMetrics getDisplayMetrics(Context context) {\n        return context.getResources().getDisplayMetrics();\n    }\n\n    public int getScreenSizeConfiguration() {\n        return context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/SettingsUniqueIdGenerator.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport org.odk.collect.shared.settings.Settings\n\nclass SettingsUniqueIdGenerator(private val settings: Settings) : UniqueIdGenerator {\n\n    override fun getInt(identifier: String): Int {\n        return synchronized(this) {\n            val identifierKey = identifierKey(identifier)\n\n            if (settings.contains(identifierKey)) {\n                settings.getInt(identifierKey)\n            } else {\n                val next = getNextInt()\n                settings.save(identifierKey, next)\n                next\n            }\n        }\n    }\n\n    private fun getNextInt(): Int {\n        val next = if (settings.contains(NEXT_ID_KEY)) {\n            settings.getInt(NEXT_ID_KEY)\n        } else {\n            1\n        }\n\n        settings.save(NEXT_ID_KEY, next + 1)\n        return next\n    }\n\n    companion object {\n        private const val KEY_PREFIX = \"uniqueId\"\n        private const val NEXT_ID_KEY = \"$KEY_PREFIX:next\"\n        private fun identifierKey(identifier: String) = \"$KEY_PREFIX:identifier:$identifier\"\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/UniqueIdGenerator.kt",
    "content": "package org.odk.collect.androidshared.utils\n\ninterface UniqueIdGenerator {\n    fun getInt(identifier: String): Int\n}\n"
  },
  {
    "path": "androidshared/src/main/java/org/odk/collect/androidshared/utils/Validator.kt",
    "content": "/*\n * Copyright 2016 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.androidshared.utils\n\nimport android.util.Patterns\nimport android.webkit.URLUtil\nimport java.util.regex.Pattern\n\nobject Validator {\n    /*\n    There are lots of ways to validate email addresses and it's hard to find one perfect.\n    That's why we use here a very simple approach just to confirm that passed string contains:\n        -any number of characters before @ (at least one)\n        -one @ char\n        -any number of characters after @ (at least one)\n     */\n    @JvmStatic\n    fun isEmailAddressValid(emailAddress: String): Boolean {\n        return Pattern\n            .compile(\".+@.+\")\n            .matcher(emailAddress)\n            .matches()\n    }\n\n    @JvmStatic\n    fun isUrlValid(url: String): Boolean {\n        return URLUtil.isValidUrl(url) && Patterns.WEB_URL.matcher(url).matches()\n    }\n}\n"
  },
  {
    "path": "androidshared/src/main/res/color/color_error_button_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"?colorError\" android:state_enabled=\"true\"/>\n    <item android:alpha=\"@dimen/material_emphasis_disabled_background\" android:color=\"?colorOnSurface\"/>\n</selector>"
  },
  {
    "path": "androidshared/src/main/res/color/color_on_primary_low_emphasis.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!--\nProvides a medium emphasis version of the `colorOnPrimary` Material Theming attribute (found\nhere: https://m3.material.io/styles/color/the-color-system/tokens#7fd4440e-986d-443f-8b3a-4933bff16646).\n-->\n\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:alpha=\"@dimen/low_emphasis\" android:color=\"?attr/colorOnPrimary\" />\n</selector>\n"
  },
  {
    "path": "androidshared/src/main/res/color/color_on_surface_high_emphasis.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!--\nProvides a medium emphasis version of the `colorOnSurface` Material Theming attribute (found\nhere: https://material.io/develop/android/theming/typography/). Material Themes suggest using\ndifferent emphasis/transparency values on text but Android doesn't have a way to set this on a\n`TextView` itself.\n-->\n\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:alpha=\"@dimen/high_emphasis\" android:color=\"?attr/colorOnSurface\" />\n</selector>\n"
  },
  {
    "path": "androidshared/src/main/res/color/color_on_surface_low_emphasis.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!--\nProvides a medium emphasis version of the `colorOnSurface` Material Theming attribute (found\nhere: https://material.io/develop/android/theming/typography/). Material Themes suggest using\ndifferent emphasis/transparency values on text but Android doesn't have a way to set this on a\n`TextView` itself.\n-->\n\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:alpha=\"@dimen/low_emphasis\" android:color=\"?attr/colorOnSurface\" />\n</selector>\n"
  },
  {
    "path": "androidshared/src/main/res/color/color_on_surface_medium_emphasis.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!--\nProvides a medium emphasis version of the `colorOnSurface` Material Theming attribute (found\nhere: https://material.io/develop/android/theming/typography/). Material Themes suggest using\ndifferent emphasis/transparency values on text but Android doesn't have a way to set this on a\n`TextView` itself.\n-->\n\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:alpha=\"@dimen/medium_emphasis\" android:color=\"?attr/colorOnSurface\" />\n</selector>\n"
  },
  {
    "path": "androidshared/src/main/res/color/color_primary_low_emphasis.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!--\nProvides a medium emphasis version of the `colorPrimary` Material Theming attribute (found\nhere: https://material.io/develop/android/theming/typography/). Material Themes suggest using\ndifferent emphasis/transparency values on text but Android doesn't have a way to set this on a\n`TextView` itself.\n-->\n\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:alpha=\"@dimen/low_emphasis\" android:color=\"?attr/colorPrimary\" />\n</selector>\n"
  },
  {
    "path": "androidshared/src/main/res/drawable/color_circle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\" />"
  },
  {
    "path": "androidshared/src/main/res/drawable/ic_close_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorSurface\">\n    <path\n        android:fillColor=\"?colorSurface\"\n        android:pathData=\"M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z\"/>\n</vector>\n"
  },
  {
    "path": "androidshared/src/main/res/drawable/list_item_divider.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <!-- Material Design reference: https://material.io/design/components/dividers.html#specs -->\n\n    <item>\n        <shape android:shape=\"rectangle\">\n            <size android:height=\"1dp\" />\n            <solid android:color=\"@color/color_on_surface_low_emphasis\"/>\n        </shape>\n    </item>\n\n</layer-list>\n"
  },
  {
    "path": "androidshared/src/main/res/drawable/radio_button_inset.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<inset xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:drawable=\"?android:attr/listChoiceIndicatorSingle\"\n    android:insetLeft=\"11dp\"\n    android:insetRight=\"@dimen/margin_standard\"/>"
  },
  {
    "path": "androidshared/src/main/res/drawable/shadow_up.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <shape\n            android:dither=\"true\"\n            android:shape=\"rectangle\">\n            <gradient\n                android:angle=\"270\"\n                android:endColor=\"#22000000\"\n                android:startColor=\"#00000000\"/>\n\n            <size android:height=\"10dp\"/>\n        </shape>\n    </item>\n</layer-list>\n\n"
  },
  {
    "path": "androidshared/src/main/res/layout/app_bar_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     When scrolling, the top app bar fills with a contrasting color to create a visual separation.\n     This works automatically if your scrolling view (e.g., `RecyclerView`, `ListView`) is placed directly\n     beneath the `AppBarLayout`. However, if the scrolling view is nested within another view\n     (such as a `ConstraintLayout`, which is common in this app), you need to help the app bar determine\n     whether it should lift by setting `app:liftOnScrollTargetViewId` to the ID of the scrolling view.\n     Since this `AppBarLayout` is used throughout the app with various scrolling views, it’s best to\n     use a shared ID like `scrollable_container`.\n     If the scrollable view is added programmatically or it is displayed in a `ViewPager` with a\n     shared id, it may not work as expected anyway, and `app:liftOnScrollTargetViewId` might\n     need to be updated programmatically after adding such a view.\n     The `ODKView` and its `odk_view_container` or `DeleteFormsActivity` are good examples of this scenario.\n-->\n<com.google.android.material.appbar.AppBarLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/appBarLayout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    app:liftOnScrollTargetViewId=\"@+id/scrollable_container\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            tools:title=\"Title\" />\n\n        <org.odk.collect.androidshared.ui.ObviousProgressBar\n            android:id=\"@+id/progressBar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"/>\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</com.google.android.material.appbar.AppBarLayout>\n"
  },
  {
    "path": "androidshared/src/main/res/layout/color_picker_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_height=\"match_parent\"\n    android:layout_width=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"@dimen/margin_standard\">\n\n        <!-- 1st row -->\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color1\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color1\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color2\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color1\"\n            app:layout_constraintTop_toTopOf=\"parent\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color2\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color3\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color2\"\n            app:layout_constraintTop_toTopOf=\"parent\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color3\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color4\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color3\"\n            app:layout_constraintTop_toTopOf=\"parent\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color4\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color5\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color4\"\n            app:layout_constraintTop_toTopOf=\"parent\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color5\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <!-- 2nd row -->\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color6\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/color1\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color6\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color7\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color6\"\n            app:layout_constraintTop_toBottomOf=\"@id/color2\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color7\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color8\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color7\"\n            app:layout_constraintTop_toBottomOf=\"@id/color3\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color8\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color9\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color8\"\n            app:layout_constraintTop_toBottomOf=\"@id/color4\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color9\"/>\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color10\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color9\"\n            app:layout_constraintTop_toBottomOf=\"@id/color5\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color10\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <!-- 3rd row -->\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color11\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/color6\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color11\"/>\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color12\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color11\"\n            app:layout_constraintTop_toBottomOf=\"@id/color7\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color12\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color13\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color12\"\n            app:layout_constraintTop_toBottomOf=\"@id/color8\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color13\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color14\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color13\"\n            app:layout_constraintTop_toBottomOf=\"@id/color9\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color14\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/color15\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:background=\"?attr/selectableItemBackgroundBorderless\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toEndOf=\"@id/color14\"\n            app:layout_constraintTop_toBottomOf=\"@id/color10\" >\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"@drawable/color_circle\"\n                android:backgroundTint=\"@color/color15\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/current_color_container\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            android:layout_marginTop=\"@dimen/margin_extra_large\"\n            app:layout_constraintWidth_percent=\"0.2\"\n            app:layout_constraintDimensionRatio=\"w,1:1\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/color11\" >\n\n            <TextView\n                android:id=\"@+id/current_color\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:padding=\"@dimen/margin_extra_extra_small\"\n                android:background=\"@drawable/color_circle\"\n                android:textColor=\"@android:color/white\"\n                android:textAllCaps=\"true\"\n                android:gravity=\"center\"\n                app:autoSizeTextType=\"uniform\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <com.google.android.material.textview.MaterialTextView\n            style=\"?textAppearanceBody1\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/hex\"\n            app:layout_constraintBottom_toBottomOf=\"@+id/hex_color_layout\"\n            app:layout_constraintEnd_toStartOf=\"@+id/hex_color_layout\"\n            app:layout_constraintTop_toTopOf=\"@+id/hex_color_layout\" />\n\n        <FrameLayout\n            android:id=\"@+id/hex_color_layout\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            android:padding=\"@dimen/margin_extra_small\"\n            app:layout_constraintTop_toTopOf=\"@+id/current_color_container\"\n            app:layout_constraintBottom_toBottomOf=\"@+id/current_color_container\"\n            app:layout_constraintStart_toStartOf=\"@+id/color13\"\n            app:layout_constraintEnd_toEndOf=\"parent\">\n\n            <com.google.android.material.textfield.TextInputLayout\n                style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                app:hintEnabled=\"false\"\n                app:prefixText=\"@string/hash\"\n                app:prefixTextAppearance=\"?textAppearanceBody1\"\n                android:descendantFocusability=\"beforeDescendants\"\n                android:focusableInTouchMode=\"true\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:id=\"@+id/hex_color\"\n                    style=\"?textAppearanceBody1\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:contentDescription=\"@string/hex_color\"\n                    android:padding=\"0dp\"\n                    android:maxLength=\"6\" />\n            </com.google.android.material.textfield.TextInputLayout>\n        </FrameLayout>\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</ScrollView>\n"
  },
  {
    "path": "androidshared/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"MultiClickSafeMaterialButton\">\n        <attr name=\"screenName\" format=\"string\" />\n    </declare-styleable>\n</resources>\n"
  },
  {
    "path": "androidshared/src/main/res/values/color_picker_dialog_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Color picker palette -->\n    <color name=\"color1\">#EA4633</color>\n    <color name=\"color2\">#E9527E</color>\n    <color name=\"color3\">#9F50B0</color>\n    <color name=\"color4\">#3F51B5</color>\n    <color name=\"color5\">#2296F3</color>\n    <color name=\"color6\">#53BDD4</color>\n    <color name=\"color7\">#489789</color>\n    <color name=\"color8\">#5DAF50</color>\n    <color name=\"color9\">#8BC34A</color>\n    <color name=\"color10\">#CDDC39</color>\n    <color name=\"color11\">#FFEB3B</color>\n    <color name=\"color12\">#F9C028</color>\n    <color name=\"color13\">#F2972C</color>\n    <color name=\"color14\">#795548</color>\n    <color name=\"color15\">#9E9E9E</color>\n</resources>\n"
  },
  {
    "path": "androidshared/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- \"Standard\" set of margin sizes for Android (pre and post Material) -->\n    <dimen name=\"margin_extra_extra_small\">4dp</dimen>\n    <dimen name=\"margin_extra_small\">8dp</dimen>\n    <dimen name=\"margin_small\">12dp</dimen>\n    <dimen name=\"margin_standard\">16dp</dimen>\n    <dimen name=\"margin_large\">24dp</dimen>\n    <dimen name=\"margin_extra_large\">32dp</dimen>\n    <dimen name=\"margin_extra_extra_large\">48dp</dimen>\n\n    <dimen name=\"max_content_width\">640dp</dimen>\n\n    <dimen name=\"low_emphasis\">0.12</dimen>\n    <dimen name=\"medium_emphasis\">0.6</dimen>\n    <dimen name=\"high_emphasis\">0.87</dimen>\n</resources>\n"
  },
  {
    "path": "androidshared/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"Widget.AndroidShared.ButtonBar\" parent=\"\">\n        <item name=\"android:background\">?colorSurface</item>\n        <item name=\"android:elevation\">4dp</item>\n        <item name=\"android:paddingTop\">@dimen/margin_extra_small</item>\n        <item name=\"android:paddingBottom\">@dimen/margin_extra_small</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/async/TrackableWorkerTest.kt",
    "content": "package org.odk.collect.androidshared.async\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.testshared.FakeScheduler\n\n@RunWith(AndroidJUnit4::class)\nclass TrackableWorkerTest {\n    private val scheduler = FakeScheduler()\n    private val trackableWorker = TrackableWorker(scheduler)\n\n    @Test\n    fun `TrackableWorker counts work in progress`() {\n        trackableWorker.immediate {}\n        trackableWorker.immediate {}\n\n        scheduler.runFirstBackground()\n        scheduler.runFirstForeground()\n        assertThat(trackableWorker.isWorking.value, equalTo(true))\n\n        scheduler.runFirstBackground()\n        scheduler.runFirstForeground()\n        assertThat(trackableWorker.isWorking.value, equalTo(false))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/livedata/LiveDataUtilsTest.kt",
    "content": "package org.odk.collect.androidshared.livedata\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.lifecycle.MutableLiveData\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.odk.collect.androidshared.livedata.LiveDataExt.combine\nimport org.odk.collect.testshared.getOrAwaitValue\n\nclass LiveDataUtilsTest {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    @Test\n    fun `#combine has initial value from both LiveData instances`() {\n        val combined = MutableLiveData(\"one\").combine(MutableLiveData(\"two\"))\n        assertThat(combined.getOrAwaitValue(), equalTo(Pair(\"one\", \"two\")))\n    }\n\n    @Test\n    fun `#combine uses null values for uninitialized LiveData instances`() {\n        val combined = MutableLiveData<String>().combine(MutableLiveData<String>())\n        assertThat(combined.getOrAwaitValue(), equalTo(Pair(null, null)))\n    }\n\n    @Test\n    fun `#combine updates when either LiveData updates`() {\n        val one = MutableLiveData(\"one\")\n        val combined = one.combine(MutableLiveData(\"two\"))\n        one.value = \"one-updated\"\n\n        assertThat(combined.getOrAwaitValue(), equalTo(Pair(\"one-updated\", \"two\")))\n    }\n\n    @Test\n    fun `#combine skips emission when result is equal to previous`() {\n        val one = MutableLiveData(\"one\")\n        val combined = one.combine(MutableLiveData<String>())\n\n        var counter = 0\n        LiveDataUtils.observe(combined) {\n            counter++\n        }\n\n        one.value = \"one\"\n\n        assertThat(counter, equalTo(1))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/system/UriExtTest.kt",
    "content": "package org.odk.collect.androidshared.system\n\nimport android.app.Application\nimport androidx.core.net.toUri\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.shared.TempFiles\n\n@RunWith(AndroidJUnit4::class)\nclass UriExtTest {\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n\n    @Test\n    fun `copyToFile copies the source file to the target file`() {\n        val sourceFile = TempFiles.createTempFile().also {\n            it.writeText(\"blah\")\n        }\n        val sourceFileUri = sourceFile.toUri()\n        val targetFile = TempFiles.createTempFile()\n\n        sourceFileUri.copyToFile(context, targetFile)\n        assertThat(targetFile.readText(), equalTo(sourceFile.readText()))\n    }\n\n    @Test\n    fun `getFileExtension returns file extension`() {\n        val file = TempFiles.createTempFile(\".jpg\")\n        val fileUri = file.toUri()\n\n        assertThat(fileUri.getFileExtension(context), equalTo(\".jpg\"))\n    }\n\n    @Test\n    fun `getFileName returns file name`() {\n        val file = TempFiles.createTempFile()\n        val fileUri = file.toUri()\n\n        assertThat(fileUri.getFileName(context), equalTo(file.name))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/ui/ColorPickerDialogTest.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.graphics.Color\nimport android.graphics.drawable.GradientDrawable\nimport android.os.Bundle\nimport androidx.appcompat.app.AlertDialog\nimport androidx.fragment.app.testing.FragmentScenario\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.pressBack\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalToIgnoringCase\nimport org.hamcrest.Matchers.`is`\nimport org.hamcrest.Matchers.nullValue\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidshared.ColorPickerDialog\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.testshared.RobolectricHelpers\n\n@RunWith(AndroidJUnit4::class)\nclass ColorPickerDialogTest {\n\n    private val args = Bundle().apply {\n        putString(ColorPickerDialog.CURRENT_COLOR, \"#cccccc\")\n        putString(ColorPickerDialog.CURRENT_ICON, \"P\")\n    }\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Test\n    fun `The dialog should be dismissed after clicking on a device back button`() {\n        val scenario = launchFragment()\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            onView(isRoot()).perform(pressBack())\n            assertThat(it.dialog, `is`(nullValue()))\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on the cancel button`() {\n        val scenario = launchFragment()\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            (it.dialog!! as AlertDialog).getButton((AlertDialog.BUTTON_NEGATIVE)).performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(nullValue()))\n        }\n    }\n\n    @Test\n    fun `No more than six characters should be accepted as hex color`() {\n        val scenario = launchFragment()\n        scenario.onFragment {\n            assertThat(it.binding.hexColor.length(), `is`(6))\n        }\n    }\n\n    @Test\n    fun `Current color should be set properly after opening the dialog`() {\n        val scenario = launchFragment()\n        scenario.onFragment {\n            assertCurrentColor(it, \"cccccc\")\n        }\n    }\n\n    @Test\n    fun `Current icon should be set properly after opening the dialog`() {\n        val scenario = launchFragment()\n        scenario.onFragment {\n            assertThat(it.binding.currentColor.text.toString(), equalToIgnoringCase(\"P\"))\n        }\n    }\n\n    @Test\n    fun `Selected color should be remembered after dialog recreation`() {\n        val scenario = launchFragment()\n        scenario.onFragment {\n            it.binding.color5.performClick()\n            assertCurrentColor(it, \"2296F3\")\n\n            scenario.recreate()\n\n            assertCurrentColor(it, \"2296F3\")\n        }\n    }\n\n    @Test\n    fun `Selecting any color should update the current color`() {\n        val scenario = launchFragment()\n        scenario.onFragment {\n            it.binding.color1.performClick()\n            assertCurrentColor(it, \"EA4633\")\n\n            it.binding.color2.performClick()\n            assertCurrentColor(it, \"E9527E\")\n\n            it.binding.color3.performClick()\n            assertCurrentColor(it, \"9F50B0\")\n\n            it.binding.color4.performClick()\n            assertCurrentColor(it, \"3F51B5\")\n\n            it.binding.color5.performClick()\n            assertCurrentColor(it, \"2296F3\")\n\n            it.binding.color6.performClick()\n            assertCurrentColor(it, \"53BDD4\")\n\n            it.binding.color7.performClick()\n            assertCurrentColor(it, \"489789\")\n\n            it.binding.color8.performClick()\n            assertCurrentColor(it, \"5DAF50\")\n\n            it.binding.color9.performClick()\n            assertCurrentColor(it, \"8BC34A\")\n\n            it.binding.color10.performClick()\n            assertCurrentColor(it, \"CDDC39\")\n\n            it.binding.color11.performClick()\n            assertCurrentColor(it, \"FFEB3B\")\n\n            it.binding.color12.performClick()\n            assertCurrentColor(it, \"F9C028\")\n\n            it.binding.color13.performClick()\n            assertCurrentColor(it, \"F2972C\")\n\n            it.binding.color14.performClick()\n            assertCurrentColor(it, \"795548\")\n\n            it.binding.color15.performClick()\n            assertCurrentColor(it, \"9E9E9E\")\n        }\n    }\n\n    private fun launchFragment(): FragmentScenario<ColorPickerDialog> {\n        return launcherRule.launch(ColorPickerDialog::class.java, args)\n    }\n\n    private fun assertCurrentColor(fragment: ColorPickerDialog, color: String) {\n        assertThat(fragment.binding.hexColor.text.toString(), equalToIgnoringCase(color))\n\n        val currentColor =\n            (fragment.binding.currentColor.background as GradientDrawable).color!!.defaultColor\n\n        assertThat(currentColor, `is`(Color.parseColor(\"#$color\")))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/ui/OneSignTextWatcherTest.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport android.widget.EditText\nimport androidx.test.platform.app.InstrumentationRegistry\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.robolectric.RobolectricTestRunner\n\n@RunWith(RobolectricTestRunner::class)\nclass OneSignTextWatcherTest {\n\n    lateinit var oneSignTextWatcher: OneSignTextWatcher\n    lateinit var editText: EditText\n\n    @Before\n    fun setup() {\n        editText = EditText(InstrumentationRegistry.getInstrumentation().targetContext)\n        oneSignTextWatcher = OneSignTextWatcher(editText)\n        editText.addTextChangedListener(oneSignTextWatcher)\n    }\n\n    @Test\n    fun `Longer strings should not be accepted`() {\n        editText.setText(\"1\")\n        editText.setText(\"12\")\n        assertThat(editText.text.toString(), `is`(\"1\"))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/ui/ReturnToAppActivityTest.kt",
    "content": "package org.odk.collect.androidshared.ui\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.test.ext.junit.rules.ActivityScenarioRule\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\n@RunWith(AndroidJUnit4::class)\nclass ReturnToAppActivityTest {\n\n    @get:Rule\n    val rule = ActivityScenarioRule(ReturnToAppActivity::class.java)\n\n    @Test\n    fun finishesImmediately() {\n        assertThat(rule.scenario.state, equalTo(Lifecycle.State.DESTROYED))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/utils/ColorUtilsTest.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\n@RunWith(AndroidJUnit4::class)\nclass ColorUtilsTest {\n    @Test\n    fun `return null when color is empty`() {\n        assertThat(\"\".sanitizeToColorInt(), equalTo(null))\n    }\n\n    @Test\n    fun `return null when color is blank`() {\n        assertThat(\" \".sanitizeToColorInt(), equalTo(null))\n    }\n\n    @Test\n    fun `return null when color is invalid`() {\n        assertThat(\"qwerty\".sanitizeToColorInt(), equalTo(null))\n    }\n\n    @Test\n    fun `return color int for valid hex color with # prefix`() {\n        assertThat(\"#aaccee\".sanitizeToColorInt(), equalTo(-5583634))\n    }\n\n    @Test\n    fun `return color int for valid hex color without # prefix`() {\n        assertThat(\"aaccee\".sanitizeToColorInt(), equalTo(-5583634))\n    }\n\n    @Test\n    fun `return color int for valid shorthand hex color with # prefix`() {\n        assertThat(\"#ace\".sanitizeToColorInt(), equalTo(-5583634))\n    }\n\n    @Test\n    fun `return color int for valid shorthand hex color without # prefix`() {\n        assertThat(\"ace\".sanitizeToColorInt(), equalTo(-5583634))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/utils/CompressionUtilsTest.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Assert.assertTrue\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport java.util.zip.DataFormatException\n\n@RunWith(AndroidJUnit4::class)\nclass CompressionUtilsTest {\n    @Test\n    fun `compressed long text should be shorter than the original one`() {\n        val text = \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \" +\n            \"incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis \" +\n            \"nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\"\n\n        val compressedText = CompressionUtils.compress(text)\n\n        assertTrue(compressedText.length < text.length)\n    }\n\n    @Test\n    fun `compressing an empty string returns an empty string`() {\n        val nullText = \"\"\n        val compressedText = CompressionUtils.compress(nullText)\n\n        assertThat(nullText, equalTo(compressedText))\n    }\n\n    @Test\n    fun `text after compressing and decompressing should be unchanged`() {\n        val text = \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor\"\n\n        val compressedText = CompressionUtils.compress(text)\n        val decompressedText = CompressionUtils.decompress(compressedText)\n\n        assertThat(text, equalTo(decompressedText))\n    }\n\n    @Test\n    fun `decompressing an empty string returns an empty string`() {\n        val emptyText = \"\"\n        val decompressedText = CompressionUtils.decompress(emptyText)\n\n        assertThat(emptyText, equalTo(decompressedText))\n    }\n\n    @Test(expected = DataFormatException::class)\n    fun `decompressing a not compressed text throws an exception`() {\n        val input = \"Decoding this will raise an error\"\n        CompressionUtils.decompress(input)\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/utils/DialogFragmentUtilsTest.java",
    "content": "package org.odk.collect.androidshared.utils;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\n\nimport android.os.Bundle;\n\nimport androidx.fragment.app.DialogFragment;\nimport androidx.fragment.app.Fragment;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\nimport org.robolectric.Robolectric;\nimport org.robolectric.android.controller.ActivityController;\n\n@RunWith(AndroidJUnit4.class)\npublic class DialogFragmentUtilsTest {\n\n    @Test\n    public void showIfNotShowing_onlyEverOpensOneDialog() {\n        FragmentActivity activity = Robolectric.setupActivity(FragmentActivity.class);\n        FragmentManager fragmentManager = activity.getSupportFragmentManager();\n\n        DialogFragmentUtils.showIfNotShowing(DialogFragment.class, fragmentManager);\n        assertThat(fragmentManager.getFragments().size(), equalTo(1));\n        Fragment dialog1 = fragmentManager.getFragments().get(0);\n\n        DialogFragmentUtils.showIfNotShowing(DialogFragment.class, fragmentManager);\n        assertThat(fragmentManager.getFragments().size(), equalTo(1));\n        assertThat(fragmentManager.getFragments().get(0), equalTo(dialog1));\n    }\n\n    @Test\n    public void showIfNotShowing_whenActivitySavedState_doesNotShowDialog() {\n        ActivityController<FragmentActivity> activityController = Robolectric.buildActivity(FragmentActivity.class).setup();\n        activityController.pause().stop().saveInstanceState(new Bundle());\n\n        FragmentManager fragmentManager = activityController.get().getSupportFragmentManager();\n        DialogFragmentUtils.showIfNotShowing(DialogFragment.class, fragmentManager);\n        assertThat(fragmentManager.getFragments().size(), equalTo(0));\n    }\n\n    @Test\n    public void showIfNotShowing_whenActivityDestroyed_doesNotShowDialog() {\n        ActivityController<FragmentActivity> activityController = Robolectric.buildActivity(FragmentActivity.class).setup();\n        activityController.pause().stop().destroy();\n\n        FragmentManager fragmentManager = activityController.get().getSupportFragmentManager();\n        DialogFragmentUtils.showIfNotShowing(DialogFragment.class, fragmentManager);\n        assertThat(fragmentManager.getFragments().size(), equalTo(0));\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/utils/InMemUniqueIdGeneratorTest.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nclass InMemUniqueIdGeneratorTest : UniqueIdGeneratorTest() {\n    override fun buildSubject(): UniqueIdGenerator {\n        return InMemUniqueIdGenerator()\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/utils/IntentLauncherImplTest.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport androidx.activity.result.ActivityResultLauncher\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoMoreInteractions\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.androidshared.system.IntentLauncherImpl\nimport java.lang.Exception\n\nclass IntentLauncherImplTest {\n    private val context = mock<Context>()\n    private val activity = mock<Activity>()\n    private val activityResultLauncher = mock<ActivityResultLauncher<Intent>>()\n    private val intent = mock<Intent>()\n    private val onError = mock<() -> Unit>()\n    private val intentLauncher = IntentLauncherImpl\n\n    @Test\n    fun `startActivity with given intent should be called on the context when calling IntentLauncher#launch(context, intent, onError)`() {\n        intentLauncher.launch(context, intent, onError)\n        verify(context).startActivity(intent)\n        verifyNoMoreInteractions(onError)\n    }\n\n    @Test\n    fun `onError should be called if any exception occurs when calling IntentLauncher#launch(context, intent, onError)`() {\n        whenever(context.startActivity(intent)).then {\n            throw Exception()\n        }\n        intentLauncher.launch(context, intent, onError)\n        verify(onError).invoke()\n    }\n\n    @Test\n    fun `onError should be called if any error occurs when calling IntentLauncher#launch(context, intent, onError)`() {\n        whenever(context.startActivity(intent)).then {\n            throw Error()\n        }\n        intentLauncher.launch(context, intent, onError)\n        verify(onError).invoke()\n    }\n\n    @Test\n    fun `startActivityForResult with given intent should be called on the context when calling IntentLauncher#launchForResult(context, intent, requestCode, onError)`() {\n        intentLauncher.launchForResult(activity, intent, 1, onError)\n        verify(activity).startActivityForResult(intent, 1)\n        verifyNoMoreInteractions(onError)\n    }\n\n    @Test\n    fun `onError should be called if any exception occurs when calling IntentLauncher#launchForResult(context, intent, requestCode, onError)`() {\n        whenever(activity.startActivityForResult(intent, 1)).then {\n            throw Exception()\n        }\n        intentLauncher.launchForResult(activity, intent, 1, onError)\n        verify(onError).invoke()\n    }\n\n    @Test\n    fun `onError should be called if any error occurs when calling IntentLauncher#launchForResult(context, intent, requestCode, onError)`() {\n        whenever(activity.startActivityForResult(intent, 1)).then {\n            throw Error()\n        }\n        intentLauncher.launchForResult(activity, intent, 1, onError)\n        verify(onError).invoke()\n    }\n\n    @Test\n    fun `startActivityForResult with given intent should be called on the context when calling IntentLauncher#launchForResult(resultLauncher, intent, onError)`() {\n        intentLauncher.launchForResult(activityResultLauncher, intent, onError)\n        verify(activityResultLauncher).launch(intent)\n        verifyNoMoreInteractions(onError)\n    }\n\n    @Test\n    fun `onError should not be called if no exception occurs when calling IntentLauncher#launchForResult(resultLauncher, intent, onError)`() {\n        intentLauncher.launchForResult(activityResultLauncher, intent, onError)\n        verifyNoMoreInteractions(onError)\n    }\n\n    @Test\n    fun `onError should be called if any exception occurs when calling IntentLauncher#launchForResult(resultLauncher, intent, onError)`() {\n        whenever(activityResultLauncher.launch(intent)).then {\n            throw Exception()\n        }\n        intentLauncher.launchForResult(activityResultLauncher, intent, onError)\n        verify(onError).invoke()\n    }\n\n    @Test\n    fun `onError should be called if any error occurs when calling IntentLauncher#launchForResult(resultLauncher, intent, onError)`() {\n        whenever(activityResultLauncher.launch(intent)).then {\n            throw Error()\n        }\n        intentLauncher.launchForResult(activityResultLauncher, intent, onError)\n        verify(onError).invoke()\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/utils/PathUtilsTest.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\nclass PathUtilsTest {\n    @Test\n    fun `getAbsoluteFilePath() returns filePath prepended with dirPath`() {\n        val path = PathUtils.getAbsoluteFilePath(\"/anotherRoot/anotherDir\", \"root/dir/file\")\n        assertThat(path, equalTo(\"/anotherRoot/anotherDir/root/dir/file\"))\n    }\n\n    @Test\n    fun `getAbsoluteFilePath() returns valid path when filePath does not start with seperator`() {\n        val path = PathUtils.getAbsoluteFilePath(\"/root/dir\", \"file\")\n        assertThat(path, equalTo(\"/root/dir/file\"))\n    }\n\n    @Test\n    fun `getAbsoluteFilePath() returns filePath when it starts with dirPath`() {\n        val path = PathUtils.getAbsoluteFilePath(\"/root/dir\", \"/root/dir/file\")\n        assertThat(path, equalTo(\"/root/dir/file\"))\n    }\n\n    @Test\n    fun `getAbsoluteFilePath() works when dirPath is not canonical`() {\n        val tempDir = TempFiles.createTempDir()\n        val nonCanonicalPath =\n            tempDir.canonicalPath + File.separator + \"..\" + File.separator + tempDir.name\n        assertThat(File(nonCanonicalPath).canonicalPath, equalTo(tempDir.canonicalPath))\n\n        val path = PathUtils.getAbsoluteFilePath(nonCanonicalPath, \"file\")\n        assertThat(path, equalTo(nonCanonicalPath + File.separator + \"file\"))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/utils/SettingsUniqueIdGeneratorTest.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.shared.settings.InMemSettings\nimport org.odk.collect.shared.settings.Settings\n\nclass SettingsUniqueIdGeneratorTest : UniqueIdGeneratorTest() {\n    override fun buildSubject(): UniqueIdGenerator {\n        return buildGenerator(InMemSettings())\n    }\n\n    @Test\n    fun `ids are consistent between instances`() {\n        val settings = InMemSettings()\n        val generator1 = buildGenerator(settings)\n        val generator2 = buildGenerator(settings)\n\n        // Initialize IDs in different orders\n        generator1.getInt(\"id1\")\n        generator1.getInt(\"id2\")\n        generator2.getInt(\"id2\")\n        generator2.getInt(\"id1\")\n\n        assertThat(generator1.getInt(\"id1\"), equalTo(generator2.getInt(\"id1\")))\n    }\n\n    @Test\n    fun `ids increment between instances`() {\n        val settings = InMemSettings()\n        val generator1 = buildGenerator(settings)\n        val generator2 = buildGenerator(settings)\n\n        val id1 = generator1.getInt(\"id1\")\n        val id2 = generator2.getInt(\"id2\")\n        assertThat(id2, equalTo(id1 + 1))\n    }\n\n    private fun buildGenerator(settings: Settings): SettingsUniqueIdGenerator =\n        SettingsUniqueIdGenerator(settings)\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/utils/UniqueIdGeneratorTest.kt",
    "content": "package org.odk.collect.androidshared.utils\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\n\nabstract class UniqueIdGeneratorTest {\n\n    abstract fun buildSubject(): UniqueIdGenerator\n\n    @Test\n    fun `returns an incrementing unique ID for each identifier`() {\n        val generator = buildSubject()\n        val id1 = generator.getInt(\"id1\")\n        val id2 = generator.getInt(\"id2\")\n\n        assertThat(id2, equalTo(id1 + 1))\n    }\n\n    @Test\n    fun `returns a consistent ID for an identifier`() {\n        val generator = buildSubject()\n        val id1 = generator.getInt(\"id1\")\n        val id2 = generator.getInt(\"id1\")\n\n        assertThat(id2, equalTo(id1))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/java/org/odk/collect/androidshared/utils/ValidatorTest.kt",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.androidshared.utils\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport junit.framework.TestCase.assertFalse\nimport junit.framework.TestCase.assertTrue\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\n@RunWith(AndroidJUnit4::class)\nclass ValidatorTest {\n    @Test\n    fun emailValidationTestCase() {\n        // https://en.wikipedia.org/wiki/Email_address\n        assertTrue(Validator.isEmailAddressValid(\"prettyandsimple@example.com\"))\n        assertTrue(Validator.isEmailAddressValid(\"very.common@example.com\"))\n        assertTrue(Validator.isEmailAddressValid(\"disposable.style.email.with+symbol@example.com\"))\n        assertTrue(Validator.isEmailAddressValid(\"other.email-with-dash@example.com\"))\n        assertTrue(Validator.isEmailAddressValid(\"fully-qualified-domain@example.com.\"))\n        assertTrue(Validator.isEmailAddressValid(\"x@example.com\"))\n        assertTrue(Validator.isEmailAddressValid(\"\\\"very.unusual.@.unusual.com\\\"@example.com\"))\n        assertTrue(Validator.isEmailAddressValid(\"\\\"very.(),:;<>[]\\\\\\\".VERY.\\\\\\\"very@\\\\\\\\ \\\\\\\"very\\\\\\\".unusual\\\"@strange.example.com\"))\n        assertTrue(Validator.isEmailAddressValid(\"example-indeed@strange-example.com\"))\n        assertTrue(Validator.isEmailAddressValid(\"admin@mailserver1\"))\n        assertTrue(Validator.isEmailAddressValid(\"#!$%&'*+-/=?^_`{}|~@example.org\"))\n        assertTrue(Validator.isEmailAddressValid(\"\\\"()<>[]:,;@\\\\\\\\\\\\\\\"!#$%&'-/=?^_`{}| ~.a\\\"@example.org\"))\n        assertTrue(Validator.isEmailAddressValid(\"\\\" \\\"@example.org\"))\n        assertTrue(Validator.isEmailAddressValid(\"example@s.solutions\"))\n        assertTrue(Validator.isEmailAddressValid(\"user@localserver\"))\n        assertTrue(Validator.isEmailAddressValid(\"user@[IPv6:2001:DB8::1\"))\n        assertFalse(Validator.isEmailAddressValid(\"plainaddress\"))\n        assertFalse(Validator.isEmailAddressValid(\"@domain.com\"))\n        assertFalse(Validator.isEmailAddressValid(\"@\"))\n        assertFalse(Validator.isEmailAddressValid(\"email@\"))\n        assertFalse(Validator.isEmailAddressValid(\"email.domain.com\"))\n    }\n\n    @Test\n    fun urlValidationTest() {\n        //  https://en.wikipedia.org/wiki/Template:URL/testcases\n        assertTrue(Validator.isUrlValid(\"http://www.example.com\"))\n        assertTrue(Validator.isUrlValid(\"https://www.example.com\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com/\"))\n        assertTrue(Validator.isUrlValid(\"https://www.EXAMPLE.cOm\"))\n        assertTrue(Validator.isUrlValid(\"HTTPS://www.EXAMPLE.cOm/\"))\n        assertTrue(Validator.isUrlValid(\"https://www.example.com/\"))\n        assertTrue(Validator.isUrlValid(\"http://example.com\"))\n        assertTrue(Validator.isUrlValid(\"https://example.com\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com/foo/bar\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com/foo/bar/\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com/foO/BaR\"))\n        assertTrue(Validator.isUrlValid(\"https://www.example.com/foo/bar\"))\n        assertTrue(Validator.isUrlValid(\"http://example.com/foo/bar\"))\n        assertTrue(Validator.isUrlValid(\"https://example.com/foo/bar\"))\n        assertTrue(Validator.isUrlValid(\"http://example.com/foo/bar\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com?foo=BaR\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com:8080\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com:8080/foo/bar\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com/#\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com/?\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example2.com\"))\n        assertTrue(Validator.isUrlValid(\"http://www.sho.com/site/dexter/home.sho\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com/foo%20bar\"))\n        assertTrue(Validator.isUrlValid(\"http://example.com/foo/bar\"))\n        assertTrue(Validator.isUrlValid(\"https://example.com/foo/bar\"))\n        assertTrue(Validator.isUrlValid(\"http://عمان.icom.museum\"))\n        assertTrue(Validator.isUrlValid(\"http://www.example.com/foo/bar?a=b&c=d\"))\n        assertFalse(Validator.isUrlValid(\"http://www.example.com#fooBaR\"))\n        assertFalse(Validator.isUrlValid(\"http://\"))\n        assertFalse(Validator.isUrlValid(\"example.com\"))\n        assertFalse(Validator.isUrlValid(\"EXAMPLE.COM\"))\n        assertFalse(Validator.isUrlValid(\"www.example.com\"))\n        assertFalse(Validator.isUrlValid(\"WWW.EXAMPLE.COM\"))\n        assertFalse(Validator.isUrlValid(\"ftp://example.com\"))\n        assertFalse(Validator.isUrlValid(\"ftp://www.example.com\"))\n        assertFalse(Validator.isUrlValid(\"example.com/foo/bar\"))\n        assertFalse(Validator.isUrlValid(\"www.example.com/foo/bar\"))\n        assertFalse(Validator.isUrlValid(\"www.example.com:8080\"))\n        assertFalse(Validator.isUrlValid(\"www.example.com:8080/foo/bar\"))\n        assertFalse(Validator.isUrlValid(\"www.example.com/foo%20bar\"))\n        assertFalse(Validator.isUrlValid(\"example.com/foo/bar\"))\n        assertFalse(Validator.isUrlValid(\"www.example.com/foo/bar\"))\n        assertFalse(Validator.isUrlValid(\"www.example.com/foo/bar\"))\n        assertFalse(Validator.isUrlValid(\"www.example.com/foo/捦挺挎/bar\"))\n        assertFalse(Validator.isUrlValid(\"عمان.icom.museum\"))\n        assertFalse(Validator.isUrlValid(\"1964thetribute.com\"))\n        assertFalse(Validator.isUrlValid(\"http:/www.example.com\"))\n        assertFalse(Validator.isUrlValid(\"http:www.example.com\"))\n        assertFalse(Validator.isUrlValid(\"https:///.\"))\n    }\n}\n"
  },
  {
    "path": "androidshared/src/test/resources/robolectric.properties",
    "content": "sdk=33\n"
  },
  {
    "path": "androidtest/.gitignore",
    "content": "/build"
  },
  {
    "path": "androidtest/README.md",
    "content": "# androidtest\n\nIncludes helpers (arguably missing from `androidx.test:core`) for writing [\"Local tests\" and \"Instrumented tests\"](https://developer.android.com/training/testing/fundamentals#instrumented-vs-local).\n```\n"
  },
  {
    "path": "androidtest/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n    namespace = \"org.odk.collect.androidtest\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(libs.junit)\n    implementation(libs.androidxTestCoreKtx)\n    implementation(libs.androidxLifecycleLivedataKtx)\n    implementation(libs.androidxLifecycleRuntimeKtx)\n    implementation(libs.androidxTestEspressoCore)\n    implementation(libs.androidxAppcompat)\n    implementation(libs.androidxTestEspressoIntents)\n    implementation(libs.timber)\n    implementation(libs.kotlinxCoroutinesTest)\n    var composeBom = platform(libs.androidxComposeBom)\n    implementation(composeBom)\n    implementation(libs.androidXComposeUiTestJunit4)\n\n    //noinspection FragmentGradleConfiguration\n    debugApi(libs.androidxFragmentTesting) {\n        exclude(group = \"androidx.test\", module = \"monitor\") // fixes issue https://github.com/android/android-test/issues/731\n    }\n}\n"
  },
  {
    "path": "androidtest/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "androidtest/src/main/java/org/odk/collect/androidtest/ActivityScenarioExtensions.kt",
    "content": "package org.odk.collect.androidtest\n\nimport android.app.Activity\nimport android.os.Bundle\nimport android.os.PersistableBundle\nimport androidx.test.core.app.ActivityScenario\n\nobject ActivityScenarioExtensions {\n\n    /**\n     * Calling finish() doesn't seem to move an Activity to the DESTROYED state when using\n     * ActivityScenario but `isFinishing` appears to work correctly. Bug for this is tracked\n     * at https://github.com/android/android-test/issues/978.\n     */\n    @JvmStatic\n    val <T : Activity> ActivityScenario<T>.isFinishing: Boolean\n        get() {\n            var isFinishing = false\n            this.onActivity {\n                isFinishing = it.isFinishing\n            }\n\n            return isFinishing\n        }\n\n    fun <T : Activity> ActivityScenario<T>.saveInstanceState(): Bundle {\n        val bundle = Bundle()\n        onActivity { it.onSaveInstanceState(bundle, PersistableBundle()) }\n        return bundle\n    }\n}\n"
  },
  {
    "path": "androidtest/src/main/java/org/odk/collect/androidtest/ActivityScenarioLauncherRule.kt",
    "content": "package org.odk.collect.androidtest\n\nimport android.app.Activity\nimport android.content.Intent\nimport androidx.test.core.app.ActivityScenario\nimport org.junit.rules.ExternalResource\nimport timber.log.Timber\n\n/**\n * Alternative to [ActivityScenario] that allows tests to do work before launching the [Activity]\n * (like switch out dependencies, construct intents etc) and also allows creation of multiple\n * scenarios in a test.\n */\nopen class ActivityScenarioLauncherRule : ExternalResource() {\n\n    private val scenarios = mutableListOf<ActivityScenario<*>>()\n\n    fun <A : Activity> launch(activityClass: Class<A>): ActivityScenario<A> {\n        return ActivityScenario.launch(activityClass).also {\n            scenarios.add(it)\n        }\n    }\n\n    fun <A : Activity> launch(intent: Intent): ActivityScenario<A> {\n        return ActivityScenario.launch<A>(intent).also {\n            scenarios.add(it)\n        }\n    }\n\n    fun <A : Activity> launchForResult(intent: Intent): ActivityScenario<A> {\n        return ActivityScenario.launchActivityForResult<A>(intent).also {\n            scenarios.add(it)\n        }\n    }\n\n    fun <A : Activity> launchForResult(activityClass: Class<A>): ActivityScenario<A> {\n        return ActivityScenario.launchActivityForResult(activityClass).also {\n            scenarios.add(it)\n        }\n    }\n\n    override fun after() {\n        scenarios.forEach {\n            try {\n                it.close()\n            } catch (e: Throwable) {\n                Timber.e(Error(\"Error closing ActivityScenario: $e\"))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "androidtest/src/main/java/org/odk/collect/androidtest/DrawableMatcher.kt",
    "content": "package org.odk.collect.androidtest\n\nimport android.app.Application\nimport android.graphics.Bitmap\nimport android.graphics.drawable.BitmapDrawable\nimport android.graphics.drawable.PictureDrawable\nimport android.graphics.drawable.VectorDrawable\nimport android.os.StrictMode\nimport android.os.StrictMode.ThreadPolicy\nimport android.view.View\nimport android.widget.ImageView\nimport androidx.appcompat.content.res.AppCompatResources\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.matcher.BoundedMatcher\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\n\nobject DrawableMatcher {\n    @JvmStatic\n    fun withImageDrawable(expectedResourceId: Int): Matcher<View> {\n        return object : BoundedMatcher<View, ImageView>(ImageView::class.java) {\n            override fun describeTo(description: Description) {\n                description.appendText(\"has image drawable resource $expectedResourceId\")\n            }\n\n            public override fun matchesSafely(imageView: ImageView): Boolean {\n                val context = ApplicationProvider.getApplicationContext<Application>()\n                return imageView.drawable.toBitmap().rowBytes == (AppCompatResources.getDrawable(context, expectedResourceId) as VectorDrawable).toBitmap().rowBytes\n            }\n        }\n    }\n\n    @JvmStatic\n    fun withBitmap(match: Bitmap?): Matcher<View> {\n        return object : BoundedMatcher<View, ImageView>(ImageView::class.java) {\n            override fun describeTo(description: Description) {\n                description.appendText(\"bitmaps did not match\")\n            }\n\n            override fun matchesSafely(imageView: ImageView): Boolean {\n                if (match == null) {\n                    return false\n                }\n\n                val actual: Bitmap? = when (val drawable = imageView.drawable) {\n                    is BitmapDrawable -> drawable.bitmap\n                    is PictureDrawable -> drawable.toBitmap(match.width, match.height)\n                    else -> null\n                }\n\n                if (actual == null) {\n                    return false\n                } else {\n                    val originalThreadPolicy = StrictMode.getThreadPolicy()\n\n                    try {\n                        // Permit slow calls to allow `sameAs` use\n                        StrictMode.setThreadPolicy(\n                            ThreadPolicy.Builder()\n                                .permitCustomSlowCalls().build()\n                        )\n\n                        return match.sameAs(actual)\n                    } finally {\n                        StrictMode.setThreadPolicy(originalThreadPolicy)\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "androidtest/src/main/java/org/odk/collect/androidtest/FakeLifecycleOwner.kt",
    "content": "package org.odk.collect.androidtest\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.LifecycleRegistry\n\nclass FakeLifecycleOwner : LifecycleOwner {\n    private val lifecycleRegistry: LifecycleRegistry by lazy {\n        LifecycleRegistry(this).also {\n            it.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)\n        }\n    }\n\n    override val lifecycle: LifecycleRegistry = lifecycleRegistry\n\n    fun destroy() {\n        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)\n    }\n}\n"
  },
  {
    "path": "androidtest/src/main/java/org/odk/collect/androidtest/FragmentScenarioExtensions.kt",
    "content": "package org.odk.collect.androidtest\n\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentResultListener\nimport androidx.fragment.app.testing.FragmentScenario\n\nobject FragmentScenarioExtensions {\n\n    fun <T : Fragment> FragmentScenario<T>.setFragmentResultListener(\n        requestKey: String,\n        listener: FragmentResultListener\n    ) {\n        onFragment {\n            it.parentFragmentManager.setFragmentResultListener(requestKey, it, listener)\n        }\n    }\n}\n"
  },
  {
    "path": "androidtest/src/main/java/org/odk/collect/androidtest/LiveDataTestUtils.kt",
    "content": "package org.odk.collect.androidtest\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.Observer\nimport java.util.concurrent.CountDownLatch\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.TimeoutException\n\n/**\n * Gets the value of a [LiveData] or waits for it to have one, with a timeout.\n *\n * Use this extension from host-side (JVM) tests. It's recommended to use it alongside\n * `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.\n *\n * From: https://github.com/android/architecture-components-samples/blob/master/GithubBrowserSample/app/src/test-common/java/com/android/example/github/util/LiveDataTestUtil.kt\n */\n@JvmOverloads\nfun <T> LiveData<T>.getOrAwaitValue(\n    time: Long = 2,\n    timeUnit: TimeUnit = TimeUnit.SECONDS,\n    afterObserve: () -> Unit = {}\n): T {\n    var data: T? = null\n    val latch = CountDownLatch(1)\n    val observer = Observer<T> { o ->\n        data = o\n        latch.countDown()\n    }\n\n    this.observeForever(observer)\n    afterObserve.invoke()\n\n    // Don't wait indefinitely if the LiveData is not set.\n    if (latch.await(time, timeUnit)) {\n        this.removeObserver(observer)\n    } else {\n        this.removeObserver(observer)\n        throw TimeoutException(\"LiveData value was never set.\")\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    return data as T\n}\n\nfun <T> LiveData<T>.recordValues(block: (List<T>) -> Unit) {\n    val list = mutableListOf<T>()\n    val observer = Observer<T> {\n        list.add(it)\n    }\n\n    try {\n        this.observeForever(observer)\n        block(list)\n    } finally {\n        this.removeObserver(observer)\n    }\n}\n"
  },
  {
    "path": "androidtest/src/main/java/org/odk/collect/androidtest/MainDispatcherRule.kt",
    "content": "package org.odk.collect.androidtest\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.StandardTestDispatcher\nimport kotlinx.coroutines.test.TestDispatcher\nimport kotlinx.coroutines.test.resetMain\nimport kotlinx.coroutines.test.setMain\nimport org.junit.rules.TestWatcher\nimport org.junit.runner.Description\n\n/**\n * Test rule that replaces `Dispatchers.Main` with a `TestDispatcher`\n * so coroutine-based code using the Main dispatcher can run in JVM tests.\n */\n@OptIn(ExperimentalCoroutinesApi::class)\nclass MainDispatcherRule(\n    private val dispatcher: TestDispatcher = StandardTestDispatcher()\n) : TestWatcher() {\n    override fun starting(description: Description) {\n        Dispatchers.setMain(dispatcher)\n    }\n\n    override fun finished(description: Description) {\n        Dispatchers.resetMain()\n    }\n}\n"
  },
  {
    "path": "androidtest/src/main/java/org/odk/collect/androidtest/NodeInteractionExtensions.kt",
    "content": "package org.odk.collect.androidtest\n\nimport android.app.Application\nimport androidx.annotation.StringRes\nimport androidx.compose.ui.semantics.SemanticsActions\nimport androidx.compose.ui.semantics.getOrNull\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.SemanticsNodeInteractionsProvider\nimport androidx.test.core.app.ApplicationProvider\n\nfun SemanticsNodeInteractionsProvider.onNodeWithClickLabel(@StringRes labelRes: Int): SemanticsNodeInteraction {\n    val context = ApplicationProvider.getApplicationContext<Application>()\n    return onNodeWithClickLabel(context.getString(labelRes))\n}\n\nfun SemanticsNodeInteractionsProvider.onNodeWithClickLabel(label: String): SemanticsNodeInteraction =\n    onNode(hasClickLabel(label))\n\nfun hasClickLabel(label: String): SemanticsMatcher {\n    return SemanticsMatcher(\"Clickable action label = '$label'\") {\n        it.config.getOrNull(SemanticsActions.OnClick)?.label?.equals(label) == true\n    }\n}\n"
  },
  {
    "path": "androidtest/src/main/java/org/odk/collect/androidtest/RecordedIntentsRule.kt",
    "content": "package org.odk.collect.androidtest\n\nimport androidx.test.core.app.ActivityScenario\nimport androidx.test.espresso.intent.Intents\nimport androidx.test.espresso.intent.rule.IntentsTestRule\nimport androidx.test.rule.ActivityTestRule\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\n\n/**\n * Like [IntentsTestRule] but doesn't extend [ActivityTestRule] (and therefore works with\n * [ActivityScenario]/[FragmentScenario]\n *\n * Discussed in issue [here](https://github.com/android/android-test/issues/174).\n */\nclass RecordedIntentsRule : TestRule {\n    override fun apply(base: Statement, description: Description): Statement {\n        return object : Statement() {\n            override fun evaluate() {\n                try {\n                    Intents.init()\n                    base.evaluate()\n                } finally {\n                    Intents.release()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "async/.gitignore",
    "content": "/build"
  },
  {
    "path": "async/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\")\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    namespace = \"org.odk.collect.async\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(fileTree(mapOf(\"dir\" to \"libs\", \"include\" to listOf(\"*.jar\"))))\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.kotlinxCoroutinesAndroid)\n    implementation(libs.androidxWorkRuntime)\n    implementation(project(\":analytics\")) {\n        exclude(\"com.google.firebase\")\n    }\n\n    implementation(project(\":icons\"))\n\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.androidxWorkTesting)\n    testImplementation(libs.mockitoKotlin)\n}\n"
  },
  {
    "path": "async/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "async/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_DATA_SYNC\" />\n\n    <application>\n        <service\n            android:name=\"org.odk.collect.async.services.TaskSpecRunnerService\"\n            android:foregroundServiceType=\"dataSync\"\n            android:exported=\"false\"\n            tools:node=\"merge\"/>\n    </application>\n</manifest>\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/Cancellable.kt",
    "content": "package org.odk.collect.async\n\ninterface Cancellable {\n    fun cancel(): Boolean\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/OngoingWorkListener.kt",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.async\n\n/**\n * Allows a client of some ongoing work to receive updates on progress and to provide a signal\n * that the work should be cancelled.\n */\ninterface OngoingWorkListener {\n    fun progressUpdate(progress: Int)\n    val isCancelled: Boolean\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/Scheduler.kt",
    "content": "package org.odk.collect.async\n\nimport androidx.annotation.StringRes\nimport kotlinx.coroutines.flow.Flow\nimport java.util.function.Consumer\nimport java.util.function.Supplier\n\n/**\n * Run and schedule tasks in the foreground (UI thread) and background. Based on terminology\n * used in Android's Background Processing documentation: https://developer.android.com/guide/background.\n *\n * This keeps the details of threading and job frameworks (which are very often in flux) away from\n * the UI code. An added advantage of this is the ability to use Kotlin's concurrency primitives from Java.\n */\ninterface Scheduler {\n\n    /**\n     * Run work in the background (off the UI thread) and then use the result of that work\n     * in the foreground. Cancelled if application closed.\n     *\n     * @param background the task to be run\n     * @param foreground run on the foreground once the task is complete\n     */\n    fun <T> immediate(background: Supplier<T>, foreground: Consumer<T>)\n\n    /**\n     * Run work in the foreground or background. Cancelled if application closed.\n     */\n    fun immediate(foreground: Boolean = false, delay: Long? = null, runnable: Runnable)\n\n    /**\n     * Run a task immediately.\n     *\n     * @param tag used to identify this task in future. If there is a previously scheduled task\n     * with the same tag then that task will be cancelled and this will replace it\n     * @param spec defines the task to be run\n     * @param notificationInfo the information needed to display a notification about this work\n     * to the user\n     */\n    fun immediate(\n        tag: String,\n        spec: TaskSpec,\n        inputData: Map<String, String>,\n        notificationInfo: NotificationInfo\n    )\n\n    /**\n     * Schedule a task to run in the background even if the app isn't running. The task\n     * will only be run when the network is available.\n     *\n     * @param tag used to identify this task in future. If there is a previously scheduled task\n     * with the same tag then that task will be cancelled and this will replace it\n     * @param spec defines the task to be run\n     * @param inputData a map of input data that can be accessed by the task\n     * @param networkConstraint the specific kind of network required\n     */\n    fun networkDeferred(\n        tag: String,\n        spec: TaskSpec,\n        inputData: Map<String, String>,\n        networkConstraint: NetworkType? = null\n    )\n\n    /**\n     * Schedule a task to run in the background repeatedly even if the app isn't running. The task\n     * will only be run when the network is available.\n     *\n     * @param tag used to identify this task in future. Previously scheduled tasks using the same\n     * tag will be replaced\n     * @param spec defines the task to be run\n     * @param repeatPeriod the period between each run of the task\n     * @param inputData a map of input data that can be accessed by the task\n     */\n    fun networkDeferredRepeat(\n        tag: String,\n        spec: TaskSpec,\n        repeatPeriod: Long,\n        inputData: Map<String, String>\n    )\n\n    /**\n     * Cancel deferred task scheduled with tag\n     */\n    fun cancelDeferred(tag: String)\n\n    /**\n     * Returns true if a deferred task scheduled with tag is currently running\n     */\n    fun isDeferredRunning(tag: String): Boolean\n\n    /**\n     * Run a task and then repeat in the foreground\n     *\n     * @param foreground the task to be run\n     * @param repeatPeriod the period between each run of the task\n     * @return object that allows task to be cancelled\n     */\n    fun repeat(foreground: Runnable, repeatPeriod: Long): Cancellable\n\n    fun cancelAllDeferred()\n\n    fun <T> flowOnBackground(flow: Flow<T>): Flow<T>\n\n    enum class NetworkType {\n        WIFI,\n        CELLULAR,\n        OTHER\n    }\n}\n\nfun <T> Flow<T>.flowOnBackground(scheduler: Scheduler): Flow<T> {\n    return scheduler.flowOnBackground(this)\n}\n\ndata class NotificationInfo(\n    val id: Int,\n    val channel: String,\n    val channelName: String,\n    @StringRes val title: Int\n)\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/SchedulerAsyncTaskMimic.kt",
    "content": "package org.odk.collect.async\n\nimport android.os.AsyncTask\n\n/**\n * Basic reimplementation of the [AsyncTask] API that allows an [AsyncTask] implementation to\n * use [Scheduler] with minimal internal and external changes.\n */\nabstract class SchedulerAsyncTaskMimic<Params, Progress, Result>(private val scheduler: Scheduler) {\n\n    @Volatile\n    private var status: AsyncTask.Status = AsyncTask.Status.PENDING\n\n    @Volatile\n    private var cancelled = false\n\n    protected abstract fun onPreExecute()\n    protected abstract fun doInBackground(vararg params: Params): Result\n    protected abstract fun onProgressUpdate(vararg values: Progress)\n    protected abstract fun onPostExecute(result: Result)\n    protected abstract fun onCancelled()\n\n    /**\n     * Execute [doInBackground] on calling thread and return the [Result] value. Should probably\n     * not be used as a replacement for [AsyncTask.get] (unless it's for testing purposes).\n     */\n    fun executeSynchronously(vararg params: Params): Result {\n        return doInBackground(*params)\n    }\n\n    fun execute(vararg params: Params): SchedulerAsyncTaskMimic<Params, Progress, Result> {\n        status = AsyncTask.Status.RUNNING\n        onPreExecute()\n\n        scheduler.immediate(\n            background = {\n                doInBackground(*params)\n            },\n            foreground = { result ->\n                if (cancelled) {\n                    onCancelled()\n                } else {\n                    onPostExecute(result)\n                }\n\n                status = AsyncTask.Status.FINISHED\n            }\n        )\n\n        return this\n    }\n\n    fun getStatus(): AsyncTask.Status {\n        return status\n    }\n\n    /**\n     * Unlike [AsyncTask.cancel], this does not offer the option to attempt to interrupt the\n     * background thread running [doInBackground]. Calling [cancel] will allow [doInBackground]\n     * to finish, but will prevent [onPostExecute] from running ([onCancelled] will be run\n     * instead).\n     */\n    fun cancel() {\n        cancelled = true\n    }\n\n    fun isCancelled(): Boolean {\n        return cancelled\n    }\n\n    protected fun publishProgress(vararg values: Progress) {\n        scheduler.immediate(foreground = true) {\n            onProgressUpdate(*values)\n        }\n    }\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/SchedulerBuilder.kt",
    "content": "package org.odk.collect.async\n\nimport kotlinx.coroutines.flow.Flow\nimport java.util.function.Consumer\nimport java.util.function.Supplier\n\nobject SchedulerBuilder {\n\n    @JvmStatic\n    fun build(\n        taskRunner: TaskRunner,\n        taskSpecRunner: TaskSpecRunner,\n        taskSpecScheduler: TaskSpecScheduler\n    ): Scheduler {\n        return object : Scheduler {\n            override fun <T> immediate(\n                background: Supplier<T>,\n                foreground: Consumer<T>\n            ) {\n                taskRunner.immediate(background, foreground)\n            }\n\n            override fun immediate(\n                foreground: Boolean,\n                delay: Long?,\n                runnable: Runnable\n            ) {\n                taskRunner.immediate(\n                    runnable,\n                    isForeground = foreground,\n                    delay = delay,\n                    repeatPeriod = null\n                )\n            }\n\n            override fun immediate(\n                tag: String,\n                spec: TaskSpec,\n                inputData: Map<String, String>,\n                notificationInfo: NotificationInfo\n            ) {\n                taskSpecRunner.run(tag, spec, inputData, notificationInfo)\n            }\n\n            override fun networkDeferred(\n                tag: String,\n                spec: TaskSpec,\n                inputData: Map<String, String>,\n                networkConstraint: Scheduler.NetworkType?\n            ) {\n                taskSpecScheduler.schedule(\n                    tag,\n                    spec,\n                    inputData,\n                    networkConstraint = networkConstraint\n                )\n            }\n\n            override fun networkDeferredRepeat(\n                tag: String,\n                spec: TaskSpec,\n                repeatPeriod: Long,\n                inputData: Map<String, String>\n            ) {\n                taskSpecScheduler.schedule(tag, spec, inputData, repeatPeriod = repeatPeriod)\n            }\n\n            override fun cancelDeferred(tag: String) {\n                taskSpecScheduler.cancel(tag)\n            }\n\n            override fun isDeferredRunning(tag: String): Boolean {\n                return taskSpecScheduler.isRunning(tag)\n            }\n\n            override fun repeat(\n                foreground: Runnable,\n                repeatPeriod: Long\n            ): Cancellable {\n                return taskRunner.immediate(\n                    foreground,\n                    isForeground = true,\n                    delay = null,\n                    repeatPeriod = repeatPeriod\n                )\n            }\n\n            override fun cancelAllDeferred() {\n                taskSpecScheduler.cancelAll()\n            }\n\n            override fun <T> flowOnBackground(flow: Flow<T>): Flow<T> {\n                return taskRunner.flowOnBackground(flow)\n            }\n\n        }\n    }\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/ScopeCancellable.kt",
    "content": "package org.odk.collect.async\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.cancel\n\ninternal class ScopeCancellable(private val scope: CoroutineScope) : Cancellable {\n\n    override fun cancel(): Boolean {\n        scope.cancel()\n        return true\n    }\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/TaskRunner.kt",
    "content": "package org.odk.collect.async\n\nimport kotlinx.coroutines.Runnable\nimport kotlinx.coroutines.flow.Flow\nimport java.util.function.Consumer\nimport java.util.function.Supplier\n\ninterface TaskRunner {\n    fun <T> immediate(background: Supplier<T>, foreground: Consumer<T>)\n    fun immediate(task: Runnable, isForeground: Boolean = false, delay: Long? = null, repeatPeriod: Long? = null): Cancellable\n    fun <T> flowOnBackground(flow: Flow<T>): Flow<T>\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/TaskSpec.kt",
    "content": "package org.odk.collect.async\n\nimport android.content.Context\nimport androidx.work.BackoffPolicy\nimport java.util.function.Supplier\n\ninterface TaskSpec {\n    val maxRetries: Int?\n    val backoffPolicy: BackoffPolicy?\n    val backoffDelay: Long?\n\n    /**\n     * Should return the work to be carried out by the task. The return value of the work\n     * indicates whether the work completed (true) or needs to be run again later (false)\n     *\n     * @param isLastUniqueExecution if a task specifies maxRetries all retries form one logical\n     * group. We want to know which task execution is the last one to for example notify a user only\n     * once instead of doing that after every single execution.\n     */\n    fun getTask(\n        context: Context,\n        inputData: Map<String, String>,\n        isLastUniqueExecution: Boolean,\n        isStopped: (() -> Boolean)\n    ): Supplier<Boolean>\n\n    /**\n     * Called if an exception is thrown while executing the work.\n     */\n    fun onException(exception: Throwable)\n\n    enum class Result {\n        SUCCESS,\n        FAILURE,\n        RETRY\n    }\n}\n\nfun TaskSpec.run(\n    context: Context,\n    inputData: Map<String, String>,\n    runAttemptCount: Int,\n    isForeground: Boolean,\n    isStopped: () -> Boolean\n): TaskSpec.Result {\n    try {\n        val isLastUniqueExecution = maxRetries?.let { runAttemptCount >= it } ?: true\n        val completed = getTask(context, inputData, isForeground || isLastUniqueExecution, isStopped).get()\n        val maxRetries = this.maxRetries\n\n        return if (completed) {\n            TaskSpec.Result.SUCCESS\n        } else if (!isForeground && (maxRetries == null || runAttemptCount < maxRetries)) {\n            TaskSpec.Result.RETRY\n        } else {\n            TaskSpec.Result.FAILURE\n        }\n    } catch (t: Throwable) {\n        onException(t)\n        return TaskSpec.Result.FAILURE\n    }\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/TaskSpecRunner.kt",
    "content": "package org.odk.collect.async\n\ninterface TaskSpecRunner {\n    fun run(tag: String, taskSpec: TaskSpec, inputData: Map<String, String>, notificationInfo: NotificationInfo)\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/TaskSpecScheduler.kt",
    "content": "package org.odk.collect.async\n\ninterface TaskSpecScheduler {\n    fun schedule(\n        tag: String,\n        spec: TaskSpec,\n        inputData: Map<String, String>,\n        networkConstraint: Scheduler.NetworkType? = null,\n        repeatPeriod: Long? = null\n    )\n\n    fun isRunning(tag: String): Boolean\n    fun cancel(tag: String)\n    fun cancelAll()\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/coroutines/CoroutineTaskRunner.kt",
    "content": "package org.odk.collect.async.coroutines\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Runnable\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.odk.collect.async.Cancellable\nimport org.odk.collect.async.ScopeCancellable\nimport org.odk.collect.async.TaskRunner\nimport java.util.function.Consumer\nimport java.util.function.Supplier\nimport kotlin.coroutines.CoroutineContext\n\nclass CoroutineTaskRunner @JvmOverloads constructor(\n    private val foregroundContext: CoroutineContext = Dispatchers.Main,\n    private val backgroundContext: CoroutineContext = Dispatchers.IO\n) : TaskRunner {\n    override fun <T> immediate(\n        background: Supplier<T>,\n        foreground: Consumer<T>\n    ) {\n        CoroutineScope(foregroundContext).launch {\n            val result = withContext(backgroundContext) { background.get() }\n            foreground.accept(result)\n        }\n    }\n\n    override fun immediate(\n        task: Runnable,\n        isForeground: Boolean,\n        delay: Long?,\n        repeatPeriod: Long?\n    ): Cancellable {\n        val context = if (isForeground) {\n            foregroundContext\n        } else {\n            backgroundContext\n        }\n\n        val coroutineScope = CoroutineScope(context)\n        coroutineScope.launch {\n            if (delay != null) {\n                delay(delay)\n            }\n\n            if (repeatPeriod != null) {\n                while (isActive) {\n                    task.run()\n                    delay(repeatPeriod)\n                }\n            } else {\n                task.run()\n            }\n        }\n\n        return ScopeCancellable(coroutineScope)\n    }\n\n    override fun <T> flowOnBackground(flow: Flow<T>): Flow<T> {\n        return flow.flowOn(backgroundContext)\n    }\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/network/ConnectivityProvider.kt",
    "content": "package org.odk.collect.async.network\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport org.odk.collect.async.Scheduler\n\nclass ConnectivityProvider(private val context: Context) : NetworkStateProvider {\n    override val currentNetwork: Scheduler.NetworkType?\n        get() {\n            return if (connectivityManager.activeNetworkInfo?.isConnected == true) {\n                when (connectivityManager.activeNetworkInfo?.type) {\n                    ConnectivityManager.TYPE_WIFI -> Scheduler.NetworkType.WIFI\n                    ConnectivityManager.TYPE_MOBILE -> Scheduler.NetworkType.CELLULAR\n                    else -> Scheduler.NetworkType.OTHER\n                }\n            } else {\n                null\n            }\n        }\n\n    private val connectivityManager: ConnectivityManager\n        get() = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/network/NetworkStateProvider.kt",
    "content": "package org.odk.collect.async.network\n\nimport org.odk.collect.async.Scheduler\n\ninterface NetworkStateProvider {\n    val currentNetwork: Scheduler.NetworkType?\n\n    val isDeviceOnline: Boolean\n        get() {\n            return currentNetwork != null\n        }\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/services/ForegroundServiceTaskSpecRunner.kt",
    "content": "package org.odk.collect.async.services\n\nimport android.app.Application\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.Service\nimport android.content.Intent\nimport android.content.pm.ServiceInfo\nimport android.os.IBinder\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.ServiceCompat\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.odk.collect.async.NotificationInfo\nimport org.odk.collect.async.TaskSpec\nimport org.odk.collect.async.TaskSpecRunner\nimport org.odk.collect.async.run\nimport org.odk.collect.async.services.TaskSpecRunnerService.Companion.EXTRA_INPUT_DATA\nimport org.odk.collect.async.services.TaskSpecRunnerService.Companion.EXTRA_NOTIFICATION_CHANNEL\nimport org.odk.collect.async.services.TaskSpecRunnerService.Companion.EXTRA_NOTIFICATION_CHANNEL_NAME\nimport org.odk.collect.async.services.TaskSpecRunnerService.Companion.EXTRA_NOTIFICATION_ID\nimport org.odk.collect.async.services.TaskSpecRunnerService.Companion.EXTRA_NOTIFICATION_TITLE\nimport org.odk.collect.async.services.TaskSpecRunnerService.Companion.EXTRA_SPEC_CLASS\nimport org.odk.collect.icons.R\n\nclass ForegroundServiceTaskSpecRunner(private val application: Application) : TaskSpecRunner {\n    override fun run(\n        tag: String,\n        taskSpec: TaskSpec,\n        inputData: Map<String, String>,\n        notificationInfo: NotificationInfo\n    ) {\n        val intent = Intent(application, TaskSpecRunnerService::class.java).also {\n            it.putExtra(EXTRA_NOTIFICATION_ID, notificationInfo.id)\n            it.putExtra(EXTRA_NOTIFICATION_CHANNEL, notificationInfo.channel)\n            it.putExtra(EXTRA_NOTIFICATION_CHANNEL_NAME, notificationInfo.channelName)\n            it.putExtra(EXTRA_NOTIFICATION_TITLE, application.getString(notificationInfo.title))\n            it.putExtra(EXTRA_SPEC_CLASS, taskSpec.javaClass.name)\n            it.putExtra(EXTRA_INPUT_DATA, HashMap(inputData))\n        }\n\n        application.startService(intent)\n    }\n}\n\nclass TaskSpecRunnerService : Service() {\n    override fun onBind(p0: Intent?): IBinder? {\n        return null\n    }\n\n    override fun onStartCommand(\n        intent: Intent?,\n        flags: Int,\n        startId: Int\n    ): Int {\n        val notificationChannel = intent!!.getStringExtra(EXTRA_NOTIFICATION_CHANNEL)!!\n        val notificationChannelName = intent.getStringExtra(EXTRA_NOTIFICATION_CHANNEL_NAME)!!\n        setupNotificationChannel(notificationChannel, notificationChannelName)\n\n        val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)\n        val notificationTitle = intent.getStringExtra(EXTRA_NOTIFICATION_TITLE)\n        val notification = NotificationCompat.Builder(applicationContext, notificationChannel)\n            .setSmallIcon(R.drawable.ic_notification_small)\n            .setContentTitle(notificationTitle)\n            .setPriority(NotificationCompat.PRIORITY_LOW)\n            .build()\n\n        ServiceCompat.startForeground(\n            this,\n            notificationId,\n            notification,\n            ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC\n        )\n\n        val inputData = intent.getSerializableExtra(EXTRA_INPUT_DATA) as HashMap<String, String>\n        val taskSpec = Class\n            .forName(intent.getStringExtra(EXTRA_SPEC_CLASS)!!)\n            .getConstructor()\n            .newInstance() as TaskSpec\n\n        CoroutineScope(Dispatchers.IO).launch {\n            try {\n                taskSpec.run(application, inputData, 1, true, { false })\n            } finally {\n                stopSelf()\n            }\n        }\n\n        return START_NOT_STICKY\n    }\n\n    private fun setupNotificationChannel(\n        notificationChannel: String,\n        notificationChannelName: String\n    ) {\n        val notificationChannel = NotificationChannel(\n            notificationChannel,\n            notificationChannelName,\n            NotificationManager.IMPORTANCE_LOW\n        )\n\n        (applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager)\n            .createNotificationChannel(notificationChannel)\n    }\n\n    companion object {\n        const val EXTRA_NOTIFICATION_ID = \"notificationId\"\n        const val EXTRA_NOTIFICATION_CHANNEL = \"notificationChannel\"\n        const val EXTRA_NOTIFICATION_CHANNEL_NAME = \"notificationChannelName\"\n        const val EXTRA_NOTIFICATION_TITLE = \"notificationTitle\"\n        const val EXTRA_SPEC_CLASS = \"specClass\"\n        const val EXTRA_INPUT_DATA = \"inputData\"\n    }\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/workmanager/TaskSpecWorker.kt",
    "content": "package org.odk.collect.async.workmanager\n\nimport android.content.Context\nimport androidx.work.Worker\nimport androidx.work.WorkerParameters\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.TaskSpec\nimport org.odk.collect.async.network.ConnectivityProvider\nimport org.odk.collect.async.run\n\nclass TaskSpecWorker(\n    context: Context,\n    workerParams: WorkerParameters\n) : Worker(context, workerParams) {\n    private var isStopped = false\n\n    private val taskSpec: TaskSpec by lazy {\n        Class\n            .forName(inputData.getString(DATA_TASK_SPEC_CLASS)!!)\n            .getConstructor()\n            .newInstance() as TaskSpec\n    }\n\n    private val stringInputData: Map<String, String> by lazy {\n        inputData.keyValueMap.mapValues { it.value.toString() }\n    }\n\n    private val connectivityProvider: ConnectivityProvider = ConnectivityProvider(context)\n\n    override fun doWork(): Result {\n        val cellularOnly = inputData.getBoolean(DATA_CELLULAR_ONLY, false)\n        if (cellularOnly && connectivityProvider.currentNetwork != Scheduler.NetworkType.CELLULAR) {\n            Analytics.Companion.setUserProperty(\"SawMeteredNonCellular\", \"true\")\n            return Result.retry()\n        }\n\n        val result = taskSpec.run(\n            applicationContext,\n            stringInputData,\n            runAttemptCount,\n            false,\n            { isStopped }\n        )\n\n        return when (result) {\n            TaskSpec.Result.SUCCESS -> Result.success()\n            TaskSpec.Result.FAILURE -> Result.failure()\n            TaskSpec.Result.RETRY -> Result.retry()\n        }\n    }\n\n    override fun onStopped() {\n        super.onStopped()\n        isStopped = true\n    }\n\n    companion object {\n        const val DATA_TASK_SPEC_CLASS = \"taskSpecClass\"\n        const val DATA_CELLULAR_ONLY = \"cellularOnly\"\n    }\n}\n"
  },
  {
    "path": "async/src/main/java/org/odk/collect/async/workmanager/WorkManagerTaskSpecScheduler.kt",
    "content": "package org.odk.collect.async.workmanager\n\nimport android.net.NetworkCapabilities\nimport android.net.NetworkRequest\nimport androidx.work.Constraints\nimport androidx.work.Data\nimport androidx.work.ExistingPeriodicWorkPolicy\nimport androidx.work.ExistingWorkPolicy\nimport androidx.work.NetworkType\nimport androidx.work.OneTimeWorkRequest\nimport androidx.work.PeriodicWorkRequest\nimport androidx.work.WorkInfo\nimport androidx.work.WorkManager\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.TaskSpec\nimport org.odk.collect.async.TaskSpecScheduler\nimport java.util.concurrent.TimeUnit\n\nclass WorkManagerTaskSpecScheduler(private val workManager: WorkManager) : TaskSpecScheduler {\n    override fun schedule(\n        tag: String,\n        spec: TaskSpec,\n        inputData: Map<String, String>,\n        networkConstraint: Scheduler.NetworkType?,\n        repeatPeriod: Long?\n    ) {\n        val constraints = getConstraints(networkConstraint)\n        val workManagerInputData = Data.Builder()\n            .putString(TaskSpecWorker.DATA_TASK_SPEC_CLASS, spec.javaClass.name)\n            .putBoolean(\n                TaskSpecWorker.DATA_CELLULAR_ONLY,\n                networkConstraint == Scheduler.NetworkType.CELLULAR\n            )\n            .putAll(inputData)\n            .build()\n\n        if (repeatPeriod != null) {\n            val builder = PeriodicWorkRequest.Builder(\n                TaskSpecWorker::class.java,\n                repeatPeriod,\n                TimeUnit.MILLISECONDS\n            )\n                .addTag(tag)\n                .setInputData(workManagerInputData)\n                .setConstraints(constraints)\n\n            spec.backoffPolicy?.let { backoffPolicy ->\n                spec.backoffDelay?.let { backoffDelay ->\n                    builder.setBackoffCriteria(backoffPolicy, backoffDelay, TimeUnit.MILLISECONDS)\n                }\n            }\n\n            workManager.enqueueUniquePeriodicWork(\n                tag,\n                ExistingPeriodicWorkPolicy.REPLACE,\n                builder.build()\n            )\n        } else {\n            val workRequest = OneTimeWorkRequest.Builder(TaskSpecWorker::class.java)\n                .addTag(tag)\n                .setConstraints(constraints)\n                .setInputData(workManagerInputData)\n                .build()\n\n            workManager.beginUniqueWork(tag, ExistingWorkPolicy.REPLACE, workRequest).enqueue()\n        }\n    }\n\n    override fun isRunning(tag: String): Boolean {\n        val statuses = workManager.getWorkInfosByTag(tag)\n        for (workInfo in statuses.get()) {\n            if (workInfo.state == WorkInfo.State.RUNNING) {\n                return true\n            }\n        }\n\n        return false\n    }\n\n    override fun cancel(tag: String) {\n        workManager.cancelUniqueWork(tag)\n    }\n\n    override fun cancelAll() {\n        workManager.cancelAllWork()\n    }\n\n    private fun getConstraints(networkConstraint: Scheduler.NetworkType?): Constraints {\n        val networkRequest = NetworkRequest.Builder().apply {\n            addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)\n\n            when (networkConstraint) {\n                Scheduler.NetworkType.WIFI -> addTransportType(NetworkCapabilities.TRANSPORT_WIFI)\n                Scheduler.NetworkType.CELLULAR -> addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)\n                else -> Unit\n            }\n        }.build()\n\n        val networkType = when (networkConstraint) {\n            Scheduler.NetworkType.WIFI -> NetworkType.UNMETERED\n            Scheduler.NetworkType.CELLULAR -> NetworkType.METERED\n            else -> NetworkType.CONNECTED\n        }\n\n        val constraints = Constraints\n            .Builder()\n            .setRequiredNetworkRequest(networkRequest, networkType)\n            .build()\n        return constraints\n    }\n}\n"
  },
  {
    "path": "async/src/test/java/org/odk/collect/async/TaskSpecTest.kt",
    "content": "package org.odk.collect.async\n\nimport android.content.Context\nimport androidx.work.BackoffPolicy\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport java.util.function.Supplier\n\nclass TaskSpecTest {\n\n    @Test\n    fun `#run doesn't return retry if isForeground is true`() {\n        val result = OneRetryTaskSpec().run(mock(), emptyMap(), 1, true, { false })\n        assertThat(result, equalTo(TaskSpec.Result.FAILURE))\n    }\n}\n\nprivate class OneRetryTaskSpec() : TaskSpec {\n    override val maxRetries: Int = 2\n    override val backoffPolicy: BackoffPolicy? = null\n    override val backoffDelay: Long? = null\n\n    override fun getTask(\n        context: Context,\n        inputData: Map<String, String>,\n        isLastUniqueExecution: Boolean,\n        isStopped: () -> Boolean\n    ): Supplier<Boolean> {\n        return Supplier {\n            false\n        }\n    }\n\n    override fun onException(exception: Throwable) {}\n}\n"
  },
  {
    "path": "async/src/test/java/org/odk/collect/async/workmanager/TaskSpecWorkerTest.kt",
    "content": "package org.odk.collect.async.workmanager\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.work.BackoffPolicy\nimport androidx.work.Data\nimport androidx.work.ListenableWorker\nimport androidx.work.Worker\nimport androidx.work.testing.TestWorkerBuilder\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.async.TaskSpec\nimport java.util.concurrent.Executors\nimport java.util.function.Supplier\n\n@RunWith(AndroidJUnit4::class)\nclass TaskSpecWorkerTest {\n    private lateinit var worker: Worker\n\n    @Before\n    fun setup() {\n        worker = TestWorkerBuilder<TaskSpecWorker>(\n            context = ApplicationProvider.getApplicationContext(),\n            executor = Executors.newSingleThreadExecutor(),\n            inputData = Data.Builder()\n                .putString(TaskSpecWorker.DATA_TASK_SPEC_CLASS, TestTaskSpec::class.java.name)\n                .build(),\n            runAttemptCount = 0 // without setting this explicitly attempts in tests are counted starting from 1 instead of 0 like in production code\n        ).build()\n\n        TestTaskSpec.reset()\n    }\n\n    @Test\n    fun `when task returns true work should succeed`() {\n        TestTaskSpec.doReturn(true)\n        assertThat(worker.doWork(), `is`(ListenableWorker.Result.success()))\n    }\n\n    @Test\n    fun `when task returns false, retries if maxRetries not specified`() {\n        TestTaskSpec.doReturn(false)\n        assertThat(worker.doWork(), `is`(ListenableWorker.Result.retry()))\n    }\n\n    @Test\n    fun `when task returns false, retries if maxRetries is specified and is higher than runAttemptCount`() {\n        TestTaskSpec\n            .withMaxRetries(1)\n            .doReturn(false)\n\n        assertThat(worker.doWork(), `is`(ListenableWorker.Result.retry()))\n    }\n\n    @Test\n    fun `when task returns false, fails if maxRetries is specified and is equal to runAttemptCount`() {\n        TestTaskSpec\n            .withMaxRetries(0)\n            .doReturn(false)\n\n        assertThat(worker.doWork(), `is`(ListenableWorker.Result.failure()))\n    }\n\n    @Test\n    fun `when task returns false, fails if maxRetries is specified and is lower than runAttemptCount`() {\n        TestTaskSpec\n            .withMaxRetries(-1)\n            .doReturn(false)\n\n        assertThat(worker.doWork(), `is`(ListenableWorker.Result.failure()))\n    }\n\n    @Test\n    fun `when maxRetries is not specified, task called with isLastUniqueExecution true`() {\n        TestTaskSpec\n            .doReturn(false)\n\n        worker.doWork()\n        assertThat(TestTaskSpec.wasLastUniqueExecution, equalTo(true))\n    }\n\n    @Test\n    fun `when maxRetries is specified and it is higher than runAttemptCount, task called with isLastUniqueExecution false`() {\n        TestTaskSpec\n            .withMaxRetries(1)\n            .doReturn(false)\n\n        worker.doWork()\n        assertThat(TestTaskSpec.wasLastUniqueExecution, equalTo(false))\n    }\n\n    @Test\n    fun `when maxRetries is specified and it is equal to runAttemptCount, task called with isLastUniqueExecution true`() {\n        TestTaskSpec\n            .withMaxRetries(0)\n            .doReturn(false)\n\n        worker.doWork()\n        assertThat(TestTaskSpec.wasLastUniqueExecution, equalTo(true))\n    }\n\n    @Test\n    fun `when maxRetries is specified and it is lower than runAttemptCount, task called with isLastUniqueExecution true`() {\n        TestTaskSpec\n            .withMaxRetries(-1)\n            .doReturn(false)\n\n        worker.doWork()\n        assertThat(TestTaskSpec.wasLastUniqueExecution, equalTo(true))\n    }\n\n    @Test\n    fun `when there is an exception, calls onException`() {\n        val exception = IllegalStateException()\n\n        TestTaskSpec\n            .doThrow(exception)\n\n        worker.doWork()\n        assertThat(TestTaskSpec.onExceptionCalledWith, equalTo(exception))\n    }\n}\n\nprivate class TestTaskSpec : TaskSpec {\n\n    companion object {\n\n        private var maxRetries: Int? = null\n        private var returnValue = true\n        private var exception: Throwable? = null\n\n        var wasLastUniqueExecution = false\n            private set\n\n        var onExceptionCalledWith: Throwable? = null\n            private set\n\n        fun reset() {\n            returnValue = true\n            maxRetries = null\n            exception = null\n            wasLastUniqueExecution = false\n            onExceptionCalledWith = null\n        }\n\n        fun doReturn(value: Boolean): Companion {\n            returnValue = value\n            return this\n        }\n\n        fun withMaxRetries(maxRetries: Int): Companion {\n            this.maxRetries = maxRetries\n            return this\n        }\n\n        fun doThrow(exception: Throwable) {\n            this.exception = exception\n        }\n    }\n\n    override val maxRetries: Int? = Companion.maxRetries\n    override val backoffPolicy: BackoffPolicy? = null\n    override val backoffDelay: Long? = null\n\n    override fun getTask(\n        context: Context,\n        inputData: Map<String, String>,\n        isLastUniqueExecution: Boolean,\n        isStopped: (() -> Boolean)\n    ): Supplier<Boolean> {\n        wasLastUniqueExecution = isLastUniqueExecution\n\n        return Supplier {\n            exception?.let {\n                throw it\n            }\n\n            returnValue\n        }\n    }\n\n    override fun onException(exception: Throwable) {\n        onExceptionCalledWith = exception\n    }\n}\n"
  },
  {
    "path": "audio-clips/.gitignore",
    "content": "/build"
  },
  {
    "path": "audio-clips/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n    namespace = \"org.odk.collect.audioclips\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(fileTree(mapOf(\"dir\" to \"libs\", \"include\" to listOf(\"*.jar\"))))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":async\"))\n    implementation(libs.androidxActivity)\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.androidxLifecycleViewmodelKtx)\n    implementation(libs.androidxLifecycleLivedataKtx)\n\n    testImplementation(project(\":androidtest\"))\n    testImplementation(project(\":test-shared\"))\n    testImplementation(libs.junit)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.androidxArchCoreTesting)\n    testImplementation(libs.hamcrest)\n}\n"
  },
  {
    "path": "audio-clips/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    /\n</manifest>"
  },
  {
    "path": "audio-clips/src/main/java/org/odk/collect/audioclips/AudioClipViewModel.kt",
    "content": "package org.odk.collect.audioclips\n\nimport android.media.MediaPlayer\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.map\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.async.Cancellable\nimport org.odk.collect.async.Scheduler\nimport java.io.File\nimport java.util.LinkedList\nimport java.util.Queue\nimport java.util.function.Supplier\n\nclass AudioClipViewModel(private val mediaPlayerFactory: Supplier<MediaPlayer>, private val scheduler: Scheduler) : ViewModel(), MediaPlayer.OnCompletionListener {\n\n    private var mediaPlayer = ThreadSafeMediaPlayerWrapper(mediaPlayerFactory::get, this)\n\n    private val currentlyPlaying = MutableLiveData<CurrentlyPlaying?>(null)\n    private val error = MutableLiveData<Consumable<Exception>?>()\n    private val positions: MutableMap<String, MutableLiveData<Int>?> = HashMap()\n    private var positionUpdatesCancellable: Cancellable? = null\n    private val isLoading = MutableLiveData(false)\n\n    fun play(clip: Clip) {\n        val playlist = LinkedList<Clip>()\n        playlist.add(clip)\n        playNext(playlist)\n    }\n\n    fun playInOrder(clips: List<Clip>) {\n        val playlist: Queue<Clip> = LinkedList(clips)\n        playNext(playlist)\n    }\n\n    fun stop() {\n        if (currentlyPlaying.value != null) {\n            mediaPlayer.stop()\n        }\n\n        cleanUpAfterClip()\n    }\n\n    fun pause() {\n        mediaPlayer.pause()\n        val currentlyPlayingValue = currentlyPlaying.value\n        if (currentlyPlayingValue != null) {\n            currentlyPlaying.value = currentlyPlayingValue.paused()\n        }\n    }\n\n    fun isPlaying(clipID: String): LiveData<Boolean> {\n        return currentlyPlaying.map { value ->\n            if (isCurrentPlayingClip(clipID, value)) {\n                !value!!.isPaused\n            } else {\n                false\n            }\n        }\n    }\n\n    fun getPosition(clipID: String): LiveData<Int> {\n        return getPositionForClip(clipID)\n    }\n\n    fun setPosition(clipID: String, newPosition: Int) {\n        if (isCurrentPlayingClip(clipID, currentlyPlaying.value)) {\n            mediaPlayer.seekTo(newPosition)\n        }\n        getPositionForClip(clipID).value = newPosition\n    }\n\n    fun background() {\n        cleanUpAfterClip()\n        mediaPlayer.release()\n    }\n\n    public override fun onCleared() {\n        background()\n    }\n\n    private fun playNext(playlist: Queue<Clip>) {\n        val nextClip = playlist.poll()\n        if (nextClip != null) {\n            if (!isCurrentPlayingClip(nextClip.clipID, currentlyPlaying.value)) {\n                loadNewClip(\n                    nextClip.uRI,\n                    onLoaded = {\n                        error.value = null\n                        startPlayBack(nextClip, playlist)\n                    },\n                    onLoadFailure = {\n                        error.value =\n                            Consumable(PlaybackFailedException(nextClip.uRI, getExceptionMsg(nextClip.uRI)))\n                        playNext(playlist)\n                    }\n                )\n            } else {\n                startPlayBack(nextClip, playlist)\n            }\n        }\n    }\n\n    private fun startPlayBack(\n        nextClip: Clip,\n        playlist: Queue<Clip>\n    ) {\n        mediaPlayer.start(getPositionForClip(nextClip.clipID).value!!)\n        currentlyPlaying.value = CurrentlyPlaying(\n            Clip(nextClip.clipID, nextClip.uRI),\n            false,\n            playlist\n        )\n\n        schedulePositionUpdates()\n    }\n\n    private fun getExceptionMsg(uri: String): Int {\n        return if (File(uri).exists()) 1 else 0\n    }\n\n    override fun onCompletion(mediaPlayer: MediaPlayer) {\n        val wasPlaying = cleanUpAfterClip()\n        if (wasPlaying != null) {\n            if (!wasPlaying.playlist.isEmpty()) {\n                playNext(wasPlaying.playlist)\n            }\n        }\n    }\n\n    private fun cleanUpAfterClip(): CurrentlyPlaying? {\n        val wasPlaying = currentlyPlaying.value\n        cancelPositionUpdates()\n        currentlyPlaying.value = null\n        if (wasPlaying != null) {\n            getPositionForClip(wasPlaying.clipID).value = 0\n        }\n        return wasPlaying\n    }\n\n    private fun getPositionForClip(clipID: String): MutableLiveData<Int> {\n        val liveData: MutableLiveData<Int>\n        if (positions.containsKey(clipID)) {\n            liveData = positions[clipID]!!\n        } else {\n            liveData = MutableLiveData()\n            liveData.value = 0\n            positions[clipID] = liveData\n        }\n        return liveData\n    }\n\n    fun getError(): LiveData<Consumable<Exception>?> {\n        return error\n    }\n\n    private fun schedulePositionUpdates() {\n        positionUpdatesCancellable = scheduler.repeat(\n            {\n                val currentlyPlaying = currentlyPlaying.value\n                val currentPosition = mediaPlayer.getPosition()\n                if (currentlyPlaying != null && currentPosition != null) {\n                    val position = getPositionForClip(currentlyPlaying.clip.clipID)\n                    position.postValue(currentPosition)\n                }\n            },\n            1000 / 12\n        ) // 12fps\n    }\n\n    private fun cancelPositionUpdates() {\n        positionUpdatesCancellable?.cancel()\n    }\n\n    private fun isCurrentPlayingClip(clipID: String, currentlyPlayingValue: CurrentlyPlaying?): Boolean {\n        return currentlyPlayingValue != null && currentlyPlayingValue.clip.clipID == clipID\n    }\n\n    private fun loadNewClip(uri: String, onLoaded: () -> Unit, onLoadFailure: () -> Unit) {\n        isLoading.value = true\n\n        scheduler.immediate(\n            background = {\n                mediaPlayer.resetWithDataSource(uri)\n            },\n            foreground = { success ->\n                if (success) {\n                    onLoaded()\n                } else {\n                    onLoadFailure()\n                }\n\n                isLoading.value = false\n            }\n        )\n    }\n\n    fun isLoading(): LiveData<Boolean> {\n        return isLoading\n    }\n\n    private class CurrentlyPlaying(val clip: Clip, val isPaused: Boolean, val playlist: Queue<Clip>) {\n\n        val clipID: String\n            get() = clip.clipID\n\n        fun paused(): CurrentlyPlaying {\n            return CurrentlyPlaying(clip, true, playlist)\n        }\n    }\n\n    class Factory(private val mediaPlayerFactory: Supplier<MediaPlayer>, private val scheduler: Scheduler) : ViewModelProvider.Factory {\n\n        @Suppress(\"UNCHECKED_CAST\")\n        override fun <T : ViewModel> create(modelClass: Class<T>): T {\n            return AudioClipViewModel(mediaPlayerFactory, scheduler) as T\n        }\n    }\n}\n"
  },
  {
    "path": "audio-clips/src/main/java/org/odk/collect/audioclips/AudioPlayer.kt",
    "content": "package org.odk.collect.audioclips\n\nimport androidx.lifecycle.LiveData\nimport java.util.function.Consumer\n\ninterface AudioPlayer {\n    fun isLoading(): LiveData<Boolean>\n\n    fun play(clip: Clip)\n\n    fun pause()\n\n    fun setPosition(clipId: String, position: Int)\n\n    fun onPlayingChanged(clipID: String, playingConsumer: Consumer<Boolean>)\n\n    fun onPositionChanged(clipID: String, positionConsumer: Consumer<Int>)\n\n    fun onPlaybackError(error: Consumer<Exception>)\n\n    fun stop()\n\n    fun playInOrder(clips: List<Clip>)\n}\n"
  },
  {
    "path": "audio-clips/src/main/java/org/odk/collect/audioclips/AudioPlayerFactory.kt",
    "content": "package org.odk.collect.audioclips\n\nimport androidx.activity.ComponentActivity\nimport androidx.lifecycle.LifecycleOwner\n\ninterface AudioPlayerFactory {\n    fun create(\n        activity: ComponentActivity,\n        lifecycleOwner: LifecycleOwner\n    ): AudioPlayer\n}\n"
  },
  {
    "path": "audio-clips/src/main/java/org/odk/collect/audioclips/Clip.kt",
    "content": "package org.odk.collect.audioclips\n\ndata class Clip(val clipID: String, val uRI: String)\n"
  },
  {
    "path": "audio-clips/src/main/java/org/odk/collect/audioclips/PlaybackFailedException.kt",
    "content": "package org.odk.collect.audioclips\n\ndata class PlaybackFailedException(val uRI: String, val exceptionMsg: Int) : Exception()\n"
  },
  {
    "path": "audio-clips/src/main/java/org/odk/collect/audioclips/ThreadSafeMediaPlayerWrapper.kt",
    "content": "package org.odk.collect.audioclips\n\nimport android.media.MediaPlayer\nimport android.os.StrictMode\nimport java.io.IOException\n\n/**\n * Allows a [MediaPlayer] to be interacted with from multiple threads while avoiding illegal states\n * such as multiple threads attempting to set up data sources at the same time (with interleaved\n * calls to [MediaPlayer.reset] for example).\n *\n * This also handles on-demand creation and allowing the [MediaPlayer] instance to be garbage\n * collected after calls to [release].\n */\ninternal class ThreadSafeMediaPlayerWrapper(\n    private val mediaPlayerFactory: () -> MediaPlayer,\n    private var onCompletionListener: MediaPlayer.OnCompletionListener\n) {\n\n    private var mediaPlayer: MediaPlayer? = null\n\n    fun resetWithDataSource(uri: String): Boolean {\n        synchronized(this) {\n            val mediaPlayer = mediaPlayer ?: run {\n                val newMediaPlayer = setupNewMediaPlayer()\n                mediaPlayer = newMediaPlayer\n                newMediaPlayer\n            }\n\n            return try {\n                mediaPlayer.reset()\n                mediaPlayer.setDataSource(uri)\n                mediaPlayer.prepare()\n                true\n            } catch (e: IOException) {\n                false\n            }\n        }\n    }\n\n    fun start(position: Int) {\n        synchronized(this) {\n            mediaPlayer?.seekTo(position)\n            mediaPlayer?.start()\n        }\n    }\n\n    fun pause() {\n        synchronized(this) {\n            mediaPlayer?.pause()\n        }\n    }\n\n    fun stop() {\n        synchronized(this) {\n            mediaPlayer?.stop()\n        }\n    }\n\n    fun seekTo(newPosition: Int) {\n        synchronized(this) {\n            mediaPlayer?.seekTo(newPosition)\n        }\n    }\n\n    fun getPosition(): Int? {\n        return mediaPlayer?.currentPosition\n    }\n\n    fun release() {\n        synchronized(this) {\n            mediaPlayer?.release()\n            mediaPlayer = null\n        }\n    }\n\n    private fun setupNewMediaPlayer(): MediaPlayer {\n        StrictMode.noteSlowCall(\"MediaPlayer instantiation can be slow\")\n\n        val newMediaPlayer: MediaPlayer = mediaPlayerFactory()\n        newMediaPlayer.setOnCompletionListener(onCompletionListener)\n        return newMediaPlayer\n    }\n}\n"
  },
  {
    "path": "audio-clips/src/test/java/org/odk/collect/audioclips/AudioClipViewModelTest.kt",
    "content": "package org.odk.collect.audioclips\n\nimport android.media.MediaPlayer\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.ArgumentCaptor\nimport org.mockito.ArgumentMatchers\nimport org.mockito.Mockito.doThrow\nimport org.mockito.Mockito.inOrder\nimport org.mockito.Mockito.mock\nimport org.mockito.Mockito.never\nimport org.mockito.Mockito.times\nimport org.mockito.Mockito.verify\nimport org.mockito.Mockito.`when`\nimport org.odk.collect.androidtest.getOrAwaitValue\nimport org.odk.collect.testshared.FakeScheduler\nimport java.io.File\nimport java.io.IOException\nimport java.util.function.Supplier\n\n@RunWith(AndroidJUnit4::class)\nclass AudioClipViewModelTest {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    private val mediaPlayer = mock(MediaPlayer::class.java)\n    private val fakeScheduler = FakeScheduler()\n    private var viewModel: AudioClipViewModel = AudioClipViewModel(Supplier { mediaPlayer }, fakeScheduler)\n\n    @Test\n    fun play_resetsAndPreparesAndStartsMediaPlayer() {\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        val inOrder = inOrder(mediaPlayer)\n        inOrder.verify(mediaPlayer).reset()\n        inOrder.verify(mediaPlayer).setDataSource(\"file://audio.mp3\")\n        inOrder.verify(mediaPlayer).prepare()\n        inOrder.verify(mediaPlayer).start()\n    }\n\n    @Test\n    fun play_whenAlreadyingPlayingClip_startsMediaPlayer() {\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        verify(mediaPlayer, times(1)).reset()\n        verify(mediaPlayer, times(2)).start()\n    }\n\n    @Test\n    fun play_whenClipHasPositionSet_startsAtPosition() {\n        viewModel.setPosition(\"clip1\", 4321)\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        val inOrder = inOrder(mediaPlayer)\n        inOrder.verify(mediaPlayer).seekTo(4321)\n        inOrder.verify(mediaPlayer).start()\n    }\n\n    @Test\n    fun playMultipleClips_updatesProgress_forAllClips() {\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        assertThat(fakeScheduler.isRepeatRunning(), equalTo(true))\n\n        viewModel.onCleared()\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n        assertThat(fakeScheduler.isRepeatRunning(), equalTo(true))\n    }\n\n    @Test\n    fun playInOrder_playsClipsOneAfterTheOther_andUpdatesProgress() {\n        viewModel.playInOrder(\n            listOf(\n                Clip(\"clip1\", \"file://audio1.mp3\"),\n                Clip(\"clip2\", \"file://audio2.mp3\")\n            )\n        )\n        fakeScheduler.flush()\n\n        val captor = ArgumentCaptor.forClass(MediaPlayer.OnCompletionListener::class.java)\n        verify(mediaPlayer).setOnCompletionListener(captor.capture())\n        val onCompletionListener = captor.value\n\n        verify(mediaPlayer).setDataSource(\"file://audio1.mp3\")\n        verify(mediaPlayer, times(1)).start()\n        assertThat(fakeScheduler.isRepeatRunning(), equalTo(true))\n\n        onCompletionListener.onCompletion(mediaPlayer)\n        fakeScheduler.flush()\n        verify(mediaPlayer).setDataSource(\"file://audio2.mp3\")\n        verify(mediaPlayer, times(2)).start()\n        assertThat(fakeScheduler.isRepeatRunning(), equalTo(true))\n\n        onCompletionListener.onCompletion(mediaPlayer)\n        fakeScheduler.flush()\n        verify(mediaPlayer, times(2)).start()\n        assertThat(fakeScheduler.isRepeatRunning(), equalTo(false))\n    }\n\n    @Test\n    fun playInOrder_whenThereIsAnErrorContinuesWithNextClip() {\n        doThrow(IOException::class.java).`when`(mediaPlayer).setDataSource(\"file://missing.mp3\")\n        viewModel.playInOrder(\n            listOf(\n                Clip(\"clip1\", \"file://missing.mp3\"),\n                Clip(\"clip2\", \"file://not-missing.mp3\")\n            )\n        )\n        fakeScheduler.flush()\n        fakeScheduler.flush()\n\n        verify(mediaPlayer).setDataSource(\"file://not-missing.mp3\")\n        verify(mediaPlayer, times(1)).start()\n    }\n\n    @Test\n    fun play_afterAPlayInOrder_doesNotContinuePlayingClips() {\n        viewModel.playInOrder(\n            listOf(\n                Clip(\"clip1\", \"file://audio1.mp3\"),\n                Clip(\"clip2\", \"file://audio2.mp3\")\n            )\n        )\n        fakeScheduler.flush()\n\n        viewModel.play(Clip(\"clip3\", \"file://audio3.mp3\"))\n        fakeScheduler.flush()\n\n        verify(mediaPlayer, times(2)).start()\n        verify(mediaPlayer).setDataSource(\"file://audio1.mp3\")\n        verify(mediaPlayer).setDataSource(\"file://audio3.mp3\")\n\n        val captor = ArgumentCaptor.forClass(MediaPlayer.OnCompletionListener::class.java)\n        verify(mediaPlayer).setOnCompletionListener(captor.capture())\n        val onCompletionListener = captor.value\n        onCompletionListener.onCompletion(mediaPlayer)\n        fakeScheduler.flush()\n\n        verify(mediaPlayer, never()).setDataSource(\"file://audio2.mp3\")\n        verify(mediaPlayer, times(2)).start()\n    }\n\n    @Test\n    fun isPlaying_whenNothingPlaying_is_false() {\n        val isPlaying = viewModel.isPlaying(\"clip1\")\n        assertThat(isPlaying.getOrAwaitValue(), equalTo(false))\n    }\n\n    @Test\n    fun isPlaying_whenClipIDPlaying_is_PLAYING() {\n        val isPlaying = viewModel.isPlaying(\"clip1\")\n\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n        assertThat(isPlaying.getOrAwaitValue(), equalTo(true))\n    }\n\n    @Test\n    fun isPlaying_whenDifferentClipIDPlaying_is_false() {\n        val isPlaying = viewModel.isPlaying(\"clip2\")\n\n        viewModel.play(Clip(\"clip1\", \"file://other.mp3\"))\n        fakeScheduler.flush()\n        assertThat(isPlaying.getOrAwaitValue(), equalTo(false))\n    }\n\n    @Test\n    fun isPlaying_whenClipIDPlaying_thenCompleted_is_false() {\n        val isPlaying = viewModel.isPlaying(\"clip1\")\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        val captor = ArgumentCaptor.forClass(MediaPlayer.OnCompletionListener::class.java)\n        verify(mediaPlayer).setOnCompletionListener(captor.capture())\n        captor.value.onCompletion(mediaPlayer)\n        assertThat(isPlaying.getOrAwaitValue(), equalTo(false))\n    }\n\n    @Test\n    fun isPlaying_whenPlaybackFails_is_false() {\n        doThrow(IOException::class.java).`when`(mediaPlayer).setDataSource(\"file://missing.mp3\")\n        val isPlaying = viewModel.isPlaying(\"clip1\")\n\n        viewModel.play(Clip(\"clip1\", \"file://missing.mp3\"))\n        fakeScheduler.flush()\n        assertThat(isPlaying.getOrAwaitValue(), equalTo(false))\n    }\n\n    @Test\n    fun stop_stopsMediaPlayer() {\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        viewModel.stop()\n        verify(mediaPlayer).stop()\n    }\n\n    @Test\n    fun stop_beforePlay_doesntCallStopOnMediaPlayer() {\n        viewModel.stop()\n        verify(mediaPlayer, never()).stop()\n    }\n\n    @Test\n    fun stop_resetsPosition() {\n        val position = viewModel.getPosition(\"clip1\")\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        `when`(mediaPlayer.currentPosition).thenReturn(1000)\n        fakeScheduler.runForeground()\n        viewModel.stop()\n        assertThat(position.getOrAwaitValue(), equalTo(0))\n    }\n\n    @Test\n    fun background_releasesMediaPlayer() {\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        viewModel.background()\n        verify(mediaPlayer).release()\n    }\n\n    @Test\n    fun background_cancelsScheduler() {\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        viewModel.background()\n        assertThat(fakeScheduler.isRepeatRunning(), equalTo(false))\n    }\n\n    @Test\n    fun isPlaying_whenPlayingAndThenBackgrounding_is_false() {\n        val isPlaying = viewModel.isPlaying(\"clip1\")\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        viewModel.background()\n        assertThat(isPlaying.getOrAwaitValue(), equalTo(false))\n    }\n\n    @Test\n    fun play_afterBackground_createsANewMediaPlayer() {\n        val factory = RecordingMockMediaPlayerFactory()\n        val scheduler = FakeScheduler()\n        val viewModel = AudioClipViewModel(factory, scheduler)\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        scheduler.flush()\n        assertThat(factory.createdInstances.size, equalTo(1))\n        verify(factory.createdInstances[0]).start()\n\n        viewModel.background()\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        scheduler.flush()\n        assertThat(factory.createdInstances.size, equalTo(2))\n        verify(factory.createdInstances[1]).start()\n    }\n\n    @Test\n    fun pause_pausesMediaPlayer() {\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        viewModel.pause()\n        verify(mediaPlayer).pause()\n    }\n\n    @Test\n    fun isPlaying_afterPause_is_false() {\n        val isPlaying = viewModel.isPlaying(\"clip1\")\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        viewModel.pause()\n        assertThat(isPlaying.getOrAwaitValue(), equalTo(false))\n    }\n\n    @Test\n    fun isPlaying_afterPause_andThenPlay_is_true() {\n        val isPlaying = viewModel.isPlaying(\"clip1\")\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        viewModel.pause()\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        assertThat(isPlaying.getOrAwaitValue(), equalTo(true))\n    }\n\n    @Test\n    fun position_returnsMediaPlayerPositionInMilliseconds() {\n        `when`(mediaPlayer.currentPosition).thenReturn(0)\n        val position = viewModel.getPosition(\"clip1\")\n        assertThat(position.getOrAwaitValue(), equalTo(0))\n\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        `when`(mediaPlayer.currentPosition).thenReturn(1000)\n        fakeScheduler.runForeground()\n        assertThat(position.value, equalTo(1000))\n\n        `when`(mediaPlayer.currentPosition).thenReturn(24135)\n        fakeScheduler.runForeground()\n        assertThat(position.value, equalTo(24135))\n    }\n\n    @Test\n    fun position_worksWhenMultipleClipsArePlayed() {\n        `when`(mediaPlayer.currentPosition).thenReturn(0)\n        val position1 = viewModel.getPosition(\"clip1\")\n        val position2 = viewModel.getPosition(\"clip2\")\n\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        `when`(mediaPlayer.currentPosition).thenReturn(1000)\n        fakeScheduler.runForeground()\n        assertThat(position1.getOrAwaitValue(), equalTo(1000))\n        assertThat(position2.getOrAwaitValue(), equalTo(0))\n\n        viewModel.play(Clip(\"clip2\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        `when`(mediaPlayer.currentPosition).thenReturn(2500)\n        fakeScheduler.runForeground()\n        assertThat(position1.getOrAwaitValue(), equalTo(1000))\n        assertThat(position2.getOrAwaitValue(), equalTo(2500))\n    }\n\n    @Test\n    fun setPosition_whenClipIsPlaying_seeksMediaPlayer() {\n        `when`(mediaPlayer.duration).thenReturn(100000)\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        viewModel.setPosition(\"clip1\", 54321)\n        verify(mediaPlayer).seekTo(54321)\n    }\n\n    @Test\n    fun setPosition_whenClipIsNotPlaying_doesNothing() {\n        viewModel.setPosition(\"clip1\", 54321)\n        verify(mediaPlayer, never()).seekTo(ArgumentMatchers.anyInt())\n    }\n\n    @Test\n    fun setPosition_updatesPosition() {\n        val duration = viewModel.getPosition(\"clip1\")\n        viewModel.setPosition(\"clip1\", 54321)\n        assertThat(duration.getOrAwaitValue(), equalTo(54321))\n    }\n\n    @Test\n    fun onCleared_releasesMediaPlayer() {\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        viewModel.onCleared()\n        verify(mediaPlayer).release()\n    }\n\n    @Test\n    fun onCleared_cancelsScheduler() {\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        viewModel.onCleared()\n        assertThat(fakeScheduler.isRepeatRunning(), equalTo(false))\n    }\n\n    @Test\n    fun whenPlaybackCompletes_cancelsScheduler() {\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        val captor = ArgumentCaptor.forClass(MediaPlayer.OnCompletionListener::class.java)\n        verify(mediaPlayer).setOnCompletionListener(captor.capture())\n        captor.value.onCompletion(mediaPlayer)\n        assertThat(fakeScheduler.isRepeatRunning(), equalTo(false))\n    }\n\n    @Test\n    fun whenPlaybackCompletes_resetsPosition() {\n        val position = viewModel.getPosition(\"clip1\")\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n\n        `when`(mediaPlayer.currentPosition).thenReturn(1000)\n        fakeScheduler.runForeground()\n        val captor = ArgumentCaptor.forClass(MediaPlayer.OnCompletionListener::class.java)\n        verify(mediaPlayer).setOnCompletionListener(captor.capture())\n        captor.value.onCompletion(mediaPlayer)\n        assertThat(position.getOrAwaitValue(), equalTo(0))\n    }\n\n    @Test\n    fun error_whenPlaybackFailsBecauseOfMissingFile_is_PlaybackFailed() {\n        val error = viewModel.getError()\n        doThrow(IOException::class.java).`when`(mediaPlayer).setDataSource(\"file://missing.mp3\")\n        viewModel.play(Clip(\"clip1\", \"file://missing.mp3\"))\n        fakeScheduler.flush()\n\n        assertThat(error.getOrAwaitValue()!!.value, equalTo(PlaybackFailedException(\"file://missing.mp3\", 0)))\n    }\n\n    @Test\n    fun error_whenPlaybackFailsBecauseOfInvalidFile_is_PlaybackFailed() {\n        val error = viewModel.getError()\n        val invalid = File.createTempFile(\"invalid\", \".mp3\")\n        doThrow(IOException::class.java).`when`(mediaPlayer).setDataSource(invalid.absolutePath)\n        viewModel.play(Clip(\"clip1\", invalid.absolutePath))\n        fakeScheduler.flush()\n\n        assertThat(error.getOrAwaitValue()!!.value, equalTo(PlaybackFailedException(invalid.absolutePath, 1)))\n    }\n\n    @Test\n    fun play_clearsErrorWhenItWorks() {\n        val error = viewModel.getError()\n        val invalid = File.createTempFile(\"invalid\", \".mp3\")\n        doThrow(IOException::class.java).`when`(mediaPlayer).setDataSource(invalid.absolutePath)\n        viewModel.play(Clip(\"clip1\", invalid.absolutePath))\n        fakeScheduler.flush()\n        assertThat(error.getOrAwaitValue()!!.value, equalTo(PlaybackFailedException(invalid.absolutePath, 1)))\n\n        viewModel.play(Clip(\"clip1\", \"file://audio.mp3\"))\n        fakeScheduler.flush()\n        assertThat(error.getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun isLoading_isTrueWhenLoadingClip() {\n        val loading = viewModel.isLoading()\n        assertThat(loading.getOrAwaitValue(), equalTo(false))\n\n        viewModel.play(Clip(\"clip1\", \"file://missing.mp3\"))\n        assertThat(loading.getOrAwaitValue(), equalTo(true))\n\n        fakeScheduler.flush()\n        assertThat(loading.getOrAwaitValue(), equalTo(false))\n    }\n\n    private class RecordingMockMediaPlayerFactory : Supplier<MediaPlayer> {\n        var createdInstances: MutableList<MediaPlayer> = ArrayList()\n        override fun get(): MediaPlayer {\n            val mock = mock(MediaPlayer::class.java)\n            createdInstances.add(mock)\n            return mock\n        }\n    }\n}\n"
  },
  {
    "path": "audio-recorder/.gitignore",
    "content": "/build"
  },
  {
    "path": "audio-recorder/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests.isIncludeAndroidResources = true\n    }\n    namespace = \"org.odk.collect.audiorecorder\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(fileTree(mapOf(\"dir\" to \"libs\", \"include\" to listOf(\"*.jar\"))))\n    implementation(project(\":icons\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":async\"))\n    implementation(project(\":androidshared\"))\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.androidxAppcompat)\n    implementation(libs.androidxLifecycleLivedataKtx)\n    implementation(libs.dagger)\n    ksp(libs.daggerCompiler)\n    implementation(libs.timber)\n\n    testImplementation(project(\":androidtest\"))\n    testImplementation(project(\":test-shared\"))\n    testImplementation(project(\":service-test\"))\n    testImplementation(libs.junit)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.androidxTestCoreKtx)\n    testImplementation(libs.androidxTestRules)\n    testImplementation(libs.androidxArchCoreTesting)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.hamcrest)\n}\n"
  },
  {
    "path": "audio-recorder/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_MICROPHONE\" />\n\n    <application>\n        <service android:name=\".recording.AudioRecorderService\"\n            android:foregroundServiceType=\"microphone\"/>\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/DaggerSetup.kt",
    "content": "package org.odk.collect.audiorecorder\n\nimport android.app.Application\nimport android.media.MediaRecorder\nimport dagger.BindsInstance\nimport dagger.Component\nimport dagger.Module\nimport dagger.Provides\nimport kotlinx.coroutines.Dispatchers\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.async.coroutines.CoroutineTaskRunner\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.SchedulerBuilder\nimport org.odk.collect.audiorecorder.mediarecorder.AACRecordingResource\nimport org.odk.collect.audiorecorder.mediarecorder.AMRRecordingResource\nimport org.odk.collect.audiorecorder.recorder.Output\nimport org.odk.collect.audiorecorder.recorder.Recorder\nimport org.odk.collect.audiorecorder.recorder.RecordingResourceRecorder\nimport org.odk.collect.audiorecorder.recording.AudioRecorderService\nimport java.io.File\nimport javax.inject.Singleton\n\n/**\n * This module follows the Android docs's multi-module example for Dagger. Any application that\n * depends on this module should implement the provider interface and return a constructed\n * component. This gives the application the opportunity to override dependencies if it wants to.\n *\n * @see [Using Dagger in multi-module apps](https://developer.android.com/training/dependency-injection/dagger-multi-module)\n */\n\ninterface AudioRecorderDependencyComponentProvider {\n    val audioRecorderDependencyComponent: AudioRecorderDependencyComponent\n}\n\n@Component(modules = [AudioRecorderDependencyModule::class])\n@Singleton\ninterface AudioRecorderDependencyComponent {\n\n    @Component.Builder\n    interface Builder {\n\n        @BindsInstance\n        fun application(application: Application): Builder\n\n        fun dependencyModule(audioRecorderDependencyModule: AudioRecorderDependencyModule): Builder\n\n        fun build(): AudioRecorderDependencyComponent\n    }\n\n    fun inject(activity: AudioRecorderService)\n}\n\n@Module\nopen class AudioRecorderDependencyModule {\n\n    @Provides\n    open fun providesCacheDir(application: Application): File {\n        val externalFilesDir = application.getExternalFilesDir(null)\n        return File(externalFilesDir, \"recordings\").also { it.mkdirs() }\n    }\n\n    @Provides\n    open fun providesRecorder(cacheDir: File): Recorder {\n        return RecordingResourceRecorder(cacheDir) { output ->\n            when (output) {\n                Output.AMR -> AMRRecordingResource(MediaRecorder())\n                Output.AAC -> AACRecordingResource(MediaRecorder(), 64)\n                Output.AAC_LOW -> AACRecordingResource(MediaRecorder(), 24)\n            }\n        }\n    }\n\n    @Provides\n    open fun providesScheduler(application: Application): Scheduler {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesUniqueIdGenerator(): UniqueIdGenerator {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/mediarecorder/MediaRecorderRecordingResource.kt",
    "content": "package org.odk.collect.audiorecorder.mediarecorder\n\nimport android.media.MediaRecorder\nimport org.odk.collect.audiorecorder.recorder.RecordingResource\n\ninternal abstract class MediaRecorderRecordingResource(private val mediaRecorder: MediaRecorder) : RecordingResource {\n\n    protected abstract fun beforePrepare(mediaRecorder: MediaRecorder)\n\n    override fun setOutputFile(path: String) {\n        mediaRecorder.setOutputFile(path)\n    }\n\n    override fun prepare() {\n        beforePrepare(mediaRecorder)\n        mediaRecorder.prepare()\n    }\n\n    @Throws(IllegalStateException::class)\n    override fun start() {\n        mediaRecorder.start()\n    }\n\n    override fun pause() {\n        mediaRecorder.pause()\n    }\n\n    override fun resume() {\n        mediaRecorder.resume()\n    }\n\n    override fun stop() {\n        mediaRecorder.stop()\n    }\n\n    override fun release() {\n        mediaRecorder.release()\n    }\n\n    override fun getMaxAmplitude(): Int {\n        return mediaRecorder.maxAmplitude\n    }\n}\n\ninternal class AACRecordingResource(mediaRecorder: MediaRecorder, private val kbitRate: Int) : MediaRecorderRecordingResource(mediaRecorder) {\n\n    override fun beforePrepare(mediaRecorder: MediaRecorder) {\n        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)\n        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)\n        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)\n        mediaRecorder.setAudioSamplingRate(32000)\n        mediaRecorder.setAudioEncodingBitRate(kbitRate * 1000)\n    }\n}\n\ninternal class AMRRecordingResource(mediaRecorder: MediaRecorder) : MediaRecorderRecordingResource(mediaRecorder) {\n\n    override fun beforePrepare(mediaRecorder: MediaRecorder) {\n        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)\n        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB)\n        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)\n        mediaRecorder.setAudioSamplingRate(8000)\n        mediaRecorder.setAudioEncodingBitRate(12200)\n    }\n\n    override fun stop() {\n        resume()\n        super.stop()\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/recorder/Recorder.kt",
    "content": "package org.odk.collect.audiorecorder.recorder\n\nimport org.odk.collect.audiorecorder.recording.MicInUseException\nimport org.odk.collect.audiorecorder.recording.SetupException\nimport java.io.File\n\ninterface Recorder {\n\n    @Throws(SetupException::class, MicInUseException::class)\n    fun start(output: Output)\n    fun pause()\n    fun resume()\n    fun stop(): File\n    fun cancel()\n\n    val amplitude: Int\n    fun isRecording(): Boolean\n}\n\nenum class Output {\n    AMR,\n    AAC,\n    AAC_LOW\n}\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResource.kt",
    "content": "package org.odk.collect.audiorecorder.recorder\n\nimport java.io.IOException\n\n/**\n * Allows faking/stubbing/mocking with our interactions with Android's MediaRecorder. Could also\n * wrap multiple implementations in the future.\n */\n\ninternal interface RecordingResource {\n    fun setOutputFile(path: String)\n\n    @Throws(IOException::class)\n    fun prepare()\n\n    @Throws(IllegalStateException::class)\n    fun start()\n\n    fun pause()\n    fun resume()\n    fun stop()\n    fun release()\n    fun getMaxAmplitude(): Int\n}\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorder.kt",
    "content": "package org.odk.collect.audiorecorder.recorder\n\nimport org.odk.collect.audiorecorder.recording.MicInUseException\nimport org.odk.collect.audiorecorder.recording.SetupException\nimport timber.log.Timber\nimport java.io.File\nimport java.io.IOException\n\ninternal class RecordingResourceRecorder(private val cacheDir: File, private val recordingResourceFactory: (Output) -> RecordingResource) : Recorder {\n\n    override val amplitude: Int\n        get() = recordingResource?.getMaxAmplitude() ?: 0\n\n    private var recordingResource: RecordingResource? = null\n    private var file: File? = null\n\n    @Throws(SetupException::class, MicInUseException::class)\n    override fun start(output: Output) {\n        recordingResource = recordingResourceFactory(output).also {\n            val suffix = when (output) {\n                Output.AMR -> \".amr\"\n                Output.AAC -> \".m4a\"\n                Output.AAC_LOW -> \".m4a\"\n            }\n\n            val tempFile = try {\n                File.createTempFile(\"recording\", suffix, cacheDir)\n            } catch (e: IOException) {\n                throw SetupException()\n            }\n\n            it.setOutputFile(tempFile.absolutePath)\n            file = tempFile\n\n            try {\n                it.prepare()\n            } catch (e: IOException) {\n                Timber.e(e)\n                throw SetupException()\n            }\n\n            try {\n                it.start()\n            } catch (e: IllegalStateException) {\n                throw MicInUseException()\n            }\n        }\n    }\n\n    override fun pause() {\n        recordingResource?.pause()\n    }\n\n    override fun resume() {\n        recordingResource?.resume()\n    }\n\n    override fun stop(): File {\n        stopAndReleaseMediaRecorder()\n        return file!!\n    }\n\n    override fun cancel() {\n        stopAndReleaseMediaRecorder()\n        file?.delete()\n    }\n\n    override fun isRecording(): Boolean {\n        return recordingResource != null\n    }\n\n    private fun stopAndReleaseMediaRecorder() {\n        recordingResource?.apply {\n            stop()\n            release()\n        }\n\n        recordingResource = null\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorder.kt",
    "content": "package org.odk.collect.audiorecorder.recording\n\nimport androidx.lifecycle.LiveData\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.audiorecorder.recorder.Output\nimport java.io.File\nimport java.io.Serializable\n\n/**\n * Interface for a component that records audio. Can only record once session\n * at a time.\n */\nabstract class AudioRecorder {\n    abstract fun isRecording(): Boolean\n    abstract fun getCurrentSession(): LiveData<RecordingSession?>\n    abstract fun failedToStart(): LiveData<Consumable<Exception?>>\n\n    abstract fun start(sessionId: Serializable, output: Output)\n    abstract fun pause()\n    abstract fun resume()\n    abstract fun stop()\n\n    /**\n     * Stops any in progress recordings, clears recordings (returned from `getRecordings`). Should\n     * be called after in-progress or finished recordings are no longer needed\n     */\n    abstract fun cleanUp()\n}\n\ndata class RecordingSession(val id: Serializable, val file: File?, val duration: Long, val amplitude: Int, val paused: Boolean)\n\nclass SetupException : java.lang.Exception()\nclass MicInUseException : java.lang.Exception()\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderFactory.kt",
    "content": "package org.odk.collect.audiorecorder.recording\n\nimport android.app.Application\nimport org.odk.collect.androidshared.data.getState\nimport org.odk.collect.audiorecorder.recording.internal.ForegroundServiceAudioRecorder\nimport org.odk.collect.audiorecorder.recording.internal.RecordingRepository\n\nopen class AudioRecorderFactory(private val application: Application) {\n\n    open fun create(): AudioRecorder {\n        return ForegroundServiceAudioRecorder(application, RecordingRepository(application.getState()))\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/AudioRecorderService.kt",
    "content": "package org.odk.collect.audiorecorder.recording\n\nimport android.app.Application\nimport android.app.Service\nimport android.content.Intent\nimport android.os.IBinder\nimport org.odk.collect.androidshared.data.getState\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.async.Cancellable\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.audiorecorder.AudioRecorderDependencyComponentProvider\nimport org.odk.collect.audiorecorder.recorder.Output\nimport org.odk.collect.audiorecorder.recorder.Recorder\nimport org.odk.collect.audiorecorder.recording.internal.RecordingForegroundServiceNotification\nimport org.odk.collect.audiorecorder.recording.internal.RecordingRepository\nimport java.io.Serializable\nimport javax.inject.Inject\n\nclass AudioRecorderService : Service() {\n\n    @Inject\n    internal lateinit var recorder: Recorder\n\n    @Inject\n    internal lateinit var scheduler: Scheduler\n\n    @Inject\n    internal lateinit var uniqueIdGenerator: UniqueIdGenerator\n\n    private lateinit var recordingRepository: RecordingRepository\n    private lateinit var notification: RecordingForegroundServiceNotification\n\n    private var duration = 0L\n    private var durationUpdates: Cancellable? = null\n    private var amplitudeUpdates: Cancellable? = null\n\n    override fun onCreate() {\n        super.onCreate()\n        val provider = applicationContext as AudioRecorderDependencyComponentProvider\n        provider.audioRecorderDependencyComponent.inject(this)\n\n        recordingRepository = RecordingRepository((applicationContext as Application).getState())\n        notification = RecordingForegroundServiceNotification(this, recordingRepository, uniqueIdGenerator)\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        when (intent?.action) {\n            ACTION_START -> {\n                val sessionId = intent.getSerializableExtra(EXTRA_SESSION_ID)\n                val output = intent.getSerializableExtra(EXTRA_OUTPUT) as Output\n\n                if (!recorder.isRecording() && sessionId != null) {\n                    startRecording(sessionId, output)\n                }\n            }\n\n            ACTION_PAUSE -> {\n                if (recorder.isRecording()) {\n                    recorder.pause()\n                    recordingRepository.setPaused(true)\n\n                    stopUpdates()\n                }\n            }\n\n            ACTION_RESUME -> {\n                if (recorder.isRecording()) {\n                    recorder.resume()\n                    recordingRepository.setPaused(false)\n\n                    startUpdates()\n                }\n            }\n\n            ACTION_STOP -> {\n                if (recorder.isRecording()) {\n                    stopRecording()\n                }\n            }\n\n            ACTION_CLEAN_UP -> {\n                cleanUp()\n            }\n        }\n\n        return START_NOT_STICKY\n    }\n\n    private fun startRecording(sessionId: Serializable, output: Output) {\n        notification.show()\n\n        try {\n            recorder.start(output)\n            recordingRepository.start(sessionId)\n            startUpdates()\n        } catch (e: Exception) {\n            notification.dismiss()\n            recordingRepository.failToStart(e)\n        }\n    }\n\n    override fun onTaskRemoved(rootIntent: Intent?) {\n        super.onTaskRemoved(rootIntent)\n        cleanUp()\n    }\n\n    override fun onBind(p0: Intent?): IBinder? {\n        return null\n    }\n\n    private fun stopRecording() {\n        stopUpdates()\n        notification.dismiss()\n\n        val file = recorder.stop()\n        recordingRepository.recordingReady(file)\n    }\n\n    private fun cleanUp() {\n        stopUpdates()\n        notification.dismiss()\n\n        recorder.cancel()\n        recordingRepository.clear()\n    }\n\n    private fun startUpdates() {\n        durationUpdates = scheduler.repeat(\n            {\n                recordingRepository.setDuration(duration)\n                duration += 1000\n            },\n            1000L\n        )\n\n        amplitudeUpdates = scheduler.repeat({ recordingRepository.setAmplitude(recorder.amplitude) }, 100L)\n    }\n\n    private fun stopUpdates() {\n        amplitudeUpdates?.cancel()\n        durationUpdates?.cancel()\n    }\n\n    companion object {\n        const val ACTION_START = \"START\"\n        const val ACTION_PAUSE = \"PAUSE\"\n        const val ACTION_RESUME = \"RESUME\"\n        const val ACTION_STOP = \"STOP\"\n        const val ACTION_CLEAN_UP = \"CLEAN_UP\"\n\n        const val EXTRA_SESSION_ID = \"EXTRA_SESSION_ID\"\n        const val EXTRA_OUTPUT = \"EXTRA_OUTPUT\"\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorder.kt",
    "content": "package org.odk.collect.audiorecorder.recording.internal\n\nimport android.app.Application\nimport android.content.Intent\nimport androidx.lifecycle.LiveData\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.audiorecorder.recorder.Output\nimport org.odk.collect.audiorecorder.recording.AudioRecorder\nimport org.odk.collect.audiorecorder.recording.AudioRecorderService\nimport org.odk.collect.audiorecorder.recording.AudioRecorderService.Companion.ACTION_CLEAN_UP\nimport org.odk.collect.audiorecorder.recording.AudioRecorderService.Companion.ACTION_PAUSE\nimport org.odk.collect.audiorecorder.recording.AudioRecorderService.Companion.ACTION_RESUME\nimport org.odk.collect.audiorecorder.recording.AudioRecorderService.Companion.ACTION_START\nimport org.odk.collect.audiorecorder.recording.AudioRecorderService.Companion.ACTION_STOP\nimport org.odk.collect.audiorecorder.recording.AudioRecorderService.Companion.EXTRA_OUTPUT\nimport org.odk.collect.audiorecorder.recording.AudioRecorderService.Companion.EXTRA_SESSION_ID\nimport org.odk.collect.audiorecorder.recording.RecordingSession\nimport java.io.Serializable\n\ninternal class ForegroundServiceAudioRecorder internal constructor(private val application: Application, private val recordingRepository: RecordingRepository) : AudioRecorder() {\n\n    override fun isRecording(): Boolean {\n        val currentSession = recordingRepository.currentSession.value\n        return currentSession != null && currentSession.file == null\n    }\n\n    override fun getCurrentSession(): LiveData<RecordingSession?> {\n        return recordingRepository.currentSession\n    }\n\n    override fun failedToStart(): LiveData<Consumable<Exception?>> {\n        return recordingRepository.failedToStart\n    }\n\n    override fun start(sessionId: Serializable, output: Output) {\n        application.startService(\n            Intent(application, AudioRecorderService::class.java).apply {\n                action = ACTION_START\n                putExtra(EXTRA_SESSION_ID, sessionId)\n                putExtra(EXTRA_OUTPUT, output)\n            }\n        )\n    }\n\n    override fun pause() {\n        application.startService(\n            Intent(application, AudioRecorderService::class.java).apply { action = ACTION_PAUSE }\n        )\n    }\n\n    override fun resume() {\n        application.startService(\n            Intent(application, AudioRecorderService::class.java).apply { action = ACTION_RESUME }\n        )\n    }\n\n    override fun stop() {\n        application.startService(\n            Intent(application, AudioRecorderService::class.java).apply { action = ACTION_STOP }\n        )\n    }\n\n    override fun cleanUp() {\n        application.startService(\n            Intent(application, AudioRecorderService::class.java).apply { action = ACTION_CLEAN_UP }\n        )\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotification.kt",
    "content": "package org.odk.collect.audiorecorder.recording.internal\n\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.app.Service\nimport android.content.Context.NOTIFICATION_SERVICE\nimport android.content.Intent\nimport androidx.core.app.NotificationCompat\nimport androidx.lifecycle.Observer\nimport org.odk.collect.androidshared.ui.ReturnToAppActivity\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.audiorecorder.recording.RecordingSession\nimport org.odk.collect.strings.format.formatLength\nimport org.odk.collect.strings.localization.getLocalizedString\n\ninternal class RecordingForegroundServiceNotification(private val service: Service, private val recordingRepository: RecordingRepository, private val uniqueIdGenerator: UniqueIdGenerator) {\n\n    private val notificationId = uniqueIdGenerator.getInt(NOTIFICATION_IDENTIFIER)\n    private val notificationIntent = Intent(service, ReturnToAppActivity::class.java)\n    private val notificationBuilder = NotificationCompat.Builder(service, NOTIFICATION_CHANNEL)\n        .setContentTitle(service.getLocalizedString(org.odk.collect.strings.R.string.recording))\n        .setContentText(formatLength(0))\n        .setSmallIcon(org.odk.collect.icons.R.drawable.ic_notification_small)\n        .setContentIntent(PendingIntent.getActivity(service, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE))\n        .setPriority(NotificationCompat.PRIORITY_LOW)\n\n    private val notificationManager = (service.getSystemService(NOTIFICATION_SERVICE) as NotificationManager)\n\n    private val sessionObserver = Observer<RecordingSession?> {\n        if (it != null) {\n            notificationBuilder.setContentText(formatLength(it.duration))\n            notificationManager.notify(notificationId, notificationBuilder.build())\n        }\n    }\n\n    fun show() {\n        setupNotificationChannel()\n        val notification = notificationBuilder\n            .build()\n\n        service.startForeground(notificationId, notification)\n        recordingRepository.currentSession.observeForever(sessionObserver)\n    }\n\n    fun dismiss() {\n        recordingRepository.currentSession.removeObserver(sessionObserver)\n        service.stopSelf()\n    }\n\n    private fun setupNotificationChannel() {\n        val notificationChannel = NotificationChannel(\n            NOTIFICATION_CHANNEL,\n            service.getLocalizedString(org.odk.collect.strings.R.string.recording_channel),\n            NotificationManager.IMPORTANCE_LOW\n        )\n\n        notificationManager.createNotificationChannel(notificationChannel)\n    }\n\n    companion object {\n        private const val NOTIFICATION_IDENTIFIER = \"recording\"\n        private const val NOTIFICATION_CHANNEL = \"recording_channel\"\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/recording/internal/RecordingRepository.kt",
    "content": "package org.odk.collect.audiorecorder.recording.internal\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport org.odk.collect.androidshared.data.AppState\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.audiorecorder.recording.RecordingSession\nimport java.io.File\nimport java.io.Serializable\n\ninternal class RecordingRepository(appState: AppState) {\n\n    private val _failedToStart =\n        appState.get(\"failedToStart\", MutableLiveData<Consumable<Exception?>>(Consumable(null)))\n    private val _currentSession =\n        appState.get(\"currentSession\", MutableLiveData<RecordingSession?>(null))\n\n    val currentSession: LiveData<RecordingSession?> = _currentSession\n    val failedToStart: LiveData<Consumable<Exception?>> = _failedToStart\n\n    fun start(sessionId: Serializable) {\n        _currentSession.value = RecordingSession(sessionId, null, 0, 0, false)\n        _failedToStart.value = Consumable(null)\n    }\n\n    fun setDuration(duration: Long) {\n        _currentSession.value?.let {\n            _currentSession.value = it.copy(duration = duration)\n        }\n    }\n\n    fun setAmplitude(amplitude: Int) {\n        _currentSession.value?.let {\n            _currentSession.value = it.copy(amplitude = amplitude)\n        }\n    }\n\n    fun setPaused(paused: Boolean) {\n        _currentSession.value?.let {\n            _currentSession.value = it.copy(paused = paused)\n        }\n    }\n\n    fun recordingReady(recording: File) {\n        _currentSession.value?.let {\n            _currentSession.value = it.copy(file = recording, paused = false)\n        }\n    }\n\n    fun clear() {\n        _currentSession.value = null\n    }\n\n    fun failToStart(exception: Exception) {\n        _currentSession.value = null\n        _failedToStart.value = Consumable(exception)\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/main/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorder.kt",
    "content": "package org.odk.collect.audiorecorder.testsupport\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.audiorecorder.recorder.Output\nimport org.odk.collect.audiorecorder.recording.AudioRecorder\nimport org.odk.collect.audiorecorder.recording.RecordingSession\nimport java.io.File\nimport java.io.Serializable\n\n/**\n * An implementation of audio recorder that always returns the same recording\n */\nclass StubAudioRecorder(private val stubRecordingPath: String) : AudioRecorder() {\n\n    var lastRecording: File? = null\n    var lastSession: Serializable? = null\n\n    var duration: Int = 0\n        set(value) {\n            field = value\n            currentSession.value?.let {\n                currentSession.value = it.copy(duration = value.toLong())\n            }\n        }\n    var amplitude: Int = 0\n        set(value) {\n            field = value\n            currentSession.value?.let {\n                currentSession.value = it.copy(amplitude = value)\n            }\n        }\n\n    var wasCleanedUp = false\n\n    private var isRecording = false\n    private var failOnStart = false\n    private val currentSession = MutableLiveData<RecordingSession?>(null)\n    private val failedToStart = MutableLiveData<Consumable<Exception?>>(Consumable(null))\n\n    override fun isRecording(): Boolean {\n        return isRecording\n    }\n\n    override fun getCurrentSession(): LiveData<RecordingSession?> {\n        return currentSession\n    }\n\n    override fun failedToStart(): LiveData<Consumable<java.lang.Exception?>> {\n        return failedToStart\n    }\n\n    override fun start(sessionId: Serializable, output: Output) {\n        if (!isRecording) {\n            if (failOnStart) {\n                currentSession.value = null\n                failedToStart.value = Consumable(Exception())\n            } else {\n                wasCleanedUp = false\n                lastSession = sessionId\n                isRecording = true\n                currentSession.value = RecordingSession(sessionId, null, 0, 0, false)\n            }\n        }\n    }\n\n    override fun pause() {\n        currentSession.value?.let {\n            currentSession.value = it.copy(paused = true)\n        }\n    }\n\n    override fun resume() {\n        currentSession.value?.let {\n            currentSession.value = it.copy(paused = false)\n        }\n    }\n\n    override fun stop() {\n        isRecording = false\n\n        val newFile = File.createTempFile(\"temp\", \".fake\")\n        File(stubRecordingPath).copyTo(newFile, overwrite = true)\n        newFile.deleteOnExit()\n        currentSession.value?.let {\n            currentSession.value = it.copy(file = newFile, paused = false)\n        }\n\n        lastRecording = newFile\n    }\n\n    override fun cleanUp() {\n        isRecording = false\n\n        currentSession.value = null\n        wasCleanedUp = true\n    }\n\n    fun failOnStart() {\n        failOnStart = true\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/test/java/org/odk/collect/audiorecorder/mediarecorder/AMRRecordingResourceTest.kt",
    "content": "package org.odk.collect.audiorecorder.mediarecorder\n\nimport android.media.MediaRecorder\nimport org.junit.Test\nimport org.mockito.Mockito.inOrder\nimport org.mockito.Mockito.mock\n\nclass AMRRecordingResourceTest {\n\n    /**\n     * Calls to stop() while paused hang which appears to be a problem in the Android\n     * framework. Resuming and the stopping does work however.\n     *\n     * @see [Gist with test demonstrating problem](https://gist.github.com/seadowg/e6cbab21e032fe2fcd9afb9b4f458ab9)\n     * @see [Issue in Google's Android tracker](https://issuetracker.google.com/issues/178630865)\n     */\n    @Test\n    fun whenPaused_stop_callsResumeThenStopOnMediaRecorder() {\n        val mediaRecorder = mock(MediaRecorder::class.java)\n        val inOrder = inOrder(mediaRecorder)\n        val amrRecordingResource = AMRRecordingResource(mediaRecorder)\n\n        amrRecordingResource.pause()\n\n        amrRecordingResource.stop()\n        inOrder.verify(mediaRecorder).resume()\n        inOrder.verify(mediaRecorder).stop()\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/test/java/org/odk/collect/audiorecorder/recorder/RecordingResourceRecorderTest.kt",
    "content": "package org.odk.collect.audiorecorder.recorder\n\nimport com.google.common.io.Files\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.endsWith\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.junit.Test\nimport org.odk.collect.audiorecorder.recording.MicInUseException\nimport org.odk.collect.audiorecorder.recording.SetupException\nimport java.io.File\nimport java.io.IOException\n\nclass RecordingResourceRecorderTest {\n\n    private val cacheDir = Files.createTempDir()\n    private val recordingResource = FakeRecordingResource()\n\n    private var lastOutput: Output? = null\n    private val recorder = RecordingResourceRecorder(cacheDir) { output ->\n        lastOutput = output\n        recordingResource\n    }\n\n    @Test\n    fun start_startsMediaRecorder() {\n        recorder.start(Output.AAC)\n        assertThat(recordingResource.hasStarted(), equalTo(true))\n    }\n\n    @Test\n    fun start_usesOutputToCreateResourceRecorder() {\n        Output.values().forEach {\n            RecordingResourceRecorder(cacheDir) { output ->\n                lastOutput = output\n                FakeRecordingResource()\n            }.start(it)\n\n            assertThat(lastOutput, equalTo(it))\n        }\n    }\n\n    @Test\n    fun start_createsAndRecordsToM4AFileInCacheDir() {\n        recorder.start(Output.AAC)\n        assertThat(recordingResource.getOutputFile()!!.parent, equalTo(cacheDir.absolutePath))\n        assertThat(recordingResource.getOutputFile()!!.absolutePath, endsWith(\".m4a\"))\n    }\n\n    @Test\n    fun start_createsAndRecordsToAMRFileInCacheDir() {\n        recorder.start(Output.AMR)\n        assertThat(recordingResource.getOutputFile()!!.parent, equalTo(cacheDir.absolutePath))\n        assertThat(recordingResource.getOutputFile()!!.absolutePath, endsWith(\".amr\"))\n    }\n\n    @Test\n    fun start_createsAndRecordsToAACLOWFileInCacheDir() {\n        recorder.start(Output.AAC_LOW)\n        assertThat(recordingResource.getOutputFile()!!.parent, equalTo(cacheDir.absolutePath))\n        assertThat(recordingResource.getOutputFile()!!.absolutePath, endsWith(\".m4a\"))\n    }\n\n    @Test\n    fun start_setsIsRecording_toTrue() {\n        recorder.start(Output.AAC)\n        assertThat(recorder.isRecording(), equalTo(true))\n    }\n\n    @Test\n    fun recordingTwice_doesntUseSameOutputFile() {\n        var mediaRecorder = FakeRecordingResource()\n        var recorder = RecordingResourceRecorder(cacheDir) { mediaRecorder }\n        recorder.start(Output.AAC)\n        val outputFile1 = mediaRecorder.getOutputFile()\n\n        mediaRecorder = FakeRecordingResource()\n        recorder = RecordingResourceRecorder(cacheDir) { mediaRecorder }\n        recorder.start(Output.AAC)\n        val outputFile2 = mediaRecorder.getOutputFile()\n\n        assertThat(outputFile1!!.absolutePath, not(equalTo(outputFile2!!.absolutePath)))\n    }\n\n    @Test(expected = SetupException::class)\n    fun start_whenFileCantBeCreated_throwsSetupException() {\n        cacheDir.deleteRecursively()\n        recorder.start(Output.AAC)\n    }\n\n    @Test(expected = SetupException::class)\n    fun start_whenPrepareFails_throwsSetupException() {\n        recordingResource.failOnPrepare()\n        recorder.start(Output.AAC)\n    }\n\n    @Test(expected = MicInUseException::class)\n    fun start_whenMicIsInUse_throwsMicInUseException() {\n        recordingResource.micInUse()\n        recorder.start(Output.AAC)\n    }\n\n    @Test\n    fun stop_releasesMediaRecorder() {\n        recorder.start(Output.AAC)\n        recorder.stop()\n        assertThat(recordingResource.isReleased(), equalTo(true))\n    }\n\n    @Test\n    fun stop_returnsOutputFile() {\n        recorder.start(Output.AAC)\n        val file = recorder.stop()\n        assertThat(file.absolutePath, equalTo(recordingResource.getOutputFile()!!.absolutePath))\n    }\n\n    @Test\n    fun stop_setsIsRecording_toFalse() {\n        recorder.start(Output.AAC)\n        recorder.stop()\n        assertThat(recorder.isRecording(), equalTo(false))\n    }\n\n    @Test\n    fun cancel_releasesMediaRecorder() {\n        recorder.start(Output.AAC)\n        recorder.cancel()\n        assertThat(recordingResource.isReleased(), equalTo(true))\n    }\n\n    @Test\n    fun cancel_deletesOutputFile() {\n        recorder.start(Output.AAC)\n        recorder.cancel()\n        assertThat(recordingResource.getOutputFile()!!.exists(), equalTo(false))\n    }\n\n    @Test\n    fun cancel_setsIsRecording_toFalse() {\n        recorder.start(Output.AAC)\n        recorder.cancel()\n        assertThat(recorder.isRecording(), equalTo(false))\n    }\n\n    @Test\n    fun cancel_beforeStart_works() {\n        recorder.cancel()\n    }\n\n    @Test\n    fun pause_pausesMediaRecorder() {\n        recorder.start(Output.AAC)\n        recorder.pause()\n        assertThat(recordingResource.isPaused(), equalTo(true))\n    }\n\n    @Test\n    fun resume_resumesMediaRecorder() {\n        recorder.start(Output.AAC)\n        recorder.pause()\n        recorder.resume()\n        assertThat(recordingResource.isPaused(), equalTo(false))\n    }\n}\n\nprivate class FakeRecordingResource : RecordingResource {\n\n    private var file: File? = null\n\n    private var started = false\n    private var prepared = false\n    private var released = false\n    private var paused = false\n    private var failOnPrepare = false\n    private var micInUse = false\n\n    override fun setOutputFile(path: String) {\n        if (prepared) {\n            throw IllegalStateException(\"MediaRecorder already prepared!\")\n        }\n\n        if (!File(path).exists()) {\n            throw IllegalArgumentException(\"Path doesn't refer to created file!\")\n        }\n\n        file = File(path)\n    }\n\n    @Throws(IOException::class)\n    override fun prepare() {\n        if (failOnPrepare) {\n            throw IOException()\n        } else {\n            prepared = true\n        }\n    }\n\n    override fun start() {\n        if (!prepared) {\n            throw IllegalStateException(\"MediaRecorder not prepared!\")\n        }\n\n        if (micInUse) {\n            throw IllegalStateException()\n        }\n\n        started = true\n    }\n\n    override fun pause() {\n        if (!started) {\n            throw IllegalStateException(\"MediaRecorder not started!\")\n        }\n\n        paused = true\n    }\n\n    override fun resume() {\n        if (!started) {\n            throw IllegalStateException(\"MediaRecorder not started!\")\n        }\n\n        paused = false\n    }\n\n    override fun stop() {\n        started = false\n    }\n\n    override fun release() {\n        if (started) {\n            throw IllegalStateException(\"Cannot release while recording! Call stop() first\")\n        }\n\n        released = true\n    }\n\n    override fun getMaxAmplitude(): Int {\n        return 0\n    }\n\n    fun hasStarted(): Boolean {\n        return started\n    }\n\n    fun getOutputFile(): File? {\n        return file\n    }\n\n    fun isReleased(): Boolean {\n        return released\n    }\n\n    fun isPaused(): Boolean {\n        return paused\n    }\n\n    fun failOnPrepare() {\n        failOnPrepare = true\n    }\n\n    fun micInUse() {\n        micInUse = true\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/AudioRecorderTest.kt",
    "content": "package org.odk.collect.audiorecorder.recording\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.androidtest.getOrAwaitValue\nimport org.odk.collect.audiorecorder.recorder.Output\nimport java.io.File\nimport java.io.Serializable\n\nabstract class AudioRecorderTest {\n\n    abstract val viewModel: AudioRecorder\n    abstract fun runBackground()\n    abstract fun getLastRecordedFile(): File?\n\n    @Test\n    fun isRecording_whenNoSession_isFalse() {\n        runBackground()\n        assertThat(viewModel.isRecording(), equalTo(false))\n    }\n\n    @Test\n    fun isRecording_whenRecording_isTrue() {\n        viewModel.start(\"session1\", Output.AAC)\n\n        runBackground()\n        assertThat(viewModel.isRecording(), equalTo(true))\n    }\n\n    @Test\n    fun isRecording_afterStop_isFalse() {\n        viewModel.start(\"session1\", Output.AAC)\n        viewModel.stop()\n\n        runBackground()\n        assertThat(viewModel.isRecording(), equalTo(false))\n    }\n\n    @Test\n    fun isRecording_afterCleanUp_isFalse() {\n        viewModel.start(\"session1\", Output.AAC)\n        viewModel.cleanUp()\n\n        runBackground()\n        assertThat(viewModel.isRecording(), equalTo(false))\n    }\n\n    @Test\n    fun getCurrentSession_beforeRecording_isNull() {\n        val recording = viewModel.getCurrentSession()\n\n        runBackground()\n        assertThat(recording.getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun getCurrentSession_whenRecording_returnsSessionWithId() {\n        val recording = viewModel.getCurrentSession()\n        viewModel.start(\"session1\", Output.AAC)\n\n        runBackground()\n        assertThat(recording.getOrAwaitValue(), equalTo(RecordingSession(\"session1\", null, 0, 0, false)))\n    }\n\n    @Test\n    fun getCurrentSession_afterStop_hasRecordedFile() {\n        val recording = viewModel.getCurrentSession()\n        viewModel.start(\"session1\", Output.AAC)\n        viewModel.stop()\n\n        runBackground()\n        assertThat(recording.getOrAwaitValue(), equalTo(RecordingSession(\"session1\", getLastRecordedFile(), 0, 0, false)))\n    }\n\n    @Test\n    fun getCurrentSession_afterCleanUp_isNull() {\n        val recording = viewModel.getCurrentSession()\n        viewModel.start(\"session1\", Output.AAC)\n        viewModel.cleanUp()\n\n        runBackground()\n        assertThat(recording.getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun getCurrentSession_whenRecording_isNotPaused() {\n        val session = viewModel.getCurrentSession()\n        viewModel.start(\"session\", Output.AAC)\n\n        runBackground()\n        assertThat(session.getOrAwaitValue()?.paused, equalTo(false))\n    }\n\n    @Test\n    fun getCurrentSession_afterStop_isNotPaused() {\n        val session = viewModel.getCurrentSession()\n\n        viewModel.start(\"session\", Output.AAC)\n        viewModel.stop()\n\n        runBackground()\n        assertThat(session.getOrAwaitValue()?.paused, equalTo(false))\n    }\n\n    @Test\n    fun getCurrentSession_afterPause_isPaused() {\n        val session = viewModel.getCurrentSession()\n\n        viewModel.start(\"session\", Output.AAC)\n        viewModel.pause()\n\n        runBackground()\n        assertThat(session.getOrAwaitValue()?.paused, equalTo(true))\n    }\n\n    @Test\n    fun getCurrentSession_afterPauseAndResume_isNotPaused() {\n        val session = viewModel.getCurrentSession()\n\n        viewModel.start(\"session\", Output.AAC)\n        viewModel.pause()\n        viewModel.resume()\n\n        runBackground()\n        assertThat(session.getOrAwaitValue()?.paused, equalTo(false))\n    }\n\n    @Test\n    fun getCurrentSession_afterPauseAndStop_isNotPaused() {\n        val session = viewModel.getCurrentSession()\n\n        viewModel.start(\"session\", Output.AAC)\n        viewModel.pause()\n        viewModel.stop()\n\n        runBackground()\n        assertThat(session.getOrAwaitValue()?.paused, equalTo(false))\n    }\n\n    @Test\n    fun supportsSerializableSessionIds() {\n        val sessionId = SerializableId()\n        val session = viewModel.getCurrentSession()\n        viewModel.start(sessionId, Output.AAC)\n\n        runBackground()\n        assertThat(session.getOrAwaitValue()!!.id, equalTo(sessionId))\n    }\n\n    @Test\n    fun start_whenAlreadyRecording_doesNothing() {\n        val session = viewModel.getCurrentSession()\n        viewModel.start(\"session1\", Output.AAC)\n        viewModel.start(\"session2\", Output.AAC)\n\n        runBackground()\n        assertThat(session.getOrAwaitValue()?.id, equalTo(\"session1\"))\n    }\n}\n\nprivate class SerializableId : Serializable\n"
  },
  {
    "path": "audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/AudioRecorderServiceTest.kt",
    "content": "package org.odk.collect.audiorecorder.recording.internal\n\nimport android.app.Application\nimport android.content.Intent\nimport androidx.lifecycle.Lifecycle\nimport androidx.test.core.app.ApplicationProvider\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.hamcrest.Matchers.nullValue\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidshared.ui.ReturnToAppActivity\nimport org.odk.collect.androidshared.utils.InMemUniqueIdGenerator\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.audiorecorder.AudioRecorderDependencyModule\nimport org.odk.collect.audiorecorder.recorder.Output\nimport org.odk.collect.audiorecorder.recorder.Recorder\nimport org.odk.collect.audiorecorder.recording.AudioRecorderService\nimport org.odk.collect.audiorecorder.support.FakeRecorder\nimport org.odk.collect.audiorecorder.testsupport.RobolectricApplication\nimport org.odk.collect.servicetest.ServiceScenario\nimport org.odk.collect.testshared.FakeScheduler\nimport org.robolectric.Robolectric.buildService\nimport org.robolectric.RobolectricTestRunner\nimport org.robolectric.Shadows.shadowOf\nimport java.io.File\n\n@RunWith(RobolectricTestRunner::class)\nclass AudioRecorderServiceTest {\n\n    private val application: RobolectricApplication by lazy { ApplicationProvider.getApplicationContext() }\n    private val recorder = FakeRecorder()\n    private val scheduler = FakeScheduler()\n\n    private var serviceInstance: ServiceScenario<AudioRecorderService>? = null\n\n    @Before\n    fun setup() {\n        application.setupDependencies(\n            object : AudioRecorderDependencyModule() {\n                override fun providesRecorder(cacheDir: File): Recorder {\n                    return recorder\n                }\n\n                override fun providesScheduler(application: Application): Scheduler {\n                    return scheduler\n                }\n\n                override fun providesUniqueIdGenerator(): UniqueIdGenerator {\n                    return InMemUniqueIdGenerator()\n                }\n            }\n        )\n    }\n\n    @Test\n    fun startAction_startsInForegroundWithNotification() {\n        val service = buildService(AudioRecorderService::class.java, createStartIntent(\"123\"))\n            .create()\n            .startCommand(0, 0)\n\n        val notification = shadowOf(service.get()).lastForegroundNotification\n        assertThat(notification, not(nullValue()))\n        assertThat(\n            shadowOf(notification).contentTitle,\n            equalTo(application.getString(org.odk.collect.strings.R.string.recording))\n        )\n        assertThat(\n            shadowOf(notification.contentIntent).savedIntent.component?.className,\n            equalTo(ReturnToAppActivity::class.qualifiedName)\n        )\n    }\n\n    @Test\n    fun startAction_startsRecorder() {\n        startAction(\"123\")\n        assertThat(recorder.isRecording(), equalTo(true))\n    }\n\n    @Test\n    fun startAction_whenRecordingInProgress_doesNothing() {\n        val intent = createStartIntent(\"456\")\n\n        startService(intent)\n        assertThat(recorder.isRecording(), equalTo(true))\n\n        startService(intent)\n        assertThat(recorder.isRecording(), equalTo(true))\n        assertThat(recorder.recordings.size, equalTo(1))\n    }\n\n    @Test\n    fun startAction_updatesDurationOnNotificationEverySecond() {\n        val service = startAction(\"456\")\n\n        scheduler.runForeground(0)\n        assertThat(\n            service.getForegroundNotification()!!.contentText,\n            equalTo(\"00:00\")\n        )\n\n        scheduler.runForeground(500)\n        assertThat(\n            service.getForegroundNotification()!!.contentText,\n            equalTo(\"00:00\")\n        )\n\n        scheduler.runForeground(1000)\n        assertThat(\n            service.getForegroundNotification()!!.contentText,\n            equalTo(\"00:01\")\n        )\n\n        scheduler.runForeground(2000)\n        assertThat(\n            service.getForegroundNotification()!!.contentText,\n            equalTo(\"00:02\")\n        )\n    }\n\n    @Test\n    fun startAction_whenRecorderStartThrowsException_stopsSelf() {\n        recorder.failOnStart(Exception())\n\n        val service = startService(createStartIntent(\"123\"))\n        assertThat(service.getState(), equalTo(Lifecycle.State.DESTROYED))\n    }\n\n    @Test\n    fun stopAction_stopsSelf() {\n        startAction(\"123\")\n        val service = stopAction()\n\n        assertThat(service.getState(), equalTo(Lifecycle.State.DESTROYED))\n    }\n\n    @Test\n    fun stopAction_stopsUpdates() {\n        startAction(\"123\")\n        stopAction()\n\n        assertThat(scheduler.isRepeatRunning(), equalTo(false))\n    }\n\n    @Test\n    fun stopAction_stopsRecorder() {\n        startAction(\"123\")\n        stopAction()\n\n        assertThat(recorder.isRecording(), equalTo(false))\n    }\n\n    @Test\n    fun stopAction_beforeStart_doesNothing() {\n        val service = stopAction()\n\n        assertThat(service.getForegroundNotification(), nullValue())\n    }\n\n    @Test\n    fun cleanUpAction_whileRecording_cancelsRecorder() {\n        startAction(\"123\")\n\n        val cancelIntent = Intent(application, AudioRecorderService::class.java)\n        cancelIntent.action = AudioRecorderService.ACTION_CLEAN_UP\n        startService(cancelIntent)\n\n        assertThat(recorder.isRecording(), equalTo(false))\n        assertThat(recorder.wasCancelled(), equalTo(true))\n    }\n\n    @Test\n    fun cleanUpAction_whileRecording_stopsUpdates() {\n        startAction(\"123\")\n\n        val cancelIntent = Intent(application, AudioRecorderService::class.java)\n        cancelIntent.action = AudioRecorderService.ACTION_CLEAN_UP\n        startService(cancelIntent)\n\n        assertThat(scheduler.isRepeatRunning(), equalTo(false))\n    }\n\n    @Test\n    fun cleanUpAction_whileRecording_stopsSelf() {\n        startAction(\"123\")\n\n        val cancelIntent = Intent(application, AudioRecorderService::class.java)\n        cancelIntent.action = AudioRecorderService.ACTION_CLEAN_UP\n        val service = startService(cancelIntent)\n\n        assertThat(service.getState(), equalTo(Lifecycle.State.DESTROYED))\n    }\n\n    @Test\n    fun cleanUpAction_afterRecording_stopsSelf() {\n        startAction(\"123\")\n        stopAction()\n\n        val cancelIntent = Intent(application, AudioRecorderService::class.java)\n        cancelIntent.action = AudioRecorderService.ACTION_CLEAN_UP\n        val service = startService(cancelIntent)\n\n        assertThat(service.getState(), equalTo(Lifecycle.State.DESTROYED))\n    }\n\n    @Test\n    fun whenUserKillsAppFromTaskManager_cancelsRecorder_stopsSelf() {\n        val service = startService(createStartIntent(\"123\"))\n        service.onService {\n            it.onTaskRemoved(Intent())\n        }\n\n        assertThat(recorder.isRecording(), equalTo(false))\n        assertThat(recorder.wasCancelled(), equalTo(true))\n        assertThat(service.getState(), equalTo(Lifecycle.State.DESTROYED))\n    }\n\n    @Test\n    fun pauseAction_pausesRecorder() {\n        startAction(\"123\")\n        pauseAction()\n\n        assertThat(recorder.paused, equalTo(true))\n    }\n\n    @Test\n    fun pauseAction_stopsUpdates() {\n        startAction(\"123\")\n        pauseAction()\n\n        assertThat(scheduler.isRepeatRunning(), equalTo(false))\n    }\n\n    @Test\n    fun pauseAction_andThenResumeAction_resumesRecorder() {\n        startAction(\"123\")\n        pauseAction()\n        resumeAction()\n\n        assertThat(recorder.paused, equalTo(false))\n    }\n\n    @Test\n    fun pauseAction_andThenResumeAction_startsUpdates() {\n        startAction(\"123\")\n        pauseAction()\n        resumeAction()\n\n        assertThat(scheduler.isRepeatRunning(), equalTo(true))\n    }\n\n    @Test\n    fun pauseAction_afterStop_doesNothing() {\n        startAction(\"123\")\n        stopAction()\n        pauseAction()\n\n        val recordingRepository = RecordingRepository(application.getState())\n        assertThat(recordingRepository.currentSession.value!!.paused, equalTo(false))\n    }\n\n    @Test\n    fun resumeAction_afterStop_doesNothing() {\n        startAction(\"123\")\n        stopAction()\n        resumeAction()\n\n        assertThat(scheduler.isRepeatRunning(), equalTo(false))\n    }\n\n    private fun pauseAction() {\n        val pauseIntent = Intent(application, AudioRecorderService::class.java)\n        pauseIntent.action = AudioRecorderService.ACTION_PAUSE\n        startService(pauseIntent)\n    }\n\n    private fun stopAction(): ServiceScenario<AudioRecorderService> {\n        val stopIntent = Intent(application, AudioRecorderService::class.java)\n        stopIntent.action = AudioRecorderService.ACTION_STOP\n        return startService(stopIntent)\n    }\n\n    private fun startAction(sessionId: String): ServiceScenario<AudioRecorderService> {\n        return startService(createStartIntent(sessionId))\n    }\n\n    private fun resumeAction() {\n        val resumeIntent = Intent(application, AudioRecorderService::class.java)\n        resumeIntent.action = AudioRecorderService.ACTION_RESUME\n        startService(resumeIntent)\n    }\n\n    private fun createStartIntent(sessionId: String): Intent {\n        val intent = Intent(application, AudioRecorderService::class.java)\n        intent.action = AudioRecorderService.ACTION_START\n        intent.putExtra(AudioRecorderService.EXTRA_SESSION_ID, sessionId)\n        intent.putExtra(AudioRecorderService.EXTRA_OUTPUT, Output.AAC)\n        return intent\n    }\n\n    private fun startService(intent: Intent): ServiceScenario<AudioRecorderService> {\n        return serviceInstance.let { instance ->\n            if (instance == null) {\n                ServiceScenario.launch(AudioRecorderService::class.java, intent).also {\n                    serviceInstance = it\n                }\n            } else {\n                instance.startWithNewIntent(intent)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/ForegroundServiceAudioRecorderTest.kt",
    "content": "package org.odk.collect.audiorecorder.recording.internal\n\nimport android.app.Application\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.test.core.app.ApplicationProvider.getApplicationContext\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.androidshared.utils.InMemUniqueIdGenerator\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.audiorecorder.AudioRecorderDependencyModule\nimport org.odk.collect.audiorecorder.recorder.Output\nimport org.odk.collect.audiorecorder.recorder.Recorder\nimport org.odk.collect.audiorecorder.recording.AudioRecorder\nimport org.odk.collect.audiorecorder.recording.AudioRecorderFactory\nimport org.odk.collect.audiorecorder.recording.AudioRecorderTest\nimport org.odk.collect.audiorecorder.recording.MicInUseException\nimport org.odk.collect.audiorecorder.support.FakeRecorder\nimport org.odk.collect.audiorecorder.testsupport.RobolectricApplication\nimport org.odk.collect.testshared.FakeScheduler\nimport org.odk.collect.testshared.RobolectricHelpers\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass ForegroundServiceAudioRecorderTest : AudioRecorderTest() {\n\n    @get:Rule\n    val instantTaskExecutor = InstantTaskExecutorRule()\n    private val application by lazy { getApplicationContext<RobolectricApplication>() }\n\n    private val fakeRecorder = FakeRecorder()\n    private val scheduler = FakeScheduler()\n\n    override val viewModel: AudioRecorder by lazy {\n        AudioRecorderFactory(application).create()\n    }\n\n    override fun runBackground() {\n        RobolectricHelpers.runServices(true)\n    }\n\n    override fun getLastRecordedFile(): File? {\n        return fakeRecorder.file\n    }\n\n    @Before\n    fun setup() {\n        application.setupDependencies(\n            object : AudioRecorderDependencyModule() {\n                override fun providesRecorder(cacheDir: File): Recorder {\n                    return fakeRecorder\n                }\n\n                override fun providesScheduler(application: Application): Scheduler {\n                    return scheduler\n                }\n\n                override fun providesUniqueIdGenerator(): UniqueIdGenerator {\n                    return InMemUniqueIdGenerator()\n                }\n            }\n        )\n    }\n\n    @After\n    fun clearServices() {\n        RobolectricHelpers.clearServices()\n    }\n\n    @Test\n    fun start_passesOutputToRecorder() {\n        Output.values().forEach {\n            viewModel.start(\"blah\", it)\n            viewModel.stop()\n            runBackground()\n            assertThat(fakeRecorder.output, equalTo(it))\n        }\n    }\n\n    @Test\n    fun start_incrementsDurationEverySecond() {\n        viewModel.start(\"blah\", Output.AAC)\n        runBackground()\n\n        val currentSession = viewModel.getCurrentSession()\n        scheduler.runForeground(0)\n        assertThat(currentSession.value?.duration, equalTo(0))\n\n        scheduler.runForeground(500)\n        assertThat(currentSession.value?.duration, equalTo(0))\n\n        scheduler.runForeground(1000)\n        assertThat(currentSession.value?.duration, equalTo(1000))\n    }\n\n    @Test\n    fun start_updatesAmplitude() {\n        viewModel.start(\"blah\", Output.AAC)\n        runBackground()\n\n        val currentSession = viewModel.getCurrentSession()\n\n        fakeRecorder.amplitude = 12\n        scheduler.runForeground()\n        assertThat(currentSession.value?.amplitude, equalTo(12))\n\n        fakeRecorder.amplitude = 45\n        scheduler.runForeground()\n        assertThat(currentSession.value?.amplitude, equalTo(45))\n    }\n\n    @Test\n    fun start_whenRecorderStartThrowsException_setsSessionToNull() {\n        val exception = MicInUseException()\n        fakeRecorder.failOnStart(exception)\n\n        viewModel.start(\"blah\", Output.AAC)\n        runBackground()\n        assertThat(viewModel.getCurrentSession().value, equalTo(null))\n    }\n\n    @Test\n    fun start_whenRecorderStartThrowsException_setsFailedToStart() {\n        val exception = MicInUseException()\n        fakeRecorder.failOnStart(exception)\n\n        viewModel.start(\"blah\", Output.AAC)\n        runBackground()\n        assertThat(viewModel.failedToStart().value, equalTo(Consumable(exception)))\n    }\n\n    @Test\n    fun start_whenRecorderStartThrowsException_thenSucceeds_setsFailedToStartToNull() {\n        val exception = MicInUseException()\n        fakeRecorder.failOnStart(exception)\n        viewModel.start(\"blah\", Output.AAC)\n        runBackground()\n\n        fakeRecorder.failOnStart(null)\n        viewModel.start(\"blah\", Output.AAC)\n        runBackground()\n\n        assertThat(viewModel.failedToStart().value, equalTo(Consumable(null)))\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/test/java/org/odk/collect/audiorecorder/recording/internal/RecordingForegroundServiceNotificationTest.kt",
    "content": "package org.odk.collect.audiorecorder.recording.internal\n\nimport android.app.Application\nimport android.app.NotificationManager\nimport android.app.Service\nimport android.content.Context.NOTIFICATION_SERVICE\nimport android.content.Intent\nimport android.os.IBinder\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidshared.data.getState\nimport org.odk.collect.androidshared.utils.InMemUniqueIdGenerator\nimport org.robolectric.Robolectric\nimport org.robolectric.Shadows.shadowOf\n\n@RunWith(AndroidJUnit4::class)\nclass RecordingForegroundServiceNotificationTest {\n\n    @Test\n    fun dismiss_stopsUpdatingNotification() {\n        val service = Robolectric.buildService(TestService::class.java).get()\n        val recordingRepository = RecordingRepository(ApplicationProvider.getApplicationContext<Application>().getState())\n        val recordingForegroundServiceNotification = RecordingForegroundServiceNotification(\n            service,\n            recordingRepository,\n            InMemUniqueIdGenerator()\n        )\n\n        recordingForegroundServiceNotification.show()\n        recordingForegroundServiceNotification.dismiss()\n\n        recordingRepository.start(\"session\")\n        recordingRepository.setDuration(5000)\n        val notificationManager = shadowOf(service.getSystemService(NOTIFICATION_SERVICE) as NotificationManager)\n        assertThat(shadowOf(notificationManager.allNotifications[0]).contentText, equalTo(\"00:00\"))\n    }\n}\n\nprivate class TestService : Service() {\n\n    override fun onBind(intent: Intent?): IBinder? {\n        return null\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/test/java/org/odk/collect/audiorecorder/support/FakeRecorder.kt",
    "content": "package org.odk.collect.audiorecorder.support\n\nimport org.odk.collect.audiorecorder.recorder.Output\nimport org.odk.collect.audiorecorder.recorder.Recorder\nimport org.odk.collect.audiorecorder.recording.MicInUseException\nimport org.odk.collect.audiorecorder.recording.SetupException\nimport java.io.File\n\nclass FakeRecorder : Recorder {\n\n    override var amplitude: Int = 0\n\n    private var _paused = false\n    val paused: Boolean\n        get() {\n            return _paused\n        }\n\n    var file: File? = null\n        private set\n\n    lateinit var output: Output\n\n    private val _recordings = mutableListOf<Unit>()\n    val recordings: List<Unit> = _recordings\n\n    private var recording = false\n    private var cancelled = false\n    private var exception: Exception? = null\n\n    override fun isRecording(): Boolean {\n        return recording\n    }\n\n    fun wasCancelled(): Boolean {\n        return cancelled\n    }\n\n    @Throws(SetupException::class, MicInUseException::class)\n    override fun start(output: Output) {\n        exception.let {\n            if (it == null) {\n                recording = true\n                cancelled = false\n                this.output = output\n                _recordings.add(Unit)\n                val newFile = File.createTempFile(\"recording\", \".mp3\")\n                file = newFile\n            } else {\n                throw it\n            }\n        }\n    }\n\n    override fun pause() {\n        _paused = true\n    }\n\n    override fun resume() {\n        _paused = false\n    }\n\n    override fun stop(): File {\n        recording = false\n        return file!!\n    }\n\n    override fun cancel() {\n        recording = false\n        cancelled = true\n    }\n\n    fun failOnStart(exception: Exception?) {\n        this.exception = exception\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/test/java/org/odk/collect/audiorecorder/testsupport/RobolectricApplication.kt",
    "content": "package org.odk.collect.audiorecorder.testsupport\n\nimport android.app.Application\nimport org.odk.collect.androidshared.data.AppState\nimport org.odk.collect.androidshared.data.StateStore\nimport org.odk.collect.audiorecorder.AudioRecorderDependencyComponent\nimport org.odk.collect.audiorecorder.AudioRecorderDependencyComponentProvider\nimport org.odk.collect.audiorecorder.AudioRecorderDependencyModule\nimport org.odk.collect.audiorecorder.DaggerAudioRecorderDependencyComponent\n\n/**\n * Used as the Application in tests in in the `test/src` root. This is setup in `robolectric.properties`\n */\ninternal class RobolectricApplication : Application(), AudioRecorderDependencyComponentProvider, StateStore {\n\n    override lateinit var audioRecorderDependencyComponent: AudioRecorderDependencyComponent\n\n    private val appState = AppState()\n\n    override fun onCreate() {\n        super.onCreate()\n        audioRecorderDependencyComponent = DaggerAudioRecorderDependencyComponent.builder()\n            .application(this)\n            .build()\n    }\n\n    fun setupDependencies(dependencyModule: AudioRecorderDependencyModule) {\n        audioRecorderDependencyComponent = DaggerAudioRecorderDependencyComponent.builder()\n            .dependencyModule(dependencyModule)\n            .application(this)\n            .build()\n    }\n\n    override fun getState(): AppState {\n        return appState\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/test/java/org/odk/collect/audiorecorder/testsupport/StubAudioRecorderTest.kt",
    "content": "package org.odk.collect.audiorecorder.testsupport\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.runner.RunWith\nimport org.odk.collect.audiorecorder.recording.AudioRecorder\nimport org.odk.collect.audiorecorder.recording.AudioRecorderTest\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass StubAudioRecorderTest : AudioRecorderTest() {\n\n    private val stubViewAudioRecorderViewModel: StubAudioRecorder by lazy {\n        val tempFile = File.createTempFile(\"blah\", \".whatever\")\n        StubAudioRecorder(tempFile.absolutePath)\n    }\n\n    override val viewModel: AudioRecorder by lazy {\n        stubViewAudioRecorderViewModel\n    }\n\n    override fun runBackground() {\n        // No op\n    }\n\n    override fun getLastRecordedFile(): File? {\n        return stubViewAudioRecorderViewModel.lastRecording\n    }\n}\n"
  },
  {
    "path": "audio-recorder/src/test/resources/robolectric.properties",
    "content": "application=org.odk.collect.audiorecorder.testsupport.RobolectricApplication\nsdk=33"
  },
  {
    "path": "benchmark.sh",
    "content": "./gradlew collect_app:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.package=org.odk.collect.android.benchmark\n"
  },
  {
    "path": "build.gradle",
    "content": "apply from: 'secrets.gradle'\n\n// Top-level build file where you can add configuration options common to all sub-projects/modules.\nbuildscript {\n    repositories {\n        google()\n\n        mavenCentral()\n        maven {\n            url \"https://plugins.gradle.org/m2/\"\n        }\n    }\n\n    dependencies {\n        classpath libs.gradle\n        classpath libs.googleServices\n        classpath libs.firebaseCrashlyticsGradle\n        classpath libs.kotlinGradlePlugin\n        classpath libs.ktlintGradle\n        classpath libs.navigationSafeArgsGradlePlugin\n        classpath libs.ossLicensesPlugin\n    }\n}\n\nallprojects {\n    repositories {\n        maven {\n            url \"${rootDir}/.local-m2/\"\n            metadataSources {\n                mavenPom()\n                artifact() // Supports artifact only dependencies like those from medicmobile repo\n            }\n        }\n\n        // Needs to go first to get specialty libraries https://stackoverflow.com/a/48438866/137744\n        google()\n\n        mavenLocal() // Only used for javarosa_local dependency\n        mavenCentral()\n\n        maven { url 'https://central.sonatype.com/repository/maven-snapshots' }\n        maven { url 'https://jitpack.io' }\n        maven {\n            url 'https://staging.dev.medicmobile.org/_couch/maven-repo'\n            metadataSources { artifact() }\n        }\n\n        maven {\n            url 'https://api.mapbox.com/downloads/v2/releases/maven'\n            authentication {\n                basic(BasicAuthentication)\n            }\n            credentials {\n                username = \"mapbox\"\n                password = getSecrets().getProperty('MAPBOX_DOWNLOADS_TOKEN', '')\n            }\n        }\n    }\n\n    tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {\n        kotlinOptions {\n            freeCompilerArgs = ['-Xjvm-default=all']\n            jvmTarget = '17'\n        }\n    }\n}\n\nconfigurations.all {\n    transitive = true\n}\n\ntask checkCode(type: GradleBuild) {\n    tasks = ['pmd', 'ktlintCheck', 'checkstyle', 'lintDebug']\n}\n\n// Create local Maven repo from cached Gradle dependencies\ntask cacheToMavenLocal(type: Sync) {\n    from new File(gradle.gradleUserHomeDir, 'caches/modules-2/files-2.1')\n    into \"${rootDir}/.local-m2\"\n\n    duplicatesStrategy = 'include'\n\n    // Convert from Gradle cache to Maven format\n    eachFile {\n        List<String> parts = it.path.split('/')\n        it.path = parts[0].replace('.','/') +\n                '/' + parts[1] +\n                '/' + parts[2] +\n                '/' + parts[4]\n    }\n\n    includeEmptyDirs false\n}\n\ntasks.register('testLab', Exec) {\n    dependsOn \"collect_app:assembleDebug\", \"collect_app:assembleDebugAndroidTest\"\n\n    executable = 'gcloud'\n    args = [\n            'beta', 'firebase', 'test', 'android', 'run',\n            '--type', 'instrumentation',\n            '--num-uniform-shards=25',\n            '--app', 'collect_app/build/outputs/apk/debug/ODK-Collect-debug.apk',\n            '--test', 'collect_app/build/outputs/apk/androidTest/debug/ODK-Collect-debug-androidTest.apk',\n            '--device', 'model=MediumPhone.arm,version=34,locale=en,orientation=portrait',\n            '--timeout', '15m',\n            '--directories-to-pull', '/sdcard',\n            '--test-targets', \"notPackage org.odk.collect.android.regression\",\n            '--test-targets', \"notPackage org.odk.collect.android.benchmark\"\n    ]\n}\n\ntasks.register('releaseCheck') {\n    dependsOn \"testLab\", \"collect_app:assembleOdkCollectRelease\"\n}\n"
  },
  {
    "path": "check-size.sh",
    "content": "set -e\n\nif [ $(ls -l collect_app/build/outputs/apk/selfSignedRelease/*.apk | awk '{print $5}') -gt $1 ];then\n  echo \"APK increased to $(ls -l collect_app/build/outputs/apk/selfSignedRelease/*.apk | awk '{print $5}') bytes!\"\n  exit 1\nfi\n"
  },
  {
    "path": "codecov.yml",
    "content": "codecov:\n  notify:\n    require_ci_to_pass: no\ncoverage:\n  precision: 2\n  round: down\n  range: \"10...100\"\n  status:\n    project: yes\n    patch: yes\nchanges: yes"
  },
  {
    "path": "collect_app/build.gradle",
    "content": "plugins {\n    alias(libs.plugins.androidApplication)\n    alias(libs.plugins.kotlinKsp)\n    alias(libs.plugins.kotlinParcelize)\n    alias(libs.plugins.ossLicenses)\n    alias(libs.plugins.safeargs)\n    alias(libs.plugins.composeCompiler)\n}\napply from: '../config/quality.gradle'\n\nimport com.android.ddmlib.DdmPreferences\n\nDdmPreferences.setTimeOut(60000)\n\nant.condition(property: 'os', value: 'windows') {\n    os(family: 'windows')\n}\nant.condition(property: 'os', value: 'unix') {\n    os(family: 'unix')\n}\n\n// Build numbers were manually set until 1067\ndef LEGACY_BUILD_NUMBER_OFFSET = 1067\n\n// Based on http://stackoverflow.com/questions/17097263#24121734\ndef getMasterCommitCount = { ->\n    try {\n        def stdout = new ByteArrayOutputStream()\n        exec {\n            switch (ant.properties.os) {\n                case 'windows':\n                    commandLine 'cmd', '/c', 'git', 'rev-list', '--first-parent', '--count', 'master'\n                    break\n                case 'unix':\n                    commandLine 'git', 'rev-list', '--first-parent', '--count', 'master'\n                    break\n            }\n            standardOutput = stdout\n        }\n        return Integer.parseInt(stdout.toString().trim())\n    } catch (ignored) {\n        return -1\n    }\n}\n\ndef getVersionName = { ->\n    return providers.exec {\n        switch (ant.properties.os) {\n            case 'windows':\n                commandLine 'cmd', '/c', 'git', 'describe', '--tags', '--dirty', '--always'\n                break\n            case 'unix':\n                commandLine 'git', 'describe', '--tags', '--dirty', '--always'\n                break\n        }\n    }.getStandardOutput().getAsText().get().trim()\n}\n\ndef secrets = getSecrets()\ndef googleMapsApiKey = secrets.getProperty('GOOGLE_MAPS_API_KEY', '')\ndef mapboxAccessToken = secrets.getProperty('MAPBOX_ACCESS_TOKEN', '')\ndef entitiesFilterProjectUrl = secrets.getProperty('ENTITIES_FILTER_PROJECT_URL', '')\ndef entitiesFilterSearchProjectUrl = secrets.getProperty('ENTITIES_FILTER_SEARCH_PROJECT_URL', '')\ndef thousandMediaFileProjectUrl = secrets.getProperty('THOUSAND_MEDIA_FILE_PROJECT_URL', '')\ndef thousandMediaFileEntityListProjectUrl = secrets.getProperty('THOUSAND_MEDIA_FILE_ENTITY_LIST_PROJECT_URL', '')\n\nandroid {\n    compileSdk libs.versions.compileSdk.get().toInteger()\n\n    buildFeatures {\n        viewBinding true\n        buildConfig true\n        compose true\n        resValues true\n    }\n\n    defaultConfig {\n        applicationId('org.odk.collect.android')\n        minSdk libs.versions.minSdk.get().toInteger()\n        targetSdkVersion libs.versions.targetSdk.get().toInteger()\n        versionCode project.hasProperty('versionCode') ? project.getProperties()['versionCode'].toInteger() : 5112\n        versionName getVersionName()\n        testInstrumentationRunner('androidx.test.runner.AndroidJUnitRunner')\n        vectorDrawables.useSupportLibrary = true\n    }\n\n    base {\n        archivesName = \"ODK-Collect\"\n    }\n\n    signingConfigs {\n        release {\n            if (secrets.getProperty('RELEASE_STORE_FILE')) {\n                storeFile file(secrets.getProperty('RELEASE_STORE_FILE'))\n                storePassword secrets.getProperty('RELEASE_STORE_PASSWORD')\n                keyAlias secrets.getProperty('RELEASE_KEY_ALIAS')\n                keyPassword secrets.getProperty('RELEASE_KEY_PASSWORD')\n            }\n        }\n\n        selfSignedRelease {\n            storeFile file('../debug.keystore')\n            storePassword 'android'\n            keyAlias 'androiddebugkey'\n            keyPassword 'android'\n        }\n    }\n\n    buildTypes {\n\n        // Release build for all forks\n        release {\n            if (secrets.getProperty('RELEASE_STORE_FILE')) {\n                signingConfig signingConfigs.release\n            }\n            minifyEnabled(true)\n            shrinkResources(true)\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt'\n            resValue(\"string\", \"GOOGLE_MAPS_API_KEY\", googleMapsApiKey)\n            resValue(\"string\", \"mapbox_access_token\", mapboxAccessToken)\n        }\n\n        // Release build for the official ODK Collect app\n        odkCollectRelease {\n            if (secrets.getProperty('RELEASE_STORE_FILE')) {\n                signingConfig signingConfigs.release\n            }\n            minifyEnabled(true)\n            shrinkResources(true)\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt'\n            resValue(\"string\", \"GOOGLE_MAPS_API_KEY\", googleMapsApiKey)\n            resValue(\"string\", \"mapbox_access_token\", mapboxAccessToken)\n\n            matchingFallbacks = ['release'] // So other modules use release build type for this\n        }\n\n        selfSignedRelease {\n            signingConfig signingConfigs.selfSignedRelease\n\n            minifyEnabled(true)\n            shrinkResources(true)\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt'\n            resValue(\"string\", \"GOOGLE_MAPS_API_KEY\", googleMapsApiKey)\n            resValue(\"string\", \"mapbox_access_token\", mapboxAccessToken)\n\n            matchingFallbacks = ['release'] // So other modules use release build type for this\n        }\n\n        debug {\n            debuggable(true)\n            resValue(\"string\", \"GOOGLE_MAPS_API_KEY\", googleMapsApiKey)\n            resValue(\"string\", \"mapbox_access_token\", mapboxAccessToken)\n            buildConfigField(\"String\", \"ENTITIES_FILTER_PROJECT_URL\", \"\\\"$entitiesFilterProjectUrl\\\"\")\n            buildConfigField(\"String\", \"ENTITIES_FILTER_SEARCH_PROJECT_URL\", \"\\\"$entitiesFilterSearchProjectUrl\\\"\")\n            buildConfigField(\"String\", \"THOUSAND_MEDIA_FILE_PROJECT_URL\", \"\\\"$thousandMediaFileProjectUrl\\\"\")\n            buildConfigField(\"String\", \"THOUSAND_MEDIA_FILE_ENTITY_LIST_PROJECT_URL\", \"\\\"$thousandMediaFileEntityListProjectUrl\\\"\")\n        }\n    }\n\n    androidComponents {\n        onVariants(selector().all()) { variant ->\n            if (variant.name == \"selfSignedRelease\" || variant.name == \"odkCollectRelease\") {\n                variant.outputs.forEach { output ->\n                    output.outputFileName.set(output.outputFileName.get().replace(\"${variant.name}.apk\", \"${defaultConfig.versionName}.apk\"))\n                }\n            }\n        }\n    }\n\n    packaging {\n        jniLibs {\n            // Without useLegacyPackaging, Mapbox native libraries (.so)\n            // can grow by several MB after upgrading to AGP 9, even if the Mapbox\n            // version itself hasn’t changed. Legacy packaging keeps them as built,\n            // avoiding extra padding, alignment, and ELF headers, which keeps the APK smaller.\n            useLegacyPackaging = true\n        }\n        resources {\n            // These library licenses will be referenced in-app\n            excludes += ['META-INF/ASL-2.0.txt', 'META-INF/LGPL-3.0.txt']\n\n            // Pick first occurrence of any files that cause conflicts\n            pickFirsts += ['META-INF/DEPENDENCIES', 'META-INF/LICENSE', 'META-INF/LICENSE.txt', 'META-INF/NOTICE', 'META-INF/NOTICE.txt', 'META-INF/services/com.fasterxml.jackson.core.JsonFactory', 'META-INF/services/com.fasterxml.jackson.core.ObjectCodec', 'META-INF/services/javax.ws.rs.ext.MessageBodyReader', 'META-INF/services/javax.ws.rs.ext.MessageBodyWriter', 'draftv4/schema', 'draftv3/schema']\n        }\n\n        // To ensure that ODK Collect is installable on all devices, we don't use\n        // abiFilters to exclude any ABIs; but to keep the APK slim, we include\n        // the Mapbox native library only for 32-bit and 64-bit ARM devices and\n        // omit it for all X86 devices.\n        if (!project.hasProperty(\"x86Libs\")) {\n            exclude 'lib/x86/libmapbox-maps.so'\n            exclude 'lib/x86/libmapbox-common.so'\n            exclude 'lib/x86/libc++_shared.so'\n            exclude 'lib/x86_64/libmapbox-maps.so'\n            exclude 'lib/x86_64/libmapbox-common.so'\n            exclude 'lib/x86_64/libc++_shared.so'\n        }\n\n        /* Exclude large properties files used by BouncyCastle's Picnic implementation (which\n           Collect doesn't use). */\n        exclude 'org/bouncycastle/pqc/crypto/picnic/lowmcL5.bin.properties'\n        exclude 'org/bouncycastle/pqc/crypto/picnic/lowmcL3.bin.properties'\n        exclude 'org/bouncycastle/pqc/crypto/picnic/lowmcL1.bin.properties'\n    }\n\n    compileOptions {\n        coreLibraryDesugaringEnabled true\n        sourceCompatibility JavaVersion.VERSION_17\n        targetCompatibility JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            includeAndroidResources = true\n            returnDefaultValues = true\n\n            all {\n                minHeapSize = '512m'\n                maxHeapSize = project.properties['test.heap.max']\n            }\n        }\n    }\n\n    lint {\n        abortOnError true\n        checkDependencies true\n        checkGeneratedSources true\n        htmlReport true\n        lintConfig file(\"$rootDir/config/lint.xml\")\n        xmlReport true\n    }\n    namespace 'org.odk.collect.android'\n}\n\nconfigurations.all {\n    resolutionStrategy {\n        cacheDynamicVersionsFor(0, 'seconds')\n        cacheChangingModulesFor(0, 'seconds')\n\n        force 'org.objenesis:objenesis:2.6'\n        force 'org.hamcrest:hamcrest:3.0'\n    }\n    \n    transitive = true\n}\n\ndependencies {\n    coreLibraryDesugaring libs.desugar\n\n    implementation project(':shared')\n    implementation project(':androidshared')\n    implementation project(':nbistubs')\n    implementation project(':icons')\n    implementation project(':strings')\n    implementation project(':material')\n    implementation project(':async')\n    implementation project(':analytics')\n    implementation project(':audio-clips')\n    implementation project(':forms')\n    implementation project(':audio-recorder')\n    implementation project(':projects')\n    implementation project(':location')\n    implementation project(':geo')\n    implementation project(':image-loader')\n    implementation project(':errors')\n    implementation project(':upgrade')\n    implementation project(':permissions')\n    implementation(project(':settings')) {\n        exclude group: 'joda-time'\n    }\n    implementation project(':external-app')\n    implementation project(':maps')\n    implementation project(':osmdroid')\n    implementation project(':entities')\n    implementation project(':crash-handler')\n    implementation project(':selfie-camera')\n    implementation project(':qr-code')\n    implementation project(':metadata')\n    implementation project(':google-maps')\n    implementation project(':draw')\n    implementation project(':printer')\n    implementation project(':lists')\n    implementation project(':web-page')\n    implementation project(':db')\n    implementation project(':open-rosa')\n    implementation project(':mobile-device-management')\n    implementation project(':timedgrid')\n\n    if (getSecrets().getProperty('MAPBOX_DOWNLOADS_TOKEN', '') != '') {\n        implementation project(':mapbox')\n    }\n\n    implementation libs.androidxStartup\n    implementation libs.androidxCoreKtx\n    implementation libs.androidxBrowser\n    implementation libs.androidxAppcompat\n    implementation libs.androidxWorkRuntime\n\n    implementation libs.androidxPreferenceKtx\n    implementation libs.androidxFragmentKtx\n\n    implementation libs.androidFlexbox\n\n    implementation libs.playServicesMaps\n    implementation libs.playServicesLocation\n    implementation libs.playServicesOssLicenses\n\n    implementation libs.firebaseAnalytics\n    implementation libs.firebaseCrashlytics\n\n    implementation (libs.persianJodaTime) {\n        exclude group: \"com.android.support\"\n    }\n    implementation libs.myanmarCalendar\n    implementation files('libs/bikram-sambat-1.8.0.jar')\n    implementation libs.commonsIo\n    implementation (libs.opencsv) {\n        exclude group: 'commons-logging'\n    }\n    implementation(libs.javarosa) {\n        exclude group: 'commons-io'\n        exclude group: 'joda-time'\n        exclude group: 'org.slf4j'\n        exclude group: 'org.hamcrest', module: 'hamcrest-all'\n    }\n    implementation libs.gson\n    implementation libs.timber\n    implementation libs.slf4jApi\n    implementation libs.slf4jTimber\n\n    implementation libs.zxingAndroidEmbedded\n\n    implementation libs.danlewAndroidJoda\n\n    implementation(libs.jsoup)\n\n    // Android Architecture Components:\n    implementation libs.androidxLifecycleRuntimeKtx\n    implementation libs.androidxLifecycleLivedataKtx\n    implementation libs.androidxLifecycleViewmodelCompose\n    implementation libs.androidxViewpager2\n\n    // Dagger:\n    implementation libs.dagger\n    ksp libs.daggerCompiler\n\n    // Needed for injection into Robolectric tests\n    kspTest libs.daggerCompiler\n\n    // Dagger Android:\n    implementation libs.daggerAndroid\n    ksp libs.daggerAndroidProcessor\n\n    implementation libs.armen101AudioRecorderView\n\n    implementation(libs.mp4parserMuxer) {\n        exclude group: 'org.hamcrest'\n    }\n\n    implementation libs.splashscreen\n    implementation libs.androidxNavigationFragmentKtx\n\n    var composeBom = platform(libs.androidxComposeBom)\n    implementation(composeBom)\n    implementation(libs.androidXComposeMaterial)\n    implementation(libs.androidXConstraintLayoutCompose)\n    implementation(libs.androidXComposeMaterialIcons)\n    implementation(libs.androidXComposePreview)\n\n    testImplementation project(':forms-test')\n\n    // Testing-only dependencies\n    testImplementation libs.junit\n    testImplementation libs.mockitoCore\n    testImplementation libs.mockitoKotlin\n\n    testImplementation project(':androidtest')\n    testImplementation(project(path: ':test-shared')) {\n        exclude group: 'org.robolectric' // Some tests in `collect_app` don't work with newer Robolectric\n    }\n    testImplementation(project(\":shadows\"))\n    testImplementation(project(\":test-forms\"))\n\n    testImplementation libs.androidXComposeUiTestJunit4\n    testImplementation libs.robolectric\n    testImplementation libs.androidxTestExtJunit\n    testImplementation libs.androidxArchCoreTesting\n\n    testImplementation libs.hamcrest\n\n    testImplementation libs.androidxTestEspressoCore\n    testImplementation libs.androidxTestEspressoIntents\n    testImplementation libs.androidxTestCoreKtx\n\n    androidTestImplementation project(':androidtest')\n    androidTestImplementation project(':test-forms')\n\n    androidTestImplementation(composeBom)\n    androidTestImplementation libs.androidXComposeUiTestJunit4\n    androidTestImplementation libs.caverockAndroidsvg\n    androidTestImplementation libs.mockitoAndroid\n    androidTestImplementation libs.androidxTestExtJunit\n    androidTestImplementation libs.androidxTestEspressoCore\n    androidTestImplementation libs.androidxArchCoreTesting\n    androidTestImplementation libs.androidxTestEspressoContrib\n    androidTestImplementation(libs.androidxTestEspressoIntents) {\n        exclude group: 'com.android.support', module: 'support-annotations'\n    }\n\n    androidTestImplementation libs.androidxWorkTesting\n    androidTestImplementation libs.uiautomator\n    androidTestImplementation libs.mockitoKotlin\n    androidTestImplementation libs.hamcrest\n\n    debugImplementation project(':fragments-test')\n    debugImplementation(project(':test-shared',)) {\n        exclude group: 'org.robolectric' // Some tests in `collect_app` don't work with newer Robolectric\n    }\n\n    // Real LeakCanary for debug and selfSigned builds only: notifications, analysis, etc\n    debugImplementation libs.leakcanary\n    debugImplementation(libs.androidXComposeTooling)\n    debugImplementation(libs.androidXComposeUiTestManifest)\n    selfSignedReleaseImplementation libs.leakcanary\n}\n\n// Must be at bottom to prevent dependency collisions\n// https://developers.google.com/android/guides/google-services-plugin\napply plugin: 'com.google.gms.google-services'\n\n// Must be after google-services to prevent build failure\n// https://stackoverflow.com/a/67995305\napply plugin: 'com.google.firebase.crashlytics'\n"
  },
  {
    "path": "collect_app/google-services.json",
    "content": "{\n  \"project_info\": {\n    \"project_number\": \"640412404956\",\n    \"firebase_url\": \"https://opendatakit-forks.firebaseio.com\",\n    \"project_id\": \"opendatakit-forks\",\n    \"storage_bucket\": \"opendatakit-forks.appspot.com\"\n  },\n  \"client\": [\n    {\n      \"client_info\": {\n        \"mobilesdk_app_id\": \"1:640412404956:android:730483f389563845cb7819\",\n        \"android_client_info\": {\n          \"package_name\": \"org.odk.collect.android\"\n        }\n      },\n      \"oauth_client\": [\n        {\n          \"client_id\": \"640412404956-ag8do5nic02k70qr2k1a0mv3du8222hv.apps.googleusercontent.com\",\n          \"client_type\": 3\n        }\n      ],\n      \"api_key\": [\n        {\n          \"current_key\": \"AIzaSyA5tuKwVrlKEs-7iMSpdTmAy6qSH-w0lik\"\n        }\n      ],\n      \"services\": {\n        \"appinvite_service\": {\n          \"other_platform_oauth_client\": [\n            {\n              \"client_id\": \"640412404956-ag8do5nic02k70qr2k1a0mv3du8222hv.apps.googleusercontent.com\",\n              \"client_type\": 3\n            }\n          ]\n        }\n      }\n    }\n  ],\n  \"configuration_version\": \"1\"\n}"
  },
  {
    "path": "collect_app/proguard-rules.txt",
    "content": "-dontwarn com.google.**\n-dontwarn au.com.bytecode.**\n-dontwarn org.joda.time.**\n-dontwarn org.osmdroid.**\n-dontwarn org.xmlpull.v1.**\n-dontwarn org.hamcrest.**\n-dontwarn com.rarepebble.**\n-dontwarn com.networknt.**\n-dontwarn android.content.res.**\n-dontwarn java.beans.**\n\n-keep class org.kxml2.** { *; }\n-keep class com.google.android.gms.oss.licenses.** { *; }\n-keep class org.odk.collect.android.logic.actions.**\n-keep class android.support.v7.widget.** { *; }\n-keep class org.mp4parser.boxes.** { *; }\n-keep class * extends androidx.fragment.app.Fragment{}\n\n-keep class * implements org.javarosa.core.util.externalizable.Externalizable {\n    public <init>();\n}\n-keep class org.javarosa.core.model.instance.geojson.GeojsonFeature { *; }\n-keep class org.javarosa.core.model.instance.geojson.GeojsonGeometry { *; }\n\n# Make sure TaskSpecs are kept as they are instantiated via reflection\n-keep public class * implements org.odk.collect.async.TaskSpec { *; }\n\n-dontobfuscate\n\n# recommended okhttp rules\n# https://github.com/square/okhttp#r8--proguard\n-dontwarn javax.annotation.**\n-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase\n-dontwarn org.codehaus.mojo.animal_sniffer.*\n-dontwarn okhttp3.internal.platform.ConscryptPlatform\n\n# Keep line numbers for Crashlytics https://stackoverflow.com/questions/38529304/android-crashlytics-sending-incorrect-line-number\n-keepattributes SourceFile,LineNumberTable\n"
  },
  {
    "path": "collect_app/src/androidTest/assets/instances/One Question_2021-06-22_15-55-50.xml",
    "content": "<?xml version='1.0' ?><data id=\"one_question\" orx:version=\"1\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><age /></data>"
  },
  {
    "path": "collect_app/src/androidTest/assets/instances/one-question-google_2023-08-08_14-51-00.xml",
    "content": "<?xml version='1.0' encoding='UTF-8' ?><data id=\"one_question_google\" orx:version=\"1\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><age /></data>"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt",
    "content": "package org.odk.collect.android.benchmark\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.blankOrNullString\nimport org.hamcrest.Matchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.benchmark.support.Benchmarker\nimport org.odk.collect.android.benchmark.support.benchmark\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.android.test.BuildConfig.ENTITIES_FILTER_PROJECT_URL\nimport org.odk.collect.android.test.BuildConfig.THOUSAND_MEDIA_FILE_ENTITY_LIST_PROJECT_URL\n\n@RunWith(AndroidJUnit4::class)\nclass EntitiesBenchmarkTest {\n\n    private val rule = CollectTestRule(useDemoProject = false)\n\n    @get:Rule\n    var chain: RuleChain = chain(TestDependencies(true)).around(rule)\n\n    /**\n     * Benchmarks the performance of entity follow up forms. [ENTITIES_FILTER_PROJECT_URL] should\n     * be set to a project that contains the \"100k Entities Filter\" benchmark form and the\n     * \"entities_100k\" entity list.\n     *\n     * Devices that currently pass:\n     * - Fairphone 3\n     * - Pixel 3\n     *\n     */\n    @Test\n    fun oneHundredThousandEntities() {\n        assertThat(\n            \"Need to set ENTITIES_FILTER_PROJECT_URL before running!\",\n            ENTITIES_FILTER_PROJECT_URL,\n            not(blankOrNullString())\n        )\n\n        val benchmarker = Benchmarker()\n\n        rule.startAtFirstLaunch()\n            .clickManuallyEnterProjectDetails()\n            .inputUrl(ENTITIES_FILTER_PROJECT_URL)\n            .addProject()\n\n            // Populate http cache and recreate project\n            .clickGetBlankForm()\n            .clickGetSelected()\n            .clickOK(MainMenuPage())\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickProjectManagement()\n            .deleteLastProject()\n            .clickManuallyEnterProjectDetails()\n            .inputUrl(ENTITIES_FILTER_PROJECT_URL)\n            .addProject()\n\n            .clickGetBlankForm()\n            .benchmark(\"Downloading form with http cache\", 40, benchmarker) {\n                it.clickGetSelected()\n            }\n\n            .clickOK(MainMenuPage())\n            .clickGetBlankForm()\n            .benchmark(\"Downloading form second time with http cache\", 5, benchmarker) {\n                it.clickGetSelected()\n            }\n\n            .clickOK(MainMenuPage())\n            .clickFillBlankForm()\n            .benchmark(\"Loading form first time\", 2, benchmarker) {\n                it.clickOnForm(\"100k Entities Filter\")\n            }\n\n            .pressBackAndDiscardForm()\n            .clickFillBlankForm()\n            .benchmark(\"Loading form second time\", 2, benchmarker) {\n                it.clickOnForm(\"100k Entities Filter\")\n            }\n\n            .answerQuestion(\"Which value do you want to filter by?\", \"1024\")\n            .benchmark(\"Filtering select\", 3, benchmarker) {\n                it.swipeToNextQuestion(\"Filtered select\")\n            }\n\n        benchmarker.assertResults()\n    }\n\n    /**\n     * Benchmarks the performance of updating forms with entity lists and many media files.\n     * [THOUSAND_MEDIA_FILE_ENTITY_LIST_PROJECT_URL] should be set to a project that contains the\n     * \"1000-media-files-entity-list\" form.\n     *\n     * This scenario could also arise when updating a form that has a single new/updated non-entity\n     * media file, but in practice this will probably be most common with entity forms as the list\n     * will always force a media file update.\n     *\n     * Devices that currently pass:\n     * - Fairphone 3\n     */\n    @Test\n    fun oneThousandMediaFilesWithEntityList() {\n        assertThat(\n            \"Need to set THOUSAND_MEDIA_FILE_ENTITY_LIST_PROJECT_URL before running!\",\n            THOUSAND_MEDIA_FILE_ENTITY_LIST_PROJECT_URL,\n            not(blankOrNullString())\n        )\n\n        val benchmarker = Benchmarker()\n\n        rule.startAtFirstLaunch()\n            .clickManuallyEnterProjectDetails()\n            .inputUrl(THOUSAND_MEDIA_FILE_ENTITY_LIST_PROJECT_URL)\n            .addProject()\n\n            // Download all forms\n            .clickGetBlankForm()\n            .clickGetSelected()\n            .clickOKOnDialog(MainMenuPage())\n\n            .clickGetBlankForm()\n            .benchmark(\n                \"Redownloading a form with 1k media files and entity list when there are no updates\",\n                5,\n                benchmarker\n            ) {\n                it\n                    .clickGetSelected()\n                    .clickOKOnDialog(MainMenuPage())\n            }\n\n        benchmarker.assertResults()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/benchmark/FormsUpdateBenchmarkTest.kt",
    "content": "package org.odk.collect.android.benchmark\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.blankOrNullString\nimport org.hamcrest.Matchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.benchmark.support.Benchmarker\nimport org.odk.collect.android.benchmark.support.benchmark\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.android.test.BuildConfig.THOUSAND_MEDIA_FILE_PROJECT_URL\n\n@RunWith(AndroidJUnit4::class)\nclass FormsUpdateBenchmarkTest {\n    private val rule = CollectTestRule(useDemoProject = false)\n\n    @get:Rule\n    val chain: RuleChain = chain(TestDependencies(true)).around(rule)\n\n    /**\n     * Benchmarks the performance of check for updates. [THOUSAND_MEDIA_FILE_PROJECT_URL] should\n     * be set to a project that contains the \"1000-media-files\" benchmark form.\n     *\n     * Devices that currently pass:\n     * - Fairphone 3\n     */\n    @Test\n    fun oneThousandMediaFiles() {\n        assertThat(\n            \"Need to set THOUSAND_MEDIA_FILE_PROJECT_URL before running!\",\n            THOUSAND_MEDIA_FILE_PROJECT_URL,\n            not(blankOrNullString())\n        )\n\n        val benchmarker = Benchmarker()\n\n        rule.startAtFirstLaunch()\n            .clickManuallyEnterProjectDetails()\n            .inputUrl(THOUSAND_MEDIA_FILE_PROJECT_URL)\n            .addProject()\n\n            // Download all forms\n            .clickGetBlankForm()\n            .clickGetSelected()\n            .clickOKOnDialog(MainMenuPage())\n\n            .benchmark(\n                \"Fetching form list with 1k media files when there are no updates\",\n                7,\n                benchmarker\n            ) {\n                it\n                    .clickGetBlankForm()\n                    .clickSelectAll()\n            }\n\n        benchmarker.assertResults()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/benchmark/SearchBenchmarkTest.kt",
    "content": "package org.odk.collect.android.benchmark\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.blankOrNullString\nimport org.hamcrest.Matchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.benchmark.support.Benchmarker\nimport org.odk.collect.android.benchmark.support.benchmark\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.android.test.BuildConfig.ENTITIES_FILTER_SEARCH_PROJECT_URL\n\n/**\n * Benchmarks the performance of search() forms. [ENTITIES_FILTER_SEARCH_PROJECT_URL] should be\n * set to a project that contains the \"100k Entities Filter search()\" benchmark form and the\n * \"entities_100k\" entity list.\n *\n * Devices that currently pass:\n * - Fairphone 3\n *\n */\n\n@RunWith(AndroidJUnit4::class)\nclass SearchBenchmarkTest {\n\n    private val rule = CollectTestRule(useDemoProject = false)\n\n    @get:Rule\n    var chain: RuleChain = chain(TestDependencies(true)).around(rule)\n\n    @Test\n    fun run() {\n        assertThat(\n            \"Need to set ENTITIES_FILTER_SEARCH_PROJECT_URL before running!\",\n            ENTITIES_FILTER_SEARCH_PROJECT_URL,\n            not(blankOrNullString())\n        )\n\n        val benchmarker = Benchmarker()\n\n        rule.startAtFirstLaunch()\n            .clickManuallyEnterProjectDetails()\n            .inputUrl(ENTITIES_FILTER_SEARCH_PROJECT_URL)\n            .addProject()\n\n            .clickGetBlankForm()\n            .clickGetSelected()\n            .clickOK(MainMenuPage())\n\n            .clickFillBlankForm()\n            .benchmark(\"Loading form first time\", 20, benchmarker) {\n                it.clickOnForm(\"100k Entities Filter search()\")\n            }\n\n            .pressBackAndDiscardForm()\n            .clickFillBlankForm()\n            .benchmark(\"Loading form second time\", 2, benchmarker) {\n                it.clickOnForm(\"100k Entities Filter search()\")\n            }\n\n            .answerQuestion(\"Which value do you want to filter by?\", \"1024\")\n            .benchmark(\"Filtering select\", 2, benchmarker) {\n                it.swipeToNextQuestion(\"Filtered select\")\n            }\n\n        benchmarker.assertResults()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/benchmark/support/Benchmarker.kt",
    "content": "package org.odk.collect.android.benchmark.support\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.lessThanOrEqualTo\nimport org.odk.collect.android.support.pages.Page\nimport org.odk.collect.shared.TimeInMs\n\nclass Benchmarker {\n    private val stopwatch = Stopwatch()\n    private val targets = mutableMapOf<String, Long>()\n\n    fun <T> benchmark(name: String, target: Long, action: () -> T): T {\n        targets[name] = target\n        return stopwatch.time(name) {\n            action()\n        }\n    }\n\n    fun assertResults() {\n        printResults()\n\n        targets.entries.forEach {\n            val time = stopwatch.getTime(it.key)\n            assertThat(\"\\\"${it.key}\\\" took ${time}s!\", time, lessThanOrEqualTo(it.value))\n        }\n    }\n\n    private fun printResults() {\n        println(\"Benchmark results:\")\n        targets.keys.forEach {\n            println(\"$it: ${stopwatch.getTime(it)}s\")\n        }\n    }\n}\n\nfun <T : Page<T>, Y : Page<Y>> Y.benchmark(\n    name: String,\n    target: Long,\n    benchmarker: Benchmarker,\n    action: (Y) -> T\n): T {\n    return benchmarker.benchmark(name, target) {\n        action(this)\n    }\n}\n\nprivate class Stopwatch {\n\n    private val times = mutableMapOf<String, Long>()\n\n    fun <T> time(name: String, action: () -> T): T {\n        val startTime = System.currentTimeMillis()\n        val result = action()\n        val endTime = System.currentTimeMillis()\n\n        times[name] = (endTime - startTime) / TimeInMs.ONE_SECOND\n        return result\n    }\n\n    fun getTime(name: String): Long {\n        return times[name]!!\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/entitymanagement/ViewEntitiesTest.kt",
    "content": "package org.odk.collect.android.feature.entitymanagement\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.StubOpenRosaServer\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\nclass ViewEntitiesTest {\n\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun canViewLocallyCreatedEntitiesInBrowser() {\n        testDependencies.server.addForm(\"one-question-entity-registration.xml\")\n        testDependencies.server.addForm(\n            \"one-question-entity-follow-up.xml\",\n            listOf(StubOpenRosaServer.EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Registration\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"Name\", \"Logan Roy\"))\n            .openEntityBrowser()\n            .clickOnList(\"people\")\n            .assertEntity(\"Logan Roy\", \"full_name: Logan Roy\")\n    }\n\n    @Test\n    fun canViewListEntitiesInBrowser() {\n        testDependencies.server.addForm(\n            \"one-question-entity-follow-up.xml\",\n            listOf(StubOpenRosaServer.EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .refreshForms()\n            .openEntityBrowser()\n            .clickOnList(\"people\")\n            .assertEntity(\"Roman Roy\", \"full_name: Roman Roy\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/external/AndroidShortcutsTest.kt",
    "content": "package org.odk.collect.android.feature.external\n\nimport android.content.Intent\nimport android.content.Intent.EXTRA_SHORTCUT_INTENT\nimport android.content.Intent.EXTRA_SHORTCUT_NAME\nimport android.os.Build\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.android.support.ContentProviderUtils\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass AndroidShortcutsTest {\n\n    private val rule = CollectTestRule()\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    var ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun showsFormsForCurrentProject_andUpdatesListWhenNewFormsAreDownloaded() {\n        testDependencies.server.addForm(\n            \"One Question\",\n            \"one_question\",\n            \"1\",\n            \"one-question.xml\"\n        )\n\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n\n        val shortcutsPage = rule.launchShortcuts()\n            .assertTextDoesNotExist(\"One Question\")\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        shortcutsPage.asyncAssertText(\"One Question\")\n    }\n\n    @Test\n    fun shortcutIsFormEditAction() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .clickFillBlankForm() // Load form\n\n        val shortcutIntent = rule.launchShortcuts()\n            .selectForm(\"One Question\")\n        assertThat(shortcutIntent.getStringExtra(EXTRA_SHORTCUT_NAME), equalTo(\"One Question\"))\n\n        val shortcutTargetIntent =\n            shortcutIntent.getParcelableExtra<Intent>(EXTRA_SHORTCUT_INTENT)!!\n        val formId = ContentProviderUtils.getFormDatabaseId(\"DEMO\", \"one_question\")\n        assertThat(shortcutTargetIntent.action, equalTo(Intent.ACTION_EDIT))\n        assertThat(shortcutTargetIntent.data, equalTo(FormsContract.getUri(\"DEMO\", formId)))\n\n        // Check we're using ShortcutManager.createShortcutResultIntent on Android 16+\n        if (Build.VERSION.SDK_INT >= 36) {\n            assertThat(\n                shortcutIntent.hasExtra(\"android.content.pm.extra.PIN_ITEM_REQUEST\"),\n                equalTo(true)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/external/FormDownloadActionTest.kt",
    "content": "package org.odk.collect.android.feature.external\n\nimport android.app.Activity\nimport android.content.Intent\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.AppClosedPage\nimport org.odk.collect.android.support.pages.FormsDownloadResultPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ServerAuthDialog\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.android.utilities.ApplicationConstants.BundleKeys.FORM_IDS\nimport org.odk.collect.android.utilities.ApplicationConstants.BundleKeys.PASSWORD\nimport org.odk.collect.android.utilities.ApplicationConstants.BundleKeys.SUCCESS_KEY\nimport org.odk.collect.android.utilities.ApplicationConstants.BundleKeys.URL\nimport org.odk.collect.android.utilities.ApplicationConstants.BundleKeys.USERNAME\n\n@RunWith(AndroidJUnit4::class)\nclass FormDownloadActionTest {\n\n    private val testDependencies = TestDependencies()\n    private val rule = CollectTestRule(useDemoProject = false)\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun passingIds_downloadsFormsFromProjectServer_andReturnsSuccessResult() {\n        testDependencies.server.addForm(\"One Question\", \"one_question\", \"1\", \"one-question.xml\")\n        testDependencies.server.addForm(\"Two Question\", \"two_question\", \"1\", \"two-question.xml\")\n\n        val intent = Intent(\"org.odk.collect.android.FORM_DOWNLOAD\")\n        intent.type = FormsContract.CONTENT_TYPE\n        intent.putExtra(FORM_IDS, arrayOf(\"one_question\"))\n\n        rule.withProject(testDependencies.server.url)\n        val result = rule.launchForResult(intent, FormsDownloadResultPage()) {\n            it.assertSuccess()\n                .clickOK(AppClosedPage())\n        }\n\n        assertThat(result.resultCode, equalTo(Activity.RESULT_OK))\n        assertThat(result.resultData.getBooleanExtra(SUCCESS_KEY, false), equalTo(true))\n        assertThat(\n            result.resultData.getSerializableExtra(FORM_IDS),\n            equalTo(\n                mapOf(\n                    \"one_question\" to true\n                )\n            )\n        )\n\n        rule.relaunch(MainMenuPage())\n            .clickFillBlankForm()\n            .assertFormExists(\"One Question\")\n            .assertFormDoesNotExist(\"Two Question\")\n    }\n\n    @Test\n    fun passingIds_andServerDetails_downloadsFormsFromServer_andReturnsSuccessResult() {\n        testDependencies.server.setCredentials(\"Pete\", \"meyre\")\n        testDependencies.server.addForm(\"One Question\", \"one_question\", \"1\", \"one-question.xml\")\n\n        val intent = Intent(\"org.odk.collect.android.FORM_DOWNLOAD\")\n        intent.type = FormsContract.CONTENT_TYPE\n        intent.putExtra(FORM_IDS, arrayOf(\"one_question\"))\n        intent.putExtra(URL, testDependencies.server.url)\n        intent.putExtra(USERNAME, \"Pete\")\n        intent.putExtra(PASSWORD, \"meyre\")\n\n        rule.withProject(\"https://server2.example.com\")\n        val result = rule.launchForResult(intent, FormsDownloadResultPage()) {\n            it.assertSuccess()\n                .clickOK(AppClosedPage())\n        }\n\n        assertThat(result.resultCode, equalTo(Activity.RESULT_OK))\n        assertThat(result.resultData.getBooleanExtra(SUCCESS_KEY, false), equalTo(true))\n        assertThat(\n            result.resultData.getSerializableExtra(FORM_IDS),\n            equalTo(\n                mapOf(\n                    \"one_question\" to true\n                )\n            )\n        )\n\n        rule.relaunch(MainMenuPage())\n            .clickFillBlankForm()\n            .assertFormExists(\"One Question\")\n    }\n\n    @Test\n    fun passingIds_andServerDetails_whenThereIsAnAuthenticationError_allowsUserToReenterCredentials_andReturnsSuccessResult() {\n        testDependencies.server.setCredentials(\"Pete\", \"meyre\")\n        testDependencies.server.addForm(\"One Question\", \"one_question\", \"1\", \"one-question.xml\")\n\n        val intent = Intent(\"org.odk.collect.android.FORM_DOWNLOAD\")\n        intent.type = FormsContract.CONTENT_TYPE\n        intent.putExtra(FORM_IDS, arrayOf(\"one_question\"))\n        intent.putExtra(URL, testDependencies.server.url)\n        intent.putExtra(USERNAME, \"wrong\")\n        intent.putExtra(PASSWORD, \"wrong\")\n\n        rule.withProject(\"https://server2.example.com\")\n        val result = rule.launchForResult(intent, ServerAuthDialog()) {\n            it.fillUsername(\"Pete\")\n                .fillPassword(\"meyre\")\n                .clickOK(FormsDownloadResultPage())\n                .clickOK(AppClosedPage())\n        }\n\n        assertThat(result.resultCode, equalTo(Activity.RESULT_OK))\n        assertThat(result.resultData.getBooleanExtra(SUCCESS_KEY, false), equalTo(true))\n        assertThat(\n            result.resultData.getSerializableExtra(FORM_IDS),\n            equalTo(\n                mapOf(\n                    \"one_question\" to true\n                )\n            )\n        )\n\n        rule.relaunch(MainMenuPage())\n            .clickFillBlankForm()\n            .assertFormExists(\"One Question\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/external/FormEditActionTest.kt",
    "content": "package org.odk.collect.android.feature.external\n\nimport android.content.Intent\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.android.external.InstancesContract\nimport org.odk.collect.android.support.ContentProviderUtils\nimport org.odk.collect.android.support.pages.AppClosedPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass FormEditActionTest {\n\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain()\n        .around(rule)\n\n    @Test\n    fun editForm_andThenFillingForm_returnsNewInstanceURI() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .clickFillBlankForm() // Sync form\n\n        val formId = ContentProviderUtils.getFormDatabaseId(\"DEMO\", \"one_question\")\n        val uri = FormsContract.getUri(\"DEMO\", formId)\n\n        val formIntent = Intent(Intent.ACTION_EDIT).also { it.data = uri }\n        val result = rule.launchForResult(formIntent, FormEntryPage(\"One Question\")) {\n            it.answerQuestion(\"what is your age\", \"31\")\n                .swipeToEndScreen()\n                .clickFinalize(AppClosedPage())\n        }\n\n        val instanceId = ContentProviderUtils.getInstanceDatabaseId(\"DEMO\", \"one_question\")\n        assertThat(result.resultData.data, equalTo(InstancesContract.getUri(\"DEMO\", instanceId)))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/external/FormPickActionTest.kt",
    "content": "package org.odk.collect.android.feature.external\n\nimport android.content.Intent\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.android.support.ContentProviderUtils\nimport org.odk.collect.android.support.pages.FillBlankFormPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass FormPickActionTest {\n\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain()\n        .around(rule)\n\n    @Test\n    fun pickForm_andTheSelectingForm_returnsFormUri() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n\n        val intent = Intent(Intent.ACTION_PICK)\n        intent.type = FormsContract.CONTENT_TYPE\n        val result = rule.launchForResult(intent, FillBlankFormPage()) {\n            it.clickOnForm(\"One Question\")\n        }\n\n        val formId = ContentProviderUtils.getFormDatabaseId(\"DEMO\", \"one_question\")\n        assertThat(result.resultData.data, equalTo(FormsContract.getUri(\"DEMO\", formId)))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceEditActionTest.kt",
    "content": "package org.odk.collect.android.feature.external\n\nimport android.content.Intent\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.external.InstancesContract\nimport org.odk.collect.android.support.ContentProviderUtils\nimport org.odk.collect.android.support.pages.AppClosedPage\nimport org.odk.collect.android.support.pages.FormHierarchyPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass InstanceEditActionTest {\n\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain()\n        .around(rule)\n\n    @Test\n    fun editingInstance_andSaving_returnsInstanceURI() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n        val instanceId = ContentProviderUtils.getInstanceDatabaseId(\"DEMO\", \"one_question\")\n        val uri = InstancesContract.getUri(\"DEMO\", instanceId)\n\n        val intent = Intent(Intent.ACTION_EDIT).also { it.data = uri }\n        val result = rule.launchForResult(intent, FormHierarchyPage(\"One Question\")) {\n            it.clickGoToStart()\n                .answerQuestion(\"what is your age\", \"32\")\n                .swipeToEndScreen()\n                .clickFinalize(AppClosedPage())\n        }\n\n        assertThat(result.resultData.data, equalTo(uri))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstancePickActionTest.kt",
    "content": "package org.odk.collect.android.feature.external\n\nimport android.content.Intent\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.external.InstancesContract\nimport org.odk.collect.android.support.ContentProviderUtils\nimport org.odk.collect.android.support.pages.EditSavedFormPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass InstancePickActionTest {\n\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain()\n        .around(rule)\n\n    @Test\n    fun pickInstance_andTheSelectingInstance_returnsInstanceUri() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n        val intent = Intent(Intent.ACTION_PICK)\n        intent.type = InstancesContract.CONTENT_TYPE\n        val result = rule.launchForResult(intent, EditSavedFormPage()) {\n            it.clickOnFormClosingApp(\"One Question\")\n        }\n\n        val instanceId = ContentProviderUtils.getInstanceDatabaseId(\"DEMO\", \"one_question\")\n        assertThat(\n            result.resultData.data,\n            equalTo(InstancesContract.getUri(\"DEMO\", instanceId))\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/external/InstanceUploadActionTest.kt",
    "content": "package org.odk.collect.android.feature.external\n\nimport android.content.Context\nimport android.content.Intent\nimport android.provider.BaseColumns._ID\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.external.InstancesContract\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.OkDialog\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.android.utilities.ApplicationConstants\n\n@RunWith(AndroidJUnit4::class)\nclass InstanceUploadActionTest {\n\n    private val rule = CollectTestRule()\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun whenIntentIncludesURLExtra_instancesAreUploadedToThatURL() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"what is your age\", \"34\"))\n\n        val instanceId =\n            context.contentResolver.query(InstancesContract.getUri(\"DEMO\"), null, null, null, null)\n                .use {\n                    it!!.moveToFirst()\n                    it.getLong(it.getColumnIndex(_ID))\n                }\n\n        val intent = Intent(\"org.odk.collect.android.INSTANCE_UPLOAD\")\n        intent.type = InstancesContract.CONTENT_TYPE\n        intent.putExtra(ApplicationConstants.BundleKeys.URL, testDependencies.server.url)\n        intent.putExtra(\"instances\", longArrayOf(instanceId))\n\n        rule.launch(intent, OkDialog())\n            .assertTextInDialog(\"One Question - Success\")\n        assertThat(testDependencies.server.submissions.size, equalTo(1))\n    }\n\n    @Test\n    fun whenInstanceDoesNotExist_showsError() {\n        rule.startAtMainMenu()\n\n        val intent = Intent(\"org.odk.collect.android.INSTANCE_UPLOAD\")\n        intent.type = InstancesContract.CONTENT_TYPE\n        intent.putExtra(\"instances\", longArrayOf(11))\n\n        rule.launch(intent, OkDialog())\n            .assertText(org.odk.collect.strings.R.string.no_forms_uploaded)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/AddRepeatTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.assertion.ViewAssertions\nimport androidx.test.espresso.matcher.ViewMatchers\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.android.support.pages.FormEndPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\n\n@RunWith(AndroidJUnit4::class)\nclass AddRepeatTest {\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = chain().around(rule)\n\n    @Test\n    fun whenInRepeat_swipingNext_andClickingAdd_addsAnotherRepeat() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-repeat.xml\")\n            .startBlankForm(\"One Question Repeat\")\n            .assertText(\"Person > 1\")\n            .swipeToNextQuestionWithRepeatGroup(\"Person\")\n            .clickOnAdd(FormEntryPage(\"One Question Repeat\"))\n            .assertText(\"Person > 2\")\n    }\n\n    @Test\n    fun whenInRepeat_swipingNext_andClickingDoNotAdd_leavesRepeatGroup() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-repeat.xml\")\n            .startBlankForm(\"One Question Repeat\")\n            .assertText(\"Person > 1\")\n            .swipeToNextQuestionWithRepeatGroup(\"Person\")\n            .clickOnDoNotAdd(FormEndPage(\"One Question Repeat\"))\n    }\n\n    @Test\n    fun whenInRepeat_thatIsAFieldList_swipingNext_andClickingAdd_addsAnotherRepeat() {\n        rule.startAtMainMenu()\n            .copyForm(\"field-list-repeat.xml\")\n            .startBlankForm(\"Field-List Repeat\")\n            .assertText(\"Person > 1\")\n            .assertText(\"What is their age?\")\n            .assertText(\"What is their name?\")\n            .swipeToNextQuestionWithRepeatGroup(\"Person\")\n            .clickOnAdd(FormEntryPage(\"Field-List Repeat\"))\n            .assertText(\"Person > 2\")\n            .assertText(\"What is their age?\")\n            .assertText(\"What is their name?\")\n    }\n\n    @Test\n    fun whenInRepeat_clickingPlus_andClickingAdd_addsRepeatToEndOfSeries() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-repeat.xml\")\n            .startBlankForm(\"One Question Repeat\")\n            .assertText(\"Person > 1\")\n            .swipeToNextQuestionWithRepeatGroup(\"Person\")\n            .clickOnAdd(FormEntryPage(\"One Question Repeat\"))\n            .swipeToPreviousQuestion(\"What is their age?\")\n            .assertText(\"Person > 1\")\n            .clickPlus(\"Person\")\n            .clickOnAdd(FormEntryPage(\"One Question Repeat\"))\n            .assertText(\"Person > 3\")\n    }\n\n    @Test\n    fun whenInARepeat_clickingPlus_andClickingDoNotAdd_returns() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-repeat.xml\")\n            .startBlankForm(\"One Question Repeat\")\n            .assertText(\"Person > 1\")\n            .swipeToNextQuestionWithRepeatGroup(\"Person\")\n            .clickOnAdd(FormEntryPage(\"One Question Repeat\"))\n            .swipeToPreviousQuestion(\"What is their age?\")\n            .assertText(\"Person > 1\")\n            .clickPlus(\"Person\")\n            .clickOnDoNotAdd(FormEntryPage(\"One Question Repeat\"))\n            .assertText(\"Person > 1\")\n    }\n\n    @Test\n    fun whenInRepeatWithFixedCount_noPlusButtonAppears() {\n        rule.startAtMainMenu()\n            .copyForm(\"fixed-count-repeat.xml\")\n            .startBlankForm(\"Fixed Count Repeat\")\n\n        Espresso.onView(ViewMatchers.withId(R.id.menu_add_repeat))\n            .check(ViewAssertions.doesNotExist())\n    }\n\n    @Test\n    fun whenInHierarchyForRepeatGroup_clickingPlus_addsRepeatAtEndOfSeries() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-repeat.xml\")\n            .startBlankForm(\"One Question Repeat\")\n            .assertText(\"Person > 1\")\n            .swipeToNextQuestionWithRepeatGroup(\"Person\")\n            .clickOnAdd(FormEntryPage(\"One Question Repeat\"))\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .addGroup()\n            .assertText(\"Person > 3\")\n    }\n\n    @Test\n    fun whenInRepeatWithoutLabel_swipingNext_displaysTheAddRepeatDialog() {\n        rule.startAtMainMenu()\n            .copyForm(\"repeat_without_label.xml\")\n            .startBlankForm(\"Repeat without label\") // group with no label\n            .swipeToNextQuestionWithRepeatGroup(\"\")\n            .clickOnDoNotAdd(FormEntryPage(\"Repeat without label\")) // group with blank label\n            .swipeToNextQuestionWithRepeatGroup(\"\")\n            .clickOnDoNotAdd(FormEntryPage(\"Repeat without label\"))\n    }\n\n    @Test\n    fun whenViewFormInHierarchyForRepeatGroup_noAddButtonAppears() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-repeat.xml\")\n            .startBlankForm(\"One Question Repeat\")\n            .swipeToNextQuestionWithRepeatGroup(\"Person\")\n            .clickOnDoNotAdd(FormEndPage(\"One Question Repeat\"))\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Repeat\")\n            .clickOnGroup(\"Person\")\n            .assertNoId(R.id.menu_add_repeat)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/AudioAutoplayTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\n\n@RunWith(AndroidJUnit4::class)\nclass AudioAutoplayTest {\n    private val testDependencies = TestDependencies()\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val copyFormChain: RuleChain = chain(testDependencies).around(rule)\n\n    @Test\n    fun whenAudioWithAutoplayInFirstQuestion_playAudioAutomaticallyAfterFormOpen() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-autoplay.xml\", listOf(\"sampleAudio.wav\"))\n            .startBlankForm(\"One Question Autoplay\")\n\n        assertThat(testDependencies.audioPlayerFactory.audioPlayer.playedClips, equalTo(1))\n    }\n\n    @Test\n    fun audioContinuesWhenNavigatingInForm() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-autoplay.xml\", listOf(\"sampleAudio.wav\"))\n            .startBlankForm(\"One Question Autoplay\")\n            .swipeToEndScreen()\n\n        assertThat(testDependencies.audioPlayerFactory.audioPlayer.isPlaying, equalTo(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/AudioRecordingTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport static org.odk.collect.android.utilities.FileUtils.copyFileFromResources;\n\nimport android.app.Application;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.TestDependencies;\nimport org.odk.collect.android.support.pages.FormEntryPage;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.pages.OkDialog;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.testsupport.StubAudioRecorder;\n\nimport java.io.File;\nimport java.io.IOException;\n\n@RunWith(AndroidJUnit4.class)\npublic class AudioRecordingTest {\n\n    private StubAudioRecorder stubAudioRecorderViewModel;\n\n    public final TestDependencies testDependencies = new TestDependencies() {\n        @Override\n        public AudioRecorder providesAudioRecorder(Application application) {\n            if (stubAudioRecorderViewModel == null) {\n                try {\n                    File stubRecording = File.createTempFile(\"test\", \".m4a\");\n                    stubRecording.deleteOnExit();\n\n                    copyFileFromResources(\"media/test.m4a\", stubRecording.getAbsolutePath());\n                    stubAudioRecorderViewModel = new StubAudioRecorder(stubRecording.getAbsolutePath());\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            }\n\n            return stubAudioRecorderViewModel;\n        }\n    };\n\n    public final CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public final RuleChain chain = TestRuleChain.chain(testDependencies)\n            .around(rule);\n\n    @Test\n    public void onAudioQuestion_withoutAudioQuality_canRecordAndPlayBackInApp() {\n        new MainMenuPage()\n                .copyForm(\"audio-question.xml\")\n                .startBlankForm(\"Audio Question\")\n                .clickOnString(org.odk.collect.strings.R.string.capture_audio)\n                .clickOnContentDescription(org.odk.collect.strings.R.string.stop_recording)\n                .assertContentDescriptionNotDisplayed(org.odk.collect.strings.R.string.stop_recording)\n                .assertTextDoesNotExist(org.odk.collect.strings.R.string.capture_audio)\n                .clickOnContentDescription(org.odk.collect.strings.R.string.play_audio);\n    }\n\n    @Test\n    public void onAudioQuestion_withQualitySpecified_canRecordAudioInApp() {\n        rule.startAtMainMenu()\n                .copyForm(\"internal-audio-question.xml\")\n                .startBlankForm(\"Audio Question\")\n                .assertContentDescriptionNotDisplayed(org.odk.collect.strings.R.string.stop_recording)\n                .clickOnString(org.odk.collect.strings.R.string.capture_audio)\n                .clickOnContentDescription(org.odk.collect.strings.R.string.stop_recording)\n                .assertContentDescriptionNotDisplayed(org.odk.collect.strings.R.string.stop_recording)\n                .assertTextDoesNotExist(org.odk.collect.strings.R.string.capture_audio)\n                .assertContentDescriptionDisplayed(org.odk.collect.strings.R.string.play_audio);\n    }\n\n    @Test\n    public void whileRecording_pressingBack_showsWarning_andStaysOnSameScreen() {\n        rule.startAtMainMenu()\n                .copyForm(\"internal-audio-question.xml\")\n                .startBlankForm(\"Audio Question\")\n                .clickOnString(org.odk.collect.strings.R.string.capture_audio)\n                .pressBack(new OkDialog())\n                .clickOK(new FormEntryPage(\"Audio Question\"))\n                .assertQuestion(\"What does it sound like?\");\n    }\n\n    @Test\n    public void whileRecording_swipingToADifferentScreen_showsWarning_andStaysOnSameScreen() {\n        rule.startAtMainMenu()\n                .copyForm(\"internal-audio-question.xml\")\n                .startBlankForm(\"Audio Question\")\n                .clickOnString(org.odk.collect.strings.R.string.capture_audio)\n                .swipeToEndScreenWhileRecording()\n                .clickOK(new FormEntryPage(\"Audio Question\"))\n                .assertQuestion(\"What does it sound like?\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/BackgroundAudioRecordingTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport static org.hamcrest.CoreMatchers.containsString;\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.hamcrest.CoreMatchers.nullValue;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.odk.collect.android.utilities.FileUtils.copyFileFromResources;\n\nimport android.Manifest;\nimport android.app.Activity;\nimport android.app.Application;\nimport android.content.Context;\n\nimport androidx.annotation.NonNull;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.platform.app.InstrumentationRegistry;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.support.TestDependencies;\nimport org.odk.collect.android.support.pages.FormEndPage;\nimport org.odk.collect.android.support.pages.FormEntryPage;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.pages.SaveOrDiscardFormDialog;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.testsupport.StubAudioRecorder;\nimport org.odk.collect.permissions.ContextCompatPermissionChecker;\nimport org.odk.collect.permissions.PermissionListener;\nimport org.odk.collect.permissions.PermissionsChecker;\nimport org.odk.collect.permissions.PermissionsProvider;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.util.Arrays;\n\n@RunWith(AndroidJUnit4.class)\npublic class BackgroundAudioRecordingTest {\n\n    private StubAudioRecorder stubAudioRecorderViewModel;\n\n    private final RevokeableRecordAudioPermissionsChecker permissionsChecker = new RevokeableRecordAudioPermissionsChecker(ApplicationProvider.getApplicationContext());\n    private final ControllableRecordAudioPermissionsProvider permissionsProvider = new ControllableRecordAudioPermissionsProvider(permissionsChecker);\n    private final TestDependencies testDependencies = new TestDependencies() {\n\n        @Override\n        public AudioRecorder providesAudioRecorder(Application application) {\n            if (stubAudioRecorderViewModel == null) {\n                try {\n                    File stubRecording = File.createTempFile(\"test\", \".m4a\");\n                    stubRecording.deleteOnExit();\n\n                    copyFileFromResources(\"media/test.m4a\", stubRecording.getAbsolutePath());\n                    stubAudioRecorderViewModel = new StubAudioRecorder(stubRecording.getAbsolutePath());\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            }\n\n            return stubAudioRecorderViewModel;\n        }\n\n        @Override\n        public PermissionsChecker providesPermissionsChecker(Context context) {\n            return permissionsChecker;\n        }\n\n        @Override\n        public PermissionsProvider providesPermissionsProvider(PermissionsChecker permissionsChecker) {\n            return permissionsProvider;\n        }\n    };\n\n    public final CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public final RuleChain chain = TestRuleChain.chain(testDependencies)\n            .around(rule);\n\n    @Test\n    public void fillingOutForm_recordsAudio() throws Exception {\n        FormEntryPage formEntryPage = rule.startAtMainMenu()\n                .copyForm(\"one-question-background-audio.xml\")\n                .startBlankForm(\"One Question\");\n        assertThat(stubAudioRecorderViewModel.isRecording(), is(true));\n\n        FormEndPage formEndPage = formEntryPage\n                .inputText(\"123\")\n                .swipeToEndScreen();\n        assertThat(stubAudioRecorderViewModel.isRecording(), is(true));\n\n        formEndPage.clickFinalize();\n        assertThat(stubAudioRecorderViewModel.isRecording(), is(false));\n\n        File instancesDir = new File(testDependencies.storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES));\n        File recording = Arrays.stream(instancesDir.listFiles()[0].listFiles()).filter(f -> f.getName().contains(\".fake\")).findAny().get();\n        File instanceFile = Arrays.stream(instancesDir.listFiles()[0].listFiles()).filter(f -> f.getName().contains(\".xml\")).findAny().get();\n        String instanceXml = new String(Files.readAllBytes(instanceFile.toPath()));\n        assertThat(instanceXml, containsString(\"<recording>\" + recording.getName() + \"</recording>\"));\n    }\n\n    @Test\n    public void fillingOutForm_withMultipleRecordActions_recordsAudioOnceForAllOfThem() throws Exception {\n        FormEntryPage formEntryPage = rule.startAtMainMenu()\n                .copyForm(\"one-question-background-audio-multiple.xml\")\n                .startBlankForm(\"One Question\");\n        assertThat(stubAudioRecorderViewModel.isRecording(), is(true));\n\n        FormEndPage formEndPage = formEntryPage\n                .inputText(\"123\")\n                .swipeToEndScreen();\n        assertThat(stubAudioRecorderViewModel.isRecording(), is(true));\n\n        formEndPage.clickFinalize();\n        assertThat(stubAudioRecorderViewModel.isRecording(), is(false));\n\n        File instancesDir = new File(testDependencies.storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES));\n        File recording = Arrays.stream(instancesDir.listFiles()[0].listFiles()).filter(f -> f.getName().contains(\".fake\")).findAny().get();\n        File instanceFile = Arrays.stream(instancesDir.listFiles()[0].listFiles()).filter(f -> f.getName().contains(\".xml\")).findAny().get();\n        String instanceXml = new String(Files.readAllBytes(instanceFile.toPath()));\n        assertThat(instanceXml, containsString(\"<recording1>\" + recording.getName() + \"</recording1>\"));\n        assertThat(instanceXml, containsString(\"<recording2>\" + recording.getName() + \"</recording2>\"));\n    }\n\n    @Test\n    public void pressingBackWhileRecording_andClickingSave_exitsForm() {\n        rule.startAtMainMenu()\n                .copyForm(\"one-question-background-audio.xml\")\n                .startBlankForm(\"One Question\")\n                .closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickSaveChanges();\n    }\n\n    @Test\n    public void uncheckingRecordAudio_andConfirming_endsAndDeletesRecording() {\n        FormEntryPage formEntryPage = rule.startAtMainMenu()\n                .copyForm(\"one-question-background-audio.xml\")\n                .startBlankForm(\"One Question\")\n                .clickOptionsIcon()\n                .clickRecordAudio()\n                .clickOk();\n\n        assertThat(stubAudioRecorderViewModel.isRecording(), is(false));\n        assertThat(stubAudioRecorderViewModel.getLastRecording(), is(nullValue()));\n\n        formEntryPage.closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickDiscardForm()\n                .startBlankForm(\"One Question\");\n\n        assertThat(stubAudioRecorderViewModel.isRecording(), is(false));\n    }\n\n    @Test\n    public void whenRecordAudioPermissionNotGranted_openingForm_andDenyingPermissions_closesForm() {\n        permissionsChecker.revoke();\n        permissionsProvider.makeControllable();\n\n        rule.startAtMainMenu()\n                .copyForm(\"one-question-background-audio.xml\")\n                .startBlankFormWithDialog(\"One Question\")\n                .assertText(org.odk.collect.strings.R.string.background_audio_permission_explanation)\n                .clickOK(new FormEntryPage(\"One Question\"));\n\n        permissionsProvider.additionalExplanationClosed();\n        new MainMenuPage().assertOnPage();\n    }\n\n    @Test\n    public void viewForm_doesNotRecordAudio() {\n        rule.startAtMainMenu()\n                .copyForm(\"one-question-background-audio.xml\")\n                .startBlankForm(\"One Question\")\n                .fillOutAndFinalize(new FormEntryPage.QuestionAndAnswer(\"what is your age\", \"17\"))\n                .clickSendFinalizedForm(1)\n                .clickOnForm(\"One Question\");\n\n        assertThat(stubAudioRecorderViewModel.isRecording(), is(false));\n    }\n\n    private static class RevokeableRecordAudioPermissionsChecker extends ContextCompatPermissionChecker {\n\n        private boolean revoked;\n\n        RevokeableRecordAudioPermissionsChecker(Context context) {\n            super(context);\n        }\n\n        @Override\n        public boolean isPermissionGranted(String... permissions) {\n            if (permissions[0].equals(Manifest.permission.RECORD_AUDIO) && revoked) {\n                return false;\n            } else {\n                return super.isPermissionGranted(permissions);\n            }\n        }\n\n        public void revoke() {\n            revoked = true;\n        }\n    }\n\n    private static class ControllableRecordAudioPermissionsProvider extends PermissionsProvider {\n\n        private PermissionListener action;\n        private boolean controllable;\n\n        ControllableRecordAudioPermissionsProvider(PermissionsChecker permissionsChecker) {\n            super(permissionsChecker);\n        }\n\n        @Override\n        public void requestRecordAudioPermission(Activity activity, @NonNull PermissionListener action) {\n            if (controllable) {\n                this.action = action;\n            } else {\n                super.requestRecordAudioPermission(activity, action);\n            }\n        }\n\n        public void makeControllable() {\n            controllable = true;\n        }\n\n        public void additionalExplanationClosed() {\n            InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> action.additionalExplanationClosed());\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/CascadingSelectTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\n\n@RunWith(AndroidJUnit4::class)\nclass CascadingSelectTest {\n    private var rule = CollectTestRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = chain().around(rule)\n\n    @Test // This was for a bug but the original issue is missing\n    fun cascadingSelect_withACSVFileWithColumnNamesStartingWithNumbers_shouldWorkCorrectly() {\n        rule.startAtMainMenu()\n            .copyForm(\"numberInCSV.xml\", listOf(\"itemSets.csv\"))\n            .startBlankForm(\"numberInCSV\")\n            .swipeToNextQuestion(\"1a\")\n            .clickOnText(\"Venda de animais\")\n            .assertText(\"1a\")\n            .swipeToNextQuestion(\"2a\")\n            .clickOnText(\"Vendas agrícolas\")\n            .assertText(\"2a\")\n            .swipeToNextQuestion(\"3a\")\n            .clickOnText(\"Pensão\")\n            .assertText(\"3a\")\n            .swipeToNextQuestion(\"Thank you for taking the time to complete this form!\")\n            .swipeToEndScreen()\n            .clickFinalize()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/CatchFormDesignExceptionsTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.OkDialog\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass CatchFormDesignExceptionsTest {\n\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain().around(rule)\n\n    @Test // https://github.com/getodk/collect/issues/4750\n    fun whenFormHasFatalErrors_explanationDialogShouldBeDisplayedAndSurviveActivityRecreationAndTheFormShouldBeClosedAfterClickingOK() {\n        rule.startAtMainMenu()\n            .copyForm(\"form_design_error.xml\")\n            .clickFillBlankForm()\n            .clickOnForm(\"Relevance and calculate loop\")\n            .answerQuestion(1, \"B\")\n            .assertText(\"third\")\n            .answerQuestion(2, \"C\")\n            // Answering C above triggers a recomputation round which resets fullName to name.\n            // They're then equal which makes the third question non-relevant. Trying to change the\n            // value of a non-relevant node throws an exception.\n            .answerQuestion(2, \"D\")\n            .assertTextInDialog(org.odk.collect.strings.R.string.update_widgets_error)\n            .rotateToLandscape(OkDialog())\n            .assertTextInDialog(org.odk.collect.strings.R.string.update_widgets_error)\n            .clickOKOnDialog()\n            .assertOnPage(MainMenuPage())\n    }\n\n    @Test\n    fun whenFormHasNonFatalErrors_explanationDialogShouldBeDisplayedAndTheFormShouldNotBeClosedAfterClickingOK() {\n        rule.startAtMainMenu()\n            .copyForm(\"g6Error.xml\")\n            .startBlankFormWithError(\"g6Error\", false)\n            .clickOK(FormEntryPage(\"g6Error\"))\n    }\n\n    @Test\n    fun whenFormHasNonFatalErrors_explanationDialogShouldNotSurviveActivityRecreation() {\n        rule.startAtMainMenu()\n            .copyForm(\"g6Error.xml\")\n            .startBlankFormWithError(\"g6Error\", false)\n            .clickOK(FormEntryPage(\"g6Error\"))\n            .rotateToLandscape(FormEntryPage(\"g6Error\"))\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.error_occured)\n    }\n\n    @Test\n    fun typeMismatchErrorMessage_shouldBeDisplayedWhenItOccurs() {\n        rule.startAtMainMenu()\n            .copyForm(\"validate.xml\")\n            .startBlankForm(\"validate\")\n            .longPressOnQuestion(\"year\")\n            .removeResponse()\n            .swipeToNextQuestionWithError(false)\n            .checkIsTextDisplayedOnDialog(\"The value \\\"-01-01\\\" can't be converted to a date.\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ContextMenuTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.support.rules.BlankFormTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\npublic class ContextMenuTest {\n    private static final String STRING_WIDGETS_TEST_FORM = \"string_widgets_in_field_list.xml\";\n\n    public BlankFormTestRule activityTestRule = new BlankFormTestRule(STRING_WIDGETS_TEST_FORM, \"fl\");\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(activityTestRule);\n\n    @Test\n    public void whenRemoveStringAnswer_ShouldAppropriateQuestionBeCleared() {\n        activityTestRule.startInFormEntry()\n                .answerQuestion(0, \"TestString\")\n                .answerQuestion(1, \"1234\")\n                .assertText(\"TestString\")\n                .assertText(\"1234\")\n                .longPressOnQuestion(\"Question1\")\n                .removeResponse()\n                .assertTextDoesNotExist(\"TestString\")\n                .assertText(\"1234\")\n                .answerQuestion(0, \"TestString\")\n                .assertText(\"TestString\")\n                .longPressOnQuestion(\"Question2\")\n                .removeResponse()\n                .assertTextDoesNotExist(\"1234\")\n                .assertText(\"TestString\");\n    }\n\n    @Test\n    public void whenLongPressedOnEditText_ShouldNotRemoveAnswerOptionAppear() {\n        activityTestRule.startInFormEntry()\n                .assertOnPage()\n                .longPressOnQuestion(R.id.answer_container, 0)\n                .assertTextDoesNotExist(org.odk.collect.strings.R.string.clear_answer);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/DeletingRepeatGroupsTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\n\nimport org.junit.Ignore;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.support.pages.FormEndPage;\nimport org.odk.collect.android.support.pages.FormEntryPage;\nimport org.odk.collect.android.support.pages.FormHierarchyPage;\nimport org.odk.collect.android.support.rules.BlankFormTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.testshared.RecyclerViewMatcher;\n\n@Ignore\npublic class DeletingRepeatGroupsTest {\n    private static final String TEST_FORM = \"repeat_groups.xml\";\n\n    private final BlankFormTestRule activityTestRule = new BlankFormTestRule(TEST_FORM, \"repeatGroups\");\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(activityTestRule);\n\n    @Test\n    public void requestingDeletionOfFirstRepeat_deletesFirstRepeat() {\n        activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .deleteGroup(\"text1\")\n                .assertText(\"2\");\n    }\n\n    @Test\n    public void requestingDeletionOfMiddleRepeat_deletesMiddleRepeat() {\n        activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .swipeToNextRepeat(\"repeatGroup\", 2)\n                .deleteGroup(\"text1\")\n                .assertText(\"3\");\n    }\n\n    @Test\n    public void requestingDeletionOfLastRepeat_deletesLastRepeat() {\n        activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .swipeToNextRepeat(\"repeatGroup\", 2)\n                .swipeToNextRepeat(\"repeatGroup\", 3)\n                .swipeToNextRepeat(\"repeatGroup\", 4)\n                .deleteGroup(\"text1\")\n                .assertText(\"number1\");\n    }\n\n    @Test\n    public void requestingDeletionOfFirstRepeatInHierarchy_deletesFirstRepeat() {\n        FormHierarchyPage page = activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon();\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(4)));\n\n        page.clickOnText(\"repeatGroup > 1\")\n                .assertText(\"1\")\n                .deleteGroup();\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(3)));\n\n        page.clickOnText(\"repeatGroup > 1\")\n                .assertText(\"2\");\n    }\n\n    @Test\n    public void requestingDeletionOfMiddleRepeatInHierarchy_deletesMiddleRepeat() {\n        FormHierarchyPage page = activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon();\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(4)));\n\n        page.clickOnText(\"repeatGroup > 2\")\n                .assertText(\"2\")\n                .deleteGroup();\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(3)));\n\n        page.clickOnText(\"repeatGroup > 2\")\n                .assertText(\"3\");\n    }\n\n    @Test\n    public void requestingDeletionOfLastRepeatInHierarchy_deletesLastRepeat() {\n        FormHierarchyPage page = activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon();\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(4)));\n\n        page.clickOnText(\"repeatGroup > 4\")\n                .assertText(\"4\")\n                .deleteGroup();\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(3)));\n\n        page.clickOnText(\"repeatGroup > 3\")\n                .assertText(\"3\");\n    }\n\n    @Test\n    public void requestingDeletionOfAllRepeatsInHierarchyStartingFromIndexThatWillBeDeleted_shouldBringAUserToTheFirstRelevantQuestionBeforeTheGroup() {\n        activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickOnText(\"repeatGroup > 4\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroup > 3\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroup > 2\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroup > 1\")\n                .deleteGroup()\n                .pressBack(new FormEntryPage(\"repeatGroups\"))\n                .assertText(\"text0\");\n    }\n\n    @Test\n    public void requestingDeletionOfAllRepeatsInHierarchyStartingFromIndexThatWillNotBeDeleted_shouldBringAUserBackToTheSameIndex() {\n        activityTestRule.startInFormEntry()\n                .clickGoToArrow()\n                .clickOnText(\"repeatGroup\")\n                .clickOnText(\"repeatGroup > 4\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroup > 3\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroup > 2\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroup > 1\")\n                .deleteGroup()\n                .pressBack(new FormEntryPage(\"repeatGroups\"))\n                .assertText(\"text0\");\n    }\n\n    @Test\n    public void requestingDeletionOfAllRepeatsInHierarchyStartingFromTheEndView_shouldBringAUserToTheEndView() {\n        activityTestRule.startInFormEntry()\n                .clickGoToArrow()\n                .clickJumpEndButton()\n                .clickGoToArrow()\n                .clickOnText(\"repeatGroup\")\n                .clickOnText(\"repeatGroup > 4\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroup > 3\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroup > 2\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroup > 1\")\n                .deleteGroup()\n                .pressBack(new FormEndPage(\"repeatGroups\"));\n    }\n\n    @Test\n    public void requestingDeletionOfFirstRepeatWithFieldList_deletesFirstRepeat() {\n        activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickGoUpIcon()\n                .clickOnText(\"repeatGroupFieldList\")\n                .clickOnText(\"repeatGroupFieldList > 1\")\n                .clickOnQuestion(\"number1\")\n                .deleteGroup(\"number1\")\n                .assertText(\"2\");\n    }\n\n    @Test\n    public void requestingDeletionOfMiddleRepeatWithFieldList_deletesMiddleRepeat() {\n        activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickGoUpIcon()\n                .clickOnText(\"repeatGroupFieldList\")\n                .clickOnText(\"repeatGroupFieldList > 2\")\n                .clickOnQuestion(\"number1\")\n                .deleteGroup(\"number1\")\n                .assertText(\"3\");\n    }\n\n    @Test\n    public void requestingDeletionOfLastRepeatWithFieldList_deletesLastRepeat() {\n        activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickGoUpIcon()\n                .clickOnText(\"repeatGroupFieldList\")\n                .clickOnText(\"repeatGroupFieldList > 4\")\n                .clickOnQuestion(\"number1\")\n                .deleteGroup(\"number1\")\n                .assertOnPage(new FormEndPage(\"repeatGroups\"));\n    }\n\n    @Test\n    public void requestingDeletionOfFirstRepeatWithFieldListInHierarchy_deletesFirstRepeat() {\n        FormHierarchyPage page = activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickGoUpIcon()\n                .clickOnText(\"repeatGroupFieldList\");\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(4)));\n\n        page.clickOnText(\"repeatGroupFieldList > 1\")\n                .deleteGroup();\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(3)));\n\n        page.clickOnText(\"repeatGroupFieldList > 1\")\n                .assertText(\"2\");\n    }\n\n    @Test\n    public void requestingDeletionOfMiddleRepeatWithFieldListInHierarchy_deletesMiddleRepeat() {\n        FormHierarchyPage page = activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickGoUpIcon()\n                .clickOnText(\"repeatGroupFieldList\");\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(4)));\n\n        page.clickOnText(\"repeatGroupFieldList > 2\")\n                .deleteGroup();\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(3)));\n\n        page.clickOnText(\"repeatGroupFieldList > 2\")\n                .assertText(\"3\");\n    }\n\n    @Test\n    public void requestingDeletionOfLastRepeatWithFieldListInHierarchy_deletesLastRepeat() {\n        FormHierarchyPage page = activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickGoUpIcon()\n                .clickOnText(\"repeatGroupFieldList\");\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(4)));\n\n        page.clickOnText(\"repeatGroupFieldList > 4\")\n                .deleteGroup();\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(3)));\n\n        page.clickOnText(\"repeatGroupFieldList > 3\")\n                .assertText(\"3\");\n    }\n\n    @Test\n    public void requestingDeletionOfAllRepeatsWithFieldListInHierarchyStartingFromIndexThatWillBeDeleted_shouldBringAUserToTheFirstRelevantQuestionBeforeTheGroup() {\n        activityTestRule.startInFormEntry()\n                .swipeToNextQuestion(\"text1\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickGoUpIcon()\n                .clickOnText(\"repeatGroupFieldList\")\n                .clickOnText(\"repeatGroupFieldList > 1\")\n                .clickOnQuestion(\"number1\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickOnText(\"repeatGroupFieldList > 4\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroupFieldList > 3\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroupFieldList > 2\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroupFieldList > 1\")\n                .deleteGroup()\n                .pressBack(new FormEntryPage(\"repeatGroups\"))\n                .assertText(\"repeatGroup > 4\");\n    }\n\n    @Test\n    public void requestingDeletionOfAllRepeatsWithFieldListInHierarchyStartingFromIndexThatWillNotBeDeleted_shouldBringAUserBackToTheSameIndex() {\n        activityTestRule.startInFormEntry()\n                .clickGoToArrow()\n                .clickOnText(\"repeatGroupFieldList\")\n                .clickOnText(\"repeatGroupFieldList > 4\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroupFieldList > 3\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroupFieldList > 2\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroupFieldList > 1\")\n                .deleteGroup()\n                .pressBack(new FormEntryPage(\"repeatGroups\"))\n                .assertText(\"text0\");\n    }\n\n    @Test\n    public void requestingDeletionOfAllRepeatsWithFieldListInHierarchyStartingFromTheEndView_shouldBringAUserToTheEndView() {\n        activityTestRule.startInFormEntry()\n                .clickGoToArrow()\n                .clickJumpEndButton()\n                .clickGoToArrow()\n                .clickOnText(\"repeatGroupFieldList\")\n                .clickOnText(\"repeatGroupFieldList > 4\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroupFieldList > 3\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroupFieldList > 2\")\n                .deleteGroup()\n                .clickOnText(\"repeatGroupFieldList > 1\")\n                .deleteGroup()\n                .pressBack(new FormEndPage(\"repeatGroups\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/EncryptedFormTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.SendFinalizedFormPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.strings.R\n\nclass EncryptedFormTest {\n    val testDependencies = TestDependencies()\n\n    val rule: CollectTestRule = CollectTestRule()\n\n    @get:Rule\n    val copyFormChain: RuleChain = chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun instanceOfEncryptedForm_cantBeViewedAfterFinalizing() {\n        rule.startAtMainMenu()\n            .copyForm(\"encrypted.xml\")\n\n            .startBlankForm(\"encrypted\")\n            .assertQuestion(\"Question 1\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnText(\"encrypted\")\n            .checkIsToastWithMessageDisplayed(R.string.encrypted_form)\n            .assertOnPage()\n    }\n\n    @Test\n    fun instanceOfEncryptedForm_cantBeViewedAfterSending() {\n        rule.startAtMainMenu()\n            .copyForm(\"encrypted.xml\")\n            .setServer(testDependencies.server.url)\n\n            .startBlankForm(\"encrypted\")\n            .assertQuestion(\"Question 1\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .pressBack(MainMenuPage())\n\n            .clickViewSentForm(1)\n            .clickOnText(\"encrypted\")\n            .assertText(R.string.encrypted_form)\n            .assertOnPage()\n    }\n\n    @Test\n    fun instanceOfEncryptedFormWithoutInstanceID_failsFinalizationWithMessage() {\n        rule.startAtMainMenu()\n            .copyForm(\"encrypted-no-instanceID.xml\")\n            .startBlankForm(\"encrypted-no-instanceID\")\n            .assertQuestion(\"Question 1\")\n            .swipeToEndScreen()\n            .clickFinalize()\n            .checkIsToastWithMessageDisplayed(\"This form does not specify an instanceID. You must specify one to enable encryption. Form has not been saved as finalized.\")\n            .clickDrafts()\n            .checkInstanceState(\"encrypted-no-instanceID\", Instance.STATUS_INCOMPLETE)\n    }\n\n    @Test\n    fun instanceOfEncryptedFormWithoutInstanceID_doesNotLeaveSavepointOnFinalization() {\n        rule.startAtMainMenu()\n            .copyForm(\"encrypted-no-instanceID.xml\")\n            .startBlankForm(\"encrypted-no-instanceID\")\n            .clickGoToArrow()\n            .clickGoToEnd()\n            .clickFinalize()\n            .checkIsToastWithMessageDisplayed(\"This form does not specify an instanceID. You must specify one to enable encryption. Form has not been saved as finalized.\")\n            .startBlankForm(\"encrypted-no-instanceID\")\n    }\n\n    @Test\n    fun unicodeInEncryptedSubmissionsShouldBePreserved() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-encrypted-unicode.xml\")\n            .setServer(testDependencies.server.url)\n\n            .startBlankForm(\"One Question Encrypted Unicode\")\n            .assertQuestion(\"what is your age\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n\n        val submission = testDependencies.server.submissions[0]\n        assertThat(submission.readText().contains(\"one_questión_encrypted_unicode\"), equalTo(true))\n        assertThat(submission.readText().contains(\"versión 4\"), equalTo(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalAudioRecordingTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport static androidx.test.espresso.intent.Intents.intending;\nimport static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;\nimport static org.odk.collect.android.utilities.FileUtils.copyFileFromResources;\n\nimport android.app.Activity;\nimport android.app.Instrumentation;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.provider.MediaStore;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.RunnableRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.androidtest.RecordedIntentsRule;\n\nimport java.io.File;\nimport java.io.IOException;\n\n@RunWith(AndroidJUnit4.class)\npublic class ExternalAudioRecordingTest {\n\n    public final CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public final RuleChain chain = TestRuleChain.chain()\n            .around(new RecordedIntentsRule())\n            .around(new RunnableRule(() -> {\n                // Return audio file when RECORD_SOUND_ACTION intent is sent\n\n                try {\n                    File stubRecording = File.createTempFile(\"test\", \".m4a\");\n                    stubRecording.deleteOnExit();\n                    copyFileFromResources(\"media/test.m4a\", stubRecording.getAbsolutePath());\n\n                    Intent intent = new Intent();\n                    intent.setData(Uri.fromFile(stubRecording));\n                    Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, intent);\n                    intending(hasAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION)).respondWith(result);\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            }))\n            .around(rule);\n\n    @Test\n    public void onAudioQuestion_whenAudioQualityIsExternal_usesExternalRecorder() throws Exception {\n        new MainMenuPage()\n                .copyForm(\"external-audio-question.xml\")\n                .startBlankForm(\"External Audio Question\")\n                .clickOnString(org.odk.collect.strings.R.string.capture_audio)\n                .assertContentDescriptionNotDisplayed(org.odk.collect.strings.R.string.stop_recording)\n                .assertTextDoesNotExist(org.odk.collect.strings.R.string.capture_audio)\n                .assertContentDescriptionDisplayed(org.odk.collect.strings.R.string.play_audio);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalSecondaryInstanceTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\nimport java.util.Collections;\n\n/**\n * This tests the \"Multiple choice from file\" feature of XLSForms which is also referred to as just\n * \"external datasets\" in ODK docs and \"External secondary instances\" in XForm docs.\n *\n * @see <a href=\"https://xlsform.org/en/#multiple-choice-from-file\">Multiple choice from file</a>\n * @see <a href=\"https://docs.getodk.org/form-datasets/\">Form datasets</a>\n * @see <a href=\"https://getodk.github.io/xforms-spec/#secondary-instances---external\">External secondary  instances</a>\n */\n\n@RunWith(AndroidJUnit4.class)\npublic class ExternalSecondaryInstanceTest {\n\n    public CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(rule);\n\n    @Test\n    public void displaysAllOptionsFromXMLSecondaryInstance() {\n        rule.startAtMainMenu()\n                .copyForm(\"external_select.xml\", Collections.singletonList(\"external_data.xml\"))\n                .startBlankForm(\"external select\")\n                .assertQuestion(\"First\")\n                .assertText(\"One\")\n                .assertText(\"Two\")\n                .assertText(\"Three\");\n    }\n\n    @Test\n    public void displaysAllOptionsFromCSVSecondaryInstance() {\n        rule.startAtMainMenu()\n                .copyForm(\"external_select_csv.xml\", Collections.singletonList(\"external_data.csv\"))\n                .startBlankForm(\"external select\")\n                .assertQuestion(\"First\")\n                .assertText(\"One\")\n                .assertText(\"Two\")\n                .assertText(\"Three\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ExternalSelectsTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.storage.StorageSubdirectory\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\n\n/**\n * This tests the \"External selects\" feature of XLSForms. This will often be referred to as \"fast\n * external itemsets\".\n *\n * @see [External selects](https://xlsform.org/en/.external-selects)\n */\nclass ExternalSelectsTest {\n    private var rule: CollectTestRule = CollectTestRule()\n\n    @get:Rule\n    val copyFormChain: RuleChain = chain()\n        .around(rule)\n\n    @Test\n    fun displaysAllChoicesFromItemsetsCSV() {\n        rule.startAtMainMenu()\n            .copyForm(\"selectOneExternal.xml\", listOf(\"selectOneExternal-media/itemsets.csv\"))\n            .startBlankForm(\"selectOneExternal\")\n            .clickOnText(\"Texas\")\n            .swipeToNextQuestion(\"county\")\n            .assertText(\"King\")\n            .assertText(\"Cameron\")\n    }\n\n    @Test\n    fun missingFileMessage_shouldBeDisplayedIfExternalFileWithChoicesIsMissing() {\n        val formsDirPath = StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS)\n\n        rule.startAtMainMenu()\n            .copyForm(\"select_one_external.xml\")\n            .startBlankForm(\"cascading select test\")\n            .clickOnText(\"Texas\")\n            .swipeToNextQuestion(\"county\")\n            .assertText(\"File: $formsDirPath/select_one_external-media/itemsets.csv is missing.\")\n            .swipeToNextQuestion(\"city\")\n            .assertText(\"File: $formsDirPath/select_one_external-media/itemsets.csv is missing.\")\n    }\n\n    @Test\n    fun missingFileMessage_shouldBeDisplayedIfExternalFileWithChoicesUsedBySearchFunctionIsMissing() {\n        val formsDirPath = StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS)\n\n        rule.startAtMainMenu()\n            .copyForm(\"search_and_select.xml\")\n            .startBlankForm(\"search_and_select\")\n            .assertText(\"File: $formsDirPath/search_and_select-media/nombre.csv is missing.\")\n            .assertText(\"File: $formsDirPath/search_and_select-media/nombre2.csv is missing.\")\n    }\n\n    @Test\n    fun dynamicChoicesCanBeMixedWithNumericInternalOnes() {\n        rule.startAtMainMenu()\n            .copyForm(\"dynamic_and_static_choices.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"dynamic_and_static_choices\")\n            .assertTexts(\"Mango\", \"Oranges\", \"Strawberries\", \"None of the above\")\n    }\n\n    @Test\n    fun missingFileMessage_shouldBeDisplayedIfDynamicChoicesUsedButTheConfigurationRowIsMissing() {\n        val formsDirPath = StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS)\n\n        rule.startAtMainMenu()\n            .copyForm(\"dynamic_and_static_choices.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"dynamic_and_static_choices\")\n            .swipeToNextQuestion(\"Choose a number\")\n            .assertText(\"File: $formsDirPath/dynamic_and_static_choices-media/numbers.csv is missing.\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FieldListUpdateTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport android.app.Activity\nimport android.app.Instrumentation\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.intent.Intents\nimport androidx.test.espresso.intent.matcher.IntentMatchers\nimport org.hamcrest.Matchers\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.R\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.rules.FormEntryActivityTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.androidtest.RecordedIntentsRule\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.util.Random\n\nclass FieldListUpdateTest {\n    private var rule: FormEntryActivityTestRule = FormEntryActivityTestRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = chain()\n        .around(RecordedIntentsRule())\n        .around(rule)\n\n    @Test\n    fun relevanceChangeAtEnd_ShouldToggleLastWidgetVisibility() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .assertNoQuestion(\"Target1\")\n            .answerQuestion(\"Source1\", \"A\")\n            .assertQuestion(\"Target1\")\n            .assertTextBelow(\"Target1\", \"Source1\")\n            .longPressOnQuestion(\"Source1\")\n            .removeResponse()\n            .assertNoQuestion(\"Target1\")\n    }\n\n    @Test\n    fun relevanceChangeAtBeginning_ShouldToggleFirstWidgetVisibility() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Single relevance at beginning\")\n            .clickOnQuestion(\"Source2\")\n            .assertNoQuestion(\"Target2\")\n            .answerQuestion(\"Source2\", \"A\")\n            .assertQuestion(\"Target2\")\n            .assertTextBelow(\"Source2\", \"Target2\")\n            .longPressOnQuestion(\"Source2\")\n            .removeResponse()\n            .assertNoQuestion(\"Target2\")\n    }\n\n    @Test\n    fun relevanceChangeInMiddle_ShouldToggleMiddleWidgetVisibility() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Single relevance in middle\")\n            .clickOnQuestion(\"Source3\")\n            .assertNoQuestion(\"Target3\")\n            .answerQuestion(\"Source3\", \"A\")\n            .assertQuestion(\"Target3\")\n            .assertTextBelow(\"Filler3\", \"Source3\")\n            .assertTextBelow(\"Filler3\", \"Target3\")\n            .longPressOnQuestion(\"Source3\")\n            .removeResponse()\n            .assertNoQuestion(\"Target3\")\n    }\n\n    @Test\n    fun changeInValueUsedInLabel_ShouldChangeLabelText() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Label change\")\n            .clickOnQuestion(\"Hello, , how are you today?\")\n            .assertQuestion(\"Hello, , how are you today?\")\n            .answerQuestion(\"What is your name?\", \"Adam\")\n            .assertQuestion(\"Hello, Adam, how are you today?\")\n            .longPressOnQuestion(\"What is your name?\")\n            .removeResponse()\n            .assertQuestion(\"Hello, , how are you today?\")\n    }\n\n    @Test\n    fun changeInValueUsedInHint_ShouldChangeHintText() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Hint change\")\n            .clickOnQuestion(\"What is your name?\")\n            .assertText(\"Please don't use your calculator, !\")\n            .answerQuestion(\"What is your name?\", \"Adam\")\n            .assertText(\"Please don't use your calculator, Adam!\")\n            .longPressOnQuestion(\"What is your name?\")\n            .removeResponse()\n            .assertText(\"Please don't use your calculator, !\")\n    }\n\n    @Test\n    fun changeInValueUsedInOtherField_ShouldChangeValue() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Value change\")\n            .clickOnQuestion(\"What is your name?\")\n            .assertAnswer(\"Name length\", \"0\")\n            .assertAnswer(\"First name letter\", \"\")\n            .answerQuestion(\"What is your name?\", \"Adam\")\n            .assertAnswer(\"Name length\", \"4\")\n            .assertAnswer(\"First name letter\", \"A\")\n            .longPressOnQuestion(\"What is your name?\")\n            .removeResponse()\n            .assertAnswer(\"Name length\", \"0\")\n            .assertAnswer(\"First name letter\", \"\")\n    }\n\n    @Test\n    fun selectionChangeAtFirstCascadeLevel_ShouldUpdateNextLevels() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\", listOf(\"fruits.csv\"))\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Cascading select\")\n            .clickOnQuestion(\"Level1\") // No choices should be shown for levels 2 and 3 when no selection is made for level 1\n            .assertTextsDoNotExist(\"A1\", \"B1\", \"C1\", \"A1A\") // Selecting C for level 1 should only reveal options for C at level 2\n            .clickOnText(\"C\")\n            .assertTextsDoNotExist(\"A1\", \"B1\", \"A1A\")\n            .assertText(\"C1\") // Selecting A for level 1 should reveal options for A at level 2\n            .clickOnText(\"A\")\n            .assertTextsDoNotExist(\"A1A\", \"B1\", \"C1\")\n            .assertText(\"A1\") // Selecting A1 for level 2 should reveal options for A1 at level 3\n            .clickOnText(\"A1\")\n            .assertText(\"A1A\")\n            .assertTextsDoNotExist(\"B1\", \"C1\")\n            .longPressOnQuestion(\"Level1\")\n            .removeResponse()\n            .assertTextsDoNotExist(\"A1\", \"B1\", \"C1\", \"A1A\")\n    }\n\n    @Test\n    fun clearingParentSelect_ShouldUpdateAllDependentLevels() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\", listOf(\"fruits.csv\"))\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Cascading select\")\n            .clickOnQuestion(\"Level1\")\n            .clickOnText(\"A\")\n            .clickOnText(\"A1\")\n            .clickOnText(\"A1B\")\n            .longPressOnQuestion(\"Level1\")\n            .removeResponse()\n            .assertTextsDoNotExist(\"A1\", \"A1B\")\n    }\n\n    @Test\n    fun selectionChangeAtOneCascadeLevelWithMinimalAppearance_ShouldUpdateNextLevels() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\", listOf(\"fruits.csv\"))\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Cascading select minimal\")\n            .clickOnQuestion(\"Level1\")\n            .assertTextsDoNotExist(\"A1\", \"B1\", \"C1\", \"A1A\") // No choices should be shown for levels 2 and 3 when no selection is made for level 1\n            .openSelectMinimalDialog(0)\n            .selectItem(\"C\") // Selecting C for level 1 should only reveal options for C at level 2\n            .assertTextsDoNotExist(\"A1\", \"B1\")\n            .openSelectMinimalDialog(1)\n            .selectItem(\"C1\")\n            .assertTextDoesNotExist(\"A1A\")\n            .clickOnText(\"C\")\n            .clickOnText(\"A\") // Selecting A for level 1 should reveal options for A at level 2\n            .openSelectMinimalDialog(1)\n            .assertText(\"A1\")\n            .assertTextsDoNotExist(\"A1A\", \"B1\", \"C1\")\n            .selectItem(\"A1\") // Selecting A1 for level 2 should reveal options for A1 at level 3\n            .openSelectMinimalDialog(2)\n            .assertText(\"A1A\")\n            .assertTextsDoNotExist(\"B1A\", \"B1\", \"C1\")\n    }\n\n    @Test\n    fun questionsAppearingBeforeCurrentTextQuestion_ShouldNotChangeFocus() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Push off screen\")\n            .clickOnQuestion(\"Source9\")\n            .assertNoQuestion(\"Target9-15\")\n            .answerQuestion(\"Source9\", \"A\")\n            .assertQuestion(\"Target9-15\")\n            .assertQuestionHasFocus(\"Source9\")\n    }\n\n    @Test\n    fun questionsAppearingBeforeCurrentBinaryQuestion_ShouldNotChangeFocus() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .apply {\n                // FormFillingActivity expects an image at a fixed path so copy the app logo there\n                val icon = BitmapFactory.decodeResource(\n                    ApplicationProvider.getApplicationContext<Context>().resources,\n                    R.drawable.notes\n                )\n                val tmpJpg = File(StoragePathProvider().getTmpImageFilePath())\n                tmpJpg.createNewFile()\n                val fos = FileOutputStream(tmpJpg)\n                icon.compress(Bitmap.CompressFormat.JPEG, 90, fos)\n                Intents.intending(Matchers.not(IntentMatchers.isInternal())).respondWith(\n                    Instrumentation.ActivityResult(\n                        Activity.RESULT_OK, null\n                    )\n                )\n            }\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Push off screen binary\")\n            .clickOnQuestion(\"Source10\")\n            .assertNoQuestion(\"Target10-15\")\n            .clickOnString(org.odk.collect.strings.R.string.capture_image)\n            .assertQuestion(\"Target10-15\")\n            .assertText(org.odk.collect.strings.R.string.capture_image)\n    }\n\n    @Test\n    fun changeInValueUsedInGuidanceHint_ShouldChangeGuidanceHintText() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickOptionsIcon()\n            .clickProjectSettings()\n            .clickFormManagement()\n            .openShowGuidanceForQuestions()\n            .clickOnString(org.odk.collect.strings.R.string.guidance_yes)\n            .pressBack(ProjectSettingsPage())\n            .pressBack(FormEntryPage(\"fieldlist-updates\"))\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Guidance hint\")\n            .clickOnQuestion(\"Source11\")\n            .assertTextDoesNotExist(\"10\")\n            .answerQuestion(\"Source11\", \"5\")\n            .assertQuestion(\"10\")\n            .longPressOnQuestion(\"Source11\")\n            .removeResponse()\n            .assertTextDoesNotExist(\"10\")\n    }\n\n    @Test\n    fun selectingADateForDateTime_ShouldChangeRelevanceOfRelatedField() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Date time\")\n            .clickOnQuestion(\"Source12\")\n            .assertNoQuestion(\"Target12\")\n            .clickOnString(org.odk.collect.strings.R.string.select_date)\n            .clickOKOnDialog()\n            .assertQuestion(\"Target12\")\n            .longPressOnQuestion(\"Source12\")\n            .removeResponse()\n            .assertNoQuestion(\"Target12\")\n    }\n\n    @Test\n    fun selectingARating_ShouldChangeRelevanceOfRelatedField() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Rating\")\n            .clickOnQuestion(\"Source13\")\n            .assertNoQuestion(\"Target13\")\n            .setRating(3.0f)\n            .assertQuestion(\"Target13\")\n            .longPressOnQuestion(\"Source13\")\n            .removeResponse()\n            .assertNoQuestion(\"Target13\")\n    }\n\n    @Test\n    fun manuallySelectingAValueForMissingExternalApp_ShouldTriggerUpdate() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"External app\")\n            .clickOnQuestion(\"Source14\")\n            .clickOnText(\"Launch\")\n            .assertNoQuestion(\"Target14\")\n            .answerQuestion(\"Source14\", Random().nextInt().toString())\n            .assertQuestion(\"Target14\")\n            .longPressOnQuestion(\"Source14\")\n            .removeResponse()\n            .assertNoQuestion(\"Target14\")\n    }\n\n    @Test\n    fun searchMinimalInFieldList() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\", listOf(\"fruits.csv\"))\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Search in field-list\")\n            .clickOnQuestion(\"Source15\")\n            .assertSelectMinimalDialogAnswer(null)\n            .assertNoQuestion(\"Target15\")\n            .openSelectMinimalDialog()\n            .assertTexts(\"Mango\", \"Oranges\", \"Strawberries\")\n            .selectItem(\"Strawberries\")\n            .assertQuestion(\"Target15\")\n            .assertSelectMinimalDialogAnswer(\"Strawberries\")\n            .longPressOnQuestion(\"Source15\")\n            .removeResponse()\n            .assertSelectMinimalDialogAnswer(null)\n            .assertNoQuestion(\"Target15\")\n    }\n\n    @Test\n    fun listOfQuestionsShouldNotBeScrolledToTheLastEditedQuestionAfterClickingOnAQuestion() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Long list of questions\")\n            .clickOnQuestion(\"Question1\")\n            .answerQuestion(0, \"X\")\n            .clickOnQuestionField(\"Question20\")\n            .assertQuestion(\"Question20\")\n    }\n\n    @Test\n    fun recordingAudio_ShouldChangeRelevanceOfRelatedField() {\n        rule.setUpProjectAndCopyForm(\"fieldlist-updates.xml\")\n            .fillNewForm(\"fieldlist-updates.xml\", \"fieldlist-updates\")\n            .clickGoToArrow()\n            .clickGoUpIcon()\n            .clickOnGroup(\"Audio\")\n            .clickOnQuestion(\"Source16\")\n            .assertNoQuestion(\"Target16\")\n            .clickOnString(org.odk.collect.strings.R.string.capture_audio)\n            .clickOnContentDescription(org.odk.collect.strings.R.string.stop_recording)\n            .assertQuestion(\"Target16\")\n            .clickOnString(org.odk.collect.strings.R.string.delete_answer_file)\n            .clickOnTextInDialog(\n                org.odk.collect.strings.R.string.delete_answer_file,\n                FormEntryPage(\"fieldlist-updates\")\n            )\n            .assertNoQuestion(\"Target16\")\n    }\n\n    @Test\n    fun changeInValueUsedToDetermineIfAQuestionIsRequired_ShouldUpdateTheRelatedRequiredQuestion() {\n        rule.setUpProjectAndCopyForm(\"dynamic_required_question.xml\")\n            .fillNewForm(\"dynamic_required_question.xml\", \"dynamic_required_question\")\n            .assertQuestion(\"Target\", false)\n            .answerQuestion(\"Source\", \"blah\")\n            .assertQuestion(\"Target\", true)\n            .swipeToNextQuestionWithConstraintViolation(org.odk.collect.strings.R.string.required_answer_error)\n            .longPressOnQuestion(\"Source\")\n            .removeResponse()\n            .assertQuestion(\"Target\", false)\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.required_answer_error)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FillBlankFormWithRepeatGroupTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.pages.AddNewRepeatDialog\nimport org.odk.collect.android.support.pages.FormEndPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\n\n@RunWith(AndroidJUnit4::class)\nclass FillBlankFormWithRepeatGroupTest {\n    private var rule = CollectTestRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = chain().around(rule)\n\n    @Test\n    fun whenNoRepeatGroupAdded_ShouldNotDoubleLastQuestion() {\n        rule.startAtMainMenu()\n            .copyForm(\"TestRepeat.xml\")\n            .startBlankForm(\"TestRepeat\")\n            .clickOptionsIcon()\n            .clickProjectSettings()\n            .clickOnUserInterface()\n            .clickNavigation()\n            .clickUseSwipesAndButtons()\n            .pressBack(ProjectSettingsPage())\n            .pressBack(FormEntryPage(\"TestRepeat\"))\n            .swipeToNextQuestionWithRepeatGroup(\"Repeat # 1\")\n            .clickOnDoNotAdd(AddNewRepeatDialog(\"Repeat # 2\"))\n            .clickOnDoNotAdd(FormEntryPage(\"TestRepeat\"))\n            .clickForwardButtonToEndScreen()\n            .clickFinalize()\n    }\n\n    @Test\n    fun dynamicGroupLabel_should_beCalculatedProperly() {\n        rule.startAtMainMenu()\n            .copyForm(\"RepeatTitles_1648.xml\")\n            .startBlankForm(\"Repeat titles 1648\")\n            .inputText(\"test\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"Name\")\n            .inputText(\"FirstPerson\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"Age\")\n            .inputText(\"25\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"Photo\")\n            .assertText(\"gr1 > 1 > Person: 25\")\n            .clickGoToArrow()\n            .assertPath(\"gr1 > 1 > Person: 25\")\n            .clickOnQuestion(\"Photo\")\n            .swipeToNextQuestionWithRepeatGroup(\"gr1\")\n            .clickOnDoNotAdd(FormEntryPage(\"Repeat titles 1648\"))\n            .inputText(\"SecondPart\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"Date\")\n            .assertText(\"Part1 > 1 > Xxx: SecondPart\")\n            .clickGoToArrow()\n            .assertPath(\"Part1 > 1 > Xxx: SecondPart\")\n            .clickOnQuestion(\"Date\")\n            .swipeToNextQuestion(\"Multi Select\")\n            .swipeToNextQuestionWithRepeatGroup(\"Part1\")\n            .clickOnDoNotAdd(FormEndPage(\"Repeat titles 1648\"))\n            .clickFinalize()\n    }\n\n    @Test\n    fun nestedGroupsWithFieldListAppearance_ShouldBeAbleToFillTheForm() {\n        rule.startAtMainMenu()\n            .copyForm(\"form1.xml\")\n            .startBlankForm(\"form1\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        rule.startAtMainMenu()\n            .copyForm(\"form2.xml\")\n            .startBlankForm(\"form2\")\n            .closeSoftKeyboard()\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        rule.startAtMainMenu()\n            .copyForm(\"form3.xml\")\n            .startBlankForm(\"form3\")\n            .closeSoftKeyboard()\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        rule.startAtMainMenu()\n            .copyForm(\"form4.xml\")\n            .startBlankForm(\"form4\")\n            .inputText(\"T1\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"T2\")\n            .inputText(\"T2\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"T3\")\n            .inputText(\"T3\")\n            .closeSoftKeyboard()\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        rule.startAtMainMenu()\n            .copyForm(\"form5.xml\")\n            .startBlankForm(\"form5\")\n            .inputText(\"T1\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"T2\")\n            .inputText(\"T2\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"T3\")\n            .inputText(\"T3\")\n            .closeSoftKeyboard()\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        rule.startAtMainMenu()\n            .copyForm(\"form6.xml\")\n            .startBlankForm(\"form6\")\n            .inputText(\"T1\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"T2\")\n            .inputText(\"T2\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"T3\")\n            .inputText(\"T3\")\n            .closeSoftKeyboard()\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        rule.startAtMainMenu()\n            .copyForm(\"form7.xml\")\n            .startBlankForm(\"form7\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        rule.startAtMainMenu()\n            .copyForm(\"form8.xml\")\n            .startBlankForm(\"form8\")\n            .closeSoftKeyboard()\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        rule.startAtMainMenu()\n            .copyForm(\"form9.xml\")\n            .startBlankForm(\"form9\")\n            .closeSoftKeyboard()\n            .swipeToEndScreen()\n            .clickFinalize()\n    }\n\n    @Test\n    fun whenNoRepeatGroupAdded_ShouldBackwardButtonBeClickable() {\n        rule.startAtMainMenu()\n            .copyForm(\"RepeatGroupAndGroup.xml\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickOnUserInterface()\n            .clickNavigation()\n            .clickUseSwipesAndButtons()\n            .pressBack(ProjectSettingsPage())\n            .pressBack(rule.startAtMainMenu())\n            .startBlankFormWithRepeatGroup(\"RepeatGroupAndGroup\", \"G1\")\n            .clickOnDoNotAdd(FormEntryPage(\"RepeatGroupAndGroup\"))\n            .closeSoftKeyboard()\n            .swipeToPreviousQuestionWithRepeatGroup(\"G1\")\n            .clickOnDoNotAdd(FormEntryPage(\"RepeatGroupAndGroup\"))\n            .closeSoftKeyboard()\n            .swipeToEndScreen()\n            .clickFinalize()\n    }\n\n    @Test\n    fun when_pageBehindRepeatGroupWithRegularGroupInsideIsVisible_should_swipeBackWork() {\n        rule.startAtMainMenu()\n            .copyForm(\"repeat_group_new.xml\")\n            .startBlankFormWithRepeatGroup(\"RepeatGroupNew\", \"People\")\n            .clickOnAdd(FormEntryPage(\"RepeatGroupNew\"))\n            .inputText(\"A\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"Age\")\n            .inputText(\"1\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestionWithRepeatGroup(\"People\")\n            .clickOnAdd(FormEntryPage(\"RepeatGroupNew\"))\n            .assertQuestion(\"Name\")\n            .inputText(\"B\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"Age\")\n            .inputText(\"2\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestionWithRepeatGroup(\"People\")\n            .clickOnAdd(FormEntryPage(\"RepeatGroupNew\"))\n            .assertQuestion(\"Name\")\n            .inputText(\"C\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"Age\")\n            .inputText(\"3\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestionWithRepeatGroup(\"People\")\n            .clickOnDoNotAdd(FormEndPage(\"RepeatGroupNew\"))\n            .swipeToPreviousQuestion(\"Age\")\n            .assertText(\"3\")\n            .swipeToNextQuestionWithRepeatGroup(\"People\")\n            .clickOnDoNotAdd(FormEndPage(\"RepeatGroupNew\"))\n            .clickFinalize()\n    }\n\n    @Test\n    fun when_navigateOnHierarchyView_should_breadcrumbPathBeVisible() {\n        rule.startAtMainMenu()\n            .copyForm(\"repeat_group_new.xml\")\n            .startBlankFormWithRepeatGroup(\"RepeatGroupNew\", \"People\")\n            .clickOnAdd(FormEntryPage(\"RepeatGroupNew\"))\n            .inputText(\"A\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"Age\")\n            .inputText(\"1\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestionWithRepeatGroup(\"People\")\n            .clickOnAdd(FormEntryPage(\"RepeatGroupNew\"))\n            .inputText(\"B\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"Age\")\n            .inputText(\"2\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestionWithRepeatGroup(\"People\")\n            .clickOnAdd(FormEntryPage(\"RepeatGroupNew\"))\n            .inputText(\"C\")\n            .closeSoftKeyboard()\n            .swipeToNextQuestion(\"Age\")\n            .inputText(\"3\")\n            .clickGoToArrow()\n            .assertPath(\"People > 3 > Person: C\")\n            .clickGoUpIcon()\n            .assertText(\"3.\\u200E Person: C\")\n            .clickJumpEndButton()\n            .clickFinalize()\n    }\n\n    @Test\n    fun openHierarchyPageFromLastView_ShouldNotDisplayError() {\n        rule.startAtMainMenu()\n            .copyForm(\"repeat_group_form.xml\")\n            .startBlankFormWithRepeatGroup(\"Repeat Group\", \"Grp1\")\n            .clickOnAdd(FormEntryPage(\"Repeat Group\"))\n            .swipeToNextQuestionWithRepeatGroup(\"Grp1\")\n            .clickOnDoNotAdd(FormEntryPage(\"Repeat Group\"))\n            .swipeToNextQuestionWithRepeatGroup(\"Grp2\")\n            .clickOnDoNotAdd(FormEndPage(\"Repeat Group\"))\n            .clickGoToArrow()\n            .clickJumpEndButton()\n            .clickFinalize()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormEndTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.pages.AccessControlPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.testshared.AssertionFramework\n\n@RunWith(AndroidJUnit4::class)\nclass FormEndTest {\n\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val copyFormChain: RuleChain = chain()\n        .around(rule)\n\n    @Test\n    fun fillingForm_andPressingFinalize_finalizesForm() {\n        rule.startAtMainMenu()\n            .copyForm(FORM)\n            .assertNumberOfFinalizedForms(0)\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"what is your age\", \"52\"))\n            .assertNumberOfEditableForms(0)\n            .assertNumberOfFinalizedForms(1)\n    }\n\n    @Test\n    fun fillingForm_andPressingSaveAsDraft_savesACompleteDraft() {\n        rule.startAtMainMenu()\n            .copyForm(FORM)\n            .assertNumberOfFinalizedForms(0)\n            .startBlankForm(\"One Question\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n            .assertNumberOfFinalizedForms(0)\n\n            .clickDrafts(1)\n            .assertContentDescriptionDisplayed(string.draft_no_errors, assertionFramework = AssertionFramework.COMPOSE)\n            .assertContentDescriptionNotDisplayed(string.draft_errors, assertionFramework = AssertionFramework.COMPOSE)\n    }\n\n    @Test\n    fun fillingForm_andPressingSaveAsDraft_whenThereAreViolatedConstraints_savesAIncompleteDraft() {\n        rule.startAtMainMenu()\n            .copyForm(\"two-question-required.xml\")\n            .assertNumberOfFinalizedForms(0)\n            .startBlankForm(\"Two Question Required\")\n            .clickGoToArrow()\n            .clickGoToEnd()\n            .clickSaveAsDraft()\n            .assertNumberOfFinalizedForms(0)\n\n            .clickDrafts(1)\n            .assertContentDescriptionDisplayed(string.draft_errors, assertionFramework = AssertionFramework.COMPOSE)\n            .assertContentDescriptionNotDisplayed(string.draft_no_errors, assertionFramework = AssertionFramework.COMPOSE)\n    }\n\n    @Test\n    fun disablingSaveAsDraftInSettings_disablesItInTheEndScreen() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickAccessControl()\n            .clickFormEntrySettings()\n            .clickOnSaveAsDraftInFormEnd()\n            .pressBack(AccessControlPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .copyForm(FORM)\n            .startBlankForm(\"One Question\")\n            .swipeToEndScreen()\n            .assertTextDoesNotExist(string.save_as_draft)\n    }\n\n    @Test\n    fun disablingFinalizeInSettings_disablesItInTheEndScreen() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickAccessControl()\n            .clickFormEntrySettings()\n            .clickOnString(string.finalize)\n            .pressBack(AccessControlPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .copyForm(FORM)\n            .startBlankForm(\"One Question\")\n            .swipeToEndScreen()\n            .assertTextDoesNotExist(string.finalize)\n    }\n\n    companion object {\n        private const val FORM = \"one-question.xml\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormHierarchyTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\nimport static org.odk.collect.testshared.RecyclerViewMatcher.withRecyclerView;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.support.pages.AddNewRepeatDialog;\nimport org.odk.collect.android.support.pages.FormEndPage;\nimport org.odk.collect.android.support.pages.FormEntryPage;\nimport org.odk.collect.android.support.pages.FormHierarchyPage;\nimport org.odk.collect.android.support.pages.ViewFormPage;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.testshared.RecyclerViewMatcher;\n\npublic class FormHierarchyTest {\n\n    public CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(rule);\n\n    @Test\n    //https://github.com/getodk/collect/issues/2871\n    public void allRelevantQuestionsShouldBeVisibleInHierarchyView() {\n        rule.startAtMainMenu()\n                .copyForm(\"formHierarchy1.xml\")\n                .startBlankForm(\"formHierarchy1\")\n                .clickGoToArrow();\n\n        onView(withRecyclerView(R.id.list)\n                .atPositionOnView(0, R.id.primary_text))\n                .check(matches(withText(\"what is your name?\")));\n\n        onView(withRecyclerView(R.id.list)\n                .atPositionOnView(1, R.id.primary_text))\n                .check(matches(withText(\"what is your age?\")));\n    }\n\n    @Test\n    //https://github.com/getodk/collect/issues/2944\n    public void notRelevantRepeatGroupsShouldNotBeVisibleInHierarchy() {\n        final FormHierarchyPage page = rule.startAtMainMenu()\n                .copyForm(\"formHierarchy2.xml\")\n                .startBlankForm(\"formHierarchy2\")\n                .inputText(\"2\")\n                .clickGoToArrow();\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(3)));\n        onView(withRecyclerView(R.id.list)\n                .atPositionOnView(0, R.id.primary_text))\n                .check(matches(withText(\"How many guests are in your party?\")));\n        onView(withRecyclerView(R.id.list)\n                .atPositionOnView(1, R.id.primary_text))\n                .check(matches(withText(\"Please provide details for each guest.\")));\n        onView(withRecyclerView(R.id.list)\n                .atPositionOnView(2, R.id.primary_text))\n                .check(matches(withText(\"Guest details\")));\n        onView(withRecyclerView(R.id.list)\n                .atPositionOnView(2, R.id.group_label))\n                .check(matches(withText(\"Repeatable Group\")));\n\n        page.clickOnText(\"Guest details\");\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(2)));\n        onView(withRecyclerView(R.id.list)\n                .atPositionOnView(0, R.id.primary_text))\n                .check(matches(withText(\"Guest details > 1\")));\n        onView(withRecyclerView(R.id.list)\n                .atPositionOnView(1, R.id.primary_text))\n                .check(matches(withText(\"Guest details > 2\")));\n\n        page.clickGoToStart()\n                .inputText(\"1\")\n                .clickGoToArrow()\n                .clickOnText(\"Guest details\");\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(1)));\n        onView(withRecyclerView(R.id.list)\n                .atPositionOnView(0, R.id.primary_text))\n                .check(matches(withText(\"Guest details > 1\")));\n    }\n\n    @Test\n    //https://github.com/getodk/collect/issues/2936\n    public void repeatGroupsShouldBeVisibleAsAppropriate() {\n        FormHierarchyPage page = rule.startAtMainMenu()\n                .copyForm(\"formHierarchy3.xml\")\n                .startBlankForm(\"formHierarchy3\")\n                .assertQuestion(\"Intro\")\n                .swipeToNextQuestion(\"Text\")\n                .swipeToNextQuestion(\"Integer 1_1\")\n                .swipeToNextQuestion(\"Integer 1_2\")\n                .swipeToNextQuestion(\"Integer 2_1\")\n                .swipeToNextQuestion(\"Integer 2_2\")\n                .swipeToNextQuestionWithRepeatGroup(\"Repeat Group 1\")\n                .clickOnAdd(new FormEntryPage(\"formHierarchy3\"))\n                .swipeToNextQuestionWithRepeatGroup(\"Repeat Group 1_1\")\n                .clickOnAdd(new FormEntryPage(\"formHierarchy3\"))\n                .swipeToNextQuestionWithRepeatGroup(\"Repeat Group 1_1\")\n                .clickOnDoNotAdd(new AddNewRepeatDialog(\"Repeat Group 1\"))\n                .clickOnDoNotAdd(new FormEntryPage(\"formHierarchy3\"))\n                .clickGoToArrow();\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(3)));\n\n        page.clickOnText(\"Group 1\");\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(3)));\n\n        page.assertTextDoesNotExist(\"Repeat Group 1\");\n    }\n\n    @Test\n    //https://github.com/getodk/collect/issues/2942\n    public void deletingLastGroupShouldNotBreakHierarchy() {\n        FormHierarchyPage page = rule.startAtMainMenu()\n                .copyForm(\"formHierarchy3.xml\")\n                .startBlankForm(\"formHierarchy3\")\n                .swipeToNextQuestion(\"Text\")\n                .swipeToNextQuestion(\"Integer 1_1\")\n                .swipeToNextQuestion(\"Integer 1_2\")\n                .swipeToNextQuestion(\"Integer 2_1\")\n                .swipeToNextQuestion(\"Integer 2_2\")\n                .swipeToNextQuestionWithRepeatGroup(\"Repeat Group 1\")\n                .clickOnAdd(new FormEntryPage(\"formHierarchy3\"))\n                .assertQuestion(\"Barcode\")\n                .swipeToNextQuestionWithRepeatGroup(\"Repeat Group 1_1\")\n                .clickOnAdd(new FormEntryPage(\"formHierarchy3\"))\n                .assertQuestion(\"Date\")\n                .swipeToNextQuestionWithRepeatGroup(\"Repeat Group 1_1\")\n                .clickOnAdd(new FormEntryPage(\"formHierarchy3\"))\n                .assertQuestion(\"Date\")\n                .swipeToNextQuestionWithRepeatGroup(\"Repeat Group 1_1\")\n                .clickOnDoNotAdd(new AddNewRepeatDialog(\"Repeat Group 1\"))\n                .clickOnDoNotAdd(new FormEntryPage(\"formHierarchy3\"))\n                .clickGoToArrow()\n                .clickOnText(\"Repeat Group 1\")\n                .clickOnText(\"Repeat Group 1 > 1\")\n                .clickOnText(\"Repeat Group 1_1\")\n                .clickOnText(\"Repeat Group 1_1 > 2\")\n                .deleteGroup();\n\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(1)));\n\n        page.assertText(\"Repeat Group 1_1 > 1\");\n    }\n\n    @Test\n    //https://github.com/getodk/collect/issues/3971\n    public void deletingLastGroupAndAddingOneShouldNotBreakHierarchy() {\n        rule.startAtMainMenu()\n                .copyForm(\"repeat_group_new.xml\")\n                .startBlankFormWithRepeatGroup(\"RepeatGroupNew\", \"People\")\n                .clickOnAdd(new FormEntryPage(\"RepeatGroupNew\"))\n                .assertQuestion(\"Name\")\n                .swipeToNextQuestion(\"Age\")\n                .swipeToNextQuestionWithRepeatGroup(\"People\")\n                .clickOnAdd(new FormEntryPage(\"RepeatGroupNew\"))\n                .assertQuestion(\"Name\")\n                .swipeToNextQuestion(\"Age\")\n                .swipeToNextQuestionWithRepeatGroup(\"People\")\n                .clickOnAdd(new FormEntryPage(\"RepeatGroupNew\"))\n                .clickGoToArrow()\n                .deleteGroup()\n                .addGroup();\n    }\n\n    @Test\n    //https://github.com/getodk/collect/issues/4570\n    public void showRepeatsPickerWhenFirstRepeatIsEmpty() {\n        rule.startAtMainMenu()\n                .copyForm(\"Empty First Repeat.xml\")\n                .startBlankFormWithRepeatGroup(\"Empty First Repeat\", \"Repeat\")\n                .clickOnAdd(new FormEntryPage(\"Empty First Repeat\"))\n                .answerQuestion(\"Question in repeat\", \"Not empty!\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickGoUpIcon()\n                .assertTexts(\"Repeat\", \"Repeatable Group\");\n    }\n\n    @Test\n    //https://github.com/getodk/collect/issues/6015\n    public void regularGroupThatWrapsARepeatableGroupShouldBeTreatedAsARegularOne() {\n        rule.startAtMainMenu()\n                .copyForm(\"repeat_group_wrapped_with_a_regular_group.xml\")\n                .startBlankForm(\"Repeat group wrapped with a regular group\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickGoUpIcon()\n                .assertPath(\"Outer\")\n                .assertNotRemovableGroup();\n    }\n\n    @Test\n    public void when_openHierarchyViewFromLastPage_should_mainGroupViewBeVisible() {\n        rule.startAtMainMenu()\n                .copyForm(\"repeat_group_form.xml\")\n                .startBlankFormWithRepeatGroup(\"Repeat Group\", \"Grp1\")\n                .clickOnDoNotAdd(new FormEntryPage(\"Repeat Group\"))\n                .clickGoToArrow()\n                .clickJumpEndButton()\n                .clickGoToArrow()\n                .checkIfElementInHierarchyMatchesToText(\"Group Name\", 0);\n    }\n\n    @Test\n    public void hierachyView_shouldNotChangeAfterScreenRotation() {\n        rule.startAtMainMenu()\n                .copyForm(\"repeat_group_form.xml\")\n                .startBlankFormWithRepeatGroup(\"Repeat Group\", \"Grp1\")\n                .clickOnDoNotAdd(new FormEntryPage(\"Repeat Group\"))\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .checkIfElementInHierarchyMatchesToText(\"Group Name\", 0)\n                .rotateToLandscape(new FormHierarchyPage(\"Repeat Group\"))\n                .checkIfElementInHierarchyMatchesToText(\"Group Name\", 0);\n    }\n\n    @Test\n    public void theListOfQuestionsShouldBeScrolledToTheLastDisplayedQuestionAfterOpeningTheHierarchy() {\n        rule.startAtMainMenu()\n                .copyForm(\"manyQ.xml\")\n                .startBlankForm(\"manyQ\")\n                .swipeToNextQuestion(\"t2\")\n                .swipeToNextQuestion(\"n1\")\n                .clickGoToArrow()\n                .assertText(\"n1\")\n                .assertTextDoesNotExist(\"t1\")\n                .assertTextDoesNotExist(\"t2\");\n    }\n\n    @Test\n    public void whenViewFormInHierarchyForRepeatGroup_noDeleteButtonAppears() {\n        rule.startAtMainMenu()\n                .copyForm(\"one-question-repeat.xml\")\n                .startBlankForm(\"One Question Repeat\")\n                .swipeToNextQuestionWithRepeatGroup(\"Person\")\n                .clickOnDoNotAdd(new FormEndPage(\"One Question Repeat\"))\n                .clickFinalize()\n\n                .clickSendFinalizedForm(1)\n                .clickOnForm(\"One Question Repeat\")\n                .clickOnGroup(\"Person\")\n                .clickOnGroup(\"Person > 1\")\n                .assertNoId(R.id.menu_delete_child);\n    }\n\n    @Test\n    public void whenViewFormInHierarchy_clickingOnQuestion_doesNothing() {\n        rule.startAtMainMenu()\n                .copyForm(\"one-question.xml\")\n                .startBlankForm(\"One Question\")\n                .fillOutAndFinalize()\n\n                .clickSendFinalizedForm(1)\n                .clickOnForm(\"One Question\")\n                .clickOnText(\"what is your age\")\n                .assertOnPage(new ViewFormPage(\"One Question\"));\n    }\n\n    @Test\n    public void clickingBackButtonAfterNavigatingInTheHierarchyOfGroups_movesToTheQuestionDisplayedBeforeOpeningTheHierarchy() {\n        rule.startAtMainMenu()\n                .copyForm(\"two-questions-in-group.xml\")\n                .startBlankForm(\"two-questions-in-group\")\n                .clickGoToArrow()\n                .clickGoUpIcon()\n                .clickOnGroup(\"Name\")\n                .pressBack(new FormEntryPage(\"two-questions-in-group\"))\n                .assertQuestion(\"First name\")\n                .assertNoQuestion(\"Last name\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormLanguageTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.pages.SaveOrDiscardFormDialog;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\n@RunWith(AndroidJUnit4.class)\npublic class FormLanguageTest {\n\n    public CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain().around(rule);\n\n    @Test\n    public void canSwitchLanguagesInForm() {\n        rule.startAtMainMenu()\n                .copyForm(\"one-question-translation.xml\")\n                .startBlankForm(\"One Question\")\n                .answerQuestion(\"what is your age\", \"64\")\n                .clickOptionsIcon()\n                .clickOnString(org.odk.collect.strings.R.string.change_language)\n                .clickOnText(\"French (fr)\")\n                .assertQuestion(\"quel âge as-tu\")\n                .assertText(\"64\"); // Check answer hasn't been cleared/changed\n    }\n\n    @Test\n    public void languageChoiceIsPersisted() {\n        rule.startAtMainMenu()\n                .copyForm(\"one-question-translation.xml\")\n                .startBlankForm(\"One Question\")\n                .clickOptionsIcon()\n                .clickOnString(org.odk.collect.strings.R.string.change_language)\n                .clickOnText(\"French (fr)\")\n                .closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickDiscardForm()\n\n                .startBlankForm(\"One Question\")\n                .assertQuestion(\"quel âge as-tu\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormMediaTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\n\n@RunWith(AndroidJUnit4::class)\nclass FormMediaTest {\n\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val ruleChain: RuleChain = chain().around(rule)\n\n    @Test\n    fun loadingFormWithZippedMedia_unzipsIntoMediaDirectory() {\n        rule.startAtMainMenu()\n            .copyForm(\"external_select_10.xml\", listOf(\"external_data_10.zip\"))\n            .startBlankForm(\"external select 10\")\n            .clickOnText(\"a\")\n            .swipeToNextQuestion(\"Second\")\n            .assertText(\"aa\")\n            .assertText(\"ab\")\n            .assertText(\"ac\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormMetadataTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass FormMetadataTest {\n\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain().around(rule)\n\n    /**\n     * CollectSetGeopointAction` instances will be serialized as part of a form definition cache.\n     * It's easy to introduce deserialization problems without realizing as they'll only\n     * appear on the second load.\n     */\n    @Test\n    fun startGeopoint_withCachedFormDefinition_doesNotCauseError() {\n        rule.startAtMainMenu()\n            .copyForm(\"start-geopoint.xml\")\n\n            // Open form once to cache definition\n            .clickFillBlankForm()\n            .clickOnEmptyForm(\"start-geopoint\")\n            .clickFinalize()\n\n            // Load with cached definition\n            .clickFillBlankForm()\n            .clickOnEmptyForm(\"start-geopoint\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormNavigationTest.kt",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.feature.formentry\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.pages.AccessControlPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\nclass FormNavigationTest {\n    var rule = CollectTestRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = TestRuleChain.chain()\n        .around(rule)\n\n    @Test\n    fun showsAndHidesButtonsCorrectlyOnEachScreen() {\n        rule.startAtMainMenu()\n            .copyForm(\"two-question.xml\")\n            .startBlankForm(\"Two Question\")\n            .assertQuestion(\"What is your name?\")\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_backward)\n            .clickForwardButton()\n            .assertQuestion(\"What is your age?\")\n            .clickBackwardButton()\n            .assertQuestion(\"What is your name?\")\n            .clickForwardButton()\n            .assertQuestion(\"What is your age?\")\n            .clickForwardButtonToEndScreen()\n            .assertText(org.odk.collect.strings.R.string.form_backward)\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_forward)\n    }\n\n    @Test\n    fun whenNavigatingBackwardsIsDisabled_showsAndHidesButtonsCorrectlyOnEachScreen() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickAccessControl()\n            .clickFormEntrySettings()\n            .clickMovingBackwards()\n            .clickOnString(org.odk.collect.strings.R.string.yes)\n            .pressBack(AccessControlPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .copyForm(\"two-question.xml\")\n            .startBlankForm(\"Two Question\")\n            .assertQuestion(\"What is your name?\")\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_backward)\n            .clickForwardButton()\n            .assertQuestion(\"What is your age?\")\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_backward)\n            .clickForwardButtonToEndScreen()\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_backward)\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_forward)\n    }\n\n    @Test\n    fun whenButtonsDisabled_buttonsNotShown() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickOnUserInterface()\n            .clickNavigation()\n            .clickSwipes()\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .copyForm(\"two-question.xml\")\n            .startBlankForm(\"Two Question\")\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_backward)\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_forward)\n            .swipeToNextQuestion(\"What is your age?\")\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_backward)\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_forward)\n            .swipeToEndScreen()\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_backward)\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_forward)\n    }\n\n    @Test\n    fun whenNavigationSettingsChangeChangesShouldBeReflectedInFormFilling() {\n        rule.startAtMainMenu()\n            .copyForm(\"two-question.xml\")\n            .startBlankForm(\"Two Question\")\n            .assertQuestion(\"What is your name?\")\n\n            // assert that 'Swipes and buttons' mode is enabled\n            .swipeToNextQuestion(\"What is your age?\")\n            .clickBackwardButton()\n            .assertText(\"What is your name?\")\n\n            // change settings to 'Horizontal swipes' mode'\n            .clickOptionsIcon()\n            .clickProjectSettings()\n            .clickOnUserInterface()\n            .clickNavigation()\n            .clickSwipes()\n            .pressBack(ProjectSettingsPage())\n            .pressBack(FormEntryPage(\"Two Question\"))\n\n            // assert that 'Horizontal swipes' mode is enabled\n            .swipeToNextQuestion(\"What is your age?\")\n            .assertNavigationButtonsAreHidden()\n\n            // change settings to 'Forward/backward buttons' mode'\n            .clickOptionsIcon()\n            .clickProjectSettings()\n            .clickOnUserInterface()\n            .clickNavigation()\n            .clickUseNavigationButtons()\n            .pressBack(ProjectSettingsPage())\n            .pressBack(FormEntryPage(\"Two Question\"))\n\n            // assert that 'Forward/backward buttons' mode is enabled\n            .swipeToPreviousQuestion(\"What is your age?\")\n            .clickBackwardButton()\n            .assertText(\"What is your name?\")\n\n            // change settings to 'Swipes and buttons' mode'\n            .clickOptionsIcon()\n            .clickProjectSettings()\n            .clickOnUserInterface()\n            .clickNavigation()\n            .clickUseSwipesAndButtons()\n            .pressBack(ProjectSettingsPage())\n            .pressBack(FormEntryPage(\"Two Question\"))\n\n            // assert that 'Swipes and buttons' mode is enabled\n            .swipeToNextQuestion(\"What is your age?\")\n            .clickBackwardButton()\n            .assertText(\"What is your name?\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormSaveTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.PageComposeRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\nclass FormSaveTest {\n    private val rule = CollectTestRule()\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val copyFormChain: RuleChain = TestRuleChain\n        .chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun whenBlankFormSavedAsDraft_displaySnackbarWithEditAction() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .answerQuestion(0, \"25\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n            .assertText(org.odk.collect.strings.R.string.form_saved_as_draft)\n            .editSavedForm(\"One Question\")\n            .assertAnswer(\"25\")\n            .assertText(org.odk.collect.strings.R.string.jump_to_beginning)\n            .assertText(org.odk.collect.strings.R.string.jump_to_end)\n    }\n\n    @Test\n    fun whenDraftFinalized_displaySnackbarWithViewAction() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .answerQuestion(0, \"25\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n            .clickDrafts()\n            .clickOnForm(\"One Question\")\n            .clickGoToEnd()\n            .clickFinalize()\n            .assertText(org.odk.collect.strings.R.string.form_saved)\n            .viewSavedForm(\"One Question\")\n            .assertAnswer(\"25\")\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.jump_to_beginning)\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.jump_to_end)\n            .assertText(org.odk.collect.strings.R.string.exit)\n    }\n\n    @Test\n    fun snackbarCanBeDismissed_andWillNotBeDisplayedAgainAfterRecreatingTheActivity() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n            .assertText(org.odk.collect.strings.R.string.form_saved_as_draft)\n            .closeSnackbar()\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_saved_as_draft)\n            .rotateToLandscape(MainMenuPage())\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.form_saved_as_draft)\n    }\n\n    @Test\n    fun whenFormDeletedDuringFilling_displayErrorOnAttemptToSave() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n            .startBlankForm(\"One Question\")\n            .also { testDependencies.scheduler.runDeferredTasks() }\n            .clickSaveWithError(\"Sorry, form save failed! Form can't be found.\")\n            .swipeToEndScreen()\n            .clickSaveAsDraftWithError(\"Sorry, form save failed! Form can't be found.\")\n            .clickFinalizeWithError(\"Sorry, form save failed! Form can't be found.\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/FormStylingTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\nprivate const val FORM_FILE_NAME = \"form_styling.xml\"\nprivate const val FORM_NAME = \"Form styling\"\n\nclass FormStylingTest {\n    var rule = CollectTestRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = TestRuleChain.chain()\n        .around(rule)\n\n    @Test\n    fun questionLabelTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .assertText(\"Note text\")\n    }\n\n    @Test\n    fun requiredQuestionLabelWithHeaderStyleTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .swipeToNextQuestion(\"Required text question with header style\", true)\n    }\n\n    @Test\n    fun questionHintTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .assertText(\"Hint text\")\n    }\n\n    @Test\n    fun questionGuidanceTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .openFormManagement()\n            .openShowGuidanceForQuestions()\n            .clickOnString(org.odk.collect.strings.R.string.guidance_yes)\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .startBlankForm(FORM_NAME)\n            .assertText(\"Guidance text\")\n            .clickOptionsIcon()\n            .clickProjectSettings()\n            .openFormManagement()\n            .openShowGuidanceForQuestions()\n            .clickOnString(org.odk.collect.strings.R.string.guidance_yes_collapsed)\n            .pressBack(ProjectSettingsPage())\n            .pressBack(FormEntryPage(FORM_NAME))\n            .clickOnText(\"Hint text\")\n            .assertText(\"Guidance text\")\n    }\n\n    @Test\n    fun selectOneWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectOneQuestions\")\n            .clickOnQuestion(\"Select one widget\")\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n\n    @Test\n    fun selectOneMinimalWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectOneQuestions\")\n            .clickOnQuestion(\"Select one minimal widget\")\n            .assertText(\"One\")\n            .openSelectMinimalDialog()\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n\n    @Test\n    fun selectOneImageMapWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectOneQuestions\")\n            .clickOnQuestion(\"Select one image-map widget\")\n            .assertText(\"Selected: One\")\n    }\n\n    @Test\n    fun selectOneLabelWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectOneQuestions\")\n            .clickOnQuestion(\"Select one label widget\")\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n\n    @Test\n    fun selectOneListWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectOneQuestions\")\n            .clickOnQuestion(\"Select one list widget\")\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n\n    @Test\n    fun selectOneLikertWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectOneQuestions\")\n            .clickOnQuestion(\"Select one likert widget\")\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n\n    @Test\n    fun selectMultipleWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectMultipleQuestions\")\n            .clickOnQuestion(\"Select multiple widget\")\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n\n    @Test\n    fun selectMultipleMinimalWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectMultipleQuestions\")\n            .clickOnQuestion(\"Select multiple minimal widget\")\n            .assertText(\"One\")\n            .openSelectMinimalDialog()\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n\n    @Test\n    fun selectMultipleImageMapWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectMultipleQuestions\")\n            .clickOnQuestion(\"Select multiple image-map widget\")\n            .assertText(\"Selected: One\")\n    }\n\n    @Test\n    fun selectMultipleLabelWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectMultipleQuestions\")\n            .clickOnQuestion(\"Select multiple label widget\")\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n\n    @Test\n    fun selectMultipleListWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectMultipleQuestions\")\n            .clickOnQuestion(\"Select multiple list widget\")\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n\n    @Test\n    fun rankWidgetTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnQuestion(\"Rank widget\")\n            .assertText(\"1. One\\n2. Two\\n3. Three\")\n            .clickOnText(\"Rank items\")\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n\n    @Test\n    fun groupNameTest() {\n        rule.startAtMainMenu()\n            .copyForm(FORM_FILE_NAME)\n            .startBlankForm(FORM_NAME)\n            .clickGoToArrow()\n            .clickOnGroup(\"selectOneQuestions\")\n            .assertPath(\"selectOneQuestions\")\n            .clickOnQuestion(\"Select one widget\")\n            .assertText(\"selectOneQuestions\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/GuidanceTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\n\n@RunWith(AndroidJUnit4::class)\nclass GuidanceTest {\n\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val ruleChain: RuleChain = chain(testDependencies).around(rule)\n\n    @Test\n    fun guidanceForQuestion_ShouldBeHiddenIfNotSelectedInSettings() {\n        rule.withProject(testDependencies.server, \"hints_textq.xml\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .openFormManagement()\n            .openShowGuidanceForQuestions()\n            .clickOnString(org.odk.collect.strings.R.string.guidance_no)\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            .startBlankForm(\"hints textq\")\n            .assertText(\"Hint 1\")\n            .checkIfElementIsGone(R.id.help_icon)\n            .assertTextDoesNotExist(\"1 very very very very very very very very very very long text\")\n    }\n\n    @Test\n    fun guidanceForQuestion_ShouldBeFullyDisplayedIfAlwaysShownEnabledInSettings() {\n        rule.withProject(testDependencies.server, \"hints_textq.xml\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .openFormManagement()\n            .openShowGuidanceForQuestions()\n            .clickOnString(org.odk.collect.strings.R.string.guidance_yes)\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            .startBlankForm(\"hints textq\")\n            .assertText(\"Hint 1\")\n            .checkIfElementIsGone(R.id.help_icon)\n            .assertText(\"1 very very very very very very very very very very long text\")\n    }\n\n    @Test\n    fun guidanceForQuestion_ShouldBeCollapsedByDefault() {\n        rule.withProject(testDependencies.server, \"hints_textq.xml\")\n            .startBlankForm(\"hints textq\")\n            .assertText(\"Hint 1\")\n            .checkIsIdDisplayed(R.id.help_icon)\n            .assertTextDoesNotExist(\"1 very very very very very very very very very very long text\")\n            .clickOnText(\"Hint 1\")\n            .assertText(\"1 very very very very very very very very very very long text\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/ImageLoadingTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.Canvas\nimport com.caverock.androidsvg.SVG\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.R\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.android.utilities.FileUtils.getResourceAsStream\n\nclass ImageLoadingTest {\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain().around(rule)\n\n    @Test\n    fun updatedImagesAreLoadedCorrectly() {\n        rule.startAtMainMenu()\n            .copyForm(\"form_with_images.xml\", listOf(\"rects_v1/rect.jpg\", \"rects_v1/rect.svg\"))\n            .startBlankForm(\"form_with_images\")\n            .assertImageViewShowsImage(\n                R.id.imageView,\n                BitmapFactory.decodeStream(getResourceAsStream(\"media/rects_v1/rect.jpg\"))\n            )\n            .swipeToNextQuestion(\"SVG\")\n            .assertImageViewShowsImage(\n                R.id.imageView,\n                getBitmapFromSvg(\"media/rects_v1/rect.svg\")\n            )\n            .pressBackAndDiscardForm()\n            .copyForm(\"form_with_images.xml\", listOf(\"rects_v2/rect.jpg\", \"rects_v2/rect.svg\"))\n            .startBlankForm(\"form_with_images\")\n            .assertImageViewShowsImage(\n                R.id.imageView,\n                BitmapFactory.decodeStream(getResourceAsStream(\"media/rects_v2/rect.jpg\"))\n            )\n            .swipeToNextQuestion(\"SVG\")\n            .assertImageViewShowsImage(\n                R.id.imageView,\n                getBitmapFromSvg(\"media/rects_v2/rect.svg\")\n            )\n    }\n\n    private fun getBitmapFromSvg(filePath: String): Bitmap {\n        val svg = SVG.getFromInputStream(getResourceAsStream(filePath))\n\n        val bitmap = Bitmap.createBitmap(\n            svg.documentWidth.toInt(),\n            svg.documentHeight.toInt(),\n            Bitmap.Config.ARGB_8888\n        )\n\n        svg.renderToCanvas(Canvas(bitmap))\n        return bitmap\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.kt",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.feature.formentry\n\nimport android.app.Activity\nimport android.app.Application\nimport android.app.Instrumentation\nimport android.content.ClipData\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.os.Environment\nimport androidx.compose.ui.test.junit4.createEmptyComposeRule\nimport androidx.core.content.FileProvider\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.scrollTo\nimport androidx.test.espresso.assertion.ViewAssertions.doesNotExist\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.intent.Intents.intending\nimport androidx.test.espresso.intent.matcher.IntentMatchers.isInternal\nimport androidx.test.espresso.matcher.ViewMatchers\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withClassName\nimport androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withTagValue\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport com.google.android.material.textfield.TextInputEditText\nimport org.hamcrest.CoreMatchers\nimport org.hamcrest.Matchers\nimport org.hamcrest.core.StringEndsWith\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.mockito.Mockito\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.support.matchers.CustomMatchers.withIndex\nimport org.odk.collect.android.support.rules.BlankFormTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.android.widgets.DecimalWidget\nimport org.odk.collect.android.widgets.IntegerWidget\nimport org.odk.collect.android.widgets.StringWidget\nimport org.odk.collect.androidtest.RecordedIntentsRule\nimport org.odk.collect.androidtest.onNodeWithClickLabel\nimport org.odk.collect.strings.R\nimport java.io.File\nimport java.io.IOException\n\n/**\n * Tests that intent groups work as described in https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields\n */\nclass IntentGroupTest {\n    private val composeRule = createEmptyComposeRule()\n    private val rule = BlankFormTestRule(INTENT_GROUP_FORM, \"intent-group\")\n\n    @JvmField\n    @Rule\n    var copyFormChain: RuleChain = chain()\n        .around(RecordedIntentsRule())\n        .around(composeRule)\n        .around(rule)\n\n    // Verifies that a value given to the label text with form buttonText is used as the button text.\n    @Test\n    fun buttonName_ShouldComeFromSpecialFormText() {\n        onView(withText(R.string.launch_app))\n            .check(doesNotExist())\n        onView(withText(\"This is buttonText\"))\n            .check(matches(isDisplayed()))\n    }\n\n    // Verifies that a value given to the label text with form noAppErrorString is used as the toast\n    // text if no app is found.\n    @Test\n    fun appMissingErrorText_ShouldComeFromSpecialFormText() {\n        rule.startInFormEntry()\n            .clickOnText(\"This is buttonText\")\n            .checkIsToastWithMessageDisplayed(\"This is noAppErrorString\")\n    }\n\n    @Test\n    @Throws(IOException::class)\n    fun externalApp_ShouldPopulateFields() {\n        // None of the integer/decimal/text questions have associated fields or text\n        onView(withId(org.odk.collect.android.R.id.edit_text))\n            .check(doesNotExist())\n        onView(withId(org.odk.collect.android.R.id.text_view))\n            .check(doesNotExist())\n\n        assertImageWidgetWithoutAnswer()\n        assertAudioWidgetWithoutAnswer()\n        assertVideoWidgetWithoutAnswer()\n        assertFileWidgetWithoutAnswer()\n\n        val resultIntent = Intent()\n\n        val imageUri = createTempFile(\"famous\", \"jpg\")\n        val audioUri = createTempFile(\"sampleAudio\", \"wav\")\n        val videoUri = createTempFile(\"sampleVideo\", \"mp4\")\n        val fileUri = createTempFile(\"fruits\", \"csv\")\n\n        resultIntent.putExtra(\"questionInteger\", \"25\")\n        resultIntent.putExtra(\"questionDecimal\", \"46.74\")\n        resultIntent.putExtra(\"questionText\", \"sampleAnswer\")\n        resultIntent.putExtra(\"questionImage\", imageUri)\n        resultIntent.putExtra(\"questionAudio\", audioUri)\n        resultIntent.putExtra(\"questionVideo\", videoUri)\n        resultIntent.putExtra(\"questionFile\", fileUri)\n\n        val clipData = ClipData.newRawUri(null, null)\n        clipData.addItem(ClipData.Item(\"questionImage\", null, imageUri))\n        clipData.addItem(ClipData.Item(\"questionAudio\", null, audioUri))\n        clipData.addItem(ClipData.Item(\"questionVideo\", null, videoUri))\n        clipData.addItem(ClipData.Item(\"questionFile\", null, fileUri))\n\n        resultIntent.clipData = clipData\n        resultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        intending(CoreMatchers.not(isInternal())).respondWith(\n            Instrumentation.ActivityResult(\n                Activity.RESULT_OK, resultIntent\n            )\n        )\n        onView(withText(\"This is buttonText\"))\n            .perform(scrollTo(), click())\n\n        assertIntegerWidgetWithAnswer()\n        assertDecimalWidgetWithAnswer()\n        assertStringWidgetWithAnswer()\n\n        assertImageWidgetWithAnswer()\n        assertAudioWidgetWithAnswer()\n        assertVideoWidgetWithAnswer()\n        assertFileWidgetWithAnswer()\n    }\n\n    @Test\n    fun externalApp_ShouldNotPopulateFieldsIfAnswersAreNull() {\n        assertImageWidgetWithoutAnswer()\n        assertAudioWidgetWithoutAnswer()\n        assertVideoWidgetWithoutAnswer()\n        assertFileWidgetWithoutAnswer()\n\n        val resultIntent = Intent()\n\n        resultIntent.putExtra(\"questionInteger\", null as Bundle?)\n        resultIntent.putExtra(\"questionDecimal\", null as Bundle?)\n        resultIntent.putExtra(\"questionText\", null as Bundle?)\n        resultIntent.putExtra(\"questionImage\", null as Bundle?)\n        resultIntent.putExtra(\"questionAudio\", null as Bundle?)\n        resultIntent.putExtra(\"questionVideo\", null as Bundle?)\n        resultIntent.putExtra(\"questionFile\", null as Bundle?)\n\n        val clipData = ClipData.newRawUri(null, null)\n        clipData.addItem(ClipData.Item(\"questionImage\", null, null))\n        clipData.addItem(ClipData.Item(\"questionAudio\", null, null))\n        clipData.addItem(ClipData.Item(\"questionVideo\", null, null))\n        clipData.addItem(ClipData.Item(\"questionFile\", null, null))\n\n        resultIntent.clipData = clipData\n        resultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        intending(CoreMatchers.not(isInternal())).respondWith(\n            Instrumentation.ActivityResult(\n                Activity.RESULT_OK, resultIntent\n            )\n        )\n        onView(withText(\"This is buttonText\"))\n            .perform(scrollTo(), click())\n\n        onView(withIndex(withClassName(StringEndsWith.endsWith(\"EditText\")), 0))\n            .check(matches(withText(\"\")))\n        onView(withIndex(withClassName(StringEndsWith.endsWith(\"EditText\")), 1))\n            .check(matches(withText(\"\")))\n        onView(withIndex(withClassName(StringEndsWith.endsWith(\"EditText\")), 2))\n            .check(matches(withText(\"\")))\n\n        assertImageWidgetWithoutAnswer()\n        assertAudioWidgetWithoutAnswer()\n        assertVideoWidgetWithoutAnswer()\n        assertFileWidgetWithoutAnswer()\n    }\n\n    @Test\n    fun collect_shouldNotCrashWhenAnyExceptionIsThrownWhileReceivingAnswer() {\n        assertImageWidgetWithoutAnswer()\n\n        val resultIntent = Intent()\n\n        val uri = Mockito.mock(Uri::class.java)\n        Mockito.`when`(uri.scheme).thenThrow(RuntimeException())\n\n        resultIntent.putExtra(\"questionImage\", uri)\n\n        intending(CoreMatchers.not(isInternal())).respondWith(\n            Instrumentation.ActivityResult(\n                Activity.RESULT_OK, resultIntent\n            )\n        )\n\n        onView(withText(\"This is buttonText\")).perform(click())\n\n        assertImageWidgetWithoutAnswer()\n    }\n\n    @Test\n    fun collect_shouldNotCrashWhenAnyErrorIsThrownWhileReceivingAnswer() {\n        assertImageWidgetWithoutAnswer()\n\n        val resultIntent = Intent()\n\n        val uri = Mockito.mock(Uri::class.java)\n        Mockito.`when`(uri.scheme).thenThrow(Error())\n\n        resultIntent.putExtra(\"questionImage\", uri)\n\n        intending(CoreMatchers.not(isInternal())).respondWith(\n            Instrumentation.ActivityResult(\n                Activity.RESULT_OK, resultIntent\n            )\n        )\n\n        onView(withText(\"This is buttonText\")).perform(click())\n\n        assertImageWidgetWithoutAnswer()\n    }\n\n    private fun assertIntegerWidgetWithAnswer() {\n        onView(withText(\"Integer external\")).perform(scrollTo())\n            .check(matches(isDisplayed()))\n        onView(CoreMatchers.allOf(\n            isDescendantOfA(isAssignableFrom(IntegerWidget::class.java)),\n            isAssignableFrom(TextInputEditText::class.java)))\n            .check(matches(withText(\"25\")))\n    }\n\n    private fun assertDecimalWidgetWithAnswer() {\n        onView(withText(\"Decimal external\")).perform(scrollTo())\n            .check(matches(isDisplayed()))\n        onView(CoreMatchers.allOf(\n            isDescendantOfA(isAssignableFrom(DecimalWidget::class.java)),\n            isAssignableFrom(TextInputEditText::class.java)))\n            .check(matches(withText(\"46.74\")))\n    }\n\n    private fun assertStringWidgetWithAnswer() {\n        onView(withText(\"Text external\")).perform(scrollTo())\n            .check(matches(isDisplayed()))\n        onView(\n            CoreMatchers.allOf(\n                isDescendantOfA(CoreMatchers.allOf(\n                    isAssignableFrom(StringWidget::class.java),\n                    CoreMatchers.not(isAssignableFrom(IntegerWidget::class.java)),\n                    CoreMatchers.not(isAssignableFrom(DecimalWidget::class.java)))),\n                isAssignableFrom(TextInputEditText::class.java))\n        ).check(matches(withText(\"sampleAnswer\")))\n    }\n\n    private fun assertImageWidgetWithoutAnswer() {\n        onView(CoreMatchers.allOf(\n            withTagValue(Matchers.`is`(\"ImageView\")),\n            withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))\n            .check(doesNotExist())\n\n        onView(withId(org.odk.collect.android.R.id.capture_button))\n            .check(matches(CoreMatchers.not(isDisplayed())))\n\n        onView(withId(org.odk.collect.android.R.id.choose_button))\n            .check(matches(CoreMatchers.not(isDisplayed())))\n    }\n\n    private fun assertAudioWidgetWithoutAnswer() {\n        onView(withId(org.odk.collect.android.R.id.audio_controller))\n            .check(matches(CoreMatchers.not(isDisplayed())))\n    }\n\n    private fun assertVideoWidgetWithoutAnswer() {\n        composeRule\n            .onNodeWithClickLabel(ApplicationProvider.getApplicationContext<Application>().getString(R.string.play_video))\n            .assertDoesNotExist()\n    }\n\n    private fun assertFileWidgetWithoutAnswer() {\n        composeRule\n            .onNodeWithClickLabel(R.string.open_file)\n            .assertDoesNotExist()\n    }\n\n    private fun assertImageWidgetWithAnswer() {\n        onView(CoreMatchers.allOf(\n            withTagValue(Matchers.`is`(\"ImageView\")),\n            withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))\n            .check(matches(CoreMatchers.not(doesNotExist())))\n\n        onView(withId(org.odk.collect.android.R.id.capture_button))\n            .check(matches(CoreMatchers.not(isDisplayed())))\n\n        onView(withId(org.odk.collect.android.R.id.choose_button))\n            .check(matches(CoreMatchers.not(isDisplayed())))\n    }\n\n    private fun assertAudioWidgetWithAnswer() {\n        onView(withId(org.odk.collect.android.R.id.audio_controller))\n            .perform(scrollTo())\n            .check(matches(isDisplayed()))\n    }\n\n    private fun assertVideoWidgetWithAnswer() {\n        composeRule\n            .onNodeWithClickLabel(R.string.play_video)\n            .assertExists()\n    }\n\n    private fun assertFileWidgetWithAnswer() {\n        composeRule\n            .onNodeWithClickLabel(ApplicationProvider.getApplicationContext<Application>().getString(R.string.open_file))\n            .assertExists()\n    }\n\n    @Throws(IOException::class)\n    private fun createTempFile(name: String, extension: String): Uri {\n        // Use the phones downloads dir for temp files\n        val downloadsDir = ApplicationProvider\n            .getApplicationContext<Context>()\n            .getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)\n\n        val file = File.createTempFile(name, extension, downloadsDir)\n        FileUtils.copyFileFromResources(\n            \"media\" + File.separator + name + \".\" + extension,\n            file.path\n        )\n        return getUriForFile(file)\n    }\n\n    private fun getUriForFile(file: File): Uri {\n        return FileProvider.getUriForFile(\n            Collect.getInstance(),\n            BuildConfig.APPLICATION_ID + \".provider\",\n            file\n        )\n    }\n\n    companion object {\n        private const val INTENT_GROUP_FORM = \"intent-group.xml\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/InvalidFormTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.strings.R\n\n@RunWith(AndroidJUnit4::class)\nclass InvalidFormTest {\n    private var rule = CollectTestRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = chain().around(rule)\n\n    @Test\n    fun brokenForm_isVisibleOnFormList_butThrowsAnErrorAfterOpeningIt() {\n        rule.startAtMainMenu()\n            .copyForm(\"invalid-form.xml\")\n            .startBlankFormWithError(\"invalid-form\", true)\n            .assertTextInDialog(\"An unknown error has occurred. Please ask your project leadership to email support@getodk.org with information about this form.\\n\" +\n                \"\\n\" +\n                \"Cycle detected in form's relevant and calculation logic!\\n\" +\n                \"The following nodes are likely involved in the loop:\\n\" +\n                \"/asasas/q2\")\n            .clickOKOnDialog(MainMenuPage())\n    }\n\n    @Test\n    fun app_ShouldNotCrash_whenFillingFormsWithEmptyGroupFieldList() {\n        MainMenuPage()\n            .copyForm(\"emptyGroupFieldList.xml\")\n            .clickFillBlankForm()\n            .clickOnEmptyForm(\"emptyGroupFieldList\")\n            .clickFinalize()\n            .checkIsSnackbarWithMessageDisplayed(R.string.form_saved)\n    }\n\n    @Test\n    fun app_ShouldNotCrash_whenFillingFormsWithRepeatInFieldList() {\n        rule.startAtMainMenu()\n            .copyForm(\"repeat_in_field_list.xml\")\n            .startBlankFormWithError(\"repeat_in_field_list\", false)\n            .clickOK(FormEntryPage(\"repeat_in_field_list\"))\n            .swipeToEndScreen()\n            .clickFinalize()\n            .checkIsSnackbarWithMessageDisplayed(R.string.form_saved)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/LikertTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.isChecked;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.isNotChecked;\nimport static androidx.test.espresso.matcher.ViewMatchers.withClassName;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\nimport static org.hamcrest.Matchers.endsWith;\nimport static org.hamcrest.Matchers.startsWith;\nimport static org.odk.collect.android.support.matchers.CustomMatchers.withIndex;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.support.rules.BlankFormTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\nimport java.util.Collections;\n\npublic class LikertTest {\n    private static final String LIKERT_TEST_FORM = \"likert_test.xml\";\n\n    public BlankFormTestRule activityTestRule = new BlankFormTestRule(LIKERT_TEST_FORM, \"All widgets likert icon\", Collections.singletonList(\"famous.jpg\"));\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(activityTestRule);\n\n    @Test\n    public void allText_canClick() {\n        openWidgetList();\n        onView(withText(\"Likert Widget\")).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).check(matches(isChecked()));\n    }\n\n    @Test\n    public void allImages_canClick() {\n        openWidgetList();\n        onView(withText(\"Likert Image Widget\")).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).check(matches(isChecked()));\n    }\n\n    @Test\n    public void insufficientText_canClick() {\n        openWidgetList();\n        onView(withText(\"Likert Widget Error\")).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).check(matches(isChecked()));\n    }\n\n    @Test\n    public void insufficientImages_canClick() {\n        openWidgetList();\n        onView(withText(\"Likert Image Widget Error\")).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).check(matches(isChecked()));\n    }\n\n    @Test\n    public void missingImage_canClick() {\n        openWidgetList();\n        onView(withText(\"Likert Image Widget Error2\")).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).check(matches(isChecked()));\n    }\n\n    @Test\n    public void missingText_canClick() {\n        openWidgetList();\n        onView(withText(\"Likert Missing text Error\")).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).check(matches(isChecked()));\n    }\n\n    @Test\n    public void onlyOneRemainsClicked() {\n        openWidgetList();\n        onView(withText(\"Likert Image Widget\")).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).check(matches(isChecked()));\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 2)).perform(click());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 2)).check(matches(isChecked()));\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).check(matches(isNotChecked()));\n    }\n\n    @Test\n    public void testImagesLoad() {\n        openWidgetList();\n        onView(withText(\"Likert Image Widget\")).perform(click());\n\n        for (int i = 0; i < 5; i++) {\n            onView(withIndex(withClassName(endsWith(\"RadioButton\")), i)).check(matches(isDisplayed()));\n        }\n    }\n\n    @Test\n    public void updateTest_SelectionChangeAtOneCascadeLevelWithLikert_ShouldUpdateNextLevels() {\n        openWidgetList();\n        onView(withText(\"Cascading likert\")).perform(click());\n\n        // No choices should be shown for levels 2 and 3 when no selection is made for level 1\n        onView(withText(startsWith(\"Level1\"))).perform(click());\n        onView(withText(\"A1\")).check(doesNotExist());\n        onView(withText(\"B1\")).check(doesNotExist());\n        onView(withText(\"C1\")).check(doesNotExist());\n        onView(withText(\"A1A\")).check(doesNotExist());\n\n        // Selecting C for level 1 should only reveal options for C at level 2\n        // and selecting C3 for level 2 shouldn't reveal options in level 3\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 2)).perform(click());\n        onView(withText(\"C1\")).check(matches(isDisplayed()));\n        onView(withText(\"C4\")).check(matches(isDisplayed()));\n        onView(withText(\"A1\")).check(doesNotExist());\n        onView(withText(\"B1\")).check(doesNotExist());\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 5)).perform(click());\n        onView(withText(\"A1A\")).check(doesNotExist());\n\n        // Selecting A for level 1 should reveal options for A at level 2\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 0)).perform(click());\n        onView(withText(\"A1\")).check(matches(isDisplayed()));\n        onView(withText(\"A1A\")).check(doesNotExist());\n        onView(withText(\"B1\")).check(doesNotExist());\n        onView(withText(\"C1\")).check(doesNotExist());\n\n        // Selecting A1 for level 2 should reveal options for A1 at level 3\n        onView(withIndex(withClassName(endsWith(\"RadioButton\")), 3)).perform(click());\n        onView(withText(\"A1A\")).check(matches(isDisplayed()));\n        onView(withText(\"B1A\")).check(doesNotExist());\n        onView(withText(\"B1\")).check(doesNotExist());\n        onView(withText(\"C1\")).check(doesNotExist());\n    }\n\n    private void openWidgetList() {\n        onView(withId(R.id.menu_goto)).perform(click());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/NestedIntentGroupTest.kt",
    "content": "/*\n * Copyright 2025 ODK\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.feature.formentry\n\nimport android.app.Activity\nimport android.app.Instrumentation\nimport android.content.Intent\nimport androidx.test.espresso.intent.Intents.intending\nimport androidx.test.espresso.intent.matcher.IntentMatchers.isInternal\nimport org.hamcrest.CoreMatchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.rules.BlankFormTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.androidtest.RecordedIntentsRule\nimport org.odk.collect.strings.R\n\n/**\n * Tests extension to https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields\n * for groups nested in field-lists.\n */\nclass NestedIntentGroupTest {\n    var rule: BlankFormTestRule = BlankFormTestRule(INTENT_GROUP_FORM, \"nested-intent-group\")\n\n    @JvmField\n    @Rule\n    var copyFormChain: RuleChain = chain()\n        .around(RecordedIntentsRule())\n        .around(rule)\n\n    @Test\n    fun appLaunchButton_isShownAtTopOfIntentGroup() {\n        rule.startInFormEntry()\n            .assertTextBelow(R.string.launch_app, \"Skip?\")\n            .assertTextBelow(\"* NFIQ Score\", R.string.launch_app)\n    }\n\n    @Test\n    fun launchingApp_populatesValuesInIntentGroup() {\n        val resultIntent = Intent()\n        resultIntent.putExtra(\"right_thumb_Registration_NFIQ\", \"2\")\n        resultIntent.putExtra(\"right_thumb_Registration_template\", \"foobar\")\n\n        intending(not(isInternal())).respondWith(\n            Instrumentation.ActivityResult(\n                Activity.RESULT_OK, resultIntent\n            )\n        )\n\n        rule.startInFormEntry()\n            .clickOnString(R.string.launch_app)\n            .assertAnswer(\"* NFIQ Score\", \"2\", true)\n            .assertAnswer(\"* Template\", \"foobar\", true)\n    }\n\n    @Test\n    fun onlyTheFirstGroupWithIntentAttr_isTreatedAsIntentGroup() {\n        rule.startInFormEntry()\n            .clickOnString(R.string.launch_app)\n            .answerQuestion(\"Some text\", \"blah\")\n    }\n\n    @Test\n    fun relevance_showsAndHidesTheIntentGroup() {\n        rule.startInFormEntry()\n            .clickOnText(\"Yes\")\n            .assertTextDoesNotExist(R.string.launch_app)\n            .assertTextsDoNotExist(\"* NFIQ Score\")\n            .assertTextsDoNotExist(\"* Template\")\n\n            .clickOnText(\"No\")\n            .assertTextBelow(R.string.launch_app, \"Skip?\")\n            .assertTextBelow(\"* NFIQ Score\", R.string.launch_app)\n            .assertQuestion(\"Template\", true)\n    }\n\n    @Test\n    fun valuesFromApp_triggerRecalculation() {\n        val resultIntent = Intent()\n        resultIntent.putExtra(\"right_thumb_Registration_NFIQ\", \"2\")\n        resultIntent.putExtra(\"right_thumb_Registration_template\", \"foobar\")\n\n        intending(not(isInternal())).respondWith(\n            Instrumentation.ActivityResult(\n                Activity.RESULT_OK, resultIntent\n            )\n        )\n\n        rule.startInFormEntry()\n            .clickOnString(R.string.launch_app)\n            .assertQuestion(\"Length of template: 6\")\n    }\n\n    companion object {\n        private const val INTENT_GROUP_FORM = \"nested-intent-group.xml\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/QuickSaveTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.pages.FormEntryPage.QuestionAndAnswer;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\n@RunWith(AndroidJUnit4.class)\npublic class QuickSaveTest {\n\n    public final CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public final RuleChain chain = TestRuleChain.chain()\n            .around(rule);\n\n    @Test\n    public void whenFillingForm_clickingSaveIcon_savesCurrentAnswers() {\n        rule.startAtMainMenu()\n                .copyForm(\"two-question.xml\")\n                .startBlankForm(\"Two Question\")\n                .fillOut(\n                        new QuestionAndAnswer(\"What is your name?\", \"Reuben\"),\n                        new QuestionAndAnswer(\"What is your age?\", \"32\")\n                )\n                .clickSave()\n                .pressBackAndDiscardChanges()\n\n                .clickDrafts(1)\n                .clickOnForm(\"Two Question\")\n                .assertAnswer(\"Reuben\")\n                .assertAnswer(\"32\");\n    }\n\n    @Test\n    public void whenFillingForm_withViolatedConstraintsOnCurrentScreen_clickingSaveIcon_savesCurrentAnswers() {\n        rule.startAtMainMenu()\n                .copyForm(\"two-question-required.xml\")\n                .startBlankForm(\"Two Question Required\")\n                .answerQuestion(\"What is your name?\", \"Reuben\")\n                .swipeToNextQuestion(\"What is your age?\", true)\n                .clickSave()\n                .pressBackAndDiscardChanges()\n\n                .clickDrafts(1)\n                .clickOnForm(\"Two Question Required\")\n                .assertAnswer(\"Reuben\");\n    }\n\n    @Test\n    public void whenEditingANonFinalizedForm_withViolatedConstraintsOnCurrentScreen_clickingSaveIcon_savesCurrentAnswers() {\n        rule.startAtMainMenu()\n                .copyForm(\"two-question-required.xml\")\n                .startBlankForm(\"Two Question Required\")\n                .answerQuestion(\"What is your name?\", \"Reuben\")\n                .clickSave()\n                .pressBackAndDiscardChanges()\n\n                .clickDrafts(1)\n                .clickOnForm(\"Two Question Required\")\n                .clickGoToStart()\n                .answerQuestion(\"What is your name?\", \"Another Reuben\")\n                .swipeToNextQuestion(\"What is your age?\", true)\n                .clickSave()\n                .pressBackAndDiscardChanges()\n\n                .clickDrafts(1)\n                .clickOnForm(\"Two Question Required\")\n                .assertAnswer(\"Another Reuben\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/QuittingFormTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.pages.FormEntryPage.QuestionAndAnswer;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.pages.SaveOrDiscardFormDialog;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\n@RunWith(AndroidJUnit4.class)\npublic class QuittingFormTest {\n\n    public CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(rule);\n\n    @Test\n    public void whenFillingForm_pressingBack_andClickingSaveChanges_savesCurrentAnswers() {\n        rule.startAtMainMenu()\n                .copyForm(\"two-question.xml\")\n                .startBlankForm(\"Two Question\")\n                .fillOut(\n                        new QuestionAndAnswer(\"What is your name?\", \"Reuben\"),\n                        new QuestionAndAnswer(\"What is your age?\", \"10\")\n                )\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickSaveChanges()\n\n                .assertNumberOfFinalizedForms(0)\n                .clickDrafts(1)\n                .clickOnForm(\"Two Question\")\n                .assertAnswer(\"Reuben\")\n                .assertAnswer(\"10\");\n    }\n\n    @Test\n    public void whenFillingForm_pressingBack_andClickingIgnoreChanges_doesNotSaveForm() {\n        rule.startAtMainMenu()\n                .copyForm(\"two-question.xml\")\n                .startBlankForm(\"Two Question\")\n                .answerQuestion(\"What is your name?\", \"Reuben\")\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickDiscardForm()\n\n                .assertNumberOfEditableForms(0);\n    }\n\n    @Test\n    public void whenFillingForm_saving_andPressingBack_andClickingIgnoreChanges_savesAnswersBeforeSave() {\n        rule.startAtMainMenu()\n                .copyForm(\"two-question.xml\")\n                .startBlankForm(\"Two Question\")\n                .answerQuestion(\"What is your name?\", \"Reuben\")\n                .clickSave()\n                .swipeToNextQuestion(\"What is your age?\")\n                .answerQuestion(\"What is your age?\", \"10\")\n                .pressBackAndDiscardChanges()\n\n                .clickDrafts(1)\n                .clickOnForm(\"Two Question\")\n                .assertAnswer(\"Reuben\")\n                .assertAnswerDoesNotExist(\"10\");\n    }\n\n    @Test\n    public void whenFillingForm_withViolatedConstraintsOnCurrentScreen_pressingBack_andClickingSaveChanges_savesCurrentAnswers() {\n        rule.startAtMainMenu()\n                .copyForm(\"two-question-required.xml\")\n                .startBlankForm(\"Two Question Required\")\n                .answerQuestion(\"What is your name?\", \"Reuben\")\n                .swipeToNextQuestion(\"What is your age?\", true)\n                .closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickSaveChanges()\n\n                .clickDrafts(1)\n                .clickOnForm(\"Two Question Required\")\n                .assertAnswer(\"Reuben\");\n    }\n\n    @Test\n    public void whenEditingANonFinalizedForm_withViolatedConstraintsOnCurrentScreen_pressingBack_andClickingSaveChanges_savesCurrentAnswers() {\n        rule.startAtMainMenu()\n                .copyForm(\"two-question-required.xml\")\n                .startBlankForm(\"Two Question Required\")\n                .answerQuestion(\"What is your name?\", \"Reuben\")\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickSaveChanges()\n\n                .clickDrafts(1)\n                .clickOnForm(\"Two Question Required\")\n                .clickGoToStart()\n                .answerQuestion(\"What is your name?\", \"Another Reuben\")\n                .swipeToNextQuestion(\"What is your age?\", true)\n                .closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickSaveChanges()\n\n                .clickDrafts(1)\n                .clickOnForm(\"Two Question Required\")\n                .assertAnswer(\"Another Reuben\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/RankingWidgetWithCSVTest.java",
    "content": "package org.odk.collect.android.feature.formentry;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.odk.collect.android.support.rules.BlankFormTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.android.support.pages.FormEntryPage;\n\nimport java.util.Collections;\n\npublic class RankingWidgetWithCSVTest {\n\n    private static final String TEST_FORM = \"ranking_widget.xml\";\n\n    public BlankFormTestRule activityTestRule = new BlankFormTestRule(TEST_FORM, \"ranking_widget\", Collections.singletonList(\"fruits.csv\"));\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(activityTestRule);\n\n\n    @Test\n    public void rankingWidget_shouldDisplayItemsFromSearchFunc() {\n        new FormEntryPage(\"ranking_widget\")\n                .clickRankingButton()\n                .assertTexts(\"Mango\", \"Oranges\", \"Strawberries\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/RequiredAndConstraintQuestionTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StorageUtils.getAuditLogForFirstInstance\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.strings.R\n\n@RunWith(AndroidJUnit4::class)\nclass RequiredAndConstraintQuestionTest {\n    var rule: CollectTestRule = CollectTestRule()\n\n    @get:Rule\n    var ruleChain: RuleChain = chain()\n        .around(rule)\n\n    @Test\n    fun requiredQuestionIsMarkedWithAnAsterisk() {\n        rule.startAtMainMenu()\n            .copyForm(\"required_question_with_custom_error_message.xml\")\n            .startBlankForm(\"required_question_with_custom_error_message\")\n            .assertQuestion(\"Required question\", true)\n    }\n\n    @Test // https://github.com/getodk/collect/issues/6327\n    fun requiredQuestionWithAudioIsMarkedWithAnAsterisk() {\n        rule.startAtMainMenu()\n            .copyForm(\"required_question_with_audio.xml\")\n            .startBlankForm(\"required_question_with_audio\")\n            .swipeToNextQuestion(\"Required question with audio\", true)\n    }\n\n    @Test\n    fun requiredQuestionDisplaysACustomErrorMessageIfSpecified() {\n        rule.startAtMainMenu()\n            .copyForm(\"required_question_with_custom_error_message.xml\")\n            .startBlankForm(\"required_question_with_custom_error_message\")\n            .swipeToNextQuestionWithConstraintViolation(\"Custom message\")\n    }\n\n    @Test\n    fun errorMessageDisappearsAfterActivityRecreation() {\n        rule.startAtMainMenu()\n            .copyForm(\"required_question_with_custom_error_message.xml\")\n            .startBlankForm(\"required_question_with_custom_error_message\")\n            .swipeToNextQuestionWithConstraintViolation(\"Custom message\")\n            .rotateToLandscape(FormEntryPage(\"required_question_with_custom_error_message\"))\n            .assertTextDoesNotExist(\"Custom message\")\n    }\n\n    @Test\n    fun validatingFormByPressingValidateInOptionsMenuOnSameScreen_usesNewlyAddedAnswers() {\n        rule.startAtMainMenu()\n            .copyForm(\"required_question_with_custom_error_message.xml\")\n            .startBlankForm(\"required_question_with_custom_error_message\")\n            .answerQuestion(\"* Required question\", \"blah\")\n            .clickOptionsIcon()\n            .clickOnString(R.string.validate)\n            .assertText(R.string.success_form_validation)\n            .assertTextDoesNotExist(\"Custom message\")\n    }\n\n    @Test\n    fun validatingFormByPressingValidateInOptionsMenuOnDifferentScreen_movesToTheQuestionWithErrorAndDisplaysError() {\n        rule.startAtMainMenu()\n            .copyForm(\"required_question_with_custom_error_message.xml\")\n            .startBlankForm(\"required_question_with_custom_error_message\")\n            .clickGoToArrow()\n            .clickGoToEnd()\n            .clickOptionsIcon()\n            .clickOnString(R.string.validate, FormEntryPage(\"required_question_with_custom_error_message\"))\n            .assertText(\"Custom message\")\n            .assertQuestion(\"Required question\", true)\n    }\n\n    @Test\n    fun validatingFormByPressingValidateInOptionsMenuOnDifferentScreen_movesToTheQuestionWithErrorAndDisplaysError_whenTheQuestionIsInFieldList() {\n        rule.startAtMainMenu()\n            .copyForm(\"requiredQuestionInFieldList.xml\")\n            .startBlankForm(\"requiredQuestionInFieldList\")\n            .clickGoToArrow()\n            .clickGoToEnd()\n            .clickOptionsIcon()\n            .clickOnString(R.string.validate, FormEntryPage(\"requiredQuestionInFieldList\"))\n            .assertText(\"Custom required message\") // Make sure both questions are still displayed on the same screen\n            .assertQuestion(\"Foo\", true)\n            .assertQuestion(\"Bar\", true)\n    }\n\n    @Test\n    fun emptyRequiredQuestion_isNotSavedToAuditLogOnMovingForward() {\n        rule.startAtMainMenu()\n            .copyForm(\"required_question_with_custom_error_message.xml\")\n            .startBlankForm(\"required_question_with_custom_error_message\")\n            .swipeToNextQuestionWithConstraintViolation(\"Custom message\")\n\n        val auditLog = getAuditLogForFirstInstance()\n        assertThat(auditLog.size, equalTo(1))\n        assertThat(auditLog[0][0], equalTo(\"form start\"))\n    }\n\n    @Test\n    fun emptyRequiredQuestion_isNotSavedToAuditLogOnFormValidation() {\n        rule.startAtMainMenu()\n            .copyForm(\"required_question_with_custom_error_message.xml\")\n            .startBlankForm(\"required_question_with_custom_error_message\")\n            .clickOptionsIcon()\n            .clickOnString(R.string.validate)\n\n        val auditLog = getAuditLogForFirstInstance()\n        assertThat(auditLog.size, equalTo(1))\n        assertThat(auditLog[0][0], equalTo(\"form start\"))\n    }\n\n    @Test\n    fun emptyRequiredQuestionInFieldListAndNotFirst_isValidatedProperly() {\n        rule.startAtMainMenu()\n            .copyForm(\"requiredQuestionInFieldList.xml\")\n            .startBlankForm(\"requiredQuestionInFieldList\")\n            .answerQuestion(\"Foo\", true, \"blah\")\n            .swipeToNextQuestionWithConstraintViolation(\"Custom required message2\")\n            .clickOptionsIcon()\n            .clickOnString(R.string.validate)\n            .assertText(\"Custom required message2\")\n            .clickGoToArrow()\n            .clickGoToEnd()\n            .clickSaveAndExitWithError(\"Custom required message2\")\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5847\n    fun savingFormWithInvalidQuestion_doesNotChangeTheCurrentQuestionIndex() {\n        rule.startAtMainMenu()\n            .copyForm(\"two-question-required.xml\")\n            .startBlankForm(\"Two Question Required\")\n            .clickSave()\n            .swipeToNextQuestion(\"What is your age?\")\n    }\n\n    @Test // https://github.com/getodk/collect/issues/7078\n    fun navigatingToNextQuestion_withInvalidAnswer_keepsInvalidAnswer() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-with-constraint.xml\")\n            .startBlankForm(\"One Question With Constraint\")\n            .answerQuestion(\"What is your age?\", \"17\")\n            .swipeToNextQuestionWithConstraintViolation(\"Age must be at least 18\")\n            .assertAnswer(\"What is your age?\", \"17\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/SaveIncompleteTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.PageComposeRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\n\n@RunWith(AndroidJUnit4::class)\nclass SaveIncompleteTest {\n\n    var rule = CollectTestRule()\n\n    @get:Rule\n    var chain: RuleChain = chain()\n        .around(rule)\n\n    @Test\n    fun viewingSaveIncompleteQuestion_savesCurrentAnswers() {\n        rule.startAtMainMenu()\n            .copyForm(\"two-question-save-incomplete.xml\")\n            .startBlankForm(\"Two Question Save Incomplete\")\n            .answerQuestion(\"What is your name?\", \"Dez\")\n            .swipeToNextQuestion(\"[saveIncomplete] What is your age?\")\n            .pressBackAndDiscardChanges()\n\n            .clickDrafts(1)\n            .clickOnForm(\"Two Question Save Incomplete\")\n            .assertAnswer(\"Dez\")\n    }\n\n    @Test\n    fun viewingSaveIncompleteQuestion_whenConstrainsAreViolated_savesCurrentAnswers() {\n        rule.startAtMainMenu()\n            .copyForm(\"two-question-save-incomplete-required.xml\")\n            .startBlankForm(\"Two Question Save Incomplete Required\")\n            .answerQuestion(\"What is your name?\", \"Dez\")\n            .swipeToNextQuestion(\"[saveIncomplete] What is your age?\", true)\n            .pressBackAndDiscardChanges()\n\n            .clickDrafts(1)\n            .clickOnForm(\"Two Question Save Incomplete Required\")\n            .assertAnswer(\"Dez\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/SavePointTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StorageUtils\nimport org.odk.collect.android.support.pages.AppClosedPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.FormHierarchyPage\nimport org.odk.collect.android.support.pages.SaveOrDiscardFormDialog\nimport org.odk.collect.android.support.rules.FormEntryActivityTestRule\nimport org.odk.collect.android.support.rules.PageComposeRule\nimport org.odk.collect.android.support.rules.RecentAppsRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass SavePointTest {\n\n    private val recentAppsRule = RecentAppsRule()\n    private val rule = FormEntryActivityTestRule()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain()\n        .around(recentAppsRule)\n        .around(rule)\n\n    @Test\n    fun savePointIsCreatedWhenMovingForwardInForm() {\n        // Create save point\n        rule.setUpProjectAndCopyForm(\"two-question-audit.xml\")\n            .fillNewForm(\"two-question-audit.xml\", \"Two Question\")\n            .answerQuestion(\"What is your name?\", \"Alexei\")\n            .swipeToNextQuestion(\"What is your age?\")\n            .answerQuestion(\"What is your age?\", \"46\")\n            .let { simulateBatteryDeath() }\n\n        // Start blank form and check save point is loaded\n        rule.fillNewFormWithSavepoint(\"two-question-audit.xml\")\n            .clickRecover(FormHierarchyPage(\"Two Question\"))\n            .assertAnswer(\"Alexei\")\n            .assertAnswerDoesNotExist(\"46\")\n            .pressBack(FormEntryPage(\"Two Question\"))\n            .closeSoftKeyboard()\n            .assertQuestion(\"What is your name?\")\n            .pressBack(SaveOrDiscardFormDialog(AppClosedPage()))\n            .clickSaveChanges()\n\n        // Check audit log\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog.size, equalTo(7))\n\n        assertThat(auditLog[0].get(\"event\"), equalTo(\"form start\"))\n        assertThat(auditLog[1].get(\"event\"), equalTo(\"question\"))\n        // Second question event not logged - possibly a problem\n\n        assertThat(auditLog[2].get(\"event\"), equalTo(\"form resume\"))\n        assertThat(auditLog[3].get(\"event\"), equalTo(\"jump\"))\n        assertThat(auditLog[4].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[5].get(\"event\"), equalTo(\"form save\"))\n        assertThat(auditLog[6].get(\"event\"), equalTo(\"form exit\"))\n    }\n\n    @Test\n    fun whenEditing_savePointIsCreatedWhenMovingForwardInForm() {\n        // Create instance\n        rule.setUpProjectAndCopyForm(\"two-question-audit.xml\", listOf(\"external_data_10.zip\"))\n            .fillNewForm(\"two-question-audit.xml\", \"Two Question\")\n            .fillOutAndSave(\n                AppClosedPage(),\n                FormEntryPage.QuestionAndAnswer(\"What is your name?\", \"Pasquale\"),\n                FormEntryPage.QuestionAndAnswer(\"What is your age?\", \"52\")\n            )\n\n        // Create save point\n        rule.editForm(\"two-question-audit.xml\", \"Two Question\")\n            .clickGoToStart()\n            .answerQuestion(\"What is your name?\", \"Alexei\")\n            .swipeToNextQuestion(\"What is your age?\")\n            .answerQuestion(\"What is your age?\", \"46\")\n            .let { simulateBatteryDeath() }\n\n        // Edit instance and check save point is loaded\n        rule.editFormWithSavepoint(\"two-question-audit.xml\")\n            .clickRecover(FormHierarchyPage(\"Two Question\"))\n            .assertAnswer(\"Alexei\")\n            .assertAnswer(\"52\")\n            .assertAnswerDoesNotExist(\"46\")\n            .pressBack(FormEntryPage(\"Two Question\"))\n            .closeSoftKeyboard()\n            .assertQuestion(\"What is your name?\")\n            .pressBack(SaveOrDiscardFormDialog(AppClosedPage()))\n            .clickSaveChanges()\n\n        // Check audit log\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog.size, equalTo(13))\n\n        assertThat(auditLog[5].get(\"event\"), equalTo(\"form resume\"))\n        assertThat(auditLog[6].get(\"event\"), equalTo(\"jump\"))\n        assertThat(auditLog[7].get(\"event\"), equalTo(\"question\"))\n        // Second question event not logged - possibly a problem\n\n        assertThat(auditLog[8].get(\"event\"), equalTo(\"form resume\"))\n        assertThat(auditLog[9].get(\"event\"), equalTo(\"jump\"))\n        assertThat(auditLog[10].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[11].get(\"event\"), equalTo(\"form save\"))\n        assertThat(auditLog[12].get(\"event\"), equalTo(\"form exit\"))\n    }\n\n    @Test\n    fun savePointIsCreatedWhenLeavingTheApp() {\n        // Create save point\n        rule.setUpProjectAndCopyForm(\"two-question-audit.xml\", listOf(\"external_data_10.zip\"))\n            .fillNewForm(\"two-question-audit.xml\", \"Two Question\")\n            .answerQuestion(\"What is your name?\", \"Alexei\")\n\n        recentAppsRule.leaveAndKillApp()\n\n        // Start blank form and check save point is loaded\n        rule.fillNewFormWithSavepoint(\"two-question-audit.xml\")\n            .clickRecover(FormHierarchyPage(\"Two Question\"))\n            .assertAnswer(\"Alexei\")\n            .pressBack(FormEntryPage(\"Two Question\"))\n            .closeSoftKeyboard()\n            .assertQuestion(\"What is your name?\")\n            .pressBack(SaveOrDiscardFormDialog(AppClosedPage()))\n            .clickSaveChanges()\n\n        // Check audit log\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog.size, equalTo(6))\n\n        assertThat(auditLog[0].get(\"event\"), equalTo(\"form start\"))\n        // Question event not logged - possibly a problem\n\n        assertThat(auditLog[1].get(\"event\"), equalTo(\"form resume\"))\n        assertThat(auditLog[2].get(\"event\"), equalTo(\"jump\"))\n        assertThat(auditLog[3].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[4].get(\"event\"), equalTo(\"form save\"))\n        assertThat(auditLog[5].get(\"event\"), equalTo(\"form exit\"))\n    }\n\n    @Test\n    fun whenEditing_savePointIsCreatedWhenLeavingTheApp() {\n        // Create instance\n        rule.setUpProjectAndCopyForm(\"two-question-audit.xml\", listOf(\"external_data_10.zip\"))\n            .fillNewForm(\"two-question-audit.xml\", \"Two Question\")\n            .fillOutAndSave(\n                AppClosedPage(),\n                FormEntryPage.QuestionAndAnswer(\"What is your name?\", \"Pasquale\"),\n                FormEntryPage.QuestionAndAnswer(\"What is your age?\", \"52\")\n            )\n\n        // Create save point\n        rule.editForm(\"two-question-audit.xml\", \"Two Question\")\n            .clickGoToStart()\n            .answerQuestion(\"What is your name?\", \"Alexei\")\n\n        recentAppsRule.leaveAndKillApp()\n\n        // Edit instance and check save point is loaded\n        rule.editFormWithSavepoint(\"two-question-audit.xml\")\n            .clickRecover(FormHierarchyPage(\"Two Question\"))\n            .assertAnswer(\"Alexei\")\n            .assertAnswer(\"52\")\n            .pressBack(FormEntryPage(\"Two Question\"))\n            .closeSoftKeyboard()\n            .assertQuestion(\"What is your name?\")\n            .pressBack(SaveOrDiscardFormDialog(AppClosedPage()))\n            .clickSaveChanges()\n\n        // Check audit log\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog.size, equalTo(12))\n\n        assertThat(auditLog[5].get(\"event\"), equalTo(\"form resume\"))\n        assertThat(auditLog[6].get(\"event\"), equalTo(\"jump\"))\n        // Question event not logged - possibly a problem\n\n        assertThat(auditLog[7].get(\"event\"), equalTo(\"form resume\"))\n        assertThat(auditLog[8].get(\"event\"), equalTo(\"jump\"))\n        assertThat(auditLog[9].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[10].get(\"event\"), equalTo(\"form save\"))\n        assertThat(auditLog[11].get(\"event\"), equalTo(\"form exit\"))\n    }\n\n    @Test\n    fun blankFormSavePointIsNotUsedWhenEditingInstance() {\n        // Create instance\n        rule.setUpProjectAndCopyForm(\"two-question-audit.xml\", listOf(\"external_data_10.zip\"))\n            .fillNewForm(\"two-question-audit.xml\", \"Two Question\")\n            .fillOutAndSave(\n                AppClosedPage(),\n                FormEntryPage.QuestionAndAnswer(\"What is your name?\", \"Pasquale\"),\n                FormEntryPage.QuestionAndAnswer(\"What is your age?\", \"52\")\n            )\n\n        // Create save point for blank form\n        rule.fillNewForm(\"two-question-audit.xml\", \"Two Question\")\n            .answerQuestion(\"What is your name?\", \"Alexei\")\n\n        recentAppsRule.leaveAndKillApp()\n\n        // Check editing instance doesn't load save point\n        rule.editForm(\"two-question-audit.xml\", \"Two Question\")\n            .assertAnswer(\"Pasquale\")\n            .assertAnswer(\"52\")\n            .assertAnswerDoesNotExist(\"Alexei\")\n    }\n\n    @Test\n    fun editedInstanceSavePointIsNotUsedWhenFillingBlankFormOfTheSameForm() {\n        // Create instance\n        rule.setUpProjectAndCopyForm(\"two-question-audit.xml\", listOf(\"external_data_10.zip\"))\n            .fillNewForm(\"two-question-audit.xml\", \"Two Question\")\n            .fillOutAndSave(\n                AppClosedPage(),\n                FormEntryPage.QuestionAndAnswer(\"What is your name?\", \"Pasquale\"),\n                FormEntryPage.QuestionAndAnswer(\"What is your age?\", \"52\")\n            )\n\n        // Create save point for instance\n        rule.editForm(\"two-question-audit.xml\", \"Two Question\")\n            .clickGoToStart()\n            .answerQuestion(\"What is your name?\", \"Alexei\")\n\n        recentAppsRule.leaveAndKillApp()\n\n        // Check starting blank form does not load save point\n        rule.fillNewForm(\"two-question-audit.xml\", \"Two Question\")\n    }\n\n    @Test // https://github.com/getodk/collect/pull/6058\n    fun whenBlankFormStartedThenSavedAndKilled_aSavepointShouldBeCreatedForASavedFormNotForTheBlankOne() {\n        // Start blank form, save it and create a savepoint\n        rule.setUpProjectAndCopyForm(\"two-question.xml\")\n            .fillNewForm(\"two-question.xml\", \"Two Question\")\n            .answerQuestion(\"What is your name?\", \"Alexei\")\n            .clickSave()\n            .swipeToNextQuestion(\"What is your age?\")\n            .answerQuestion(\"What is your age?\", \"46\")\n\n        recentAppsRule.leaveAndKillApp()\n\n        // Start blank form and check save point is not loaded\n        rule.fillNewForm(\"two-question.xml\", \"Two Question\")\n            .pressBackAndDiscardForm(AppClosedPage())\n\n        // Edit saved form and check save point is loaded\n        rule.editFormWithSavepoint(\"two-question.xml\")\n            .clickRecover(FormHierarchyPage(\"Two Question\"))\n            .assertAnswer(\"Alexei\")\n            .assertAnswer(\"46\")\n    }\n\n    /**\n     * Simulates a case where the process is killed without lifecycle clean up (like a phone\n     * being battery dying).\n     */\n    private fun simulateBatteryDeath(): FormEntryActivityTestRule {\n        return rule.simulateProcessRestart()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/SearchAppearancesTest.kt",
    "content": "package org.odk.collect.android.feature.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass SearchAppearancesTest {\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain().around(rule)\n\n    @Test\n    fun searchFunctionFetchesChoicesForSelectOneFromCSVFile() {\n        rule.startAtMainMenu()\n            .copyForm(\"different-search-appearances.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"different-search-appearances\")\n            .assertTexts(\"Mango\", \"Oranges\", \"Strawberries\")\n    }\n\n    @Test\n    fun searchAppearanceEnablesFilteringChoicesForSelectOne() {\n        rule.startAtMainMenu()\n            .copyForm(\"different-search-appearances.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"different-search-appearances\")\n            .clickGoToArrow()\n            .clickOnQuestion(\"Static select one with search appearance\")\n            .inputText(\"w\")\n            .closeSoftKeyboard()\n            .assertTexts(\"Wolf\", \"Warthog\")\n            .assertTextsDoNotExist(\"Racoon\", \"Rabbit\")\n    }\n\n    @Test\n    fun autocompleteAppearanceEnablesFilteringChoicesForSelectOne() {\n        rule.startAtMainMenu()\n            .copyForm(\"different-search-appearances.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"different-search-appearances\")\n            .clickGoToArrow()\n            .clickOnQuestion(\"Static select one with autocomplete appearance\")\n            .inputText(\"r\")\n            .closeSoftKeyboard()\n            .assertTexts(\"Warthog\", \"Raccoon\", \"Rabbit\")\n            .assertTextDoesNotExist(\"Wolf\")\n    }\n\n    @Test\n    fun searchFunctionCanBeCombinedWithSearchAppearanceForSelectOne() {\n        rule.startAtMainMenu()\n            .copyForm(\"different-search-appearances.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"different-search-appearances\")\n            .clickGoToArrow()\n            .clickOnQuestion(\"Select one from a CSV using search() appearance/function and search appearance\")\n            .inputText(\"r\")\n            .closeSoftKeyboard()\n            .assertTexts(\"Oranges\", \"Strawberries\")\n            .assertTextDoesNotExist(\"Mango\")\n    }\n\n    @Test\n    fun searchFunctionCanBeCombinedWithAutocompleteAppearanceForSelectOne() {\n        rule.startAtMainMenu()\n            .copyForm(\"different-search-appearances.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"different-search-appearances\")\n            .clickGoToArrow()\n            .clickOnQuestion(\"Select one from a CSV using search() appearance/function and autocomplete appearance\")\n            .inputText(\"n\")\n            .closeSoftKeyboard()\n            .assertTexts(\"Mango\", \"Oranges\")\n            .assertTextDoesNotExist(\"Strawberries\")\n    }\n\n    @Test\n    fun searchFunctionFetchesChoicesForSelectMultipleFromCSVFile() {\n        rule.startAtMainMenu()\n            .copyForm(\"different-search-appearances.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"different-search-appearances\")\n            .clickGoToArrow()\n            .clickOnQuestion(\"Select multiple from a CSV using search() appearance/function\")\n            .assertTexts(\"Mango\", \"Oranges\", \"Strawberries\")\n    }\n\n    @Test\n    fun searchAppearanceEnablesFilteringChoicesForSelectMultiple() {\n        rule.startAtMainMenu()\n            .copyForm(\"different-search-appearances.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"different-search-appearances\")\n            .clickGoToArrow()\n            .clickOnQuestion(\"Static select multiple with search appearance\")\n            .inputText(\"w\")\n            .closeSoftKeyboard()\n            .assertTexts(\"Wolf\", \"Warthog\")\n            .assertTextsDoNotExist(\"Racoon\", \"Rabbit\")\n    }\n\n    @Test\n    fun autocompleteAppearanceEnablesFilteringChoicesForSelectMultiple() {\n        rule.startAtMainMenu()\n            .copyForm(\"different-search-appearances.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"different-search-appearances\")\n            .clickGoToArrow()\n            .clickOnQuestion(\"Static select multiple with autocomplete appearance\")\n            .inputText(\"r\")\n            .closeSoftKeyboard()\n            .assertTexts(\"Warthog\", \"Raccoon\", \"Rabbit\")\n            .assertTextDoesNotExist(\"Wolf\")\n    }\n\n    @Test\n    fun searchFunctionCanBeCombinedWithSearchAppearanceForSelectMultiple() {\n        rule.startAtMainMenu()\n            .copyForm(\"different-search-appearances.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"different-search-appearances\")\n            .clickGoToArrow()\n            .clickOnQuestion(\"Select multiple from a CSV using search() appearance/function and search appearance\")\n            .inputText(\"r\")\n            .closeSoftKeyboard()\n            .assertTexts(\"Oranges\", \"Strawberries\")\n            .assertTextDoesNotExist(\"Mango\")\n    }\n\n    @Test\n    fun searchFunctionCanBeCombinedWithAutocompleteAppearanceForSelectMultiple() {\n        rule.startAtMainMenu()\n            .copyForm(\"different-search-appearances.xml\", listOf(\"fruits.csv\"))\n            .startBlankForm(\"different-search-appearances\")\n            .clickGoToArrow()\n            .clickOnQuestion(\"Select multiple from a CSV using search() appearance/function and autocomplete appearance\")\n            .inputText(\"n\")\n            .closeSoftKeyboard()\n            .assertTexts(\"Mango\", \"Oranges\")\n            .assertTextDoesNotExist(\"Strawberries\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/audit/AuditTest.kt",
    "content": "package org.odk.collect.android.feature.formentry.audit\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StorageUtils\nimport org.odk.collect.android.support.pages.AccessControlPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.RecentAppsRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass AuditTest {\n\n    private val rule = CollectTestRule()\n    private val recentAppsRule = RecentAppsRule()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain()\n        .around(recentAppsRule)\n        .around(rule)\n\n    @Test\n    fun fillAndEditingForm_updatesAuditLogForInstance() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-audit.xml\")\n            .startBlankForm(\"One Question Audit\")\n            .fillOut(\n                FormEntryPage.QuestionAndAnswer(\"what is your age\", \"31\")\n            )\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n            .clickDrafts(1)\n            .clickOnForm(\"One Question Audit\")\n            .clickGoToStart()\n            .fillOutAndFinalize(\n                FormEntryPage.QuestionAndAnswer(\"what is your age\", \"32\")\n            )\n\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog.size, equalTo(12))\n\n        assertThat(auditLog[0].get(\"event\"), equalTo(\"form start\"))\n        assertThat(auditLog[1].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[2].get(\"event\"), equalTo(\"end screen\"))\n        assertThat(auditLog[3].get(\"event\"), equalTo(\"form save\"))\n        assertThat(auditLog[4].get(\"event\"), equalTo(\"form exit\"))\n\n        assertThat(auditLog[5].get(\"event\"), equalTo(\"form resume\"))\n        assertThat(auditLog[6].get(\"event\"), equalTo(\"jump\"))\n        assertThat(auditLog[7].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[8].get(\"event\"), equalTo(\"end screen\"))\n        assertThat(auditLog[9].get(\"event\"), equalTo(\"form save\"))\n        assertThat(auditLog[10].get(\"event\"), equalTo(\"form exit\"))\n        assertThat(auditLog[11].get(\"event\"), equalTo(\"form finalize\"))\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5551\n    fun navigatingToSettings_savesAnswersFromCurrentScreenToAuditLog() {\n        rule.startAtMainMenu()\n            .copyForm(\"two-question-audit-track-changes.xml\")\n            .startBlankForm(\"One Question Audit Track Changes\")\n            .fillOut(FormEntryPage.QuestionAndAnswer(\"What is your age?\", \"31\"))\n            .clickOptionsIcon()\n            .clickProjectSettings()\n\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog[1].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[1].get(\"new-value\"), equalTo(\"31\"))\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5900\n    fun navigatingToNextQuestion_savesAnswersFromCurrentScreenToAuditLog() {\n        rule.startAtMainMenu()\n            .copyForm(\"two-question-audit-track-changes.xml\")\n            .startBlankForm(\"One Question Audit Track Changes\")\n            .fillOut(FormEntryPage.QuestionAndAnswer(\"What is your age?\", \"31\"))\n            .swipeToNextQuestion(\"What is your name?\")\n            .fillOut(FormEntryPage.QuestionAndAnswer(\"What is your name?\", \"Adam\"))\n            .swipeToEndScreen()\n\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog[1].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[1].get(\"new-value\"), equalTo(\"31\"))\n        assertThat(auditLog[2].get(\"new-value\"), equalTo(\"Adam\"))\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5253\n    fun navigatingBackToTheFormAfterKillingTheAppWhenMovingBackwardsIsDisabled_savesFormResumeEventToAuditLog() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-audit.xml\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickAccessControl()\n            .clickFormEntrySettings()\n            .clickMovingBackwards()\n            .clickOnString(org.odk.collect.strings.R.string.yes)\n            .pressBack(AccessControlPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .startBlankForm(\"One Question Audit\")\n            .fillOut(FormEntryPage.QuestionAndAnswer(\"what is your age\", \"31\"))\n            .killAndReopenApp(rule, recentAppsRule, MainMenuPage())\n            .startBlankFormWithSavepoint(\"One Question Audit\")\n            .clickRecover(FormEntryPage(\"One Question Audit\"))\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog[1].get(\"event\"), equalTo(\"form resume\"))\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5659\n    fun savingFormWithBackgroundRecording_doesNotDuplicateAnyEvent() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-background-audio-audit.xml\")\n            .startBlankForm(\"One Question Background Audio And Audit\")\n            .fillOutAndSave(\n                MainMenuPage(),\n                FormEntryPage.QuestionAndAnswer(\"what is your age\", \"31\")\n            )\n\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog.size, equalTo(4))\n\n        assertThat(auditLog[0].get(\"event\"), equalTo(\"form start\"))\n        assertThat(auditLog[1].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[2].get(\"event\"), equalTo(\"form save\"))\n        assertThat(auditLog[3].get(\"event\"), equalTo(\"form exit\"))\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5262\n    fun locationTrackingEventsShouldBeLoggedBeforeQuestionEventsWhenANewFormIsStartedOrAPreviouslySavedOneEdited() {\n        rule.startAtMainMenu()\n            .copyForm(\"location-audit.xml\")\n            .startBlankForm(\"Audit with Location\")\n            .pressBackAndSaveAsDraft()\n            .clickDrafts()\n            .clickOnForm(\"Audit with Location\")\n            .clickGoToStart()\n            .pressBackAndSaveAsDraft()\n\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog.size, equalTo(15))\n\n        assertThat(auditLog[0].get(\"event\"), equalTo(\"form start\"))\n        assertThat(auditLog[1].get(\"event\"), equalTo(\"location tracking enabled\"))\n        assertThat(auditLog[2].get(\"event\"), equalTo(\"location permissions granted\"))\n        assertThat(auditLog[3].get(\"event\"), equalTo(\"location providers enabled\"))\n        assertThat(auditLog[4].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[5].get(\"event\"), equalTo(\"form save\"))\n        assertThat(auditLog[6].get(\"event\"), equalTo(\"form exit\"))\n        assertThat(auditLog[7].get(\"event\"), equalTo(\"form resume\"))\n        assertThat(auditLog[8].get(\"event\"), equalTo(\"jump\"))\n        assertThat(auditLog[9].get(\"event\"), equalTo(\"location tracking enabled\"))\n        assertThat(auditLog[10].get(\"event\"), equalTo(\"location permissions granted\"))\n        assertThat(auditLog[11].get(\"event\"), equalTo(\"location providers enabled\"))\n        assertThat(auditLog[12].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[13].get(\"event\"), equalTo(\"form save\"))\n        assertThat(auditLog[14].get(\"event\"), equalTo(\"form exit\"))\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5915\n    fun changingTheAnswerAfterSavingAFormOnTheSamePage_shouldLogTheNewAnswer() {\n        rule.startAtMainMenu()\n            .copyForm(\"two-question-audit-track-changes.xml\")\n            .startBlankForm(\"One Question Audit Track Changes\")\n            .clickSave()\n            .fillOut(FormEntryPage.QuestionAndAnswer(\"What is your age?\", \"31\"))\n            .swipeToNextQuestion(\"What is your name?\")\n            .clickSave()\n            .fillOut(FormEntryPage.QuestionAndAnswer(\"What is your name?\", \"Adam\"))\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n        val auditLog = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLog.size, equalTo(10))\n\n        assertThat(auditLog[0].get(\"event\"), equalTo(\"form start\"))\n\n        assertThat(auditLog[1].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[1].get(\"node\"), equalTo(\"/data/age\"))\n        assertThat(auditLog[1].get(\"new-value\"), equalTo(\"\"))\n\n        assertThat(auditLog[2].get(\"event\"), equalTo(\"form save\"))\n\n        assertThat(auditLog[3].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[3].get(\"node\"), equalTo(\"/data/age\"))\n        assertThat(auditLog[3].get(\"new-value\"), equalTo(\"31\"))\n\n        assertThat(auditLog[4].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[4].get(\"node\"), equalTo(\"/data/name\"))\n        assertThat(auditLog[4].get(\"new-value\"), equalTo(\"\"))\n\n        assertThat(auditLog[5].get(\"event\"), equalTo(\"form save\"))\n\n        assertThat(auditLog[6].get(\"event\"), equalTo(\"question\"))\n        assertThat(auditLog[6].get(\"node\"), equalTo(\"/data/name\"))\n        assertThat(auditLog[6].get(\"new-value\"), equalTo(\"Adam\"))\n\n        assertThat(auditLog[7].get(\"event\"), equalTo(\"end screen\"))\n        assertThat(auditLog[8].get(\"event\"), equalTo(\"form save\"))\n        assertThat(auditLog[9].get(\"event\"), equalTo(\"form exit\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/audit/IdentifyUserTest.kt",
    "content": "package org.odk.collect.android.feature.formentry.audit\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StorageUtils.getAuditLogForFirstInstance\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.FormHierarchyPage\nimport org.odk.collect.android.support.pages.IdentifyUserPromptPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport java.io.IOException\n\n@RunWith(AndroidJUnit4::class)\nclass IdentifyUserTest {\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain\n        .chain()\n        .around(rule)\n\n    @Test\n    @Throws(IOException::class)\n    fun openingForm_andThenEnteringIdentity_andThenFillingForm_logsUser() {\n        rule.startAtMainMenu()\n            .copyForm(IDENTIFY_USER_AUDIT_FORM)\n            .clickFillBlankForm()\n            .clickOnFormWithIdentityPrompt(\"Identify User\")\n            .enterIdentity(\"Lucius\")\n            .clickKeyboardEnter(FormEntryPage(\"Identify User\"))\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        val auditLog = getAuditLogForFirstInstance()\n        val formStartEvent = auditLog[0]\n        assertThat(formStartEvent[0], equalTo(\"form start\"))\n        assertThat(formStartEvent[4], equalTo(\"Lucius\"))\n    }\n\n    @Test\n    @Throws(IOException::class)\n    fun openingSavedForm_andThenEnteringIdentity_andThenFillingForm_logsUser() {\n        rule.startAtMainMenu()\n            .copyForm(IDENTIFY_USER_AUDIT_FORM)\n            .clickFillBlankForm()\n            .clickOnFormWithIdentityPrompt(\"Identify User\")\n            .enterIdentity(\"Lucius\")\n            .clickKeyboardEnter(FormEntryPage(\"Identify User\"))\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n            .clickDrafts()\n            .clickOnFormWithIdentityPrompt(\"Identify User\")\n            .enterIdentity(\"Jack\")\n            .clickKeyboardEnter(FormHierarchyPage(\"Identify User\"))\n            .clickJumpEndButton()\n            .clickFinalize()\n\n        val auditLog = getAuditLogForFirstInstance()\n        val formResumeEvent = auditLog[5]\n        assertThat(formResumeEvent[0], equalTo(\"form resume\"))\n        assertThat(formResumeEvent[4], equalTo(\"Jack\"))\n    }\n\n    @Test\n    fun openingForm_andEnteringBlankIdentity_remainsOnIdentityPrompt() {\n        rule.startAtMainMenu()\n            .copyForm(IDENTIFY_USER_AUDIT_FORM)\n            .clickFillBlankForm()\n            .clickOnFormWithIdentityPrompt(\"Identify User\")\n            .enterIdentity(\"  \")\n            .clickKeyboardEnterWithValidationError()\n    }\n\n    @Test\n    fun openingForm_andPressingBack_returnsToMainMenu() {\n        rule.startAtMainMenu()\n            .copyForm(IDENTIFY_USER_AUDIT_FORM)\n            .clickFillBlankForm()\n            .clickOnFormWithIdentityPrompt(\"Identify User\")\n            .closeSoftKeyboard()\n            .pressBack(MainMenuPage())\n    }\n\n    @Test\n    fun openingForm_andRotating_remainsOnIdentityPrompt() {\n        rule.startAtMainMenu()\n            .copyForm(IDENTIFY_USER_AUDIT_FORM)\n            .clickFillBlankForm()\n            .clickOnFormWithIdentityPrompt(\"Identify User\")\n            .enterIdentity(\"Blah\")\n            .rotateToLandscape(IdentifyUserPromptPage(\"Identify User\"))\n            .assertText(\"Blah\")\n    }\n\n    @Test\n    fun minimizingAndReopeningApp_remainsOnIdentityPrompt() {\n        rule.startAtMainMenu()\n            .copyForm(IDENTIFY_USER_AUDIT_FORM)\n            .clickFillBlankForm()\n            .clickOnFormWithIdentityPrompt(\"Identify User\")\n            .enterIdentity(\"Blah\")\n            .minimizeAndReopenApp(IdentifyUserPromptPage(\"Identify User\"))\n            .assertText(\"Blah\")\n    }\n\n    @Test\n    fun openingForm_andPressingCloseCross_returnsToMainMenu() {\n        rule.startAtMainMenu()\n            .copyForm(IDENTIFY_USER_AUDIT_FORM)\n            .clickFillBlankForm()\n            .clickOnFormWithIdentityPrompt(\"Identify User\")\n            .pressClose()\n    }\n\n    @Test\n    fun openFormWithIdentifyUserFalse_proceedsToForm() {\n        rule.startAtMainMenu()\n            .copyForm(IDENTIFY_USER_AUDIT_FALSE_FORM)\n            .clickFillBlankForm()\n            .clickOnForm(\"Identify User False\")\n            .swipeToEndScreen()\n            .clickFinalize()\n    }\n\n    companion object {\n        private const val IDENTIFY_USER_AUDIT_FORM = \"identify-user-audit.xml\"\n        private const val IDENTIFY_USER_AUDIT_FALSE_FORM = \"identify-user-audit-false.xml\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/audit/TrackChangesReasonTest.kt",
    "content": "package org.odk.collect.android.feature.formentry.audit\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StorageUtils\nimport org.odk.collect.android.support.pages.ChangesReasonPromptPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.SaveOrDiscardFormDialog\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass TrackChangesReasonTest {\n\n    var rule = CollectTestRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = TestRuleChain.chain()\n        .around(rule)\n\n    @Test\n    fun openingAFormToEdit_andClickingSaveAndExit_andEnteringReason_andClickingSave_returnsToMainMenu() {\n        rule.startAtMainMenu()\n            .copyForm(\"track-changes-reason-on-edit.xml\")\n            .startBlankForm(\"Track Changes Reason\")\n            .inputText(\"Nothing much...\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n            .clickDrafts()\n            .clickOnForm(\"Track Changes Reason\")\n            .clickGoToEnd()\n            .clickSaveAndExitWithChangesReasonPrompt()\n            .enterReason(\"Needed to be more exciting and less mysterious\")\n            .clickSave()\n\n        val auditLogForFirstInstance = StorageUtils.getAuditLogForFirstInstance()\n        assertThat(auditLogForFirstInstance[8].get(\"event\"), equalTo(\"change reason\"))\n        assertThat(\n            auditLogForFirstInstance[8].get(\"change-reason\"),\n            equalTo(\"Needed to be more exciting and less mysterious\")\n        )\n    }\n\n    @Test\n    fun openingAFormToEdit_andClickingSaveAndExit_andPressingBack_returnsToForm() {\n        rule.startAtMainMenu()\n            .copyForm(\"track-changes-reason-on-edit.xml\")\n            .startBlankForm(\"Track Changes Reason\")\n            .inputText(\"Nothing much...\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n            .clickDrafts()\n\n            .clickOnForm(\"Track Changes Reason\")\n            .clickGoToEnd()\n            .clickSaveAndExitWithChangesReasonPrompt()\n            .closeSoftKeyboard()\n            .pressBack(FormEntryPage(\"Track Changes Reason\"))\n    }\n\n    @Test\n    fun openingAFormToEdit_andClickingSaveAndExit_andClickingCross_returnsToForm() {\n        rule.startAtMainMenu()\n            .copyForm(\"track-changes-reason-on-edit.xml\")\n            .startBlankForm(\"Track Changes Reason\")\n            .inputText(\"Nothing much...\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n            .clickDrafts()\n            .clickOnForm(\"Track Changes Reason\")\n            .clickGoToEnd()\n            .clickSaveAndExitWithChangesReasonPrompt()\n            .closeSoftKeyboard()\n            .pressClose(FormEntryPage(\"Track Changes Reason\"))\n    }\n\n    @Test\n    fun openingAFormToEdit_andClickingSaveAndExit_andRotating_remainsOnPrompt() {\n        rule.startAtMainMenu()\n            .copyForm(\"track-changes-reason-on-edit.xml\")\n            .startBlankForm(\"Track Changes Reason\")\n            .inputText(\"Nothing much...\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n            .clickDrafts()\n            .clickOnForm(\"Track Changes Reason\")\n            .clickGoToEnd()\n            .clickSaveAndExitWithChangesReasonPrompt()\n            .enterReason(\"Something\")\n            .rotateToLandscape(ChangesReasonPromptPage(\"Track Changes Reason\"))\n            .assertText(\"Something\")\n            .closeSoftKeyboard()\n            .clickSave()\n    }\n\n    @Test\n    fun openingAFormToEdit_andPressingBack_andClickingSaveChanges_promptsForReason() {\n        rule.startAtMainMenu()\n            .copyForm(\"track-changes-reason-on-edit.xml\")\n            .startBlankForm(\"Track Changes Reason\")\n            .inputText(\"Nothing much...\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n            .clickDrafts()\n            .clickOnForm(\"Track Changes Reason\")\n            .clickGoToStart()\n            .closeSoftKeyboard()\n            .pressBack(\n                SaveOrDiscardFormDialog(\n                    ChangesReasonPromptPage(\"Track Changes Reason\")\n                )\n            )\n            .clickSaveChanges()\n    }\n\n    @Test\n    fun openingAFormToEdit_andPressingBack_andIgnoringChanges_returnsToMainMenu() {\n        rule.startAtMainMenu()\n            .copyForm(\"track-changes-reason-on-edit.xml\")\n            .startBlankForm(\"Track Changes Reason\")\n            .inputText(\"Nothing much...\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n            .clickDrafts()\n            .clickOnForm(\"Track Changes Reason\")\n            .clickGoToStart()\n            .closeSoftKeyboard()\n            .pressBack(SaveOrDiscardFormDialog(MainMenuPage()))\n            .clickDiscardChanges()\n    }\n\n    @Test\n    fun openingFormToEdit_andClickingSave_promptsForReason() {\n        rule.startAtMainMenu()\n            .copyForm(\"track-changes-reason-on-edit.xml\")\n            .startBlankForm(\"Track Changes Reason\")\n            .inputText(\"Nothing much...\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n            .clickDrafts()\n            .clickOnForm(\"Track Changes Reason\")\n            .clickGoToStart()\n            .clickSaveWithChangesReasonPrompt()\n            .enterReason(\"Bah\")\n            .clickSave(FormEntryPage(\"Track Changes Reason\"))\n            .assertQuestion(\"What up?\")\n    }\n\n    @Test\n    fun fillingABlankForm_andClickingSave_andClickingSaveAndExit_doesNotPromptForReason() {\n        rule.startAtMainMenu()\n            .copyForm(\"track-changes-reason-on-edit.xml\")\n            .startBlankForm(\"Track Changes Reason\")\n            .clickSave()\n            .closeSoftKeyboard()\n            .pressBack(SaveOrDiscardFormDialog(MainMenuPage()))\n            .clickSaveChanges()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/backgroundlocation/LocationTrackingAuditTest.java",
    "content": "package org.odk.collect.android.feature.formentry.backgroundlocation;\n\nimport static androidx.test.espresso.matcher.ViewMatchers.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\n\nimport android.app.Application;\n\nimport org.apache.commons.csv.CSVRecord;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.odk.collect.android.support.FakeLocationClient;\nimport org.odk.collect.android.support.StorageUtils;\nimport org.odk.collect.android.support.TestDependencies;\nimport org.odk.collect.android.support.pages.FormEntryPage;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.location.LocationClient;\nimport org.odk.collect.testshared.FakeLocation;\n\nimport java.io.IOException;\nimport java.util.List;\n\npublic class LocationTrackingAuditTest {\n\n    private final FakeLocationClient locationClient = new FakeLocationClient();\n\n    public CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain(new TestDependencies() {\n                @Override\n                public LocationClient providesFusedLocationClient(Application application) {\n                    return locationClient;\n                }\n            })\n            .around(rule);\n\n    @Test\n    public void locationTrackingIsLogged_andLocationIsLoggedForEachQuestion() throws IOException {\n        FormEntryPage formEntry = rule.startAtMainMenu()\n                .copyForm(\"location-audit.xml\")\n                .startBlankForm(\"Audit with Location\")\n                .assertBackgroundLocationSnackbarShown();\n\n        FakeLocation location1 = new FakeLocation(null);\n        location1.setLatitude(1.0);\n        location1.setLongitude(1.0);\n        locationClient.setLocation(location1);\n\n        formEntry\n                .assertQuestion(\"Text1\")\n                .swipeToNextQuestion(\"Text2\")\n                .clickSave();\n\n        List<CSVRecord> auditLog = StorageUtils.getAuditLogForFirstInstance();\n        assertThat(auditLog.get(1).get(0), equalTo(\"location tracking enabled\"));\n        assertThat(auditLog.get(2).get(0), equalTo(\"location permissions granted\"));\n        assertThat(auditLog.get(3).get(0), equalTo(\"location providers enabled\"));\n\n        CSVRecord firstQuestionEvent = auditLog.get(4);\n        assertThat(firstQuestionEvent.get(0), equalTo(\"question\"));\n        assertThat(firstQuestionEvent.get(4), equalTo(\"1.0\"));\n        assertThat(firstQuestionEvent.get(5), equalTo(\"1.0\"));\n\n        CSVRecord secondQuestionEvent = auditLog.get(5);\n        assertThat(secondQuestionEvent.get(0), equalTo(\"question\"));\n        assertThat(secondQuestionEvent.get(4), equalTo(\"1.0\"));\n        assertThat(secondQuestionEvent.get(5), equalTo(\"1.0\"));\n    }\n\n    @Test\n    public void locationCollectionToggle_ShouldBeAvailable() {\n        rule.startAtMainMenu()\n                .copyForm(\"location-audit.xml\")\n                .startBlankForm(\"Audit with Location\")\n                .clickOptionsIcon()\n                .assertText(org.odk.collect.strings.R.string.track_location_on);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/backgroundlocation/SetGeopointActionTest.java",
    "content": "package org.odk.collect.android.feature.formentry.backgroundlocation;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.odk.collect.android.formentry.FormEntryMenuProvider;\nimport org.odk.collect.android.support.rules.BlankFormTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\npublic class SetGeopointActionTest {\n    private static final String SETGEOPOINT_ACTION_FORM = \"setgeopoint-action.xml\";\n\n    public BlankFormTestRule rule = new BlankFormTestRule(SETGEOPOINT_ACTION_FORM, \"setgeopoint-action-instance-load\");\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(rule);\n\n    @Test\n    public void locationCollectionSnackbar_ShouldBeDisplayedAtFormLaunch() {\n        rule.startInFormEntry()\n                .checkIsSnackbarWithMessageDisplayed(org.odk.collect.strings.R.string.background_location_enabled, \"⋮\");\n    }\n\n    /**\n     * Could be replaced in test for {@link FormEntryMenuProvider}\n     */\n    @Test\n    public void locationCollectionToggle_ShouldBeAvailable() {\n        rule.startInFormEntry()\n                .clickOptionsIcon()\n                .assertText(org.odk.collect.strings.R.string.track_location_on);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/dynamicpreload/DynamicPreLoadedDataPullTest.kt",
    "content": "package org.odk.collect.android.feature.formentry.dynamicpreload\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.StubOpenRosaServer.EntityListItem\nimport org.odk.collect.android.support.StubOpenRosaServer.MediaFileItem\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\n\n/**\n * This tests the [\"Pull data from CSV\" feature of XLSForms](https://xlsform.org/en/#how-to-pull-data-from-csv).\n *\n */\nclass DynamicPreLoadedDataPullTest {\n\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val chain: RuleChain = chain(testDependencies).around(rule)\n\n    @Test\n    fun canUsePullDataFunctionToPullDataFromCSV() {\n        testDependencies.server.addForm(\"pull_data.xml\", listOf(MediaFileItem(\"fruits.csv\")))\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"pull_data\")\n            .assertText(\"The fruit Mango is pulled csv data.\")\n    }\n\n    @Test\n    fun canUsePullDataFunctionToPullDataFromLocalEntities() {\n        testDependencies.server.addForm(\"one-question-entity-registration.xml\")\n        testDependencies.server.addForm(\n            \"entity-update-pulldata.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Registration\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"Name\", \"Logan Roy\"))\n\n            .startBlankForm(\"Entity Update Pull Data\")\n            .clickOnText(\"Logan Roy\")\n            .swipeToNextQuestion(\"Name\")\n            .assertText(\"Logan Roy\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/dynamicpreload/DynamicPreLoadedDataSelects.java",
    "content": "package org.odk.collect.android.feature.formentry.dynamicpreload;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.support.rules.FormEntryActivityTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\nimport java.util.Collections;\n\n/**\n * This tests the \"Dynamic selects from pre-loaded data\" feature of XLSForms.\n * <p>\n * * @see <a href=\"https://xlsform.org/en/#dynamic-selects-from-pre-loaded-data\">Dynamic selects from pre-loaded data</a>\n */\npublic class DynamicPreLoadedDataSelects {\n\n    private final FormEntryActivityTestRule rule = new FormEntryActivityTestRule();\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(rule);\n\n    @Test\n    public void withoutFilterAndWithFilter_displaysMatchingChoices() {\n        rule.setUpProjectAndCopyForm(\"external-csv-search.xml\", Collections.singletonList(\"external-csv-search-produce.csv\"))\n                .fillNewForm(\"external-csv-search.xml\", \"external-csv-search\")\n                .assertQuestion(\"Multiple produce\")\n                .assertText(\"Artichoke\")\n                .assertText(\"Apple\")\n                .assertText(\"Banana\")\n                .assertText(\"Blueberry\")\n                .assertText(\"Cherimoya\")\n                .assertText(\"Carrot\")\n\n                .swipeToNextQuestion(\"Produce search\")\n                .inputText(\"A\")\n                .swipeToNextQuestion(\"Produce\")\n                .assertText(\"Artichoke\")\n                .assertText(\"Apple\")\n                .assertText(\"Banana\")\n                .assertText(\"Cherimoya\")\n                .assertText(\"Carrot\")\n                .assertTextDoesNotExist(\"Blueberry\");\n    }\n\n    @Test\n    public void displayErrorWhenFilesAreMissing() {\n        rule.setUpProjectAndCopyForm(\"external_data_questions.xml\")\n                .fillNewForm(\"external_data_questions.xml\", \"externalDataQuestions\")\n                .assertText(org.odk.collect.strings.R.string.file_missing, new StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS) + \"/external_data_questions-media/fruits.csv\")\n                .swipeToNextQuestion(\"External csv\")\n                .assertText(org.odk.collect.strings.R.string.file_missing, new StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS) + \"/external_data_questions-media/itemsets.csv\");\n    }\n\n    @Test\n    public void displayWarningWhenQueryIsBad() {\n        rule.setUpProjectAndCopyForm(\"external-csv-search-broken.xml\", Collections.singletonList(\"external-csv-search-produce.csv\"))\n                .fillNewForm(\"external-csv-search-broken.xml\", \"external-csv-search\")\n                .answerQuestion(\"Produce search\", \"blah\")\n                .swipeToNextQuestion(\"Produce\")\n                .assertText(\"no such column: c_wat (code 1 SQLITE_ERROR): , while compiling: SELECT c_name, c_label FROM externalData WHERE c_wat LIKE ?\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/entities/EntityFormApprovalTest.kt",
    "content": "package org.odk.collect.android.feature.formentry.entities\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StubOpenRosaServer.EntityListItem\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass EntityFormApprovalTest {\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun whenListIsApprovalEntityList_localEntitiesCannotBeCreated() {\n        testDependencies.server.addForm(\"one-question-entity-registration.xml\")\n        testDependencies.server.addForm(\n            \"one-question-entity-update.xml\",\n            listOf(EntityListItem(\"people.csv\", true))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Registration\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"Name\", \"Logan Roy\"))\n\n            .startBlankForm(\"One Question Entity Update\")\n            .assertQuestion(\"Select person\")\n            .assertTextDoesNotExist(\"Logan Roy\")\n    }\n\n    @Test\n    fun whenListIsApprovalEntityList_localEntitiesCanBeUpdated() {\n        testDependencies.server.addForm(\"one-question-entity-registration.xml\")\n        testDependencies.server.addForm(\n            \"one-question-entity-update.xml\",\n            listOf(EntityListItem(\"people.csv\", true))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Update\")\n            .assertQuestion(\"Select person\")\n            .clickOnText(\"Roman Roy\")\n            .swipeToNextQuestion(\"Name\")\n            .answerQuestion(\"Name\", \"Romulus Roy\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .startBlankForm(\"One Question Entity Update\")\n            .assertText(\"Romulus Roy\")\n            .assertTextDoesNotExist(\"Roman Roy\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/entities/EntityFormCreateUpdateTest.kt",
    "content": "package org.odk.collect.android.feature.formentry.entities\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StubOpenRosaServer.EntityListItem\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass EntityFormCreateUpdateTest {\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun fillingEntityRegistrationForm_createsEntityForFollowUpForms() {\n        testDependencies.server.addForm(\"one-question-entity-registration.xml\")\n        testDependencies.server.addForm(\n            \"one-question-entity-update.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Registration\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"Name\", \"Logan Roy\"))\n\n            .startBlankForm(\"One Question Entity Update\")\n            .assertQuestion(\"Select person\")\n            .assertTexts(\"Roman Roy\", \"Logan Roy\")\n    }\n\n    @Test\n    fun fillingEntityRegistrationForm_createsEntityForFollowUpFormsWithCachedFormDefs() {\n        testDependencies.server.addForm(\"one-question-entity-registration.xml\")\n        testDependencies.server.addForm(\n            \"one-question-entity-update.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Update\") // Open to create cached form def\n            .pressBackAndDiscardForm()\n\n            .startBlankForm(\"One Question Entity Registration\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"Name\", \"Logan Roy\"))\n\n            .startBlankForm(\"One Question Entity Update\")\n            .assertQuestion(\"Select person\")\n            .assertTexts(\"Roman Roy\", \"Logan Roy\")\n    }\n\n    @Test\n    fun fillingEntityUpdateForm_updatesEntityForFollowUpForms() {\n        testDependencies.server.addForm(\n            \"one-question-entity-update.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Update\")\n            .assertQuestion(\"Select person\")\n            .clickOnText(\"Roman Roy\")\n            .swipeToNextQuestion(\"Name\")\n            .answerQuestion(\"Name\", \"Romulus Roy\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .startBlankForm(\"One Question Entity Update\")\n            .assertText(\"Romulus Roy\")\n            .assertTextDoesNotExist(\"Roman Roy\")\n    }\n\n    @Test\n    fun fillingEntityCreateForm_withUpdate_doesNotCreateEntityForFollowUpForms() {\n        testDependencies.server.addForm(\"one-question-entity-create-and-update.xml\")\n        testDependencies.server.addForm(\n            \"one-question-entity-update.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Registration\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"Name\", \"Logan Roy\"))\n\n            .startBlankForm(\"One Question Entity Update\")\n            .assertQuestion(\"Select person\")\n            .assertText(\"Roman Roy\")\n            .assertTextDoesNotExist(\"Logan Roy\")\n    }\n\n    @Test\n    fun fillingEntityUpdateForm_withCreate_doesNotUpdateEntityForFollowUpForms() {\n        testDependencies.server.addForm(\n            \"one-question-entity-update-and-create.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Update\")\n            .assertQuestion(\"Select person\")\n            .clickOnText(\"Roman Roy\")\n            .swipeToNextQuestion(\"Name\")\n            .answerQuestion(\"Name\", \"Romulus Roy\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .startBlankForm(\"One Question Entity Update\")\n            .assertTextDoesNotExist(\"Romulus Roy\")\n            .assertText(\"Roman Roy\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/entities/EntityFormEditTest.kt",
    "content": "package org.odk.collect.android.feature.formentry.entities\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StubOpenRosaServer.EntityListItem\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass EntityFormEditTest {\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun editingEntityRegistrationForm_doesNotCreateEntityForFollowUpForms() {\n        testDependencies.server.addForm(\"one-question-entity-registration-editable.xml\")\n        testDependencies.server.addForm(\n            \"one-question-entity-update.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Registration Editable\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"Name\", \"Logan Roy\"))\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Entity Registration Editable\")\n            .editForm(\"One Question Entity Registration Editable\")\n            .clickOnQuestion(\"Name\")\n            .answerQuestion(\"Name\", \"Kendall Roy\")\n            .swipeToEndScreen(\"One Question Entity Registration Editable (Edit 1)\")\n            .clickFinalize()\n\n            .startBlankForm(\"One Question Entity Update\")\n            .assertQuestion(\"Select person\")\n            .assertTexts(\"Roman Roy\", \"Logan Roy\")\n            .assertTextDoesNotExist(\"Kendall Roy\")\n    }\n\n    @Test\n    fun editingEntityUpdateForm_doesNotUpdateEntityForFollowUpForms() {\n        testDependencies.server.addForm(\n            \"one-question-entity-update-editable.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Update Editable\")\n            .assertQuestion(\"Select person\")\n            .clickOnText(\"Roman Roy\")\n            .swipeToNextQuestion(\"Name\")\n            .swipeToEndScreen()\n            .clickFinalize()\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Entity Update Editable\")\n            .editForm(\"One Question Entity Update Editable\")\n            .clickOnQuestion(\"Name\")\n            .answerQuestion(\"Name\", \"Romulus Roy\")\n            .swipeToEndScreen(\"One Question Entity Update Editable (Edit 1)\")\n            .clickFinalize()\n\n            .startBlankForm(\"One Question Entity Update Editable\")\n            .assertText(\"Roman Roy\")\n            .assertTextDoesNotExist(\"Romulus Roy\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/entities/EntityFormLockingTest.kt",
    "content": "package org.odk.collect.android.feature.formentry.entities\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass EntityFormLockingTest {\n\n    private val rule = CollectTestRule(useDemoProject = false)\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain()\n        .around(rule)\n\n    @Test\n    fun closingEntityForm_releasesTheLockAndLetsOtherEntityFormsToBeStarted() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .copyForm(\"one-question-entity-registration.xml\")\n            .startBlankForm(\"One Question Entity Registration\")\n            .pressBackAndDiscardForm()\n            .startBlankForm(\"One Question Entity Registration\")\n    }\n\n    @Test\n    fun closingBrokenEntityForm_releasesTheLockAndLetsOtherEntityFormsToBeStarted() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .copyForm(\"one-question-entity-registration-broken.xml\")\n            .copyForm(\"one-question-entity-registration.xml\")\n            .startBlankFormWithError(\"One Question Entity Registration Broken\", true)\n            .clickOKOnDialog(MainMenuPage())\n            .startBlankForm(\"One Question Entity Registration\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/entities/EntityFormSpecVersionTest.kt",
    "content": "package org.odk.collect.android.feature.formentry.entities\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StubOpenRosaServer.EntityListItem\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.strings.R\n\n@RunWith(AndroidJUnit4::class)\nclass EntityFormSpecVersionTest {\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun fillingEntityRegistrationForm_whenFormUsesOldSpecVersion_doesNotCreateEntityForFollowUpForms() {\n        testDependencies.server.addForm(\"one-question-entity-registration-v2023.1.xml\")\n        testDependencies.server.addForm(\n            \"one-question-entity-update.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Registration\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"Name\", \"Logan Roy\"))\n\n            .startBlankForm(\"One Question Entity Update\")\n            .assertQuestion(\"Select person\")\n            .assertText(\"Roman Roy\")\n            .assertTextDoesNotExist(\"Logan Roy\")\n    }\n\n    @Test\n    fun manualEntityFormDownload_withUnsupportedSpecVersion_completesSuccessfully_butThrowsAnErrorAfterOpeningIt() {\n        testDependencies.server.addForm(\"one-question-entity-registration-v2020.1.xml\")\n\n        rule.withProject(testDependencies.server)\n            .clickGetBlankForm()\n            .clickClearAll()\n            .clickForm(\"One Question Entity Registration\")\n            .clickGetSelected()\n            .clickOK(MainMenuPage())\n            .startBlankFormWithError(\"One Question Entity Registration\", true)\n            .assertTextInDialog(R.string.unrecognized_entity_version, \"2020.1.0\")\n            .clickOKOnDialog(MainMenuPage())\n    }\n\n    @Test\n    fun automaticEntityFormDownload_withUnsupportedSpecVersion_completesSuccessfully_butThrowsAnErrorAfterOpeningIt() {\n        testDependencies.server.addForm(\"one-question-entity-registration-v2020.1.xml\")\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankFormWithError(\"One Question Entity Registration\", true)\n            .assertTextInDialog(R.string.unrecognized_entity_version, \"2020.1.0\")\n            .clickOKOnDialog(MainMenuPage())\n    }\n\n    @Test\n    fun syncEntityFormFromDisc_withUnsupportedSpecVersion_completesSuccessfully_butThrowsAnErrorAfterOpeningIt() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .copyForm(\"one-question-entity-registration-v2020.1.xml\")\n            .startBlankFormWithError(\"One Question Entity Registration\", true)\n            .assertTextInDialog(R.string.unrecognized_entity_version, \"2020.1.0\")\n            .clickOKOnDialog(MainMenuPage())\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/entities/EntityListSyncTest.kt",
    "content": "package org.odk.collect.android.feature.formentry.entities\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.Matchers.containsString\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StubOpenRosaServer.EntityListItem\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass EntityListSyncTest {\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun entityListSecondaryInstancesAreConsistentBetweenFollowUpForms() {\n        testDependencies.server.apply {\n            addForm(\n                \"one-question-entity-update.xml\",\n                listOf(EntityListItem(\"people.csv\", \"people.csv\", 1))\n            )\n\n            addForm(\n                \"one-question-entity-follow-up.xml\",\n                listOf(EntityListItem(\"people.csv\", \"updated-people.csv\", 2))\n            )\n        }\n\n        rule.withProject(testDependencies.server)\n            .clickGetBlankForm()\n            .clickClearAll()\n            .clickForm(\"One Question Entity Update\")\n            .clickGetSelected()\n            .clickOK(MainMenuPage())\n            .startBlankForm(\"One Question Entity Update\")\n            .assertText(\"Roman Roy\")\n            .pressBackAndDiscardForm()\n\n            .clickGetBlankForm()\n            .clickGetSelected() // Collect automatically only selects the un-downloaded forms\n            .clickOK(MainMenuPage())\n            .startBlankForm(\"One Question Entity Update\")\n            .assertText(\"Ro-Ro Roy\")\n            .assertTextDoesNotExist(\"Roman Roy\")\n    }\n\n    @Test\n    fun entityListFormsAllShowAsUpdatedTogether() {\n        testDependencies.server.apply {\n            addForm(\n                \"one-question-entity-update.xml\",\n                listOf(EntityListItem(\"people.csv\", \"people.csv\", 1))\n            )\n\n            addForm(\n                \"one-question-entity-follow-up.xml\",\n                listOf(EntityListItem(\"people.csv\", \"people.csv\", 1))\n            )\n        }\n\n        val mainMenuPage = rule.withProject(testDependencies.server.url, matchExactly = true)\n\n        testDependencies.server.apply {\n            removeForm(\"One Question Entity Update\")\n            removeForm(\"One Question Entity Follow Up\")\n\n            addForm(\n                \"one-question-entity-update.xml\",\n                listOf(EntityListItem(\"people.csv\", \"people.csv\", 2))\n            )\n\n            addForm(\n                \"one-question-entity-follow-up.xml\",\n                listOf(EntityListItem(\"people.csv\", \"people.csv\", 2))\n            )\n        }\n\n        mainMenuPage.clickFillBlankForm()\n            .assertTextBesides(equalTo(\"One Question Entity Update\"), containsString(\"Added on\"))\n            .assertTextBesides(equalTo(\"One Question Entity Follow Up\"), containsString(\"Added on\"))\n            .clickRefresh()\n            .assertTextBesides(equalTo(\"One Question Entity Update\"), containsString(\"Updated on\"))\n            .assertTextBesides(\n                equalTo(\"One Question Entity Follow Up\"),\n                containsString(\"Updated on\")\n            )\n    }\n\n    @Test\n    fun aLocallyCreatedEntity_thatIsDeletedOnTheServer_isNotAvailableToFollowUpForms() {\n        testDependencies.server.includeIntegrityUrl()\n        testDependencies.server.addForm(\"one-question-entity-registration-id.xml\")\n        testDependencies.server.addForm(\n            \"one-question-entity-update.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question Entity Registration\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"Name\", \"Logan Roy\"))\n\n            .also {\n                testDependencies.server.deleteEntity(\"Logan Roy\")\n            }\n\n            .clickFillBlankForm()\n            .clickRefresh()\n\n            .clickOnForm(\"One Question Entity Update\")\n            .assertQuestion(\"Select person\")\n            .assertText(\"Roman Roy\")\n            .assertTextDoesNotExist(\"Logan Roy\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt",
    "content": "package org.odk.collect.android.feature.formmanagement\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.SubmissionParser\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.AccessControlPage\nimport org.odk.collect.android.support.pages.EditSavedFormPage\nimport org.odk.collect.android.support.pages.FormEntryPage.QuestionAndAnswer\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.pages.SaveOrDiscardFormDialog\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.RecentAppsRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.strings.R.plurals\nimport org.odk.collect.strings.R.string\n\n@RunWith(AndroidJUnit4::class)\nclass BulkFinalizationTest {\n\n    private val testDependencies = TestDependencies()\n    private val recentAppsRule = RecentAppsRule()\n    private val rule = CollectTestRule(useDemoProject = false)\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(recentAppsRule)\n        .around(rule)\n\n    @Test\n    fun canBulkFinalizeDraftsInTheListOfDrafts() {\n        rule.withProject(\"http://example.com\")\n            .copyForm(\"one-question.xml\", \"example.com\")\n            .startBlankForm(\"One Question\")\n            .fillOutAndSave(QuestionAndAnswer(\"what is your age\", \"97\"))\n            .startBlankForm(\"One Question\")\n            .fillOutAndSave(QuestionAndAnswer(\"what is your age\", \"98\"))\n\n            .clickDrafts(2)\n            .clickFinalizeAll(2)\n            .clickFinalize()\n            .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_success, 2)\n            .assertTextDoesNotExist(\"One Question\")\n            .pressBack(MainMenuPage())\n\n            .assertNumberOfFinalizedForms(2)\n    }\n\n    @Test\n    fun canNotBulkFinalizeDraftsInTheListOfSentForms() {\n        rule.withProject(\"http://example.com\")\n            .copyForm(\"one-question.xml\", \"example.com\")\n            .startBlankForm(\"One Question\")\n            .fillOutAndSave(QuestionAndAnswer(\"what is your age\", \"97\"))\n            .clickViewSentForm(0)\n            .assertNoOptionsMenu()\n    }\n\n    @Test\n    fun whenThereAreDraftsWithConstraintViolations_marksFormsAsHavingErrors() {\n        rule.withProject(\"http://example.com\")\n            .copyForm(\"two-question-required.xml\", \"example.com\")\n            .startBlankForm(\"Two Question Required\")\n            .fillOut(QuestionAndAnswer(\"What is your name?\", \"Dan\"))\n            .pressBack(SaveOrDiscardFormDialog(MainMenuPage()))\n            .clickSaveChanges()\n\n            .startBlankForm(\"Two Question Required\")\n            .fillOutAndSave(\n                QuestionAndAnswer(\"What is your name?\", \"Tim\"),\n                QuestionAndAnswer(\"What is your age?\", \"45\", true)\n            )\n\n            .clickDrafts(2)\n            .clickFinalizeAll(2)\n            .clickFinalize()\n            .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_partial_success, 1, 1)\n            .assertText(\"Two Question Required\")\n            .pressBack(MainMenuPage())\n\n            .assertNumberOfEditableForms(1)\n            .assertNumberOfFinalizedForms(1)\n    }\n\n    @Test\n    fun whenADraftPreviouslyHadConstraintViolations_marksFormsAsHavingErrors() {\n        rule.withProject(\"http://example.com\")\n            .copyForm(\"two-question-required.xml\", \"example.com\")\n            .startBlankForm(\"Two Question Required\")\n            .fillOut(QuestionAndAnswer(\"What is your name?\", \"Dan\"))\n            .pressBack(SaveOrDiscardFormDialog(MainMenuPage()))\n            .clickSaveChanges()\n\n            .clickDrafts(1)\n            .clickFinalizeAll(1)\n            .clickFinalize()\n            .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1)\n\n            .clickOptionsIcon(string.finalize_all_drafts)\n            .clickOnString(string.finalize_all_drafts)\n            .clickOnTextInDialog(string.finalize, EditSavedFormPage())\n            .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1)\n    }\n\n    @Test\n    fun doesNotFinalizeInstancesWithSavePoints() {\n        rule.withProject(\"http://example.com\")\n            .copyForm(\"one-question.xml\", \"example.com\")\n            .startBlankForm(\"One Question\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n            .clickDrafts()\n            .clickOnForm(\"One Question\")\n            .killAndReopenApp(rule, recentAppsRule, MainMenuPage())\n\n            .clickDrafts()\n            .clickFinalizeAll(1)\n            .clickFinalize()\n            .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_unsupported, 0)\n            .assertText(\"One Question\")\n            .pressBack(MainMenuPage())\n\n            .assertNumberOfEditableForms(1)\n            .assertNumberOfFinalizedForms(0)\n    }\n\n    @Test\n    fun doesNotFinalizeInstancesFromEncryptedForms() {\n        rule.withProject(\"http://example.com\")\n            .copyForm(\"encrypted.xml\", \"example.com\")\n            .startBlankForm(\"encrypted\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n            .clickDrafts(1)\n            .clickFinalizeAll(1)\n            .clickFinalize()\n            .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_unsupported, 0)\n            .assertText(\"encrypted\")\n            .pressBack(MainMenuPage())\n\n            .assertNumberOfEditableForms(1)\n            .assertNumberOfFinalizedForms(0)\n    }\n\n    @Test\n    fun doesNotFinalizeAlreadyFinalizedInstances() {\n        rule.withProject(\"http://example.com\")\n            .copyForm(\"one-question.xml\", \"example.com\")\n            .startBlankForm(\"One Question\")\n            .fillOutAndSave(QuestionAndAnswer(\"what is your age\", \"97\"))\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(QuestionAndAnswer(\"what is your age\", \"98\"))\n\n            .clickDrafts(1)\n            .clickFinalizeAll(1)\n            .clickFinalize()\n            .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_success, 1)\n            .assertTextDoesNotExist(\"One Question\")\n            .pressBack(MainMenuPage())\n\n            .assertNumberOfFinalizedForms(2)\n    }\n\n    @Test\n    fun whenAutoSendIsEnabled_draftsAreSentAfterFinalizing() {\n        val mainMenuPage = rule.withProject(testDependencies.server.url)\n            .enableAutoSend(\n                testDependencies.scheduler,\n                string.wifi_cellular_autosend\n            )\n\n            .copyForm(\"one-question.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question\")\n            .fillOutAndSave(QuestionAndAnswer(\"what is your age\", \"97\"))\n\n            .clickDrafts(1)\n            .clickFinalizeAll(1)\n            .clickFinalize()\n            .pressBack(MainMenuPage())\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        mainMenuPage.clickViewSentForm(1)\n            .assertText(\"One Question\")\n\n        assertThat(testDependencies.server.submissions.size, equalTo(1))\n    }\n\n    @Test\n    fun whenDraftFormHasAutoSendEnabled_draftsAreSentAfterFinalizing() {\n        val mainMenuPage = rule.withProject(testDependencies.server.url)\n            .copyForm(\"one-question-autosend.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question Autosend\")\n            .fillOutAndSave(QuestionAndAnswer(\"what is your age\", \"97\"))\n\n            .clickDrafts(1)\n            .clickFinalizeAll(1)\n            .clickFinalize()\n            .pressBack(MainMenuPage())\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        mainMenuPage.clickViewSentForm(1)\n            .assertText(\"One Question Autosend\")\n\n        assertThat(testDependencies.server.submissions.size, equalTo(1))\n    }\n\n    @Test\n    fun canCancel() {\n        rule.withProject(\"http://example.com\")\n            .copyForm(\"one-question.xml\", \"example.com\")\n            .startBlankForm(\"One Question\")\n            .fillOutAndSave(QuestionAndAnswer(\"what is your age\", \"97\"))\n\n            .clickDrafts(1)\n            .clickFinalizeAll(1)\n            .clickCancel()\n            .assertText(\"One Question\")\n            .pressBack(MainMenuPage())\n\n            .assertNumberOfEditableForms(1)\n    }\n\n    @Test\n    fun canBeDisabled() {\n        rule.withProject(\"http://example.com\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickAccessControl()\n            .clickFormEntrySettings()\n            .clickOnString(string.finalize_all_drafts)\n            .pressBack(AccessControlPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            .copyForm(\"one-question.xml\", \"example.com\")\n            .startBlankForm(\"One Question\")\n            .fillOutAndSave(QuestionAndAnswer(\"what is your age\", \"1892\"))\n            .clickDrafts()\n            .assertNoOptionsMenu()\n    }\n\n    @Test\n    fun finalizingFinalizedEditViaBulkFinalize_savesFormWithCorrectInstanceIdAndDeprecatedId() {\n        rule.withProject(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question Editable\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .swipeToEndScreen(\"One Question Editable (Edit 1)\")\n            .clickSaveAsDraft()\n\n            .clickDrafts(1)\n            .clickFinalizeAll(1)\n            .clickFinalize()\n            .pressBack(MainMenuPage())\n\n            .clickSendFinalizedForm(2)\n            .clickSelectAll()\n            .clickSendSelected()\n\n        val (firstFormInstanceID, firstFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[0])\n        val (secondFormInstanceID, secondFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[1])\n\n        assertThat(firstFormDeprecatedID, equalTo(null))\n        assertThat(firstFormInstanceID, equalTo(secondFormDeprecatedID))\n        assertThat(secondFormInstanceID, not(firstFormInstanceID))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/DeleteBlankFormTest.java",
    "content": "package org.odk.collect.android.feature.formmanagement;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.TestDependencies;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\n@RunWith(AndroidJUnit4.class)\npublic class DeleteBlankFormTest {\n\n    public final TestDependencies testDependencies = new TestDependencies();\n    public final CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public final RuleChain chain = TestRuleChain.chain(testDependencies)\n            .around(rule);\n\n    @Test\n    public void deletingAForm_removesFormFromBlankFormList() {\n        rule.startAtMainMenu()\n                .copyForm(\"one-question.xml\")\n                .clickDeleteSavedForm()\n                .clickBlankForms()\n                .clickForm(\"One Question\")\n                .clickDeleteSelected(1)\n                .clickDeleteForms()\n                .assertTextDoesNotExist(\"One Question\")\n                .pressBack(new MainMenuPage())\n                .clickFillBlankForm()\n                .assertNoForms();\n    }\n\n    @Test\n    public void deletingAForm_whenThereFilledForms_removesFormFromBlankFormList_butAllowsEditingFilledForms() {\n        rule.startAtMainMenu()\n                .copyForm(\"one-question.xml\")\n                .startBlankForm(\"One Question\")\n                .answerQuestion(\"what is your age\", \"22\")\n                .swipeToEndScreen()\n                .clickSaveAsDraft()\n\n                .clickDeleteSavedForm()\n                .clickBlankForms()\n                .clickForm(\"One Question\")\n                .clickDeleteSelected(1)\n                .clickDeleteForms()\n                .pressBack(new MainMenuPage())\n                .clickFillBlankForm()\n                .assertNoForms()\n                .pressBack(new MainMenuPage())\n\n                .clickDrafts()\n                .clickOnForm(\"One Question\")\n                .clickOnQuestion(\"what is your age\")\n                .answerQuestion(\"what is your age\", \"30\")\n                .swipeToEndScreen()\n                .clickFinalize();\n    }\n\n    @Test\n    public void afterFillingAForm_andDeletingIt_allowsFormToBeReDownloaded() {\n        testDependencies.server.addForm(\"One Question\", \"one_question\", \"1\", \"one-question.xml\");\n\n        rule.startAtMainMenu()\n                .setServer(testDependencies.server.getUrl())\n                .clickGetBlankForm()\n                .clickGetSelected()\n                .assertMessage(\"All downloads succeeded!\")\n                .clickOKOnDialog(new MainMenuPage())\n                .startBlankForm(\"One Question\")\n                .answerQuestion(\"what is your age\", \"22\")\n                .swipeToEndScreen()\n                .clickFinalize()\n\n                .clickDeleteSavedForm()\n                .clickBlankForms()\n                .clickForm(\"One Question\")\n                .clickDeleteSelected(1)\n                .clickDeleteForms()\n                .pressBack(new MainMenuPage())\n\n                .clickGetBlankForm()\n                .clickGetSelected()\n                .assertMessage(\"All downloads succeeded!\")\n                .clickOKOnDialog(new MainMenuPage())\n                .clickFillBlankForm()\n                .assertFormExists(\"One Question\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/FormUpdateTest.kt",
    "content": "package org.odk.collect.android.feature.formmanagement\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.StubOpenRosaServer\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\n\n@RunWith(AndroidJUnit4::class)\nclass FormUpdateTest {\n\n    private val rule = CollectTestRule(false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    var chain: RuleChain = chain(testDependencies).around(rule)\n\n    @Test\n    fun updateOn_instead_addedOn_subtextShouldBeDisplayedAfterDownloadingNewAttachments() {\n        testDependencies.server.addForm(\n            \"One Question\",\n            \"one_question\",\n            \"1\",\n            \"one-question.xml\"\n        )\n\n        val mainMenuPage = rule.withProject(testDependencies.server.url)\n            .clickGetBlankForm()\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.newer_version_of_a_form_info)\n            .clickGetSelected()\n            .assertMessage(\"All downloads succeeded!\")\n            .clickOKOnDialog(MainMenuPage())\n            .clickFillBlankForm()\n            .assertTextThatContainsExists(\"Added on\")\n            .pressBack(MainMenuPage())\n            .clickDeleteSavedForm()\n            .clickBlankForms()\n            .assertTextThatContainsExists(\"Added on\")\n            .pressBack(MainMenuPage())\n\n        testDependencies.server.removeForm(\"One Question\")\n\n        testDependencies.server.addForm(\n            \"One Question\",\n            \"one_question\",\n            \"1\",\n            \"one-question.xml\",\n            listOf(\"fruits.csv\")\n        )\n\n        mainMenuPage.clickGetBlankForm()\n            .assertText(org.odk.collect.strings.R.string.newer_version_of_a_form_info)\n            .clickGetSelected()\n            .assertMessage(\"All downloads succeeded!\")\n            .clickOKOnDialog(MainMenuPage())\n            .clickFillBlankForm()\n            .assertTextThatContainsExists(\"Updated on\")\n            .pressBack(MainMenuPage())\n            .clickDeleteSavedForm()\n            .clickBlankForms()\n            .assertTextThatContainsExists(\"Updated on\")\n    }\n\n    @Test\n    fun addedOn_subtextShouldBeDisplayedAfterDownloadingNewFormVersionEvenIfThatFormHasNewAttachments() {\n        testDependencies.server.addForm(\n            \"One Question\",\n            \"one_question\",\n            \"1\",\n            \"one-question.xml\"\n        )\n\n        val mainMenuPage = rule.withProject(testDependencies.server.url)\n            .clickGetBlankForm()\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.newer_version_of_a_form_info)\n            .clickGetSelected()\n            .assertMessage(\"All downloads succeeded!\")\n            .clickOKOnDialog(MainMenuPage())\n            .clickFillBlankForm()\n            .assertTextThatContainsExists(\"Added on\")\n            .pressBack(MainMenuPage())\n            .clickDeleteSavedForm()\n            .clickBlankForms()\n            .assertTextThatContainsExists(\"Added on\")\n            .pressBack(MainMenuPage())\n\n        testDependencies.server.removeForm(\"One Question\")\n\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\",\n            listOf(\"fruits.csv\")\n        )\n\n        mainMenuPage.clickGetBlankForm()\n            .assertText(org.odk.collect.strings.R.string.newer_version_of_a_form_info)\n            .clickGetSelected()\n            .assertMessage(\"All downloads succeeded!\")\n            .clickOKOnDialog(MainMenuPage())\n            .clickFillBlankForm()\n            .assertTextThatContainsExists(\"Added on\")\n            .pressBack(MainMenuPage())\n            .clickDeleteSavedForm()\n            .clickBlankForms()\n            .assertTextThatContainsExists(\"Added on\")\n            .assertTextThatContainsExists(\"Added on\", 1)\n    }\n\n    @Test\n    fun whenAnUpdatedFormIsDownloaded_copyLastSavedFileFromPreviousFormVersion() {\n        testDependencies.server.addForm(\n            \"One Question Last Saved\",\n            \"one_question_last_saved\",\n            \"1\",\n            \"one-question-last-saved.xml\"\n        )\n\n        val mainMenuPage = rule.withProject(testDependencies.server.url)\n            .clickGetBlankForm()\n            .clickGetSelected()\n            .assertMessage(\"All downloads succeeded!\")\n            .clickOKOnDialog(MainMenuPage())\n            .startBlankForm(\"One Question Last Saved\")\n            .fillOutAndFinalize(\n                FormEntryPage.QuestionAndAnswer(\"what is your age\", \"32\")\n            )\n\n        testDependencies.server.removeForm(\"One Question Last Saved\")\n\n        testDependencies.server.addForm(\n            \"One Question Last Saved\",\n            \"one_question_last_saved\",\n            \"2\",\n            \"one-question-last-saved-updated.xml\"\n        )\n\n        mainMenuPage.clickGetBlankForm()\n            .assertText(org.odk.collect.strings.R.string.newer_version_of_a_form_info)\n            .clickGetSelected()\n            .assertMessage(\"All downloads succeeded!\")\n            .clickOKOnDialog(MainMenuPage())\n            .startBlankForm(\"One Question Last Saved\")\n            .assertText(\"32\")\n    }\n\n    @Test // https://github.com/getodk/collect/issues/6097\n    fun itIsPossibleToDownloadAnUpdate_afterDownloadingAndOpeningAFormWithBrokenExternalDataset() {\n        testDependencies.server.addForm(\n            \"external_select_csv.xml\",\n            listOf(StubOpenRosaServer.MediaFileItem(\"external_data.csv\", \"external_data_broken.csv\"))\n        )\n\n        val mainMenuPage = rule.withProject(testDependencies.server.url)\n            .clickGetBlankForm()\n            .clickGetSelected()\n            .clickOKOnDialog(MainMenuPage())\n            .startBlankFormWithError(\"external select\", true)\n            .clickOKOnDialog(MainMenuPage())\n\n        testDependencies.server.addForm(\n            \"external_select_csv.xml\",\n            listOf(StubOpenRosaServer.MediaFileItem(\"external_data.csv\"))\n        )\n\n        mainMenuPage.clickGetBlankForm()\n            .clickGetSelected()\n            .clickOKOnDialog(MainMenuPage())\n            .startBlankForm(\"external select\")\n            .assertTextDoesNotExist(\"Error Occurred\")\n            .assertTexts(\"One\", \"Two\", \"Three\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/FormsAdbTest.java",
    "content": "package org.odk.collect.android.feature.formmanagement;\n\nimport static org.junit.Assert.assertTrue;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.support.StorageUtils;\nimport org.odk.collect.android.support.TestDependencies;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\nimport java.io.File;\n\n@RunWith(AndroidJUnit4.class)\npublic class FormsAdbTest {\n\n    public final TestDependencies testDependencies = new TestDependencies();\n    public final CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public final RuleChain chain = TestRuleChain.chain(testDependencies)\n            .around(rule);\n\n    @Test\n    public void canUpdateAFormFromDisk() throws Exception {\n        MainMenuPage mainMenuPage = rule.startAtMainMenu()\n                .copyForm(\"one-question.xml\")\n                .clickFillBlankForm()\n                .assertFormExists(\"One Question\")\n                .pressBack(new MainMenuPage());\n\n        StorageUtils.copyFormToDemoProject(\"one-question-updated.xml\", \"one-question.xml\");\n\n        mainMenuPage\n                .clickFillBlankForm()\n                .assertFormExists(\"One Question Updated\")\n                .assertFormDoesNotExist(\"One Question\");\n    }\n\n    @Test\n    public void canUpdateFormOnDiskFromServer() throws Exception {\n        testDependencies.server.addForm(\"One Question Updated\", \"one_question\", \"2\", \"one-question-updated.xml\");\n\n        StorageUtils.copyFormToDemoProject(\"one-question.xml\");\n\n        rule.startAtMainMenu()\n                .setServer(testDependencies.server.getUrl())\n                .clickGetBlankForm()\n                .assertText(org.odk.collect.strings.R.string.newer_version_of_a_form_info)\n\n                .clickGetSelected()\n                .clickOKOnDialog(new MainMenuPage())\n                .clickFillBlankForm()\n                .assertFormExists(\"One Question Updated\")\n                .assertFormDoesNotExist(\"One Question\");\n    }\n\n    @Test\n    public void canDeleteFormFromDisk() {\n        MainMenuPage mainMenuPage = rule.startAtMainMenu()\n                .copyForm(\"one-question.xml\")\n                .clickFillBlankForm()\n                .assertFormExists(\"One Question\")\n                .pressBack(new MainMenuPage());\n\n        String formsDir = testDependencies.storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS);\n        boolean formDeleted = new File(formsDir, \"one-question.xml\").delete();\n        assertTrue(formDeleted);\n\n        mainMenuPage\n                .clickFillBlankForm()\n                .assertNoForms();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/GetBlankFormsTest.java",
    "content": "package org.odk.collect.android.feature.formmanagement;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.TestDependencies;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.android.support.pages.GetBlankFormPage;\n\n@RunWith(AndroidJUnit4.class)\npublic class GetBlankFormsTest {\n\n    public CollectTestRule rule = new CollectTestRule(false);\n\n    final TestDependencies testDependencies = new TestDependencies();\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain(testDependencies)\n            .around(rule);\n\n    @Test\n    public void whenThereIsAnAuthenticationErrorFetchingFormList_allowsUserToReenterCredentials() {\n        testDependencies.server.setCredentials(\"Draymond\", \"Green\");\n        testDependencies.server.addForm(\"One Question\", \"one-question\", \"1\", \"one-question.xml\");\n\n        rule.withProject(testDependencies.server.getUrl())\n                .clickGetBlankFormWithAuthenticationError()\n                .fillUsername(\"Draymond\")\n                .fillPassword(\"Green\")\n                .clickOK(new GetBlankFormPage())\n                .assertText(\"One Question\");\n    }\n\n    @Test\n    public void whenThereIsAnErrorFetchingFormList_showsError() {\n        testDependencies.server.alwaysReturnError();\n\n        rule.withProject(testDependencies.server.getUrl())\n                .clickGetBlankFormWithError()\n                .assertText(org.odk.collect.strings.R.string.load_remote_form_error)\n                .clickOK(new GetBlankFormPage());\n    }\n\n    @Test\n    public void whenThereIsAnErrorFetchingForms_showsError() {\n        testDependencies.server.addForm(\"One Question\", \"one-question\", \"1\", \"one-question.xml\");\n        testDependencies.server.errorOnFetchingForms();\n\n        rule.withProject(testDependencies.server.getUrl())\n                .clickGetBlankForm()\n                .clickGetSelected()\n                .assertMessage(\"1 of 1 downloads failed!\")\n                .showDetails()\n                .assertError(\"The server https://server.example.com returned status code 500. If you keep having this problem, report it to the person who asked you to collect data.\")\n                .navigateBack();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/HideOldVersionsTest.java",
    "content": "package org.odk.collect.android.feature.formmanagement;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.android.support.pages.ProjectSettingsPage;\nimport org.odk.collect.android.support.pages.MainMenuPage;\n\n@RunWith(AndroidJUnit4.class)\npublic class HideOldVersionsTest {\n\n    public final CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public final RuleChain chain = TestRuleChain.chain()\n            .around(rule);\n\n    @Test\n    public void whenHideOldVersionsEnabled_onlyTheNewestVersionOfAFormShowsInFormList() {\n        rule.startAtMainMenu()\n                .copyForm(\"one-question.xml\")\n                .clickFillBlankForm() // Sync forms on disk\n                .pressBack(new MainMenuPage())\n                .copyForm(\"one-question-updated.xml\")\n\n                .clickFillBlankForm()\n                .assertFormExists(\"One Question Updated\")\n                .assertFormDoesNotExist(\"One Question\");\n    }\n\n    @Test\n    public void whenHideOldVersionsDisabled_allVersionOfAFormShowsInFormList() {\n        rule.startAtMainMenu()\n                .openProjectSettingsDialog()\n                .clickSettings()\n                .clickFormManagement()\n                .scrollToRecyclerViewItemAndClickText(org.odk.collect.strings.R.string.hide_old_form_versions_setting_title)\n                .pressBack(new ProjectSettingsPage())\n                .pressBack(new MainMenuPage())\n\n                .copyForm(\"one-question.xml\")\n                .copyForm(\"one-question-updated.xml\")\n\n                .clickFillBlankForm()\n                .assertFormExists(\"One Question Updated\")\n                .assertFormExists(\"One Question\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/ManualUpdatesTest.java",
    "content": "package org.odk.collect.android.feature.formmanagement;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\n\n@RunWith(AndroidJUnit4.class)\npublic class ManualUpdatesTest {\n\n    public CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(rule);\n\n    @Test\n    public void whenManualUpdatesEnabled_getBlankFormsIsAvailable() {\n        rule.startAtMainMenu()\n                .enableManualUpdates()\n                .assertText(org.odk.collect.strings.R.string.get_forms);\n    }\n\n    @Test\n    public void whenManualUpdatesEnabled_fillBlankFormRefreshButtonIsGone() {\n        rule.startAtMainMenu()\n                .enableManualUpdates()\n                .clickFillBlankForm();\n\n        onView(withId(R.id.menu_refresh)).check(doesNotExist());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/MatchExactlyTest.kt",
    "content": "package org.odk.collect.android.feature.formmanagement\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.empty\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.ErrorPage\nimport org.odk.collect.android.support.pages.FillBlankFormPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.NotificationDrawerRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass MatchExactlyTest {\n    private val rule = CollectTestRule()\n    private val testDependencies = TestDependencies()\n    private val notificationDrawerRule = NotificationDrawerRule()\n\n    @get:Rule\n    var ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(notificationDrawerRule)\n        .around(rule)\n\n    @Test\n    fun whenMatchExactlyEnabled_clickingFillBlankForm_andClickingRefresh_getsLatestFormsFromServer() {\n        val page = rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .copyForm(\"one-question-repeat.xml\")\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n            .clickFillBlankForm()\n            .assertText(\"One Question\")\n            .assertText(\"One Question Repeat\")\n\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\"\n        )\n\n        testDependencies.server.addForm(\"Two Question\", \"two_question\", \"1\", \"two-question.xml\")\n\n        page.clickRefresh()\n            .assertText(\"Two Question\") // Check new form downloaded\n            .assertText(\"One Question Updated\") // Check updated form updated\n            .assertTextDoesNotExist(\"One Question Repeat\") // Check deleted form deleted\n    }\n\n    @Test\n    fun whenMatchExactlyEnabled_clickingFillBlankForm_andClickingRefresh_whenThereIsAnError_showsNotification_andClickingNotification_returnsToApp() {\n        testDependencies.server.alwaysReturnError()\n\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .copyForm(\"one-question-repeat.xml\")\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n            .clickFillBlankForm()\n            .clickRefreshWithError()\n\n        notificationDrawerRule\n            .open()\n            .assertNotification(\"ODK Collect\", \"Form update failed\", \"Demo project\")\n            .clickNotification(\n                \"ODK Collect\",\n                \"Form update failed\",\n                FillBlankFormPage()\n            ).pressBack(MainMenuPage())\n    }\n\n    @Test\n    fun whenMatchExactlyEnabled_clickingFillBlankForm_andClickingRefresh_whenThereIsAnError_showsNotification_andClickingShowDetails_showsErrorDetails() {\n        testDependencies.server.alwaysReturnError()\n\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .copyForm(\"one-question-repeat.xml\")\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n            .clickFillBlankForm()\n            .clickRefreshWithError()\n\n        notificationDrawerRule\n            .open()\n            .assertNotification(\"ODK Collect\", \"Form update failed\", \"Demo project\")\n            .clickAction(\n                \"ODK Collect\",\n                \"Form update failed\",\n                \"Show details\",\n                ErrorPage()\n            ).assertText(org.odk.collect.strings.R.string.form_update_error)\n            .assertText(\"Demo project\")\n            .assertText(\"The server https://server.example.com returned status code 500. If you keep having this problem, report it to the person who asked you to collect data.\")\n    }\n\n    @Test\n    fun whenMatchExactlyEnabled_clickingFillBlankForm_andClickingRefresh_whenThereIsAnAuthenticationError_promptsForCredentials() {\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\"\n        )\n\n        testDependencies.server.setCredentials(\"Klay\", \"Thompson\")\n\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .copyForm(\"one-question-repeat.xml\")\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n            .clickFillBlankForm()\n            .clickRefreshWithAuthError()\n            .fillUsername(\"Klay\")\n            .fillPassword(\"Thompson\")\n            .clickOK(FillBlankFormPage())\n            .clickRefresh()\n            .assertText(\"One Question Updated\")\n    }\n\n    @Test\n    fun whenMatchExactlyEnabled_getsLatestFormsFromServer_automaticallyAndRepeatedly() {\n        var page = rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .copyForm(\"one-question-repeat.xml\")\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\"\n        )\n\n        testDependencies.server.addForm(\"Two Question\", \"two_question\", \"1\", \"two-question.xml\")\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        page = page.clickFillBlankForm()\n            .assertText(\"Two Question\")\n            .assertText(\"One Question Updated\")\n            .assertTextDoesNotExist(\"One Question Repeat\")\n            .pressBack(MainMenuPage())\n\n        testDependencies.server.removeForm(\"Two Question\")\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        page.assertOnPage()\n            .clickFillBlankForm()\n            .assertText(\"One Question Updated\")\n            .assertTextDoesNotExist(\"Two Question\")\n    }\n\n    @Test\n    fun whenMatchExactlyEnabled_hidesGetBlankFormsAndDeleteBlankForms() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .copyForm(\"one-question-repeat.xml\")\n            .enableMatchExactly()\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.get_forms)\n            .clickDeleteSavedForm()\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.forms)\n    }\n\n    @Test\n    fun whenMatchExactlyDisabled_stopsSyncingAutomatically() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .copyForm(\"one-question-repeat.xml\")\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n            .enableManualUpdates()\n\n        assertThat(testDependencies.scheduler.getDeferredTasks(), `is`(empty()))\n    }\n\n    @Test\n    fun whenMatchExactlyEnabled_getsLatestFormsFromServerDuringFillingAFormWithoutEntities() {\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\"\n        )\n\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n            .startBlankForm(\"One Question\")\n            .also { testDependencies.scheduler.runDeferredTasks() }\n            .pressBackAndDiscardForm()\n            .clickFillBlankForm()\n            .assertFormExists(\"One Question Updated\")\n    }\n\n    @Test\n    fun whenMatchExactlyEnabled_doesNotGetLatestFormsFromServerDuringFillingAFormWithEntities() {\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\"\n        )\n\n        rule.startAtMainMenu()\n            .copyForm(\"one-question-entity-registration.xml\")\n            .copyForm(\"one-question.xml\")\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n            .startBlankForm(\"One Question Entity Registration\")\n            .also { testDependencies.scheduler.runDeferredTasks() }\n            .pressBackAndDiscardForm()\n            .clickFillBlankForm()\n            .assertFormDoesNotExist(\"One Question Updated\")\n    }\n\n    @Test\n    fun whenMatchExactlyStopped_andFails_showNotification_andOpenFormListOnClick() {\n        testDependencies.server.alwaysReturnError()\n\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n\n        testDependencies.scheduler.runDeferredTasks(isLastUniqueExecution = false, isStopped = true)\n\n        notificationDrawerRule\n            .open()\n            .assertNotification(\"ODK Collect\", \"Form update failed\", \"Demo project\")\n            .clickNotification(\n                \"ODK Collect\",\n                \"Form update failed\",\n                FillBlankFormPage()\n            )\n    }\n\n    @Test\n    fun whenMatchExactlyStopped_andSucceeds_showNotification_andOpenFormListOnClick() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .enableMatchExactly()\n\n        testDependencies.scheduler.runDeferredTasks(isLastUniqueExecution = false, isStopped = true)\n\n        notificationDrawerRule\n            .open()\n            .assertNoNotification(\"ODK Collect\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/PreviouslyDownloadedOnlyTest.kt",
    "content": "package org.odk.collect.android.feature.formmanagement\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.doesNotExist\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.R\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.ErrorPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.NotificationDrawerRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\nclass PreviouslyDownloadedOnlyTest {\n\n    private val testDependencies = TestDependencies()\n    private val notificationDrawerRule = NotificationDrawerRule()\n    private val rule = CollectTestRule(useDemoProject = false)\n\n    @get:Rule\n    var ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(notificationDrawerRule)\n        .around(rule)\n\n    @Test\n    fun whenPreviouslyDownloadedOnlyEnabled_notifiesOnFormUpdates_automaticallyAndRepeatedly() {\n        rule.withProject(testDependencies.server, \"one-question.xml\", \"two-question.xml\")\n            .setServer(testDependencies.server.url)\n            .enablePreviouslyDownloadedOnlyUpdates()\n\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\"\n        )\n        testDependencies.scheduler.runDeferredTasks()\n\n        notificationDrawerRule.open()\n            .assertNotification(\n                \"ODK Collect\",\n                \"Form updates available\",\n                testDependencies.server.hostName\n            )\n            .clearAll()\n\n        testDependencies.server.addForm(\n            \"Two Question Updated\",\n            \"two_question\",\n            \"1\",\n            \"two-question-updated.xml\"\n        )\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        notificationDrawerRule.open()\n            .assertNotification(\n                \"ODK Collect\",\n                \"Form updates available\",\n                testDependencies.server.hostName\n            )\n            .clickNotification(\n                \"ODK Collect\",\n                \"Form updates available\",\n                MainMenuPage()\n            )\n    }\n\n    @Test\n    fun whenPreviouslyDownloadedOnlyEnabledWithAutomaticDownload_checkingAutoDownload_downloadsUpdatedForms_andDisplaysNotification() {\n        rule.withProject(testDependencies.server, \"one-question.xml\")\n            .enablePreviouslyDownloadedOnlyUpdatesWithAutomaticDownload()\n\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\"\n        )\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        notificationDrawerRule.open()\n            .assertNotification(\n                \"ODK Collect\",\n                \"Forms download succeeded\",\n                \"All downloads succeeded!\"\n            )\n            .clickNotification(\n                \"ODK Collect\",\n                \"Forms download succeeded\",\n                MainMenuPage()\n            ).clickFillBlankForm()\n            .assertText(\"One Question Updated\")\n    }\n\n    @Test\n    fun whenPreviouslyDownloadedOnlyEnabledWithAutomaticDownload_checkingAutoDownload_downloadsUpdatedForms_andDisplaysNotificationWhenFails() {\n        testDependencies.server.errorOnFetchingForms()\n\n        val mainMenuPage =\n            rule.withProject(testDependencies.server, \"one-question.xml\")\n                .enablePreviouslyDownloadedOnlyUpdatesWithAutomaticDownload()\n\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\"\n        )\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        mainMenuPage.clickFillBlankForm()\n            .assertFormDoesNotExist(\"One Question Updated\")\n\n        notificationDrawerRule.open()\n            .assertNotification(\n                \"ODK Collect\",\n                \"Forms download failed\",\n                \"1 of 1 downloads failed!\"\n            )\n            .clickAction(\n                \"ODK Collect\",\n                \"Forms download failed\",\n                \"Show details\",\n                ErrorPage()\n            )\n    }\n\n    @Test\n    fun whenPreviouslyDownloadedOnlyEnabled_getBlankFormsIsAvailable() {\n        rule.withProject(testDependencies.server.url)\n            .enablePreviouslyDownloadedOnlyUpdates()\n            .assertText(org.odk.collect.strings.R.string.get_forms)\n    }\n\n    @Test\n    fun whenPreviouslyDownloadedOnlyEnabled_fillBlankFormRefreshButtonIsGone() {\n        rule.withProject(testDependencies.server.url)\n            .enablePreviouslyDownloadedOnlyUpdates()\n            .clickFillBlankForm()\n        onView(withId(R.id.menu_refresh)).check(doesNotExist())\n    }\n\n    @Test\n    fun whenPreviouslyDownloadedOnlyDisabled_stopsCheckingForUpdates() {\n        rule.withProject(testDependencies.server.url)\n            .enablePreviouslyDownloadedOnlyUpdates()\n            .enableManualUpdates()\n\n        assertThat(testDependencies.scheduler.getDeferredTasks(), equalTo(emptyList()))\n    }\n\n    @Test\n    fun whenPreviouslyDownloadedOnlyEnabledWithAutomaticDownload_getsLatestFormsFromServerDuringFillingAFormWithoutEntities() {\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\"\n        )\n\n        rule.withProject(testDependencies.server, \"one-question.xml\")\n            .enablePreviouslyDownloadedOnlyUpdatesWithAutomaticDownload()\n            .startBlankForm(\"One Question\")\n            .also { testDependencies.scheduler.runDeferredTasks() }\n            .pressBackAndDiscardForm()\n            .clickFillBlankForm()\n            .assertFormExists(\"One Question Updated\")\n    }\n\n    @Test\n    fun whenPreviouslyDownloadedOnlyEnabledWithAutomaticDownload_doesNotGetLatestFormsFromServerDuringFillingAFormWithEntities() {\n        testDependencies.server.addForm(\n            \"One Question Updated\",\n            \"one_question\",\n            \"2\",\n            \"one-question-updated.xml\"\n        )\n\n        rule.withProject(testDependencies.server, \"one-question-entity-registration.xml\", \"one-question.xml\")\n            .enablePreviouslyDownloadedOnlyUpdatesWithAutomaticDownload()\n            .startBlankForm(\"One Question Entity Registration\")\n            .also { testDependencies.scheduler.runDeferredTasks() }\n            .pressBackAndDiscardForm()\n            .clickFillBlankForm()\n            .assertFormDoesNotExist(\"One Question Updated\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/AutoSendTest.kt",
    "content": "package org.odk.collect.android.feature.instancemanagement\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.xform.parse.XFormParser\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.kxml2.kdom.Element\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.ErrorPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.SendFinalizedFormPage\nimport org.odk.collect.android.support.pages.ViewSentFormPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.NotificationDrawerRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.strings.R\n\n@RunWith(AndroidJUnit4::class)\nclass AutoSendTest {\n    private val rule = CollectTestRule()\n    private val testDependencies = TestDependencies()\n    private val notificationDrawerRule = NotificationDrawerRule()\n\n    @get:Rule\n    var ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(notificationDrawerRule)\n        .around(rule)\n\n    @Test\n    fun whenAutoSendEnabled_fillingAndFinalizingForm_sendsFormAndNotifiesUser() {\n        val mainMenuPage = rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .enableAutoSend(\n                testDependencies.scheduler,\n                R.string.wifi_cellular_autosend\n            )\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .inputText(\"31\")\n            .swipeToEndScreen()\n            .clickSend()\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        mainMenuPage\n            .clickViewSentForm(1)\n            .assertText(\"One Question\")\n\n        notificationDrawerRule\n            .open()\n            .assertNotification(\"ODK Collect\", \"Forms upload succeeded\", \"All uploads succeeded!\")\n            .clickNotification(\n                \"ODK Collect\",\n                \"Forms upload succeeded\",\n                ViewSentFormPage()\n            ).pressBack(MainMenuPage())\n    }\n\n    @Test\n    fun whenAutoSendEnabled_fillingAndFinalizingForm_notifiesUserWhenSendingFails() {\n        testDependencies.server.alwaysReturnError()\n\n        val mainMenuPage = rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .enableAutoSend(\n                testDependencies.scheduler,\n                R.string.wifi_cellular_autosend\n            )\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .inputText(\"31\")\n            .swipeToEndScreen()\n            .clickSend()\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        mainMenuPage.clickViewSentForm(1)\n            .assertText(\"One Question\")\n\n        notificationDrawerRule\n            .open()\n            .assertNotification(\"ODK Collect\", \"Forms upload failed\", \"1 of 1 uploads failed!\")\n            .clickAction(\n                \"ODK Collect\",\n                \"Forms upload failed\",\n                \"Show details\",\n                ErrorPage()\n            )\n    }\n\n    @Test\n    fun whenFormHasAutoSend_fillingAndFinalizingForm_sendsFormAndNotifiesUser_regardlessOfSetting() {\n        val mainMenuPage = rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .enableAutoSend(\n                testDependencies.scheduler,\n                R.string.wifi_autosend\n            )\n            .copyForm(\"one-question-autosend.xml\")\n            .startBlankForm(\"One Question Autosend\")\n            .inputText(\"31\")\n            .swipeToEndScreen()\n            .clickSend()\n\n        testDependencies.networkStateProvider.goOnline(Scheduler.NetworkType.CELLULAR)\n        testDependencies.scheduler.runDeferredTasks()\n\n        mainMenuPage\n            .clickViewSentForm(1)\n            .assertText(\"One Question Autosend\")\n\n        notificationDrawerRule\n            .open()\n            .assertNotification(\"ODK Collect\", \"Forms upload succeeded\", \"All uploads succeeded!\")\n            .clickNotification(\n                \"ODK Collect\",\n                \"Forms upload succeeded\",\n                ViewSentFormPage()\n            ).pressBack(MainMenuPage())\n    }\n\n    @Test\n    fun whenFormHasAutoSend_canAutoSendMultipleForms() {\n        val mainMenuPage = rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-autosend.xml\")\n\n            .startBlankForm(\"One Question Autosend\")\n            .inputText(\"31\")\n            .swipeToEndScreen()\n            .clickSend()\n\n            .startBlankForm(\"One Question Autosend\")\n            .inputText(\"32\")\n            .swipeToEndScreen()\n            .clickSend()\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        mainMenuPage\n            .clickViewSentForm(2)\n    }\n\n    @Test\n    fun whenFormHasAutoSend_fillingAndFinalizingForm_notifiesUserWhenSendingFails_regardlessOfSetting() {\n        testDependencies.server.alwaysReturnError()\n\n        val mainMenuPage = rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .enableAutoSend(\n                testDependencies.scheduler,\n                R.string.wifi_autosend\n            )\n            .copyForm(\"one-question-autosend.xml\")\n            .startBlankForm(\"One Question Autosend\")\n            .inputText(\"31\")\n            .swipeToEndScreen()\n            .clickSend()\n\n        testDependencies.networkStateProvider.goOnline(Scheduler.NetworkType.CELLULAR)\n        testDependencies.scheduler.runDeferredTasks()\n\n        mainMenuPage.clickViewSentForm(1)\n            .assertText(\"One Question Autosend\")\n\n        notificationDrawerRule\n            .open()\n            .assertNotification(\"ODK Collect\", \"Forms upload failed\", \"1 of 1 uploads failed!\")\n            .clickAction(\n                \"ODK Collect\",\n                \"Forms upload failed\",\n                \"Show details\",\n                ErrorPage()\n            )\n    }\n\n    @Test\n    fun whenFormHasAutoSendDisabled_fillingAndFinalizingForm_doesNotSendForm_regardlessOfSetting() {\n        val mainMenuPage = rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .enableAutoSend(\n                testDependencies.scheduler,\n                R.string.wifi_cellular_autosend\n            )\n            .copyForm(\"one-question-autosend-disabled.xml\")\n            .startBlankForm(\"One Question Autosend Disabled\")\n            .inputText(\"31\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n        testDependencies.scheduler.runDeferredTasks()\n        mainMenuPage.assertNumberOfFinalizedForms(1)\n    }\n\n    @Test\n    fun whenAutoSendDisabled_fillingAndFinalizingForm_doesNotSendFormAutomatically() {\n        val mainMenuPage = rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"what is your age\", \"31\"))\n\n        testDependencies.scheduler.runDeferredTasks()\n\n        mainMenuPage.assertNumberOfFinalizedForms(1)\n    }\n\n    @Test\n    fun whenFormHasAutoSend_formsAreSentInOldestFirstOrderBasedOnFinalizationTime() {\n        /**\n         * Historically, the only timestamp we tracked for instances was the \"last status changed\" date.\n         * However, this timestamp is updated any time the instance status changes—not only when a form\n         * is finalized, but also, for example, when a submission attempt fails.\n         *\n         * This could lead to incorrect ordering when sending finalized forms. For instance, if forms A and B\n         * were finalized in that order, and submission of form A failed, its \"last status changed\" timestamp\n         * would be updated. As a result, when attempting to send both forms later, form B could be sent first,\n         * even though form A was finalized earlier.\n         *\n         * To ensure that forms are always sent in the order they were finalized, we introduced a new timestamp\n         * to track the finalization time specifically.\n         *\n         * This test reproduces the scenario described above to verify that the new finalization timestamp is used\n         * for ordering. It deliberately updates the \"last status changed\" date of the older instance\n         * to confirm that it does not affect the sending order.\n         */\n        testDependencies.server.alwaysReturnError()\n\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-autosend.xml\")\n\n            .startBlankForm(\"One Question Autosend\")\n            .inputText(\"31\")\n            .swipeToEndScreen()\n            .clickSend()\n\n            .startBlankForm(\"One Question Autosend\")\n            .inputText(\"32\")\n            .swipeToEndScreen()\n            .clickSend()\n\n            .clickSendFinalizedForm(2)\n            .sortByDateOldestFirst()\n            .selectForm(0)\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .also {\n                testDependencies.server.neverReturnError()\n                testDependencies.scheduler.runDeferredTasks()\n            }\n\n        val firstFormRootElement = XFormParser.getXMLDocument(testDependencies.server.submissions[0].inputStream().reader()).rootElement\n        val secondFormRootElement = XFormParser.getXMLDocument(testDependencies.server.submissions[1].inputStream().reader()).rootElement\n\n        assertThat((firstFormRootElement.getChild(0) as Element).getChild(0), equalTo(\"31\"))\n        assertThat((secondFormRootElement.getChild(0) as Element).getChild(0), equalTo(\"32\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/DeleteSavedFormTest.kt",
    "content": "package org.odk.collect.android.feature.instancemanagement\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.DeleteSavedFormPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.SendFinalizedFormPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.strings.R.string\n\n@RunWith(AndroidJUnit4::class)\nclass DeleteSavedFormTest {\n\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val chain: RuleChain = chain(testDependencies).around(rule)\n\n    @Test\n    fun deletingAForm_removesFormFromFinalizedForms() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .answerQuestion(\"what is your age\", \"30\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickDeleteSavedForm()\n            .clickForm(\"One Question\")\n            .clickDeleteSelected(1)\n            .clickDeleteForms()\n            .checkIsSnackbarWithMessageDisplayed(string.file_deleted_ok, 1)\n            .assertTextDoesNotExist(\"One Question\")\n            .pressBack(MainMenuPage())\n            .assertNumberOfFinalizedForms(0)\n    }\n\n    @Test\n    fun whenFinalizedButNotSentFormHasCreatedALocalEntity_doesNotAppearInListToDelete() {\n        testDependencies.server.addForm(\"one-question-entity-registration.xml\")\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            // Drafts can be deleted\n            .startBlankForm(\"One Question Entity Registration\")\n            .fillOutAndSave(FormEntryPage.QuestionAndAnswer(\"Name\", \"Logan Roy\"))\n            .clickDeleteSavedForm()\n            .assertText(\"One Question Entity Registration\")\n            .pressBack(MainMenuPage())\n\n            // Finalized forms can not be deleted\n            .clickDrafts(1)\n            .clickOnForm(\"One Question Entity Registration\")\n            .clickGoToEnd()\n            .clickFinalize()\n            .clickDeleteSavedForm()\n            .assertTextDoesNotExist(\"One Question Entity Registration\")\n            .pressBack(MainMenuPage())\n\n            // Sent forms can be deleted\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .pressBack(MainMenuPage())\n            .clickDeleteSavedForm()\n            .assertText(\"One Question Entity Registration\")\n            .pressBack(MainMenuPage())\n    }\n\n    @Test\n    fun accessingSortMenuInDeleteSavedInstancesShouldNotCrashTheAppAfterRotatingTheDevice() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .answerQuestion(\"what is your age\", \"30\")\n            .swipeToEndScreen()\n            .clickFinalize()\n            .clickDeleteSavedForm()\n            .rotateToLandscape(DeleteSavedFormPage())\n            .clickOnId(R.id.menu_sort)\n            .assertText(string.sort_by)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/EditSavedFormTest.kt",
    "content": "package org.odk.collect.android.feature.instancemanagement\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.javarosa.xform.parse.XFormParser\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.SubmissionParser\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormHierarchyPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.SendFinalizedFormPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.PageComposeRule\nimport org.odk.collect.android.support.rules.RecentAppsRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass EditSavedFormTest {\n\n    private val rule = CollectTestRule()\n\n    val testDependencies: TestDependencies = TestDependencies()\n    private val recentAppsRule = RecentAppsRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = chain(testDependencies)\n        .around(recentAppsRule)\n        .around(rule)\n\n    @Test\n    fun sentFormIsNotAvailableForEditsInTheListOfDrafts() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .pressBack(MainMenuPage())\n\n            .assertNumberOfEditableForms(0)\n            .clickDrafts()\n            .assertTextDoesNotExist(\"One Question\")\n\n            // Tests that search doesn't change visibility. Move down to lower testing level.\n            // (possibly when replacing CursorLoader)\n            .clickMenuFilter()\n            .searchInBar(\"One Question\".substring(0, 1))\n            .assertTextDoesNotExist(\"One Question\")\n    }\n\n    @Test\n    fun failedToSendFormIsNotAvailableForEditsInTheListOfDrafts() {\n        testDependencies.server.alwaysReturnError()\n\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .pressBack(MainMenuPage())\n\n            .assertNumberOfEditableForms(0)\n            .clickDrafts()\n            .assertTextDoesNotExist(\"One Question\")\n\n            // Tests that search doesn't change visibility. Move down to lower testing level\n            // (possibly when replacing CursorLoader)\n            .clickMenuFilter()\n            .searchInBar(\"One Question\".substring(0, 1))\n            .assertTextDoesNotExist(\"One Question\")\n    }\n\n    @Test\n    fun formsThatDoNotOptInToBeingEditableCannotBeEdited() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question\")\n            .assertNonEditableForm()\n    }\n\n    @Test\n    fun editingAFinalizedForm_createsANewFormAndKeepsTheOriginalOneIntact() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\")\n            .startBlankForm(\"One Question Editable\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .answerQuestion(\"what is your age\", \"456\")\n            .swipeToEndScreen(\"One Question Editable (Edit 1)\")\n            .clickFinalize()\n\n            .clickSendFinalizedForm(2)\n            .clickSelectAll()\n            .clickSendSelected()\n\n        val originalAnswer = getAnswer(testDependencies.server.submissions[0], \"age\")\n        val updatedAnswer = getAnswer(testDependencies.server.submissions[1], \"age\")\n\n        assertThat(originalAnswer, equalTo(\"123\"))\n        assertThat(updatedAnswer, equalTo(\"456\"))\n\n        val (firstFormInstanceID, firstFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[0])\n        val (secondFormInstanceID, secondFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[1])\n\n        assertThat(firstFormDeprecatedID, equalTo(null))\n        assertThat(firstFormInstanceID, equalTo(secondFormDeprecatedID))\n        assertThat(secondFormInstanceID, not(firstFormInstanceID))\n    }\n\n    @Test\n    fun editingASentForm_createsANewFormAndKeepsTheOriginalOneIntact() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\")\n            .startBlankForm(\"One Question Editable\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .pressBack(MainMenuPage())\n\n            .clickViewSentForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .answerQuestion(\"what is your age\", \"456\")\n            .swipeToEndScreen(\"One Question Editable (Edit 1)\")\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelected()\n\n        val originalAnswer = getAnswer(testDependencies.server.submissions[0], \"age\")\n        val updatedAnswer = getAnswer(testDependencies.server.submissions[1], \"age\")\n\n        assertThat(originalAnswer, equalTo(\"123\"))\n        assertThat(updatedAnswer, equalTo(\"456\"))\n\n        val (firstFormInstanceID, firstFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[0])\n        val (secondFormInstanceID, secondFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[1])\n\n        assertThat(firstFormDeprecatedID, equalTo(null))\n        assertThat(firstFormInstanceID, equalTo(secondFormDeprecatedID))\n        assertThat(secondFormInstanceID, not(firstFormInstanceID))\n    }\n\n    @Test\n    fun editingAFinalizedForm_createsANewFormThatCanBeDiscarded() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\")\n            .startBlankForm(\"One Question Editable\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .pressBackAndDiscardForm()\n            .clickDrafts(0)\n    }\n\n    @Test\n    fun killingAppWhenEditingFinalizedForm_createsSavepointForFormRecovery() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\")\n            .startBlankForm(\"One Question Editable\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .killAndReopenApp(rule, recentAppsRule, MainMenuPage(), testDependencies)\n\n            .clickDrafts(1)\n            .clickOnFormWithSavepoint(\"One Question Editable (Edit 1)\")\n            .clickRecover(FormHierarchyPage(\"One Question Editable\"))\n            .closeSnackbar()\n            .clickGoToEnd(\"One Question Editable (Edit 1)\")\n            .clickFinalize()\n\n            .clickSendFinalizedForm(2)\n            .clickSelectAll()\n            .clickSendSelected()\n\n        val (firstFormInstanceID, firstFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[0])\n        val (secondFormInstanceID, secondFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[1])\n\n        assertThat(firstFormDeprecatedID, equalTo(null))\n        assertThat(firstFormInstanceID, equalTo(secondFormDeprecatedID))\n        assertThat(secondFormInstanceID, not(firstFormInstanceID))\n    }\n\n    @Test\n    fun editingAnEditedForm_updatesInstanceIdAndDeprecatedId() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\")\n            .startBlankForm(\"One Question Editable\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .pressBackAndSaveAsDraft()\n\n            .clickDrafts(1)\n            .clickOnForm(\"One Question Editable\", \"One Question Editable (Edit 1)\")\n            .clickGoToEnd(\"One Question Editable (Edit 1)\")\n            .clickFinalize()\n\n            .clickSendFinalizedForm(2)\n            .clickOnForm(\"One Question Editable\", \"One Question Editable (Edit 1)\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .swipeToEndScreen(\"One Question Editable (Edit 2)\")\n            .clickFinalize()\n\n            .clickSendFinalizedForm(3)\n            .clickSelectAll()\n            .clickSendSelected()\n\n        val (firstFormInstanceID, firstFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[0])\n        val (secondFormInstanceID, secondFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[1])\n        val (thirdFormInstanceID, thirdFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[2])\n\n        assertThat(firstFormDeprecatedID, equalTo(null))\n        assertThat(firstFormInstanceID, equalTo(secondFormDeprecatedID))\n        assertThat(secondFormInstanceID, not(firstFormInstanceID))\n        assertThat(secondFormInstanceID, equalTo(thirdFormDeprecatedID))\n        assertThat(thirdFormInstanceID, not(secondFormInstanceID))\n    }\n\n    @Test\n    fun editingFinalizedForm_whenNewerDraftEditExists_promptsToOpenNewerEditInstead() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\")\n            .startBlankForm(\"One Question Editable\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .answerQuestion(\"what is your age\", \"456\")\n            .pressBackAndSaveAsDraft()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editFormWithError()\n            .acceptEditingNewerDraftEdit(\"One Question Editable\")\n            .assertAnswer(\"456\")\n    }\n\n    @Test\n    fun editingFinalizedForm_whenNewerDraftEditExists_andPromptIsDeclined_staysInCurrentForm() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\")\n            .startBlankForm(\"One Question Editable\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .answerQuestion(\"what is your age\", \"456\")\n            .pressBackAndSaveAsDraft()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editFormWithError()\n            .discardEditingNewerEdit()\n            .assertAnswer(\"123\")\n    }\n\n    @Test\n    fun editingFinalizedForm_whenNewerFinalizedEditExists_promptsToOpenNewerEditInstead() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\")\n            .startBlankForm(\"One Question Editable\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .answerQuestion(\"what is your age\", \"456\")\n            .swipeToEndScreen(\"One Question Editable (Edit 1)\")\n            .clickFinalize()\n\n            .clickSendFinalizedForm(2)\n            .clickOnForm(\"One Question Editable\")\n            .editFormWithError()\n            .acceptEditingNewerFinalizedEdit(\"One Question Editable\")\n            .assertAnswer(\"456\")\n    }\n\n    @Test\n    fun editingFinalizedForm_whenNewerFinalizedEditExists_andPromptIsDeclined_staysInCurrentForm() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\")\n            .startBlankForm(\"One Question Editable\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .answerQuestion(\"what is your age\", \"456\")\n            .swipeToEndScreen(\"One Question Editable (Edit 1)\")\n            .clickFinalize()\n\n            .clickSendFinalizedForm(2)\n            .clickOnForm(\"One Question Editable\")\n            .editFormWithError()\n            .discardEditingNewerEdit()\n            .assertAnswer(\"123\")\n    }\n\n    @Test\n    fun finalizingFinalizedEditInFormEntry_savesFormWithCorrectInstanceIdAndDeprecatedId() {\n        rule.startAtMainMenu()\n            .setServer(testDependencies.server.url)\n            .copyForm(\"one-question-editable.xml\")\n            .startBlankForm(\"One Question Editable\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question Editable\")\n            .editForm(\"One Question Editable\")\n            .clickOnQuestion(\"what is your age\")\n            .answerQuestion(\"what is your age\", \"456\")\n            .swipeToEndScreen(\"One Question Editable (Edit 1)\")\n            .clickSaveAsDraft()\n\n            .clickDrafts(1)\n            .clickOnForm(\"One Question Editable\", \"One Question Editable (Edit 1)\")\n            .clickGoToEnd(\"One Question Editable (Edit 1)\")\n            .clickFinalize()\n\n            .clickSendFinalizedForm(2)\n            .clickSelectAll()\n            .clickSendSelected()\n\n        val originalAnswer = getAnswer(testDependencies.server.submissions[0], \"age\")\n        val updatedAnswer = getAnswer(testDependencies.server.submissions[1], \"age\")\n\n        assertThat(originalAnswer, equalTo(\"123\"))\n        assertThat(updatedAnswer, equalTo(\"456\"))\n\n        val (firstFormInstanceID, firstFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[0])\n        val (secondFormInstanceID, secondFormDeprecatedID) = SubmissionParser.getMetaIds(testDependencies.server.submissions[1])\n\n        assertThat(firstFormDeprecatedID, equalTo(null))\n        assertThat(firstFormInstanceID, equalTo(secondFormDeprecatedID))\n        assertThat(secondFormInstanceID, not(firstFormInstanceID))\n    }\n\n    private fun getAnswer(file: File, questionName: String): String? {\n        val formRootElement = XFormParser.getXMLDocument(file.inputStream().reader()).rootElement\n\n        return formRootElement.getElement(null, questionName).getText(0)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/InstancesAdbTest.kt",
    "content": "package org.odk.collect.android.feature.instancemanagement\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Assert\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.storage.StorageSubdirectory\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass InstancesAdbTest {\n\n    private val testDependencies = TestDependencies()\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain(testDependencies).around(rule)\n\n    @Test\n    fun canAddInstanceOnDisk() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .copyInstance(\"One Question_2021-06-22_15-55-50.xml\")\n            .clickFillBlankForm() // Add form via disk sync\n            .pressBack(MainMenuPage()) // Return to main menu to trigger instance disk sync\n            .clickSendFinalizedForm(1)\n            .assertText(\"One Question\")\n    }\n\n    @Test\n    fun deletingInstanceOnDisk_andThenOpeningInstance_showsWarning_andRemovesInstance() {\n        val mainMenuPage = rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n\n        val instancesDir =\n            testDependencies.storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES)\n        val instanceDeleted = File(instancesDir).listFiles()[0].deleteRecursively()\n        Assert.assertTrue(instanceDeleted)\n\n        mainMenuPage\n            .clickDrafts(1)\n            .clickOnFormWithDialog(\"One Question\")\n            .assertText(org.odk.collect.strings.R.string.instance_deleted_message)\n            .clickOK(MainMenuPage())\n            .assertNumberOfEditableForms(0)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/PartialSubmissionTest.kt",
    "content": "package org.odk.collect.android.feature.instancemanagement\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.xform.parse.XFormParser\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass PartialSubmissionTest {\n\n    private val testDependencies = TestDependencies()\n    private val rule = CollectTestRule(useDemoProject = false)\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun canFillAndSubmitAFormWithPartialSubmission() {\n        rule.withProject(testDependencies.server.url)\n            .copyForm(\"one-question-partial.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"what is your age\", \"123\"))\n\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelected()\n\n        val submissions = testDependencies.server.submissions\n        assertThat(submissions.size, equalTo(1))\n\n        val root = XFormParser.getXMLDocument(submissions[0].inputStream().reader()).rootElement\n        assertThat(root.name, equalTo(\"age\"))\n        assertThat(root.childCount, equalTo(1))\n        assertThat(root.getChild(0), equalTo(\"123\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt",
    "content": "package org.odk.collect.android.feature.instancemanagement\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.xform.parse.XFormParser\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.kxml2.kdom.Element\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.FormEntryPage.QuestionAndAnswer\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.OkDialog\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.pages.SendFinalizedFormPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.PageComposeRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.androidtest.RecordedIntentsRule\n\n@RunWith(AndroidJUnit4::class)\nclass SendFinalizedFormTest {\n\n    private val testDependencies = TestDependencies()\n\n    private val rule = CollectTestRule(useDemoProject = false)\n\n    @get:Rule\n    val chain: RuleChain = chain(testDependencies)\n        .around(RecordedIntentsRule())\n        .around(rule)\n\n    @Test\n    fun canViewFormsBeforeSending() {\n        rule.withProject(testDependencies.server.url)\n            .copyForm(\"one-question.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(QuestionAndAnswer(\"what is your age\", \"52\"))\n            .clickSendFinalizedForm(1)\n            .clickOnForm(\"One Question\")\n            .assertAnswer(\"52\")\n    }\n\n    @Test\n    fun whenThereIsAnAuthenticationError_allowsUserToReenterCredentials() {\n        testDependencies.server.setCredentials(\"Draymond\", \"Green\")\n        rule.withProject(testDependencies.server.url)\n            .copyForm(\"one-question.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelectedWithAuthenticationError()\n            .fillUsername(\"Draymond\")\n            .fillPassword(\"Green\")\n            .clickOK(OkDialog())\n            .assertText(\"One Question - Success\")\n    }\n\n    @Test\n    fun canViewSentForms() {\n        rule.withProject(testDependencies.server.url)\n            .copyForm(\"one-question.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .pressBack(MainMenuPage())\n            .clickViewSentForm(1)\n            .clickOnForm(\"One Question\")\n            .assertAnswer(\"123\")\n            .assertText(org.odk.collect.strings.R.string.exit)\n    }\n\n    @Test\n    fun canSendIndividualForms() {\n        rule.withProject(testDependencies.server.url)\n            .copyForm(\"one-question.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(QuestionAndAnswer(\"what is your age\", \"123\"))\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(QuestionAndAnswer(\"what is your age\", \"124\"))\n\n            .clickSendFinalizedForm(2)\n            .selectForm(0)\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .pressBack(MainMenuPage())\n\n            .assertNumberOfFinalizedForms(1)\n            .clickViewSentForm(1)\n            .clickOnForm(\"One Question\")\n            .assertAnswer(\"123\")\n    }\n\n    @Test\n    fun whenDeleteAfterSendIsEnabled_deletesFilledForm() {\n        rule.withProject(testDependencies.server.url)\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickFormManagement()\n            .scrollToRecyclerViewItemAndClickText(org.odk.collect.strings.R.string.delete_after_send)\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .copyForm(\"one-question.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question\")\n            .answerQuestion(\"what is your age\", \"123\")\n            .swipeToEndScreen()\n            .clickFinalize()\n            .clickSendFinalizedForm(1)\n            .clickSelectAll()\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .pressBack(MainMenuPage())\n            .clickViewSentForm(1)\n            .clickOnText(\"One Question\")\n            .assertOnPage()\n    }\n\n    @Test\n    fun whenThereAreSentAndReadyToSendForms_displayTheBanner() {\n        rule.withProject(testDependencies.server.url)\n            .copyForm(\"one-question.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(QuestionAndAnswer(\"what is your age\", \"123\"))\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(QuestionAndAnswer(\"what is your age\", \"124\"))\n\n            .clickSendFinalizedForm(2)\n            .selectForm(0)\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .assertQuantityText(org.odk.collect.strings.R.plurals.forms_ready_to_send, 1, 1)\n    }\n\n    @Test\n    fun formsAreSentInOldestFirstOrderBasedOnFinalizationTime() {\n        /**\n         * Historically, the only timestamp we tracked for instances was the \"last status changed\" date.\n         * However, this timestamp is updated any time the instance status changes—not only when a form\n         * is finalized, but also, for example, when a submission attempt fails.\n         *\n         * This could lead to incorrect ordering when sending finalized forms. For instance, if forms A and B\n         * were finalized in that order, and submission of form A failed, its \"last status changed\" timestamp\n         * would be updated. As a result, when attempting to send both forms later, form B could be sent first,\n         * even though form A was finalized earlier.\n         *\n         * To ensure that forms are always sent in the order they were finalized, we introduced a new timestamp\n         * to track the finalization time specifically.\n         *\n         * This test reproduces the scenario described above to verify that the new finalization timestamp is used\n         * for ordering. It deliberately updates the \"last status changed\" date of the older instance\n         * to confirm that it does not affect the sending order.\n         */\n        testDependencies.server.alwaysReturnError()\n\n        rule.withProject(testDependencies.server.url)\n            .copyForm(\"one-question.xml\", testDependencies.server.hostName)\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(QuestionAndAnswer(\"what is your age\", \"123\"))\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(QuestionAndAnswer(\"what is your age\", \"124\"))\n\n            .clickSendFinalizedForm(2)\n            .sortByDateOldestFirst()\n            .selectForm(0)\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n            .also { testDependencies.server.neverReturnError() }\n\n            .clickSelectAll()\n            .clickSendSelected()\n            .clickOK(SendFinalizedFormPage())\n\n        val firstFormRootElement = XFormParser.getXMLDocument(testDependencies.server.submissions[0].inputStream().reader()).rootElement\n        val secondFormRootElement = XFormParser.getXMLDocument(testDependencies.server.submissions[1].inputStream().reader()).rootElement\n\n        assertThat((firstFormRootElement.getChild(0) as Element).getChild(0), equalTo(\"123\"))\n        assertThat((secondFormRootElement.getChild(0) as Element).getChild(0), equalTo(\"124\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/maps/FormMapTest.kt",
    "content": "package org.odk.collect.android.feature.maps\n\nimport android.app.Activity\nimport android.app.Instrumentation\nimport android.content.Context\nimport android.location.Location\nimport androidx.test.core.app.ApplicationProvider.getApplicationContext\nimport androidx.test.espresso.intent.Intents.intending\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.android.support.FakeClickableMapFragment\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.androidtest.RecordedIntentsRule\nimport org.odk.collect.externalapp.ExternalAppUtils.getReturnIntent\nimport org.odk.collect.geo.GeoUtils\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.settings.SettingsProvider\n\n@RunWith(AndroidJUnit4::class)\nclass FormMapTest {\n\n    private val mapFragment = FakeClickableMapFragment()\n    private val testDependencies = object : TestDependencies() {\n        override fun providesMapFragmentFactory(settingsProvider: SettingsProvider): MapFragmentFactory {\n            return object : MapFragmentFactory {\n                override fun createMapFragment(): MapFragment {\n                    return mapFragment\n                }\n            }\n        }\n    }\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(RecordedIntentsRule())\n        .around(rule)\n\n    @Test\n    fun gettingBlankFormList_showsMapIcon_onlyForFormsWithGeometry() {\n        rule.startAtMainMenu()\n            .copyForm(SINGLE_GEOPOINT_FORM)\n            .copyForm(NO_GEOPOINT_FORM)\n            .clickFillBlankForm()\n            .checkMapIconDisplayedForForm(\"Single geopoint\")\n            .checkMapIconNotDisplayedForForm(\"basic\")\n    }\n\n    @Test\n    fun fillingBlankForm_addsInstanceToMap() {\n        stubGeopointIntent()\n\n        rule.startAtMainMenu()\n            .copyForm(SINGLE_GEOPOINT_FORM)\n            .copyForm(NO_GEOPOINT_FORM)\n            .clickFillBlankForm()\n            .clickOnMapIconForForm(\"Single geopoint\")\n            .clickFillBlankFormButton(\"Single geopoint\")\n\n            .inputText(\"Foo\")\n            .swipeToNextQuestion(\"Location\")\n            .clickWidgetButton()\n            .swipeToEndScreen()\n            .clickSaveAndExitBackToMap()\n\n            .assertText(\n                getApplicationContext<Context>().resources.getString(\n                    org.odk.collect.strings.R.string.select_item_count,\n                    getApplicationContext<Context>().resources.getString(org.odk.collect.strings.R.string.saved_forms),\n                    1,\n                    1\n                )\n            )\n            .selectForm(mapFragment, 1)\n            .clickEditSavedForm(\"Single geopoint\")\n            .clickOnQuestion(\"Name\")\n            .assertText(\"Foo\")\n    }\n\n    private fun stubGeopointIntent() {\n        val location = Location(\"gps\")\n        location.latitude = 125.1\n        location.longitude = 10.1\n        location.altitude = 5.0\n\n        val intent = getReturnIntent(GeoUtils.formatLocationResultString(location))\n        val result = Instrumentation.ActivityResult(Activity.RESULT_OK, intent)\n\n        intending(hasComponent(\"org.odk.collect.geo.geopoint.GeoPointActivity\"))\n            .respondWith(result)\n    }\n\n    companion object {\n        private const val SINGLE_GEOPOINT_FORM = \"single-geopoint.xml\"\n        private const val NO_GEOPOINT_FORM = \"basic.xml\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/projects/AddNewProjectTest.kt",
    "content": "package org.odk.collect.android.feature.projects\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.androidtest.RecordedIntentsRule\n\nclass AddNewProjectTest {\n\n    val rule = CollectTestRule()\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(RecordedIntentsRule())\n        .around(rule)\n\n    @Test\n    fun addingProjectManually_addsNewProject() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickAddProject()\n            .switchToManualMode()\n            .inputUrl(\"https://my-server.com\")\n            .inputUsername(\"John\")\n            .addProject()\n\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"my-server.com\", \"John / my-server.com\")\n            .assertInactiveProject(\"Demo project\", \"demo.getodk.org\")\n    }\n\n    @Test\n    fun addingProjectFromQrCode_addsNewProject() {\n        val page = rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickAddProject()\n\n        testDependencies.fakeBarcodeScannerViewFactory.scan(\"{\\\"general\\\":{\\\"server_url\\\":\\\"https:\\\\/\\\\/my-server.com\\\",\\\"username\\\":\\\"adam\\\",\\\"password\\\":\\\"1234\\\"},\\\"admin\\\":{}}\")\n        page.checkIsToastWithMessageDisplayed(org.odk.collect.strings.R.string.switched_project, \"my-server.com\")\n\n        MainMenuPage()\n            .assertOnPage()\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"my-server.com\", \"adam / my-server.com\")\n            .assertInactiveProject(\"Demo project\", \"demo.getodk.org\")\n    }\n\n    @Test\n    fun switchesToExistingProject_whenDuplicateProjectScanned_andOptionToSwitchToExistingSelected() {\n        val page = rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickAddProject()\n\n        testDependencies.fakeBarcodeScannerViewFactory.scan(\"{\\\"general\\\":{\\\"server_url\\\":\\\"https://demo.getodk.org\\\"},\\\"admin\\\":{}}\")\n\n        page.assertDuplicateDialogShown()\n            .switchToExistingProject()\n            .checkIsToastWithMessageDisplayed(org.odk.collect.strings.R.string.switched_project, \"Demo project\")\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"Demo project\", \"demo.getodk.org\")\n            .assertNotInactiveProject(\"Demo project\")\n    }\n\n    @Test\n    fun addsDuplicateProject_whenDuplicateProjectScanned_andOptionToAddDuplicateProjectSelected() {\n        val page = rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickAddProject()\n\n        testDependencies.fakeBarcodeScannerViewFactory.scan(\"{\\\"general\\\":{\\\"server_url\\\":\\\"https://demo.getodk.org\\\"},\\\"admin\\\":{}}\")\n\n        page.assertDuplicateDialogShown()\n            .addDuplicateProject()\n            .checkIsToastWithMessageDisplayed(org.odk.collect.strings.R.string.switched_project, \"Demo project\")\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"Demo project\", \"demo.getodk.org\")\n            .assertInactiveProject(\"Demo project\", \"demo.getodk.org\")\n    }\n\n    @Test\n    fun switchesToExistingProject_whenDuplicateProjectEnteredManually_andOptionToSwitchToExistingSelected() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickAddProject()\n            .switchToManualMode()\n            .inputUrl(\"https://demo.getodk.org\")\n            .addProjectAndAssertDuplicateDialogShown()\n            .switchToExistingProject()\n            .checkIsToastWithMessageDisplayed(org.odk.collect.strings.R.string.switched_project, \"Demo project\")\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"Demo project\", \"demo.getodk.org\")\n            .assertNotInactiveProject(\"Demo project\")\n    }\n\n    @Test\n    fun addsDuplicateProject_whenDuplicateProjectEnteredManually_andOptionToAddDuplicateProjectSelected() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickAddProject()\n            .switchToManualMode()\n            .inputUrl(\"https://demo.getodk.org\")\n            .addProjectAndAssertDuplicateDialogShown()\n            .addDuplicateProject()\n            .checkIsToastWithMessageDisplayed(org.odk.collect.strings.R.string.switched_project, \"Demo project\")\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"Demo project\", \"demo.getodk.org\")\n            .assertInactiveProject(\"Demo project\", \"demo.getodk.org\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/projects/DeleteProjectTest.kt",
    "content": "package org.odk.collect.android.feature.projects\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.R\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\nclass DeleteProjectTest {\n\n    val rule = CollectTestRule()\n\n    @get:Rule\n    var chain: RuleChain = TestRuleChain\n        .chain()\n        .around(rule)\n\n    @Test\n    fun deleteProjectTest() {\n        // Add project Turtle nesting\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickAddProject()\n            .switchToManualMode()\n            .inputUrl(\"https://my-server.com\")\n            .inputUsername(\"John\")\n            .addProject()\n\n            // Delete Turtle nesting project\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickProjectManagement()\n            .deleteProject()\n\n            // Assert switching to Turtle nesting\n            .checkIsToastWithMessageDisplayed(org.odk.collect.strings.R.string.switched_project, \"Demo project\")\n            .assertProjectIcon(\"D\")\n\n            // Delete Demo project\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickProjectManagement()\n            .deleteLastProject()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/projects/GoogleDriveDeprecationTest.kt",
    "content": "package org.odk.collect.android.feature.projects\n\nimport android.app.Application\nimport androidx.core.net.toUri\nimport androidx.test.core.app.ApplicationProvider\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.androidtest.RecordedIntentsRule\nimport org.odk.collect.projects.Project\n\nclass GoogleDriveDeprecationTest {\n    private val rule = CollectTestRule()\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(RecordedIntentsRule())\n        .around(rule)\n\n    @Test\n    fun bannerIsNotVisibleInNonGoogleDriveProjects() {\n        rule\n            .startAtMainMenu()\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.google_drive_removed_message)\n    }\n\n    @Test\n    fun bannerIsVisibleInGoogleDriveProjects() {\n        addProject(Project.Saved(\"1\", \"Old GD project\", \"A\", \"#ffffff\", true))\n\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .selectProject(\"Old GD project\")\n            .assertText(org.odk.collect.strings.R.string.google_drive_removed_message)\n    }\n\n    @Test\n    fun forumThreadIsOpenedAfterClickingLearnMore() {\n        addProject(Project.Saved(\"1\", \"Old GD project\", \"A\", \"#ffffff\", true))\n\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .selectProject(\"Old GD project\")\n            .clickOnString(org.odk.collect.strings.R.string.learn_more_button_text)\n\n        assertThat(\n            testDependencies.webPageService.openedPages,\n            equalTo(listOf(\"https://forum.getodk.org/t/40097\".toUri()))\n        )\n    }\n\n    @Test\n    fun reconfiguringShouldBeVisibleInNonGoogleDriveProjects() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickProjectManagement()\n            .assertText(org.odk.collect.strings.R.string.reconfigure_with_qr_code_settings_title)\n    }\n\n    private fun addProject(project: Project): Project.Saved {\n        val component =\n            DaggerUtils.getComponent(ApplicationProvider.getApplicationContext<Application>())\n        return component.projectsRepository().save(project)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/projects/LaunchScreenTest.kt",
    "content": "package org.odk.collect.android.feature.projects\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass LaunchScreenTest {\n\n    private val rule = CollectTestRule(false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain(testDependencies).around(rule)\n\n    @Test\n    fun clickingTryCollectAtLaunch_setsAppUpWithDemoProject() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"Demo project\", \"demo.getodk.org\")\n            .clickSettings()\n            .clickServerSettings()\n            .clickOnURL()\n            .assertText(\"https://demo.getodk.org\")\n    }\n\n    @Test\n    fun clickingManuallyEnterProjectDetails_andAddingProjectDetails_setsAppUpWithProjectDetails() {\n        rule.startAtFirstLaunch()\n            .clickManuallyEnterProjectDetails()\n            .inputUrl(\"https://my-server.com\")\n            .inputUsername(\"John\")\n            .addProject()\n            .assertProjectIcon(\"M\")\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"my-server.com\", \"John / my-server.com\")\n    }\n\n    @Test\n    fun clickingAutomaticallyEnterProjectDetails_andScanningQRCode_setsAppUpWithProjectDetails() {\n        val page = rule.startAtFirstLaunch()\n            .clickConfigureWithQrCode()\n\n        testDependencies.fakeBarcodeScannerViewFactory.scan(\"{\\\"general\\\":{\\\"server_url\\\":\\\"https:\\\\/\\\\/my-server.com\\\",\\\"username\\\":\\\"adam\\\",\\\"password\\\":\\\"1234\\\"},\\\"admin\\\":{}}\")\n        page.checkIsToastWithMessageDisplayed(org.odk.collect.strings.R.string.switched_project, \"my-server.com\")\n\n        MainMenuPage()\n            .assertOnPage()\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"my-server.com\", \"adam / my-server.com\")\n    }\n\n    @Test\n    fun whenThereAreProjects_goesToMainMenu() {\n        rule.withProject(\"https://example.com\")\n        rule.relaunch(MainMenuPage())\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/projects/MobileDeviceManagementTest.kt",
    "content": "package org.odk.collect.android.feature.projects\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.RecentAppsRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.mobiledevicemanagement.SETTINGS_JSON_KEY\n\n@RunWith(AndroidJUnit4::class)\nclass MobileDeviceManagementTest {\n    private val recentAppsRule = RecentAppsRule()\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(recentAppsRule)\n        .around(rule)\n\n    @Test\n    fun whenNewProjectIsCreatedViaMDMWhileOnTheLandingScreen_navigateToMainMenu() {\n        rule.startAtFirstLaunch()\n            .also {\n                sendBroadcast(\n                    \"\"\"\n                    {\n                        \"general\": {\n                            \"server_url\": \"http://john.com\",\n                            \"username\": \"john\"\n                        },\n                        \"admin\": {},\n                        \"project\": {\n                            \"name\": \"project1\"\n                        }\n                    }\n                    \"\"\".trimIndent()\n                )\n            }\n            .assertOnPage(MainMenuPage())\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"project1\", \"john / john.com\")\n    }\n\n    @Test\n    fun whenNewProjectIsCreatedViaMDMWhileOutsideTheLandingScreen_navigateToMainMenuNextTimeTheLandingScreenOpens() {\n        sendBroadcast(\n            \"\"\"\n            {\n                \"general\": {\n                    \"server_url\": \"http://john.com\",\n                    \"username\": \"john\"\n                },\n                \"admin\": {},\n                \"project\": {\n                    \"name\": \"project1\"\n                }\n            }\n            \"\"\".trimIndent()\n        )\n\n        rule.relaunch(MainMenuPage())\n    }\n\n    @Test\n    fun whenNewProjectIsCreatedViaMDMWhileOnTheMainMenu_createButDoNotSwitch() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .also {\n                sendBroadcast(\n                    \"\"\"\n                    {\n                        \"general\": {\n                            \"server_url\": \"http://john.com\",\n                            \"username\": \"john\"\n                        },\n                        \"admin\": {},\n                        \"project\": {\n                            \"name\": \"project1\"\n                        }\n                    }\n                    \"\"\".trimIndent()\n                )\n            }\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"Demo project\", \"demo.getodk.org\")\n            .assertInactiveProject(\"project1\", \"john / john.com\")\n    }\n\n    @Test\n    fun whenNewProjectIsCreatedViaMDMWhileOutsideMainMenu_createButDoNotSwitchNextTimeMainMenuOpens() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .clickFillBlankForm()\n            .also {\n                sendBroadcast(\n                    \"\"\"\n                    {\n                        \"general\": {\n                            \"server_url\": \"http://john.com\",\n                            \"username\": \"john\"\n                        },\n                        \"admin\": {},\n                        \"project\": {\n                            \"name\": \"project1\"\n                        }\n                    }\n                    \"\"\".trimIndent()\n                )\n            }\n            .pressBack(MainMenuPage())\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"Demo project\", \"demo.getodk.org\")\n            .assertInactiveProject(\"project1\", \"john / john.com\")\n    }\n\n    @Test\n    fun whenCurrentProjectIsUpdatedViaMDMWWhileOnTheMainMenu_applySettingsImmediately() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .also {\n                sendBroadcast(\n                    \"\"\"\n                    {\n                        \"general\": {},\n                        \"admin\": {\n                            \"edit_saved\": false\n                        }\n                    }\n                    \"\"\".trimIndent()\n                )\n            }\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickAccessControl()\n            .openMainMenuSettings()\n            .assertDraftsUnchecked()\n    }\n\n    @Test\n    fun whenCurrentProjectIsUpdatedViaMDMWhileOutsideMainMenu_updateApplySettingsNextTimeMainMenuOpens() {\n        testDependencies.server.addForm(\"One Question\", \"one-question\", \"1\", \"one-question.xml\")\n\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .setServer(testDependencies.server.url)\n            .clickGetBlankForm()\n            .clickGetSelected()\n            .clickOKOnDialog(MainMenuPage())\n            .clickFillBlankForm()\n            .also {\n                sendBroadcast(\n                    \"\"\"\n                    {\n                        \"general\": {\n                            \"server_url\": \"${testDependencies.server.url}\"\n                        },\n                        \"admin\": {\n                            \"jump_to\": false\n                        }\n                    }\n                    \"\"\".trimIndent()\n                )\n            }\n            .clickOnForm(\"One Question\")\n            .assertGoToIconExists()\n            .pressBackAndDiscardForm()\n            .clickFillBlankForm()\n            .clickOnForm(\"One Question\")\n            .assertGoToIconDoesNotExist()\n    }\n\n    @Test\n    fun whenInactiveProjectIsUpdatedViaMDMWW_settingsAreUpdatedAfterSwitchingToThatProject() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .openProjectSettingsDialog()\n            .clickAddProject()\n            .switchToManualMode()\n            .inputUrl(\"http://john.com\")\n            .inputUsername(\"john\")\n            .addProject()\n            .openProjectSettingsDialog()\n            .selectProject(\"Demo project\")\n            .also {\n                sendBroadcast(\n                    \"\"\"\n                    {\n                        \"general\": {\n                            \"server_url\": \"http://john.com\",\n                            \"username\": \"john\"\n                        },\n                        \"admin\": {\n                            \"edit_saved\": false\n                        }\n                    }\n                    \"\"\".trimIndent()\n                )\n            }\n            .openProjectSettingsDialog()\n            .selectProject(\"john.com\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickAccessControl()\n            .openMainMenuSettings()\n            .assertDraftsUnchecked()\n    }\n\n    private fun sendBroadcast(settings: String) {\n        val bundle = Bundle().apply {\n            putString(SETTINGS_JSON_KEY, settings)\n        }\n        whenever(testDependencies.restrictionsManager.applicationRestrictions).thenReturn(bundle)\n\n        testDependencies.broadcastReceiverRegister.broadcast(\n            ApplicationProvider.getApplicationContext(),\n            Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/projects/ProjectsAdbTest.kt",
    "content": "package org.odk.collect.android.feature.projects\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider.getApplicationContext\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass ProjectsAdbTest {\n\n    val rule = CollectTestRule()\n\n    @get:Rule\n    var chain: RuleChain = TestRuleChain\n        .chain()\n        .around(rule)\n\n    @Test\n    fun clearingStorage_andReturningToApp_recreatesStorageForProject() {\n        val fillBlankFormPage = rule.startAtMainMenu().clickFillBlankForm()\n\n        val storage = getApplicationContext<Application>().getExternalFilesDir(null)\n        storage!!.listFiles()!!.forEach { it.deleteRecursively() }\n\n        fillBlankFormPage.pressBack(MainMenuPage())\n            .assertProjectIcon(\"D\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/projects/SwitchProjectTest.kt",
    "content": "package org.odk.collect.android.feature.projects\n\nimport android.Manifest\nimport androidx.test.rule.GrantPermissionRule\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.StubOpenRosaServer.EntityListItem\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.EntitiesPage\nimport org.odk.collect.android.support.pages.ExperimentalPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\nclass SwitchProjectTest {\n\n    val rule = CollectTestRule()\n    val testDependencies = TestDependencies()\n\n    @get:Rule\n    var chain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(GrantPermissionRule.grant(Manifest.permission.CAMERA))\n        .around(rule)\n\n    @Test\n    fun canSwitchActiveProjectToAnotherInList() {\n        // Add project Turtle nesting\n        rule.startAtMainMenu()\n            .assertProjectIcon(\"D\")\n            .openProjectSettingsDialog()\n            .clickAddProject()\n            .switchToManualMode()\n            .inputUrl(\"https://my-server.com\")\n            .inputUsername(\"John\")\n            .addProject()\n\n            // Switch to Turtle nesting\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"my-server.com\", \"John / my-server.com\")\n            .assertInactiveProject(\"Demo project\", \"demo.getodk.org\")\n            .selectProject(\"Demo project\")\n            .checkIsToastWithMessageDisplayed(org.odk.collect.strings.R.string.switched_project, \"Demo project\")\n            .assertProjectIcon(\"D\")\n    }\n\n    @Test\n    fun switchingProject_switchesSettingsFormsInstancesAndEntities() {\n        testDependencies.server.addForm(\n            \"One Question Entity Registration\",\n            \"one-question-entity\",\n            \"1\",\n            \"one-question-entity-registration.xml\"\n        )\n        testDependencies.server.addForm(\n            \"one-question-entity-update.xml\",\n            listOf(EntityListItem(\"people.csv\"))\n        )\n\n        rule.startAtMainMenu()\n            // Copy and fill form\n            .copyForm(\"two-question.xml\")\n            .startBlankForm(\"Two Question\")\n            .swipeToNextQuestion(\"What is your age?\")\n            .swipeToEndScreen()\n            .clickSaveAsDraft()\n            .clickDrafts(1)\n            .assertText(\"Two Question\")\n            .pressBack(MainMenuPage())\n\n            // Create and switch to new project\n            .assertProjectIcon(\"D\")\n            .openProjectSettingsDialog()\n            .clickAddProject()\n            .switchToManualMode()\n            .inputUrl(\"https://my-server.com\")\n            .inputUsername(\"John\")\n            .addProject()\n\n            // Set server and download form\n            .setServer(testDependencies.server.url)\n            .clickGetBlankForm()\n            .clickGetSelected()\n            .clickOKOnDialog(MainMenuPage())\n\n            // Fill form\n            .startBlankForm(\"One Question Entity Registration\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"Name\", \"Alice\"))\n            .clickSendFinalizedForm(1)\n            .assertText(\"One Question Entity Registration\")\n            .pressBack(MainMenuPage())\n\n            .openEntityBrowser()\n            .clickOnList(\"people\")\n            .assertEntity(\"Alice\", \"full_name: Alice\")\n            .pressBack(EntitiesPage())\n            .pressBack(ExperimentalPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            // Switch back to first project\n            .openProjectSettingsDialog()\n            .selectProject(\"Demo project\")\n\n            // Check server\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickServerSettings()\n            .clickOnURL()\n            .assertText(\"https://demo.getodk.org\")\n            .clickOKOnDialog()\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            // Check forms\n            .clickFillBlankForm()\n            .assertFormExists(\"Two Question\")\n            .pressBack(MainMenuPage())\n\n            // Check instances\n            .clickDrafts(1)\n            .assertText(\"Two Question\")\n            .pressBack(MainMenuPage())\n\n            // Check entities\n            .openEntityBrowser()\n            .assertTextDoesNotExist(\"people\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/projects/UpdateProjectTest.kt",
    "content": "package org.odk.collect.android.feature.projects\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\nclass UpdateProjectTest {\n\n    val rule = CollectTestRule()\n\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    var chain: RuleChain = TestRuleChain\n        .chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun updateProjectTest() {\n        rule.startAtMainMenu()\n            .assertProjectIcon(\"D\")\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"Demo project\", \"demo.getodk.org\")\n            .clickSettings()\n            .clickProjectDisplay()\n            .setProjectName(\"Project X\")\n            .assertFileWithProjectNameUpdated(\"Demo project\", \"Project X\")\n            .setProjectIcon(\"XY\")\n            .setProjectColor(\"cccccc\")\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickServerSettings()\n            .clickServerUsername()\n            .inputText(\"Anna\")\n            .clickOKOnDialog()\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            .assertProjectIcon(\"X\")\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"Project X\", \"Anna / demo.getodk.org\")\n    }\n\n    @Test\n    fun updateProjectName_updatesProjectNameFileSanitizingIt() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickProjectDisplay()\n            .setProjectName(\"Project<>\")\n            .assertFileWithProjectNameUpdated(\"Demo project\", \"Project__\")\n            .setProjectName(\":*Project<>\")\n            .assertFileWithProjectNameUpdated(\"Project__\", \"__Project__\")\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5902\n    fun updatingProjectDetails_whenThereIsMoreThanOneProject_doesNotDuplicateInactiveProjectsOnTheList() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickAddProject()\n\n        testDependencies.fakeBarcodeScannerViewFactory.scan(\"{\\\"general\\\":{\\\"server_url\\\":\\\"https:\\\\/\\\\/my-server.com\\\",\\\"username\\\":\\\"adam\\\",\\\"password\\\":\\\"1234\\\"},\\\"admin\\\":{}}\")\n\n        MainMenuPage()\n            // assert there are two projects displayed\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"my-server.com\", \"adam / my-server.com\")\n            .assertInactiveProject(\"Demo project\", \"demo.getodk.org\")\n\n            // Update project icon\n            .clickSettings()\n            .clickProjectDisplay()\n            .setProjectIcon(\"Z\")\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            // assert there are two projects displayed\n            .openProjectSettingsDialog()\n            .assertCurrentProject(\"my-server.com\", \"adam / my-server.com\")\n            .assertInactiveProject(\"Demo project\", \"demo.getodk.org\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/ConfigureWithQRCodeTest.java",
    "content": "package org.odk.collect.android.feature.settings;\n\nimport static androidx.test.core.app.ApplicationProvider.getApplicationContext;\n\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.After;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.configure.qr.AppConfigurationGenerator;\nimport org.odk.collect.android.configure.qr.QRCodeGenerator;\nimport org.odk.collect.android.support.TestDependencies;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.pages.ProjectSettingsPage;\nimport org.odk.collect.android.support.pages.QRCodePage;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.RunnableRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.util.Collection;\n\n@RunWith(AndroidJUnit4.class)\npublic class ConfigureWithQRCodeTest {\n\n    private final TestDependencies testDependencies = new TestDependencies() {\n        @Override\n        public QRCodeGenerator providesQRCodeGenerator() {\n            return stubQRCodeGenerator;\n        }\n    };\n    private final CollectTestRule rule = new CollectTestRule();\n    private final StubQRCodeGenerator stubQRCodeGenerator = new StubQRCodeGenerator();\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain(testDependencies)\n            .around(new RunnableRule(stubQRCodeGenerator::setup))\n            .around(rule);\n\n    @After\n    public void teardown() {\n        // Clean up files created by stub generator\n        stubQRCodeGenerator.teardown();\n    }\n\n    @Test\n    public void clickConfigureQRCode_opensScanner_andThenScanning_importsSettings() {\n        QRCodePage qrCodePage = rule.startAtMainMenu()\n                .openProjectSettingsDialog()\n                .clickSettings()\n                .clickProjectManagement()\n                .clickConfigureQR();\n\n        testDependencies.getFakeBarcodeScannerViewFactory().scan(\"{\\\"general\\\":{ \\\"server_url\\\": \\\"http://gallops.example\\\" },\\\"admin\\\":{}}\");\n        qrCodePage\n                .checkIsToastWithMessageDisplayed(org.odk.collect.strings.R.string.successfully_imported_settings)\n                .assertFileWithProjectNameUpdated(\"Demo project\", \"gallops.example\");\n\n        new MainMenuPage()\n                .assertOnPage()\n                .openProjectSettingsDialog()\n                .clickSettings()\n                .clickServerSettings()\n                .assertText(\"http://gallops.example\");\n    }\n\n    @Test\n    public void clickConfigureQRCode_andClickingOnView_showsQRCode() {\n        rule.startAtMainMenu()\n                .openProjectSettingsDialog()\n                .clickSettings()\n                .clickProjectManagement()\n                .clickConfigureQR()\n                .clickView()\n                .assertImageViewShowsImage(R.id.ivQRcode, BitmapFactory.decodeResource(\n                        getApplicationContext().getResources(),\n                        stubQRCodeGenerator.getDrawableID()\n                ));\n    }\n\n    @Test\n    public void whenThereIsAnAdminPassword_canRemoveFromQRCode() {\n        rule.startAtMainMenu()\n                .openProjectSettingsDialog()\n                .clickSettings()\n                .setAdminPassword(\"blah\")\n                .clickProjectManagement()\n                .clickConfigureQR()\n                .clickView()\n                .clickOnString(org.odk.collect.strings.R.string.qrcode_with_admin_password)\n                .clickOnString(org.odk.collect.strings.R.string.admin_password)\n                .clickOnString(org.odk.collect.strings.R.string.generate)\n                .assertText(org.odk.collect.strings.R.string.qrcode_without_passwords);\n    }\n\n    @Test\n    public void whenThereIsAServerPassword_canRemoveFromQRCode() {\n        rule.startAtMainMenu()\n                .openProjectSettingsDialog()\n                .clickSettings()\n                .clickServerSettings()\n                .clickServerPassword()\n                .inputText(\"blah\")\n                .clickOKOnDialog()\n                .pressBack(new ProjectSettingsPage())\n                .pressBack(new MainMenuPage())\n\n                .openProjectSettingsDialog()\n                .clickSettings()\n                .clickProjectManagement()\n                .clickConfigureQR()\n                .clickView()\n                .clickOnString(org.odk.collect.strings.R.string.qrcode_with_server_password)\n                .clickOnString(org.odk.collect.strings.R.string.server_password)\n                .clickOnString(org.odk.collect.strings.R.string.generate)\n                .assertText(org.odk.collect.strings.R.string.qrcode_without_passwords);\n    }\n\n    private static class StubQRCodeGenerator implements QRCodeGenerator {\n\n        private static final int CHECKER_BACKGROUND_DRAWABLE_ID = com.rarepebble.colorpicker.R.drawable.checker_background;\n\n        @Override\n        public String generateQRCode(Collection<String> selectedPasswordKeys, AppConfigurationGenerator appConfigurationGenerator) {\n            return getQRCodeFilePath();\n        }\n\n        public void setup() {\n            Bitmap bitmap = BitmapFactory.decodeResource(\n                    getApplicationContext().getResources(),\n                    getDrawableID());\n            saveBitmap(bitmap);\n        }\n\n        public void teardown() {\n            File file = new File(getQRCodeFilePath());\n            if (file.exists()) {\n                file.delete();\n            }\n        }\n\n        String getQRCodeFilePath() {\n            return getApplicationContext().getExternalFilesDir(null) + File.separator + \"test-collect-settings.png\";\n        }\n\n        int getDrawableID() {\n            return CHECKER_BACKGROUND_DRAWABLE_ID;\n        }\n\n        private void saveBitmap(Bitmap bitmap) {\n            try (FileOutputStream out = new FileOutputStream(getQRCodeFilePath())) {\n                bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/FormEntrySettingsTest.kt",
    "content": "package org.odk.collect.android.feature.settings\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.strings.R\n\n@RunWith(AndroidJUnit4::class)\nclass FormEntrySettingsTest {\n\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    val chain: RuleChain = TestRuleChain.chain().around(rule)\n\n    @Test\n    fun settingsThatResetAppAreBlocked() {\n        rule.startAtMainMenu()\n            .copyForm(\"one-question.xml\")\n            .startBlankForm(\"One Question\")\n            .clickOptionsIcon()\n            .clickProjectSettings()\n            .assertDisabled(R.string.project_management_section_title)\n\n            .clickOnUserInterface()\n            .assertDisabled(R.string.language)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/FormManagementSettingsTest.kt",
    "content": "package org.odk.collect.android.feature.settings\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.empty\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.androidtest.RecordedIntentsRule\n\n@RunWith(AndroidJUnit4::class)\nclass FormManagementSettingsTest {\n    private val testDependencies = TestDependencies()\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    var ruleChain: RuleChain = TestRuleChain.chain(testDependencies)\n        .around(RecordedIntentsRule())\n        .around(rule)\n\n    @Test\n    fun whenMatchExactlyEnabled_changingAutomaticUpdateFrequency_changesTaskFrequency() {\n        var deferredTasks = testDependencies.scheduler.getDeferredTasks()\n\n        assertThat(deferredTasks, `is`(empty()))\n\n        val page = MainMenuPage().assertOnPage()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickFormManagement()\n            .clickUpdateForms()\n            .clickOption(org.odk.collect.strings.R.string.match_exactly)\n\n        deferredTasks = testDependencies.scheduler.getDeferredTasks()\n\n        assertThat(deferredTasks.size, `is`(1))\n\n        val matchExactlyTag = deferredTasks[0].tag\n\n        page.clickAutomaticUpdateFrequency().clickOption(org.odk.collect.strings.R.string.every_one_hour)\n        deferredTasks = testDependencies.scheduler.getDeferredTasks()\n\n        assertThat(deferredTasks.size, `is`(1))\n        assertThat(deferredTasks[0].tag, `is`(matchExactlyTag))\n        assertThat(deferredTasks[0].repeatPeriod, `is`(1000L * 60 * 60))\n    }\n\n    @Test\n    fun whenPreviouslyDownloadedOnlyEnabled_changingAutomaticUpdateFrequency_changesTaskFrequency() {\n        var deferredTasks = testDependencies.scheduler.getDeferredTasks()\n\n        assertThat(deferredTasks, `is`(empty()))\n\n        val page = MainMenuPage().assertOnPage()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickFormManagement()\n            .clickUpdateForms()\n            .clickOption(org.odk.collect.strings.R.string.previously_downloaded_only)\n\n        deferredTasks = testDependencies.scheduler.getDeferredTasks()\n\n        assertThat(deferredTasks.size, `is`(1))\n\n        val previouslyDownloadedTag = deferredTasks[0].tag\n        page.clickAutomaticUpdateFrequency().clickOption(org.odk.collect.strings.R.string.every_one_hour)\n\n        deferredTasks = testDependencies.scheduler.getDeferredTasks()\n\n        assertThat(deferredTasks.size, `is`(1))\n        assertThat(deferredTasks[0].tag, `is`(previouslyDownloadedTag))\n        assertThat(deferredTasks[0].repeatPeriod, `is`(1000L * 60 * 60))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/FormMetadataSettingsTest.kt",
    "content": "package org.odk.collect.android.feature.settings\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.pages.SaveOrDiscardFormDialog\nimport org.odk.collect.android.support.pages.UserAndDeviceIdentitySettingsPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.ResetStateRule\nimport org.odk.collect.android.support.rules.TestRuleChain\nimport org.odk.collect.metadata.InstallIDProvider\nimport org.odk.collect.settings.SettingsProvider\n\n@RunWith(AndroidJUnit4::class)\nclass FormMetadataSettingsTest {\n    private val installIDProvider = FakeInstallIDProvider()\n    var rule = CollectTestRule()\n\n    @get:Rule\n    var copyFormChain: RuleChain = TestRuleChain.chain()\n        .around(\n            ResetStateRule(\n                object : AppDependencyModule() {\n                    override fun providesInstallIDProvider(settingsProvider: SettingsProvider): InstallIDProvider {\n                        return installIDProvider\n                    }\n                }\n            )\n        )\n        .around(rule)\n\n    @Test\n    fun metadataShouldBeDisplayedInPreferences() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickUserAndDeviceIdentity()\n            .clickFormMetadata()\n\n            .clickUsername()\n            .inputText(\"Chino\")\n            .clickOKOnDialog()\n            .clickPhoneNumber()\n            .inputText(\"123\")\n            .clickOKOnDialog()\n            .clickEmail()\n            .inputText(\"chino@whitepony.com\")\n            .clickOKOnDialog()\n\n            .assertPreference(org.odk.collect.strings.R.string.username, \"Chino\")\n            .assertPreference(org.odk.collect.strings.R.string.phone_number, \"123\")\n            .assertPreference(org.odk.collect.strings.R.string.email, \"chino@whitepony.com\")\n            .assertPreference(org.odk.collect.strings.R.string.device_id, installIDProvider.installID)\n    }\n\n    @Test\n    fun metadataShouldBeDisplayedInForm() {\n        rule.startAtMainMenu()\n            .copyForm(\"metadata.xml\")\n\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickUserAndDeviceIdentity()\n            .clickFormMetadata()\n            .clickUsername()\n            .inputText(\"Chino\")\n            .clickOKOnDialog()\n            .clickPhoneNumber()\n            .inputText(\"664615\")\n            .clickOKOnDialog()\n            .clickEmail()\n            .inputText(\"chino@whitepony.com\")\n            .clickOKOnDialog()\n            .pressBack(UserAndDeviceIdentitySettingsPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            // And verify that new metadata is displayed\n            .startBlankForm(\"Metadata\")\n            .assertTexts(\"Chino\", \"664615\", \"chino@whitepony.com\", installIDProvider.installID)\n    }\n\n    @Test\n    fun settingServerUsername_usedAsFallbackForMetadataUsername() {\n        rule.startAtMainMenu()\n            .copyForm(\"metadata.xml\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickServerSettings()\n            .clickServerUsername()\n            .inputText(\"Chino\")\n            .clickOKOnDialog()\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .startBlankForm(\"Metadata\")\n            .assertText(\"Chino\")\n            .pressBack(SaveOrDiscardFormDialog(MainMenuPage()))\n            .clickDiscardForm()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickUserAndDeviceIdentity()\n            .clickFormMetadata()\n            .clickUsername()\n            .inputText(\"Stephen\")\n            .clickOKOnDialog()\n            .pressBack(UserAndDeviceIdentitySettingsPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .startBlankForm(\"Metadata\")\n            .assertText(\"Stephen\")\n    }\n\n    @Test // https://github.com/getodk/collect/issues/4792\n    fun metadataProperties_shouldBeReloadedAfterSwitchingProjects() {\n        rule.startAtMainMenu()\n            .copyForm(\"metadata.xml\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickUserAndDeviceIdentity()\n            .clickFormMetadata()\n            .clickEmail()\n            .inputText(\"demo@getodk.com\")\n            .clickOKOnDialog()\n            .clickPhoneNumber()\n            .inputText(\"123456789\")\n            .clickOKOnDialog()\n            .clickUsername()\n            .inputText(\"Demo user\")\n            .clickOKOnDialog()\n            .pressBack(UserAndDeviceIdentitySettingsPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            .addAndSwitchToProject(\"https://second-project.com\")\n            .copyForm(\"metadata.xml\", \"second-project.com\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickUserAndDeviceIdentity()\n            .clickFormMetadata()\n            .clickEmail()\n            .inputText(\"john@second-project.com\")\n            .clickOKOnDialog()\n            .clickPhoneNumber()\n            .inputText(\"987654321\")\n            .clickOKOnDialog()\n            .clickUsername()\n            .inputText(\"John Smith\")\n            .clickOKOnDialog()\n            .pressBack(UserAndDeviceIdentitySettingsPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            .clickFillBlankForm()\n            .clickOnForm(\"Metadata\")\n            .assertTexts(\"john@second-project.com\", \"987654321\", \"John Smith\")\n            .swipeToEndScreen()\n            .clickFinalize()\n\n            .openProjectSettingsDialog()\n            .selectProject(\"Demo project\")\n            .clickFillBlankForm()\n            .clickOnForm(\"Metadata\")\n            .assertTexts(\"demo@getodk.com\", \"123456789\", \"Demo user\")\n            .swipeToEndScreen()\n            .clickFinalize()\n    }\n\n    @Test\n    fun setEmail_validatesEmail() {\n        rule.startAtMainMenu()\n            .copyForm(\"metadata.xml\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickUserAndDeviceIdentity()\n            .clickFormMetadata()\n            .clickEmail()\n            .inputText(\"aabb\")\n            .clickOKOnDialog()\n            .checkIsToastWithMessageDisplayed(org.odk.collect.strings.R.string.invalid_email_address)\n            .clickEmail()\n            .inputText(\"aa@bb\")\n            .clickOKOnDialog()\n            .assertText(\"aa@bb\")\n    }\n\n    private class FakeInstallIDProvider : InstallIDProvider {\n        override val installID: String\n            get() = \"deviceID\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/MovingBackwardsTest.kt",
    "content": "package org.odk.collect.android.feature.settings\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.pages.AccessControlPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass MovingBackwardsTest {\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    var ruleChain: RuleChain = TestRuleChain.chain().around(rule)\n\n    @Test\n    fun whenMovingBackwardDisabledWithPreventingUsersFormBypassingIt_relatedOptionsShouldBeUpdated() {\n        rule.startAtMainMenu()\n            .disableFinalizeInFormEntry()\n            .setConstraintProcessingToOnFinalize()\n            .assertSettingsBeforeDisablingMovingBackwards()\n\n            .clickMovingBackwards()\n            .clickOnString(org.odk.collect.strings.R.string.yes)\n\n            // after disabling moving backward - the state of the 4 related options is reversed\n            .assertGoToPromptDisabled()\n            .assertGoToPromptUnchecked()\n\n            .assertSaveAsDraftInFormEntryDisabled()\n            .assertSaveAsDraftInFormEntryUnchecked()\n\n            .assertSaveAsDraftInFormEndDisabled()\n            .assertSaveAsDraftInFormEndUnchecked()\n\n            .assertFinalizeDisabled()\n            .assertFinalizeChecked()\n\n            .pressBack(AccessControlPage())\n            .pressBack(ProjectSettingsPage())\n            .openFormManagement()\n            .scrollToConstraintProcessing()\n            .checkIfConstraintProcessingIsDisabled()\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.constraint_behavior_on_finalize)\n            .assertText(org.odk.collect.strings.R.string.constraint_behavior_on_swipe)\n    }\n\n    @Test\n    fun whenMovingBackwardDisabledWithoutPreventingUsersFormBypassingIt_relatedOptionsShouldNotBeUpdated() {\n        rule.startAtMainMenu()\n            .disableFinalizeInFormEntry()\n            .setConstraintProcessingToOnFinalize()\n            .assertSettingsBeforeDisablingMovingBackwards()\n\n            .clickMovingBackwards()\n            .clickOnString(org.odk.collect.strings.R.string.no)\n\n            .assertGoToPromptEnabled()\n            .assertGoToPromptChecked()\n\n            .assertSaveAsDraftInFormEntryEnabled()\n            .assertSaveAsDraftInFormEntryChecked()\n\n            .assertSaveAsDraftInFormEndDisabled()\n            .assertSaveAsDraftInFormEndChecked()\n\n            .assertFinalizeEnabled()\n            .assertFinalizeUnchecked()\n\n            .pressBack(AccessControlPage())\n            .pressBack(ProjectSettingsPage())\n            .openFormManagement()\n            .scrollToConstraintProcessing()\n            .checkIfConstraintProcessingIsEnabled()\n            .assertText(org.odk.collect.strings.R.string.constraint_behavior_on_finalize)\n            .assertTextDoesNotExist(org.odk.collect.strings.R.string.constraint_behavior_on_swipe)\n    }\n\n    private fun MainMenuPage.disableFinalizeInFormEntry(): ProjectSettingsPage {\n        return openProjectSettingsDialog()\n            .clickSettings()\n            .clickAccessControl()\n            .clickFormEntrySettings()\n            .clickOnString(org.odk.collect.strings.R.string.finalize)\n            .pressBack(AccessControlPage())\n            .pressBack(ProjectSettingsPage())\n    }\n\n    private fun ProjectSettingsPage.setConstraintProcessingToOnFinalize(): ProjectSettingsPage {\n        return openFormManagement()\n            .openConstraintProcessing()\n            .clickOnString(org.odk.collect.strings.R.string.constraint_behavior_on_finalize)\n            .pressBack(ProjectSettingsPage())\n    }\n\n    private fun ProjectSettingsPage.assertSettingsBeforeDisablingMovingBackwards(): AccessControlPage {\n        return clickAccessControl()\n            .clickFormEntrySettings()\n            .assertGoToPromptEnabled()\n            .assertGoToPromptChecked()\n\n            .assertSaveAsDraftInFormEntryEnabled()\n            .assertSaveAsDraftInFormEntryChecked()\n\n            .assertSaveAsDraftInFormEndDisabled()\n            .assertSaveAsDraftInFormEndChecked()\n\n            .assertFinalizeEnabled()\n            .assertFinalizeUnchecked()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/ResetProjectTest.kt",
    "content": "package org.odk.collect.android.feature.settings\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.pages.AccessControlPage\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.ProjectSettingsPage\nimport org.odk.collect.android.support.pages.ResetApplicationDialog\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.strings.R\n\nclass ResetProjectTest {\n\n    private val rule = CollectTestRule(useDemoProject = false)\n    private val testDependencies = TestDependencies()\n\n    @get:Rule\n    val ruleChain: RuleChain = chain(testDependencies)\n        .around(rule)\n\n    @Test\n    fun canResetBlankForms() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .copyForm(\"all-widgets.xml\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickProjectManagement()\n            .clickOnResetProject()\n            .assertDisabled(R.string.reset_settings_button_reset)\n            .clickOnString(R.string.reset_blank_forms)\n            .clickOnString(R.string.reset_settings_button_reset)\n            .clickOKOnDialog(MainMenuPage())\n            .clickFillBlankForm()\n            .assertTextDoesNotExist(\"All widgets\")\n    }\n\n    @Test\n    fun canResetSavedForms() {\n        testDependencies.server.addForm(\"one-question.xml\")\n\n        rule.withProject(testDependencies.server.url, matchExactly = true)\n            .startBlankForm(\"One Question\")\n            .fillOutAndFinalize(FormEntryPage.QuestionAndAnswer(\"what is your age\", \"34\"))\n\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickProjectManagement()\n            .clickOnResetProject()\n            .assertDisabled(R.string.reset_settings_button_reset)\n            .clickOnString(R.string.reset_saved_forms)\n            .clickOnString(R.string.reset_settings_button_reset)\n            .clickOKOnDialog(MainMenuPage())\n\n            .clickDrafts()\n            .assertTextDoesNotExist(\"One Question\")\n    }\n\n    @Test\n    fun canResetProtectedSettings() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickAccessControl()\n            .openUserSettings()\n            .uncheckServerOption()\n            .pressBack(AccessControlPage())\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .checkIfServerOptionIsNotDisplayed()\n            .pressBack(MainMenuPage())\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickProjectManagement()\n            .clickOnResetProject()\n            .clickOnString(R.string.reset_settings)\n            .clickOnString(R.string.reset_settings_button_reset)\n            .clickOKOnDialog(MainMenuPage())\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .checkIfServerOptionIsDisplayed()\n    }\n\n    @Test\n    fun canResetUserInterfaceSettings() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickOnUserInterface()\n            .clickOnLanguage()\n            .clickOnSelectedLanguage(\"español\")\n\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickOnUserInterface()\n            .assertText(\"español\")\n            .pressBack(ProjectSettingsPage())\n            .pressBack(MainMenuPage())\n\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickProjectManagement()\n            .clickOnResetProject()\n            .clickOnString(R.string.reset_settings)\n            .clickOnString(R.string.reset_settings_button_reset)\n            .clickOKOnDialog(MainMenuPage())\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickOnUserInterface()\n            .assertText(R.string.use_device_language)\n            .assertTextDoesNotExist(\"español\")\n    }\n\n    @Test\n    fun when_rotateScreen_should_resetDialogNotDisappear() {\n        rule.startAtFirstLaunch()\n            .clickTryCollect()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickProjectManagement()\n            .clickOnResetProject()\n            .assertText(R.string.reset_settings_dialog_title)\n            .rotateToLandscape(ResetApplicationDialog())\n            .assertText(R.string.reset_settings_dialog_title)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/ServerSettingsTest.java",
    "content": "package org.odk.collect.android.feature.settings;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.TestDependencies;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.pages.ProjectSettingsPage;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.androidtest.RecordedIntentsRule;\n\n@RunWith(AndroidJUnit4.class)\npublic class ServerSettingsTest {\n\n    private final TestDependencies testDependencies = new TestDependencies();\n\n    public final CollectTestRule rule = new CollectTestRule();\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain(testDependencies)\n            .around(new RecordedIntentsRule())\n            .around(rule);\n\n    @Test\n    public void whenUsingODKServer_canAddCredentialsForServer() {\n        testDependencies.server.setCredentials(\"Joe\", \"netsky\");\n        testDependencies.server.addForm(\"One Question\", \"one-question\", \"1\", \"one-question.xml\");\n\n        new MainMenuPage().assertOnPage()\n                .openProjectSettingsDialog()\n                .clickSettings()\n                .clickServerSettings()\n                .clickOnURL()\n                .inputText(testDependencies.server.getUrl())\n                .clickOKOnDialog()\n                .assertText(testDependencies.server.getUrl())\n                .clickServerUsername()\n                .inputText(\"Joe\")\n                .clickOKOnDialog()\n                .assertText(\"Joe\")\n                .clickServerPassword()\n                .inputText(\"netsky\")\n                .clickOKOnDialog()\n                .assertText(\"********\")\n                .pressBack(new ProjectSettingsPage())\n                .pressBack(new MainMenuPage())\n\n                .clickGetBlankForm()\n                .clickGetSelected()\n                .assertMessage(\"All downloads succeeded!\")\n                .clickOKOnDialog(new MainMenuPage());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/SettingLanguageTest.kt",
    "content": "package org.odk.collect.android.feature.settings\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.rules.RuleChain\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.rules.CollectTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain\n\n@RunWith(AndroidJUnit4::class)\nclass SettingLanguageTest {\n    private val rule = CollectTestRule()\n\n    @get:Rule\n    var ruleChain: RuleChain = TestRuleChain.chain().around(rule)\n\n    @Test\n    fun settingLanguageWithoutCountryCodeShouldLoadProperTranslations() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickOnUserInterface()\n            .clickOnLanguage()\n            .clickOnSelectedLanguage(\"dansk\")\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .assertText(\"Kort\")\n    }\n\n    @Test\n    fun settingLanguageWithCountryCodeShouldLoadProperTranslations() {\n        rule.startAtMainMenu()\n            .openProjectSettingsDialog()\n            .clickSettings()\n            .clickOnUserInterface()\n            .clickOnLanguage()\n            .clickOnSelectedLanguage(\"svenska (Sverige)\")\n            .assertText(\"Börja nytt formulär\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/smoke/AllWidgetsFormTest.kt",
    "content": "package org.odk.collect.android.feature.smoke\n\nimport org.junit.Rule\nimport org.junit.Test\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.rules.BlankFormTestRule\nimport org.odk.collect.android.support.rules.TestRuleChain.chain\nimport org.odk.collect.strings.R.string\n\n/**\n * Integration test that runs through a form with all question types.\n */\nclass AllWidgetsFormTest {\n\n    private val activityTestRule = BlankFormTestRule(\"all-widgets.xml\", \"All widgets\")\n\n    @get:Rule\n    val copyFormChain = chain()\n        .around(activityTestRule)\n\n    @Test\n    fun testActivityOpen() {\n        activityTestRule.startInFormEntry()\n            .swipeToNextQuestion(\"String widget\")\n            .swipeToNextQuestion(\"String number widget\")\n            .swipeToNextQuestion(\"URL widget\")\n            .swipeToNextQuestion(\"Ex string widget\")\n            .swipeToNextQuestion(\"Ex printer widget\")\n\n            .swipeToNextQuestion(\"Integer widget\")\n            .swipeToNextQuestion(\"Integer widget with thousands separators\")\n            .swipeToNextQuestion(\"Ex integer widget\")\n            .swipeToNextQuestion(\"Decimal widget\")\n            .swipeToNextQuestion(\"Ex decimal widget\")\n            .swipeToNextQuestion(\"Bearing widget\")\n\n            .swipeToNextQuestion(\"Range integer widget\")\n            .swipeToNextQuestion(\"Range decimal widget\")\n            .swipeToNextQuestion(\"Range vertical integer widget\")\n\n            .swipeToNextQuestion(\"Range picker integer widget\")\n            .clickOnText(\"Select value\")\n            .pressBack(FormEntryPage(\"All widgets\"))\n\n            .swipeToNextQuestion(\"Range rating integer widget\")\n\n            .swipeToNextQuestion(\"Image widget\")\n            .swipeToNextQuestion(\"Image widget without Choose button\")\n            .swipeToNextQuestion(\"Selfie widget\")\n            .clickOnText(\"Take Picture\")\n            .pressBack(FormEntryPage(\"All widgets\"))\n            .swipeToNextQuestion(\"Draw widget\")\n            .swipeToNextQuestion(\"Annotate widget\")\n            .swipeToNextQuestion(\"Signature widget\")\n            .swipeToNextQuestion(\"Barcode widget\")\n            .swipeToNextQuestion(\"Audio widget\")\n            .swipeToNextQuestion(\"Video widget\")\n            .swipeToNextQuestion(\"File widget\")\n\n            .swipeToNextQuestion(\"Date widget\")\n            .swipeToNextQuestion(\"Date Widget\")\n            .swipeToNextQuestion(\"Date widget\")\n            .swipeToNextQuestion(\"Date widget\")\n            .swipeToNextQuestion(\"Time widget\")\n            .swipeToNextQuestion(\"Date time widget\")\n            .swipeToNextQuestion(\"Date time widget\")\n            .swipeToNextQuestion(\"Ethiopian date widget\")\n            .swipeToNextQuestion(\"Coptic date widget\")\n            .swipeToNextQuestion(\"Islamic date widget\")\n            .swipeToNextQuestion(\"Bikram Sambat date widget\")\n            .swipeToNextQuestion(\"Myanmar date widget\")\n            .swipeToNextQuestion(\"Persian date widget\")\n\n            .swipeToNextQuestion(\"Geopoint widget\")\n            .swipeToNextQuestion(\"Geopoint widget\")\n            .swipeToNextQuestion(\"Geopoint widget\")\n            .swipeToNextQuestion(\"Geotrace widget\")\n            .clickOnString(string.get_line)\n            .pressBack(FormEntryPage(\"All widgets\"))\n            .swipeToNextQuestion(\"Geoshape widget\")\n            .clickOnString(string.get_polygon)\n            .pressBack(FormEntryPage(\"All widgets\"))\n            .swipeToNextQuestion(\"OSM integration\")\n\n            .swipeToNextQuestion(\"Select one widget\")\n            .swipeToNextQuestion(\"Spinner widget\")\n            .swipeToNextQuestion(\"Select one autoadvance widget\")\n            .swipeToNextQuestion(\"Select one search widget\")\n            .swipeToNextQuestion(\"Select one search widget\")\n            .swipeToNextQuestion(\"Grid select one widget\")\n            .swipeToNextQuestion(\"Grid select one widget\")\n            .swipeToNextQuestion(\"Grid select one widget\")\n            .swipeToNextQuestion(\"Grid select one widget\")\n            .swipeToNextQuestion(\"Grid select one widget\")\n            .swipeToNextQuestion(\"Image select one widget\")\n            .swipeToNextQuestion(\"Likert widget\")\n            .swipeToNextQuestion(\"Select one from map widget\")\n            .clickOnString(string.select_place)\n            .pressBack(FormEntryPage(\"All widgets\"))\n            .swipeToNextQuestion(\"Multi select widget\")\n            .swipeToNextQuestion(\"Multi select autocomplete widget\")\n            .swipeToNextQuestion(\"Grid select multiple widget\")\n            .swipeToNextQuestion(\"Grid select multiple widget\")\n            .swipeToNextQuestion(\"Spinner widget: select multiple\")\n            .swipeToNextQuestion(\"Image select multiple widget\")\n\n            .swipeToNextQuestion(\"Label widget\")\n            .swipeToNextQuestion(\"Rank widget\")\n            .swipeToNextQuestion(\"Trigger widget\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/smoke/BadServerTest.java",
    "content": "package org.odk.collect.android.feature.smoke;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.TestDependencies;\nimport org.odk.collect.android.support.TranslatedStringBuilder;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\nimport java.util.Arrays;\n\n@RunWith(AndroidJUnit4.class)\npublic class BadServerTest {\n\n    private final CollectTestRule rule = new CollectTestRule(false);\n    private final TestDependencies testDependencies = new TestDependencies();\n\n\n    @Rule\n    public RuleChain chain = TestRuleChain.chain(testDependencies).around(rule);\n\n    @Test\n    /*\n     The hash from the form list wasn't used for a long time so some server implementations\n     omitted it even though it's required by the spec. Now we explicitly show an error.\n    */\n    public void whenHashNotIncludedInFormList_showError() {\n        testDependencies.server.removeHashInFormList();\n        testDependencies.server.addForm(\"One Question\", \"one_question\", \"1\", \"one-question.xml\");\n\n        rule.withProject(testDependencies.server.getUrl())\n                .clickGetBlankForm()\n                .clickGetSelected()\n                .assertMessage(\"1 of 1 downloads failed!\")\n                .showDetails()\n                .assertError(new TranslatedStringBuilder()\n                        .addString(org.odk.collect.strings.R.string.form_with_no_hash_error)\n                        .addString(org.odk.collect.strings.R.string.report_to_project_lead)\n                        .build()\n                )\n                .navigateBack();\n    }\n\n    @Test\n    /*\n     A server that doesn't return hashes correctly for media files would fool Collect into thinking\n     there was a new one each time. This happens because we would compare the existing file's\n     (computed) hash with the server one assuming there is a prefix and end up comparing against\n     a substring.\n    */\n    public void whenMediaFileHasMissingPrefix_showsAsUpdated() {\n        testDependencies.server.removeMediaFileHashPrefix();\n        testDependencies.server.addForm(\"One Question\", \"one_question\", \"1\", \"one-question.xml\", Arrays.asList(\"fruits.csv\"));\n\n        rule.withProject(testDependencies.server.getUrl())\n                .copyForm(\"one-question.xml\", Arrays.asList(\"fruits.csv\"), testDependencies.server.getHostName())\n                .clickGetBlankForm()\n                .assertText(org.odk.collect.strings.R.string.newer_version_of_a_form_info);\n    }\n\n    @Test\n    /*\n     A server that doesn't return hashes based on the md5 of the file for media files\n     would fool Collect into thinking there was a new one each time. Collect should still redownload\n     the file in this case (there's nothing else it can do), but it should only identify the form\n     as being updated if the file actually changed.\n    */\n    public void whenMediaFileHasUnstableHash_butIsIdentical_doesNotShowAsUpdatedAfterRedownload() {\n        testDependencies.server.returnRandomMediaFileHash();\n        testDependencies.server.addForm(\"One Question\", \"one_question\", \"1\", \"one-question.xml\", Arrays.asList(\"fruits.csv\"));\n\n        rule.withProject(testDependencies.server.getUrl())\n                .copyForm(\"one-question.xml\", Arrays.asList(\"fruits.csv\"), testDependencies.server.getHostName())\n                .clickGetBlankForm()\n                .assertText(org.odk.collect.strings.R.string.newer_version_of_a_form_info)\n                .clickGetSelected()\n                .clickOKOnDialog(new MainMenuPage())\n                .clickFillBlankForm()\n                .assertTextThatContainsDoesNoExist(\"Updated on\")\n                .assertTextThatContainsExists(\"Added on\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/feature/smoke/GetAndSubmitFormTest.java",
    "content": "package org.odk.collect.android.feature.smoke;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.TestDependencies;\nimport org.odk.collect.android.support.pages.MainMenuPage;\nimport org.odk.collect.android.support.pages.SendFinalizedFormPage;\nimport org.odk.collect.android.support.rules.CollectTestRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\n\n@RunWith(AndroidJUnit4.class)\npublic class GetAndSubmitFormTest {\n\n    private final CollectTestRule rule = new CollectTestRule(false);\n    private final TestDependencies testDependencies = new TestDependencies();\n\n    @Rule\n    public RuleChain chain = TestRuleChain.chain(testDependencies)\n            .around(rule);\n\n    @Test\n    public void canGetBlankForm_fillItIn_andSubmit() {\n        testDependencies.server.addForm(\"One Question\", \"one-question\", \"1\", \"one-question.xml\");\n\n        rule.withProject(testDependencies.server.getUrl())\n                // Fetch form\n                .clickGetBlankForm()\n                .clickGetSelected()\n                .assertMessage(\"All downloads succeeded!\")\n                .clickOKOnDialog(new MainMenuPage())\n\n                // Fill out form\n                .startBlankForm(\"One Question\")\n                .swipeToEndScreen()\n                .clickFinalize()\n\n                // Send form\n                .clickSendFinalizedForm(1)\n                .clickSelectAll()\n                .clickSendSelected()\n                .assertText(\"One Question - Success\")\n                .clickOK(new SendFinalizedFormPage())\n                .assertTextDoesNotExist(\"One Question\")\n\n                // Back to the start\n                .pressBack(new MainMenuPage())\n                .assertNumberOfFinalizedForms(0);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/instrumented/forms/FormUtilsTest.java",
    "content": "package org.odk.collect.android.instrumented.forms;\n\nimport static org.mockito.Mockito.mock;\nimport static org.odk.collect.android.support.StorageUtils.copyFormToStorage;\nimport static java.util.Collections.emptyList;\n\nimport android.net.Uri;\n\nimport androidx.test.core.app.ApplicationProvider;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.reference.RootTranslator;\nimport org.javarosa.form.api.FormEntryController;\nimport org.javarosa.form.api.FormEntryModel;\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.odk.collect.android.external.FormsContract;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.rules.ResetStateRule;\nimport org.odk.collect.android.tasks.FormLoaderTask;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.FormUtils;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.instances.Instance;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.List;\n\npublic class FormUtilsTest {\n    private static final String BASIC_FORM = \"basic.xml\";\n\n    private final FormLoaderTask.FormEntryControllerFactory formEntryControllerFactory = new FormLoaderTask.FormEntryControllerFactory() {\n        @Override\n        public FormEntryController create(FormDef formDef, File formMediaDir, Instance instance) {\n            return new FormEntryController(new FormEntryModel(formDef));\n        }\n    };\n\n    @Rule\n    public ResetStateRule resetStateRule = new ResetStateRule();\n\n    @Before\n    public void setUp() throws IOException {\n        CollectHelpers.addDemoProject();\n        copyFormToStorage(BASIC_FORM, emptyList(), true);\n    }\n\n    /* Verify that each host string matches only a single root translator, allowing for them to\n     be defined in any order. See: https://github.com/getodk/collect/issues/3334\n    */\n    @Test\n    public void sessionRootTranslatorOrderDoesNotMatter() throws Exception {\n        final String formPath = new StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS) + File.separator + BASIC_FORM;\n        final Form form = new FormsRepositoryProvider(ApplicationProvider.getApplicationContext()).create().getOneByPath(formPath);\n        final Uri formUri = FormsContract.getUri(\"DEMO\", form.getDbId());\n\n        // Load the form in order to populate the ReferenceManager\n        FormLoaderTask formLoaderTask = new FormLoaderTask(formUri, FormsContract.CONTENT_ITEM_TYPE, null, null, formEntryControllerFactory, mock(), mock());\n        formLoaderTask.executeSynchronously();\n\n        final File formXml = new File(formPath);\n        final File formMediaDir = FileUtils.getFormMediaDir(formXml);\n        List<RootTranslator> rootTranslators = FormUtils.buildSessionRootTranslators(formMediaDir.getName(), FormUtils.enumerateHostStrings());\n\n        // Check each type of host string to determine that only one match is resolved.\n        for (String hostString : FormUtils.enumerateHostStrings()) {\n            String uri = String.format(\"jr://%s/test\", hostString);\n            int matchCount = 0;\n            for (RootTranslator rootTranslator : rootTranslators) {\n                if (rootTranslator.derives(uri)) {\n                    matchCount++;\n                }\n            }\n            Assert.assertEquals(\"Expected only a single match for URI: \" + uri, 1, matchCount);\n        }\n    }\n\n    /* Verify that the host strings appear in an order that does not allow for greedy matches, e.g.\n     matching 'file' instead of 'file-csv'. According to the behavior in the test above,\n     sessionRootTranslatorOrderDoesNotMatter, it is not actually a requirement to have the test\n     below pass. This simply follows the cautionary remarks in the following issue:\n     https://github.com/getodk/collect/issues/3334\n     */\n    @Test\n    public void hostStringsOrderedCorrectly() throws Exception {\n        String[] hostStrings = FormUtils.enumerateHostStrings();\n        // No host string should be a substring of the subsequent ones.\n        for (int i = 0; i < hostStrings.length; ++i) {\n            String currentHostString = hostStrings[i];\n            for (int j = i + 1; j < hostStrings.length; ++j) {\n                String subsequentHostString = hostStrings[j];\n                Assert.assertFalse(subsequentHostString.contains(currentHostString));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/instrumented/tasks/FormLoaderTaskTest.java",
    "content": "package org.odk.collect.android.instrumented.tasks;\n\nimport static org.mockito.Mockito.mock;\n\nimport android.app.Application;\nimport android.net.Uri;\n\nimport androidx.test.core.app.ApplicationProvider;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.form.api.FormEntryController;\nimport org.javarosa.form.api.FormEntryModel;\nimport org.junit.Assert;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.odk.collect.android.external.FormsContract;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.injection.config.AppDependencyComponent;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.support.StorageUtils;\nimport org.odk.collect.android.support.rules.RunnableRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.android.tasks.FormLoaderTask;\nimport org.odk.collect.android.tasks.FormLoaderTask.FormEntryControllerFactory;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.projects.Project;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Collections;\n\npublic class FormLoaderTaskTest {\n\n    private final StoragePathProvider storagePathProvider = new StoragePathProvider();\n\n    private static final String SECONDARY_INSTANCE_EXTERNAL_CSV_FORM = \"external_csv_form.xml\";\n    private static final String SIMPLE_SEARCH_EXTERNAL_CSV_FORM = \"simple-search-external-csv.xml\";\n    private static final String SIMPLE_SEARCH_EXTERNAL_CSV_FILE = \"simple-search-external-csv-fruits.csv\";\n    private static final String SIMPLE_SEARCH_EXTERNAL_DB_FILE = \"simple-search-external-csv-fruits.db\";\n\n    private final FormEntryControllerFactory formEntryControllerFactory = new FormEntryControllerFactory() {\n        @Override\n        public FormEntryController create(FormDef formDef, File formMediaDir, Instance instance) {\n            return new FormEntryController(new FormEntryModel(formDef));\n        }\n    };\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(new RunnableRule(() -> {\n                try {\n                    // Set up demo project\n                    AppDependencyComponent component = DaggerUtils.getComponent(ApplicationProvider.<Application>getApplicationContext());\n                    component.projectsRepository().save(Project.Companion.getDEMO_PROJECT());\n                    component.currentProjectProvider().setCurrentProject(Project.DEMO_PROJECT_ID);\n\n                    StorageUtils.copyFormToDemoProject(SECONDARY_INSTANCE_EXTERNAL_CSV_FORM, Arrays.asList(\"external_csv_cities.csv\", \"external_csv_countries.csv\", \"external_csv_neighbourhoods.csv\"), true);\n                    StorageUtils.copyFormToDemoProject(SIMPLE_SEARCH_EXTERNAL_CSV_FORM, Collections.singletonList(SIMPLE_SEARCH_EXTERNAL_CSV_FILE), true);\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            }));\n\n    // Validate that importing external data multiple times does not fail due to side effects from import\n    @Test\n    public void loadSearchFromExternalCSVmultipleTimes() throws Exception {\n        final String formPath = storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS) + File.separator + SIMPLE_SEARCH_EXTERNAL_CSV_FORM;\n        final Form form = new FormsRepositoryProvider(ApplicationProvider.getApplicationContext()).create().getOneByPath(formPath);\n        final Uri formUri = FormsContract.getUri(\"DEMO\", form.getDbId());\n\n        // initial load with side effects\n        FormLoaderTask formLoaderTask = new FormLoaderTask(formUri, FormsContract.CONTENT_ITEM_TYPE, null, null, formEntryControllerFactory, mock(), mock());\n        FormLoaderTask.FECWrapper wrapper = formLoaderTask.executeSynchronously();\n        Assert.assertNotNull(wrapper);\n        Assert.assertNotNull(wrapper.getController());\n\n        File mediaFolder = wrapper.getController().getMediaFolder();\n        File dbFile = new File(mediaFolder + File.separator + SIMPLE_SEARCH_EXTERNAL_DB_FILE);\n        Assert.assertTrue(dbFile.exists());\n        long dbLastModified = dbFile.lastModified();\n\n        // subsequent load should succeed despite side effects from import\n        formLoaderTask = new FormLoaderTask(formUri, FormsContract.CONTENT_ITEM_TYPE, null, null, formEntryControllerFactory, mock(), mock());\n        wrapper = formLoaderTask.executeSynchronously();\n        Assert.assertNotNull(wrapper);\n        Assert.assertNotNull(wrapper.getController());\n        Assert.assertEquals(\"expected file modification timestamp to be unchanged\", dbLastModified, dbFile.lastModified());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/CustomSQLiteQueryExecutionTest.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.instrumented.utilities;\n\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.RuleChain;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.support.rules.RunnableRule;\nimport org.odk.collect.android.support.rules.TestRuleChain;\nimport org.odk.collect.db.sqlite.CustomSQLiteQueryExecutor;\n\nimport java.io.File;\nimport java.io.IOException;\n\nimport static junit.framework.Assert.assertEquals;\nimport static junit.framework.Assert.assertFalse;\nimport static junit.framework.Assert.assertTrue;\n\n@RunWith(AndroidJUnit4.class)\npublic class CustomSQLiteQueryExecutionTest {\n\n    private static final String TEST_TABLE_NAME = \"testTable\";\n    private static final String TEST_TABLE_NAME_2 = \"testTable2\";\n    private static final String[] TABLE_COLUMNS = {\"_id\", \"col1\", \"col2\", \"col3\"};\n\n    private SQLiteDatabase sqLiteDatabase;\n\n    @Rule\n    public RuleChain copyFormChain = TestRuleChain.chain()\n            .around(new RunnableRule(() -> {\n                try {\n                    File dbPath = File.createTempFile(\"test\", \".db\");\n                    dbPath.deleteOnExit();\n                    sqLiteDatabase = SQLiteDatabase.openOrCreateDatabase(dbPath.getAbsolutePath(), null, null);\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            }));\n\n    @Test\n    public void dropTableTest() {\n        createTestTable();\n        assertTrue(tableExists(TEST_TABLE_NAME));\n        CustomSQLiteQueryExecutor\n                .begin(sqLiteDatabase)\n                .dropIfExists(TEST_TABLE_NAME)\n                .end();\n        assertFalse(tableExists(TEST_TABLE_NAME));\n    }\n\n    @Test\n    public void renameTableTest() {\n        createTestTable();\n        insertExampleData(TEST_TABLE_NAME);\n        assertTrue(tableExists(TEST_TABLE_NAME));\n        assertFalse(tableExists(TEST_TABLE_NAME_2));\n        checkValues(TEST_TABLE_NAME);\n        CustomSQLiteQueryExecutor\n                .begin(sqLiteDatabase)\n                .renameTable(TEST_TABLE_NAME)\n                .to(TEST_TABLE_NAME_2)\n                .end();\n        assertFalse(tableExists(TEST_TABLE_NAME));\n        assertTrue(tableExists(TEST_TABLE_NAME_2));\n        checkValues(TEST_TABLE_NAME_2);\n    }\n\n    @Test\n    public void copyTableTest() {\n        createTestTable();\n        insertExampleData(TEST_TABLE_NAME);\n        assertTrue(tableExists(TEST_TABLE_NAME));\n        assertFalse(tableExists(TEST_TABLE_NAME_2));\n        checkValues(TEST_TABLE_NAME);\n\n        CustomSQLiteQueryExecutor\n                .begin(sqLiteDatabase)\n                .renameTable(TEST_TABLE_NAME)\n                .to(TEST_TABLE_NAME_2)\n                .end();\n\n        assertFalse(tableExists(TEST_TABLE_NAME));\n        assertTrue(tableExists(TEST_TABLE_NAME_2));\n        checkValues(TEST_TABLE_NAME_2);\n\n        createTestTable();\n\n        CustomSQLiteQueryExecutor\n                .begin(sqLiteDatabase)\n                .insertInto(TEST_TABLE_NAME)\n                .columnsForInsert(TABLE_COLUMNS)\n                .select()\n                .columnsForSelect(TABLE_COLUMNS)\n                .from(TEST_TABLE_NAME_2)\n                .end();\n\n        checkValues(TEST_TABLE_NAME);\n\n        CustomSQLiteQueryExecutor\n                .begin(sqLiteDatabase)\n                .dropIfExists(TEST_TABLE_NAME_2)\n                .end();\n\n        assertTrue(tableExists(TEST_TABLE_NAME));\n        assertFalse(tableExists(TEST_TABLE_NAME_2));\n    }\n\n    @Test\n    public void addColumnTest() {\n        createTestTable();\n        assertTrue(tableExists(TEST_TABLE_NAME));\n\n        CustomSQLiteQueryExecutor\n                .begin(sqLiteDatabase)\n                .alter()\n                .table(TEST_TABLE_NAME)\n                .addColumn(\"col4\", \"text\")\n                .end();\n\n        Cursor cursor = sqLiteDatabase.query(TEST_TABLE_NAME, null, null, null, null, null, null);\n        String[] columnNames = cursor.getColumnNames();\n\n        assertEquals(5, columnNames.length);\n        assertEquals(\"col1\", columnNames[1]);\n        assertEquals(\"col2\", columnNames[2]);\n        assertEquals(\"col3\", columnNames[3]);\n        assertEquals(\"col4\", columnNames[4]);\n    }\n\n    private void checkValues(String tableName) {\n        Cursor cursor = sqLiteDatabase.rawQuery(\"SELECT * FROM \" + tableName, null);\n        int index = 1;\n        if (cursor != null) {\n            try {\n                while (cursor.moveToNext()) {\n                    assertEquals(String.valueOf(index), cursor.getString(cursor.getColumnIndex(\"_id\")));\n                    assertEquals(\"col\" + index + \"x1Value\", cursor.getString(cursor.getColumnIndex(\"col1\")));\n                    assertEquals(\"col\" + index + \"x2Value\", cursor.getString(cursor.getColumnIndex(\"col2\")));\n                    assertEquals(\"col\" + index + \"x3Value\", cursor.getString(cursor.getColumnIndex(\"col3\")));\n\n                    index++;\n                }\n            } finally {\n                cursor.close();\n            }\n        }\n    }\n\n    private void createTestTable() {\n        sqLiteDatabase.execSQL(\"CREATE TABLE \" + TEST_TABLE_NAME + \" (\"\n                + \"_id\" + \" integer primary key, \"\n                + \"col1\" + \" text not null, \"\n                + \"col2\" + \" text, \"\n                + \"col3\" + \" text);\");\n    }\n\n    private void insertExampleData(String tableName) {\n        sqLiteDatabase.execSQL(\"INSERT INTO \" + tableName + \" VALUES \"\n                + \"(1, 'col1x1Value', 'col1x2Value', 'col1x3Value'), \"\n                + \"(2, 'col2x1Value', 'col2x2Value', 'col2x3Value'), \"\n                + \"(3, 'col3x1Value', 'col3x2Value', 'col3x3Value');\");\n    }\n\n    private boolean tableExists(String tableName) {\n        boolean isExist = false;\n        Cursor cursor = sqLiteDatabase.rawQuery(\"select DISTINCT tbl_name from sqlite_master where tbl_name = '\" + tableName + \"'\", null);\n        if (cursor != null) {\n            if (cursor.getCount() > 0) {\n                isExist = true;\n            }\n            cursor.close();\n        }\n        return isExist;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/DateTimeUtilsTest.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.instrumented.utilities;\n\nimport android.content.Context;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.After;\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.testshared.TimeZoneSetter;\n\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Locale;\nimport java.util.TimeZone;\n\nimport static org.junit.Assert.assertEquals;\n\n@RunWith(AndroidJUnit4.class)\npublic class DateTimeUtilsTest {\n\n    private DatePickerDetails gregorianDatePickerDetails;\n    private DatePickerDetails ethiopianDatePickerDetails;\n    private DatePickerDetails copticDatePickerDetails;\n    private DatePickerDetails islamicDatePickerDetails;\n    private DatePickerDetails bikramSambatDatePickerDetails;\n    private DatePickerDetails myanmarDatePickerDetails;\n    private DatePickerDetails persianDatePickerDetails;\n    private DatePickerDetails buddhistDatePickerDetails;\n\n    private Context context;\n    private Locale defaultLocale;\n    private TimeZone defaultTimezone;\n\n    @Before\n    public void setUp() {\n        gregorianDatePickerDetails = new DatePickerDetails(DatePickerDetails.DatePickerType.GREGORIAN, DatePickerDetails.DatePickerMode.CALENDAR);\n        ethiopianDatePickerDetails = new DatePickerDetails(DatePickerDetails.DatePickerType.ETHIOPIAN, DatePickerDetails.DatePickerMode.SPINNERS);\n        copticDatePickerDetails = new DatePickerDetails(DatePickerDetails.DatePickerType.COPTIC, DatePickerDetails.DatePickerMode.SPINNERS);\n        islamicDatePickerDetails = new DatePickerDetails(DatePickerDetails.DatePickerType.ISLAMIC, DatePickerDetails.DatePickerMode.SPINNERS);\n        bikramSambatDatePickerDetails = new DatePickerDetails(DatePickerDetails.DatePickerType.BIKRAM_SAMBAT, DatePickerDetails.DatePickerMode.SPINNERS);\n        myanmarDatePickerDetails = new DatePickerDetails(DatePickerDetails.DatePickerType.MYANMAR, DatePickerDetails.DatePickerMode.SPINNERS);\n        persianDatePickerDetails = new DatePickerDetails(DatePickerDetails.DatePickerType.PERSIAN, DatePickerDetails.DatePickerMode.SPINNERS);\n        buddhistDatePickerDetails = new DatePickerDetails(DatePickerDetails.DatePickerType.BUDDHIST, DatePickerDetails.DatePickerMode.SPINNERS);\n\n        context = Collect.getInstance();\n        defaultLocale = Locale.getDefault();\n        defaultTimezone = TimeZone.getDefault();\n    }\n\n    @Test\n    public void getDateTimeLabelTest() {\n        Locale.setDefault(Locale.ENGLISH);\n        TimeZoneSetter.setTimezone(TimeZone.getTimeZone(\"GMT\"));\n\n        // 20 Oct 1991 14:00 GMT\n        Calendar calendar = Calendar.getInstance();\n        calendar.set(1991, 9, 20, 14, 0, 0);\n        Date date = calendar.getTime();\n\n        Assert.assertEquals(\"Oct 20, 1991\", DateTimeWidgetUtils.getDateTimeLabel(date, gregorianDatePickerDetails, false, context));\n        assertEquals(\"Oct 20, 1991, 14:00\", DateTimeWidgetUtils.getDateTimeLabel(date, gregorianDatePickerDetails, true, context));\n\n        assertEquals(\"9 Tikimt 1984 (Oct 20, 1991)\", DateTimeWidgetUtils.getDateTimeLabel(date, ethiopianDatePickerDetails, false, context));\n        assertEquals(\"9 Tikimt 1984, 14:00 (Oct 20, 1991, 14:00)\", DateTimeWidgetUtils.getDateTimeLabel(date, ethiopianDatePickerDetails, true, context));\n\n        assertEquals(\"9 Paopi 1708 (Oct 20, 1991)\", DateTimeWidgetUtils.getDateTimeLabel(date, copticDatePickerDetails, false, context));\n        assertEquals(\"9 Paopi 1708, 14:00 (Oct 20, 1991, 14:00)\", DateTimeWidgetUtils.getDateTimeLabel(date, copticDatePickerDetails, true, context));\n\n        assertEquals(\"11 Rabi' al-thani 1412 (Oct 20, 1991)\", DateTimeWidgetUtils.getDateTimeLabel(date, islamicDatePickerDetails, false, context));\n        assertEquals(\"11 Rabi' al-thani 1412, 14:00 (Oct 20, 1991, 14:00)\", DateTimeWidgetUtils.getDateTimeLabel(date, islamicDatePickerDetails, true, context));\n\n        assertEquals(\"3 कार्तिक 2048 (Oct 20, 1991)\", DateTimeWidgetUtils.getDateTimeLabel(date, bikramSambatDatePickerDetails, false, context));\n        assertEquals(\"3 कार्तिक 2048, 14:00 (Oct 20, 1991, 14:00)\", DateTimeWidgetUtils.getDateTimeLabel(date, bikramSambatDatePickerDetails, true, context));\n\n        assertEquals(\"12 သီတင်းကျွတ် 1353 (Oct 20, 1991)\", DateTimeWidgetUtils.getDateTimeLabel(date, myanmarDatePickerDetails, false, context));\n        assertEquals(\"12 သီတင်းကျွတ် 1353, 14:00 (Oct 20, 1991, 14:00)\", DateTimeWidgetUtils.getDateTimeLabel(date, myanmarDatePickerDetails, true, context));\n\n        assertEquals(\"28 Mehr 1370 (Oct 20, 1991)\", DateTimeWidgetUtils.getDateTimeLabel(date, persianDatePickerDetails, false, context));\n        assertEquals(\"28 Mehr 1370, 14:00 (Oct 20, 1991, 14:00)\", DateTimeWidgetUtils.getDateTimeLabel(date, persianDatePickerDetails, true, context));\n\n        assertEquals(\"20 ตุลาคม 2534 (Oct 20, 1991)\", DateTimeWidgetUtils.getDateTimeLabel(date, buddhistDatePickerDetails, false, context));\n        assertEquals(\"20 ตุลาคม 2534, 14:00 (Oct 20, 1991, 14:00)\", DateTimeWidgetUtils.getDateTimeLabel(date, buddhistDatePickerDetails, true, context));\n    }\n\n    @After\n    public void resetTimeZone() {\n        Locale.setDefault(defaultLocale);\n        TimeZoneSetter.setTimezone(defaultTimezone);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/ActivityHelpers.java",
    "content": "package org.odk.collect.android.support;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static org.hamcrest.core.AllOf.allOf;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.view.ContextThemeWrapper;\nimport android.view.View;\n\nimport androidx.test.espresso.UiController;\nimport androidx.test.espresso.ViewAction;\nimport androidx.test.platform.app.InstrumentationRegistry;\n\nimport org.hamcrest.Matcher;\nimport org.odk.collect.android.BuildConfig;\n\npublic final class ActivityHelpers {\n\n    private ActivityHelpers() {\n    }\n\n    public static Activity getActivity() {\n        final Activity[] currentActivity = new Activity[1];\n        ViewAction getActivityViewAction = new ViewAction() {\n            @Override\n            public Matcher<View> getConstraints() {\n                return isAssignableFrom(View.class);\n            }\n\n            @Override\n            public String getDescription() {\n                return \"getting text from a TextView\";\n            }\n\n            @Override\n            public void perform(UiController uiController, View view) {\n                if (view.getContext() instanceof Activity) {\n                    Activity activity = (Activity) view.getContext();\n                    currentActivity[0] = activity;\n                } else if (view.getContext() instanceof ContextThemeWrapper) {\n                    Activity activity = (Activity) ((ContextThemeWrapper) view.getContext()).getBaseContext();\n                    currentActivity[0] = activity;\n                }\n            }\n        };\n\n        onView(allOf(withId(android.R.id.content), isDisplayed())).perform(getActivityViewAction);\n        return currentActivity[0];\n    }\n\n    public static Intent getLaunchIntent() {\n        Intent launchIntentForPackage = InstrumentationRegistry.getInstrumentation()\n                .getTargetContext()\n                .getPackageManager()\n                .getLaunchIntentForPackage(BuildConfig.APPLICATION_ID);\n\n        launchIntentForPackage.addCategory(Intent.CATEGORY_LAUNCHER);\n        return launchIntentForPackage;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/CollectHelpers.kt",
    "content": "package org.odk.collect.android.support\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.injection.config.AppDependencyComponent\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.injection.config.DaggerAppDependencyComponent\nimport org.odk.collect.projects.Project\n\nobject CollectHelpers {\n    fun overrideAppDependencyModule(appDependencyModule: AppDependencyModule): AppDependencyComponent {\n        val application = ApplicationProvider.getApplicationContext<Collect>()\n        val testComponent = DaggerAppDependencyComponent.builder()\n            .application(application)\n            .appDependencyModule(appDependencyModule)\n            .build()\n        application.component = testComponent\n        return testComponent\n    }\n\n    fun simulateProcessRestart(appDependencyModule: AppDependencyModule? = null) {\n        ApplicationProvider.getApplicationContext<Collect>().getState().clear()\n\n        val newComponent =\n            overrideAppDependencyModule(appDependencyModule ?: AppDependencyModule())\n\n        // Reinitialize any application state with new deps/state\n        newComponent.applicationInitializer().initialize()\n    }\n\n    @JvmStatic\n    fun addDemoProject() {\n        val component =\n            DaggerUtils.getComponent(ApplicationProvider.getApplicationContext<Application>())\n        component.projectsRepository().save(Project.DEMO_PROJECT)\n        component.currentProjectProvider().setCurrentProject(Project.DEMO_PROJECT_ID)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/ContentProviderUtils.kt",
    "content": "package org.odk.collect.android.support\n\nimport android.app.Application\nimport android.provider.BaseColumns\nimport androidx.test.core.app.ApplicationProvider\nimport org.odk.collect.android.database.forms.DatabaseFormColumns\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.android.external.InstancesContract\n\nobject ContentProviderUtils {\n\n    fun getFormDatabaseId(projectId: String, formId: String): Long {\n        val contentResolver =\n            ApplicationProvider.getApplicationContext<Application>().contentResolver\n        val uri = FormsContract.getUri(projectId)\n        return contentResolver.query(uri, null, null, null, null, null).use {\n            if (it != null) {\n                var dbId: Long? = null\n                while (it.moveToNext()) {\n                    if (it.getString(it.getColumnIndex(DatabaseFormColumns.JR_FORM_ID)) == formId) {\n                        dbId = it.getLong(it.getColumnIndex(BaseColumns._ID))\n                    }\n                }\n\n                dbId ?: throw IllegalArgumentException(\"Form does not exist!\")\n            } else {\n                throw RuntimeException(\"Null cursor!\")\n            }\n        }\n    }\n\n    fun getInstanceDatabaseId(projectId: String, formId: String): Long {\n        val contentResolver =\n            ApplicationProvider.getApplicationContext<Application>().contentResolver\n        val uri = InstancesContract.getUri(projectId)\n        return contentResolver.query(uri, null, null, null, null, null).use {\n            if (it != null) {\n                var dbId: Long? = null\n                while (it.moveToNext()) {\n                    if (it.getString(it.getColumnIndex(DatabaseInstanceColumns.JR_FORM_ID)) == formId) {\n                        dbId = it.getLong(it.getColumnIndex(BaseColumns._ID))\n                    }\n                }\n\n                dbId ?: throw IllegalArgumentException(\"Form does not exist!\")\n            } else {\n                throw RuntimeException(\"Null cursor!\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/CountingTaskExecutorIdlingResource.java",
    "content": "package org.odk.collect.android.support;\n\nimport androidx.arch.core.executor.testing.CountingTaskExecutorRule;\nimport androidx.test.espresso.IdlingResource;\n\npublic class CountingTaskExecutorIdlingResource extends CountingTaskExecutorRule implements IdlingResource {\n\n    private IdlingResource.ResourceCallback resourceCallback;\n\n    @Override\n    public String getName() {\n        return CountingTaskExecutorIdlingResource.class.getName();\n    }\n\n    @Override\n    public boolean isIdleNow() {\n        return isIdle();\n    }\n\n    @Override\n    public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) {\n        this.resourceCallback = resourceCallback;\n    }\n\n    @Override\n    protected void onIdle() {\n        if (resourceCallback != null) {\n            resourceCallback.onTransitionToIdle();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/DummyActivityLauncher.kt",
    "content": "package org.odk.collect.android.support\n\nimport android.content.Intent\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.uiautomator.By\nimport androidx.test.uiautomator.UiDevice\nimport androidx.test.uiautomator.Until\nimport org.odk.collect.shared.TimeInMs\nimport org.odk.collect.testshared.DummyActivity\n\nobject DummyActivityLauncher {\n\n    fun launch(block: (UiDevice) -> Unit) {\n        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n        InstrumentationRegistry.getInstrumentation().targetContext.apply {\n            val intent = Intent(this.applicationContext, DummyActivity::class.java)\n            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            startActivity(intent)\n            device.wait(Until.hasObject(By.textStartsWith(DummyActivity.TEXT)), TimeInMs.ONE_SECOND)\n        }\n\n        block(device)\n        device.pressBack()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt",
    "content": "package org.odk.collect.android.support\n\nimport android.os.Handler\nimport android.os.Looper\nimport androidx.fragment.app.Fragment\nimport org.odk.collect.maps.traces.LineDescription\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.circles.CircleDescription\nimport org.odk.collect.maps.traces.PolygonDescription\nimport org.odk.collect.maps.markers.MarkerDescription\nimport org.odk.collect.maps.markers.MarkerIconDescription\n\nclass FakeClickableMapFragment : Fragment(), MapFragment {\n\n    private var idCounter = 1\n    private var featureClickListener: MapFragment.FeatureListener? = null\n\n    override fun init(\n        readyListener: MapFragment.ReadyListener?,\n        errorListener: MapFragment.ErrorListener?\n    ) {\n        readyListener?.onReady(this)\n    }\n\n    override fun getCenter(): MapPoint {\n        return MapPoint(0.0, 0.0)\n    }\n\n    override fun getZoom(): Double {\n        return 1.0\n    }\n\n    override fun setCenter(center: MapPoint?, animate: Boolean) {}\n    override fun zoomToCurrentLocation(center: MapPoint?) {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun zoomToPoint(center: MapPoint?, animate: Boolean) {}\n\n    override fun zoomToPoint(center: MapPoint?, zoom: Double, animate: Boolean) {}\n\n    override fun zoomToBoundingBox(\n        points: Iterable<MapPoint>,\n        scaleFactor: Double,\n        animate: Boolean\n    ) {}\n\n    override fun addMarker(markerDescription: MarkerDescription): Int {\n        val id = idCounter++\n        return id\n    }\n\n    override fun updateMarker(\n        featureId: Int,\n        markerDescription: MarkerDescription\n    ) {\n\n    }\n\n    override fun addMarkers(markers: List<MarkerDescription>): List<Int> {\n        return markers.map {\n            addMarker(it)\n        }\n    }\n\n    override fun setMarkerIcon(featureId: Int, markerIconDescription: MarkerIconDescription) {}\n\n    override fun getMarkerPoint(featureId: Int): MapPoint {\n        return MapPoint(0.0, 0.0)\n    }\n\n    override fun addPolyLine(lineDescription: LineDescription): Int {\n        return -1\n    }\n\n    override fun updatePolyLine(featureId: Int, lineDescription: LineDescription) {}\n\n    override fun addPolygon(polygonDescription: PolygonDescription): Int {\n        return -1\n    }\n\n    override fun updatePolygon(featureId: Int, polygonDescription: PolygonDescription) {}\n    override fun addCircle(circleDescription: CircleDescription): Int {\n        return -1\n    }\n\n    override fun updateCircle(\n        featureId: Int,\n        circleDescription: CircleDescription\n    ) {\n\n    }\n\n    override fun getPolyPoints(featureId: Int): MutableList<MapPoint> {\n        return mutableListOf()\n    }\n\n    override fun clearFeatures() {}\n\n    override fun setClickListener(listener: MapFragment.PointListener?) {}\n\n    override fun setLongPressListener(listener: MapFragment.PointListener?) {}\n\n    override fun setFeatureClickListener(listener: MapFragment.FeatureListener?) {\n        featureClickListener = listener\n    }\n\n    override fun setDragEndListener(listener: MapFragment.FeatureListener?) {}\n\n    override fun setGpsLocationEnabled(enabled: Boolean) {}\n\n    override fun getGpsLocation(): MapPoint? {\n        return null\n    }\n\n    override fun setGpsLocationListener(listener: MapFragment.PointListener?) {}\n\n    override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) {}\n\n    override fun hasCenter(): Boolean {\n        return false\n    }\n\n    fun clickOnFeature(featureId: Int) {\n        var done = false\n\n        Handler(Looper.getMainLooper()).post {\n            featureClickListener?.onFeature(featureId)\n            done = true\n        }\n\n        while (!done) {\n            Thread.sleep(1)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/FakeLocationClient.java",
    "content": "package org.odk.collect.android.support;\n\nimport android.location.Location;\n\nimport androidx.annotation.Nullable;\n\nimport com.google.android.gms.location.LocationListener;\n\nimport org.odk.collect.location.LocationClient;\n\npublic class FakeLocationClient implements LocationClient {\n\n    private LocationClientListener listener;\n    private LocationListener locationListener;\n    private Location lastLocation;\n\n    public void start(LocationClientListener listener) {\n        setListener(listener);\n        if (getListener() != null) {\n            getListener().onClientStart();\n        }\n    }\n\n    public void stop() {\n        stopLocationUpdates();\n        if (getListener() != null) {\n            getListener().onClientStop();\n        }\n        setListener(null);\n    }\n\n    public boolean isLocationAvailable() {\n        return true;\n    }\n\n    public void requestLocationUpdates(LocationListener locationListener) {\n        this.locationListener = locationListener;\n        updateLocationListener();\n    }\n\n    public void stopLocationUpdates() {\n        this.locationListener = null;\n    }\n\n    @Override\n    public void setListener(@Nullable LocationClientListener locationClientListener) {\n        this.listener = locationClientListener;\n    }\n\n    public Location getLastLocation() {\n        return lastLocation;\n    }\n\n    public boolean isMonitoringLocation() {\n        return locationListener != null;\n    }\n\n    public void setPriority(Priority priority) { }\n\n    @Override\n    public void setRetainMockAccuracy(boolean retainMockAccuracy) {\n        throw new UnsupportedOperationException();\n    }\n\n    public void setUpdateInterval(long updateInterval) { }\n\n    public void setLocation(Location location) {\n        this.lastLocation = location;\n        updateLocationListener();\n    }\n\n    private void updateLocationListener() {\n        if (this.locationListener != null && lastLocation != null) {\n            this.locationListener.onLocationChanged(lastLocation);\n        }\n    }\n\n    private LocationClientListener getListener() {\n        return listener;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/FakeNetworkStateProvider.kt",
    "content": "package org.odk.collect.android.support\n\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.network.NetworkStateProvider\n\nclass FakeNetworkStateProvider : NetworkStateProvider {\n\n    private var type: Scheduler.NetworkType? = Scheduler.NetworkType.WIFI\n\n    fun goOnline(networkType: Scheduler.NetworkType) {\n        type = networkType\n    }\n\n    fun goOffline() {\n        type = null\n    }\n\n    override val currentNetwork: Scheduler.NetworkType?\n        get() = type\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/StorageUtils.kt",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.support\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.platform.app.InstrumentationRegistry\nimport org.apache.commons.csv.CSVFormat\nimport org.apache.commons.csv.CSVRecord\nimport org.odk.collect.android.formmanagement.LocalFormUseCases\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.storage.StorageSubdirectory\nimport org.odk.collect.android.utilities.FileUtils\nimport java.io.File\nimport java.io.FileReader\nimport java.io.IOException\nimport java.util.Arrays\n\n/**\n * Emulates the process of copying a form via ADB\n */\nobject StorageUtils {\n    /**\n     * Copies a form with the given file name and given associated media to the SD Card.\n     *\n     * @param copyToDatabase if true the forms will be loaded into the database as if a form list\n     * had been opened.\n     */\n    @JvmStatic\n    @JvmOverloads\n    @Throws(IOException::class)\n    fun copyFormToStorage(\n        formFilename: String,\n        mediaFilePaths: List<String>? = null,\n        copyToDatabase: Boolean = false,\n        copyTo: String = formFilename,\n        projectName: String = \"Demo project\"\n    ) {\n        copyForm(formFilename, copyTo, projectName)\n        if (mediaFilePaths != null) {\n            copyFormMediaFiles(formFilename, mediaFilePaths, projectName)\n        }\n\n        if (copyToDatabase) {\n            val component = DaggerUtils.getComponent(ApplicationProvider.getApplicationContext<Application>())\n            val formsRepository = component.formsRepositoryProvider().create()\n            val formsDir = component.storagePathProvider().getOdkDirPath(StorageSubdirectory.FORMS)\n            LocalFormUseCases.synchronizeWithDisk(formsRepository, formsDir)\n        }\n    }\n\n    /**\n     * Copies a form with the given file name to the SD Card where it will be loaded by\n     * [FormLoaderTask].\n     */\n    @JvmStatic\n    @JvmOverloads\n    @Throws(IOException::class)\n    fun copyFormToDemoProject(formFilename: String, mediaFilePaths: List<String>? = null, copyToDatabase: Boolean = false, copyTo: String? = null) {\n        copyFormToStorage(formFilename, mediaFilePaths, copyToDatabase, copyTo ?: formFilename, \"Demo project\")\n    }\n\n    @JvmStatic\n    @Throws(IOException::class)\n    fun copyFormToDemoProject(formFilename: String, copyTo: String?) {\n        copyFormToDemoProject(formFilename, null, copyTo = copyTo)\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    @Throws(IOException::class)\n    fun copyInstance(instanceFileName: String, projectName: String) {\n        val instanceDirPath = getInstancesDirPath(projectName) + instanceFileName.split(\"\\\\.\".toRegex()).toTypedArray()[0]\n        File(instanceDirPath).mkdir()\n        FileUtils.copyFileFromAssets(\n            InstrumentationRegistry.getInstrumentation().getContext(),\n            \"$instanceDirPath/$instanceFileName\",\n            \"instances/$instanceFileName\"\n        )\n    }\n\n    @Throws(IOException::class)\n    private fun copyForm(formFilename: String, copyTo: String, projectName: String): String {\n        val pathname = getFormsDirPath(projectName) + copyTo\n        FileUtils.copyFileFromResources(\n            \"forms/$formFilename\",\n            pathname\n        )\n        return pathname\n    }\n\n    @Throws(IOException::class)\n    private fun copyFormMediaFiles(formFilename: String, mediaFilePaths: List<String>, projectName: String) {\n        val mediaPathName = getFormsDirPath(projectName) + formFilename.replace(\".xml\", \"\") + org.odk.collect.android.utilities.FileUtils.MEDIA_SUFFIX + \"/\"\n        org.odk.collect.android.utilities.FileUtils.checkMediaPath(File(mediaPathName))\n        for (mediaFilePath in mediaFilePaths) {\n            FileUtils.copyFileFromResources(\n                \"media/$mediaFilePath\",\n                mediaPathName + getMediaFileName(mediaFilePath)\n            )\n        }\n    }\n\n    private fun getMediaFileName(mediaFilePath: String): String {\n        return if (mediaFilePath.contains(File.separator)) mediaFilePath.substring(mediaFilePath.indexOf(File.separator) + 1) else mediaFilePath\n    }\n\n    /**\n     * @return the forms dir path that the user would expect (from docs)\n     */\n    private fun getFormsDirPath(projectName: String): String {\n        val path = getProjectPath(projectName) + \"/forms/\"\n        File(path).mkdirs()\n        return path\n    }\n\n    /**\n     * @return the instances dir path that the user would expect (from docs)\n     */\n    @JvmStatic\n    @JvmOverloads\n    fun getInstancesDirPath(projectName: String = \"Demo project\"): String {\n        val path = getProjectPath(projectName) + \"/instances/\"\n        File(path).mkdirs()\n        return path\n    }\n\n    private fun getProjectPath(projectName: String): String {\n        val externalFilesDir = ApplicationProvider.getApplicationContext<Application>().getExternalFilesDir(null)\n        val projectsDirPath = externalFilesDir.toString() + File.separator + \"projects\"\n        if (projectName == \"Demo project\") {\n            return projectsDirPath + File.separator + \"DEMO\"\n        } else {\n            for (projectDir in File(projectsDirPath).listFiles()) {\n                if (Arrays.stream(projectDir.listFiles()).anyMatch { file: File -> file.name == projectName }) {\n                    return projectDir.absolutePath\n                }\n            }\n        }\n        throw IllegalArgumentException(\"No project on disk with that name!\")\n    }\n\n    @Throws(IOException::class)\n    @JvmStatic\n    public fun getAuditLogForFirstInstance(): List<CSVRecord> {\n        val instanceDir = File(getInstancesDirPath()).listFiles()[0]\n        val auditLog = Arrays.stream(instanceDir.listFiles())\n            .filter { file: File -> file.name == \"audit.csv\" }\n            .findFirst()\n            .get()\n\n        val records = FileReader(auditLog).use { auditLogReader ->\n            CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(auditLogReader).use { parser ->\n                parser.records\n            }\n        }\n\n        return records\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/StubOpenRosaServer.kt",
    "content": "package org.odk.collect.android.support\n\nimport org.odk.collect.android.formmanagement.metadata.FormMetadataParser.readMetadata\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.openrosa.http.CaseInsensitiveEmptyHeaders\nimport org.odk.collect.openrosa.http.CaseInsensitiveHeaders\nimport org.odk.collect.openrosa.http.HttpCredentialsInterface\nimport org.odk.collect.openrosa.http.HttpGetResult\nimport org.odk.collect.openrosa.http.HttpHeadResult\nimport org.odk.collect.openrosa.http.HttpPostResult\nimport org.odk.collect.openrosa.http.OpenRosaConstants\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface\nimport org.odk.collect.shared.strings.Md5.getMd5Hash\nimport org.odk.collect.shared.strings.RandomString\nimport java.io.ByteArrayInputStream\nimport java.io.File\nimport java.io.IOException\nimport java.io.InputStream\nimport java.net.URI\nimport java.util.UUID\nimport java.util.stream.Collectors\n\nclass StubOpenRosaServer : OpenRosaHttpInterface {\n    private val forms: MutableList<XFormItem> = mutableListOf()\n    private var username: String? = null\n    private var password: String? = null\n    private var alwaysReturnError = false\n    private var fetchingFormsError = false\n    private var noHashInFormList = false\n    private var noHashPrefixInMediaFiles = false\n    private var randomHash = false\n\n    /**\n     * A list of submitted forms, maintained in the original order of submission, with the oldest forms appearing first.\n     */\n    private val submittedForms: MutableList<File> = mutableListOf()\n    private val deletedEntities: MutableList<String> = mutableListOf()\n    private var includeIntegrityUrl = false\n\n    val hostName = \"server.example.com\"\n    val url: String\n        get() = \"https://$hostName\"\n\n    val submissions: List<File>\n        get() = submittedForms\n\n    var accesses = 0\n        private set\n\n    override fun executeGetRequest(\n        uri: URI,\n        contentType: String?,\n        credentials: HttpCredentialsInterface\n    ): HttpGetResult {\n        accesses += 1\n\n        if (alwaysReturnError) {\n            return HttpGetResult(null, HashMap(), \"\", 500)\n        }\n\n        if (uri.host != hostName) {\n            return HttpGetResult(\n                null,\n                HashMap(),\n                \"Trying to connect to incorrect server: \" + uri.host,\n                410\n            )\n        } else if (credentialsIncorrect(credentials)) {\n            return HttpGetResult(null, HashMap(), \"\", 401)\n        } else if (uri.path == OpenRosaConstants.FORM_LIST) {\n            return HttpGetResult(formListResponse, standardHeaders, \"\", 200)\n        } else if (uri.path == \"/form\") {\n            if (fetchingFormsError) {\n                return HttpGetResult(null, HashMap(), \"\", 500)\n            }\n\n            return HttpGetResult(getFormResponse(uri), standardHeaders, \"\", 200)\n        } else if (uri.path == \"/manifest\") {\n            val manifestResponse = getManifestResponse(uri)\n\n            return if (manifestResponse != null) {\n                HttpGetResult(manifestResponse, standardHeaders, \"\", 200)\n            } else {\n                HttpGetResult(null, HashMap(), \"\", 404)\n            }\n        } else if (uri.path.startsWith(\"/mediaFile\")) {\n            return HttpGetResult(getMediaFile(uri), HashMap(), \"\", 200)\n        } else if (uri.path.startsWith(\"/integrityUrl\")) {\n            return HttpGetResult(getIntegrityResponse(uri), standardHeaders, \"\", 200)\n        } else {\n            return HttpGetResult(null, HashMap(), \"\", 404)\n        }\n    }\n\n    override fun executeHeadRequest(\n        uri: URI,\n        credentials: HttpCredentialsInterface\n    ): HttpHeadResult {\n        accesses += 1\n\n        if (alwaysReturnError) {\n            return HttpHeadResult(500, CaseInsensitiveEmptyHeaders())\n        }\n\n        if (uri.host != hostName) {\n            return HttpHeadResult(410, CaseInsensitiveEmptyHeaders())\n        } else if (credentialsIncorrect(credentials)) {\n            return HttpHeadResult(401, CaseInsensitiveEmptyHeaders())\n        } else if (uri.path == OpenRosaConstants.SUBMISSION) {\n            val headers = standardHeaders\n            headers[\"x-openrosa-accept-content-length\"] = \"10485760\"\n\n            return HttpHeadResult(204, MapHeaders(headers))\n        } else {\n            return HttpHeadResult(404, CaseInsensitiveEmptyHeaders())\n        }\n    }\n\n    override fun uploadSubmissionAndFiles(\n        submissionFile: File,\n        fileList: List<File>,\n        uri: URI,\n        credentials: HttpCredentialsInterface,\n        contentLength: Long\n    ): HttpPostResult {\n        accesses += 1\n\n        if (alwaysReturnError) {\n            return HttpPostResult(\"\", 500, \"\")\n        }\n\n        if (uri.host != hostName) {\n            return HttpPostResult(\"Trying to connect to incorrect server: \" + uri.host, 410, \"\")\n        } else if (credentialsIncorrect(credentials)) {\n            return HttpPostResult(\"\", 401, \"\")\n        } else if (uri.path == OpenRosaConstants.SUBMISSION) {\n            submittedForms.add(submissionFile)\n            return HttpPostResult(\"\", 201, \"\")\n        } else {\n            return HttpPostResult(\"\", 404, \"\")\n        }\n    }\n\n    fun setCredentials(username: String?, password: String?) {\n        this.username = username\n        this.password = password\n    }\n\n    fun addForm(formLabel: String?, id: String?, version: String?, formXML: String) {\n        forms.add(XFormItem(formLabel, formXML, id, version))\n    }\n\n    fun addForm(\n        formLabel: String?,\n        id: String?,\n        version: String?,\n        formXML: String,\n        mediaFiles: List<String>\n    ) {\n        forms.add(\n            XFormItem(\n                formLabel,\n                formXML,\n                id,\n                version,\n                mediaFiles.stream().map { name: String -> MediaFileItem(name, name) }.collect(\n                    Collectors.toList()\n                )\n            )\n        )\n    }\n\n    fun addForm(formXML: String, mediaFiles: List<MediaFileItem> = emptyList()) {\n        try {\n            FileUtils.getResourceAsStream(\"forms/$formXML\").use { formDefStream ->\n                addFormFromInputStream(\n                    formXML, mediaFiles,\n                    formDefStream!!\n                )\n            }\n        } catch (e: IOException) {\n            throw RuntimeException(e)\n        }\n    }\n\n    fun removeForm(formLabel: String) {\n        forms.removeIf { xFormItem: XFormItem -> xFormItem.formLabel == formLabel }\n    }\n\n    fun alwaysReturnError() {\n        alwaysReturnError = true\n    }\n\n    fun neverReturnError() {\n        alwaysReturnError = false\n    }\n\n    fun errorOnFetchingForms() {\n        fetchingFormsError = true\n    }\n\n    fun removeHashInFormList() {\n        noHashInFormList = true\n    }\n\n    fun removeMediaFileHashPrefix() {\n        noHashPrefixInMediaFiles = true\n    }\n\n    fun returnRandomMediaFileHash() {\n        randomHash = true\n    }\n\n    fun includeIntegrityUrl() {\n        includeIntegrityUrl = true\n    }\n\n    private fun credentialsIncorrect(credentials: HttpCredentialsInterface?): Boolean {\n        return if (username == null && password == null) {\n            false\n        } else {\n            if (credentials == null) {\n                true\n            } else {\n                credentials.username != username || credentials.password != password\n            }\n        }\n    }\n\n    private val standardHeaders: HashMap<String, String>\n        get() {\n            val headers =\n                HashMap<String, String>()\n            headers[\"x-openrosa-version\"] = \"1.0\"\n            return headers\n        }\n\n    private val formListResponse: InputStream\n        get() {\n            val stringBuilder = StringBuilder()\n            stringBuilder\n                .append(\"<?xml version='1.0' encoding='UTF-8' ?>\\n\")\n                .append(\"<xforms xmlns=\\\"http://openrosa.org/xforms/xformsList\\\">\\n\")\n\n            for (i in forms.indices) {\n                val form = forms[i]\n\n                val xform = stringBuilder\n                    .append(\"<xform>\\n\")\n                    .append(\"<formID>\" + form.iD + \"</formID>\\n\")\n                    .append(\"<name>\" + form.formLabel + \"</name>\\n\")\n                    .append(\"<version>\" + form.version + \"</version>\\n\")\n\n                if (!noHashInFormList) {\n                    val hash = getMd5Hash(getFormXML(i.toString()))\n                    xform.append(\"<hash>md5:$hash</hash>\\n\")\n                }\n\n                xform.append(\"<downloadUrl>$url/form?formId=$i</downloadUrl>\\n\")\n\n                if (form.mediaFiles.isNotEmpty()) {\n                    xform.append(\"<manifestUrl>$url/manifest?formId=$i</manifestUrl>\\n\")\n                }\n\n                stringBuilder.append(\"</xform>\\n\")\n            }\n\n            stringBuilder.append(\"</xforms>\")\n            return ByteArrayInputStream(stringBuilder.toString().toByteArray())\n        }\n\n    private fun getFormResponse(uri: URI): InputStream {\n        val formID = uri.query.split(\"=\".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]\n        return getFormXML(formID)\n    }\n\n    private fun getManifestResponse(uri: URI): InputStream? {\n        val formID = uri.query.split(\"=\".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]\n        val xformItem = forms[formID.toInt()]\n\n        if (xformItem.mediaFiles.isEmpty()) {\n            return null\n        } else {\n            val stringBuilder = StringBuilder()\n            stringBuilder\n                .append(\"<?xml version='1.0' encoding='UTF-8' ?>\\n\")\n                .append(\"<manifest xmlns=\\\"http://openrosa.org/xforms/xformsManifest\\\">\\n\")\n\n            for (mediaFile in xformItem.mediaFiles) {\n                val mediaFileHash = mediaFile.hash\n                    ?: if (randomHash) {\n                        RandomString.randomString(8)\n                    } else {\n                        getMd5Hash(FileUtils.getResourceAsStream(\"media/\" + mediaFile.file)!!)\n                    }\n\n                if (mediaFile is EntityListItem) {\n                    if (mediaFile.isApprovalList) {\n                        stringBuilder\n                            .append(\"<mediaFile type=\\\"approvalEntityList\\\">\")\n                    } else {\n                        stringBuilder\n                            .append(\"<mediaFile type=\\\"entityList\\\">\")\n                    }\n                } else {\n                    stringBuilder\n                        .append(\"<mediaFile>\")\n                }\n\n                stringBuilder\n                    .append(\"<filename>\" + mediaFile.name + \"</filename>\\n\")\n\n                if (noHashPrefixInMediaFiles) {\n                    stringBuilder.append(\"<hash>$mediaFileHash </hash>\\n\")\n                } else if (mediaFile is EntityListItem) {\n                    stringBuilder.append(\"<hash>md5:\" + mediaFile.version + \" </hash>\\n\")\n                } else {\n                    stringBuilder.append(\"<hash>md5:$mediaFileHash </hash>\\n\")\n                }\n\n                stringBuilder\n                    .append(\"<downloadUrl>\" + url + \"/mediaFile/\" + formID + \"/\" + mediaFile.id + \"</downloadUrl>\\n\")\n\n                if (mediaFile is EntityListItem && includeIntegrityUrl) {\n                    stringBuilder.append(\"<integrityUrl>\" + url + \"/integrityUrl</integrityUrl>\\n\")\n                }\n\n                stringBuilder.append(\"</mediaFile>\\n\")\n            }\n\n            stringBuilder.append(\"</manifest>\")\n            return ByteArrayInputStream(stringBuilder.toString().toByteArray())\n        }\n    }\n\n    private fun getIntegrityResponse(uri: URI): InputStream {\n        val ids = uri.query.split(\"=\".toRegex()).dropLastWhile { it.isEmpty() }\n            .toTypedArray()[1].split(\",\".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()\n\n        val stringBuilder = StringBuilder()\n        stringBuilder\n            .append(\"<?xml version='1.0' encoding='UTF-8' ?>\\n\")\n            .append(\"<data>\\n\")\n            .append(\"<entities>\\n\")\n\n        for (id in ids) {\n            stringBuilder\n                .append(\"<entity id=\\\"$id\\\">\\n\")\n                .append(\"<deleted>${deletedEntities.contains(id)}</deleted>\\n\")\n                .append(\"</entity>\\n\")\n        }\n\n        stringBuilder\n            .append(\"</entities>\\n\")\n            .append(\"</data>\\n\")\n\n        return ByteArrayInputStream(stringBuilder.toString().toByteArray())\n    }\n\n    private fun getFormXML(formID: String): InputStream {\n        val xmlPath = forms[formID.toInt()].formXML\n        return FileUtils.getResourceAsStream(\"forms/$xmlPath\")!!\n    }\n\n    private fun getMediaFile(uri: URI): InputStream {\n        val formID = uri.path.split(\"/mediaFile/\".toRegex()).dropLastWhile { it.isEmpty() }\n            .toTypedArray()[1].split(\"/\".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0]\n        val id = uri.path.split(\"/mediaFile/\".toRegex()).dropLastWhile { it.isEmpty() }\n            .toTypedArray()[1].split(\"/\".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]\n        val xformItem = forms[formID.toInt()]\n        val actualFileName =\n            xformItem.mediaFiles.stream().filter { mediaFile: MediaFileItem -> mediaFile.id == id }\n                .findFirst().get().file\n        return FileUtils.getResourceAsStream(\"media/$actualFileName\")!!\n    }\n\n    private fun addFormFromInputStream(\n        formXML: String,\n        mediaFiles: List<MediaFileItem>,\n        formDefStream: InputStream\n    ) {\n        val formMetadata = readMetadata(formDefStream)\n        forms.add(\n            XFormItem(\n                formMetadata.title,\n                formXML,\n                formMetadata.id,\n                formMetadata.version,\n                mediaFiles\n            )\n        )\n    }\n\n    fun deleteEntity(id: String) {\n        deletedEntities.add(id)\n    }\n\n    private class XFormItem(\n        val formLabel: String?,\n        val formXML: String,\n        val iD: String?,\n        val version: String?,\n        val mediaFiles: List<MediaFileItem> = emptyList()\n    )\n\n    open class MediaFileItem(\n        val name: String,\n        val file: String = name,\n        val hash: String? = null\n    ) {\n        val id: String = UUID.randomUUID().toString()\n    }\n\n    class EntityListItem : MediaFileItem {\n        var version: Int = 0\n            private set\n        var isApprovalList: Boolean = false\n            private set\n\n        constructor(\n            name: String,\n            file: String,\n            version: Int,\n            approvalList: Boolean = false\n        ) : super(name, file) {\n            this.version = version\n            this.isApprovalList = approvalList\n        }\n\n        constructor(name: String) : super(name, name, name)\n\n        constructor(name: String, approvalList: Boolean) : this(name, name, 0, approvalList)\n\n        fun incrementVersion() {\n            version++\n        }\n    }\n\n    private class MapHeaders(private val headers: Map<String, String>) :\n        CaseInsensitiveHeaders {\n        override fun getHeaders(): Set<String> {\n            return headers.keys\n        }\n\n        override fun containsHeader(header: String): Boolean {\n            return headers.containsKey(header.lowercase())\n        }\n\n        override fun getAnyValue(header: String): String? {\n            return headers[header.lowercase()]\n        }\n\n        override fun getValues(header: String): List<String?> {\n            return listOf(headers[header.lowercase()])\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/SubmissionParser.kt",
    "content": "package org.odk.collect.android.support\n\nimport org.javarosa.xform.parse.XFormParser\nimport java.io.File\n\nobject SubmissionParser {\n    fun getMetaIds(file: File): Pair<String, String?> {\n        val formRootElement = XFormParser.getXMLDocument(file.inputStream().reader()).rootElement\n        val formMetaElement = formRootElement.getElement(null, \"meta\")\n        val instanceID = formMetaElement.getElement(null, \"instanceID\").getText(0)\n        val deprecatedID = try {\n            formMetaElement.getElement(null, \"deprecatedID\").getText(0)\n        } catch (e: Exception) {\n            null\n        }\n\n        return Pair(instanceID, deprecatedID)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/TestDependencies.kt",
    "content": "package org.odk.collect.android.support\n\nimport android.app.Application\nimport android.content.Context\nimport android.content.RestrictionsManager\nimport android.os.Bundle\nimport android.webkit.MimeTypeMap\nimport androidx.work.WorkManager\nimport org.mockito.Mockito.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.support.async.TrackingCoroutineAndWorkManagerScheduler\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.androidshared.system.BroadcastReceiverRegister\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.network.NetworkStateProvider\nimport org.odk.collect.audioclips.AudioPlayerFactory\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.testshared.FakeAudioPlayerFactory\nimport org.odk.collect.testshared.FakeBarcodeScannerViewFactory\nimport org.odk.collect.testshared.FakeBroadcastReceiverRegister\nimport org.odk.collect.testshared.MockWebPageService\nimport org.odk.collect.utilities.UserAgentProvider\nimport org.odk.collect.webpage.WebPageService\n\nopen class TestDependencies @JvmOverloads constructor(\n    private val useRealServer: Boolean = false\n) : AppDependencyModule() {\n    @JvmField val server: StubOpenRosaServer = StubOpenRosaServer()\n\n    @JvmField val storagePathProvider: StoragePathProvider = StoragePathProvider()\n\n    val networkStateProvider: FakeNetworkStateProvider = FakeNetworkStateProvider()\n    val scheduler: TrackingCoroutineAndWorkManagerScheduler =\n        TrackingCoroutineAndWorkManagerScheduler(networkStateProvider)\n    val fakeBarcodeScannerViewFactory = FakeBarcodeScannerViewFactory()\n    val broadcastReceiverRegister: FakeBroadcastReceiverRegister = FakeBroadcastReceiverRegister()\n    val restrictionsManager: RestrictionsManager = mock<RestrictionsManager>().apply {\n        whenever(applicationRestrictions).thenReturn(Bundle())\n    }\n    val audioPlayerFactory = FakeAudioPlayerFactory()\n    val webPageService = MockWebPageService()\n\n    override fun provideHttpInterface(\n        mimeTypeMap: MimeTypeMap,\n        userAgentProvider: UserAgentProvider,\n        application: Application,\n        versionInformation: VersionInformation\n    ): OpenRosaHttpInterface {\n        return if (useRealServer) {\n            super.provideHttpInterface(\n                mimeTypeMap,\n                userAgentProvider,\n                application,\n                versionInformation\n            )\n        } else {\n            server\n        }\n    }\n\n    override fun providesScheduler(workManager: WorkManager, application: Application): Scheduler {\n        return scheduler\n    }\n\n    override fun providesBarcodeScannerViewFactory(settingsProvider: SettingsProvider): BarcodeScannerViewContainer.Factory {\n        return fakeBarcodeScannerViewFactory\n    }\n\n    override fun providesNetworkStateProvider(context: Context): NetworkStateProvider {\n        return networkStateProvider\n    }\n\n    override fun providesBroadcastReceiverRegister(context: Context): BroadcastReceiverRegister {\n        return broadcastReceiverRegister\n    }\n\n    override fun providesRestrictionsManager(context: Context): RestrictionsManager {\n        return restrictionsManager\n    }\n\n    override fun providesAudioPlayerFactory(scheduler: Scheduler): AudioPlayerFactory {\n        return audioPlayerFactory\n    }\n\n    override fun providesWebPageService(): WebPageService {\n        return webPageService\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/TranslatedStringBuilder.kt",
    "content": "package org.odk.collect.android.support\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport org.odk.collect.strings.localization.getLocalizedString\n\nclass TranslatedStringBuilder(private val separator: String = \" \") {\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n    private var string = \"\"\n\n    fun addString(stringId: Int, vararg formatArgs: Any): TranslatedStringBuilder {\n        string += if (string.isEmpty()) {\n            context.getLocalizedString(stringId, formatArgs)\n        } else {\n            separator + context.getLocalizedString(stringId, formatArgs)\n        }\n\n        return this\n    }\n\n    fun build(): String {\n        return string\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/actions/RotateAction.java",
    "content": "package org.odk.collect.android.support.actions;\n\nimport static androidx.test.espresso.matcher.ViewMatchers.isRoot;\nimport static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;\n\nimport android.content.pm.ActivityInfo;\nimport android.os.RemoteException;\nimport android.view.View;\n\nimport androidx.test.espresso.UiController;\nimport androidx.test.espresso.ViewAction;\nimport androidx.test.uiautomator.UiDevice;\n\nimport org.hamcrest.Matcher;\n\npublic class RotateAction implements ViewAction {\n\n    private final int screenOrientation;\n\n    public RotateAction(int screenOrientation) {\n        this.screenOrientation = screenOrientation;\n    }\n\n    @Override\n    public Matcher<View> getConstraints() {\n        return isRoot();\n    }\n\n    @Override\n    public String getDescription() {\n        return \"\";\n    }\n\n    @Override\n    public void perform(UiController uiController, View view) {\n        UiDevice device = UiDevice.getInstance(getInstrumentation());\n\n        try {\n            if (screenOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {\n                device.setOrientationLeft();\n            } else {\n                device.setOrientationNatural();\n            }\n        } catch (RemoteException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/async/AsyncWorkTracker.kt",
    "content": "package org.odk.collect.android.support.async\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nobject AsyncWorkTracker {\n    private val runningTasks = AtomicInteger(0)\n\n    @JvmStatic\n    val taskCount: Int\n        get() {\n            return runningTasks.get()\n        }\n\n    fun startWork() {\n        runningTasks.incrementAndGet()\n    }\n\n    fun finishWork() {\n        val decremented = runningTasks.decrementAndGet()\n        if (decremented < 0) {\n            throw IllegalStateException()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/async/AsyncWorkTrackerIdlingResource.kt",
    "content": "package org.odk.collect.android.support.async\n\nimport androidx.test.espresso.IdlingResource\nimport org.odk.collect.android.support.async.AsyncWorkTracker.taskCount\n\nclass AsyncWorkTrackerIdlingResource : IdlingResource {\n\n    private var resourceCallback: IdlingResource.ResourceCallback? = null\n\n    override fun getName(): String {\n        return AsyncWorkTrackerIdlingResource::class.java.name\n    }\n\n    override fun isIdleNow(): Boolean {\n        val idle = taskCount == 0\n        if (idle) {\n            resourceCallback?.onTransitionToIdle()\n        }\n\n        return idle\n    }\n\n    override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback?) {\n        this.resourceCallback = resourceCallback\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/async/TrackingCoroutineAndWorkManagerScheduler.kt",
    "content": "package org.odk.collect.android.support.async\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.work.BackoffPolicy\nimport androidx.work.WorkManager\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Runnable\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flowOn\nimport org.odk.collect.android.support.async.TestSchedulerTaskSpec.Companion.DATA_WRAPPED_SPEC\nimport org.odk.collect.async.Cancellable\nimport org.odk.collect.async.coroutines.CoroutineTaskRunner\nimport org.odk.collect.async.NotificationInfo\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.SchedulerBuilder\nimport org.odk.collect.async.TaskSpec\nimport org.odk.collect.async.workmanager.WorkManagerTaskSpecScheduler\nimport org.odk.collect.async.network.NetworkStateProvider\nimport org.odk.collect.async.services.ForegroundServiceTaskSpecRunner\nimport java.util.function.Consumer\nimport java.util.function.Supplier\n\nclass TrackingCoroutineAndWorkManagerScheduler(private val networkStateProvider: NetworkStateProvider) : Scheduler {\n\n    private val wrappedScheduler: Scheduler\n    private val deferredTasks: MutableList<DeferredTask> = ArrayList()\n    private val backgroundDispatcher = TrackingCoroutineDispatcher(Dispatchers.IO)\n\n    init {\n        val application = ApplicationProvider.getApplicationContext<Application>()\n        val workManager = WorkManager.getInstance(application)\n        wrappedScheduler = SchedulerBuilder.build(\n            CoroutineTaskRunner(Dispatchers.Main, backgroundDispatcher),\n            ForegroundServiceTaskSpecRunner(application),\n            WorkManagerTaskSpecScheduler(workManager)\n        )\n    }\n\n    override fun repeat(foreground: Runnable, repeatPeriod: Long): Cancellable {\n        return wrappedScheduler.repeat({ foreground.run() }, repeatPeriod)\n    }\n\n    override fun <T> immediate(background: Supplier<T>, foreground: Consumer<T>) {\n        AsyncWorkTracker.startWork()\n        wrappedScheduler.immediate(background) { t: T ->\n            foreground.accept(t)\n            AsyncWorkTracker.finishWork()\n        }\n    }\n\n    override fun immediate(foreground: Boolean, delay: Long?, runnable: Runnable) {\n        AsyncWorkTracker.startWork()\n        wrappedScheduler.immediate(foreground, delay) {\n            runnable.run()\n            AsyncWorkTracker.finishWork()\n        }\n    }\n\n    override fun immediate(\n        tag: String,\n        spec: TaskSpec,\n        inputData: Map<String, String>,\n        notificationInfo: NotificationInfo\n    ) {\n        AsyncWorkTracker.startWork()\n        val augmentedInputData = inputData + Pair(DATA_WRAPPED_SPEC, spec.javaClass.name)\n        wrappedScheduler.immediate(\n            tag,\n            TestSchedulerTaskSpec(),\n            augmentedInputData,\n            notificationInfo\n        )\n    }\n\n    override fun networkDeferred(\n        tag: String,\n        spec: TaskSpec,\n        inputData: Map<String, String>,\n        networkConstraint: Scheduler.NetworkType?\n    ) {\n        cancelDeferred(tag)\n        deferredTasks.add(DeferredTask(tag, spec, null, inputData, networkConstraint))\n    }\n\n    override fun networkDeferredRepeat(\n        tag: String,\n        spec: TaskSpec,\n        repeatPeriod: Long,\n        inputData: Map<String, String>\n    ) {\n        cancelDeferred(tag)\n        deferredTasks.add(DeferredTask(tag, spec, repeatPeriod, inputData, null))\n    }\n\n    override fun cancelDeferred(tag: String) {\n        deferredTasks.removeIf { t: DeferredTask -> t.tag == tag }\n    }\n\n    override fun isDeferredRunning(tag: String): Boolean {\n        return wrappedScheduler.isDeferredRunning(tag)\n    }\n\n    @JvmOverloads\n    fun runDeferredTasks(isLastUniqueExecution: Boolean = true, isStopped: Boolean = false) {\n        if (networkStateProvider.isDeviceOnline) {\n            val applicationContext = ApplicationProvider.getApplicationContext<Context>()\n            deferredTasks.removeIf { deferredTask ->\n                if (deferredTask.networkConstraint == null || deferredTask.networkConstraint == networkStateProvider.currentNetwork) {\n                    deferredTask.spec.getTask(applicationContext, deferredTask.inputData, isLastUniqueExecution) { isStopped }\n                        .get()\n                    deferredTask.repeatPeriod == null\n                } else {\n                    false\n                }\n            }\n        }\n    }\n\n    fun getDeferredTasks(): List<DeferredTask> {\n        return deferredTasks\n    }\n\n    override fun cancelAllDeferred() {}\n\n    override fun <T> flowOnBackground(flow: Flow<T>): Flow<T> {\n        return flow.flowOn(backgroundDispatcher)\n    }\n\n    class DeferredTask(\n        val tag: String,\n        val spec: TaskSpec,\n        val repeatPeriod: Long?,\n        val inputData: Map<String, String>,\n        val networkConstraint: Scheduler.NetworkType?\n    )\n}\n\nclass TestSchedulerTaskSpec : TaskSpec {\n    override val maxRetries: Int? = null\n    override val backoffPolicy: BackoffPolicy? = null\n    override val backoffDelay: Long? = null\n\n    private lateinit var wrappedSpec: TaskSpec\n\n    override fun getTask(\n        context: Context,\n        inputData: Map<String, String>,\n        isLastUniqueExecution: Boolean,\n        isStopped: () -> Boolean\n    ): Supplier<Boolean> {\n        wrappedSpec = Class.forName(inputData[DATA_WRAPPED_SPEC]!!)\n            .getConstructor()\n            .newInstance() as TaskSpec\n\n        return Supplier {\n            val result =\n                wrappedSpec.getTask(context, inputData, isLastUniqueExecution, isStopped).get()\n\n            AsyncWorkTracker.finishWork()\n            result\n        }\n    }\n\n    override fun onException(exception: Throwable) {\n        wrappedSpec.onException(exception)\n    }\n\n    companion object {\n        const val DATA_WRAPPED_SPEC = \"wrapped_spec\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/async/TrackingCoroutineDispatcher.kt",
    "content": "package org.odk.collect.android.support.async\n\nimport kotlinx.coroutines.CoroutineDispatcher\nimport kotlinx.coroutines.Runnable\nimport org.odk.collect.android.support.async.AsyncWorkTracker\nimport kotlin.coroutines.CoroutineContext\n\nclass TrackingCoroutineDispatcher(private val dispatcher: CoroutineDispatcher) : CoroutineDispatcher() {\n    override fun dispatch(\n        context: CoroutineContext,\n        block: Runnable\n    ) {\n        AsyncWorkTracker.startWork()\n        dispatcher.dispatch(context) {\n            try {\n                block.run()\n            } finally {\n                AsyncWorkTracker.finishWork()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/CustomMatchers.kt",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.support.matchers\n\nimport android.view.View\nimport android.widget.TextView\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\nimport org.hamcrest.TypeSafeMatcher\nimport org.odk.collect.android.R\nimport org.odk.collect.android.widgets.QuestionWidget\n\n/**\n * Grab bag of Hamcrest matchers.\n */\nobject CustomMatchers {\n    /**\n     * Matches the view at the given index. Useful when several views have the same properties.\n     * https://stackoverflow.com/a/39756832\n     *\n     */\n    @Deprecated(\"this matcher is stateful and will cause problems if used more than once\")\n    @JvmStatic\n    fun withIndex(matcher: Matcher<View>, index: Int): TypeSafeMatcher<View> {\n        return object : TypeSafeMatcher<View>() {\n            var currentIndex: Int = 0\n\n            override fun describeTo(description: Description) {\n                description.appendText(\"with index: \")\n                description.appendValue(index)\n                matcher.describeTo(description)\n            }\n\n            override fun matchesSafely(view: View): Boolean {\n                return matcher.matches(view) && currentIndex++ == index\n            }\n        }\n    }\n\n    @JvmStatic\n    fun isQuestionView(questionText: String): Matcher<View> {\n        return object : TypeSafeMatcher<View>() {\n            override fun describeTo(description: Description) {\n                description.appendText(\"is question view with text: \")\n                description.appendValue(questionText)\n            }\n\n            override fun matchesSafely(item: View): Boolean {\n                return if (item is QuestionWidget) {\n                    val questionTextView = item.findViewById<TextView>(R.id.text_label)\n                    questionTextView.text.toString() == questionText\n                } else {\n                    false\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/matchers/ToastMatcher.java",
    "content": "package org.odk.collect.android.support.matchers;\n\nimport android.os.IBinder;\nimport android.view.WindowManager;\n\nimport androidx.test.espresso.Root;\n\nimport org.hamcrest.Description;\nimport org.hamcrest.TypeSafeMatcher;\n\npublic class ToastMatcher extends TypeSafeMatcher<Root> {\n\n    @Override\n    public void describeTo(Description description) {\n        description.appendText(\"is toast\");\n    }\n\n    @Override\n    public boolean matchesSafely(Root root) {\n        int type = root.getWindowLayoutParams().get().type;\n        if (type == WindowManager.LayoutParams.TYPE_TOAST) {\n            IBinder windowToken = root.getDecorView().getWindowToken();\n            IBinder appToken = root.getDecorView().getApplicationWindowToken();\n            // windowToken == appToken means this window isn't contained by any other windows.\n            // if it was a window for an activity, it would have TYPE_BASE_APPLICATION.\n            return windowToken == appToken;\n        }\n        return false;\n    }\n\n}"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/AboutPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport org.odk.collect.android.R;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\npublic class AboutPage extends Page<AboutPage> {\n\n    @Override\n    public AboutPage assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.about_preferences);\n        return this;\n    }\n\n    public AboutPage scrollToOpenSourceLibrariesLicenses() {\n        onView(withId(R.id.recyclerView)).perform(scrollToPosition(4));\n        return this;\n    }\n\n    public OpenSourceLicensesPage clickOnOpenSourceLibrariesLicenses() {\n        onView(withText(org.odk.collect.strings.R.string.all_open_source_licenses)).perform(click());\n        return new OpenSourceLicensesPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/AccessControlPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.ViewMatchers.isChecked\nimport androidx.test.espresso.matcher.ViewMatchers.isEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.isNotChecked\nimport androidx.test.espresso.matcher.ViewMatchers.withClassName\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport org.hamcrest.Matchers.endsWith\nimport org.hamcrest.Matchers.not\nimport org.odk.collect.android.R\nimport org.odk.collect.android.support.matchers.CustomMatchers.withIndex\n\nclass AccessControlPage : Page<AccessControlPage>() {\n\n    override fun assertOnPage(): AccessControlPage {\n        assertText(org.odk.collect.strings.R.string.access_control_section_title)\n        return this\n    }\n\n    fun openMainMenuSettings(): MainMenuSettingsPage {\n        scrollToRecyclerViewItemAndClickText(getTranslatedString(org.odk.collect.strings.R.string.main_menu_settings))\n        return MainMenuSettingsPage()\n    }\n\n    fun openUserSettings(): AccessControlPage {\n        scrollToRecyclerViewItemAndClickText(getTranslatedString(org.odk.collect.strings.R.string.user_settings))\n        return this\n    }\n\n    fun clickFormEntrySettings(): AccessControlPage {\n        scrollToRecyclerViewItemAndClickText(getTranslatedString(org.odk.collect.strings.R.string.form_entry_setting))\n        return this\n    }\n\n    fun clickMovingBackwards(): AccessControlPage {\n        clickOnString(org.odk.collect.strings.R.string.moving_backwards_title)\n        return this\n    }\n\n    fun assertGoToPromptEnabled(): AccessControlPage {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.view_hierarchy))).check(matches(isEnabled()))\n        return this\n    }\n\n    fun assertGoToPromptDisabled(): AccessControlPage {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.view_hierarchy))).check(matches(not(isEnabled())))\n        return this\n    }\n\n    fun assertGoToPromptChecked(): AccessControlPage {\n        onView(withIndex(withClassName(endsWith(\"CheckBox\")), 3)).check(matches(isChecked()))\n        return this\n    }\n\n    fun assertGoToPromptUnchecked(): AccessControlPage {\n        onView(withIndex(withClassName(endsWith(\"CheckBox\")), 3)).check(matches(isNotChecked()))\n        return this\n    }\n\n    fun assertSaveAsDraftInFormEntryEnabled(): AccessControlPage {\n        onView(withIndex(withText(getTranslatedString(org.odk.collect.strings.R.string.save_mid)), 0)).check(matches(isEnabled()))\n        return this\n    }\n\n    fun assertSaveAsDraftInFormEntryDisabled(): AccessControlPage {\n        onView(withIndex(withText(getTranslatedString(org.odk.collect.strings.R.string.save_mid)), 0)).check(matches(not(isEnabled())))\n        return this\n    }\n\n    fun assertSaveAsDraftInFormEntryChecked(): AccessControlPage {\n        onView(withIndex(withClassName(endsWith(\"CheckBox\")), 4)).check(matches(isChecked()))\n        return this\n    }\n\n    fun assertSaveAsDraftInFormEntryUnchecked(): AccessControlPage {\n        onView(withIndex(withClassName(endsWith(\"CheckBox\")), 4)).check(matches(isNotChecked()))\n        return this\n    }\n\n    fun assertSaveAsDraftInFormEndEnabled(): AccessControlPage {\n        onView(withIndex(withText(getTranslatedString(org.odk.collect.strings.R.string.save_as_draft)), 1)).check(matches(isEnabled()))\n        return this\n    }\n\n    fun assertSaveAsDraftInFormEndDisabled(): AccessControlPage {\n        onView(withIndex(withText(getTranslatedString(org.odk.collect.strings.R.string.save_as_draft)), 1)).check(matches(not(isEnabled())))\n        return this\n    }\n\n    fun assertSaveAsDraftInFormEndChecked(): AccessControlPage {\n        onView(withIndex(withClassName(endsWith(\"CheckBox\")), 5)).check(matches(isChecked()))\n        return this\n    }\n\n    fun assertSaveAsDraftInFormEndUnchecked(): AccessControlPage {\n        onView(withIndex(withClassName(endsWith(\"CheckBox\")), 5)).check(matches(isNotChecked()))\n        return this\n    }\n\n    fun assertFinalizeEnabled(): AccessControlPage {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.finalize))).check(matches(isEnabled()))\n        return this\n    }\n\n    fun assertFinalizeDisabled(): AccessControlPage {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.finalize))).check(matches(not(isEnabled())))\n        return this\n    }\n\n    fun assertFinalizeChecked(): AccessControlPage {\n        onView(withIndex(withClassName(endsWith(\"CheckBox\")), 6)).check(matches(isChecked()))\n        return this\n    }\n\n    fun assertFinalizeUnchecked(): AccessControlPage {\n        onView(withIndex(withClassName(endsWith(\"CheckBox\")), 6)).check(matches(isNotChecked()))\n        return this\n    }\n\n    fun clickOnSaveAsDraftInFormEnd(): AccessControlPage {\n        onView(withIndex(withText(getTranslatedString(org.odk.collect.strings.R.string.save_mid)), 1)).perform(click())\n        return this\n    }\n\n    fun uncheckServerOption(): AccessControlPage {\n        clickOnString(org.odk.collect.strings.R.string.server_settings_title)\n        return this\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/AddNewRepeatDialog.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport org.odk.collect.strings.R\n\nclass AddNewRepeatDialog(private val repeatName: String?) : Page<AddNewRepeatDialog>() {\n    override fun assertOnPage(): AddNewRepeatDialog {\n        val dialogMessage = if (repeatName.isNullOrBlank()) {\n            getTranslatedString(R.string.add_another_question)\n        } else {\n            getTranslatedString(R.string.add_repeat_question, repeatName)\n        }\n        onView(withText(dialogMessage))\n            .inRoot(isDialog())\n            .check(matches(isDisplayed()))\n        return this\n    }\n\n    fun <D : Page<D>> clickOnAdd(destination: D): D {\n        return clickOnTextInDialog(R.string.add_repeat, destination)\n    }\n\n    fun <D : Page<D>> clickOnDoNotAdd(destination: D): D {\n        return clickOnTextInDialog(R.string.do_not_add_repeat, destination)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/AppClosedPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport android.app.Activity\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry\nimport androidx.test.runner.lifecycle.Stage\nimport com.google.common.collect.Iterables\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\n\nclass AppClosedPage : Page<AppClosedPage>() {\n\n    override fun assertOnPage(): AppClosedPage {\n        assertThat(currentActivity, equalTo(null))\n        return this\n    }\n\n    private val currentActivity: Activity?\n        get() {\n            InstrumentationRegistry.getInstrumentation().waitForIdleSync()\n            val activity = arrayOfNulls<Activity>(1)\n            InstrumentationRegistry.getInstrumentation().runOnMainSync {\n                val activities =\n                    ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(\n                        Stage.RESUMED\n                    )\n                if (!activities.isEmpty()) {\n                    activity[0] = Iterables.getOnlyElement(activities) as Activity\n                } else {\n                    activity[0] = null\n                }\n            }\n            return activity[0]\n        }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/AsyncPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport org.odk.collect.testshared.WaitFor\n\nclass AsyncPage<T : Page<T>>(private val destination: T) : Page<T>() {\n    override fun assertOnPage(): T {\n        return WaitFor.waitFor {\n            destination.assertOnPage()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/BlankFormSearchPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withHint;\n\npublic class BlankFormSearchPage extends Page<BlankFormSearchPage> {\n\n    @Override\n    public BlankFormSearchPage assertOnPage() {\n        onView(withHint(getTranslatedString(org.odk.collect.strings.R.string.search))).check(matches(isDisplayed()));\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/BulkFinalizationConfirmationDialogPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport org.odk.collect.strings.R\nimport org.odk.collect.strings.R.plurals\nimport org.odk.collect.strings.localization.getLocalizedQuantityString\n\nclass BulkFinalizationConfirmationDialogPage(private val count: Int) : Page<BulkFinalizationConfirmationDialogPage>() {\n    override fun assertOnPage(): BulkFinalizationConfirmationDialogPage {\n        val title = ApplicationProvider.getApplicationContext<Application>()\n            .getLocalizedQuantityString(plurals.bulk_finalize_confirmation, count, count)\n\n        onView(withText(title)).inRoot(isDialog()).check(matches(isDisplayed()))\n        return this\n    }\n\n    fun clickFinalize(): EditSavedFormPage {\n        return this.clickOnTextInDialog(R.string.finalize, AsyncPage(EditSavedFormPage()))\n    }\n\n    fun clickCancel(): EditSavedFormPage {\n        return this.clickOnTextInDialog(R.string.cancel, EditSavedFormPage())\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/CancelRecordingDialog.java",
    "content": "package org.odk.collect.android.support.pages;\n\npublic class CancelRecordingDialog extends Page<CancelRecordingDialog> {\n\n    private final String formName;\n\n    CancelRecordingDialog(String formName) {\n        this.formName = formName;\n    }\n\n    @Override\n    public CancelRecordingDialog assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.stop_recording_confirmation);\n        return this;\n    }\n\n    public FormEntryPage clickOk() {\n        clickOKOnDialog();\n        return new FormEntryPage(formName).assertOnPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ChangesReasonPromptPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.action.ViewActions.replaceText;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;\nimport static androidx.test.espresso.matcher.ViewMatchers.withHint;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\nimport org.odk.collect.testshared.WaitFor;\n\nimport java.util.concurrent.Callable;\n\npublic class ChangesReasonPromptPage extends Page<ChangesReasonPromptPage> {\n\n    private final String formName;\n\n    public ChangesReasonPromptPage(String formName) {\n        this.formName = formName;\n    }\n\n    @Override\n    public ChangesReasonPromptPage assertOnPage() {\n        closeSoftKeyboard(); // Might open before assertion has a chance to run\n\n        assertToolbarTitle(formName);\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.reason_for_changes))).check(matches(isDisplayed()));\n        return this;\n    }\n\n    public ChangesReasonPromptPage enterReason(String reason) {\n        onView(withHint(getTranslatedString(org.odk.collect.strings.R.string.reason))).perform(replaceText(reason));\n        return this;\n    }\n\n    public MainMenuPage clickSave() {\n        clickOnString(org.odk.collect.strings.R.string.save);\n        return new MainMenuPage().assertOnPage();\n    }\n\n    public <D extends Page<D>> D clickSave(D destination) {\n        clickOnString(org.odk.collect.strings.R.string.save);\n\n        // Make sure we wait for form saving to finish\n        WaitFor.waitFor((Callable<Void>) () -> {\n            assertTextDoesNotExist(org.odk.collect.strings.R.string.saving_form);\n            return null;\n        });\n\n        return destination.assertOnPage();\n    }\n\n    public <D extends Page<D>> D pressClose(D destination) {\n        onView(withContentDescription(getTranslatedString(org.odk.collect.strings.R.string.close))).perform(click());\n        return destination.assertOnPage();\n    }\n\n    public ChangesReasonPromptPage clickSaveWithValidationError() {\n        clickOnString(org.odk.collect.strings.R.string.save);\n        return this.assertOnPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/DeleteSavedFormPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.action.ViewActions.scrollTo;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\npublic class DeleteSavedFormPage extends Page<DeleteSavedFormPage> {\n\n    @Override\n    public DeleteSavedFormPage assertOnPage() {\n        assertToolbarTitle(getTranslatedString(org.odk.collect.strings.R.string.manage_files));\n        return this;\n    }\n\n    public DeleteSavedFormPage clickBlankForms() {\n        clickOnString(org.odk.collect.strings.R.string.forms);\n        return this;\n    }\n\n    public DeleteSavedFormPage clickForm(String formName) {\n        onView(withText(formName)).perform(scrollTo(), click());\n        return this;\n    }\n\n    public DeleteSelectedDialog clickDeleteSelected(int numberSelected) {\n        clickOnString(org.odk.collect.strings.R.string.delete_file);\n        return new DeleteSelectedDialog(numberSelected, this).assertOnPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/DeleteSelectedDialog.java",
    "content": "package org.odk.collect.android.support.pages;\n\npublic class DeleteSelectedDialog extends Page<DeleteSelectedDialog> {\n\n    private final int numberSelected;\n    private final DeleteSavedFormPage destination;\n\n    public DeleteSelectedDialog(int numberSelected, DeleteSavedFormPage destination) {\n        this.numberSelected = numberSelected;\n        this.destination = destination;\n    }\n\n    @Override\n    public DeleteSelectedDialog assertOnPage() {\n        assertTextInDialog(getTranslatedString(org.odk.collect.strings.R.string.delete_confirm, numberSelected));\n        return this;\n    }\n\n    public DeleteSavedFormPage clickDeleteForms() {\n        clickOnTextInDialog(org.odk.collect.strings.R.string.delete_yes);\n        return destination.assertOnPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EditSavedFormPage.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.action.ViewActions.replaceText;\nimport static androidx.test.espresso.action.ViewActions.scrollTo;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static androidx.test.espresso.matcher.ViewMatchers.withTagValue;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\nimport static org.hamcrest.CoreMatchers.allOf;\nimport static org.hamcrest.CoreMatchers.equalTo;\nimport static org.hamcrest.CoreMatchers.instanceOf;\nimport static org.hamcrest.Matchers.not;\nimport static org.odk.collect.android.instancemanagement.InstanceExtKt.getInstanceIcon;\n\nimport android.widget.RelativeLayout;\n\nimport androidx.appcompat.widget.Toolbar;\n\nimport org.odk.collect.android.R;\n\npublic class EditSavedFormPage extends Page<EditSavedFormPage> {\n\n    @Override\n    public EditSavedFormPage assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.review_data);\n        return this;\n    }\n\n    public EditSavedFormPage checkInstanceState(String instanceName, String desiredStatus) {\n        int desiredImageId = getInstanceIcon(desiredStatus);\n\n        onView(allOf(instanceOf(RelativeLayout.class),\n                hasDescendant(withText(instanceName)),\n                not(hasDescendant(instanceOf(Toolbar.class)))))\n                .check(matches(hasDescendant(withTagValue(equalTo(desiredImageId)))));\n        return this;\n    }\n\n    public OkDialog clickOnFormWithDialog(String instanceName) {\n        scrollToAndClickOnForm(instanceName);\n        return new OkDialog().assertOnPage();\n    }\n\n    public IdentifyUserPromptPage clickOnFormWithIdentityPrompt(String formName) {\n        scrollToAndClickOnForm(formName);\n        return new IdentifyUserPromptPage(formName).assertOnPage();\n    }\n\n    public FormHierarchyPage clickOnForm(String formName, String instanceName) {\n        scrollToAndClickOnForm(instanceName);\n        return new FormHierarchyPage(formName).assertOnPage();\n    }\n\n    public FormHierarchyPage clickOnForm(String formName) {\n        scrollToAndClickOnForm(formName);\n        return new FormHierarchyPage(formName).assertOnPage();\n    }\n\n    public SavepointRecoveryDialogPage clickOnFormWithSavepoint(String formName) {\n        scrollToAndClickOnForm(formName);\n        return new SavepointRecoveryDialogPage().assertOnPage();\n    }\n\n    public AppClosedPage clickOnFormClosingApp(String formName) {\n        scrollToAndClickOnForm(formName);\n        return new AppClosedPage().assertOnPage();\n    }\n\n    private void scrollToAndClickOnForm(String formName) {\n        onView(withText(formName)).perform(scrollTo(), click());\n    }\n\n    public EditSavedFormPage clickMenuFilter() {\n        onView(withId(R.id.menu_filter)).perform(click());\n        return this;\n    }\n\n    public EditSavedFormPage searchInBar(String query) {\n        onView(withId(androidx.appcompat.R.id.search_src_text)).perform(replaceText(query));\n        return this;\n    }\n\n    public BulkFinalizationConfirmationDialogPage clickFinalizeAll(int count) {\n        this.clickOptionsIcon(org.odk.collect.strings.R.string.finalize_all_drafts)\n                .clickOnString(org.odk.collect.strings.R.string.finalize_all_drafts);\n\n        return new BulkFinalizationConfirmationDialogPage(count).assertOnPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EntitiesPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nclass EntitiesPage : Page<EntitiesPage>() {\n\n    override fun assertOnPage(): EntitiesPage {\n        assertToolbarTitle(org.odk.collect.strings.R.string.entities_title)\n        return this\n    }\n\n    fun clickOnList(list: String): EntityListPage {\n        clickOnText(list)\n        return EntityListPage(list)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EntityListPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport org.odk.collect.testshared.AssertionFramework\n\nclass EntityListPage(private val list: String) : Page<EntityListPage>() {\n\n    override fun assertOnPage(): EntityListPage {\n        assertToolbarTitle(list)\n        return this\n    }\n\n    fun assertEntity(label: String, properties: String): EntityListPage {\n        assertText(label, assertionFramework = AssertionFramework.COMPOSE)\n        assertText(properties, assertionFramework = AssertionFramework.COMPOSE)\n        return this\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ErrorDialog.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport org.odk.collect.strings.R\n\nclass ErrorDialog : OkDialog() {\n    fun assertOnPage(isFatal: Boolean): ErrorDialog {\n        assertOnPage()\n        if (isFatal) {\n            assertText(R.string.form_cannot_be_used)\n        } else {\n            assertText(R.string.error_occured)\n        }\n        return this\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ErrorPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.appcompat.widget.AppCompatImageButton\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withParent\nimport org.hamcrest.Matchers.allOf\nimport org.hamcrest.Matchers.instanceOf\nimport org.odk.collect.android.R\n\nclass ErrorPage : Page<ErrorPage>() {\n\n    override fun assertOnPage(): ErrorPage {\n        assertText(org.odk.collect.strings.R.string.errors)\n        return this\n    }\n\n    fun assertError(errorMessage: String): ErrorPage {\n        assertText(errorMessage)\n        return this\n    }\n\n    fun navigateBack(): MainMenuPage {\n        onView(allOf(instanceOf(AppCompatImageButton::class.java), withParent(withId(org.odk.collect.androidshared.R.id.toolbar)))).perform(click())\n        return MainMenuPage()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ExperimentalPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\npublic class ExperimentalPage extends Page<ExperimentalPage> {\n\n    @Override\n    public ExperimentalPage assertOnPage() {\n        assertToolbarTitle(getTranslatedString(org.odk.collect.strings.R.string.experimental));\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FillBlankFormPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.action.ViewActions.replaceText;\nimport static androidx.test.espresso.action.ViewActions.scrollTo;\nimport static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;\nimport static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;\nimport static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\nimport static org.hamcrest.CoreMatchers.allOf;\nimport static org.odk.collect.testshared.ViewActions.clickOnViewContentDescription;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.espresso.contrib.RecyclerViewActions;\nimport androidx.test.espresso.matcher.ViewMatchers;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.testshared.WaitFor;\n\npublic class FillBlankFormPage extends Page<FillBlankFormPage> {\n\n    @Override\n    public FillBlankFormPage assertOnPage() {\n        assertToolbarTitle(org.odk.collect.strings.R.string.enter_data);\n        return this;\n    }\n\n    public IdentifyUserPromptPage clickOnFormWithIdentityPrompt(String formName) {\n        clickOnFormButton(formName);\n        return new IdentifyUserPromptPage(formName).assertOnPage();\n    }\n\n    public FillBlankFormPage clickOnSortByButton() {\n        onView(withId(R.id.menu_sort)).perform(click());\n        return this;\n    }\n\n    public FillBlankFormPage clickMenuFilter() {\n        onView(withId(R.id.menu_filter)).perform(click());\n        return this;\n    }\n\n    public BlankFormSearchPage searchInBar(String query) {\n        onView(withId(androidx.appcompat.R.id.search_src_text)).perform(replaceText(query));\n        return new BlankFormSearchPage().assertOnPage();\n    }\n\n    public FillBlankFormPage checkMapIconDisplayedForForm(String formName) {\n        onView(withId(R.id.form_list))\n                .perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(formName)), scrollTo()))\n                .check(matches(hasDescendant(allOf(withContentDescription(org.odk.collect.strings.R.string.open_form_map), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))));\n        return this;\n    }\n\n    public FillBlankFormPage checkMapIconNotDisplayedForForm(String formName) {\n        onView(withId(R.id.form_list))\n                .perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(formName)), scrollTo()))\n                .check(matches(hasDescendant(allOf(withContentDescription(org.odk.collect.strings.R.string.open_form_map), withEffectiveVisibility(ViewMatchers.Visibility.GONE)))));\n        return this;\n    }\n\n    public FormMapPage clickOnMapIconForForm(String formName) {\n        onView(withId(R.id.form_list))\n                .perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(formName)), clickOnViewContentDescription(org.odk.collect.strings.R.string.open_form_map, ApplicationProvider.getApplicationContext())));\n\n        return new FormMapPage(formName).assertOnPage();\n    }\n\n    public FormEntryPage clickOnForm(String formName) {\n        clickOnFormButton(formName);\n        return new FormEntryPage(formName);\n    }\n\n    private void clickOnFormButton(String formName) {\n        assertFormExists(formName);\n        onView(withId(R.id.form_list))\n                .perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(formName)), click()));\n    }\n\n    public FormEndPage clickOnEmptyForm(String formName) {\n        clickOnFormButton(formName);\n        return new FormEndPage(formName).assertOnPage();\n    }\n\n    public FillBlankFormPage clickRefresh() {\n        onView(withId(R.id.menu_refresh)).perform(click());\n        return this;\n    }\n\n    public FillBlankFormPage clickRefreshWithError() {\n        onView(withId(R.id.menu_refresh)).perform(click());\n        return this;\n    }\n\n    public ServerAuthDialog clickRefreshWithAuthError() {\n        onView(withId(R.id.menu_refresh)).perform(click());\n        return new ServerAuthDialog().assertOnPage();\n    }\n\n    public FillBlankFormPage assertFormExists(String formName) {\n        // Seen problems with disk syncing not being waited for even though it's an AsyncTask\n        return WaitFor.waitFor(() -> {\n            assertTextDoesNotExist(org.odk.collect.strings.R.string.empty_list_of_drafts_title);\n\n            onView(withId(R.id.form_list))\n                    .perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(formName)), scrollTo()));\n            return this;\n        });\n    }\n\n    public FillBlankFormPage assertFormDoesNotExist(String formName) {\n        // It seems like `doesNotExist` doesn't work with onData (you get an error that the thing\n        // you're looking for doesn't exists)\n        onView(withText(formName)).check(doesNotExist());\n        return this;\n    }\n\n    public FillBlankFormPage assertNoForms() {\n        assertText(org.odk.collect.strings.R.string.empty_list_of_blank_forms_title);\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.matcher.ViewMatchers.withSubstring\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.testshared.EspressoInteractions\n\nclass FirstLaunchPage : Page<FirstLaunchPage>() {\n\n    override fun assertOnPage(): FirstLaunchPage {\n        assertText(string.configure_with_qr_code)\n        return this\n    }\n\n    fun clickTryCollect(): MainMenuPage {\n        EspressoInteractions.clickOn(withSubstring(getTranslatedString(string.try_demo))) {\n            MainMenuPage().assertOnPage()\n        }\n\n        return MainMenuPage()\n    }\n\n    fun clickManuallyEnterProjectDetails(): ManualProjectCreatorDialogPage {\n        return clickOnString(\n            string.configure_manually,\n            ManualProjectCreatorDialogPage()\n        )\n    }\n\n    fun clickConfigureWithQrCode(): QrCodeProjectCreatorDialogPage {\n        return clickOnString(\n            string.configure_with_qr_code,\n            QrCodeProjectCreatorDialogPage()\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEndPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.testshared.AssertionFramework;\n\npublic class FormEndPage extends Page<FormEndPage> {\n\n    private final String formName;\n\n    public FormEndPage(String formName) {\n        this.formName = formName;\n    }\n\n    @Override\n    public FormEndPage assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.save_enter_data_description, AssertionFramework.COMPOSE, formName);\n        return this;\n    }\n\n    public <D extends Page<D>> D clickSaveAsDraft(D destination) {\n        return clickOnString(org.odk.collect.strings.R.string.save_as_draft, destination, AssertionFramework.COMPOSE);\n    }\n\n    public MainMenuPage clickSaveAsDraft() {\n        return clickSaveAsDraft(new MainMenuPage());\n    }\n\n    public FormEndPage clickSaveAsDraftWithError(String errorMsg) {\n        clickOnString(org.odk.collect.strings.R.string.save_as_draft, this, AssertionFramework.COMPOSE);\n        checkIsToastWithMessageDisplayed(errorMsg);\n        return this;\n    }\n\n    public <D extends Page<D>> D clickFinalize(D destination) {\n        return clickOnString(org.odk.collect.strings.R.string.finalize, destination, AssertionFramework.COMPOSE);\n    }\n\n    public MainMenuPage clickFinalize() {\n        clickFinalize(new MainMenuPage());\n        return new MainMenuPage();\n    }\n\n    public FormEndPage clickFinalizeWithError(String errorMsg) {\n        clickOnString(org.odk.collect.strings.R.string.finalize, this, AssertionFramework.COMPOSE);\n        checkIsToastWithMessageDisplayed(errorMsg);\n        return this;\n    }\n\n    public MainMenuPage clickSend() {\n        return clickOnString(org.odk.collect.strings.R.string.send, new MainMenuPage(), AssertionFramework.COMPOSE);\n    }\n\n    public FormMapPage clickSaveAndExitBackToMap() {\n        return clickSaveAsDraft(new FormMapPage(formName));\n    }\n\n    public FormEntryPage clickSaveAndExitWithError(String errorText) {\n        FormEntryPage page = clickOnString(org.odk.collect.strings.R.string.finalize, new FormEntryPage(formName), AssertionFramework.COMPOSE);\n        assertText(errorText);\n        return page;\n    }\n\n    public ChangesReasonPromptPage clickSaveAndExitWithChangesReasonPrompt() {\n        return clickFinalize(new ChangesReasonPromptPage(formName));\n    }\n\n    public FormHierarchyPage clickGoToArrow() {\n        onView(withId(R.id.menu_goto)).perform(click());\n        return new FormHierarchyPage(formName);\n    }\n\n    public FormEntryPage swipeToPreviousQuestion(String questionText) {\n        return new FormEntryPage(formName).swipeToPreviousQuestion(questionText);\n    }\n\n    public FormEntryPage swipeToPreviousQuestion(String questionText, boolean isRequired) {\n        return new FormEntryPage(formName).swipeToPreviousQuestion(questionText, isRequired);\n    }\n\n    public FormEndPage clickOptionsIcon() {\n        return clickOptionsIcon(org.odk.collect.strings.R.string.project_settings);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormEntryPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.action.ViewActions.longClick;\nimport static androidx.test.espresso.action.ViewActions.replaceText;\nimport static androidx.test.espresso.action.ViewActions.scrollTo;\nimport static androidx.test.espresso.action.ViewActions.swipeLeft;\nimport static androidx.test.espresso.action.ViewActions.swipeRight;\nimport static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.hasFocus;\nimport static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withClassName;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\nimport static org.hamcrest.Matchers.allOf;\nimport static org.hamcrest.Matchers.containsString;\nimport static org.hamcrest.Matchers.not;\nimport static org.hamcrest.core.StringEndsWith.endsWith;\nimport static org.odk.collect.android.support.matchers.CustomMatchers.isQuestionView;\nimport static org.odk.collect.android.support.matchers.CustomMatchers.withIndex;\n\nimport android.graphics.Bitmap;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.test.core.app.ApplicationProvider;\n\nimport org.hamcrest.Matcher;\nimport org.hamcrest.Matchers;\nimport org.odk.collect.android.R;\nimport org.odk.collect.androidtest.DrawableMatcher;\nimport org.odk.collect.testshared.EspressoInteractions;\nimport org.odk.collect.testshared.ViewActions;\nimport org.odk.collect.testshared.WaitFor;\n\nimport java.util.concurrent.Callable;\n\npublic class FormEntryPage extends Page<FormEntryPage> {\n\n    private final String formName;\n\n    public FormEntryPage(String formName) {\n        this.formName = formName;\n    }\n\n    @Override\n    public FormEntryPage assertOnPage() {\n        // Make sure we wait for loading to finish\n        WaitFor.waitFor((Callable<Void>) () -> {\n            assertTextDoesNotExist(org.odk.collect.strings.R.string.loading_form);\n            return null;\n        });\n\n        WaitFor.waitFor((Callable<Void>) () -> {\n            assertToolbarTitle(formName);\n            return null;\n        });\n\n        // Check we are not on the Form Hierarchy page\n        assertTextDoesNotExist(org.odk.collect.strings.R.string.jump_to_beginning);\n        assertTextDoesNotExist(org.odk.collect.strings.R.string.jump_to_end);\n\n        return this;\n    }\n\n    public FormEntryPage fillOut(QuestionAndAnswer... questionsAndAnswers) {\n        FormEntryPage page = this;\n\n        for (int i = 0; i < questionsAndAnswers.length; i++) {\n            QuestionAndAnswer current = questionsAndAnswers[i];\n            page = page.answerQuestion(current.question, current.isRequired, current.answer);\n\n            if (i < questionsAndAnswers.length - 1) {\n                QuestionAndAnswer next = questionsAndAnswers[i + 1];\n                page = page.swipeToNextQuestion(next.question, current.isRequired);\n            }\n        }\n\n        return page;\n    }\n\n    public <D extends Page<D>> D fillOutAndSave(D destination, QuestionAndAnswer... questionsAndAnswers) {\n        return fillOut(questionsAndAnswers)\n                .pressBack(new SaveOrDiscardFormDialog<>(destination))\n                .clickSaveChanges();\n    }\n\n    public MainMenuPage fillOutAndSave(QuestionAndAnswer... questionsAndAnswers) {\n        return fillOut(questionsAndAnswers)\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickSaveChanges();\n    }\n\n    public MainMenuPage fillOutAndFinalize(QuestionAndAnswer... questionsAndAnswers) {\n        return fillOut(questionsAndAnswers)\n                .swipeToEndScreen()\n                .clickFinalize();\n    }\n\n    public FormEntryPage swipeToNextQuestion(String questionText) {\n        return swipeToNextQuestion(questionText, false);\n    }\n\n    public FormEntryPage swipeToNextQuestion(String questionText, boolean isRequired) {\n        flingLeft();\n\n        if (isRequired) {\n            assertQuestionText(\"* \" + questionText);\n        } else {\n            assertQuestionText(questionText);\n        }\n\n        return this;\n    }\n\n    public FormEntryPage swipeToPreviousQuestion(String questionText) {\n        return swipeToPreviousQuestion(questionText, false);\n    }\n\n    public FormEntryPage swipeToPreviousQuestion(String questionText, boolean isRequired) {\n        onView(withId(R.id.questionholder)).perform(swipeRight());\n\n        if (isRequired) {\n            assertQuestionText(\"* \" + questionText);\n        } else {\n            assertQuestionText(questionText);\n        }\n\n        return this;\n    }\n\n    public FormEntryPage swipeToNextRepeat(String repeatLabel, int repeatNumber) {\n        asyncAssertText(repeatLabel + \" > \" + (repeatNumber - 1));\n        flingLeft();\n        asyncAssertText(repeatLabel + \" > \" + repeatNumber);\n        return this;\n    }\n\n    public FormEndPage swipeToEndScreen(String instanceName) {\n        flingLeft();\n        return WaitFor.waitFor(() -> new FormEndPage(instanceName).assertOnPage());\n    }\n\n    public FormEndPage swipeToEndScreen() {\n        flingLeft();\n        return WaitFor.waitFor(() -> new FormEndPage(formName).assertOnPage());\n    }\n\n    public ErrorDialog swipeToNextQuestionWithError(boolean isFatal) {\n        flingLeft();\n        return new ErrorDialog().assertOnPage(isFatal);\n    }\n\n    public FormEntryPage swipeToNextQuestionWithConstraintViolation(int constraintText) {\n        flingLeft();\n        assertText(constraintText);\n\n        return this;\n    }\n\n    public FormEntryPage swipeToNextQuestionWithConstraintViolation(String constraintText) {\n        flingLeft();\n        assertText(constraintText);\n\n        return this;\n    }\n\n    private void assertQuestionText(String text) {\n        onView(withIndex(withId(R.id.text_label), 0)).check(matches(withText(containsString(text))));\n    }\n\n    public FormEntryPage clickOptionsIcon() {\n        return clickOptionsIcon(org.odk.collect.strings.R.string.project_settings);\n    }\n\n    public ProjectSettingsPage clickProjectSettings() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.project_settings))).perform(click());\n        return new ProjectSettingsPage().assertOnPage();\n    }\n\n    public FormEntryPage assertNavigationButtonsAreDisplayed() {\n        onView(withId(R.id.form_forward_button)).check(matches(isDisplayed()));\n        onView(withId(R.id.form_back_button)).check(matches(isDisplayed()));\n        return this;\n    }\n\n    public FormEntryPage assertNavigationButtonsAreHidden() {\n        onView(withId(R.id.form_forward_button)).check(matches(not(isDisplayed())));\n        onView(withId(R.id.form_back_button)).check(matches(not(isDisplayed())));\n        return this;\n    }\n\n    public FormEntryPage assertGoToIconExists() {\n        onView(withId(R.id.menu_goto)).check(matches(isDisplayed()));\n        return this;\n    }\n\n    public FormEntryPage assertGoToIconDoesNotExist() {\n        onView(withId(R.id.menu_goto)).check(doesNotExist());\n        return this;\n    }\n\n    public FormHierarchyPage clickGoToArrow() {\n        // This click sometimes ends up turning into a long press\n        tryFlakyAction(() -> {\n            onView(withId(R.id.menu_goto)).perform(click());\n        });\n\n        return new FormHierarchyPage(formName);\n    }\n\n    public FormEntryPage clickWidgetButton() {\n        onView(withId(R.id.simple_button)).perform(click());\n        return this;\n    }\n\n    public FormEntryPage clickRankingButton() {\n        onView(withId(R.id.rank_items_button)).perform(click());\n        return this;\n    }\n\n    public FormEntryPage deleteGroup(String questionText) {\n        longClickOnText(questionText);\n        clickOnTextInPopup(org.odk.collect.strings.R.string.delete_repeat);\n        clickOnTextInDialog(org.odk.collect.strings.R.string.discard_group, this);\n        return this;\n    }\n\n    public FormEntryPage clickForwardButton() {\n        closeSoftKeyboard();\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.form_forward))).perform(click());\n        return this;\n    }\n\n    public FormEndPage clickForwardButtonToEndScreen() {\n        closeSoftKeyboard();\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.form_forward))).perform(click());\n        return new FormEndPage(formName).assertOnPage();\n    }\n\n    public FormEntryPage clickBackwardButton() {\n        closeSoftKeyboard();\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.form_backward))).perform(click());\n        return this;\n    }\n\n    public FormEntryPage clickSave() {\n        onView(withId(R.id.menu_save)).perform(click());\n        return this;\n    }\n\n    public FormEntryPage clickSaveWithError(String errorMsg) {\n        onView(withId(R.id.menu_save)).perform(click());\n        checkIsToastWithMessageDisplayed(errorMsg);\n        return this;\n    }\n\n    public ChangesReasonPromptPage clickSaveWithChangesReasonPrompt() {\n        onView(withId(R.id.menu_save)).perform(click());\n        return new ChangesReasonPromptPage(formName).assertOnPage();\n    }\n\n    public AddNewRepeatDialog clickPlus(String repeatName) {\n        onView(withId(R.id.menu_add_repeat)).perform(click());\n        return new AddNewRepeatDialog(repeatName).assertOnPage();\n    }\n\n    public FormEntryPage longPressOnQuestion(int id, int index) {\n        onView(withIndex(withId(id), index)).perform(longClick());\n        return this;\n    }\n\n    public FormEntryPage longPressOnQuestion(String question) {\n        longPressOnQuestion(question, false);\n        return this;\n    }\n\n    public FormEntryPage longPressOnQuestion(String question, boolean isRequired) {\n        WaitFor.tryAgainOnFail(() -> {\n            if (isRequired) {\n                onView(withText(\"* \" + question)).perform(longClick());\n            } else {\n                onView(withText(question)).perform(longClick());\n            }\n\n            assertText(org.odk.collect.strings.R.string.clear_answer);\n        });\n\n        return this;\n    }\n\n    public FormEntryPage removeResponse() {\n        onView(withText(org.odk.collect.strings.R.string.clear_answer)).perform(click());\n        return clickOnTextInDialog(org.odk.collect.strings.R.string.discard_answer, this);\n    }\n\n    public AddNewRepeatDialog swipeToNextQuestionWithRepeatGroup(String repeatName) {\n        flingLeft();\n        return WaitFor.waitFor(() -> new AddNewRepeatDialog(repeatName).assertOnPage());\n    }\n\n    public AddNewRepeatDialog swipeToPreviousQuestionWithRepeatGroup(String repeatName) {\n        flingRight();\n        return WaitFor.waitFor(() -> new AddNewRepeatDialog(repeatName).assertOnPage());\n    }\n\n    public FormEntryPage answerQuestion(String question, String answer) {\n        answerQuestion(question, false, answer);\n        return this;\n    }\n\n    public FormEntryPage answerQuestion(String question, boolean isRequired, String answer) {\n        String questionText;\n        if (isRequired) {\n            questionText = \"* \" + question;\n        } else {\n            questionText = question;\n        }\n\n        EspressoInteractions.replaceText(getQuestionFieldMatcher(questionText), answer);\n        return this;\n    }\n\n    /**\n     * @deprecated Use {@link #answerQuestion(String, String)} instead\n     */\n    @Deprecated\n    public FormEntryPage answerQuestion(int index, String answer) {\n        onView(withIndex(withClassName(endsWith(\"EditText\")), index)).perform(scrollTo());\n        onView(withIndex(withClassName(endsWith(\"EditText\")), index)).perform(replaceText(answer));\n        return this;\n    }\n\n    public FormEntryPage assertAnswer(String questionText, String answer) {\n        return assertAnswer(questionText, answer, false);\n    }\n\n    public FormEntryPage assertAnswer(String questionText, String answer, Boolean readOnly) {\n        if (readOnly) {\n            onView(allOf(isDescendantOfA(isQuestionView(questionText)), isDisplayed(), withText(answer)))\n                    .check(matches(not(doesNotExist())));\n        } else {\n            onView(getQuestionFieldMatcher(questionText)).check(matches(withText(answer)));\n        }\n\n        return this;\n    }\n\n    public FormEntryPage clickOnQuestionField(String questionText) {\n        EspressoInteractions.clickOn(getQuestionFieldMatcher(questionText));\n        return this;\n    }\n\n    public FormEntryPage assertQuestion(String text) {\n        return assertQuestion(text, false);\n    }\n\n    public FormEntryPage assertQuestion(String text, boolean isRequired) {\n        if (isRequired) {\n            asyncAssertText(\"* \" + text);\n        } else {\n            asyncAssertText(text);\n        }\n\n        return this;\n    }\n\n    public FormEntryPage assertNoQuestion(String text) {\n        return assertNoQuestion(text, false);\n    }\n\n    public FormEntryPage assertNoQuestion(String text, boolean isRequired) {\n        if (isRequired) {\n            assertTextDoesNotExist(\"* \" + text);\n        } else {\n            assertTextDoesNotExist(text);\n        }\n\n        return this;\n    }\n\n    private void flingLeft() {\n        tryFlakyAction(() -> {\n            onView(withId(R.id.questionholder)).perform(swipeLeft());\n        });\n    }\n\n    private void flingRight() {\n        tryFlakyAction(() -> {\n            onView(withId(R.id.questionholder)).perform(swipeRight());\n        });\n    }\n\n    public SelectMinimalDialogPage openSelectMinimalDialog() {\n        return openSelectMinimalDialog(0);\n    }\n\n    public SelectMinimalDialogPage openSelectMinimalDialog(int index) {\n        onView(withIndex(withClassName(Matchers.endsWith(\"TextInputEditText\")), index)).perform(click());\n        return new SelectMinimalDialogPage(formName).assertOnPage();\n    }\n\n    public FormEntryPage assertSelectMinimalDialogAnswer(@Nullable String answer) {\n        if (answer == null) {\n            onView(withId(R.id.answer)).check(matches(withText(org.odk.collect.strings.R.string.select_answer)));\n        } else {\n            onView(withId(R.id.answer)).check(matches(withText(answer)));\n        }\n        return this;\n    }\n\n    public OkDialog swipeToEndScreenWhileRecording() {\n        flingLeft();\n        OkDialog okDialog = new OkDialog().assertOnPage();\n        assertText(org.odk.collect.strings.R.string.recording_warning);\n        return okDialog;\n    }\n\n    public CancelRecordingDialog clickRecordAudio() {\n        clickOnString(org.odk.collect.strings.R.string.record_audio_on);\n        return new CancelRecordingDialog(formName);\n    }\n\n    public MainMenuPage pressBackAndDiscardChanges() {\n        return closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickDiscardChanges();\n    }\n\n    public <D extends Page<D>> D pressBackAndDiscardChanges(D destination) {\n        return closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<>(destination))\n                .clickDiscardChanges();\n    }\n\n    public MainMenuPage pressBackAndSaveAsDraft() {\n        return closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickSaveChanges();\n    }\n\n    public <D extends Page<D>> D pressBackAndSaveAsDraft(D destination) {\n        return closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<>(destination))\n                .clickSaveChanges();\n    }\n\n    public MainMenuPage pressBackAndDiscardForm() {\n        return closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<>(new MainMenuPage()))\n                .clickDiscardForm();\n    }\n\n    public <D extends Page<D>> D pressBackAndDiscardForm(D destination) {\n        return closeSoftKeyboard()\n                .pressBack(new SaveOrDiscardFormDialog<D>(destination))\n                .clickDiscardForm();\n    }\n\n    public FormEntryPage assertBackgroundLocationSnackbarShown() {\n        checkIsSnackbarWithMessageDisplayed(String.format(ApplicationProvider.getApplicationContext().getString(org.odk.collect.strings.R.string.background_location_enabled), \"⋮\"));\n        return this;\n    }\n\n    public FormEntryPage setRating(float value) {\n        onView(allOf(withId(R.id.rating_bar1), isDisplayed())).perform(ViewActions.setRating(value));\n        return this;\n    }\n\n    public FormEntryPage assertQuestionHasFocus(String questionText) {\n        onView(getQuestionFieldMatcher(questionText)).check(matches(isCompletelyDisplayed()));\n        onView(getQuestionFieldMatcher(questionText)).check(matches(hasFocus()));\n        return this;\n    }\n\n    private static @NonNull Matcher<View> getQuestionFieldMatcher(String question) {\n        return allOf(\n                withClassName(endsWith(\"EditText\")),\n                isDescendantOfA(isQuestionView(question))\n        );\n    }\n\n    public FormEntryPage assertImageViewShowsImage(int resourceid, Bitmap image) {\n        onView(withId(resourceid)).check(matches(DrawableMatcher.withBitmap(image)));\n        return this;\n    }\n\n    public static class QuestionAndAnswer {\n\n        private final String question;\n        private final String answer;\n        private final boolean isRequired;\n\n        public QuestionAndAnswer(String question, String answer) {\n            this(question, answer, false);\n        }\n\n        public QuestionAndAnswer(String question, String answer, boolean isRequired) {\n            this.question = question;\n            this.answer = answer;\n            this.isRequired = isRequired;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormHierarchyPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.doesNotExist\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.contrib.RecyclerViewActions.scrollTo\nimport androidx.test.espresso.matcher.ViewMatchers.hasDescendant\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport org.odk.collect.android.R\nimport org.odk.collect.testshared.AssertionFramework\nimport org.odk.collect.testshared.WaitFor.waitFor\nimport java.util.concurrent.Callable\n\nclass FormHierarchyPage(private val formName: String) : Page<FormHierarchyPage>() {\n    override fun assertOnPage(): FormHierarchyPage {\n        // Make sure we've left the fill blank form screen\n        waitFor(Callable {\n            onView(withId(R.id.menu_goto)).check(doesNotExist())\n            null\n        } as Callable<*>)\n\n        assertToolbarTitle(formName)\n        assertText(org.odk.collect.strings.R.string.jump_to_beginning)\n        assertText(org.odk.collect.strings.R.string.jump_to_end)\n        return this\n    }\n\n    fun assertNotRemovableGroup(): FormHierarchyPage {\n        onView(withId(R.id.menu_delete_child)).check(doesNotExist())\n        return this\n    }\n\n    fun clickGoUpIcon(): FormHierarchyPage {\n        onView(withId(R.id.menu_go_up)).perform(click())\n        return this\n    }\n\n    fun clickGoToStart(): FormEntryPage {\n        onView(withId(R.id.jumpBeginningButton)).perform(click())\n        return FormEntryPage(formName).assertOnPage()\n    }\n\n    fun clickGoToEnd(): FormEndPage {\n        return clickOnString(org.odk.collect.strings.R.string.jump_to_end)\n            .assertOnPage<FormEndPage>(FormEndPage(formName))\n    }\n\n    fun clickGoToEnd(instanceName: String): FormEndPage {\n        return clickOnString(org.odk.collect.strings.R.string.jump_to_end)\n            .assertOnPage<FormEndPage>(FormEndPage(instanceName))\n    }\n\n    fun addGroup(): FormEntryPage {\n        onView(withId(R.id.menu_add_repeat)).perform(click())\n        return FormEntryPage(formName).assertOnPage()\n    }\n\n    fun deleteGroup(): FormHierarchyPage {\n        onView(withId(R.id.menu_delete_child)).perform(click())\n        return clickOnTextInDialog(\n            org.odk.collect.strings.R.string.delete_repeat,\n            this\n        )\n    }\n\n    fun clickJumpEndButton(): FormEndPage {\n        onView(withId(R.id.jumpEndButton)).perform(click())\n        return FormEndPage(formName).assertOnPage()\n    }\n\n    fun assertPath(text: String): FormHierarchyPage {\n        onView(withId(R.id.pathtext)).check(\n            matches(withText(text))\n        )\n        return this\n    }\n\n    @JvmOverloads\n    fun clickOnQuestion(questionLabel: String, isRequired: Boolean = false): FormEntryPage {\n        var questionLabel = questionLabel\n        if (isRequired) {\n            questionLabel = \"* $questionLabel\"\n        }\n\n        onView(withId(R.id.list)).perform(\n            scrollTo<RecyclerView.ViewHolder>(\n                hasDescendant(withText(questionLabel))\n            )\n        )\n        clickOnText(questionLabel)\n        return FormEntryPage(formName)\n    }\n\n    fun clickOnGroup(groupLabel: String): FormHierarchyPage {\n        onView(withId(R.id.list)).perform(\n            scrollTo<RecyclerView.ViewHolder>(\n                hasDescendant(withText(groupLabel))\n            )\n        )\n        clickOnText(groupLabel)\n        return this\n    }\n\n    fun assertAnswer(answer: String): FormHierarchyPage {\n        assertText(answer, AssertionFramework.COMPOSE)\n        return this\n    }\n\n    fun assertAnswerDoesNotExist(answer: String): FormHierarchyPage {\n        assertTextDoesNotExist(answer, AssertionFramework.COMPOSE)\n        return this\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormManagementPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.scrollTo;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;\nimport static androidx.test.espresso.matcher.ViewMatchers.isEnabled;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\nimport static org.hamcrest.CoreMatchers.not;\n\nimport androidx.test.espresso.contrib.RecyclerViewActions;\n\npublic class FormManagementPage extends Page<FormManagementPage> {\n\n    @Override\n    public FormManagementPage assertOnPage() {\n        assertToolbarTitle(getTranslatedString(org.odk.collect.strings.R.string.form_management_preferences));\n        return this;\n    }\n\n    public ListPreferenceDialog<FormManagementPage> clickUpdateForms() {\n        clickOnString(org.odk.collect.strings.R.string.form_update_mode_title);\n        return new ListPreferenceDialog<>(org.odk.collect.strings.R.string.form_update_mode_title, this).assertOnPage();\n    }\n\n    public ListPreferenceDialog<FormManagementPage> clickAutomaticUpdateFrequency() {\n        clickOnString(org.odk.collect.strings.R.string.form_update_frequency_title);\n        return new ListPreferenceDialog<>(org.odk.collect.strings.R.string.form_update_frequency_title, this).assertOnPage();\n    }\n\n    public ListPreferenceDialog<FormManagementPage> clickAutoSend() {\n        clickOnString(org.odk.collect.strings.R.string.autosend);\n        return new ListPreferenceDialog<>(org.odk.collect.strings.R.string.autosend, this).assertOnPage();\n    }\n\n    public FormManagementPage openShowGuidanceForQuestions() {\n        scrollToRecyclerViewItemAndClickText(getTranslatedString(org.odk.collect.strings.R.string.guidance_hint_title));\n        return this;\n    }\n\n    public FormManagementPage openConstraintProcessing() {\n        scrollToRecyclerViewItemAndClickText(getTranslatedString(org.odk.collect.strings.R.string.constraint_behavior_title));\n        return this;\n    }\n\n    public FormManagementPage scrollToConstraintProcessing() {\n        onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions\n                .actionOnItem(hasDescendant(withText(getTranslatedString(org.odk.collect.strings.R.string.constraint_behavior_title))), scrollTo()));\n        return this;\n    }\n\n    public FormManagementPage checkIfConstraintProcessingIsDisabled() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.constraint_behavior_title))).check(matches(not(isEnabled())));\n        return this;\n    }\n\n    public FormManagementPage checkIfConstraintProcessingIsEnabled() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.constraint_behavior_title))).check(matches(isEnabled()));\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormMapPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport org.odk.collect.android.support.FakeClickableMapFragment;\n\npublic class FormMapPage extends Page<FormMapPage> {\n\n    private final String formName;\n\n    public FormMapPage(String formName) {\n        this.formName = formName;\n    }\n\n    @Override\n    public FormMapPage assertOnPage() {\n        assertText(formName);\n        checkIsIdDisplayed(org.odk.collect.geo.R.id.geometry_status);\n        return this;\n    }\n\n    public FormEntryPage clickFillBlankFormButton(String formName) {\n        clickOnId(org.odk.collect.geo.R.id.new_item);\n        return new FormEntryPage(formName).assertOnPage();\n    }\n\n    public FormMapPage selectForm(FakeClickableMapFragment mapFragment, int index) {\n        mapFragment.clickOnFeature(index);\n        return this;\n    }\n\n    public FormHierarchyPage clickEditSavedForm(String formName) {\n        clickOnString(org.odk.collect.strings.R.string.edit_data);\n        return new FormHierarchyPage(formName).assertOnPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormMetadataPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\npublic class FormMetadataPage extends PreferencePage<FormMetadataPage> {\n\n    @Override\n    public FormMetadataPage assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.form_metadata_title);\n        return this;\n    }\n\n    public FormMetadataPage clickEmail() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.email))).perform(click());\n        return this;\n    }\n\n    public FormMetadataPage clickUsername() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.username))).perform(click());\n        return this;\n    }\n\n    public FormMetadataPage clickPhoneNumber() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.phone_number))).perform(click());\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FormsDownloadResultPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withText\n\nclass FormsDownloadResultPage : Page<FormsDownloadResultPage>() {\n\n    override fun assertOnPage(): FormsDownloadResultPage {\n        onView(withText(org.odk.collect.strings.R.string.ok)).inRoot(RootMatchers.isDialog()).check(matches(isDisplayed()))\n        return this\n    }\n\n    fun assertMessage(message: String): FormsDownloadResultPage {\n        assertText(message)\n        return this\n    }\n\n    fun assertSuccess(): FormsDownloadResultPage {\n        assertText(org.odk.collect.strings.R.string.all_downloads_succeeded)\n        return this\n    }\n\n    fun showDetails(): ErrorPage {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.show_details))).perform(click())\n        return ErrorPage().assertOnPage()\n    }\n\n    fun <D : Page<D>> clickOK(destination: D): D {\n        clickOKOnDialog()\n        return destination.assertOnPage()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/GetBlankFormPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\nimport org.odk.collect.strings.R.string;\n\npublic class GetBlankFormPage extends Page<GetBlankFormPage> {\n\n    @Override\n    public GetBlankFormPage assertOnPage() {\n        onView(withText(getTranslatedString(string.get_forms))).check(matches(isDisplayed()));\n        return this;\n    }\n\n    public FormsDownloadResultPage clickGetSelected() {\n        onView(withText(getTranslatedString(string.download))).perform(click());\n        return new FormsDownloadResultPage().assertOnPage();\n    }\n\n    public GetBlankFormPage clickClearAll() {\n        clickOnString(string.clear_all);\n        return this;\n    }\n\n    public GetBlankFormPage clickSelectAll() {\n        clickOnString(string.select_all);\n        return this;\n    }\n\n    public GetBlankFormPage clickForm(String formName) {\n        clickOnText(formName);\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/IdentifyUserPromptPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.action.ViewActions.pressImeActionButton;\nimport static androidx.test.espresso.action.ViewActions.replaceText;\nimport static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;\nimport static androidx.test.espresso.matcher.ViewMatchers.withHint;\n\npublic class IdentifyUserPromptPage extends Page<IdentifyUserPromptPage> {\n\n    private final String formName;\n\n    public IdentifyUserPromptPage(String formName) {\n        super();\n        this.formName = formName;\n    }\n\n    @Override\n    public IdentifyUserPromptPage assertOnPage() {\n        assertTextInDialog(formName);\n        assertTextInDialog(org.odk.collect.strings.R.string.enter_identity);\n        return this;\n    }\n\n    public IdentifyUserPromptPage enterIdentity(String identity) {\n        onView(withHint(getTranslatedString(org.odk.collect.strings.R.string.identity))).perform(replaceText(identity));\n        return this;\n    }\n\n    public <D extends Page<D>> D clickKeyboardEnter(D destination) {\n        onView(withHint(getTranslatedString(org.odk.collect.strings.R.string.identity))).perform(pressImeActionButton());\n        return destination.assertOnPage();\n    }\n\n    public IdentifyUserPromptPage clickKeyboardEnterWithValidationError() {\n        onView(withHint(getTranslatedString(org.odk.collect.strings.R.string.identity))).perform(pressImeActionButton());\n        return this.assertOnPage();\n    }\n\n    public MainMenuPage pressClose() {\n        onView(withContentDescription(getTranslatedString(org.odk.collect.strings.R.string.close))).perform(click());\n        return new MainMenuPage().assertOnPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ListPreferenceDialog.java",
    "content": "package org.odk.collect.android.support.pages;\n\npublic class ListPreferenceDialog<T extends Page<T>> extends Page<ListPreferenceDialog<T>> {\n\n    private final int title;\n    private final T page;\n\n    ListPreferenceDialog(int title, T page) {\n        this.title = title;\n        this.page = page;\n    }\n\n    @Override\n    public ListPreferenceDialog<T> assertOnPage() {\n        assertText(title);\n        return this;\n    }\n\n    public T clickOption(int option) {\n        return clickOnTextInDialog(option, page);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/MainMenuPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.action.ViewActions.scrollTo;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\nimport static org.hamcrest.core.AllOf.allOf;\nimport static org.hamcrest.core.StringContains.containsString;\n\nimport androidx.test.espresso.matcher.ViewMatchers;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.support.StorageUtils;\nimport org.odk.collect.android.support.async.TrackingCoroutineAndWorkManagerScheduler;\nimport org.odk.collect.testshared.WaitFor;\n\nimport java.io.IOException;\nimport java.util.List;\n\npublic class MainMenuPage extends Page<MainMenuPage> {\n\n    @Override\n    public MainMenuPage assertOnPage() {\n        return WaitFor.waitFor(() -> {\n            onView(withText(org.odk.collect.strings.R.string.enter_data)).check(matches(isDisplayed()));\n            onView(allOf(\n                    withText(containsString(getTranslatedString(org.odk.collect.strings.R.string.collect_app_name))),\n                    withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))\n            ).perform(scrollTo()).check(matches(isDisplayed()));\n            return this;\n        });\n    }\n\n    public ProjectSettingsDialogPage openProjectSettingsDialog() {\n        assertOnPage(); // Make sure we've waited for the application load correctly\n\n        clickOnContentDescription(org.odk.collect.strings.R.string.projects);\n        return new ProjectSettingsDialogPage().assertOnPage();\n    }\n\n    public FormEntryPage startBlankForm(String formName) {\n        goToBlankForm(formName);\n        return new FormEntryPage(formName).assertOnPage();\n    }\n\n    public SavepointRecoveryDialogPage startBlankFormWithSavepoint(String formName) {\n        goToBlankForm(formName);\n        return new SavepointRecoveryDialogPage().assertOnPage();\n    }\n\n    public AddNewRepeatDialog startBlankFormWithRepeatGroup(String formName, String repeatName) {\n        goToBlankForm(formName);\n        return new AddNewRepeatDialog(repeatName).assertOnPage();\n    }\n\n    public ErrorDialog startBlankFormWithError(String formName, boolean isFatal) {\n        goToBlankForm(formName);\n        return new ErrorDialog().assertOnPage(isFatal);\n    }\n\n    public OkDialog startBlankFormWithDialog(String formName) {\n        goToBlankForm(formName);\n        return new OkDialog().assertOnPage();\n    }\n\n    public FillBlankFormPage clickFillBlankForm() {\n        tryFlakyAction(() -> {\n            onView(withId(R.id.enter_data)).perform(click());\n        });\n\n        return new FillBlankFormPage().assertOnPage();\n    }\n\n    private void goToBlankForm(String formName) {\n        clickFillBlankForm().clickOnForm(formName);\n    }\n\n    public EditSavedFormPage clickDrafts() {\n        onView(withId(R.id.review_data)).perform(click());\n        return new EditSavedFormPage().assertOnPage();\n    }\n\n    public EditSavedFormPage clickDrafts(int formCount) {\n        assertNumberOfEditableForms(formCount);\n        return clickDrafts();\n    }\n\n    public MainMenuPage assertNumberOfFinalizedForms(int number) {\n        if (number == 0) {\n            onView(allOf(withId(R.id.number), isDescendantOfA(withId(R.id.send_data)))).check(matches(withText(\"\")));\n        } else {\n            onView(allOf(withId(R.id.number), isDescendantOfA(withId(R.id.send_data)))).check(matches(withText(String.valueOf(number))));\n        }\n        return this;\n    }\n\n    public MainMenuPage assertNumberOfEditableForms(int number) {\n        if (number == 0) {\n            onView(allOf(withId(R.id.number), isDescendantOfA(withId(R.id.review_data)))).check(matches(withText(\"\")));\n        } else {\n            onView(allOf(withId(R.id.number), isDescendantOfA(withId(R.id.review_data)))).check(matches(withText(String.valueOf(number))));\n        }\n\n        return this;\n    }\n\n    private MainMenuPage assertNumberOfSentForms(int number) {\n        if (number == 0) {\n            onView(allOf(withId(R.id.number), isDescendantOfA(withId(R.id.view_sent_forms)))).check(matches(withText(\"\")));\n        } else {\n            onView(allOf(withId(R.id.number), isDescendantOfA(withId(R.id.view_sent_forms)))).check(matches(withText(String.valueOf(number))));\n        }\n\n        return this;\n    }\n\n    public GetBlankFormPage clickGetBlankForm() {\n        return clickGetBlankForm(new GetBlankFormPage());\n    }\n\n    public <D extends Page<D>> D clickGetBlankForm(D destination) {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.get_forms))).perform(scrollTo(), click());\n        return destination.assertOnPage();\n    }\n\n    public SendFinalizedFormPage clickSendFinalizedForm(int number) {\n        assertNumberOfFinalizedForms(number);\n        onView(withId(R.id.send_data)).perform(click());\n        return new SendFinalizedFormPage();\n    }\n\n    public MainMenuPage setServer(String url) {\n        return openProjectSettingsDialog()\n                .clickSettings()\n                .clickServerSettings()\n                .clickOnURL()\n                .inputText(url)\n                .clickOKOnDialog()\n                .pressBack(new ProjectSettingsPage())\n                .pressBack(new MainMenuPage());\n    }\n\n    public MainMenuPage enableManualUpdates() {\n        return openProjectSettingsDialog()\n                .clickSettings()\n                .clickFormManagement()\n                .clickUpdateForms()\n                .clickOption(org.odk.collect.strings.R.string.manual)\n                .pressBack(new ProjectSettingsPage())\n                .pressBack(new MainMenuPage());\n    }\n\n    public MainMenuPage enablePreviouslyDownloadedOnlyUpdates() {\n        return openProjectSettingsDialog()\n                .clickSettings()\n                .clickFormManagement()\n                .clickUpdateForms()\n                .clickOption(org.odk.collect.strings.R.string.previously_downloaded_only)\n                .pressBack(new ProjectSettingsPage())\n                .pressBack(new MainMenuPage());\n    }\n\n    public MainMenuPage enablePreviouslyDownloadedOnlyUpdatesWithAutomaticDownload() {\n        return openProjectSettingsDialog()\n                .clickSettings()\n                .clickFormManagement()\n                .clickUpdateForms()\n                .clickOption(org.odk.collect.strings.R.string.previously_downloaded_only)\n                .clickOnString(org.odk.collect.strings.R.string.automatic_download)\n                .pressBack(new ProjectSettingsPage())\n                .pressBack(new MainMenuPage());\n    }\n\n    public MainMenuPage enableMatchExactly() {\n        return openProjectSettingsDialog()\n                .clickSettings()\n                .clickFormManagement()\n                .clickUpdateForms()\n                .clickOption(org.odk.collect.strings.R.string.match_exactly)\n                .pressBack(new ProjectSettingsPage())\n                .pressBack(new MainMenuPage());\n    }\n\n    public MainMenuPage enableAutoSend(TrackingCoroutineAndWorkManagerScheduler scheduler, int setting) {\n        MainMenuPage mainMenuPage = openProjectSettingsDialog()\n                .clickSettings()\n                .clickFormManagement()\n                .clickOnString(org.odk.collect.strings.R.string.autosend)\n                .clickOnString(setting)\n                .pressBack(new ProjectSettingsPage())\n                .pressBack(new MainMenuPage());\n\n        scheduler.runDeferredTasks(); // Run autosend scheduled after enabling\n        return mainMenuPage;\n    }\n\n    public MainMenuPage addAndSwitchToProject(String serverUrl) {\n        return openProjectSettingsDialog()\n                .clickAddProject()\n                .switchToManualMode()\n                .inputUrl(serverUrl)\n                .addProject();\n    }\n\n    public ServerAuthDialog clickGetBlankFormWithAuthenticationError() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.get_forms))).perform(scrollTo(), click());\n        return new ServerAuthDialog().assertOnPage();\n    }\n\n    public OkDialog clickGetBlankFormWithError() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.get_forms))).perform(scrollTo(), click());\n        return new OkDialog().assertOnPage();\n    }\n\n    public ViewSentFormPage clickViewSentForm(int number) {\n        assertNumberOfSentForms(number);\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.view_sent_forms))).perform(click());\n        return new ViewSentFormPage().assertOnPage();\n    }\n\n    public DeleteSavedFormPage clickDeleteSavedForm() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.manage_files))).perform(scrollTo(), click());\n        return new DeleteSavedFormPage().assertOnPage();\n    }\n\n    public MainMenuPage assertProjectIcon(String projectIcon) {\n        onView(allOf(hasDescendant(withText(projectIcon)), withId(R.id.projects))).check(matches(isDisplayed()));\n        return this;\n    }\n\n    public MainMenuPage copyForm(String formFilename) {\n        return copyForm(formFilename, null, \"Demo project\");\n    }\n\n    public MainMenuPage copyForm(String formFilename, String projectName) {\n        return copyForm(formFilename, null, projectName);\n    }\n\n    public MainMenuPage copyForm(String formFilename, List<String> mediaFilePaths) {\n        return copyForm(formFilename, mediaFilePaths, \"Demo project\");\n    }\n\n    public MainMenuPage copyForm(String formFilename, List<String> mediaFilePaths, String projectName) {\n        return copyForm(formFilename, mediaFilePaths, false, projectName);\n    }\n\n    public MainMenuPage copyForm(String formFilename, List<String> mediaFilePaths, boolean copyToDatabase, String projectName) {\n        try {\n            StorageUtils.copyFormToStorage(formFilename, mediaFilePaths, copyToDatabase, formFilename, projectName);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        return this;\n    }\n\n    public MainMenuPage copyInstance(String instanceFileName) {\n        copyInstance(instanceFileName, \"Demo project\");\n        return this;\n    }\n\n    public MainMenuPage copyInstance(String instanceFileName, String projectName) {\n        try {\n            StorageUtils.copyInstance(instanceFileName, projectName);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        return this;\n    }\n\n    public EntitiesPage openEntityBrowser() {\n        openProjectSettingsDialog()\n                .clickSettings()\n                .clickExperimental()\n                .clickOnString(org.odk.collect.strings.R.string.entity_browser_button);\n\n        return new EntitiesPage().assertOnPage();\n    }\n\n    public MainMenuPage refreshForms() {\n        return clickFillBlankForm()\n                .clickRefresh()\n                .pressBack(new MainMenuPage());\n    }\n}\n\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/MainMenuSettingsPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.ViewMatchers.isNotChecked\nimport androidx.test.espresso.matcher.ViewMatchers.withClassName\nimport org.hamcrest.Matchers.endsWith\nimport org.odk.collect.android.support.matchers.CustomMatchers.withIndex\n\nclass MainMenuSettingsPage : Page<MainMenuSettingsPage>() {\n    override fun assertOnPage(): MainMenuSettingsPage {\n        assertText(org.odk.collect.strings.R.string.main_menu_settings)\n        return this\n    }\n\n    fun assertDraftsUnchecked(): MainMenuSettingsPage {\n        onView(withIndex(withClassName(endsWith(\"CheckBox\")), 0)).check(matches(isNotChecked()))\n        return this\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ManualProjectCreatorDialogPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport org.odk.collect.testshared.WaitFor.tryAgainOnFail\n\nclass ManualProjectCreatorDialogPage : Page<ManualProjectCreatorDialogPage>() {\n    override fun assertOnPage(): ManualProjectCreatorDialogPage {\n        assertText(org.odk.collect.strings.R.string.add_project)\n        return this\n    }\n\n    fun inputUrl(url: String): ManualProjectCreatorDialogPage {\n        inputText(org.odk.collect.strings.R.string.server_url, url)\n        return this\n    }\n\n    fun inputUsername(username: String): ManualProjectCreatorDialogPage {\n        inputText(org.odk.collect.strings.R.string.username, username)\n        return this\n    }\n\n    fun inputPassword(password: String): ManualProjectCreatorDialogPage {\n        inputText(org.odk.collect.strings.R.string.password, password)\n        return this\n    }\n\n    fun addProject(): MainMenuPage {\n        tryAgainOnFail {\n            clickOnString(org.odk.collect.strings.R.string.add)\n            MainMenuPage().assertOnPage()\n        }\n\n        return MainMenuPage()\n    }\n\n    fun addProjectAndAssertDuplicateDialogShown(): ManualProjectCreatorDialogPage {\n        onView(withText(org.odk.collect.strings.R.string.add)).perform(click())\n        assertText(org.odk.collect.strings.R.string.duplicate_project_details)\n        return this\n    }\n\n    fun switchToExistingProject(): MainMenuPage {\n        clickOnString(org.odk.collect.strings.R.string.switch_to_existing)\n        return MainMenuPage().assertOnPage()\n    }\n\n    fun addDuplicateProject(): MainMenuPage {\n        clickOnString(org.odk.collect.strings.R.string.add_duplicate_project)\n        return MainMenuPage().assertOnPage()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/MapsSettingsPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\npublic class MapsSettingsPage extends Page<MapsSettingsPage> {\n\n    @Override\n    public MapsSettingsPage assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.maps);\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/NotificationDrawer.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.uiautomator.By\nimport androidx.test.uiautomator.UiDevice\nimport androidx.test.uiautomator.UiObject2\nimport androidx.test.uiautomator.Until\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.CoreMatchers.not\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.odk.collect.shared.TimeInMs\nimport org.odk.collect.testshared.WaitFor.waitFor\nimport java.util.regex.Pattern\n\nclass NotificationDrawer {\n    private var isOpen = false\n\n    fun open(): NotificationDrawer {\n        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n        device.openNotification()\n\n        assertThat(\n            \"Couldn't open notification drawer!\",\n            device.wait(Until.hasObject(DRAWER_SEARCH_CONDITION), TimeInMs.ONE_SECOND),\n            equalTo(true)\n        )\n        isOpen = true\n        return this\n    }\n\n    fun teardown() {\n        if (!isOpen) {\n            open()\n        }\n\n        clearAll()\n    }\n\n    @JvmOverloads\n    fun assertNotification(\n        appName: String,\n        title: String,\n        subtext: String? = null,\n        body: String? = null\n    ): NotificationDrawer {\n        val device = waitForNotification(appName, title)\n\n        if (subtext != null) {\n            assertExpandedText(device, appName, subtext)\n        }\n\n        if (body != null) {\n            assertExpandedText(device, appName, body)\n        }\n\n        return this\n    }\n\n    fun <D : Page<D>> clickAction(\n        appName: String,\n        title: String,\n        actionText: String,\n        destination: D\n    ): D {\n        val device = waitForNotification(appName, title)\n\n        val actionElement = getExpandedElement(device, appName, actionText) ?: getExpandedElement(\n            device,\n            appName,\n            actionText.uppercase()\n        )\n        if (actionElement != null) {\n            actionElement.click()\n            ensureClosed()\n        } else {\n            throw AssertionError(\"Could not find \\\"$actionText\\\"\")\n        }\n\n        val page = waitFor {\n            destination.assertOnPage()\n        }\n\n        assertNoNotification(appName)\n        return page\n    }\n\n    fun <D : Page<D>> clickNotification(\n        appName: String,\n        title: String,\n        destination: D\n    ): D {\n        val device = waitForNotification(appName, title)\n        device.findObject(By.text(title)).click()\n        ensureClosed()\n\n        return waitFor {\n            destination.assertOnPage()\n        }\n    }\n\n    fun clearAll() {\n        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n        val clearAll = device.findObject(By.text(\"Clear all\"))\n        if (clearAll != null) {\n            clearAll.click()\n            ensureClosed()\n        } else {\n            close()\n        }\n    }\n\n    fun assertNoNotification(appName: String) {\n        open()\n\n        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n        val result = device.wait(Until.hasObject(By.textStartsWith(appName)), 0L)\n        assertThat(\"Expected no notification for app: $appName\", result, equalTo(false))\n\n        close()\n    }\n\n    private fun assertExpandedText(\n        device: UiDevice,\n        appName: String,\n        text: String\n    ) {\n        val element = getExpandedElement(device, appName, text)\n        assertThat(\"Could not find \\\"$text\\\"\", element, not(nullValue()))\n    }\n\n    private fun getExpandedElement(\n        device: UiDevice,\n        appName: String,\n        text: String\n    ): UiObject2? {\n        var element = device.findObject(By.text(text))\n        if (element == null) {\n            expandOrCollapseNotification(device, appName)\n            element = device.findObject(By.text(text))\n        }\n        return element\n    }\n\n    private fun expandOrCollapseNotification(device: UiDevice, appName: String) {\n        device.findObject(By.text(appName)).click()\n    }\n\n    private fun waitForNotification(appName: String, title: String): UiDevice {\n        return waitFor {\n            val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n            val result = device.wait(Until.hasObject(By.text(appName)), 0L) && device.wait(Until.hasObject(By.text(title)), 0L)\n            assertThat(\n                \"No notification for app: $appName with title $title\",\n                result,\n                `is`(true)\n            )\n            device\n        }\n    }\n\n    private fun close() {\n        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n        if (isOpen) {\n            device.pressBack()\n        }\n\n        ensureClosed()\n    }\n\n    /**\n     * It appears that sometimes the notification drawer does not close automatically when it should\n     * such as after clicking on a notification or its action. This could be due to a bug in Android.\n     */\n    private fun ensureClosed() {\n        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n        if (!device.wait(Until.gone(DRAWER_SEARCH_CONDITION), TimeInMs.ONE_SECOND * 3)) {\n            device.pressBack()\n            assertThat(\n                \"Couldn't close notification drawer!\",\n                device.wait(Until.gone(DRAWER_SEARCH_CONDITION), TimeInMs.ONE_SECOND),\n                equalTo(true)\n            )\n        }\n\n        this.isOpen = false\n    }\n\n    companion object {\n        val SUPPORTED_SDKS = listOf(30, 34)\n\n        private val DRAWER_SEARCH_CONDITION = By.text(Pattern.compile(\"Manage|No notifications|Silent\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/OkDialog.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.RootMatchers.isDialog;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\npublic class OkDialog extends Page<OkDialog> {\n\n    @Override\n    public OkDialog assertOnPage() {\n        onView(withText(org.odk.collect.strings.R.string.ok)).inRoot(isDialog()).check(matches(isDisplayed()));\n        return this;\n    }\n\n    public <D extends Page<D>> D clickOK(D destination) {\n        return clickOnTextInDialog(org.odk.collect.strings.R.string.ok, destination);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/OpenSourceLicensesPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nclass OpenSourceLicensesPage extends Page<OpenSourceLicensesPage> {\n\n    @Override\n    public OpenSourceLicensesPage assertOnPage() {\n        asyncAssertText(\"Open Source Licenses\");\n        checkIfWebViewActivityIsDisplayed();\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport android.app.Activity\nimport android.app.Application\nimport android.content.pm.ActivityInfo\nimport android.view.View\nimport androidx.annotation.StringRes\nimport androidx.compose.ui.test.hasContentDescription\nimport androidx.compose.ui.test.hasText\nimport androidx.compose.ui.test.junit4.ComposeTestRule\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.NoActivityResumedException\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.longClick\nimport androidx.test.espresso.action.ViewActions.replaceText\nimport androidx.test.espresso.action.ViewActions.scrollTo\nimport androidx.test.espresso.action.ViewActions.typeText\nimport androidx.test.espresso.assertion.ViewAssertions.doesNotExist\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.contrib.RecyclerViewActions\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.RootMatchers.isPlatformPopup\nimport androidx.test.espresso.matcher.ViewMatchers\nimport androidx.test.espresso.matcher.ViewMatchers.hasDescendant\nimport androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.isEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.espresso.matcher.ViewMatchers.withClassName\nimport androidx.test.espresso.matcher.ViewMatchers.withContentDescription\nimport androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility\nimport androidx.test.espresso.matcher.ViewMatchers.withHint\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.uiautomator.UiDevice\nimport org.hamcrest.CoreMatchers.not\nimport org.hamcrest.Matcher\nimport org.hamcrest.Matchers\nimport org.hamcrest.Matchers.allOf\nimport org.hamcrest.core.StringContains.containsString\nimport org.hamcrest.core.StringEndsWith.endsWith\nimport org.junit.Assert\nimport org.junit.Assert.fail\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.android.R\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.support.ActivityHelpers.getLaunchIntent\nimport org.odk.collect.android.support.actions.RotateAction\nimport org.odk.collect.android.support.matchers.CustomMatchers.withIndex\nimport org.odk.collect.android.support.rules.RecentAppsRule\nimport org.odk.collect.android.utilities.ActionRegister\nimport org.odk.collect.androidshared.ui.SnackbarUtils\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport org.odk.collect.strings.localization.getLocalizedQuantityString\nimport org.odk.collect.strings.localization.getLocalizedString\nimport org.odk.collect.testshared.AssertionFramework\nimport org.odk.collect.testshared.ComposeAssertions\nimport org.odk.collect.testshared.ComposeInteractions\nimport org.odk.collect.testshared.EspressoAssertions\nimport org.odk.collect.testshared.EspressoInteractions\nimport org.odk.collect.testshared.RecyclerViewMatcher\nimport org.odk.collect.testshared.WaitFor.tryAgainOnFail\nimport org.odk.collect.testshared.WaitFor.waitFor\nimport timber.log.Timber\nimport java.io.File\n\n/**\n * Base class for Page Objects used in Espresso tests. Provides shared helpers/setup.\n *\n *\n * Sub classes of `Page` should represent a page or part of a \"page\" that the user would\n * interact with. Operations on these objects should return `this` (unless\n * transitioning to a new page) so that they can be used in a fluent style.\n *\n *\n * The generic typing is a little strange here but enables shared methods such as\n * [Page.closeSoftKeyboard] to return the sub class type rather than `Page` so\n * operations can be chained without casting. For example `FooPage` would extend\n * `Page<FooPage>` and calling `fooPage.closeSoftKeyboard()` would return\n * a `FooPage`.\n *\n * @see [Page Objects](https://www.martinfowler.com/bliki/PageObject.html)\n *\n * @see [Fluent Interfaces](https://en.wikipedia.org/wiki/Fluent_interface)\n */\n\n@Suppress(\"UNCHECKED_CAST\")\nabstract class Page<T : Page<T>> {\n\n    abstract fun assertOnPage(): T\n\n    fun <P : Page<P>> assertOnPage(page: P): P {\n        return page.assertOnPage()\n    }\n\n    /**\n     * Presses back and then returns the Page object passed in after\n     * asserting we're there\n     */\n    fun <D : Page<D>> pressBack(destination: D): D {\n        Espresso.pressBack()\n        return destination.assertOnPage()\n    }\n\n    fun pressBackKillingApp() {\n        try {\n            Espresso.pressBack()\n            fail(\"App was not killed!\")\n        } catch (e: NoActivityResumedException) {\n            // App killed as expected\n        }\n    }\n\n    fun assertTexts(vararg texts: String): T {\n        closeSoftKeyboard()\n        for (text in texts) {\n            assertText(text)\n        }\n        return this as T\n    }\n\n    fun assertText(stringID: Int, vararg formatArgs: Any): T {\n        assertText(getTranslatedString(stringID, *formatArgs))\n        return this as T\n    }\n\n    fun assertText(\n        stringID: Int,\n        assertionFramework: AssertionFramework = AssertionFramework.ESPRESSO,\n        vararg formatArgs: Any\n    ): T {\n        assertText(getTranslatedString(stringID, *formatArgs), assertionFramework)\n        return this as T\n    }\n\n    fun assertQuantityText(stringID: Int, quantity: Int, vararg formatArgs: Any): T {\n        assertText(getTranslatedQuantityString(stringID, quantity, *formatArgs))\n        return this as T\n    }\n\n    @JvmOverloads\n    fun assertText(stringID: Int, assertionFramework: AssertionFramework = AssertionFramework.ESPRESSO): T {\n        return assertText(getTranslatedString(stringID), assertionFramework)\n    }\n\n    @JvmOverloads\n    fun assertText(text: String, assertionFramework: AssertionFramework = AssertionFramework.ESPRESSO): T {\n        when (assertionFramework) {\n            AssertionFramework.ESPRESSO -> EspressoAssertions.assertVisible(withText(text))\n            AssertionFramework.COMPOSE -> ComposeAssertions.assertVisible(composeRule!!, text)\n        }\n\n        return this as T\n    }\n\n    fun asyncAssertText(text: String): T {\n        return waitFor {\n            assertText(text)\n        }\n    }\n\n    @JvmOverloads\n    fun assertTextThatContainsExists(text: String, index: Int = 0): T {\n        onView(\n            allOf(\n                withIndex(\n                    withText(\n                        containsString(\n                            text\n                        )\n                    ),\n                    index\n                ),\n                withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)\n            )\n        ).check(matches(not(doesNotExist())))\n        return this as T\n    }\n\n    @JvmOverloads\n    fun assertTextThatContainsDoesNoExist(text: String, index: Int = 0): T {\n        onView(\n            withIndex(\n                withText(\n                    containsString(\n                        text\n                    )\n                ),\n                index\n            )\n        ).check(doesNotExist())\n        return this as T\n    }\n\n    fun closeSoftKeyboard(): T {\n        Espresso.closeSoftKeyboard()\n        return this as T\n    }\n\n    fun assertTextsDoNotExist(vararg texts: String): T {\n        for (text in texts) {\n            assertTextDoesNotExist(text)\n        }\n        return this as T\n    }\n\n    @JvmOverloads\n    fun assertTextDoesNotExist(string: Int, assertionFramework: AssertionFramework = AssertionFramework.ESPRESSO): T {\n        return assertTextDoesNotExist(getTranslatedString(string), assertionFramework)\n    }\n\n    @JvmOverloads\n    fun assertTextDoesNotExist(text: String, assertionFramework: AssertionFramework = AssertionFramework.ESPRESSO): T {\n        when (assertionFramework) {\n            AssertionFramework.ESPRESSO -> EspressoAssertions.assertNotVisible(withText(text))\n            AssertionFramework.COMPOSE -> ComposeAssertions.assertNotVisible(\n                composeRule!!,\n                hasText(text)\n            )\n        }\n\n        return this as T\n    }\n\n    fun assertTextDoesNotExistInDialog(text: String?): T {\n        onView(allOf(withText(text), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))\n            .inRoot(isDialog())\n            .check(doesNotExist())\n        return this as T\n    }\n\n    fun checkIsSnackbarWithQuantityDisplayed(message: Int, quantity: Int): T {\n        return checkIsSnackbarWithMessageDisplayed(\n            ApplicationProvider.getApplicationContext<Application>()\n                .getLocalizedQuantityString(message, quantity, quantity)\n        )\n    }\n\n    fun checkIsSnackbarWithMessageDisplayed(message: Int, vararg formatArgs: Any): T {\n        return checkIsSnackbarWithMessageDisplayed(getTranslatedString(message, *formatArgs))\n    }\n\n    fun checkIsSnackbarWithMessageDisplayed(message: String): T {\n        EspressoAssertions.assertAlert(\n            SnackbarUtils.alertStore,\n            message,\n            \"No Snackbar with text \\\"$message\\\" shown on screen!\"\n        )\n        return this as T\n    }\n\n    fun assertToastNotDisplayed(message: String): T {\n        EspressoAssertions.assertNoAlert(\n            ToastUtils.alertStore,\n            message,\n            \"Toast with text \\\"$message\\\" shown on screen!\"\n        )\n        return this as T\n    }\n\n    fun checkIsToastWithMessageDisplayed(message: String): T {\n        EspressoAssertions.assertAlert(\n            ToastUtils.alertStore,\n            message,\n            \"No Toast with text \\\"$message\\\" shown on screen!\"\n        )\n        return this as T\n    }\n\n    fun checkIsToastWithMessageDisplayed(id: Int?, vararg formatArgs: Any): T {\n        return checkIsToastWithMessageDisplayed(getTranslatedString(id, *formatArgs))\n    }\n\n    @JvmOverloads\n    fun <D : Page<D>> clickOnString(\n        stringID: Int,\n        destination: D,\n        assertionFramework: AssertionFramework = AssertionFramework.ESPRESSO\n    ): D {\n        when (assertionFramework) {\n            AssertionFramework.ESPRESSO -> {\n                EspressoInteractions.clickOn(withText(getTranslatedString(stringID))) {\n                    destination.assertOnPage()\n                }\n            }\n\n            AssertionFramework.COMPOSE -> {\n                ComposeInteractions.clickOn(composeRule!!, hasText(getTranslatedString(stringID))) {\n                    destination.assertOnPage()\n                }\n            }\n        }\n\n        return destination\n    }\n\n    fun clickOnString(stringID: Int): T {\n        clickOnText(getTranslatedString(stringID))\n        return this as T\n    }\n\n    fun editSavedForm(formName: String): FormHierarchyPage {\n        clickOnText(getTranslatedString(org.odk.collect.strings.R.string.edit_form))\n        return FormHierarchyPage(formName)\n    }\n\n    fun viewSavedForm(formName: String): FormHierarchyPage {\n        clickOnText(getTranslatedString(org.odk.collect.strings.R.string.view_form))\n        return FormHierarchyPage(formName)\n    }\n\n    fun clickOnText(text: String): T {\n        EspressoInteractions.clickOn(\n            allOf(\n                withText(text),\n                withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)\n            )\n        )\n        return this as T\n    }\n\n    fun clickOnId(id: Int): T {\n        EspressoInteractions.clickOn(withId(id))\n        return this as T\n    }\n\n    fun checkIsIdDisplayed(id: Int): T {\n        onView(withId(id)).check(matches(isDisplayed()))\n        return this as T\n    }\n\n    fun clickOKOnDialog(): T {\n        closeSoftKeyboard() // Make sure to avoid issues with keyboard being up\n        waitForDialogToSettle()\n        EspressoInteractions.clickOn(withId(android.R.id.button1), root = isDialog())\n        return this as T\n    }\n\n    fun <D : Page<D>?> clickOKOnDialog(destination: D): D {\n        closeSoftKeyboard() // Make sure to avoid issues with keyboard being up\n        waitForDialogToSettle()\n        EspressoInteractions.clickOn(withId(android.R.id.button1), root = isDialog())\n        return destination!!.assertOnPage()\n    }\n\n    fun clickOnTextInDialog(text: String): T {\n        waitForDialogToSettle()\n        EspressoInteractions.clickOn(withText(text), root = isDialog())\n        return this as T\n    }\n\n    fun clickOnTextInDialog(text: Int): T {\n        return clickOnTextInDialog(getTranslatedString(text))\n    }\n\n    fun <D : Page<D>> clickOnTextInDialog(text: Int, destination: Page<D>): D {\n        return clickOnTextInDialog(getTranslatedString(text), destination)\n    }\n\n    fun <D : Page<D>> clickOnTextInDialog(text: String, destination: Page<D>): D {\n        clickOnTextInDialog(text)\n        return destination.assertOnPage()\n    }\n\n    fun getTranslatedString(id: Int?, vararg formatArgs: Any): String {\n        return ApplicationProvider.getApplicationContext<Collect>()\n            .getLocalizedString(id!!, *formatArgs)\n    }\n\n    fun getTranslatedQuantityString(id: Int?, quantity: Int, vararg formatArgs: Any): String {\n        return ApplicationProvider.getApplicationContext<Collect>()\n            .getLocalizedQuantityString(id!!, quantity, *formatArgs)\n    }\n\n    fun clickOnAreaWithIndex(clazz: String?, index: Int): T {\n        EspressoInteractions.clickOn(withIndex(withClassName(endsWith(clazz)), index))\n        return this as T\n    }\n\n    fun addText(existingText: String?, text: String?): T {\n        onView(withText(existingText)).perform(typeText(text))\n        return this as T\n    }\n\n    fun inputText(text: String): T {\n        onView(withClassName(endsWith(\"EditText\"))).perform(replaceText(text))\n        closeSoftKeyboard()\n        return this as T\n    }\n\n    fun inputText(hint: Int, text: String): T {\n        onView(withHint(getTranslatedString(hint))).perform(replaceText(text))\n        closeSoftKeyboard()\n        return this as T\n    }\n\n    fun checkIfElementIsGone(id: Int): T {\n        onView(withId(id)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))\n        return this as T\n    }\n\n    fun checkIsTextDisplayedOnDialog(text: String?): T {\n        onView(withId(android.R.id.message)).check(matches(withText(containsString(text))))\n        return this as T\n    }\n\n    fun assertEnabled(string: Int): T {\n        onView(withText(string)).check(matches(allOf(isDisplayed(), isEnabled())))\n        return this as T\n    }\n\n    fun assertDisabled(string: Int): T {\n        onView(withText(string)).check(matches(not(isEnabled())))\n        return this as T\n    }\n\n    fun <D : Page<D>?> rotateToLandscape(destination: D): D {\n        onView(isRoot()).perform(rotateToLandscape())\n        waitForRotationToEnd()\n        return destination!!.assertOnPage()\n    }\n\n    fun <D : Page<D>?> rotateToPortrait(destination: D): D {\n        onView(isRoot()).perform(rotateToPortrait())\n        waitForRotationToEnd()\n        return destination!!.assertOnPage()\n    }\n\n    fun waitForRotationToEnd(): T {\n        try {\n            Thread.sleep(3000)\n        } catch (e: InterruptedException) {\n            Timber.i(e)\n        }\n        return this as T\n    }\n\n    fun scrollToRecyclerViewItemAndClickText(text: String?): T {\n        onView(withId(androidx.preference.R.id.recycler_view)).perform(\n            RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(\n                hasDescendant(withText(text)),\n                scrollTo()\n            )\n        )\n        onView(withId(androidx.preference.R.id.recycler_view)).perform(\n            RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(\n                hasDescendant(withText(text)),\n                click()\n            )\n        )\n        return this as T\n    }\n\n    fun scrollToRecyclerViewItemAndClickText(string: Int): T {\n        onView(ViewMatchers.isAssignableFrom(RecyclerView::class.java)).perform(\n            RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(\n                hasDescendant(withText(getTranslatedString(string))),\n                scrollTo()\n            )\n        )\n        onView(ViewMatchers.isAssignableFrom(RecyclerView::class.java)).perform(\n            RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(\n                hasDescendant(withText(getTranslatedString(string))),\n                click()\n            )\n        )\n        return this as T\n    }\n\n    fun clickOnElementInHierarchy(index: Int): T {\n        onView(withId(R.id.list)).perform(\n            RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(\n                index\n            )\n        )\n        onView(\n            RecyclerViewMatcher.withRecyclerView(R.id.list)\n                .atPositionOnView(index, R.id.primary_text)\n        ).perform(click())\n        return this as T\n    }\n\n    fun checkListSizeInHierarchy(index: Int): T {\n        onView(withId(R.id.list)).check(matches(RecyclerViewMatcher.withListSize(index)))\n        return this as T\n    }\n\n    fun checkIfElementInHierarchyMatchesToText(text: String?, index: Int): T {\n        onView(\n            RecyclerViewMatcher.withRecyclerView(R.id.list)\n                .atPositionOnView(index, R.id.primary_text)\n        ).check(matches(withText(text)))\n        return this as T\n    }\n\n    fun checkIfWebViewActivityIsDisplayed(): T {\n        onView(withClassName(endsWith(\"WebView\"))).check(matches(isDisplayed()))\n        return this as T\n    }\n\n    protected fun waitForDialogToSettle() {\n        Thread.sleep(250) // https://github.com/android/android-test/issues/444\n    }\n\n    protected fun assertToolbarTitle(title: String?) {\n        onView(\n            allOf(\n                withText(title),\n                isDescendantOfA(withId(org.odk.collect.androidshared.R.id.toolbar))\n            )\n        ).check(matches(isDisplayed()))\n    }\n\n    protected fun assertToolbarTitle(title: Int) {\n        assertToolbarTitle(getTranslatedString(title))\n    }\n\n    @JvmOverloads\n    fun assertContentDescriptionDisplayed(\n        string: Int,\n        assertionFramework: AssertionFramework = AssertionFramework.ESPRESSO\n    ): T {\n        val translatedString = getTranslatedString(string)\n\n        when (assertionFramework) {\n            AssertionFramework.ESPRESSO -> {\n                EspressoAssertions.assertVisible(withContentDescription(translatedString))\n            }\n\n            AssertionFramework.COMPOSE -> {\n                ComposeAssertions.assertVisible(\n                    composeRule!!,\n                    hasContentDescription(translatedString)\n                )\n            }\n        }\n\n        return this as T\n    }\n\n    @JvmOverloads\n    fun assertContentDescriptionNotDisplayed(\n        string: Int,\n        assertionFramework: AssertionFramework = AssertionFramework.ESPRESSO\n    ): T {\n        val translatedString = getTranslatedString(string)\n\n        when (assertionFramework) {\n            AssertionFramework.ESPRESSO -> {\n                EspressoAssertions.assertNotVisible(withContentDescription(translatedString))\n            }\n\n            AssertionFramework.COMPOSE -> {\n                ComposeAssertions.assertNotVisible(\n                    composeRule!!,\n                    hasContentDescription(translatedString)\n                )\n            }\n        }\n\n        return this as T\n    }\n\n    fun clickOnContentDescription(string: Int): T {\n        EspressoInteractions.clickOn(withContentDescription(string))\n        return this as T\n    }\n\n    fun assertFileWithProjectNameUpdated(\n        sanitizedOldProjectName: String,\n        sanitizedNewProjectName: String\n    ): T {\n        val storagePathProvider = StoragePathProvider()\n        Assert.assertFalse(File(storagePathProvider.getProjectRootDirPath() + File.separator + sanitizedOldProjectName).exists())\n        Assert.assertTrue(File(storagePathProvider.getProjectRootDirPath() + File.separator + sanitizedNewProjectName).exists())\n        return this as T\n    }\n\n    fun assertTextInDialog(text: String): T {\n        onView(withText(text)).inRoot(isDialog()).check(matches(isDisplayed()))\n        return this as T\n    }\n\n    fun assertTextInDialog(text: Int, vararg formatArgs: Any): T {\n        return assertTextInDialog(getTranslatedString(text, *formatArgs))\n    }\n\n    fun closeSnackbar(): T {\n        EspressoInteractions.clickOn(withContentDescription(org.odk.collect.strings.R.string.close_snackbar))\n        return this as T\n    }\n\n    fun clickOptionsIcon(@StringRes expectedOptionString: Int): T {\n        return clickOptionsIcon(getTranslatedString(expectedOptionString))\n    }\n\n    fun clickOptionsIcon(expectedOptionString: String): T {\n        EspressoInteractions.clickOn(OVERFLOW_BUTTON_MATCHER) {\n            assertText(expectedOptionString)\n        }\n\n        return this as T\n    }\n\n    fun <D : Page<D>?> minimizeAndReopenApp(destination: D): D {\n        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n\n        // minimize\n        device.pressHome()\n\n        // reopen\n        InstrumentationRegistry.getInstrumentation().targetContext.apply {\n            val intent = packageManager.getLaunchIntentForPackage(BuildConfig.APPLICATION_ID)!!\n            startActivity(intent)\n        }\n        return destination!!.assertOnPage()\n    }\n\n    fun <D : Page<D>> killAndReopenApp(\n        launcherRule: ActivityScenarioLauncherRule,\n        recentAppsRule: RecentAppsRule,\n        destination: D,\n        appDependencyModule: AppDependencyModule? = null\n    ): D {\n        recentAppsRule.leaveAndKillApp(appDependencyModule)\n\n        // reopen\n        launcherRule.launch<Activity>(getLaunchIntent())\n        return destination.assertOnPage()\n    }\n\n    fun assertNoOptionsMenu(): T {\n        onView(OVERFLOW_BUTTON_MATCHER).check(doesNotExist())\n        return this as T\n    }\n\n    fun longClickOnText(text: String): T {\n        onView(withText(text)).perform(longClick())\n        return this as T\n    }\n\n    fun clickOnTextInPopup(text: Int): T {\n        EspressoInteractions.clickOn(withText(text), root = isPlatformPopup())\n        return this as T\n    }\n\n    fun tryFlakyAction(action: Runnable) {\n        tryAgainOnFail {\n            ActionRegister.attemptingAction()\n            action.run()\n            waitFor {\n                if (!ActionRegister.isActionDetected) {\n                    throw java.lang.RuntimeException(\"Action never detected!\")\n                }\n            }\n        }\n    }\n\n    fun <D : Page<D>> tryAgainOnFail(destination: D, action: Runnable): D {\n        tryAgainOnFail {\n            action.run()\n            destination.assertOnPage()\n        }\n\n        return destination\n    }\n\n    fun assertNoId(id: Int): T {\n        onView(withId(id)).check(doesNotExist())\n        return this as T\n    }\n\n    fun async(): AsyncPage<T> {\n        return AsyncPage(this as T)\n    }\n\n    fun assertTextBelow(below: String, above: String): T {\n        EspressoAssertions.assertBelow(withText(below), withText(above))\n        return this as T\n    }\n\n    fun assertTextBelow(below: Int, above: String): T {\n        EspressoAssertions.assertBelow(withText(getTranslatedString(below)), withText(above))\n        return this as T\n    }\n\n    fun assertTextBelow(below: String, above: Int): T {\n        EspressoAssertions.assertBelow(withText(below), withText(getTranslatedString(above)))\n        return this as T\n    }\n\n    fun assertTextBesides(one: Matcher<String>, two: Matcher<String>): T {\n        EspressoAssertions.assertVisible(withText(one), sibling = withText(two))\n        return this as T\n    }\n\n    companion object {\n        @JvmStatic\n        var composeRule: ComposeTestRule? = null\n\n        private fun rotateToLandscape(): ViewAction {\n            return RotateAction(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)\n        }\n\n        private fun rotateToPortrait(): ViewAction {\n            return RotateAction(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)\n        }\n\n        private val OVERFLOW_BUTTON_MATCHER: Matcher<View> = Matchers.anyOf(\n            allOf(isDisplayed(), withContentDescription(\"More options\")),\n            allOf(isDisplayed(), withClassName(Matchers.endsWith(\"OverflowMenuButton\")))\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/PreferencePage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.contrib.RecyclerViewActions.scrollTo;\nimport static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;\nimport static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\nimport static org.hamcrest.Matchers.allOf;\n\nabstract class PreferencePage<T extends Page<T>> extends Page<T> {\n\n    public T assertPreference(int name, String summary) {\n        onView(isAssignableFrom(RecyclerView.class))\n                .perform(scrollTo(allOf(hasDescendant(withText(getTranslatedString(name))), hasDescendant(withText(summary)))))\n                .check(matches(isDisplayed()));\n\n        return (T) this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ProjectDisplayPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.replaceText\nimport androidx.test.espresso.matcher.ViewMatchers.withContentDescription\nimport org.odk.collect.android.R\n\nclass ProjectDisplayPage : Page<ProjectDisplayPage>() {\n    override fun assertOnPage(): ProjectDisplayPage {\n        assertText(org.odk.collect.strings.R.string.project_display_title)\n        return this\n    }\n\n    fun setProjectName(projectName: String): ProjectDisplayPage {\n        clickOnString(org.odk.collect.strings.R.string.project_name)\n        inputText(projectName)\n        clickOKOnDialog()\n        return this\n    }\n\n    fun setProjectIcon(projectIcon: String): ProjectDisplayPage {\n        clickOnString(org.odk.collect.strings.R.string.project_icon)\n        inputText(projectIcon)\n        clickOKOnDialog()\n        return this\n    }\n\n    fun setProjectColor(projectColor: String): ProjectDisplayPage {\n        clickOnString(org.odk.collect.strings.R.string.project_color)\n        onView(withContentDescription(org.odk.collect.strings.R.string.hex_color)).perform(replaceText(projectColor))\n        clickOKOnDialog()\n        return this\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ProjectManagementPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nclass ProjectManagementPage : Page<ProjectManagementPage>() {\n\n    override fun assertOnPage(): ProjectManagementPage {\n        assertText(org.odk.collect.strings.R.string.project_management_section_title)\n        return this\n    }\n\n    fun clickOnResetProject(): ProjectManagementPage {\n        clickOnString(org.odk.collect.strings.R.string.reset_project_settings_title)\n        return this\n    }\n\n    fun clickConfigureQR(): QRCodePage {\n        clickOnString(org.odk.collect.strings.R.string.reconfigure_with_qr_code_settings_title)\n        return QRCodePage().assertOnPage()\n    }\n\n    fun deleteProject(): MainMenuPage {\n        scrollToRecyclerViewItemAndClickText(org.odk.collect.strings.R.string.delete_project)\n        inputText(getTranslatedString(org.odk.collect.strings.R.string.delete_trigger))\n        clickOnString(org.odk.collect.strings.R.string.delete_project_confirm_button_text)\n        return MainMenuPage()\n    }\n\n    fun deleteLastProject(): FirstLaunchPage {\n        scrollToRecyclerViewItemAndClickText(org.odk.collect.strings.R.string.delete_project)\n        inputText(\"delete\")\n        clickOnString(org.odk.collect.strings.R.string.delete_project_confirm_button_text)\n        return FirstLaunchPage()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ProjectSettingsDialogPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.doesNotExist\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers\nimport androidx.test.espresso.matcher.ViewMatchers.hasDescendant\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withContentDescription\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport org.hamcrest.Matchers.allOf\n\ninternal class ProjectSettingsDialogPage : Page<ProjectSettingsDialogPage>() {\n\n    override fun assertOnPage(): ProjectSettingsDialogPage {\n        assertText(org.odk.collect.strings.R.string.projects)\n        return this\n    }\n\n    fun clickSettings(): ProjectSettingsPage {\n        return clickOnTextInDialog(\n            org.odk.collect.strings.R.string.settings,\n            ProjectSettingsPage()\n        )\n    }\n\n    fun clickAbout(): AboutPage {\n        return clickOnTextInDialog(\n            org.odk.collect.strings.R.string.about_preferences,\n            AboutPage()\n        )\n    }\n\n    fun clickAddProject(): QrCodeProjectCreatorDialogPage {\n        return clickOnTextInDialog(\n            org.odk.collect.strings.R.string.add_project,\n            QrCodeProjectCreatorDialogPage()\n        )\n    }\n\n    fun assertCurrentProject(projectName: String, subtext: String): ProjectSettingsDialogPage {\n        onView(\n            allOf(\n                hasDescendant(withText(projectName)),\n                hasDescendant(withText(subtext)),\n                withContentDescription(\n                    getTranslatedString(\n                        org.odk.collect.strings.R.string.using_project,\n                        projectName\n                    )\n                )\n            )\n        ).check(matches(isDisplayed()))\n        return this\n    }\n\n    fun assertInactiveProject(projectName: String, subtext: String): ProjectSettingsDialogPage {\n        onView(\n            allOf(\n                hasDescendant(withText(projectName)),\n                hasDescendant(withText(subtext)),\n                withContentDescription(\n                    getTranslatedString(\n                        org.odk.collect.strings.R.string.switch_to_project,\n                        projectName\n                    )\n                )\n            )\n        ).check(matches(isDisplayed()))\n        return this\n    }\n\n    fun assertNotInactiveProject(projectName: String): ProjectSettingsDialogPage {\n        onView(\n            allOf(\n                hasDescendant(withText(projectName)),\n                withContentDescription(\n                    getTranslatedString(\n                        org.odk.collect.strings.R.string.switch_to_project,\n                        projectName\n                    )\n                )\n            )\n        ).check(doesNotExist())\n        return this\n    }\n\n    fun selectProject(projectName: String): MainMenuPage {\n        waitForDialogToSettle()\n        onView(\n            allOf(\n                hasDescendant(withText(projectName)),\n                withContentDescription(\n                    getTranslatedString(\n                        org.odk.collect.strings.R.string.switch_to_project,\n                        projectName\n                    )\n                )\n            )\n        )\n            .inRoot(RootMatchers.isDialog())\n            .perform(click())\n        return MainMenuPage().assertOnPage()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ProjectSettingsPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\npublic class ProjectSettingsPage extends Page<ProjectSettingsPage> {\n\n    @Override\n    public ProjectSettingsPage assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.project_settings);\n        return this;\n    }\n\n    public ProjectDisplayPage clickProjectDisplay() {\n        scrollToRecyclerViewItemAndClickText(org.odk.collect.strings.R.string.project_display_title);\n        return new ProjectDisplayPage().assertOnPage();\n    }\n\n    public ProjectManagementPage clickProjectManagement() {\n        scrollToRecyclerViewItemAndClickText(org.odk.collect.strings.R.string.project_management_section_title);\n        return new ProjectManagementPage().assertOnPage();\n    }\n\n    public AccessControlPage clickAccessControl() {\n        scrollToRecyclerViewItemAndClickText(org.odk.collect.strings.R.string.access_control_section_title);\n        return new AccessControlPage().assertOnPage();\n    }\n\n    public UserInterfacePage clickOnUserInterface() {\n        clickOnString(org.odk.collect.strings.R.string.client);\n        return new UserInterfacePage().assertOnPage();\n    }\n\n    public FormManagementPage openFormManagement() {\n        clickOnString(org.odk.collect.strings.R.string.form_management_preferences);\n        return new FormManagementPage();\n    }\n\n    public ServerSettingsPage clickServerSettings() {\n        clickOnString(org.odk.collect.strings.R.string.server_settings_title);\n        return new ServerSettingsPage().assertOnPage();\n    }\n\n    public MapsSettingsPage clickMaps() {\n        clickOnString(org.odk.collect.strings.R.string.maps);\n        return new MapsSettingsPage().assertOnPage();\n    }\n\n\n    public UserAndDeviceIdentitySettingsPage clickUserAndDeviceIdentity() {\n        clickOnString(org.odk.collect.strings.R.string.user_and_device_identity_title);\n        return new UserAndDeviceIdentitySettingsPage().assertOnPage();\n    }\n\n    public ProjectSettingsPage checkIfServerOptionIsDisplayed() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.server_settings_title))).check(matches(isDisplayed()));\n        return this;\n    }\n\n    public ProjectSettingsPage checkIfFormManagementOptionIsDisplayed() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.form_management_preferences))).check(matches(isDisplayed()));\n        return this;\n    }\n\n    public ProjectSettingsPage checkIfServerOptionIsNotDisplayed() {\n        onView(withText(\"Server\")).check(doesNotExist());\n        return this;\n    }\n\n    public FormManagementPage clickFormManagement() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.form_management_preferences))).perform(click());\n        return new FormManagementPage();\n    }\n\n    public ExperimentalPage clickExperimental() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.experimental))).perform(click());\n        return new ExperimentalPage().assertOnPage();\n    }\n\n    public ProjectSettingsPage setAdminPassword(String password) {\n        scrollToRecyclerViewItemAndClickText(org.odk.collect.strings.R.string.set_admin_password);\n        inputText(password);\n        clickOKOnDialog();\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/QRCodePage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\nimport android.graphics.Bitmap;\n\nimport androidx.test.espresso.Espresso;\n\nimport org.odk.collect.android.support.ActivityHelpers;\nimport org.odk.collect.testshared.WaitFor;\nimport org.odk.collect.androidtest.DrawableMatcher;\n\npublic class QRCodePage extends Page<QRCodePage> {\n    @Override\n    public QRCodePage assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.reconfigure_with_qr_code_settings_title);\n        return this;\n    }\n\n    public QRCodePage clickScanFragment() {\n        onView(withText(org.odk.collect.strings.R.string.scan_qr_code_fragment_title)).perform(click());\n        return this;\n    }\n\n    public QRCodePage clickView() {\n        // Switching tabs doesn't seem to work sometimes\n        WaitFor.waitFor(() -> {\n            onView(withText(org.odk.collect.strings.R.string.view_qr_code_fragment_title)).perform(click());\n            onView(withText(org.odk.collect.strings.R.string.barcode_scanner_prompt)).check(doesNotExist());\n            return null;\n        });\n\n        return this;\n    }\n\n    public QRCodePage assertImageViewShowsImage(int resourceid, Bitmap image) {\n        onView(withId(resourceid)).check(matches(DrawableMatcher.withBitmap(image)));\n        return this;\n    }\n\n    public QRCodePage clickOnMenu() {\n        return tryAgainOnFail(this, () -> {\n            Espresso.openActionBarOverflowOrOptionsMenu(ActivityHelpers.getActivity());\n            onView(withText(getTranslatedString(org.odk.collect.strings.R.string.import_qrcode_sd))).check(matches(isDisplayed()));\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/QrCodeProjectCreatorDialogPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withText\n\nclass QrCodeProjectCreatorDialogPage : Page<QrCodeProjectCreatorDialogPage>() {\n    override fun assertOnPage(): QrCodeProjectCreatorDialogPage {\n        assertText(org.odk.collect.strings.R.string.add_project)\n        return this\n    }\n\n    fun switchToManualMode(): ManualProjectCreatorDialogPage {\n        return clickOnTextInDialog(org.odk.collect.strings.R.string.configure_manually, ManualProjectCreatorDialogPage())\n    }\n\n    fun assertDuplicateDialogShown(): QrCodeProjectCreatorDialogPage {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.duplicate_project_details)))\n            .inRoot(isDialog())\n            .check(matches(isDisplayed()))\n\n        return this\n    }\n\n    fun switchToExistingProject(): MainMenuPage {\n        return clickOnTextInDialog(org.odk.collect.strings.R.string.switch_to_existing, MainMenuPage())\n    }\n\n    fun addDuplicateProject(): MainMenuPage {\n        return clickOnTextInDialog(org.odk.collect.strings.R.string.add_duplicate_project, MainMenuPage())\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ResetApplicationDialog.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.assertion.ViewAssertions.matches;\nimport static androidx.test.espresso.matcher.RootMatchers.isDialog;\nimport static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\npublic class ResetApplicationDialog extends Page<ResetApplicationDialog> {\n\n    @Override\n    public ResetApplicationDialog assertOnPage() {\n        onView(withText(org.odk.collect.strings.R.string.reset_settings_dialog_title)).inRoot(isDialog()).check(matches(isDisplayed()));\n        return this;\n    }\n}\n\n\n\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SaveOrDiscardFormDialog.kt",
    "content": "package org.odk.collect.android.support.pages\n\nclass SaveOrDiscardFormDialog<D : Page<D>> @JvmOverloads constructor(\n    private val destination: D,\n    private val saveAsDraftEnabled: Boolean = true\n) : Page<SaveOrDiscardFormDialog<D>>() {\n\n    override fun assertOnPage(): SaveOrDiscardFormDialog<D> {\n        if (saveAsDraftEnabled) {\n            assertTextInDialog(org.odk.collect.strings.R.string.quit_form_title)\n        } else {\n            assertTextInDialog(org.odk.collect.strings.R.string.quit_form_continue_title)\n        }\n\n        return this\n    }\n\n    fun clickSaveChanges(): D {\n        return clickOnTextInDialog(org.odk.collect.strings.R.string.save_as_draft, destination)\n    }\n\n    fun clickDiscardForm(): D {\n        return clickOnTextInDialog(org.odk.collect.strings.R.string.do_not_save, destination)\n    }\n\n    fun clickDiscardChanges(): D {\n        return clickOnTextInDialog(org.odk.collect.strings.R.string.discard_changes, destination)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SaveOrIgnoreDrawingDialog.kt",
    "content": "package org.odk.collect.android.support.pages\n\nclass SaveOrIgnoreDrawingDialog<D : Page<D>>(\n    private val drawingName: String,\n    private val destination: D\n) : Page<SaveOrIgnoreDrawingDialog<D>>() {\n\n    override fun assertOnPage(): SaveOrIgnoreDrawingDialog<D> {\n        val title = getTranslatedString(org.odk.collect.strings.R.string.exit) + \" \" + drawingName\n        assertText(title)\n        return this\n    }\n\n    fun clickSaveChanges(): D {\n        return clickOnTextInDialog(org.odk.collect.strings.R.string.keep_changes, destination)\n    }\n\n    fun clickDiscardChanges(): D {\n        return clickOnTextInDialog(org.odk.collect.strings.R.string.discard_changes, destination)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SavepointRecoveryDialogPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport org.odk.collect.strings.R\n\nclass SavepointRecoveryDialogPage : Page<SavepointRecoveryDialogPage>() {\n    override fun assertOnPage(): SavepointRecoveryDialogPage {\n        val title = getTranslatedString(R.string.savepoint_recovery_dialog_title)\n        onView(withText(title)).inRoot(isDialog()).check(matches(isDisplayed()))\n        return this\n    }\n\n    fun <D : Page<D>> clickRecover(destination: D): D {\n        return this.clickOnTextInDialog(R.string.recover, destination)\n    }\n\n    fun <D : Page<D>> clickDoNotRecover(destination: D): D {\n        return this.clickOnTextInDialog(R.string.do_not_recover, destination)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SelectMinimalDialogPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nclass SelectMinimalDialogPage(private val formName: String) : Page<SelectMinimalDialogPage>() {\n    override fun assertOnPage(): SelectMinimalDialogPage {\n        assertTextDoesNotExistInDialog(formName)\n        return this\n    }\n\n    fun selectItem(item: String): FormEntryPage {\n        return clickOnTextInDialog(item, FormEntryPage(formName))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/SendFinalizedFormPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.ViewMatchers\nimport androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport org.hamcrest.CoreMatchers.allOf\nimport org.odk.collect.android.support.matchers.CustomMatchers.withIndex\nimport org.odk.collect.strings.R\n\nclass SendFinalizedFormPage : Page<SendFinalizedFormPage>() {\n    override fun assertOnPage(): SendFinalizedFormPage {\n        onView(\n            allOf(\n                withText(getTranslatedString(R.string.send_data)),\n                isDescendantOfA(withId(org.odk.collect.androidshared.R.id.toolbar))\n            )\n        ).check(matches(isDisplayed()))\n        return this\n    }\n\n    fun clickOnForm(formLabel: String): ViewFormPage {\n        clickOnText(formLabel)\n        return ViewFormPage(formLabel).assertOnPage()\n    }\n\n    fun clickOnForm(formName: String, instanceName: String): ViewFormPage {\n        clickOnText(instanceName)\n        return ViewFormPage(formName).assertOnPage()\n    }\n\n    fun clickSendSelected(): OkDialog {\n        clickOnText(getTranslatedString(R.string.send_selected_data))\n        return OkDialog()\n    }\n\n    fun clickSendSelectedWithAuthenticationError(): ServerAuthDialog {\n        clickOnText(getTranslatedString(R.string.send_selected_data))\n        return ServerAuthDialog().assertOnPage()\n    }\n\n    fun clickSelectAll(): SendFinalizedFormPage {\n        clickOnString(R.string.select_all)\n        return this\n    }\n\n    @Deprecated(\"uses the deprecated {@link org.odk.collect.android.support.matchers.CustomMatchers#withIndex(Matcher, int)})} helper.\")\n    fun selectForm(index: Int): SendFinalizedFormPage {\n        onView(withIndex(ViewMatchers.withId(androidx.appcompat.R.id.checkbox), index)).perform(click())\n        return this\n    }\n\n    fun sortByDateOldestFirst(): SendFinalizedFormPage {\n        onView(withId(org.odk.collect.android.R.id.menu_sort)).perform(click())\n        clickOnString(R.string.sort_by_date_asc)\n        return this\n    }\n\n    fun sortByDateNewestFirst(): SendFinalizedFormPage {\n        onView(withId(org.odk.collect.android.R.id.menu_sort)).perform(click())\n        clickOnString(R.string.sort_by_date_desc)\n        return this\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ServerAuthDialog.java",
    "content": "package org.odk.collect.android.support.pages;\n\npublic class ServerAuthDialog extends Page<ServerAuthDialog> {\n\n    @Override\n    public ServerAuthDialog assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.server_requires_auth);\n        return this;\n    }\n\n    public ServerAuthDialog fillUsername(String username) {\n        inputText(org.odk.collect.strings.R.string.username, username);\n        return this;\n    }\n\n    public ServerAuthDialog fillPassword(String password) {\n        inputText(org.odk.collect.strings.R.string.password, password);\n        return this;\n    }\n\n    public <D extends Page<D>> D clickOK(D destination) {\n        clickOnString(org.odk.collect.strings.R.string.ok);\n        return destination.assertOnPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ServerSettingsPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\n\npublic class ServerSettingsPage extends Page<ServerSettingsPage> {\n\n    @Override\n    public ServerSettingsPage assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.server_preferences);\n        return this;\n    }\n\n    public ServerSettingsPage clickServerUsername() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.username))).perform(click());\n        return this;\n    }\n\n    public ServerSettingsPage clickOnURL() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.server_url))).perform(click());\n        return this;\n    }\n\n    public ServerSettingsPage clickServerPassword() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.password))).perform(click());\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ShortcutsPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport android.content.Intent;\n\nimport androidx.test.core.app.ActivityScenario;\n\nimport org.odk.collect.android.external.AndroidShortcutsActivity;\n\npublic class ShortcutsPage extends Page<ShortcutsPage> {\n\n    private final ActivityScenario<AndroidShortcutsActivity> scenario;\n\n    public ShortcutsPage(ActivityScenario<AndroidShortcutsActivity> scenario) {\n        this.scenario = scenario;\n    }\n\n    @Override\n    public ShortcutsPage assertOnPage() {\n        assertTextInDialog(org.odk.collect.strings.R.string.select_odk_shortcut);\n        return this;\n    }\n\n    public Intent selectForm(String formName) {\n        clickOnText(formName);\n        return scenario.getResult().getResultData();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/UserAndDeviceIdentitySettingsPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\npublic class UserAndDeviceIdentitySettingsPage extends Page<UserAndDeviceIdentitySettingsPage> {\n\n    @Override\n    public UserAndDeviceIdentitySettingsPage assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.user_and_device_identity_title);\n        return this;\n    }\n\n    public FormMetadataPage clickFormMetadata() {\n        clickOnString(org.odk.collect.strings.R.string.form_metadata);\n        return new FormMetadataPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/UserInterfacePage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onData;\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\nimport static org.hamcrest.Matchers.equalTo;\n\npublic class UserInterfacePage extends Page<UserInterfacePage> {\n\n    @Override\n    public UserInterfacePage assertOnPage() {\n        assertText(org.odk.collect.strings.R.string.client);\n        return this;\n    }\n\n    public UserInterfacePage clickOnLanguage() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.language))).perform(click());\n        return this;\n    }\n\n    public MainMenuPage clickOnSelectedLanguage(String language) {\n        onData(equalTo(language)).perform(click());\n        return new MainMenuPage().assertOnPage();\n    }\n\n    public UserInterfacePage clickNavigation() {\n        clickOnString(org.odk.collect.strings.R.string.navigation);\n        return this;\n    }\n\n    public UserInterfacePage clickUseSwipesAndButtons() {\n        clickOnString(org.odk.collect.strings.R.string.swipe_buttons_navigation);\n        return this;\n    }\n\n    public UserInterfacePage clickOnTheme() {\n        onView(withText(getTranslatedString(org.odk.collect.strings.R.string.app_theme))).perform(click());\n        return this;\n    }\n\n    public UserInterfacePage clickUseNavigationButtons() {\n        clickOnString(org.odk.collect.strings.R.string.buttons_navigation);\n        return this;\n    }\n\n    public UserInterfacePage clickSwipes() {\n        clickOnString(org.odk.collect.strings.R.string.swipe_navigation);\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ViewFormPage.kt",
    "content": "package org.odk.collect.android.support.pages\n\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.doesNotExist\nimport androidx.test.espresso.contrib.RecyclerViewActions.scrollTo\nimport androidx.test.espresso.matcher.ViewMatchers.hasDescendant\nimport androidx.test.espresso.matcher.ViewMatchers.withContentDescription\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport org.odk.collect.android.R\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.testshared.AssertionFramework\n\nclass ViewFormPage(private val formName: String) : Page<ViewFormPage>() {\n\n    override fun assertOnPage(): ViewFormPage {\n        assertToolbarTitle(formName)\n        assertText(string.exit)\n        return this\n    }\n\n    fun clickOnGroup(groupLabel: String): ViewFormPage {\n        onView(withId(R.id.list)).perform(scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText(groupLabel))))\n\n        clickOnText(groupLabel)\n        return this\n    }\n\n    fun editForm(formName: String): FormHierarchyPage {\n        clickOnContentDescription(string.edit_finalized_form)\n        checkIsSnackbarWithMessageDisplayed(string.finalized_form_edit_started)\n        return FormHierarchyPage(formName).assertOnPage()\n    }\n\n    fun editFormWithError(): ViewFormPage {\n        clickOnContentDescription(string.edit_finalized_form)\n        return this\n    }\n\n    fun acceptEditingNewerDraftEdit(formName: String): FormHierarchyPage {\n        clickOnTextInDialog(string.newer_draft_edit_found_dialog_positive_button)\n        return FormHierarchyPage(formName).assertOnPage()\n    }\n\n    fun acceptEditingNewerFinalizedEdit(formName: String): FormHierarchyPage {\n        clickOnTextInDialog(string.newer_finalized_edit_found_dialog_positive_button)\n        checkIsSnackbarWithMessageDisplayed(string.finalized_form_edit_started)\n        return FormHierarchyPage(formName).assertOnPage()\n    }\n\n    fun discardEditingNewerEdit(): ViewFormPage {\n        clickOnTextInDialog(string.cancel)\n        return this\n    }\n\n    fun assertNonEditableForm(): ViewFormPage {\n        onView(withContentDescription(string.edit_finalized_form)).check(doesNotExist())\n        return this\n    }\n\n    fun assertAnswer(answer: String): ViewFormPage {\n        assertText(answer, AssertionFramework.COMPOSE)\n        return this\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/pages/ViewSentFormPage.java",
    "content": "package org.odk.collect.android.support.pages;\n\nimport static androidx.test.espresso.Espresso.onData;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.matcher.CursorMatchers.withRowString;\n\nimport org.odk.collect.android.database.forms.DatabaseFormColumns;\n\npublic class ViewSentFormPage extends Page<ViewSentFormPage> {\n\n    @Override\n    public ViewSentFormPage assertOnPage() {\n        assertToolbarTitle(org.odk.collect.strings.R.string.view_sent_forms);\n        return this;\n    }\n\n    public ViewFormPage clickOnForm(String formName) {\n        onData(withRowString(DatabaseFormColumns.DISPLAY_NAME, formName)).perform(click());\n        return new ViewFormPage(formName).assertOnPage();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/BlankFormTestRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport org.odk.collect.android.support.pages.FormEntryPage\n\nclass BlankFormTestRule @JvmOverloads constructor(\n    private val formFilename: String,\n    private val formName: String,\n    private val mediaFilePaths: List<String>? = null\n) : FormEntryActivityTestRule() {\n\n    private lateinit var formEntryPage: FormEntryPage\n\n    override fun before() {\n        super.before()\n        setUpProjectAndCopyForm(formFilename, mediaFilePaths)\n        formEntryPage = fillNewForm(formFilename, formName)\n    }\n\n    fun startInFormEntry(): FormEntryPage {\n        return formEntryPage\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/CollectTestRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport android.app.Activity\nimport android.app.Instrumentation\nimport android.content.Intent\nimport org.odk.collect.android.external.AndroidShortcutsActivity\nimport org.odk.collect.android.support.ActivityHelpers.getLaunchIntent\nimport org.odk.collect.android.support.StubOpenRosaServer\nimport org.odk.collect.android.support.pages.FirstLaunchPage\nimport org.odk.collect.android.support.pages.MainMenuPage\nimport org.odk.collect.android.support.pages.Page\nimport org.odk.collect.android.support.pages.ShortcutsPage\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport java.util.function.Consumer\n\nclass CollectTestRule @JvmOverloads constructor(\n    private val useDemoProject: Boolean = true\n) : ActivityScenarioLauncherRule() {\n\n    override fun before() {\n        super.before()\n\n        val firstLaunchPage = launch(\n            getLaunchIntent(),\n            FirstLaunchPage()\n        ).assertOnPage()\n\n        if (useDemoProject) {\n            firstLaunchPage.clickTryCollect()\n        }\n    }\n\n    fun startAtMainMenu() = MainMenuPage()\n\n    fun startAtFirstLaunch() = FirstLaunchPage()\n\n    @JvmOverloads\n    fun withProject(serverUrl: String, matchExactly: Boolean = false): MainMenuPage {\n        return if (matchExactly) {\n            startAtFirstLaunch()\n                .clickManuallyEnterProjectDetails()\n                .inputUrl(serverUrl)\n                .addProject()\n                .enableMatchExactly()\n                .clickFillBlankForm()\n                .clickRefresh()\n                .pressBack(MainMenuPage())\n        } else {\n            startAtFirstLaunch()\n                .clickManuallyEnterProjectDetails()\n                .inputUrl(serverUrl)\n                .addProject()\n        }\n    }\n\n    fun withProject(testServer: StubOpenRosaServer, vararg formFiles: String): MainMenuPage {\n        val mainMenuPage = startAtFirstLaunch()\n            .clickManuallyEnterProjectDetails()\n            .inputUrl(testServer.url)\n            .addProject()\n\n        return if (formFiles.isNotEmpty()) {\n            formFiles.fold(mainMenuPage) { page, formFile -> page.copyForm(formFile, testServer.hostName) }\n        } else {\n            mainMenuPage\n        }\n    }\n\n    fun launchShortcuts(): ShortcutsPage {\n        val scenario = launchForResult(AndroidShortcutsActivity::class.java)\n        return ShortcutsPage(scenario).assertOnPage()\n    }\n\n    fun <D : Page<D>> relaunch(destination: D): D {\n        return launch(getLaunchIntent(), destination)\n    }\n\n    fun <T : Page<T>> launch(intent: Intent, destination: T): T {\n        launch<Activity>(intent)\n        return destination.assertOnPage()\n    }\n\n    fun <T : Page<T>> launchForResult(\n        intent: Intent,\n        destination: T,\n        actions: Consumer<T>\n    ): Instrumentation.ActivityResult {\n        val scenario = launchForResult<Activity>(intent)\n        destination.async().assertOnPage()\n        actions.accept(destination)\n        return scenario.result\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/FormEntryActivityTestRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport android.app.Activity\nimport android.app.Application\nimport android.content.Intent\nimport androidx.test.core.app.ActivityScenario\nimport androidx.test.core.app.ApplicationProvider\nimport org.junit.rules.ExternalResource\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.android.formmanagement.FormFillingIntentFactory\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.storage.StorageSubdirectory\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.StorageUtils\nimport org.odk.collect.android.support.pages.FormEntryPage\nimport org.odk.collect.android.support.pages.FormHierarchyPage\nimport org.odk.collect.android.support.pages.Page\nimport org.odk.collect.android.support.pages.SavepointRecoveryDialogPage\nimport timber.log.Timber\nimport java.io.IOException\n\nopen class FormEntryActivityTestRule :\n    ExternalResource() {\n\n    private lateinit var intent: Intent\n    private lateinit var scenario: ActivityScenario<Activity>\n\n    override fun after() {\n        try {\n            scenario.close()\n        } catch (e: Throwable) {\n            Timber.e(Error(\"Error closing ActivityScenario: $e\"))\n        }\n    }\n\n    @JvmOverloads\n    fun setUpProjectAndCopyForm(\n        formFilename: String,\n        mediaFilePaths: List<String>? = null\n    ): FormEntryActivityTestRule {\n        try {\n            // Set up demo project\n            CollectHelpers.addDemoProject()\n            StorageUtils.copyFormToDemoProject(formFilename, mediaFilePaths, true)\n        } catch (e: IOException) {\n            throw RuntimeException(e)\n        }\n\n        return this\n    }\n\n    fun <D : Page<D>> fillNewForm(formFilename: String, destination: D): D {\n        intent = createNewFormIntent(formFilename)\n        scenario = ActivityScenario.launch(intent)\n        return destination.assertOnPage()\n    }\n\n    fun fillNewForm(formFilename: String, formName: String): FormEntryPage {\n        return fillNewForm(formFilename, FormEntryPage(formName))\n    }\n\n    fun fillNewFormWithSavepoint(formFilename: String): SavepointRecoveryDialogPage {\n        intent = createNewFormIntent(formFilename)\n        scenario = ActivityScenario.launch(intent)\n        return SavepointRecoveryDialogPage().assertOnPage()\n    }\n\n    fun editForm(formFilename: String, instanceName: String): FormHierarchyPage {\n        intent = createEditFormIntent(formFilename)\n        scenario = ActivityScenario.launch(intent)\n        return FormHierarchyPage(instanceName).async().assertOnPage()\n    }\n\n    fun editFormWithSavepoint(formFilename: String): SavepointRecoveryDialogPage {\n        intent = createEditFormIntent(formFilename)\n        scenario = ActivityScenario.launch(intent)\n        return SavepointRecoveryDialogPage().assertOnPage()\n    }\n\n    fun simulateProcessRestart(): FormEntryActivityTestRule {\n        CollectHelpers.simulateProcessRestart()\n        return this\n    }\n\n    private fun createNewFormIntent(formFilename: String): Intent {\n        val application = ApplicationProvider.getApplicationContext<Application>()\n        val formPath = DaggerUtils.getComponent(application).storagePathProvider()\n            .getOdkDirPath(StorageSubdirectory.FORMS) + \"/\" + formFilename\n        val form = DaggerUtils.getComponent(application).formsRepositoryProvider().create()\n            .getOneByPath(formPath)\n        val projectId = DaggerUtils.getComponent(application).currentProjectProvider()\n            .requireCurrentProject().uuid\n\n        return FormFillingIntentFactory.newFormIntent(\n            application,\n            FormsContract.getUri(projectId, form!!.dbId)\n        )\n    }\n\n    private fun createEditFormIntent(formFilename: String): Intent {\n        val application = ApplicationProvider.getApplicationContext<Application>()\n        val formPath = DaggerUtils.getComponent(application).storagePathProvider()\n            .getOdkDirPath(StorageSubdirectory.FORMS) + \"/\" + formFilename\n        val form = DaggerUtils.getComponent(application).formsRepositoryProvider().create()\n            .getOneByPath(formPath)\n        val instance = DaggerUtils.getComponent(application).instancesRepositoryProvider().create()\n            .getAllByFormId(form!!.formId).first()\n        val projectId = DaggerUtils.getComponent(application).currentProjectProvider()\n            .requireCurrentProject().uuid\n\n        return FormFillingIntentFactory.editDraftFormIntent(\n            application,\n            projectId,\n            instance.dbId\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/IdlingResourceRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport androidx.test.espresso.IdlingRegistry\nimport androidx.test.espresso.IdlingResource\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\n\nclass IdlingResourceRule(\n    private val idlingResources: List<IdlingResource>\n) : TestRule {\n\n    constructor(idlingResource: IdlingResource) : this(listOf(idlingResource))\n\n    override fun apply(base: Statement, description: Description): Statement =\n        IdlingResourceStatement(idlingResources, base)\n\n    private class IdlingResourceStatement(\n        private val idlingResources: List<IdlingResource>,\n        private val base: Statement\n    ) : Statement() {\n\n        override fun evaluate() {\n            for (idlingResources in idlingResources) {\n                IdlingRegistry.getInstance().register(idlingResources)\n            }\n\n            try {\n                base.evaluate()\n            } finally {\n                for (idlingResources in idlingResources) {\n                    IdlingRegistry.getInstance().unregister(idlingResources)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/NotificationDrawerRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport android.os.Build\nimport org.junit.Assert.assertTrue\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\nimport org.odk.collect.android.support.pages.NotificationDrawer\nimport org.odk.collect.android.support.pages.NotificationDrawer.Companion.SUPPORTED_SDKS\n\nclass NotificationDrawerRule : TestRule {\n    private val notificationDrawer = NotificationDrawer()\n\n    override fun apply(base: Statement, description: Description): Statement {\n        return object : Statement() {\n            @Throws(Throwable::class)\n            override fun evaluate() {\n                assertTrue(\n                    \"${this.javaClass.simpleName} does not support this API level!\",\n                    SUPPORTED_SDKS.contains(Build.VERSION.SDK_INT)\n                )\n\n                try {\n                    base.evaluate()\n                } finally {\n                    notificationDrawer.teardown()\n                }\n            }\n        }\n    }\n\n    fun open(): NotificationDrawer {\n        return notificationDrawer.open()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/PageComposeRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport androidx.compose.ui.test.junit4.ComposeTestRule\nimport androidx.compose.ui.test.junit4.createEmptyComposeRule\nimport org.junit.rules.ExternalResource\nimport org.odk.collect.android.support.pages.Page\n\n/**\n * Wrapper around a ComposeTestRule that automatically assigns Page.composeRule\n * before the test.\n */\nclass PageComposeRule(\n    val composeRule: ComposeTestRule = createEmptyComposeRule()\n) : ExternalResource() {\n\n    override fun before() {\n        Page.composeRule = composeRule\n    }\n\n    override fun after() {\n        Page.composeRule = null\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/PrepDeviceForTestsRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport androidx.test.platform.app.InstrumentationRegistry.getInstrumentation\nimport androidx.test.uiautomator.UiDevice\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\n\n/**\n * Disables animations and sets long press timeout to 3 seconds in an attempt to avoid flakiness.\n */\nclass PrepDeviceForTestsRule : TestRule {\n\n    override fun apply(base: Statement, description: Description): Statement {\n        return object : Statement() {\n            override fun evaluate() {\n                setAnimationScale(0)\n                setLongPressTimeout(3000)\n\n                try {\n                    base.evaluate()\n                } finally {\n                    setAnimationScale(1)\n                    setLongPressTimeout(500)\n                }\n            }\n        }\n    }\n\n    private fun setLongPressTimeout(timeout: Int) {\n        executeShellCommand(\"settings put secure long_press_timeout $timeout\")\n    }\n\n    private fun setAnimationScale(scale: Int) {\n        ANIMATIONS.forEach { executeShellCommand(\"settings put global $it $scale\") }\n    }\n\n    private fun executeShellCommand(command: String) {\n        UiDevice.getInstance(getInstrumentation()).executeShellCommand(command)\n    }\n\n    companion object {\n        private val ANIMATIONS: List<String> = listOf(\n            \"transition_animation_scale\",\n            \"window_animation_scale\",\n            \"animator_duration_scale\"\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/RecentAppsRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport android.os.Build\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.uiautomator.By\nimport androidx.test.uiautomator.UiDevice\nimport androidx.test.uiautomator.UiSelector\nimport androidx.test.uiautomator.Until\nimport org.junit.Assert.assertTrue\nimport org.junit.rules.ExternalResource\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.DummyActivityLauncher\nimport org.odk.collect.shared.TimeInMs\n\nclass RecentAppsRule : ExternalResource() {\n\n    override fun before() {\n        assertTrue(\n            \"${this.javaClass.simpleName} does not support this API level!\",\n            SUPPORTED_SDKS.contains(Build.VERSION.SDK_INT)\n        )\n\n        if (Build.VERSION.SDK_INT == 30) {\n            removeRecentAppsTooltips()\n        }\n    }\n\n    fun leaveAndKillApp(appDependencyModule: AppDependencyModule? = null) {\n        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())\n\n        if (Build.VERSION.SDK_INT == 30) {\n            device.pressRecentApps()\n            device.wait(Until.hasObject(By.descContains(\"Collect\")), TimeInMs.ONE_SECOND)\n            device.findObject(UiSelector().descriptionContains(\"Collect\"))\n                .swipeUp(10).also {\n                    CollectHelpers.simulateProcessRestart(appDependencyModule) // the process is not restarted automatically (probably to keep the test running) so we have simulate it\n                }\n        } else if (Build.VERSION.SDK_INT == 34) {\n            device.pressHome() // Pressing recent apps does not actually \"leave\" the app on API 31+ (cause onPause etc). You need to go home or switch apps.\n            device.pressRecentApps()\n            device.wait(Until.hasObject(By.descContains(\"Screenshot\")), TimeInMs.ONE_SECOND)\n            while (!device.wait(Until.hasObject(By.text(\"Clear all\")), 0)) {\n                device.swipe(\n                    device.displayWidth / 2,\n                    device.displayHeight / 2,\n                    device.displayWidth,\n                    device.displayHeight / 2,\n                    5\n                )\n            }\n\n            device.findObject(UiSelector().text(\"Clear all\")).click()\n            CollectHelpers.simulateProcessRestart(appDependencyModule) // the process is not restarted automatically (probably to keep the test running) so we have simulate it\n        }\n    }\n\n    /**\n     * Makes sure [leaveAndKillApp] doesn't run into problems with tooltips by opening\n     * Recent Apps and dismissing before any test runs. Only needs to run once per test process.\n     */\n    private fun removeRecentAppsTooltips() {\n        // Open dummy activity so there is something in Recent Apps\n        DummyActivityLauncher.launch { device ->\n            // Open Recent Apps and dismiss tooltips if they're there\n            device.pressRecentApps()\n            val foundToolTip = device.wait(\n                Until.hasObject(By.textStartsWith(\"Select text and images to copy\")),\n                TimeInMs.ONE_SECOND\n            )\n            if (foundToolTip) {\n                device.pressBack() // the first time we open the list of recent apps, a tooltip might be displayed and we need to close it\n            }\n\n            // Close recent apps\n            device.pressBack()\n        }\n    }\n\n    companion object {\n        private val SUPPORTED_SDKS = listOf(30, 34)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/ResetRotationRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport org.junit.rules.ExternalResource\nimport org.odk.collect.android.support.DummyActivityLauncher\n\nclass ResetRotationRule : ExternalResource() {\n\n    override fun before() {\n        // Some devices are always portrait at the home screen so we need to launch something\n        DummyActivityLauncher.launch { device ->\n            device.setOrientationNatural()\n            device.pressBack()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/ResetStateRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.injection.config.AppDependencyComponent\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.views.DecoratedBarcodeView\nimport org.odk.collect.androidshared.ui.SnackbarUtils\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\nimport org.odk.collect.db.sqlite.DatabaseConnection\nimport org.odk.collect.material.BottomSheetBehavior\nimport java.io.IOException\n\nprivate class ResetStateStatement(\n    private val base: Statement,\n    private val appDependencyModule: AppDependencyModule? = null\n) : Statement() {\n\n    override fun evaluate() {\n        val application = ApplicationProvider.getApplicationContext<Application>()\n        val oldComponent = DaggerUtils.getComponent(application)\n\n        clearPrefs(oldComponent)\n        clearDisk()\n        setTestState()\n        CollectHelpers.simulateProcessRestart(appDependencyModule)\n        base.evaluate()\n    }\n\n    private fun setTestState() {\n        MultiClickGuard.test = true\n        DecoratedBarcodeView.test = true\n        ToastUtils.alertStore.enabled = true\n        SnackbarUtils.alertStore.enabled = true\n        BottomSheetBehavior.DRAGGING_ENABLED = false\n    }\n\n    private fun clearDisk() {\n        try {\n            val internalFilesDir = ApplicationProvider.getApplicationContext<Application>().filesDir\n            internalFilesDir.deleteRecursively()\n\n            val externalFilesDir =\n                ApplicationProvider.getApplicationContext<Application>().getExternalFilesDir(null)!!\n            externalFilesDir.deleteRecursively()\n        } catch (e: IOException) {\n            throw RuntimeException(e)\n        }\n\n        DatabaseConnection.closeAll()\n    }\n\n    private fun clearPrefs(component: AppDependencyComponent) {\n        val projectIds = component.projectsRepository().getAll().map { it.uuid }\n        component.settingsProvider().clearAll(projectIds)\n    }\n}\n\nclass ResetStateRule @JvmOverloads constructor(\n    private val appDependencyModule: AppDependencyModule? = null\n) : TestRule {\n\n    override fun apply(base: Statement, description: Description): Statement =\n        ResetStateStatement(base, appDependencyModule)\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/RetryOnDeviceErrorRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport androidx.test.espresso.NoMatchingRootException\nimport androidx.test.espresso.PerformException\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\nimport timber.log.Timber\n\nclass RetryOnDeviceErrorRule : TestRule {\n    override fun apply(base: Statement, description: Description): Statement {\n        return object : Statement() {\n            override fun evaluate() {\n                try {\n                    base.evaluate()\n                } catch (e: Exception) {\n                    if (e is PerformException) {\n                        Timber.w(\"RetryOnDeviceErrorRule: Retrying due to PerformException!\")\n                        Timber.e(e)\n                        base.evaluate()\n                    } else if (e is NoMatchingRootException) {\n                        Timber.w(\"RetryOnDeviceErrorRule: Retrying due to NoMatchingRootException!\")\n                        Timber.e(e)\n                        base.evaluate()\n                    } else if (e::class.simpleName == \"RootViewWithoutFocusException\") {\n                        Timber.w(\"RetryOnDeviceErrorRule: Retrying due to RootViewWithoutFocusException!\")\n                        Timber.e(e)\n                        base.evaluate()\n                    } else {\n                        throw e\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/RunnableRule.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\n\nclass RunnableRule(\n    private val runnable: Runnable\n) : TestRule {\n\n    override fun apply(base: Statement, description: Description): Statement =\n        object : Statement() {\n            override fun evaluate() {\n                runnable.run()\n                base.evaluate()\n            }\n        }\n}\n"
  },
  {
    "path": "collect_app/src/androidTest/java/org/odk/collect/android/support/rules/TestRuleChain.kt",
    "content": "package org.odk.collect.android.support.rules\n\nimport android.Manifest\nimport android.os.Build\nimport androidx.test.rule.GrantPermissionRule\nimport org.junit.rules.RuleChain\nimport org.odk.collect.android.support.CountingTaskExecutorIdlingResource\nimport org.odk.collect.android.support.TestDependencies\nimport org.odk.collect.android.support.async.AsyncWorkTrackerIdlingResource\n\nobject TestRuleChain {\n\n    @JvmStatic\n    @JvmOverloads\n    fun chain(testDependencies: TestDependencies = TestDependencies()): RuleChain {\n        val asyncWorkTrackerIdlingResource = AsyncWorkTrackerIdlingResource()\n        val countingTaskExecutorIdlingResource = CountingTaskExecutorIdlingResource()\n        val pageComposeRule = PageComposeRule()\n\n        return RuleChain\n            .outerRule(pageComposeRule.composeRule)\n            .around(RetryOnDeviceErrorRule())\n            .around(createGrantPermissionRule())\n            .around(ResetRotationRule())\n            .around(PrepDeviceForTestsRule())\n            .around(ResetStateRule(testDependencies))\n            .around(countingTaskExecutorIdlingResource)\n            .around(\n                IdlingResourceRule(\n                    listOf(asyncWorkTrackerIdlingResource, countingTaskExecutorIdlingResource)\n                )\n            )\n            .around(pageComposeRule)\n    }\n\n    private fun createGrantPermissionRule() =\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            GrantPermissionRule.grant(\n                Manifest.permission.ACCESS_FINE_LOCATION,\n                Manifest.permission.ACCESS_COARSE_LOCATION,\n                Manifest.permission.READ_EXTERNAL_STORAGE,\n                Manifest.permission.CAMERA,\n                Manifest.permission.RECORD_AUDIO,\n                Manifest.permission.POST_NOTIFICATIONS\n            )\n        } else {\n            GrantPermissionRule.grant(\n                Manifest.permission.ACCESS_FINE_LOCATION,\n                Manifest.permission.ACCESS_COARSE_LOCATION,\n                Manifest.permission.READ_EXTERNAL_STORAGE,\n                Manifest.permission.CAMERA,\n                Manifest.permission.RECORD_AUDIO\n            )\n        }\n}\n"
  },
  {
    "path": "collect_app/src/debug/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission android:name=\"android.permission.DISABLE_KEYGUARD\"/>\n\n    <!-- Allows changing locales -->\n    <uses-permission android:name=\"android.permission.CHANGE_CONFIGURATION\" tools:ignore=\"ProtectedPermissions\" />\n\n</manifest>\n"
  },
  {
    "path": "collect_app/src/debug/google-services.json",
    "content": "{\n  \"project_info\": {\n    \"project_number\": \"462257085307\",\n    \"firebase_url\": \"https://opendatakit-debug.firebaseio.com\",\n    \"project_id\": \"opendatakit-debug\",\n    \"storage_bucket\": \"opendatakit-debug.appspot.com\"\n  },\n  \"client\": [\n    {\n      \"client_info\": {\n        \"mobilesdk_app_id\": \"1:462257085307:android:64acb5e3c9e68ab5173bde\",\n        \"android_client_info\": {\n          \"package_name\": \"org.odk.collect.android\"\n        }\n      },\n      \"oauth_client\": [\n        {\n          \"client_id\": \"462257085307-o9ll0ogq8jm4ta3q9av2mf2rh4ume9vj.apps.googleusercontent.com\",\n          \"client_type\": 3\n        }\n      ],\n      \"api_key\": [\n        {\n          \"current_key\": \"AIzaSyAClt29XaVTTO1m2R9u3WW-RimapSqjALg\"\n        }\n      ],\n      \"services\": {\n        \"appinvite_service\": {\n          \"other_platform_oauth_client\": [\n            {\n              \"client_id\": \"462257085307-o9ll0ogq8jm4ta3q9av2mf2rh4ume9vj.apps.googleusercontent.com\",\n              \"client_type\": 3\n            }\n          ]\n        }\n      }\n    }\n  ],\n  \"configuration_version\": \"1\"\n}"
  },
  {
    "path": "collect_app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Copyright (C) 2009 University of Washington Licensed under the Apache\nLicense, Version 2.0 (the \"License\"); you may not use this file except in\ncompliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software distributed\nunder the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES\nOR CONDITIONS OF ANY KIND, either express or implied. See the License for\nthe specific language governing permissions and limitations under the License.\n-->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:ignore=\"GoogleAppIndexingWarning\">\n\n    <uses-feature\n        android:name=\"android.hardware.location\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.location.network\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.location.gps\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.telephony\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.wifi\"\n        android:required=\"false\" />\n    <uses-feature\n        android:glEsVersion=\"0x00030000\"\n        tools:node=\"remove\" />\n\n    <!-- Dangerous permissions -->\n    <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n    <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\"/>\n\n    <!-- Normal permissions -->\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n\n    <!--\n        Since we use Scoped Storage, we should not need this permission. However, we discovered that\n        this might be required in some rare cases, probably due to bugs in Android OS.\n        See: https://github.com/getodk/collect/issues/5145\n     -->\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n\n    <!-- Removed in API 23 -->\n    <uses-permission\n        android:name=\"android.permission.USE_CREDENTIALS\"\n        android:maxSdkVersion=\"22\" />\n    <uses-permission\n        android:name=\"android.permission.MANAGE_ACCOUNTS\"\n        android:maxSdkVersion=\"22\" />\n\n    <uses-feature\n        android:name=\"android.hardware.camera\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.camera.autofocus\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.screen.portrait\"\n        android:required=\"false\" />\n\n    <supports-screens\n        android:anyDensity=\"true\"\n        android:largeScreens=\"true\"\n        android:normalScreens=\"true\"\n        android:resizeable=\"true\"\n        android:smallScreens=\"true\"\n        android:xlargeScreens=\"true\" />\n\n    <application\n        android:name=\".application.Collect\"\n        android:allowBackup=\"true\"\n        android:icon=\"@drawable/notes\"\n        android:installLocation=\"auto\"\n        android:label=\"@string/collect_app_name\"\n        android:largeHeap=\"true\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:supportsRtl=\"true\"\n        android:usesCleartextTraffic=\"true\"\n        android:theme=\"@style/Theme.Collect\">\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.provider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/provider_paths\" />\n        </provider>\n\n        <activity\n            android:name=\".activities.FirstLaunchActivity\">\n        </activity>\n\n        <activity\n            android:name=\".mainmenu.MainMenuActivity\"\n            android:theme=\"@style/Theme.Collect.SplashScreen\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n\n        <activity android:name=\"org.odk.collect.android.activities.CrashHandlerActivity\" />\n\n        <activity\n            android:name=\".activities.ScannerWithFlashlightActivity\"\n            android:configChanges=\"orientation|screenSize\"/>\n        <activity\n            android:name=\".activities.FormFillingActivity\"\n            android:theme=\"@style/Theme.Collect.FormEntry\"\n            android:windowSoftInputMode=\"adjustResize\" />\n        <activity\n            android:name=\"org.odk.collect.draw.DrawActivity\"\n            android:theme=\"@style/Theme.Collect\"\n            tools:replace=\"android:theme\" />\n        <activity android:name=\".activities.InstanceChooserList\" />\n        <activity\n            android:name=\".formlists.blankformlist.BlankFormListActivity\"\n            android:launchMode=\"singleTop\" />\n        <activity android:name=\".activities.FormDownloadListActivity\" />\n        <activity android:name=\".activities.DeleteFormsActivity\" />\n        <activity\n            android:name=\".instancemanagement.send.InstanceUploaderListActivity\"\n            android:configChanges=\"orientation|screenSize\" />\n        <activity android:name=\".instancemanagement.send.InstanceUploaderActivity\" />\n        <activity android:name=\".activities.AboutActivity\" />\n        <activity android:name=\".configure.qr.QRCodeTabsActivity\" />\n        <activity\n            android:name=\".preferences.screens.ProjectPreferencesActivity\"\n            android:theme=\"@style/Theme.Collect.Settings\" />\n        <activity\n            android:name=\"org.odk.collect.geo.geopoint.GeoPointActivity\"\n            android:theme=\"@style/Theme.Collect\"\n            tools:replace=\"android:theme\" />\n        <activity\n            android:name=\"org.odk.collect.geo.geopoint.GeoPointMapActivity\"\n            android:theme=\"@style/Theme.Collect\"\n            tools:replace=\"android:theme\" />\n        <activity\n            android:name=\"org.odk.collect.geo.geopoly.GeoPolyActivity\"\n            android:theme=\"@style/Theme.Collect\"\n            tools:replace=\"android:theme\" />\n        <activity\n            android:name=\"org.odk.collect.errors.ErrorActivity\"\n            android:theme=\"@style/Theme.Collect\"\n            tools:replace=\"android:theme\" />\n\n        <activity\n            android:name=\"org.odk.collect.entities.browser.EntityBrowserActivity\"\n            android:theme=\"@style/Theme.Collect\"\n            tools:replace=\"android:theme\" />\n\n        <activity android:name=\".activities.FormMapActivity\" />\n        <activity android:name=\".activities.BearingActivity\" />\n        <activity\n            android:name=\"org.odk.collect.selfiecamera.CaptureSelfieActivity\"\n            android:theme=\"@style/Theme.Collect\"\n            tools:replace=\"android:theme\">\n\n        </activity>\n\n        <!-- Configure this key by setting GOOGLE_MAPS_API_KEY in secrets.properties. -->\n        <meta-data\n            android:name=\"com.google.android.geo.API_KEY\"\n            android:value=\"@string/GOOGLE_MAPS_API_KEY\" />\n        <meta-data\n            android:name=\"com.google.android.gms.version\"\n            android:value=\"@integer/google_play_services_version\"\n            tools:replace=\"android:value\" /> <!-- integer/google_play_services_version -->\n\n        <meta-data\n            android:name=\"firebase_analytics_collection_enabled\"\n            android:value=\"false\" />\n\n        <meta-data\n            android:name=\"firebase_crashlytics_collection_enabled\"\n            android:value=\"false\" />\n\n        <meta-data\n            android:name=\"google_analytics_adid_collection_enabled\"\n            android:value=\"false\" />\n\n        <meta-data\n            android:name=\"google_analytics_ssaid_collection_enabled\"\n            android:value=\"false\" />\n\n        <uses-library\n            android:name=\"com.google.android.maps\"\n            android:required=\"false\" />\n\n        <!-- https://issuetracker.google.com/issues/476788722 -->\n        <uses-library\n            android:name=\"org.apache.http.legacy\"\n            android:required=\"false\"/>\n\n        <meta-data\n            android:name=\"com.google.mlkit.vision.DEPENDENCIES\"\n            android:value=\"barcode\" />\n\n        <!--\n             ******************External API, DO NOT CHANGE BEFORE DISCUSSING***********************\n             * The providers and activities below are available for external applications to      *\n             * integrate with and are actively in use. If one of these needs to change for some   *\n             * reason, it needs to be placed on a deprecation path so users can adapt before the  *\n             * change. If an activity needs to be renamed internally, use an activity-alias. If   *\n             * the external API needs to be expanded, introduce carefully-named actions.          *\n             **************************************************************************************\n         -->\n\n        <provider\n            android:name=\".external.FormsProvider\"\n            android:authorities=\"org.odk.collect.android.provider.odk.forms\"\n            android:exported=\"true\" />\n        <provider\n            android:name=\".external.InstanceProvider\"\n            android:authorities=\"org.odk.collect.android.provider.odk.instances\"\n            android:exported=\"true\" />\n\n        <activity-alias\n            android:name=\".activities.FormEntryActivity\"\n            android:targetActivity=\".external.FormUriActivity\"\n            tools:ignore=\"AppLinkUrlError\"\n            android:exported=\"true\">\n        </activity-alias>\n\n        <activity-alias\n            android:name=\".activities.InstanceChooserList\"\n            android:targetActivity=\".activities.InstanceChooserList\"\n            android:exported=\"true\"\n            tools:ignore=\"AppLinkUrlError\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <action android:name=\"android.intent.action.EDIT\" />\n                <action android:name=\"android.intent.action.PICK\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n\n                <data android:mimeType=\"vnd.android.cursor.dir/vnd.odk.instance\" />\n            </intent-filter>\n        </activity-alias>\n\n        <activity-alias\n            android:name=\".activities.FormChooserList\"\n            android:targetActivity=\".formlists.blankformlist.BlankFormListActivity\"\n            android:exported=\"true\"\n            tools:ignore=\"AppLinkUrlError\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <action android:name=\"android.intent.action.EDIT\" />\n                <action android:name=\"android.intent.action.PICK\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n\n                <data android:mimeType=\"vnd.android.cursor.dir/vnd.odk.form\" />\n            </intent-filter>\n        </activity-alias>\n\n        <activity-alias\n            android:name=\".activities.FormDownloadList\"\n            android:targetActivity=\".activities.FormDownloadListActivity\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"org.odk.collect.android.FORM_DOWNLOAD\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <data android:mimeType=\"vnd.android.cursor.dir/vnd.odk.form\" />\n            </intent-filter>\n        </activity-alias>\n\n        <activity-alias\n            android:name=\".activities.InstanceUploaderList\"\n            android:targetActivity=\".instancemanagement.send.InstanceUploaderListActivity\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <action android:name=\"android.intent.action.EDIT\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n            </intent-filter>\n        </activity-alias>\n\n        <activity-alias\n            android:name=\".activities.InstanceUploaderActivity\"\n            android:targetActivity=\".instancemanagement.send.InstanceUploaderActivity\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"org.odk.collect.android.INSTANCE_UPLOAD\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <data android:mimeType=\"vnd.android.cursor.dir/vnd.odk.instance\" />\n            </intent-filter>\n        </activity-alias>\n\n        <!-- Enable Shortcuts for Command Actions -->\n        <activity\n            android:name=\".external.AndroidShortcutsActivity\"\n            android:label=\"ODK Form\"\n            android:theme=\"@style/Theme.MaterialComponents.Light.NoActionBar\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.CREATE_SHORTCUT\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n            </intent-filter>\n        </activity>\n\n        <!-- Supports VIEW and EDIT actions for forms and instances (documented at\n        https://docs.getodk.org/launch-collect-from-app/#using-a-uri-to-edit-a-form-or-instance).\n        Do not use the activity directly as its name/package might change. -->\n        <activity\n            android:name=\".external.FormUriActivity\"\n            android:exported=\"true\"\n            tools:ignore=\"AppLinkUrlError\">\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <action android:name=\"android.intent.action.EDIT\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <data android:mimeType=\"vnd.android.cursor.item/vnd.odk.form\" />\n                <data android:mimeType=\"vnd.android.cursor.item/vnd.odk.instance\" />\n            </intent-filter>\n        </activity>\n        <activity android:name=\"org.odk.collect.android.formhierarchy.FormHierarchyFragmentHostActivity\" />\n    </application>\n\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.MAIN\" />\n        </intent>\n    </queries>\n</manifest>\n"
  },
  {
    "path": "collect_app/src/main/assets/svg_map_helper.js",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n* JavaScript class which contains methods used for managing svg maps\n*/\nvar selectedAreas = [];\nvar originalColors = {};\nvar lastSelectedAreaId;\nvar isSingleSelect;\n\nfunction selectArea(areaId) {\n    imageMapInterface.selectArea(areaId);\n}\n\nfunction unselectArea(areaId) {\n    imageMapInterface.unselectArea(areaId);\n}\n\nfunction notifyChanges() {\n    imageMapInterface.notifyChanges();\n}\n\nfunction clearAreas() {\n    selectedAreas.forEach(function(areaId) {\n        if (selectedAreas.indexOf(areaId) !== -1) {\n            document.getElementById(areaId).setAttribute('style', 'fill: ' + originalColors[areaId]);\n        }\n    });\n    selectedAreas = [];\n    notifyChanges();\n}\n\nfunction addSelectedArea(selectedAreaId) {\n    selectedAreas.push(selectedAreaId);\n    document.getElementById(selectedAreaId).setAttribute('style', 'fill: #E65100');\n    if (Boolean(isSingleSelect)) {\n        lastSelectedAreaId = selectedAreaId;\n    }\n}\n\nfunction addArea(areaId) {\n    originalColors[areaId] = document.getElementById(areaId).style.color;\n}\n\nfunction setSelectMode(isSingleSelect) {\n    this.isSingleSelect = isSingleSelect;\n}\n\nfunction clickOnArea(areaId) {\n    if (Boolean(isSingleSelect)) {\n        // single select mode\n        if (Boolean(isSingleSelect) && !!lastSelectedAreaId) {\n            document.getElementById(lastSelectedAreaId).setAttribute('style', 'fill: ' + originalColors[lastSelectedAreaId]);\n            selectedAreas.splice(selectedAreas.indexOf(lastSelectedAreaId), 1);\n            unselectArea(lastSelectedAreaId);\n        }\n        document.getElementById(areaId).setAttribute('style', 'fill: #E65100');\n        selectedAreas.push(areaId);\n        selectArea(areaId);\n        lastSelectedAreaId = areaId;\n    } else {\n        // multiple select mode\n        if (selectedAreas.indexOf(areaId) !== -1) {\n            document.getElementById(areaId).setAttribute('style', 'fill: ' + originalColors[areaId]);\n            selectedAreas.splice(selectedAreas.indexOf(areaId), 1);\n            unselectArea(areaId);\n        } else {\n            document.getElementById(areaId).setAttribute('style', 'fill: #E65100');\n            selectedAreas.push(areaId);\n            selectArea(areaId);\n        }\n    }\n    notifyChanges();\n}"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/AboutActivity.kt",
    "content": "/*\n * Copyright 2018 Shobhit Agarwal\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.activities\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport androidx.appcompat.widget.Toolbar\nimport androidx.recyclerview.widget.DefaultItemAnimator\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.gms.oss.licenses.OssLicensesMenuActivity\nimport org.odk.collect.android.R\nimport org.odk.collect.android.adapters.AboutItemClickListener\nimport org.odk.collect.android.adapters.AboutListAdapter\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard.allowClick\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport org.odk.collect.webpage.WebPageService\nimport javax.inject.Inject\n\nclass AboutActivity : LocalizedActivity(), AboutItemClickListener {\n    private lateinit var websiteUri: Uri\n    private lateinit var forumUri: Uri\n\n    @Inject\n    lateinit var intentLauncher: IntentLauncher\n\n    @Inject\n    lateinit var webPageService: WebPageService\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setView(R.layout.about_layout, false)\n        DaggerUtils.getComponent(this).inject(this)\n        initToolbar()\n\n        findViewById<RecyclerView>(R.id.recyclerView).apply {\n            layoutManager = LinearLayoutManager(this@AboutActivity)\n            adapter = AboutListAdapter(ITEMS, this@AboutActivity)\n            itemAnimator = DefaultItemAnimator()\n        }\n\n        websiteUri = Uri.parse(getString(org.odk.collect.strings.R.string.app_url))\n        forumUri = Uri.parse(getString(org.odk.collect.strings.R.string.forum_url))\n    }\n\n    private fun initToolbar() {\n        val toolbar = findViewById<Toolbar>(org.odk.collect.androidshared.R.id.toolbar)\n        title = getString(org.odk.collect.strings.R.string.about_preferences)\n        setSupportActionBar(toolbar)\n    }\n\n    override fun onClick(position: Int) {\n        if (allowClick(javaClass.name)) {\n            when (position) {\n                0 -> webPageService.openWebPage(this, websiteUri)\n                1 -> webPageService.openWebPage(this, forumUri)\n                2 -> shareApp()\n                3 -> addReview()\n                4 -> startActivity(Intent(this, OssLicensesMenuActivity::class.java))\n            }\n        }\n    }\n\n    private fun shareApp() {\n        val shareIntent = Intent(Intent.ACTION_SEND).apply {\n            type = \"text/plain\"\n            putExtra(\n                Intent.EXTRA_TEXT,\n                getString(org.odk.collect.strings.R.string.tell_your_friends_msg) + \" \" + GOOGLE_PLAY_URL + packageName\n            )\n        }\n        startActivity(\n            Intent.createChooser(\n                shareIntent,\n                getString(org.odk.collect.strings.R.string.tell_your_friends)\n            )\n        )\n    }\n\n    private fun addReview() {\n        val intent = Intent(\n            Intent.ACTION_VIEW,\n            Uri.parse(\"market://details?id=$packageName\")\n        )\n        intentLauncher.launch(this, intent) {\n            // Show a list of all available browsers if user doesn't have a default browser\n            startActivity(\n                Intent(\n                    Intent.ACTION_VIEW,\n                    Uri.parse(GOOGLE_PLAY_URL + packageName)\n                )\n            )\n        }\n    }\n\n    companion object {\n        private const val GOOGLE_PLAY_URL = \"https://play.google.com/store/apps/details?id=\"\n        private val ITEMS = arrayOf(\n            intArrayOf(\n                R.drawable.ic_outline_website_24,\n                org.odk.collect.strings.R.string.odk_website,\n                org.odk.collect.strings.R.string.odk_website_summary\n            ),\n            intArrayOf(\n                R.drawable.ic_outline_forum_24,\n                org.odk.collect.strings.R.string.odk_forum,\n                org.odk.collect.strings.R.string.odk_forum_summary\n            ),\n            intArrayOf(\n                R.drawable.ic_outline_share_24,\n                org.odk.collect.strings.R.string.tell_your_friends,\n                org.odk.collect.strings.R.string.tell_your_friends_msg\n            ),\n            intArrayOf(\n                R.drawable.ic_outline_rate_review_24,\n                org.odk.collect.strings.R.string.leave_a_review,\n                org.odk.collect.strings.R.string.leave_a_review_msg\n            ),\n            intArrayOf(\n                R.drawable.ic_outline_stars_24,\n                org.odk.collect.strings.R.string.all_open_source_licenses,\n                org.odk.collect.strings.R.string.all_open_source_licenses_msg\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/ActivityUtils.java",
    "content": "package org.odk.collect.android.activities;\n\nimport android.app.Activity;\nimport android.content.Intent;\n\npublic final class ActivityUtils {\n\n    private ActivityUtils() {\n\n    }\n\n    public static <A extends Activity> void startActivityAndCloseAllOthers(Activity activity, Class<A> activityClass) {\n        activity.startActivity(new Intent(activity, activityClass));\n        activity.overridePendingTransition(0, 0);\n        activity.finishAffinity();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/AppListActivity.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.activities;\n\nimport android.database.Cursor;\nimport android.os.Bundle;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.widget.AdapterView;\nimport android.widget.Button;\nimport android.widget.CursorAdapter;\nimport android.widget.ListView;\n\nimport androidx.annotation.LayoutRes;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.SearchView;\nimport androidx.core.content.ContextCompat;\nimport androidx.core.view.MenuItemCompat;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns;\nimport org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog;\nimport org.odk.collect.android.formlists.sorting.FormListSortingOption;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.androidshared.ui.ObviousProgressBar;\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.strings.localization.LocalizedActivity;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashSet;\nimport java.util.List;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\npublic abstract class AppListActivity extends LocalizedActivity {\n\n    public static final int LOADER_ID = 0x01;\n    private static final String SELECTED_INSTANCES = \"selectedInstances\";\n    private static final String IS_SEARCH_BOX_SHOWN = \"isSearchBoxShown\";\n    private static final String SEARCH_TEXT = \"searchText\";\n\n    protected CursorAdapter listAdapter;\n    protected LinkedHashSet<Long> selectedInstances = new LinkedHashSet<>();\n    protected List<FormListSortingOption> sortingOptions;\n    protected Integer selectedSortingOrder;\n    protected ListView listView;\n    protected ObviousProgressBar progressBar;\n\n    private String filterText;\n    private String savedFilterText;\n    private boolean isSearchBoxShown;\n\n    private SearchView searchView;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        DaggerUtils.getComponent(this).inject(this);\n    }\n\n    // toggles to all checked or all unchecked\n    // returns:\n    // true if result is all checked\n    // false if result is all unchecked\n    //\n    // Toggle behavior is as follows:\n    // if ANY items are unchecked, check them all\n    // if ALL items are checked, uncheck them all\n    public static boolean toggleChecked(ListView lv) {\n        // shortcut null case\n        if (lv == null) {\n            return false;\n        }\n\n        boolean newCheckState = lv.getCount() > lv.getCheckedItemCount();\n        setAllToCheckedState(lv, newCheckState);\n        return newCheckState;\n    }\n\n    public static void setAllToCheckedState(ListView lv, boolean check) {\n        // no-op if ListView null\n        if (lv == null) {\n            return;\n        }\n\n        for (int x = 0; x < lv.getCount(); x++) {\n            lv.setItemChecked(x, check);\n        }\n    }\n\n    // Function to toggle button label\n    public static void toggleButtonLabel(Button toggleButton, ListView lv) {\n        if (lv.getCheckedItemCount() != lv.getCount()) {\n            toggleButton.setText(org.odk.collect.strings.R.string.select_all);\n        } else {\n            toggleButton.setText(org.odk.collect.strings.R.string.clear_all);\n        }\n    }\n\n    @Override\n    public void setContentView(View view) {\n        super.setContentView(view);\n        init();\n    }\n\n    @Override\n    public void setContentView(@LayoutRes int layoutResID) {\n        super.setContentView(layoutResID);\n        init();\n    }\n\n    private void init() {\n        listView = findViewById(R.id.scrollable_container);\n\n        if (this instanceof AdapterView.OnItemClickListener) {\n            listView.setOnItemClickListener((AdapterView.OnItemClickListener) this);\n        }\n\n        listView.setEmptyView(findViewById(android.R.id.empty));\n        progressBar = findViewById(org.odk.collect.androidshared.R.id.progressBar);\n\n        // Use the nicer-looking drawable with Material Design insets.\n        listView.setDivider(ContextCompat.getDrawable(this, org.odk.collect.androidshared.R.drawable.list_item_divider));\n        listView.setDividerHeight(1);\n\n        setSupportActionBar(findViewById(org.odk.collect.androidshared.R.id.toolbar));\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        restoreSelectedSortingOrder();\n    }\n\n    @Override\n    protected void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putSerializable(SELECTED_INSTANCES, selectedInstances);\n\n        if (searchView != null) {\n            outState.putBoolean(IS_SEARCH_BOX_SHOWN, !searchView.isIconified());\n            outState.putString(SEARCH_TEXT, String.valueOf(searchView.getQuery()));\n        } else {\n            Timber.e(new Error(\"Unexpected null search view (issue #1412)\"));\n        }\n    }\n\n    @Override\n    protected void onRestoreInstanceState(Bundle state) {\n        super.onRestoreInstanceState(state);\n        selectedInstances = (LinkedHashSet<Long>) state.getSerializable(SELECTED_INSTANCES);\n        isSearchBoxShown = state.getBoolean(IS_SEARCH_BOX_SHOWN);\n        savedFilterText = state.getString(SEARCH_TEXT);\n    }\n\n    @Override\n    public boolean onCreateOptionsMenu(final Menu menu) {\n        getMenuInflater().inflate(R.menu.blank_form_list_menu, menu);\n        final MenuItem sortItem = menu.findItem(R.id.menu_sort);\n        final MenuItem searchItem = menu.findItem(R.id.menu_filter);\n        searchView = (SearchView) MenuItemCompat.getActionView(searchItem);\n        searchView.setQueryHint(getResources().getString(org.odk.collect.strings.R.string.search));\n        searchView.setMaxWidth(Integer.MAX_VALUE);\n        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {\n            @Override\n            public boolean onQueryTextSubmit(String query) {\n                filterText = query;\n                updateAdapter();\n                searchView.clearFocus();\n                return false;\n            }\n\n            @Override\n            public boolean onQueryTextChange(String newText) {\n                filterText = newText;\n                updateAdapter();\n                return false;\n            }\n        });\n\n        MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {\n            @Override\n            public boolean onMenuItemActionExpand(MenuItem item) {\n                sortItem.setVisible(false);\n                return true;\n            }\n\n            @Override\n            public boolean onMenuItemActionCollapse(MenuItem item) {\n                sortItem.setVisible(true);\n                return true;\n            }\n        });\n\n        if (isSearchBoxShown) {\n            searchItem.expandActionView();\n            searchView.setQuery(savedFilterText, false);\n        }\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        if (!MultiClickGuard.allowClick(getClass().getName())) {\n            return true;\n        }\n\n        if (item.getItemId() == R.id.menu_sort) {\n            new FormListSortingBottomSheetDialog(\n                    this,\n                    sortingOptions,\n                    selectedSortingOrder,\n                    selectedOption -> {\n                        saveSelectedSortingOrder(selectedOption);\n                        updateAdapter();\n                    }\n            ).show();\n            return true;\n        }\n\n        return super.onOptionsItemSelected(item);\n    }\n\n    protected void checkPreviouslyCheckedItems() {\n        listView.clearChoices();\n        List<Integer> selectedPositions = new ArrayList<>();\n        int listViewPosition = 0;\n        Cursor cursor = listAdapter.getCursor();\n        if (cursor != null && cursor.moveToFirst()) {\n            do {\n                long instanceId = cursor.getLong(cursor.getColumnIndex(DatabaseInstanceColumns._ID));\n                if (selectedInstances.contains(instanceId)) {\n                    selectedPositions.add(listViewPosition);\n                }\n                listViewPosition++;\n            } while (cursor.moveToNext());\n        }\n\n        for (int position : selectedPositions) {\n            listView.setItemChecked(position, true);\n        }\n    }\n\n    protected abstract void updateAdapter();\n\n    protected abstract String getSortingOrderKey();\n\n    protected boolean areCheckedItems() {\n        return getCheckedCount() > 0;\n    }\n\n    protected int getCheckedCount() {\n        return listView.getCheckedItemCount();\n    }\n\n    private void saveSelectedSortingOrder(int selectedStringOrder) {\n        selectedSortingOrder = selectedStringOrder;\n        settingsProvider.getUnprotectedSettings().save(getSortingOrderKey(), selectedStringOrder);\n    }\n\n    protected void restoreSelectedSortingOrder() {\n        selectedSortingOrder = settingsProvider.getUnprotectedSettings().getInt(getSortingOrderKey());\n    }\n\n    protected int getSelectedSortingOrder() {\n        if (selectedSortingOrder == null) {\n            restoreSelectedSortingOrder();\n        }\n        return selectedSortingOrder;\n    }\n\n    protected CharSequence getFilterText() {\n        return filterText != null ? filterText : \"\";\n    }\n\n    protected void clearSearchView() {\n        searchView.setQuery(\"\", false);\n    }\n\n    protected void hideProgressBarAndAllow() {\n        hideProgressBar();\n    }\n\n    private void hideProgressBar() {\n        progressBar.hide();\n    }\n\n    protected void showProgressBar() {\n        progressBar.show();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/BearingActivity.java",
    "content": "/*\n * Copyright (C) 2013 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.activities;\n\nimport android.app.ProgressDialog;\nimport android.content.DialogInterface;\nimport android.hardware.Sensor;\nimport android.hardware.SensorEvent;\nimport android.hardware.SensorEventListener;\nimport android.hardware.SensorManager;\nimport android.os.Bundle;\n\nimport org.odk.collect.android.views.DayNightProgressDialog;\nimport org.odk.collect.externalapp.ExternalAppUtils;\nimport org.odk.collect.strings.localization.LocalizedActivity;\n\nimport java.util.Locale;\n\npublic class BearingActivity extends LocalizedActivity implements SensorEventListener {\n    private ProgressDialog bearingDialog;\n\n    private SensorManager sensorManager;\n    private Sensor accelerometer;\n    private Sensor magnetometer;\n\n    private static float[] mAccelerometer;\n    private static float[] mGeomagnetic;\n\n    private String bearingDecimal;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setTitle(getString(org.odk.collect.strings.R.string.get_bearing));\n\n        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);\n        accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);\n        magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);\n\n        setupBearingDialog();\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n\n        sensorManager.unregisterListener(this, accelerometer);\n        sensorManager.unregisterListener(this, magnetometer);\n\n        if (bearingDialog != null && bearingDialog.isShowing()) {\n            bearingDialog.dismiss();\n        }\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);\n        sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_GAME);\n        bearingDialog.show();\n    }\n\n    /**\n     * Sets up the look and actions for the progress dialog while the compass is\n     * searching.\n     */\n    private void setupBearingDialog() {\n        // dialog displayed while fetching bearing\n        bearingDialog = new DayNightProgressDialog(this);\n        DialogInterface.OnClickListener geopointButtonListener =\n                new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int which) {\n                        switch (which) {\n                            case DialogInterface.BUTTON_POSITIVE:\n                                returnBearing();\n                                break;\n                            case DialogInterface.BUTTON_NEGATIVE:\n                                bearingDecimal = null;\n                                finish();\n                                break;\n                        }\n                    }\n                };\n\n        // back button doesn't cancel\n        bearingDialog.setCancelable(false);\n        bearingDialog.setIndeterminate(true);\n        bearingDialog.setTitle(getString(org.odk.collect.strings.R.string.getting_bearing));\n        bearingDialog.setMessage(getString(org.odk.collect.strings.R.string.please_wait_long));\n        bearingDialog.setButton(DialogInterface.BUTTON_POSITIVE,\n                getString(org.odk.collect.strings.R.string.accept_bearing),\n                geopointButtonListener);\n        bearingDialog.setButton(DialogInterface.BUTTON_NEGATIVE,\n                getString(org.odk.collect.strings.R.string.cancel_location),\n                geopointButtonListener);\n    }\n\n    private void returnBearing() {\n        if (bearingDecimal != null) {\n            ExternalAppUtils.returnSingleValue(this, bearingDecimal);\n        } else {\n            finish();\n        }\n    }\n\n    @Override\n    public void onAccuracyChanged(Sensor arg0, int arg1) {\n        // TODO Auto-generated method stub\n\n    }\n\n    @Override\n    public void onSensorChanged(SensorEvent event) {\n        // onSensorChanged gets called for each sensor so we have to remember\n        // the values\n        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {\n            mAccelerometer = event.values;\n        }\n\n        if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {\n            mGeomagnetic = event.values;\n        }\n\n        if (mAccelerometer != null && mGeomagnetic != null) {\n            float[] rot = new float[9];\n            float[] inclination = new float[9];\n            boolean success = SensorManager.getRotationMatrix(rot, inclination, mAccelerometer,\n                    mGeomagnetic);\n\n            if (success) {\n                float[] orientation = new float[3];\n                SensorManager.getOrientation(rot, orientation);\n                // at this point, orientation contains the azimuth(direction),\n                // pitch and roll values.\n                double azimuth = 180 * orientation[0] / Math.PI;\n                // double pitch = 180 * orientation[1] / Math.PI;\n                // double roll = 180 * orientation[2] / Math.PI;\n                double degrees = normalizeDegrees(azimuth);\n                bearingDecimal = formatDegrees(degrees);\n\n                String dir = \"N\";\n                if ((degrees > 0 && degrees <= 22.5) || degrees > 337.5) {\n                    dir = \"N\";\n                } else if (degrees > 22.5 && degrees <= 67.5) {\n                    dir = \"NE\";\n                } else if (degrees > 67.5 && degrees <= 112.5) {\n                    dir = \"E\";\n                } else if (degrees > 112.5 && degrees <= 157.5) {\n                    dir = \"SE\";\n                } else if (degrees > 157.5 && degrees <= 222.5) {\n                    dir = \"S\";\n                } else if (degrees > 222.5 && degrees <= 247.5) {\n                    dir = \"SW\";\n                } else if (degrees > 247.5 && degrees <= 292.5) {\n                    dir = \"W\";\n                } else if (degrees > 292.5 && degrees <= 337.5) {\n                    dir = \"NW\";\n                }\n                bearingDialog.setMessage(getString(org.odk.collect.strings.R.string.direction, dir)\n                        + \"\\n\" + getString(org.odk.collect.strings.R.string.bearing, degrees));\n\n            }\n        }\n    }\n\n    public static String formatDegrees(double degrees) {\n        return String.format(Locale.US, \"%.3f\", degrees);\n    }\n\n    public static double normalizeDegrees(double value) {\n        if (value >= 0.0f && value <= 180.0f) {\n            return value;\n        } else {\n            return 180 + (180 + value);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/CrashHandlerActivity.kt",
    "content": "package org.odk.collect.android.activities\n\nimport android.os.Bundle\nimport androidx.activity.OnBackPressedCallback\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.crashhandler.CrashHandler\nimport org.odk.collect.strings.localization.LocalizedActivity\n\nclass CrashHandlerActivity : LocalizedActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        val crashHandler = CrashHandler.getInstance(this)!!\n        val crashView = crashHandler.getCrashView(this) {\n            ActivityUtils.startActivityAndCloseAllOthers(this, MainMenuActivity::class.java)\n        }\n\n        if (crashView != null) {\n            setView(crashView, false)\n        } else {\n            finish()\n        }\n\n        onBackPressedDispatcher.addCallback(\n            object : OnBackPressedCallback(true) {\n                override fun handleOnBackPressed() {\n                    crashView?.dismiss()\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/DeleteFormsActivity.kt",
    "content": "/*\n * Copyright (C) 2017 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.activities\n\nimport android.app.Application\nimport android.os.Bundle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewmodel.CreationExtras\nimport com.google.android.material.tabs.TabLayout\nimport com.google.android.material.tabs.TabLayoutMediator\nimport org.odk.collect.android.databinding.TabsLayoutBinding\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListViewModel\nimport org.odk.collect.android.formlists.blankformlist.DeleteBlankFormFragment\nimport org.odk.collect.android.formlists.savedformlist.DeleteSavedFormFragment\nimport org.odk.collect.android.formlists.savedformlist.SavedFormListViewModel\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.injection.config.ProjectDependencyModuleFactory\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.androidshared.ui.ListFragmentStateAdapter\nimport org.odk.collect.androidshared.utils.AppBarUtils.setupAppBarLayout\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.shared.settings.Settings\nimport org.odk.collect.strings.R\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport javax.inject.Inject\n\nclass DeleteFormsActivity : LocalizedActivity() {\n    @Inject\n    lateinit var projectDependencyModuleFactory: ProjectDependencyModuleFactory\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    @Inject\n    lateinit var formsDataService: FormsDataService\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    @Inject\n    lateinit var instanceDataService: InstancesDataService\n\n    @Inject\n    lateinit var uniqueIdGenerator: UniqueIdGenerator\n\n    private lateinit var binding: TabsLayoutBinding\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        DaggerUtils.getComponent(this).inject(this)\n\n        val projectId = projectsDataService.requireCurrentProject().uuid\n        val projectDependencyModule = projectDependencyModuleFactory.create(projectId)\n\n        val viewModelFactory = ViewModelFactory(\n            projectDependencyModule.instancesRepository,\n            this.application,\n            formsDataService,\n            scheduler,\n            projectDependencyModule.generalSettings,\n            projectId,\n            instanceDataService,\n            uniqueIdGenerator\n        )\n\n        val viewModelProvider = ViewModelProvider(this, viewModelFactory)\n        val blankFormsListViewModel = viewModelProvider[BlankFormListViewModel::class.java]\n\n        supportFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(DeleteBlankFormFragment::class) {\n                DeleteBlankFormFragment(viewModelFactory, this)\n            }\n            .forClass(DeleteSavedFormFragment::class.java) {\n                DeleteSavedFormFragment(viewModelFactory, this)\n            }\n            .build()\n\n        super.onCreate(savedInstanceState)\n        binding = TabsLayoutBinding.inflate(layoutInflater)\n        setView(binding.root, false)\n        setupAppBarLayout(this, getString(R.string.manage_files))\n        setUpViewPager(blankFormsListViewModel)\n    }\n\n    private fun setUpViewPager(viewModel: BlankFormListViewModel) {\n        val fragments = if (viewModel.isMatchExactlyEnabled()) {\n            listOf(DeleteSavedFormFragment::class.java)\n        } else {\n            listOf(\n                DeleteSavedFormFragment::class.java,\n                DeleteBlankFormFragment::class.java\n            )\n        }\n\n        val viewPager = binding.viewPager.also {\n            it.adapter =\n                ListFragmentStateAdapter(\n                    this,\n                    fragments\n                )\n        }\n\n        TabLayoutMediator(binding.tabLayout, viewPager) { tab: TabLayout.Tab, position: Int ->\n            tab.text = if (position == 0) {\n                getString(org.odk.collect.strings.R.string.data)\n            } else {\n                getString(org.odk.collect.strings.R.string.forms)\n            }\n        }.attach()\n    }\n\n    private class ViewModelFactory(\n        private val instancesRepository: InstancesRepository,\n        private val application: Application,\n        private val formsDataService: FormsDataService,\n        private val scheduler: Scheduler,\n        private val generalSettings: Settings,\n        private val projectId: String,\n        private val instancesDataService: InstancesDataService,\n        private val uniqueIdGenerator: UniqueIdGenerator\n    ) :\n        ViewModelProvider.Factory {\n\n        override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {\n            return when (modelClass) {\n                BlankFormListViewModel::class.java -> BlankFormListViewModel(\n                    instancesRepository,\n                    application,\n                    formsDataService,\n                    scheduler,\n                    generalSettings,\n                    projectId,\n                    showAllVersions = true,\n                    uniqueIdGenerator\n                )\n\n                SavedFormListViewModel::class.java -> SavedFormListViewModel(\n                    scheduler,\n                    generalSettings,\n                    instancesDataService,\n                    projectId\n                )\n\n                else -> throw IllegalArgumentException()\n            } as T\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/FirstLaunchActivity.kt",
    "content": "package org.odk.collect.android.activities\n\nimport android.os.Bundle\nimport android.text.SpannableStringBuilder\nimport androidx.activity.viewModels\nimport androidx.core.text.color\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.asLiveData\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.application.CollectComposeThemeProvider\nimport org.odk.collect.android.databinding.FirstLaunchLayoutBinding\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.android.projects.ManualProjectCreatorDialog\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.projects.QrCodeProjectCreatorDialog\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.androidshared.system.ContextExt.getThemeAttributeValue\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.material.MaterialProgressDialogFragment\nimport org.odk.collect.mobiledevicemanagement.MDMConfigObserver\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.R\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport javax.inject.Inject\n\nclass FirstLaunchActivity : LocalizedActivity(), CollectComposeThemeProvider {\n\n    @Inject\n    lateinit var projectsRepository: ProjectsRepository\n\n    @Inject\n    lateinit var versionInformation: VersionInformation\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    @Inject\n    lateinit var mdmConfigObserver: MDMConfigObserver\n\n    private val viewModel: FirstLaunchViewModel by viewModels {\n        object : ViewModelProvider.Factory {\n            override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                return FirstLaunchViewModel(scheduler, projectsRepository, projectsDataService) as T\n            }\n        }\n    }\n\n    public override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        DaggerUtils.getComponent(this).inject(this)\n\n        FirstLaunchLayoutBinding.inflate(layoutInflater).apply {\n            setView(this.root, false)\n\n            MaterialProgressDialogFragment.showOn(\n                this@FirstLaunchActivity,\n                viewModel.isLoading,\n                supportFragmentManager\n            ) {\n                MaterialProgressDialogFragment().also { dialog ->\n                    dialog.message = getString(R.string.loading)\n                }\n            }\n\n            viewModel.currentProject.observe(this@FirstLaunchActivity) { currentProject ->\n                if (currentProject != null) {\n                    ActivityUtils.startActivityAndCloseAllOthers(\n                        this@FirstLaunchActivity,\n                        MainMenuActivity::class.java\n                    )\n                }\n            }\n\n            viewModel.isLoading.observe(this@FirstLaunchActivity) { isLoading ->\n                if (!isLoading) {\n                    ActivityUtils.startActivityAndCloseAllOthers(\n                        this@FirstLaunchActivity,\n                        MainMenuActivity::class.java\n                    )\n                }\n            }\n\n            configureViaQrButton.setOnClickListener {\n                DialogFragmentUtils.showIfNotShowing(\n                    QrCodeProjectCreatorDialog::class.java,\n                    supportFragmentManager\n                )\n            }\n\n            configureManuallyButton.setOnClickListener {\n                DialogFragmentUtils.showIfNotShowing(\n                    ManualProjectCreatorDialog::class.java,\n                    supportFragmentManager\n                )\n            }\n\n            appName.text = String.format(\n                \"%s %s\",\n                getString(R.string.collect_app_name),\n                versionInformation.versionToDisplay\n            )\n\n            dontHaveServer.apply {\n                text = SpannableStringBuilder()\n                    .append(getString(R.string.dont_have_project))\n                    .append(\" \")\n                    .color(\n                        getThemeAttributeValue(\n                            context,\n                            androidx.appcompat.R.attr.colorPrimary\n                        )\n                    ) {\n                        append(getString(R.string.try_demo))\n                    }\n\n                setOnClickListener {\n                    viewModel.tryDemo()\n                }\n            }\n        }\n        lifecycle.addObserver(mdmConfigObserver)\n    }\n}\n\nprivate class FirstLaunchViewModel(\n    private val scheduler: Scheduler,\n    private val projectsRepository: ProjectsRepository,\n    private val projectsDataService: ProjectsDataService\n) : ViewModel() {\n    private val _isLoading = MutableLiveData<Boolean>()\n    val isLoading: LiveData<Boolean> = _isLoading\n\n    val currentProject = projectsDataService.getCurrentProject().asLiveData()\n\n    fun tryDemo() {\n        Analytics.log(AnalyticsEvents.TRY_DEMO)\n\n        _isLoading.value = true\n        scheduler.immediate(\n            background = {\n                projectsRepository.save(Project.DEMO_PROJECT)\n                projectsDataService.setCurrentProject(Project.DEMO_PROJECT_ID)\n            },\n            foreground = {\n                _isLoading.value = false\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/FormDownloadListActivity.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.activities;\n\nimport static org.odk.collect.androidshared.ui.EdgeToEdge.setView;\n\nimport android.app.ProgressDialog;\nimport android.content.DialogInterface;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Bundle;\nimport android.util.SparseBooleanArray;\nimport android.view.View;\nimport android.view.View.OnClickListener;\nimport android.widget.AdapterView;\nimport android.widget.Button;\nimport android.widget.ListView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.activities.viewmodels.FormDownloadListViewModel;\nimport org.odk.collect.android.adapters.FormDownloadListAdapter;\nimport org.odk.collect.android.formentry.RefreshFormListDialogFragment;\nimport org.odk.collect.android.formlists.sorting.FormListSortingOption;\nimport org.odk.collect.android.formmanagement.FormSourceExceptionMapper;\nimport org.odk.collect.android.formmanagement.FormsDataService;\nimport org.odk.collect.android.formmanagement.ServerFormDetails;\nimport org.odk.collect.android.formmanagement.download.FormDownloadException;\nimport org.odk.collect.android.fragments.dialogs.FormsDownloadResultDialog;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.listeners.DownloadFormsTaskListener;\nimport org.odk.collect.android.listeners.FormListDownloaderListener;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.android.tasks.DownloadFormListTask;\nimport org.odk.collect.android.tasks.DownloadFormsTask;\nimport org.odk.collect.android.utilities.ApplicationConstants;\nimport org.odk.collect.android.utilities.AuthDialogUtility;\nimport org.odk.collect.android.utilities.DialogUtils;\nimport org.odk.collect.android.utilities.WebCredentialsUtils;\nimport org.odk.collect.android.views.DayNightProgressDialog;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.async.network.NetworkStateProvider;\nimport org.odk.collect.forms.FormSourceException;\nimport org.odk.collect.openrosa.http.HttpCredentialsInterface;\n\nimport java.io.Serializable;\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Map;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\n/**\n * Responsible for displaying, adding and deleting all the valid forms in the forms directory. One\n * caveat. If the server requires authentication, a dialog will pop up asking when you request the\n * form list. If somehow you manage to wait long enough and then try to download selected forms and\n * your authorization has timed out, it won't again ask for authentication, it will just throw a\n * 401\n * and you'll have to hit 'refresh' where it will ask for credentials again. Technically a server\n * could point at other servers requiring authentication to download the forms, but the current\n * implementation in Collect doesn't allow for that. Mostly this is just because it's a pain in the\n * butt to keep track of which forms we've downloaded and where we're needing to authenticate. I\n * think we do something similar in the instanceuploader task/activity, so should change the\n * implementation eventually.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n */\npublic class FormDownloadListActivity extends FormListActivity implements FormListDownloaderListener,\n        DownloadFormsTaskListener, AuthDialogUtility.AuthDialogUtilityResultListener,\n        AdapterView.OnItemClickListener, RefreshFormListDialogFragment.RefreshFormListDialogFragmentListener,\n        FormsDownloadResultDialog.FormDownloadResultDialogListener {\n    private static final String FORM_DOWNLOAD_LIST_SORTING_ORDER = \"formDownloadListSortingOrder\";\n    private static final String BUNDLE_SELECTED_COUNT = \"selectedcount\";\n\n    public static final String FORMNAME = \"formname\";\n    private static final String FORMDETAIL_KEY = \"formdetailkey\";\n    public static final String FORMID_DISPLAY = \"formiddisplay\";\n\n    public static final String FORM_ID_KEY = \"formid\";\n    private static final String FORM_VERSION_KEY = \"formversion\";\n\n    private AlertDialog alertDialog;\n    private ProgressDialog cancelDialog;\n    private Button downloadButton;\n\n    private DownloadFormListTask downloadFormListTask;\n    private DownloadFormsTask downloadFormsTask;\n    private Button toggleButton;\n\n    private final ArrayList<HashMap<String, String>> filteredFormList = new ArrayList<>();\n\n    private static final boolean DO_NOT_EXIT = false;\n\n    private FormDownloadListViewModel viewModel;\n\n    @Inject\n    WebCredentialsUtils webCredentialsUtils;\n\n    @Inject\n    NetworkStateProvider connectivityProvider;\n\n    @Inject\n    FormsDataService formsDataService;\n\n    @Inject\n    ProjectsDataService projectsDataService;\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        DaggerUtils.getComponent(this).inject(this);\n\n        setView(this, R.layout.form_download_list, false);\n        setTitle(getString(org.odk.collect.strings.R.string.get_forms));\n\n        viewModel = new ViewModelProvider(this, new FormDownloadListViewModel.Factory())\n                .get(FormDownloadListViewModel.class);\n\n        init(savedInstanceState);\n    }\n\n    private void init(Bundle savedInstanceState) {\n        Bundle bundle = getIntent().getExtras();\n        if (bundle != null) {\n            if (bundle.containsKey(ApplicationConstants.BundleKeys.FORM_IDS)) {\n                viewModel.setDownloadOnlyMode(true);\n                viewModel.setFormIdsToDownload(bundle.getStringArray(ApplicationConstants.BundleKeys.FORM_IDS));\n\n                if (viewModel.getFormIdsToDownload() == null) {\n                    setReturnResult(false, \"Form Ids is null\", null);\n                    finish();\n                }\n\n                if (bundle.containsKey(ApplicationConstants.BundleKeys.URL)) {\n                    viewModel.setUrl(bundle.getString(ApplicationConstants.BundleKeys.URL));\n\n                    if (bundle.containsKey(ApplicationConstants.BundleKeys.USERNAME)\n                            && bundle.containsKey(ApplicationConstants.BundleKeys.PASSWORD)) {\n                        viewModel.setUsername(bundle.getString(ApplicationConstants.BundleKeys.USERNAME));\n                        viewModel.setPassword(bundle.getString(ApplicationConstants.BundleKeys.PASSWORD));\n                    }\n                }\n            }\n        }\n\n        downloadButton = findViewById(R.id.add_button);\n        downloadButton.setEnabled(listView.getCheckedItemCount() > 0);\n        downloadButton.setOnClickListener(v -> {\n            ArrayList<ServerFormDetails> filesToDownload = getFilesToDownload();\n            startFormsDownload(filesToDownload);\n        });\n\n        toggleButton = findViewById(R.id.toggle_button);\n        toggleButton.setEnabled(false);\n        toggleButton.setOnClickListener(new OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                downloadButton.setEnabled(toggleChecked(listView));\n                toggleButtonLabel(toggleButton, listView);\n                viewModel.clearSelectedFormIds();\n                if (listView.getCheckedItemCount() == listView.getCount()) {\n                    for (HashMap<String, String> map : viewModel.getFormList()) {\n                        viewModel.addSelectedFormId(map.get(FORMDETAIL_KEY));\n                    }\n                }\n            }\n        });\n\n        Button refreshButton = findViewById(R.id.refresh_button);\n        refreshButton.setOnClickListener(new OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                viewModel.setLoadingCanceled(false);\n                viewModel.clearFormList();\n                updateAdapter();\n                clearChoices();\n                downloadFormList();\n            }\n        });\n\n        if (savedInstanceState != null) {\n            // how many items we've selected\n            // Android should keep track of this, but broken on rotate...\n            if (savedInstanceState.containsKey(BUNDLE_SELECTED_COUNT)) {\n                downloadButton.setEnabled(savedInstanceState.getInt(BUNDLE_SELECTED_COUNT) > 0);\n            }\n        }\n\n        filteredFormList.addAll(viewModel.getFormList());\n\n        if (getLastCustomNonConfigurationInstance() instanceof DownloadFormListTask) {\n            downloadFormListTask = (DownloadFormListTask) getLastCustomNonConfigurationInstance();\n            if (downloadFormListTask.getStatus() == AsyncTask.Status.FINISHED) {\n                DialogFragmentUtils.dismissDialog(RefreshFormListDialogFragment.class, getSupportFragmentManager());\n                downloadFormsTask = null;\n            }\n        } else if (getLastCustomNonConfigurationInstance() instanceof DownloadFormsTask) {\n            downloadFormsTask = (DownloadFormsTask) getLastCustomNonConfigurationInstance();\n            if (downloadFormsTask.getStatus() == AsyncTask.Status.FINISHED) {\n                DialogFragmentUtils.dismissDialog(RefreshFormListDialogFragment.class, getSupportFragmentManager());\n                downloadFormsTask = null;\n            }\n        } else if (viewModel.getFormDetailsByFormId().isEmpty()\n                && getLastCustomNonConfigurationInstance() == null\n                && !viewModel.wasLoadingCanceled()) {\n            // first time, so get the formlist\n            downloadFormList();\n        }\n\n        listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);\n        listView.setItemsCanFocus(false);\n\n        sortingOptions = Arrays.asList(\n                new FormListSortingOption(\n                        R.drawable.ic_sort_by_alpha,\n                        org.odk.collect.strings.R.string.sort_by_name_asc\n                ),\n                new FormListSortingOption(\n                        R.drawable.ic_sort_by_alpha,\n                        org.odk.collect.strings.R.string.sort_by_name_desc\n                )\n        );\n    }\n\n    private void clearChoices() {\n        listView.clearChoices();\n        downloadButton.setEnabled(false);\n    }\n\n    @Override\n    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {\n        toggleButtonLabel(toggleButton, listView);\n        downloadButton.setEnabled(listView.getCheckedItemCount() > 0);\n\n        if (listView.isItemChecked(position)) {\n            viewModel.addSelectedFormId(((HashMap<String, String>) listView.getAdapter().getItem(position)).get(FORMDETAIL_KEY));\n        } else {\n            viewModel.removeSelectedFormId(((HashMap<String, String>) listView.getAdapter().getItem(position)).get(FORMDETAIL_KEY));\n        }\n    }\n\n    /**\n     * Starts the download task and shows the progress dialog.\n     */\n    private void downloadFormList() {\n        if (!connectivityProvider.isDeviceOnline()) {\n            ToastUtils.showShortToast(org.odk.collect.strings.R.string.no_connection);\n\n            if (viewModel.isDownloadOnlyMode()) {\n                setReturnResult(false, getString(org.odk.collect.strings.R.string.no_connection), viewModel.getFormResults());\n                finish();\n            }\n        } else {\n            viewModel.clearFormDetailsByFormId();\n            DialogFragmentUtils.showIfNotShowing(RefreshFormListDialogFragment.class, getSupportFragmentManager());\n\n            if (downloadFormListTask != null\n                    && downloadFormListTask.getStatus() != AsyncTask.Status.FINISHED) {\n                return; // we are already doing the download!!!\n            } else if (downloadFormListTask != null) {\n                downloadFormListTask.setDownloaderListener(null);\n                downloadFormListTask.cancel(true);\n                downloadFormListTask = null;\n            }\n\n            if (viewModel.isDownloadOnlyMode()) {\n                // Handle external app download case with different server\n                downloadFormListTask = new DownloadFormListTask();\n                downloadFormListTask.setAlternateCredentials(webCredentialsUtils, viewModel.getUrl(), viewModel.getUsername(), viewModel.getPassword());\n                downloadFormListTask.setDownloaderListener(this);\n                downloadFormListTask.execute();\n            } else {\n                downloadFormListTask = new DownloadFormListTask();\n                downloadFormListTask.setDownloaderListener(this);\n                downloadFormListTask.execute();\n            }\n        }\n    }\n\n    @Override\n    protected void onRestoreInstanceState(Bundle state) {\n        super.onRestoreInstanceState(state);\n        updateAdapter();\n    }\n\n    @Override\n    protected void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putInt(BUNDLE_SELECTED_COUNT, listView.getCheckedItemCount());\n    }\n\n    @Override\n    protected String getSortingOrderKey() {\n        return FORM_DOWNLOAD_LIST_SORTING_ORDER;\n    }\n\n    @Override\n    protected void updateAdapter() {\n        CharSequence charSequence = getFilterText();\n        filteredFormList.clear();\n        if (charSequence.length() > 0) {\n            for (HashMap<String, String> form : viewModel.getFormList()) {\n                if (form.get(FORMNAME).toLowerCase(Locale.US).contains(charSequence.toString().toLowerCase(Locale.US))) {\n                    filteredFormList.add(form);\n                }\n            }\n        } else {\n            filteredFormList.addAll(viewModel.getFormList());\n        }\n        sortList();\n        if (listView.getAdapter() == null) {\n            listView.setAdapter(new FormDownloadListAdapter(this, filteredFormList, viewModel.getFormDetailsByFormId()));\n        } else {\n            FormDownloadListAdapter formDownloadListAdapter = (FormDownloadListAdapter) listView.getAdapter();\n            formDownloadListAdapter.setFromIdsToDetails(viewModel.getFormDetailsByFormId());\n            formDownloadListAdapter.notifyDataSetChanged();\n        }\n        toggleButton.setEnabled(!filteredFormList.isEmpty());\n        checkPreviouslyCheckedItems();\n        toggleButtonLabel(toggleButton, listView);\n    }\n\n    @Override\n    protected void checkPreviouslyCheckedItems() {\n        listView.clearChoices();\n        for (int i = 0; i < listView.getCount(); i++) {\n            HashMap<String, String> item =\n                    (HashMap<String, String>) listView.getAdapter().getItem(i);\n            if (viewModel.getSelectedFormIds().contains(item.get(FORMDETAIL_KEY))) {\n                listView.setItemChecked(i, true);\n            }\n        }\n    }\n\n    private void sortList() {\n        Collections.sort(filteredFormList, (lhs, rhs) -> {\n            if (getSortingOrder().equals(SORT_BY_NAME_ASC)) {\n                return lhs.get(FORMNAME).compareToIgnoreCase(rhs.get(FORMNAME));\n            } else {\n                return rhs.get(FORMNAME).compareToIgnoreCase(lhs.get(FORMNAME));\n            }\n        });\n    }\n\n    private ArrayList<ServerFormDetails> getFilesToDownload() {\n        ArrayList<ServerFormDetails> filesToDownload = new ArrayList<>();\n\n        SparseBooleanArray sba = listView.getCheckedItemPositions();\n        for (int i = 0; i < listView.getCount(); i++) {\n            if (sba.get(i, false)) {\n                HashMap<String, String> item =\n                        (HashMap<String, String>) listView.getAdapter().getItem(i);\n                filesToDownload.add(viewModel.getFormDetailsByFormId().get(item.get(FORMDETAIL_KEY)));\n            }\n        }\n        return filesToDownload;\n    }\n\n    /**\n     * starts the task to download the selected forms, also shows progress dialog\n     */\n    @SuppressWarnings(\"unchecked\")\n    private void startFormsDownload(@NonNull ArrayList<ServerFormDetails> filesToDownload) {\n        int totalCount = filesToDownload.size();\n        if (totalCount > 0) {\n            // show dialog box\n            DialogFragmentUtils.showIfNotShowing(RefreshFormListDialogFragment.class, getSupportFragmentManager());\n\n            downloadFormsTask = new DownloadFormsTask(projectsDataService.requireCurrentProject().getUuid(), formsDataService);\n            downloadFormsTask.setDownloaderListener(this);\n\n            if (viewModel.getUrl() != null) {\n                if (viewModel.getUsername() != null && viewModel.getPassword() != null) {\n                    webCredentialsUtils.saveCredentials(viewModel.getUrl(), viewModel.getUsername(), viewModel.getPassword());\n                } else {\n                    webCredentialsUtils.clearCredentials(viewModel.getUrl());\n                }\n            }\n\n            downloadFormsTask.execute(filesToDownload);\n        } else {\n            ToastUtils.showShortToast(org.odk.collect.strings.R.string.noselect_error);\n        }\n    }\n\n    @Override\n    public Object onRetainCustomNonConfigurationInstance() {\n        if (downloadFormsTask != null) {\n            return downloadFormsTask;\n        } else {\n            return downloadFormListTask;\n        }\n    }\n\n    @Override\n    protected void onDestroy() {\n        if (downloadFormListTask != null) {\n            downloadFormListTask.setDownloaderListener(null);\n        }\n        if (downloadFormsTask != null) {\n            downloadFormsTask.setDownloaderListener(null);\n        }\n        super.onDestroy();\n    }\n\n    @Override\n    protected void onResume() {\n        if (downloadFormListTask != null) {\n            downloadFormListTask.setDownloaderListener(this);\n        }\n        if (downloadFormsTask != null) {\n            downloadFormsTask.setDownloaderListener(this);\n        }\n        if (viewModel.isAlertShowing()) {\n            createAlertDialog(viewModel.getAlertTitle(), viewModel.getAlertDialogMsg(), viewModel.shouldExit());\n        }\n        if (viewModel.isCancelDialogShowing()) {\n            createCancelDialog();\n        }\n        super.onResume();\n    }\n\n    @Override\n    protected void onPause() {\n        if (alertDialog != null && alertDialog.isShowing()) {\n            alertDialog.dismiss();\n        }\n        super.onPause();\n    }\n\n    public boolean isLocalFormSuperseded(String formId) {\n        if (formId == null) {\n            Timber.e(new Error(\"isLocalFormSuperseded: server is not OpenRosa-compliant. <formID> is null!\"));\n            return true;\n        }\n\n        ServerFormDetails form = viewModel.getFormDetailsByFormId().get(formId);\n        return form.getType() != ServerFormDetails.Type.OnDevice;\n    }\n\n    /**\n     * Causes any local forms that have been updated on the server to become checked in the list.\n     * This is a prompt and a\n     * convenience to users to download the latest version of those forms from the server.\n     */\n    private void selectSupersededForms() {\n        ListView ls = listView;\n        for (int idx = 0; idx < filteredFormList.size(); idx++) {\n            HashMap<String, String> item = filteredFormList.get(idx);\n            if (isLocalFormSuperseded(item.get(FORM_ID_KEY))) {\n                ls.setItemChecked(idx, true);\n                viewModel.addSelectedFormId(item.get(FORMDETAIL_KEY));\n            }\n        }\n    }\n\n    @Override\n    public void formListDownloadingComplete(HashMap<String, ServerFormDetails> formList, FormSourceException exception) {\n        DialogFragmentUtils.dismissDialog(RefreshFormListDialogFragment.class, getSupportFragmentManager());\n        downloadFormListTask.setDownloaderListener(null);\n        downloadFormListTask = null;\n\n        if (exception == null) {\n            // Everything worked. Clear the list and add the results.\n            viewModel.setFormDetailsByFormId(formList);\n            viewModel.clearFormList();\n\n            ArrayList<String> ids = new ArrayList<>(viewModel.getFormDetailsByFormId().keySet());\n            for (int i = 0; i < formList.size(); i++) {\n                String formDetailsKey = ids.get(i);\n                ServerFormDetails details = viewModel.getFormDetailsByFormId().get(formDetailsKey);\n\n                HashMap<String, String> item = new HashMap<>();\n                item.put(FORMNAME, details.getFormName());\n                item.put(FORMID_DISPLAY,\n                        ((details.getFormVersion() == null) ? \"\" : (getString(org.odk.collect.strings.R.string.version) + \" \"\n                                + details.getFormVersion() + \" \")) + \"ID: \" + details.getFormId());\n                item.put(FORMDETAIL_KEY, formDetailsKey);\n                item.put(FORM_ID_KEY, details.getFormId());\n                item.put(FORM_VERSION_KEY, details.getFormVersion());\n\n                // Insert the new form in alphabetical order.\n                if (viewModel.getFormList().isEmpty()) {\n                    viewModel.addForm(item);\n                } else {\n                    int j;\n                    for (j = 0; j < viewModel.getFormList().size(); j++) {\n                        HashMap<String, String> compareMe = viewModel.getFormList().get(j);\n                        String name = compareMe.get(FORMNAME);\n                        if (name.compareTo(viewModel.getFormDetailsByFormId().get(ids.get(i)).getFormName()) > 0) {\n                            break;\n                        }\n                    }\n                    viewModel.addForm(j, item);\n                }\n            }\n\n            filteredFormList.addAll(viewModel.getFormList());\n            updateAdapter();\n            selectSupersededForms();\n            downloadButton.setEnabled(listView.getCheckedItemCount() > 0);\n            toggleButton.setEnabled(listView.getCount() > 0);\n            toggleButtonLabel(toggleButton, listView);\n\n            if (viewModel.isDownloadOnlyMode()) {\n                performDownloadModeDownload();\n            }\n        } else {\n            if (exception instanceof FormSourceException.AuthRequired) {\n                createAuthDialog();\n            } else {\n                String dialogMessage = new FormSourceExceptionMapper(this).getMessage(exception);\n                String dialogTitle = getString(org.odk.collect.strings.R.string.load_remote_form_error);\n\n                if (viewModel.isDownloadOnlyMode()) {\n                    setReturnResult(false, dialogMessage, viewModel.getFormResults());\n                }\n\n                createAlertDialog(dialogTitle, dialogMessage, DO_NOT_EXIT);\n            }\n        }\n    }\n\n    private void performDownloadModeDownload() {\n        //1. First check if all form IDS could be found on the server - Register forms that could not be found\n\n        for (String formId : viewModel.getFormIdsToDownload()) {\n            viewModel.putFormResult(formId, false);\n        }\n\n        ArrayList<ServerFormDetails> filesToDownload = new ArrayList<>();\n\n        for (ServerFormDetails serverFormDetails : viewModel.getFormDetailsByFormId().values()) {\n            String formId = serverFormDetails.getFormId();\n\n            if (viewModel.getFormResults().containsKey(formId)) {\n                filesToDownload.add(serverFormDetails);\n            }\n        }\n\n        //2. Select forms and start downloading\n        if (!filesToDownload.isEmpty()) {\n            startFormsDownload(filesToDownload);\n        } else {\n            // None of the forms was found\n            setReturnResult(false, \"Forms not found on server\", viewModel.getFormResults());\n            finish();\n        }\n    }\n\n    /**\n     * Creates an alert dialog with the given tite and message. If shouldExit is set to true, the\n     * activity will exit when the user clicks \"ok\".\n     */\n    private void createAlertDialog(String title, String message, final boolean shouldExit) {\n        alertDialog = new MaterialAlertDialogBuilder(this).create();\n        alertDialog.setTitle(title);\n        alertDialog.setMessage(message);\n        DialogInterface.OnClickListener quitListener = new DialogInterface.OnClickListener() {\n            @Override\n            public void onClick(DialogInterface dialog, int i) {\n                switch (i) {\n                    case DialogInterface.BUTTON_POSITIVE: // ok\n                        // just close the dialog\n                        viewModel.setAlertShowing(false);\n                        // successful download, so quit\n                        // Also quit if in download_mode only(called by another app/activity just to download)\n                        if (shouldExit || viewModel.isDownloadOnlyMode()) {\n                            finish();\n                        }\n                        break;\n                }\n            }\n        };\n        alertDialog.setCancelable(false);\n        alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, getString(org.odk.collect.strings.R.string.ok), quitListener);\n        viewModel.setAlertDialogMsg(message);\n        viewModel.setAlertTitle(title);\n        viewModel.setAlertShowing(true);\n        viewModel.setShouldExit(shouldExit);\n        DialogUtils.showDialog(alertDialog, this);\n    }\n\n    private void createAuthDialog() {\n        viewModel.setAlertShowing(false);\n\n        AuthDialogUtility authDialogUtility = new AuthDialogUtility();\n        if (viewModel.getUrl() != null && viewModel.getUsername() != null && viewModel.getPassword() != null) {\n            authDialogUtility.setCustomUsername(viewModel.getUsername());\n            authDialogUtility.setCustomPassword(viewModel.getPassword());\n        }\n        DialogUtils.showDialog(authDialogUtility.createDialog(this, this, viewModel.getUrl()), this);\n    }\n\n    private void createCancelDialog() {\n        cancelDialog = new DayNightProgressDialog(this);\n        cancelDialog.setTitle(getString(org.odk.collect.strings.R.string.canceling));\n        cancelDialog.setMessage(getString(org.odk.collect.strings.R.string.please_wait));\n        cancelDialog.setIndeterminate(true);\n        cancelDialog.setCancelable(false);\n        viewModel.setCancelDialogShowing(true);\n        DialogUtils.showDialog(cancelDialog, this);\n    }\n\n    @Override\n    public void progressUpdate(String currentFile, int progress, int total) {\n        RefreshFormListDialogFragment fragment = (RefreshFormListDialogFragment) getSupportFragmentManager().findFragmentByTag(RefreshFormListDialogFragment.class.getName());\n\n        if (fragment != null) {\n            fragment.setMessage(getString(org.odk.collect.strings.R.string.fetching_file, currentFile,\n                    String.valueOf(progress), String.valueOf(total)));\n        }\n    }\n\n    @Override\n    public void formsDownloadingComplete(Map<ServerFormDetails, FormDownloadException> result) {\n        if (downloadFormsTask != null) {\n            downloadFormsTask.setDownloaderListener(null);\n        }\n\n        cleanUpWebCredentials();\n\n        DialogFragmentUtils.dismissDialog(RefreshFormListDialogFragment.class, getSupportFragmentManager());\n\n        Bundle args = new Bundle();\n        args.putSerializable(FormsDownloadResultDialog.ARG_RESULT, (Serializable) result);\n        DialogFragmentUtils.showIfNotShowing(FormsDownloadResultDialog.class, args, getSupportFragmentManager());\n\n        // Set result to true for forms which were downloaded\n        if (viewModel.isDownloadOnlyMode()) {\n            for (ServerFormDetails serverFormDetails : result.keySet()) {\n                if (result.get(serverFormDetails) == null) {\n                    if (viewModel.getFormResults().containsKey(serverFormDetails.getFormId())) {\n                        viewModel.putFormResult(serverFormDetails.getFormId(), true);\n                    }\n                }\n            }\n\n            setReturnResult(true, null, viewModel.getFormResults());\n        }\n    }\n\n    @Override\n    public void formsDownloadingCancelled() {\n        if (downloadFormsTask != null) {\n            downloadFormsTask.setDownloaderListener(null);\n            downloadFormsTask = null;\n        }\n\n        cleanUpWebCredentials();\n\n        if (cancelDialog != null && cancelDialog.isShowing()) {\n            cancelDialog.dismiss();\n            viewModel.setCancelDialogShowing(false);\n        }\n\n        if (viewModel.isDownloadOnlyMode()) {\n            setReturnResult(false, \"Download cancelled\", null);\n            finish();\n        }\n    }\n\n    @Override\n    public void updatedCredentials() {\n        // If the user updated the custom credentials using the dialog, let us update our\n        // variables holding the custom credentials\n        if (viewModel.getUrl() != null) {\n            HttpCredentialsInterface httpCredentials = webCredentialsUtils.getCredentials(URI.create(viewModel.getUrl()));\n\n            if (httpCredentials != null) {\n                viewModel.setUsername(httpCredentials.getUsername());\n                viewModel.setPassword(httpCredentials.getPassword());\n            }\n        }\n\n        downloadFormList();\n    }\n\n    @Override\n    public void cancelledUpdatingCredentials() {\n        finish();\n    }\n\n    private void setReturnResult(boolean successful, @Nullable String message, @Nullable HashMap<String, Boolean> resultFormIds) {\n        Intent intent = new Intent();\n        intent.putExtra(ApplicationConstants.BundleKeys.SUCCESS_KEY, successful);\n        if (message != null) {\n            intent.putExtra(ApplicationConstants.BundleKeys.MESSAGE, message);\n        }\n        if (resultFormIds != null) {\n            intent.putExtra(ApplicationConstants.BundleKeys.FORM_IDS, resultFormIds);\n        }\n\n        setResult(RESULT_OK, intent);\n    }\n\n    private void cleanUpWebCredentials() {\n        if (viewModel.getUrl() != null) {\n            String host = Uri.parse(viewModel.getUrl())\n                    .getHost();\n\n            if (host != null) {\n                webCredentialsUtils.clearCredentials(viewModel.getUrl());\n            }\n        }\n    }\n\n    @Override\n    public void onCancelFormLoading() {\n        if (downloadFormListTask != null) {\n            downloadFormListTask.setDownloaderListener(null);\n            downloadFormListTask.cancel(true);\n            downloadFormListTask = null;\n\n            // Only explicitly exit if DownloadFormListTask is running since\n            // DownloadFormTask has a callback when cancelled and has code to handle\n            // cancellation when in download mode only\n            if (viewModel.isDownloadOnlyMode()) {\n                setReturnResult(false, \"User cancelled the operation\", viewModel.getFormResults());\n                finish();\n            }\n        }\n\n        if (downloadFormsTask != null) {\n            createCancelDialog();\n            downloadFormsTask.cancel(true);\n        }\n        viewModel.setLoadingCanceled(true);\n    }\n\n    @Override\n    public void onCloseDownloadingResult() {\n        finish();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt",
    "content": "package org.odk.collect.android.activities\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.createSavedStateHandle\nimport androidx.lifecycle.viewmodel.CreationExtras\nimport org.javarosa.core.model.actions.recordaudio.RecordAudioActions\nimport org.javarosa.core.model.instance.TreeReference\nimport org.odk.collect.android.entities.EntitiesRepositoryProvider\nimport org.odk.collect.android.formentry.BackgroundAudioViewModel\nimport org.odk.collect.android.formentry.BackgroundAudioViewModel.RecordAudioActionRegistry\nimport org.odk.collect.android.formentry.FormEndViewModel\nimport org.odk.collect.android.formentry.FormEntryViewModel\nimport org.odk.collect.android.formentry.FormOpeningMode\nimport org.odk.collect.android.formentry.FormSessionRepository\nimport org.odk.collect.android.formentry.PrinterWidgetViewModel\nimport org.odk.collect.android.formentry.audit.IdentityPromptViewModel\nimport org.odk.collect.android.formentry.backgroundlocation.BackgroundLocationHelper\nimport org.odk.collect.android.formentry.backgroundlocation.BackgroundLocationManager\nimport org.odk.collect.android.formentry.backgroundlocation.BackgroundLocationViewModel\nimport org.odk.collect.android.formentry.saving.DiskFormSaver\nimport org.odk.collect.android.formentry.saving.FormSaveViewModel\nimport org.odk.collect.android.injection.config.ProjectDependencyModuleFactory\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.utilities.ChangeLockProvider\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.utilities.MediaUtils\nimport org.odk.collect.android.utilities.SavepointsRepositoryProvider\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel\nimport org.odk.collect.android.widgets.viewmodels.QuestionViewModel\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.audiorecorder.recording.AudioRecorder\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.permissions.PermissionsProvider\nimport org.odk.collect.printer.HtmlPrinter\nimport org.odk.collect.qrcode.zxing.QRCodeCreator\nimport org.odk.collect.settings.SettingsProvider\nimport java.util.function.BiConsumer\n\nclass FormEntryViewModelFactory(\n    private val mode: String?,\n    private val sessionId: String,\n    private val scheduler: Scheduler,\n    private val formSessionRepository: FormSessionRepository,\n    private val mediaUtils: MediaUtils,\n    private val audioRecorder: AudioRecorder,\n    private val projectsDataService: ProjectsDataService,\n    private val entitiesRepositoryProvider: EntitiesRepositoryProvider,\n    private val settingsProvider: SettingsProvider,\n    private val permissionsChecker: PermissionsChecker,\n    private val fusedLocationClient: LocationClient,\n    private val permissionsProvider: PermissionsProvider,\n    private val autoSendSettingsProvider: AutoSendSettingsProvider,\n    private val formsRepositoryProvider: FormsRepositoryProvider,\n    private val instancesRepositoryProvider: InstancesRepositoryProvider,\n    private val savepointsRepositoryProvider: SavepointsRepositoryProvider,\n    private val qrCodeCreator: QRCodeCreator,\n    private val htmlPrinter: HtmlPrinter,\n    private val instancesDataService: InstancesDataService,\n    private val changeLockProvider: ChangeLockProvider,\n    private val projectDependencyModuleFactory: ProjectDependencyModuleFactory\n) : ViewModelProvider.Factory {\n\n    override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {\n        val projectId = projectsDataService.requireCurrentProject().uuid\n\n        return when (modelClass) {\n            FormEntryViewModel::class.java -> FormEntryViewModel(\n                System::currentTimeMillis,\n                scheduler,\n                formSessionRepository,\n                sessionId,\n                formsRepositoryProvider.create(projectId),\n                changeLockProvider.create(projectId)\n            )\n\n            FormSaveViewModel::class.java -> createFormSaveViewModel(extras.createSavedStateHandle())\n\n            BackgroundAudioViewModel::class.java -> {\n                val recordAudioActionRegistry =\n                    if (mode == FormOpeningMode.VIEW_SENT) {\n                        object : RecordAudioActionRegistry {\n                            override fun register(listener: BiConsumer<TreeReference, String?>) {}\n                            override fun unregister() {}\n                        }\n                    } else {\n                        object : RecordAudioActionRegistry {\n                            override fun register(listener: BiConsumer<TreeReference, String?>) {\n                                RecordAudioActions.setRecordAudioListener { absoluteTargetRef: TreeReference, quality: String? ->\n                                    listener.accept(absoluteTargetRef, quality)\n                                }\n                            }\n\n                            override fun unregister() {\n                                RecordAudioActions.setRecordAudioListener(null)\n                            }\n                        }\n                    }\n\n                BackgroundAudioViewModel(\n                    audioRecorder,\n                    settingsProvider.getUnprotectedSettings(),\n                    recordAudioActionRegistry,\n                    permissionsChecker,\n                    System::currentTimeMillis,\n                    formSessionRepository.get(sessionId)\n                )\n            }\n\n            BackgroundLocationViewModel::class.java -> {\n                val locationManager = BackgroundLocationManager(\n                    fusedLocationClient,\n                    BackgroundLocationHelper(\n                        permissionsProvider,\n                        settingsProvider.getUnprotectedSettings(),\n                        formSessionRepository,\n                        sessionId\n                    )\n                )\n\n                BackgroundLocationViewModel(locationManager)\n            }\n\n            IdentityPromptViewModel::class.java -> IdentityPromptViewModel()\n\n            FormEndViewModel::class.java -> FormEndViewModel(\n                formSessionRepository,\n                sessionId,\n                settingsProvider,\n                autoSendSettingsProvider\n            )\n\n            PrinterWidgetViewModel::class.java -> PrinterWidgetViewModel(\n                scheduler,\n                qrCodeCreator,\n                htmlPrinter\n            )\n\n            QuestionViewModel::class.java -> QuestionViewModel(scheduler, formSessionRepository, sessionId)\n\n            MediaWidgetAnswerViewModel::class.java -> MediaWidgetAnswerViewModel(\n                scheduler,\n                createFormSaveViewModel(extras.createSavedStateHandle()),\n                mediaUtils\n            )\n\n            else -> throw IllegalArgumentException()\n        } as T\n    }\n\n    private fun createFormSaveViewModel(handle: SavedStateHandle): FormSaveViewModel {\n        val projectId = projectsDataService.requireCurrentProject().uuid\n\n        return FormSaveViewModel(\n            handle,\n            System::currentTimeMillis,\n            DiskFormSaver(),\n            mediaUtils,\n            scheduler,\n            audioRecorder,\n            projectsDataService,\n            formSessionRepository.get(sessionId),\n            entitiesRepositoryProvider.create(projectId),\n            instancesRepositoryProvider.create(projectId),\n            savepointsRepositoryProvider.create(projectId),\n            instancesDataService,\n            projectDependencyModuleFactory.create(projectId).debugLogger\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.activities;\n\nimport static android.content.DialogInterface.BUTTON_NEGATIVE;\nimport static android.content.DialogInterface.BUTTON_POSITIVE;\nimport static android.view.animation.AnimationUtils.loadAnimation;\nimport static org.javarosa.form.api.FormEntryController.EVENT_PROMPT_NEW_REPEAT;\nimport static org.odk.collect.android.formentry.FormIndexAnimationHandler.Direction.BACKWARDS;\nimport static org.odk.collect.android.formentry.FormIndexAnimationHandler.Direction.FORWARDS;\nimport static org.odk.collect.android.formentry.repeats.DeleteRepeatDialogFragment.REQUEST_DELETE_REPEAT;\nimport static org.odk.collect.android.javarosawrapper.FormControllerExt.getGroupsForCurrentIndex;\nimport static org.odk.collect.android.javarosawrapper.FormControllerExt.getQuestionPrompts;\nimport static org.odk.collect.android.utilities.AnimationUtils.areAnimationsEnabled;\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\nimport static org.odk.collect.android.utilities.DialogUtils.getDialog;\nimport static org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing;\nimport static org.odk.collect.androidshared.ui.EdgeToEdge.setView;\nimport static org.odk.collect.androidshared.ui.ToastUtils.showLongToast;\nimport static org.odk.collect.androidshared.ui.ToastUtils.showShortToast;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_NAVIGATION;\nimport static org.odk.collect.settings.keys.ProtectedProjectKeys.KEY_MOVING_BACKWARDS;\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.location.LocationManager;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.view.ContextMenu;\nimport android.view.ContextMenu.ContextMenuInfo;\nimport android.view.KeyEvent;\nimport android.view.MenuItem;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewGroup.LayoutParams;\nimport android.view.animation.Animation;\nimport android.view.animation.Animation.AnimationListener;\nimport android.widget.FrameLayout;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.activity.OnBackPressedCallback;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.core.widget.NestedScrollView;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport com.google.android.material.appbar.AppBarLayout;\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.model.instance.TreeElement;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.javarosa.form.api.FormEntryController;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.jetbrains.annotations.NotNull;\nimport org.joda.time.DateTime;\nimport org.joda.time.LocalDateTime;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.analytics.AnalyticsUtils;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.application.CollectComposeThemeProvider;\nimport org.odk.collect.android.audio.AMRAppender;\nimport org.odk.collect.android.audio.AudioControllerView;\nimport org.odk.collect.android.audio.AudioRecordingControllerFragment;\nimport org.odk.collect.android.audio.M4AAppender;\nimport org.odk.collect.android.dao.helpers.InstancesDaoHelper;\nimport org.odk.collect.android.entities.EntitiesRepositoryProvider;\nimport org.odk.collect.android.exception.JavaRosaException;\nimport org.odk.collect.android.external.InstancesContract;\nimport org.odk.collect.android.formentry.BackgroundAudioPermissionDialogFragment;\nimport org.odk.collect.android.formentry.BackgroundAudioViewModel;\nimport org.odk.collect.android.formentry.CurrentFormIndex;\nimport org.odk.collect.android.formentry.FormAnimation;\nimport org.odk.collect.android.formentry.FormAnimationType;\nimport org.odk.collect.android.formentry.FormEndView;\nimport org.odk.collect.android.formentry.FormEndViewModel;\nimport org.odk.collect.android.formentry.FormEntryMenuProvider;\nimport org.odk.collect.android.formentry.FormEntryViewModel;\nimport org.odk.collect.android.formentry.FormError;\nimport org.odk.collect.android.formentry.FormIndexAnimationHandler;\nimport org.odk.collect.android.formentry.FormIndexAnimationHandler.Direction;\nimport org.odk.collect.android.formentry.FormLoadingDialogFragment;\nimport org.odk.collect.android.formentry.FormOpeningMode;\nimport org.odk.collect.android.formentry.FormSessionRepository;\nimport org.odk.collect.android.formentry.ODKView;\nimport org.odk.collect.android.formentry.PrinterWidgetViewModel;\nimport org.odk.collect.android.formentry.QuitFormDialog;\nimport org.odk.collect.android.formentry.RecordingHandler;\nimport org.odk.collect.android.formentry.RecordingWarningDialogFragment;\nimport org.odk.collect.android.formentry.SwipeHandler;\nimport org.odk.collect.android.formentry.audit.AuditEvent;\nimport org.odk.collect.android.formentry.audit.ChangesReasonPromptDialogFragment;\nimport org.odk.collect.android.formentry.audit.IdentifyUserPromptDialogFragment;\nimport org.odk.collect.android.formentry.audit.IdentityPromptViewModel;\nimport org.odk.collect.android.formentry.backgroundlocation.BackgroundLocationManager;\nimport org.odk.collect.android.formentry.backgroundlocation.BackgroundLocationViewModel;\nimport org.odk.collect.android.formentry.repeats.AddRepeatDialog;\nimport org.odk.collect.android.formentry.repeats.DeleteRepeatDialogFragment;\nimport org.odk.collect.android.formentry.saving.FormSaveViewModel;\nimport org.odk.collect.android.formentry.saving.SaveAnswerFileErrorDialogFragment;\nimport org.odk.collect.android.formentry.saving.SaveAnswerFileProgressDialogFragment;\nimport org.odk.collect.android.formentry.saving.SaveFormProgressDialogFragment;\nimport org.odk.collect.android.formhierarchy.FormHierarchyFragmentHostActivity;\nimport org.odk.collect.android.fragments.MediaLoadingFragment;\nimport org.odk.collect.android.fragments.dialogs.LocationProvidersDisabledDialog;\nimport org.odk.collect.android.fragments.dialogs.RangePickerDialogFragment;\nimport org.odk.collect.android.fragments.dialogs.RankingWidgetDialog;\nimport org.odk.collect.android.fragments.dialogs.SelectMinimalDialog;\nimport org.odk.collect.android.injection.config.ProjectDependencyModuleFactory;\nimport org.odk.collect.android.instancemanagement.InstanceExtKt;\nimport org.odk.collect.android.instancemanagement.InstancesDataService;\nimport org.odk.collect.android.instancemanagement.LocalInstancesUseCases;\nimport org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider;\nimport org.odk.collect.android.javarosawrapper.FailedValidationResult;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.javarosawrapper.RepeatsInFieldListException;\nimport org.odk.collect.android.javarosawrapper.SuccessValidationResult;\nimport org.odk.collect.android.javarosawrapper.ValidationResult;\nimport org.odk.collect.android.listeners.AdvanceToNextListener;\nimport org.odk.collect.android.listeners.FormLoaderListener;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.android.savepoints.SavepointListener;\nimport org.odk.collect.android.savepoints.SavepointTask;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.tasks.FormLoaderTask;\nimport org.odk.collect.android.tasks.SaveFormIndexTask;\nimport org.odk.collect.android.utilities.ChangeLockProvider;\nimport org.odk.collect.android.utilities.ContentUriHelper;\nimport org.odk.collect.android.utilities.ControllableLifecyleOwner;\nimport org.odk.collect.android.utilities.ExternalAppIntentProvider;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.android.utilities.SavepointsRepositoryProvider;\nimport org.odk.collect.android.utilities.SoftKeyboardController;\nimport org.odk.collect.android.widgets.GeoShapeWidget;\nimport org.odk.collect.android.widgets.GeoTraceWidget;\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.datetime.DateTimeWidget;\nimport org.odk.collect.android.widgets.datetime.pickers.CustomDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.CustomTimePickerDialog;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.items.SelectOneFromMapDialogFragment;\nimport org.odk.collect.android.widgets.utilities.ExternalAppRecordingRequester;\nimport org.odk.collect.android.widgets.utilities.FormControllerWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.utilities.GeoPolyDialogFragment;\nimport org.odk.collect.android.widgets.utilities.InternalRecordingRequester;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.androidshared.system.IntentLauncher;\nimport org.odk.collect.androidshared.system.PlayServicesChecker;\nimport org.odk.collect.androidshared.system.ProcessRestoreDetector;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder;\nimport org.odk.collect.androidshared.ui.DialogUtils;\nimport org.odk.collect.androidshared.ui.SnackbarUtils;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.audioclips.AudioPlayerFactory;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.externalapp.ExternalAppUtils;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.location.LocationClient;\nimport org.odk.collect.material.MaterialProgressDialogFragment;\nimport org.odk.collect.metadata.PropertyManager;\nimport org.odk.collect.permissions.PermissionListener;\nimport org.odk.collect.permissions.PermissionsChecker;\nimport org.odk.collect.permissions.PermissionsProvider;\nimport org.odk.collect.printer.HtmlPrinter;\nimport org.odk.collect.qrcode.zxing.QRCodeCreatorImpl;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.strings.localization.LocalizedActivity;\nimport org.odk.collect.timedgrid.NavigationWarning;\n\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.List;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\nimport timber.log.Timber;\n\n/**\n * FormFillingActivity is responsible for displaying questions, animating\n * transitions between questions, and allowing the user to enter data.\n *\n * This class should never be started directly. Instead {@link org.odk.collect.android.external.FormUriActivity}\n * should be used to start form filling.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n * @author Thomas Smyth, Sassafras Tech Collective (tom@sassafrastech.com; constraint behavior\n * option)\n */\npublic class FormFillingActivity extends LocalizedActivity implements CollectComposeThemeProvider, AnimationListener,\n        FormLoaderListener, AdvanceToNextListener, SwipeHandler.OnSwipeListener,\n        SavepointListener,\n        RankingWidgetDialog.RankingListener, SaveFormIndexTask.SaveFormIndexListener,\n        WidgetValueChangedListener, FormLoadingDialogFragment.FormLoadingDialogFragmentListener,\n        AudioControllerView.SwipableParent, FormIndexAnimationHandler.Listener,\n        SelectMinimalDialog.SelectMinimalDialogListener, CustomDatePickerDialog.DateChangeListener,\n        CustomTimePickerDialog.TimeChangeListener {\n\n    public static final String KEY_INSTANCES = \"instances\";\n    public static final String KEY_SUCCESS = \"success\";\n    public static final String KEY_ERROR = \"error\";\n    private static final String KEY_LOCATION_PERMISSIONS_GRANTED = \"location_permissions_granted\";\n\n    private static final String TAG_MEDIA_LOADING_FRAGMENT = \"media_loading_fragment\";\n\n    // Identifies whether this is a new form, or reloading a form after a screen\n    // rotation (or similar)\n    private static final String NEWFORM = \"newform\";\n    // these are only processed if we shut down and are restoring after an\n    // external intent fires\n\n    public static final String KEY_XPATH = \"xpath\";\n    public static final String KEY_XPATH_WAITING_FOR_DATA = \"xpathwaiting\";\n\n    // Tracks whether we are autosaving\n    public static final String KEY_AUTO_SAVED = \"autosaved\";\n\n    public static final String TAG_PROGRESS_DIALOG_MEDIA_LOADING = FormFillingActivity.class.getName() + MaterialProgressDialogFragment.class.getName() + \"mediaLoading\";\n\n    private boolean autoSaved;\n    private boolean allowMovingBackwards;\n\n    // Random ID\n    private static final int DELETE_REPEAT = 654321;\n    private String saveName;\n\n    private Animation inAnimation;\n    private Animation outAnimation;\n\n    private AppBarLayout appBarLayout;\n    private FrameLayout questionHolder;\n    private SwipeHandler.View currentView;\n\n    private AlertDialog alertDialog;\n    private FormError formError;\n    private boolean shownAlertDialogIsGroupRepeat;\n\n    private FormLoaderTask formLoaderTask;\n\n    private TextView nextButton;\n    private TextView backButton;\n\n    private ODKView odkView;\n    private ControllableLifecyleOwner odkViewLifecycle;\n\n    private String startingXPath;\n    private String waitingXPath;\n    private boolean newForm = true;\n\n    MediaLoadingFragment mediaLoadingFragment;\n    private FormIndexAnimationHandler formIndexAnimationHandler;\n    private WaitingForDataRegistry waitingForDataRegistry;\n    private InternalRecordingRequester internalRecordingRequester;\n    private ExternalAppRecordingRequester externalAppRecordingRequester;\n    private FormEntryViewModelFactory viewModelFactory;\n\n    @Override\n    public void allowSwiping(boolean doSwipe) {\n        swipeHandler.setAllowSwiping(doSwipe);\n    }\n\n    private boolean showNavigationButtons;\n\n    @Inject\n    StoragePathProvider storagePathProvider;\n\n    @Inject\n    FormsRepositoryProvider formsRepositoryProvider;\n\n    @Inject\n    PropertyManager propertyManager;\n\n    @Inject\n    InstancesDataService instancesDataService;\n\n    @Inject\n    Scheduler scheduler;\n\n    @Inject\n    AudioRecorder audioRecorder;\n\n    @Inject\n    SoftKeyboardController softKeyboardController;\n\n    @Inject\n    PermissionsChecker permissionsChecker;\n\n    @Inject\n    ExternalAppIntentProvider externalAppIntentProvider;\n\n    @Inject\n    ProjectsDataService projectsDataService;\n\n    @Inject\n    IntentLauncher intentLauncher;\n\n    @Inject\n    FormSessionRepository formSessionRepository;\n\n    @Inject\n    PermissionsProvider permissionsProvider;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    @Inject\n    MediaUtils mediaUtils;\n\n    @Inject\n    EntitiesRepositoryProvider entitiesRepositoryProvider;\n\n    @Inject\n    @Named(\"fused\")\n    LocationClient fusedLocatonClient;\n\n    @Inject\n    public FormLoaderTask.FormEntryControllerFactory formEntryControllerFactory;\n\n    @Inject\n    public AutoSendSettingsProvider autoSendSettingsProvider;\n\n    @Inject\n    public InstancesRepositoryProvider instancesRepositoryProvider;\n\n    @Inject\n    public SavepointsRepositoryProvider savepointsRepositoryProvider;\n\n    @Inject\n    public ChangeLockProvider changeLockProvider;\n\n    @Inject\n    public AudioPlayerFactory audioPlayerFactory;\n\n    @Inject\n    public ProjectDependencyModuleFactory projectDependencyModuleFactory;\n\n    private final LocationProvidersReceiver locationProvidersReceiver = new LocationProvidersReceiver();\n\n    private SwipeHandler swipeHandler;\n\n    /**\n     * True if the Android location permission was granted last time it was checked. Allows for\n     * detection of location permissions changes while the activity is in the background.\n     */\n    private boolean locationPermissionsPreviouslyGranted;\n\n    private BackgroundLocationViewModel backgroundLocationViewModel;\n    private IdentityPromptViewModel identityPromptViewModel;\n    private FormSaveViewModel formSaveViewModel;\n    private FormEntryViewModel formEntryViewModel;\n    private PrinterWidgetViewModel printerWidgetViewModel;\n    private BackgroundAudioViewModel backgroundAudioViewModel;\n    private FormEndViewModel formEndViewModel;\n    private MediaWidgetAnswerViewModel mediaWidgetAnswerViewModel;\n\n    private static final String KEY_SESSION_ID = \"sessionId\";\n    private String sessionId;\n\n    private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {\n        @Override\n        public void handleOnBackPressed() {\n            NavigationWarning navigationWarning = odkView != null ? odkView.isNavigationBlocked() : null;\n            if (navigationWarning != null) {\n                DialogUtils.show(FormFillingActivity.this, navigationWarning.getTitleRes(), navigationWarning.getMessageRes());\n            } else if (audioRecorder.isRecording() && !backgroundAudioViewModel.isBackgroundRecording()) {\n                // We want the user to stop recording before changing screens\n                DialogFragmentUtils.showIfNotShowing(RecordingWarningDialogFragment.class, getSupportFragmentManager());\n            } else {\n                QuitFormDialog.show(FormFillingActivity.this, formSaveViewModel, formEntryViewModel, settingsProvider, () -> {\n                    saveForm(true, InstancesDaoHelper.isInstanceComplete(getFormController()), null, true);\n                });\n            }\n        }\n    };\n\n\n    /**\n     * Called when the activity is first created.\n     */\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        Collect.getInstance().getComponent().inject(this);\n\n        if (savedInstanceState == null) {\n            sessionId = formSessionRepository.create();\n        } else {\n            sessionId = savedInstanceState.getString(KEY_SESSION_ID);\n        }\n\n        viewModelFactory = new FormEntryViewModelFactory(\n                getIntent().getStringExtra(FormOpeningMode.FORM_MODE_KEY),\n                sessionId,\n                scheduler,\n                formSessionRepository,\n                mediaUtils,\n                audioRecorder,\n                projectsDataService,\n                entitiesRepositoryProvider,\n                settingsProvider,\n                permissionsChecker,\n                fusedLocatonClient,\n                permissionsProvider,\n                autoSendSettingsProvider,\n                formsRepositoryProvider,\n                instancesRepositoryProvider,\n                savepointsRepositoryProvider,\n                new QRCodeCreatorImpl(),\n                new HtmlPrinter(),\n                instancesDataService,\n                changeLockProvider,\n                projectDependencyModuleFactory\n        );\n\n        this.getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder()\n                .forClass(AudioRecordingControllerFragment.class, () -> new AudioRecordingControllerFragment(viewModelFactory))\n                .forClass(SaveFormProgressDialogFragment.class, () -> new SaveFormProgressDialogFragment(viewModelFactory))\n                .forClass(DeleteRepeatDialogFragment.class, () -> new DeleteRepeatDialogFragment(viewModelFactory))\n                .forClass(BackgroundAudioPermissionDialogFragment.class, () -> new BackgroundAudioPermissionDialogFragment(viewModelFactory))\n                .forClass(SelectOneFromMapDialogFragment.class, () -> new SelectOneFromMapDialogFragment(viewModelFactory))\n                .forClass(GeoPolyDialogFragment.class, () -> new GeoPolyDialogFragment(viewModelFactory))\n                .forClass(RangePickerDialogFragment.class, () -> new RangePickerDialogFragment(viewModelFactory))\n                .build());\n\n        getSupportFragmentManager().setFragmentResultListener(REQUEST_DELETE_REPEAT, this, (requestKey, result) -> deleteGroup());\n\n        if (ProcessRestoreDetector.isProcessRestoring(this, savedInstanceState)) {\n            if (savedInstanceState.containsKey(KEY_XPATH)) {\n                startingXPath = savedInstanceState.getString(KEY_XPATH);\n            }\n\n            if (savedInstanceState.containsKey(KEY_XPATH_WAITING_FOR_DATA)) {\n                waitingXPath = savedInstanceState.getString(KEY_XPATH_WAITING_FOR_DATA);\n            }\n\n            savedInstanceState = null;\n        }\n\n        super.onCreate(savedInstanceState);\n\n        setView(this, R.layout.form_entry, false);\n        setupViewModels(viewModelFactory);\n\n        // https://github.com/getodk/collect/issues/5469\n        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {\n            getWindow().getDecorView().setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);\n        }\n        swipeHandler = new SwipeHandler(this, settingsProvider.getUnprotectedSettings());\n\n        formError = null;\n\n        appBarLayout = findViewById(org.odk.collect.androidshared.R.id.appBarLayout);\n        questionHolder = findViewById(R.id.questionholder);\n\n        initToolbar();\n\n        formIndexAnimationHandler = new FormIndexAnimationHandler(this);\n        FormEntryMenuProvider menuProvider = new FormEntryMenuProvider(\n                this,\n                this::getAnswers,\n                formEntryViewModel,\n                audioRecorder,\n                backgroundLocationViewModel,\n                backgroundAudioViewModel,\n                settingsProvider,\n                new FormEntryMenuProvider.FormEntryMenuClickListener() {\n                    @Override\n                    public void changeLanguage() {\n                        createLanguageDialog();\n                    }\n\n                    @Override\n                    public void save() {\n                        saveForm(false, InstancesDaoHelper.isInstanceComplete(getFormController()), null, true);\n                    }\n                },\n                () -> {\n                    NavigationWarning navigationWarning = odkView != null ? odkView.isNavigationBlocked() : null;\n                    if (navigationWarning != null) {\n                        DialogUtils.show(this, navigationWarning.getTitleRes(), navigationWarning.getMessageRes());\n                        swipeHandler.setBeenSwiped(false);\n                        return false;\n                    }\n                    return true;\n                }\n        );\n\n        addMenuProvider(menuProvider, this);\n\n        nextButton = findViewById(R.id.form_forward_button);\n        nextButton.setOnClickListener(v -> {\n            swipeHandler.setBeenSwiped(true);\n            onSwipeForward();\n        });\n\n        backButton = findViewById(R.id.form_back_button);\n        backButton.setOnClickListener(v -> {\n            swipeHandler.setBeenSwiped(true);\n            onSwipeBackward();\n        });\n\n        if (savedInstanceState == null) {\n            mediaLoadingFragment = new MediaLoadingFragment();\n            getSupportFragmentManager().beginTransaction().add(mediaLoadingFragment, TAG_MEDIA_LOADING_FRAGMENT).commit();\n        } else {\n            mediaLoadingFragment = (MediaLoadingFragment) getSupportFragmentManager().findFragmentByTag(TAG_MEDIA_LOADING_FRAGMENT);\n        }\n\n        setupFields(savedInstanceState);\n        loadForm();\n\n        getOnBackPressedDispatcher().addCallback(onBackPressedCallback);\n\n        MaterialProgressDialogFragment.showOn(this, printerWidgetViewModel.isLoading(), getSupportFragmentManager(), () -> {\n            MaterialProgressDialogFragment dialog = new MaterialProgressDialogFragment();\n            dialog.setMessage(getLocalizedString(this, org.odk.collect.strings.R.string.loading));\n            return dialog;\n        });\n    }\n\n    private void setupViewModels(FormEntryViewModelFactory formEntryViewModelFactory) {\n        ViewModelProvider viewModelProvider = new ViewModelProvider(\n                this,\n                formEntryViewModelFactory\n        );\n\n        backgroundLocationViewModel = viewModelProvider.get(BackgroundLocationViewModel.class);\n\n        backgroundAudioViewModel = viewModelProvider.get(BackgroundAudioViewModel.class);\n        backgroundAudioViewModel.isPermissionRequired().observe(this, isPermissionRequired -> {\n            if (isPermissionRequired) {\n                showIfNotShowing(BackgroundAudioPermissionDialogFragment.class, getSupportFragmentManager());\n            }\n        });\n\n        identityPromptViewModel = viewModelProvider.get(IdentityPromptViewModel.class);\n        identityPromptViewModel.requiresIdentityToContinue().observe(this, requiresIdentity -> {\n            if (requiresIdentity) {\n                showIfNotShowing(IdentifyUserPromptDialogFragment.class, getSupportFragmentManager());\n            }\n        });\n\n        identityPromptViewModel.isFormEntryCancelled().observe(this, isFormEntryCancelled -> {\n            if (isFormEntryCancelled) {\n                exit();\n            }\n        });\n\n        formEntryViewModel = viewModelProvider.get(FormEntryViewModel.class);\n        printerWidgetViewModel = viewModelProvider.get(PrinterWidgetViewModel.class);\n\n        formEntryViewModel.getCurrentIndex().observe(this, index -> {\n            if (index != null) {\n                formIndexAnimationHandler.handle(index.getScreenIndex());\n            }\n        });\n\n        formEntryViewModel.isLoading().observe(this, isLoading -> {\n            findViewById(R.id.loading_screen).setVisibility(isLoading ? View.VISIBLE : View.GONE);\n        });\n\n        formEntryViewModel.getError().observe(this, error -> {\n            if (error instanceof FormError.NonFatal) {\n                createErrorDialog(error);\n                formEntryViewModel.errorDisplayed();\n            }\n        });\n\n        formSaveViewModel = viewModelProvider.get(FormSaveViewModel.class);\n        formSaveViewModel.getSaveResult().observe(this, this::handleSaveResult);\n        formSaveViewModel.isSavingAnswerFile().observe(this, isSavingAnswerFile -> {\n            if (isSavingAnswerFile) {\n                DialogFragmentUtils.showIfNotShowing(SaveAnswerFileProgressDialogFragment.class, getSupportFragmentManager());\n            } else {\n                DialogFragmentUtils.dismissDialog(SaveAnswerFileProgressDialogFragment.class, getSupportFragmentManager());\n            }\n        });\n\n        formSaveViewModel.getAnswerFileError().observe(this, file -> {\n            if (file != null) {\n                DialogFragmentUtils.showIfNotShowing(SaveAnswerFileErrorDialogFragment.class, getSupportFragmentManager());\n            }\n        });\n\n        formEndViewModel = viewModelProvider.get(FormEndViewModel.class);\n        mediaWidgetAnswerViewModel = viewModelProvider.get(MediaWidgetAnswerViewModel.class);\n\n        internalRecordingRequester = new InternalRecordingRequester(this, audioRecorder, permissionsProvider);\n\n        waitingForDataRegistry = new FormControllerWaitingForDataRegistry(this::getFormController);\n        externalAppRecordingRequester = new ExternalAppRecordingRequester(this, intentLauncher, waitingForDataRegistry, permissionsProvider);\n\n        RecordingHandler recordingHandler = new RecordingHandler(formSaveViewModel, this, audioRecorder, new AMRAppender(), new M4AAppender());\n        audioRecorder.getCurrentSession().observe(this, session -> {\n            if (session != null && session.getFile() != null) {\n                recordingHandler.handle(getFormController(), session, file -> {\n                    if (file != null) {\n                        if (session.getId() instanceof FormIndex) {\n                            waitingForDataRegistry.waitForData((FormIndex) session.getId());\n                            setWidgetData(file);\n                            session.getFile().delete();\n                        }\n                        formSaveViewModel.resumeSave();\n                    } else {\n                        String path = session.getFile().getAbsolutePath();\n                        String message = getString(org.odk.collect.strings.R.string.answer_file_copy_failed_message, path);\n                        Toast.makeText(this, message, Toast.LENGTH_LONG).show();\n                    }\n                });\n            }\n        });\n    }\n\n    private void handleValidationResult(ODKView view, ValidationResult validationResult) {\n        if (validationResult instanceof FailedValidationResult failedValidationResult) {\n            String errorMessage = failedValidationResult.getCustomErrorMessage();\n            if (errorMessage == null) {\n                errorMessage = getString(failedValidationResult.getDefaultErrorMessage());\n            }\n            view.setErrorForQuestionWithIndex(failedValidationResult.getIndex(), errorMessage);\n            swipeHandler.setBeenSwiped(false);\n        } else if (validationResult instanceof SuccessValidationResult) {\n            SnackbarUtils.showSnackbar(\n                    findViewById(R.id.llParent),\n                    getString(org.odk.collect.strings.R.string.success_form_validation),\n                    SnackbarUtils.DURATION_LONG,\n                    findViewById(R.id.buttonholder)\n            );\n        }\n    }\n\n    private void formControllerAvailable(@NonNull FormController formController, @NonNull Form form, @Nullable Instance instance) {\n        formSessionRepository.set(sessionId, formController, form, instance);\n        AnalyticsUtils.setForm(formController);\n        backgroundLocationViewModel.formFinishedLoading();\n    }\n\n    private void setupFields(Bundle savedInstanceState) {\n        if (savedInstanceState != null) {\n            if (savedInstanceState.containsKey(KEY_XPATH)) {\n                startingXPath = savedInstanceState.getString(KEY_XPATH);\n                Timber.i(\"startingXPath is: %s\", startingXPath);\n            }\n            if (savedInstanceState.containsKey(KEY_XPATH_WAITING_FOR_DATA)) {\n                waitingXPath = savedInstanceState\n                        .getString(KEY_XPATH_WAITING_FOR_DATA);\n                Timber.i(\"waitingXPath is: %s\", waitingXPath);\n            }\n            if (savedInstanceState.containsKey(NEWFORM)) {\n                newForm = savedInstanceState.getBoolean(NEWFORM, true);\n            }\n            if (savedInstanceState.containsKey(KEY_ERROR)) {\n                formError = savedInstanceState.getParcelable(KEY_ERROR);\n            }\n            if (savedInstanceState.containsKey(KEY_AUTO_SAVED)) {\n                autoSaved = savedInstanceState.getBoolean(KEY_AUTO_SAVED);\n            }\n            if (savedInstanceState.containsKey(KEY_LOCATION_PERMISSIONS_GRANTED)) {\n                locationPermissionsPreviouslyGranted = savedInstanceState.getBoolean(KEY_LOCATION_PERMISSIONS_GRANTED);\n            }\n        }\n    }\n\n    private void loadForm() {\n        propertyManager.reload();\n        allowMovingBackwards = settingsProvider.getProtectedSettings().getBoolean(KEY_MOVING_BACKWARDS);\n\n        // If a parse error message is showing then nothing else is loaded\n        // Dialogs mid form just disappear on rotation.\n        if (formError instanceof FormError.Fatal) {\n            createErrorDialog(formError);\n            return;\n        }\n\n        // Check to see if this is a screen flip or a new form load.\n        Object data = getLastCustomNonConfigurationInstance();\n        if (data instanceof FormLoaderTask) {\n            formLoaderTask = (FormLoaderTask) data;\n        } else if (data == null) {\n            if (!newForm) {\n                FormController formController = getFormController();\n\n                if (formController != null) {\n                    activityDisplayed();\n                    formEntryViewModel.refreshSync();\n                } else {\n                    Timber.w(\"Reloading form and restoring state.\");\n                    loadFromIntent(getIntent());\n                }\n\n                return;\n            }\n\n            Intent intent = getIntent();\n            if (intent != null) {\n                loadFromIntent(intent);\n            }\n        }\n    }\n\n    private void loadFromIntent(Intent intent) {\n        Uri uri = intent.getData();\n        String uriMimeType = null;\n\n        if (uri != null) {\n            uriMimeType = getContentResolver().getType(uri);\n        }\n\n        formLoaderTask = new FormLoaderTask(uri, uriMimeType, startingXPath, waitingXPath, formEntryControllerFactory, scheduler, savepointsRepositoryProvider.create());\n        formLoaderTask.setFormLoaderListener(this);\n        showIfNotShowing(FormLoadingDialogFragment.class, getSupportFragmentManager());\n        formLoaderTask.execute();\n    }\n\n\n    private void initToolbar() {\n        Toolbar toolbar = findViewById(org.odk.collect.androidshared.R.id.toolbar);\n        setSupportActionBar(toolbar);\n        setTitle(getString(org.odk.collect.strings.R.string.loading_form));\n    }\n\n    /**\n     * Creates save-points asynchronously in order to not affect swiping performance on larger forms.\n     * If moving backwards through a form is disabled, also saves the index of the form element that\n     * was last shown to the user so that no matter how the app exits and relaunches, the user can't\n     * see previous questions.\n     */\n    private void nonblockingCreateSavePointData() {\n        try {\n            Long formDbId = formSessionRepository.get(sessionId).getValue().getForm().getDbId();\n            Long instanceDbId = null;\n            Instance instance = formSessionRepository.get(sessionId).getValue().getInstance();\n            if (instance != null) {\n                instanceDbId = instance.getDbId();\n            }\n            SavepointTask savePointTask = new SavepointTask(\n                    this,\n                    getFormController(),\n                    formDbId,\n                    instanceDbId,\n                    storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE),\n                    savepointsRepositoryProvider.create(),\n                    scheduler\n            );\n            savePointTask.execute();\n\n            if (!allowMovingBackwards) {\n                FormController formController = getFormController();\n                if (formController != null) {\n                    new SaveFormIndexTask(this, formController.getFormIndex(), formController.getInstanceFile()).execute();\n                }\n            }\n        } catch (Exception e) {\n            Timber.e(new Error(\"Could not schedule SavePointTask. Perhaps a lot of swiping is taking place?\"));\n        }\n    }\n\n    // This method may return null if called before form loading is finished\n    @Nullable\n    private FormController getFormController() {\n        return formEntryViewModel.getFormController();\n    }\n\n    @Override\n    protected void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n\n        outState.putString(KEY_SESSION_ID, sessionId);\n\n        FormController formController = getFormController();\n        if (formController != null) {\n            outState.putString(KEY_XPATH,\n                    formController.getXPath(formController.getFormIndex()));\n            FormIndex waiting = formController.getIndexWaitingForData();\n            if (waiting != null) {\n                outState.putString(KEY_XPATH_WAITING_FOR_DATA,\n                        formController.getXPath(waiting));\n            }\n\n            // make sure we're not already saving to disk. if we are, currentPrompt\n            // is getting constantly updated\n            if (!formSaveViewModel.isSaving()) {\n                if (currentView != null && formController != null\n                        && formController.currentPromptIsQuestion()) {\n\n                    // Update answers before creating save point\n                    formEntryViewModel.saveScreenAnswersToFormController(getAnswers(), false);\n                }\n            }\n\n            // save the instance to a temp path...\n            nonblockingCreateSavePointData();\n        }\n        outState.putBoolean(NEWFORM, false);\n        outState.putParcelable(KEY_ERROR, formError);\n        outState.putBoolean(KEY_AUTO_SAVED, autoSaved);\n        outState.putBoolean(KEY_LOCATION_PERMISSIONS_GRANTED, locationPermissionsPreviouslyGranted);\n\n        ProcessRestoreDetector.registerOnSaveInstanceState(this, outState);\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, final Intent intent) {\n        super.onActivityResult(requestCode, resultCode, intent);\n        // If we're coming back from the hierarchy view, the user has either tapped the back\n        // button or another question to jump to so we need to rebuild the view.\n        if (requestCode == RequestCodes.HIERARCHY_ACTIVITY || requestCode == RequestCodes.CHANGE_SETTINGS) {\n            activityDisplayed();\n            formEntryViewModel.refreshSync();\n            return;\n        }\n\n        FormController formController = getFormController();\n        if (formController == null) {\n            // we must be in the midst of a reload of the FormController.\n            // try to save this callback data to the FormLoaderTask\n            if (formLoaderTask != null\n                    && formLoaderTask.getStatus() != AsyncTask.Status.FINISHED) {\n                formLoaderTask.setActivityResult(requestCode, resultCode, intent);\n            } else {\n                Timber.e(new Error(\"Got an activityResult without any pending form loader\"));\n            }\n            return;\n        }\n\n        if (resultCode == RESULT_CANCELED) {\n            waitingForDataRegistry.cancelWaitingForData();\n            return;\n        }\n\n        // intent is needed for all requestCodes except of DRAW_IMAGE, ANNOTATE_IMAGE, SIGNATURE_CAPTURE, IMAGE_CAPTURE and HIERARCHY_ACTIVITY\n        if (intent == null && requestCode != RequestCodes.DRAW_IMAGE && requestCode != RequestCodes.ANNOTATE_IMAGE\n                && requestCode != RequestCodes.SIGNATURE_CAPTURE && requestCode != RequestCodes.IMAGE_CAPTURE) {\n            Timber.d(\"The intent has a null value for requestCode: %s\", requestCode);\n            showLongToast(getString(org.odk.collect.strings.R.string.null_intent_value));\n            return;\n        }\n\n        switch (requestCode) {\n            case RequestCodes.OSM_CAPTURE:\n                setWidgetData(intent.getStringExtra(\"OSM_FILE_NAME\"));\n                break;\n            case RequestCodes.EX_ARBITRARY_FILE_CHOOSER:\n            case RequestCodes.EX_VIDEO_CHOOSER:\n            case RequestCodes.EX_IMAGE_CHOOSER:\n            case RequestCodes.EX_AUDIO_CHOOSER:\n                if (intent.getClipData() != null\n                        && intent.getClipData().getItemCount() > 0\n                        && intent.getClipData().getItemAt(0) != null) {\n                    loadMedia(intent.getClipData().getItemAt(0).getUri());\n                } else {\n                    setWidgetData(null);\n                }\n                break;\n            case RequestCodes.EX_GROUP_CAPTURE:\n                try {\n                    Bundle extras = intent.getExtras();\n                    if (getCurrentViewIfODKView() != null) {\n                        getCurrentViewIfODKView().setDataForFields(extras);\n                    }\n                } catch (JavaRosaException e) {\n                    Timber.e(e);\n                    createErrorDialog(new FormError.NonFatal(e.getCause().getMessage()));\n                }\n                break;\n            case RequestCodes.DRAW_IMAGE:\n            case RequestCodes.ANNOTATE_IMAGE:\n            case RequestCodes.SIGNATURE_CAPTURE:\n            case RequestCodes.IMAGE_CAPTURE:\n                loadMedia(Uri.fromFile(new File(storagePathProvider.getTmpImageFilePath())));\n                break;\n            case RequestCodes.ALIGNED_IMAGE:\n            case RequestCodes.ARBITRARY_FILE_CHOOSER:\n            case RequestCodes.AUDIO_CAPTURE:\n            case RequestCodes.AUDIO_CHOOSER:\n            case RequestCodes.VIDEO_CAPTURE:\n            case RequestCodes.VIDEO_CHOOSER:\n            case RequestCodes.IMAGE_CHOOSER:\n                loadMedia(intent.getData());\n                break;\n            case RequestCodes.LOCATION_CAPTURE:\n            case RequestCodes.BEARING_CAPTURE:\n            case RequestCodes.BARCODE_CAPTURE:\n            case RequestCodes.EX_STRING_CAPTURE:\n            case RequestCodes.EX_INT_CAPTURE:\n            case RequestCodes.EX_DECIMAL_CAPTURE:\n                setWidgetData(ExternalAppUtils.getReturnedSingleValue(intent));\n                break;\n            case RequestCodes.MEDIA_FILE_PATH:\n                loadMedia(Uri.fromFile(new File((String) ExternalAppUtils.getReturnedSingleValue(intent))));\n                break;\n        }\n    }\n\n    private void loadMedia(Uri uri) {\n        permissionsProvider.requestReadUriPermission(this, uri, getContentResolver(), () -> {\n            MaterialProgressDialogFragment progressDialog = new MaterialProgressDialogFragment();\n            progressDialog.setMessage(getString(org.odk.collect.strings.R.string.please_wait));\n            DialogFragmentUtils.showIfNotShowing(progressDialog, TAG_PROGRESS_DIALOG_MEDIA_LOADING, getSupportFragmentManager());\n\n            mediaLoadingFragment.beginMediaLoadingTask(uri, getFormController());\n        });\n    }\n\n    public QuestionWidget getWidgetWaitingForBinaryData() {\n        ODKView odkView = getCurrentViewIfODKView();\n\n        if (odkView != null) {\n            for (QuestionWidget qw : odkView.getWidgets()) {\n                if (waitingForDataRegistry.isWaitingForData(qw.getFormEntryPrompt().getIndex())) {\n                    return qw;\n                }\n            }\n        } else {\n            Timber.e(new Error(\"currentView returned null.\"));\n        }\n        return null;\n    }\n\n    public void setWidgetData(Object data) {\n        ODKView currentViewIfODKView = getCurrentViewIfODKView();\n\n        if (currentViewIfODKView != null) {\n            boolean set = false;\n            for (QuestionWidget widget : currentViewIfODKView.getWidgets()) {\n                if (widget instanceof WidgetDataReceiver) {\n                    if (waitingForDataRegistry.isWaitingForData(widget.getFormEntryPrompt().getIndex())) {\n                        try {\n                            ((WidgetDataReceiver) widget).setData(data);\n                            waitingForDataRegistry.cancelWaitingForData();\n                        } catch (Exception e) {\n                            Timber.e(e);\n                            ToastUtils.showLongToast(currentViewIfODKView.getContext().getString(org.odk.collect.strings.R.string.error_attaching_binary_file,\n                                    e.getMessage()));\n                        }\n                        set = true;\n                        break;\n                    }\n                }\n            }\n\n            if (!set) {\n                Timber.e(new Error(\"Attempting to return data to a widget or set of widgets not looking for data\"));\n            }\n        }\n    }\n\n    /**\n     * Clears the answer on the screen.\n     */\n    private void clearAnswer(QuestionWidget qw) {\n        if (qw instanceof GeoTraceWidget || qw instanceof GeoShapeWidget) {\n            formEntryViewModel.answerQuestion(qw.getFormEntryPrompt().getIndex(), null);\n        } else if (qw.getAnswer() != null || qw instanceof DateTimeWidget) {\n            qw.clearAnswer();\n        }\n    }\n\n    @Override\n    public void onCreateContextMenu(ContextMenu menu, View v,\n                                    ContextMenuInfo menuInfo) {\n        if (!swipeHandler.beenSwiped()) {\n            super.onCreateContextMenu(menu, v, menuInfo);\n            FormController formController = getFormController();\n\n            menu.add(0, v.getId(), 0, getString(org.odk.collect.strings.R.string.clear_answer));\n            if (formController.indexContainsRepeatableGroup()) {\n                menu.add(0, DELETE_REPEAT, 0, getString(org.odk.collect.strings.R.string.delete_repeat));\n            }\n            menu.setHeaderTitle(getString(org.odk.collect.strings.R.string.edit_prompt));\n        }\n    }\n\n    @Override\n    public boolean onContextItemSelected(MenuItem item) {\n        if (item.getItemId() == DELETE_REPEAT) {\n            DialogFragmentUtils.showIfNotShowing(DeleteRepeatDialogFragment.class, getSupportFragmentManager());\n        } else {\n            ODKView odkView = getCurrentViewIfODKView();\n            if (odkView != null) {\n                for (QuestionWidget qw : odkView.getWidgets()) {\n                    if (item.getItemId() == qw.getId()) {\n                        createClearDialog(qw);\n                        break;\n                    }\n                }\n            }\n        }\n\n        return super.onContextItemSelected(item);\n    }\n\n    public void deleteGroup() {\n        FormController formController = getFormController();\n        if (formController != null && !formController.indexIsInFieldList()) {\n            swipeHandler.setBeenSwiped(true);\n            onSwipeForward();\n        } else {\n            onScreenRefresh(false);\n        }\n    }\n\n    /**\n     * If we're loading, then we pass the loading thread to our next instance.\n     */\n    @Override\n    public Object onRetainCustomNonConfigurationInstance() {\n        // if a form is loading, pass the loader task\n        if (formLoaderTask != null\n                && formLoaderTask.getStatus() != AsyncTask.Status.FINISHED) {\n            return formLoaderTask;\n        }\n\n        return null;\n    }\n\n    /**\n     * Creates and returns a new view based on the event type passed in. The view returned is\n     * of type {@link View} if the event passed in represents the end of the form or of type\n     * {@link ODKView} otherwise.\n     *\n     * @param advancingPage -- true if this results from advancing through the form\n     * @return newly created View\n     */\n    private SwipeHandler.View createView(int event, boolean advancingPage) {\n        releaseOdkView();\n\n        FormController formController = getFormController();\n\n        String formTitle = formController.getFormTitle();\n        setTitle(formTitle);\n\n        if (event != FormEntryController.EVENT_QUESTION) {\n            formController.getAuditEventLogger().logEvent(AuditEvent.getAuditEventTypeFromFecType(event),\n                    formController.getFormIndex(), true, null, System.currentTimeMillis(), null);\n        }\n\n        switch (event) {\n            case FormEntryController.EVENT_END_OF_FORM:\n                return createViewForFormEnd(formController);\n            case FormEntryController.EVENT_QUESTION:\n            case FormEntryController.EVENT_GROUP:\n            case FormEntryController.EVENT_REPEAT:\n                // should only be a group here if the event_group is a field-list\n                try {\n                    FormEntryCaption[] groups = getGroupsForCurrentIndex(formController);\n                    FormEntryPrompt[] prompts = getQuestionPrompts(formController);\n\n                    odkView = createODKView(advancingPage, prompts, groups);\n                    odkView.setWidgetValueChangedListener(this);\n                    Timber.i(\"Created view for group %s %s\",\n                            groups.length > 0 ? groups[groups.length - 1].getLongText() : \"[top]\",\n                            prompts.length > 0 ? prompts[0].getQuestionText() : \"[no question]\");\n                } catch (RuntimeException | RepeatsInFieldListException e) {\n                    if (e instanceof RuntimeException) {\n                        Timber.e(e);\n                    }\n                    // this is badness to avoid a crash.\n                    try {\n                        event = formController.stepToNextScreenEvent();\n                        createErrorDialog(new FormError.NonFatal(e.getMessage()));\n                    } catch (JavaRosaException e1) {\n                        Timber.d(e1);\n                        createErrorDialog(new FormError.NonFatal(e.getMessage() + \"\\n\\n\" + e1.getCause().getMessage()));\n                    }\n                    return createView(event, advancingPage);\n                }\n\n                if (showNavigationButtons) {\n                    updateNavigationButtonVisibility();\n                }\n\n                return odkView;\n\n            case EVENT_PROMPT_NEW_REPEAT:\n                createRepeatDialog();\n                return new EmptyView(this);\n\n            default:\n                Timber.e(new Error(\"Attempted to create a view that does not exist.\"));\n                // this is badness to avoid a crash.\n                try {\n                    event = formController.stepToNextScreenEvent();\n                    createErrorDialog(new FormError.Fatal(getString(org.odk.collect.strings.R.string.survey_internal_error)));\n                } catch (JavaRosaException e) {\n                    Timber.d(e);\n                    createErrorDialog(new FormError.Fatal(e.getCause().getMessage()));\n                }\n                return createView(event, advancingPage);\n        }\n    }\n\n    @NotNull\n    private ODKView createODKView(boolean advancingPage, FormEntryPrompt[] prompts, FormEntryCaption[] groups) {\n        odkViewLifecycle = new ControllableLifecyleOwner();\n        odkViewLifecycle.start();\n        AudioPlayer audioPlayer = audioPlayerFactory.create(this, odkViewLifecycle);\n        audioPlayer.isLoading().observe(odkViewLifecycle, (isLoading) -> {\n            findViewById(R.id.loading_screen).setVisibility(isLoading ? View.VISIBLE : View.GONE);\n        });\n\n        return new ODKView(this, prompts, groups, advancingPage, formSaveViewModel, waitingForDataRegistry, audioPlayer, audioRecorder, formEntryViewModel, printerWidgetViewModel, internalRecordingRequester, externalAppRecordingRequester, odkViewLifecycle, mediaWidgetAnswerViewModel);\n    }\n\n    private void releaseOdkView() {\n        if (odkViewLifecycle != null) {\n            odkViewLifecycle.destroy();\n        }\n\n        if (odkView != null) {\n            odkView = null;\n        }\n    }\n\n    /**\n     * Creates the final screen in a form-filling interaction. Allows the user to set a display\n     * name for the instance and to decide whether the form should be finalized or not. Presents\n     * a button for saving and exiting.\n     */\n    private SwipeHandler.View createViewForFormEnd(FormController formController) {\n        String userVisibleInstanceName = null;\n        if (formController.getSubmissionMetadata().instanceName != null) {\n            saveName = formController.getSubmissionMetadata().instanceName;\n        } else {\n            // no meta/instanceName field in the form -- see if we have a\n            // name for this instance from a previous save attempt...\n            String uriMimeType = null;\n            Uri instanceUri = getIntent().getData();\n            if (instanceUri != null) {\n                uriMimeType = getContentResolver().getType(instanceUri);\n            }\n\n            if (saveName == null && uriMimeType != null\n                    && uriMimeType.equals(InstancesContract.CONTENT_ITEM_TYPE)) {\n                Instance instance = new InstancesRepositoryProvider(Collect.getInstance()).create().get(ContentUriHelper.getIdFromUri(instanceUri));\n                if (instance != null) {\n                    saveName = instance.getDisplayName();\n                    userVisibleInstanceName = InstanceExtKt.userVisibleInstanceName(instance, getResources());\n                }\n            }\n\n            if (saveName == null) {\n                saveName = formSaveViewModel.getFormName();\n            }\n        }\n\n        if (showNavigationButtons) {\n            updateNavigationButtonVisibility();\n        }\n\n        return new FormEndView(\n                this,\n                userVisibleInstanceName != null ? userVisibleInstanceName : saveName,\n                formEntryViewModel.isFormEditableAfterFinalization(),\n                formEndViewModel,\n                markAsFinalized -> saveForm(true, markAsFinalized, saveName, false)\n        );\n    }\n\n    @Override\n    public boolean dispatchTouchEvent(MotionEvent mv) {\n        boolean handled = swipeHandler.getGestureDetector().onTouchEvent(mv);\n        if (!handled) {\n            return super.dispatchTouchEvent(mv);\n        }\n\n        return handled; // this is always true\n    }\n\n    @Override\n    public void onSwipeForward() {\n        moveScreen(FORWARDS);\n    }\n\n    @Override\n    public void onSwipeBackward() {\n        moveScreen(BACKWARDS);\n    }\n\n    private void moveScreen(Direction direction) {\n        if (currentView != null) {\n            currentView.cancelPendingInputEvents();\n        }\n\n        closeContextMenu();\n        FormController formController = getFormController();\n        if (formController == null) {\n            Timber.d(\"FormController has a null value\");\n            swipeHandler.setBeenSwiped(false);\n            return;\n        }\n\n        NavigationWarning navigationWarning = odkView != null ? odkView.isNavigationBlocked() : null;\n        if (navigationWarning != null) {\n            DialogUtils.show(this, navigationWarning.getTitleRes(), navigationWarning.getMessageRes());\n            swipeHandler.setBeenSwiped(false);\n            return;\n        }\n\n        if (audioRecorder.isRecording() && !backgroundAudioViewModel.isBackgroundRecording()) {\n            // We want the user to stop recording before changing screens\n            DialogFragmentUtils.showIfNotShowing(RecordingWarningDialogFragment.class, getSupportFragmentManager());\n            swipeHandler.setBeenSwiped(false);\n            return;\n        }\n\n        if (direction == FORWARDS) {\n            if (formController.getEvent() == FormEntryController.EVENT_END_OF_FORM) {\n                swipeHandler.setBeenSwiped(false);\n                return;\n            }\n\n            if (formController.currentPromptIsQuestion()) {\n                // get constraint behavior preference value with appropriate default\n                String constraintBehavior = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR);\n                formEntryViewModel.moveForward(getAnswers(), constraintBehavior.equals(ProjectKeys.CONSTRAINT_BEHAVIOR_ON_SWIPE));\n            } else {\n                formEntryViewModel.moveForward(getAnswers());\n            }\n        } else {\n            if (formController.isCurrentQuestionFirstInForm() || !allowMovingBackwards) {\n                swipeHandler.setBeenSwiped(false);\n                return;\n            }\n\n            formEntryViewModel.moveBackward(getAnswers());\n        }\n    }\n\n    @Override\n    public void onScreenChange(Direction direction) {\n        final int event = getFormController().getEvent();\n\n        switch (direction) {\n            case FORWARDS:\n                animateToNextView(event);\n                break;\n            case BACKWARDS:\n                if (event == FormEntryController.EVENT_GROUP || event == FormEntryController.EVENT_QUESTION) {\n                    // create savepoint\n                    nonblockingCreateSavePointData();\n                }\n\n                animateToPreviousView(event);\n                break;\n        }\n    }\n\n    /**\n     * Rebuilds the current view. the controller and the displayed view can get\n     * out of sync due to dialogs and restarts caused by screen orientation\n     * changes, so they're resynchronized here.\n     */\n    @Override\n    public void onScreenRefresh(boolean isFormStart) {\n        int event = getFormController().getEvent();\n\n        SwipeHandler.View current = createView(event, isFormStart);\n        showView(current, FormAnimationType.FADE);\n\n        formIndexAnimationHandler.setLastIndex(getFormController().getFormIndex());\n    }\n\n    private void animateToNextView(int event) {\n        switch (event) {\n            case FormEntryController.EVENT_QUESTION:\n            case FormEntryController.EVENT_GROUP:\n                // create a savepoint\n                nonblockingCreateSavePointData();\n                showView(createView(event, true), FormAnimationType.RIGHT);\n                break;\n            case FormEntryController.EVENT_END_OF_FORM:\n            case FormEntryController.EVENT_REPEAT:\n            case EVENT_PROMPT_NEW_REPEAT:\n                showView(createView(event, true), FormAnimationType.RIGHT);\n                break;\n            case FormEntryController.EVENT_REPEAT_JUNCTURE:\n                Timber.i(\"Repeat juncture: %s\", getFormController().getFormIndex().getReference());\n                // skip repeat junctures until we implement them\n                break;\n            default:\n                Timber.d(\"JavaRosa added a new EVENT type and didn't tell us... shame on them.\");\n                break;\n        }\n    }\n\n    private void animateToPreviousView(int event) {\n        SwipeHandler.View next = createView(event, false);\n        showView(next, FormAnimationType.LEFT);\n    }\n\n    /**\n     * Displays the View specified by the parameter 'next', animating both the\n     * current view and next appropriately given the AnimationType. Also updates\n     * the progress bar.\n     */\n    public void showView(SwipeHandler.View next, FormAnimationType from) {\n        invalidateOptionsMenu();\n\n        // disable notifications...\n        if (inAnimation != null) {\n            inAnimation.setAnimationListener(null);\n        }\n        if (outAnimation != null) {\n            outAnimation.setAnimationListener(null);\n        }\n\n        // logging of the view being shown is already done, as this was handled\n        // by createView()\n        switch (FormAnimation.getAnimationTypeBasedOnLanguageDirection(this, from)) {\n            case RIGHT:\n                inAnimation = loadAnimation(this,\n                        R.anim.push_left_in);\n                outAnimation = loadAnimation(this,\n                        R.anim.push_left_out);\n                // if animation is left or right then it was a swipe, and we want to re-save on\n                // entry\n                autoSaved = false;\n                break;\n            case LEFT:\n                inAnimation = loadAnimation(this,\n                        R.anim.push_right_in);\n                outAnimation = loadAnimation(this,\n                        R.anim.push_right_out);\n                autoSaved = false;\n                break;\n            case FADE:\n                inAnimation = loadAnimation(this, R.anim.fade_in);\n                outAnimation = loadAnimation(this, R.anim.fade_out);\n                break;\n        }\n\n        // complete setup for animations...\n        inAnimation.setAnimationListener(this);\n        outAnimation.setAnimationListener(this);\n\n        if (!areAnimationsEnabled(this)) {\n            inAnimation.setDuration(0);\n            outAnimation.setDuration(0);\n        }\n\n        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(\n                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);\n\n        // adjust which view is in the layout container...\n        SwipeHandler.View staleView = currentView;\n        currentView = next;\n        swipeHandler.setView(currentView);\n        questionHolder.addView(currentView, lp);\n        animationCompletionSet = 0;\n\n        if (staleView != null) {\n            // start OutAnimation for transition...\n            staleView.startAnimation(outAnimation);\n            // and remove the old view (MUST occur after start of animation!!!)\n            questionHolder.removeView(staleView);\n        } else {\n            animationCompletionSet = 2;\n        }\n        appBarLayout.setLiftOnScrollTargetViewId(R.id.odk_view_container);\n        // start InAnimation for transition...\n        currentView.startAnimation(inAnimation);\n\n        FormController formController = getFormController();\n        if (formController.getEvent() == FormEntryController.EVENT_QUESTION\n                || formController.getEvent() == FormEntryController.EVENT_GROUP\n                || formController.getEvent() == FormEntryController.EVENT_REPEAT) {\n\n            try {\n                FormEntryPrompt[] prompts = getQuestionPrompts(getFormController());\n                for (FormEntryPrompt p : prompts) {\n                    List<TreeElement> attrs = p.getBindAttributes();\n                    for (int i = 0; i < attrs.size(); i++) {\n                        if (!autoSaved && \"saveIncomplete\".equals(attrs.get(i).getName())) {\n                            saveForm(false, false, null, false);\n                            autoSaved = true;\n                        }\n                    }\n                }\n            } catch (RepeatsInFieldListException e) {\n                createErrorDialog(new FormError.NonFatal(e.getMessage()));\n            }\n        }\n    }\n\n    /**\n     * Creates and displays a dialog asking the user if they'd like to create a\n     * repeat of the current group.\n     */\n    private void createRepeatDialog() {\n        swipeHandler.setBeenSwiped(true);\n\n        // In some cases dialog might be present twice because refreshView() is being called\n        // from onResume(). This ensures that we do not preset this modal dialog if it's already\n        // visible. Checking for shownAlertDialogIsGroupRepeat because the same field\n        // alertDialog is being used for all alert dialogs in this activity.\n        if (shownAlertDialogIsGroupRepeat) {\n            return;\n        }\n\n        shownAlertDialogIsGroupRepeat = true;\n\n        AddRepeatDialog.show(this, getFormController().getLastGroupText(), new AddRepeatDialog.Listener() {\n            @Override\n            public void onAddRepeatClicked() {\n                swipeHandler.setBeenSwiped(false);\n                shownAlertDialogIsGroupRepeat = false;\n                formEntryViewModel.addRepeat();\n            }\n\n            @Override\n            public void onCancelClicked() {\n                swipeHandler.setBeenSwiped(false);\n                shownAlertDialogIsGroupRepeat = false;\n\n                // Make sure the error dialog will not disappear.\n                //\n                // When showNextView() popups an error dialog (because of a\n                // JavaRosaException)\n                // the issue is that the \"add new repeat dialog\" is referenced by\n                // alertDialog\n                // like the error dialog. When the \"no repeat\" is clicked, the error dialog\n                // is shown. Android by default dismisses the dialogs when a button is\n                // clicked,\n                // so instead of closing the first dialog, it closes the second.\n                new Thread() {\n                    @Override\n                    public void run() {\n                        FormFillingActivity.this.runOnUiThread(() -> {\n                            try {\n                                Thread.sleep(500);\n                            } catch (InterruptedException e) {\n                                //This is rare\n                                Timber.e(e);\n                            }\n\n                            formEntryViewModel.cancelRepeatPrompt();\n                        });\n                    }\n                }.start();\n            }\n        });\n    }\n\n    /**\n     * Creates and displays dialog with the given errorMsg.\n     */\n    private void createErrorDialog(FormError error) {\n        formError = error;\n\n        alertDialog = new MaterialAlertDialogBuilder(this).create();\n        if (formError instanceof FormError.Fatal) {\n            alertDialog.setTitle(getString(org.odk.collect.strings.R.string.form_cannot_be_used));\n        } else {\n            alertDialog.setTitle(getString(org.odk.collect.strings.R.string.error_occured));\n        }\n        alertDialog.setMessage(formError.getMessage());\n        DialogInterface.OnClickListener errorListener = new DialogInterface.OnClickListener() {\n            @Override\n            public void onClick(DialogInterface dialog, int i) {\n                switch (i) {\n                    case BUTTON_POSITIVE:\n                        if (formError instanceof FormError.Fatal) {\n                            formError = null;\n                            exit();\n                        }\n                        break;\n                }\n            }\n        };\n        alertDialog.setCancelable(false);\n        alertDialog.setButton(BUTTON_POSITIVE, getString(org.odk.collect.strings.R.string.ok), errorListener);\n        swipeHandler.setBeenSwiped(false);\n        alertDialog.show();\n    }\n\n    /**\n     * Saves data and writes it to disk. If exit is set, program will exit after\n     * save completes. Complete indicates whether the user has marked the\n     * isntancs as complete. If updatedSaveName is non-null, the instances\n     * content provider is updated with the new name\n     */\n    public boolean saveForm(boolean exit, boolean complete, String updatedSaveName,\n                            boolean current) {\n        // save current answer\n        if (current) {\n            if (!formEntryViewModel.updateAnswersForScreen(getAnswers(), complete)) {\n                showShortToast(org.odk.collect.strings.R.string.data_saved_error);\n                return false;\n            }\n        }\n\n        formSaveViewModel.saveForm(getIntent().getData(), complete, updatedSaveName, exit);\n\n        return true;\n    }\n\n    private void handleSaveResult(FormSaveViewModel.SaveResult result) {\n        if (result == null) {\n            return;\n        }\n\n        switch (result.getState()) {\n            case CHANGE_REASON_REQUIRED:\n                showIfNotShowing(ChangesReasonPromptDialogFragment.class, getSupportFragmentManager());\n                break;\n\n            case SAVING:\n                autoSaved = true;\n                showIfNotShowing(SaveFormProgressDialogFragment.class, getSupportFragmentManager());\n                break;\n\n            case SAVED:\n                DialogFragmentUtils.dismissDialog(SaveFormProgressDialogFragment.class, getSupportFragmentManager());\n                DialogFragmentUtils.dismissDialog(ChangesReasonPromptDialogFragment.class, getSupportFragmentManager());\n\n                if (result.getRequest().viewExiting()) {\n                    finishAndReturnInstance();\n                } else {\n                    showShortToast(org.odk.collect.strings.R.string.data_saved_ok);\n                }\n\n                formSessionRepository.update(sessionId, formSaveViewModel.getInstance());\n                formSaveViewModel.resumeFormEntry();\n                break;\n\n            case SAVE_ERROR:\n                DialogFragmentUtils.dismissDialog(SaveFormProgressDialogFragment.class, getSupportFragmentManager());\n                DialogFragmentUtils.dismissDialog(ChangesReasonPromptDialogFragment.class, getSupportFragmentManager());\n\n                String message;\n\n                if (result.getMessage() != null) {\n                    message = getString(org.odk.collect.strings.R.string.data_saved_error) + \" \"\n                            + result.getMessage();\n                } else {\n                    message = getString(org.odk.collect.strings.R.string.data_saved_error);\n                }\n\n                showLongToast(message);\n                formSaveViewModel.resumeFormEntry();\n                break;\n\n            case FINALIZE_ERROR:\n                DialogFragmentUtils.dismissDialog(SaveFormProgressDialogFragment.class, getSupportFragmentManager());\n                DialogFragmentUtils.dismissDialog(ChangesReasonPromptDialogFragment.class, getSupportFragmentManager());\n\n                showLongToast(String.format(getString(org.odk.collect.strings.R.string.encryption_error_message),\n                        result.getMessage()));\n                finishAndReturnInstance();\n                formSaveViewModel.resumeFormEntry();\n                break;\n\n            case CONSTRAINT_ERROR: {\n                DialogFragmentUtils.dismissDialog(SaveFormProgressDialogFragment.class, getSupportFragmentManager());\n                DialogFragmentUtils.dismissDialog(ChangesReasonPromptDialogFragment.class, getSupportFragmentManager());\n\n                onScreenRefresh(false);\n\n                // get constraint behavior preference value with appropriate default\n                String constraintBehavior = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR);\n\n                // an answer constraint was violated, so we need to display the proper toast(s)\n                // if constraint behavior is on_swipe, this will happen if we do a 'swipe' to the\n                // next question\n                if (constraintBehavior.equals(ProjectKeys.CONSTRAINT_BEHAVIOR_ON_SWIPE)) {\n                    next();\n                } else {\n                    // otherwise, we can get the proper toast(s) by saving with constraint check\n                    formEntryViewModel.updateAnswersForScreen(getAnswers(), true);\n                }\n                formSaveViewModel.resumeFormEntry();\n                break;\n            }\n        }\n    }\n\n    @Nullable\n    private String getAbsoluteInstancePath() {\n        FormController formController = getFormController();\n        return formController != null ? formController.getAbsoluteInstancePath() : null;\n    }\n\n    /**\n     * Confirm clear answer dialog\n     */\n    private void createClearDialog(final QuestionWidget qw) {\n        alertDialog = new MaterialAlertDialogBuilder(this).create();\n        alertDialog.setTitle(getString(org.odk.collect.strings.R.string.clear_answer_ask));\n\n        String question = qw.getFormEntryPrompt().getLongText();\n        if (question == null) {\n            question = \"\";\n        }\n        if (question.length() > 50) {\n            question = question.substring(0, 50) + \"...\";\n        }\n\n        alertDialog.setMessage(getString(org.odk.collect.strings.R.string.clearanswer_confirm,\n                question));\n\n        DialogInterface.OnClickListener quitListener = new DialogInterface.OnClickListener() {\n\n            @Override\n            public void onClick(DialogInterface dialog, int i) {\n                switch (i) {\n                    case BUTTON_POSITIVE: // yes\n                        clearAnswer(qw);\n                        break;\n                }\n            }\n        };\n        alertDialog.setCancelable(false);\n        alertDialog\n                .setButton(BUTTON_POSITIVE, getString(org.odk.collect.strings.R.string.discard_answer), quitListener);\n        alertDialog.setButton(BUTTON_NEGATIVE, getString(org.odk.collect.strings.R.string.clear_answer_no),\n                quitListener);\n        alertDialog.show();\n    }\n\n    /**\n     * Creates and displays a dialog allowing the user to set the language for\n     * the form.\n     */\n    public void createLanguageDialog() {\n        FormController formController = getFormController();\n        final String[] languages = formController.getLanguages();\n        int selected = -1;\n        if (languages != null) {\n            String language = formController.getLanguage();\n            for (int i = 0; i < languages.length; i++) {\n                if (language.equals(languages[i])) {\n                    selected = i;\n                }\n            }\n        }\n        alertDialog = new MaterialAlertDialogBuilder(this)\n                .setSingleChoiceItems(languages, selected,\n                        (dialog, whichButton) -> {\n                            formEntryViewModel.changeLanguage(languages[whichButton]);\n                            formEntryViewModel.updateAnswersForScreen(getAnswers(), false);\n\n                            dialog.dismiss();\n                            onScreenRefresh(false);\n                        })\n                .setTitle(getString(org.odk.collect.strings.R.string.change_language))\n                .setNegativeButton(getString(org.odk.collect.strings.R.string.do_not_change), null).create();\n        alertDialog.show();\n    }\n\n    /**\n     * Shows the next or back button, neither or both. Both buttons are displayed unless:\n     * - we are at the first question in the form so the back button is hidden\n     * - we are at the end screen so the next button is hidden\n     * - settings prevent backwards navigation of the form so the back button is hidden\n     * <p>\n     * The visibility of the container for these buttons is determined once {@link #onResume()}.\n     */\n    private void updateNavigationButtonVisibility() {\n        FormController formController = getFormController();\n        if (formController == null) {\n            return;\n        }\n\n        backButton.setVisibility(!formController.isCurrentQuestionFirstInForm() && allowMovingBackwards ? View.VISIBLE : View.INVISIBLE);\n        nextButton.setVisibility(formController.getEvent() != FormEntryController.EVENT_END_OF_FORM ? View.VISIBLE : View.INVISIBLE);\n    }\n\n    @Override\n    protected void onStart() {\n        super.onStart();\n        FormController formController = getFormController();\n\n        // Register to receive location provider change updates and write them to the audit log\n        if (formController != null && formController.currentFormAuditsLocation()\n                && new PlayServicesChecker().isGooglePlayServicesAvailable(this)) {\n            registerReceiver(locationProvidersReceiver, new IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION));\n        }\n\n        // User may have changed location permissions in Android settings\n        if (permissionsProvider.areLocationPermissionsGranted() != locationPermissionsPreviouslyGranted) {\n            backgroundLocationViewModel.locationPermissionChanged();\n            locationPermissionsPreviouslyGranted = !locationPermissionsPreviouslyGranted;\n        }\n    }\n\n    @Override\n    protected void onPause() {\n        backgroundLocationViewModel.activityHidden();\n\n        super.onPause();\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n\n        activityDisplayed();\n\n        String navigation = settingsProvider.getUnprotectedSettings().getString(KEY_NAVIGATION);\n        showNavigationButtons = navigation.contains(ProjectKeys.NAVIGATION_BUTTONS);\n\n        findViewById(R.id.buttonholder).setVisibility(showNavigationButtons ? View.VISIBLE : View.GONE);\n\n        if (showNavigationButtons) {\n            updateNavigationButtonVisibility();\n        }\n\n        if (formError instanceof FormError.Fatal) {\n            if (alertDialog != null && !alertDialog.isShowing()) {\n                createErrorDialog(formError);\n            } else {\n                return;\n            }\n        }\n\n        FormController formController = getFormController();\n\n        if (formLoaderTask != null) {\n            formLoaderTask.setFormLoaderListener(this);\n            if (formController == null\n                    && formLoaderTask.getStatus() == AsyncTask.Status.FINISHED) {\n                FormController fec = formLoaderTask.getFormController();\n                if (fec != null) {\n                    loadingComplete(formLoaderTask, formLoaderTask.getFormDef(), null);\n                } else {\n                    throw new IllegalStateException(\"Null formController!\");\n                }\n            }\n        } else {\n            if (formController == null && !identityPromptViewModel.requiresIdentityToContinue().getValue()) {\n                throw new IllegalStateException(\"Null formController!\");\n            }\n        }\n    }\n\n    @Override\n    protected void onResumeFragments() {\n        super.onResumeFragments();\n\n        /*\n          Make sure the progress dialog is dismissed.\n          In most cases that dialog is dismissed in MediaLoadingTask#onPostExecute() but if the app\n          is in the background when MediaLoadingTask#onPostExecute() is called then the dialog\n          can not be dismissed. In such a case we need to make sure it's dismissed in order\n          to avoid blocking the UI.\n         */\n        if (!mediaLoadingFragment.isMediaLoadingTaskRunning()) {\n            DialogFragmentUtils.dismissDialog(TAG_PROGRESS_DIALOG_MEDIA_LOADING, getSupportFragmentManager());\n        }\n    }\n\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        switch (keyCode) {\n            case KeyEvent.KEYCODE_DPAD_RIGHT:\n                if (event.isAltPressed() && !swipeHandler.beenSwiped()) {\n                    swipeHandler.setBeenSwiped(true);\n                    onSwipeForward();\n                    return true;\n                }\n                break;\n            case KeyEvent.KEYCODE_DPAD_LEFT:\n                if (event.isAltPressed() && !swipeHandler.beenSwiped()) {\n                    swipeHandler.setBeenSwiped(true);\n                    onSwipeBackward();\n                    return true;\n                }\n                break;\n        }\n        return super.onKeyDown(keyCode, event);\n    }\n\n    @Override\n    protected void onDestroy() {\n        if (formLoaderTask != null) {\n            formLoaderTask.setFormLoaderListener(null);\n            // We have to call cancel to terminate the thread, otherwise it\n            // lives on and retains the FEC in memory.\n            // but only if it's done, otherwise the thread never returns\n            if (formLoaderTask.getStatus() == AsyncTask.Status.FINISHED) {\n                FormLoaderTask t = formLoaderTask;\n                formLoaderTask = null;\n                t.cancel();\n                t.destroy();\n            }\n        }\n\n        releaseOdkView();\n\n        try {\n            unregisterReceiver(locationProvidersReceiver);\n        } catch (IllegalArgumentException e) {\n            // This is the common case -- the form didn't have location audits enabled so the\n            // receiver was not registered.\n        }\n\n        super.onDestroy();\n    }\n\n    private int animationCompletionSet;\n\n    private void afterAllAnimations() {\n        ODKView view = getCurrentViewIfODKView();\n        if (view != null) {\n            CurrentFormIndex index = formEntryViewModel.getCurrentIndex().getValue();\n            ValidationResult validationResult = index.getValidationResult();\n            if (validationResult != null) {\n                handleValidationResult(view, validationResult);\n            } else if (index.getQuestionIndex() != null) {\n                view.focusToTopOf(index.getQuestionIndex());\n            } else {\n                view.setFocus(this);\n            }\n        }\n\n        swipeHandler.setBeenSwiped(false);\n    }\n\n    @Override\n    public void onAnimationEnd(Animation animation) {\n        if (inAnimation == animation) {\n            animationCompletionSet |= 1;\n        } else if (outAnimation == animation) {\n            animationCompletionSet |= 2;\n        } else {\n            Timber.e(new Error(\"Unexpected animation\"));\n        }\n\n        if (animationCompletionSet == 3) {\n            this.afterAllAnimations();\n        }\n    }\n\n    @Override\n    public void onAnimationRepeat(Animation animation) {\n    }\n\n    @Override\n    public void onAnimationStart(Animation animation) {\n    }\n\n    /**\n     * Given a {@link FormLoaderTask} which has created a {@link FormController} for either a new or\n     * existing instance, shows that instance to the user. Either launches {@link FormHierarchyFragmentHostActivity}\n     * if an existing instance is being edited or builds the view for the current question(s) if a\n     * new instance is being created.\n     * <p>\n     * May do some or all of these depending on current state:\n     * - Ensures phone state permissions are given if this form needs them\n     * - Cleans up {@link #formLoaderTask}\n     * - Sets the global form controller and database manager for search()/pulldata()\n     * - Restores the last-used language\n     * - Handles activity results that may have come in while the form was loading\n     * - Alerts the user of a recovery from savepoint\n     * - Verifies whether an instance folder exists and creates one if not\n     * - Initializes background location capture (only if the instance being loaded is a new one)\n     */\n    @Override\n    public void loadingComplete(FormLoaderTask task, FormDef formDef, String warningMsg) {\n        DialogFragmentUtils.dismissDialog(FormLoadingDialogFragment.class, getSupportFragmentManager());\n\n        final FormController formController = task.getFormController();\n        Instance instance = task.getInstance();\n        Form form = task.getForm();\n\n        if (formController != null) {\n            formLoaderTask.setFormLoaderListener(null);\n            FormLoaderTask t = formLoaderTask;\n            formLoaderTask = null;\n            t.cancel();\n            t.destroy();\n\n            // Set the language if one has already been set in the past\n            String[] languageTest = formController.getLanguages();\n            if (languageTest != null) {\n                String defaultLanguage = formController.getLanguage();\n                if (form != null) {\n                    String newLanguage = form.getLanguage();\n\n                    try {\n                        formController.setLanguage(newLanguage);\n                    } catch (Exception e) {\n                        // if somehow we end up with a bad language, set it to the default\n                        Timber.i(\"Ended up with a bad language. %s\", newLanguage);\n                        formController.setLanguage(defaultLanguage);\n                    }\n                }\n            }\n\n            // it can be a normal flow for a pending activity result to restore from a savepoint\n            // (the call flow handled by the above if statement). For all other use cases, the\n            // user should be notified, as it means they wandered off doing other things then\n            // returned to ODK Collect and chose Edit Saved Form, but that the savepoint for\n            // that form is newer than the last saved version of their form data.\n            boolean hasUsedSavepoint = task.hasUsedSavepoint();\n\n            if (hasUsedSavepoint) {\n                runOnUiThread(() -> showLongToast(org.odk.collect.strings.R.string.savepoint_used));\n            }\n\n            if (formController.getInstanceFile() == null) {\n                File instanceFile = LocalInstancesUseCases.createInstanceFile(\n                        form.getDisplayName(),\n                        storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES)\n                );\n                if (instanceFile != null) {\n                    formController.setInstanceFile(instanceFile);\n                } else {\n                    showFormLoadErrorAndExit(getString(org.odk.collect.strings.R.string.loading_form_failed));\n                }\n\n                identityPromptViewModel.formLoaded(formController);\n                identityPromptViewModel.requiresIdentityToContinue().observe(this, requiresIdentity -> {\n                    if (!requiresIdentity) {\n                        formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_START, true, System.currentTimeMillis());\n\n                        // Register to receive location provider change updates and write them to the audit\n                        // log. onStart has already run but the formController was null so try again.\n                        if (formController.currentFormAuditsLocation()\n                                && new PlayServicesChecker().isGooglePlayServicesAvailable(this)) {\n                            registerReceiver(locationProvidersReceiver, new IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION));\n                        }\n\n                        formControllerAvailable(formController, form, instance);\n\n                        // onResume ran before the form was loaded. Let the viewModel know that the activity\n                        // is about to be displayed and configured. Do this before the refresh actually\n                        // happens because if audit logging is enabled, the refresh logs a question event\n                        // and we want that to show up after initialization events.\n                        activityDisplayed();\n                        formEntryViewModel.refresh();\n\n                        if (warningMsg != null) {\n                            showLongToast(warningMsg);\n                            Timber.w(warningMsg);\n                        }\n                    }\n                });\n            } else {\n                Intent reqIntent = getIntent();\n\n                // we've just loaded a saved form, so start in the hierarchy view\n                String formMode = reqIntent.getStringExtra(FormOpeningMode.FORM_MODE_KEY);\n                if (FormOpeningMode.isEditableMode(formMode)) {\n                    identityPromptViewModel.formLoaded(formController);\n                    identityPromptViewModel.requiresIdentityToContinue().observe(this, requiresIdentity -> {\n                        if (!requiresIdentity) {\n                            formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_RESUME, true, System.currentTimeMillis());\n                            if (!allowMovingBackwards) {\n                                // we aren't allowed to jump around the form so attempt to\n                                // go directly to the question we were on last time the\n                                // form was saved.\n                                // TODO: revisit the fallback. If for some reason the index\n                                // wasn't saved, we can now jump around which doesn't seem right.\n                                FormIndex formIndex = SaveFormIndexTask.loadFormIndexFromFile(formController);\n                                if (formIndex != null) {\n                                    formController.jumpToIndex(formIndex);\n                                    formControllerAvailable(formController, form, instance);\n                                    formEntryViewModel.refresh();\n                                    return;\n                                }\n                            }\n\n                            boolean pendingActivityResult = task.hasPendingActivityResult();\n                            if (pendingActivityResult) {\n                                formControllerAvailable(formController, form, instance);\n                                formEntryViewModel.refreshSync();\n                                onActivityResult(task.getRequestCode(), task.getResultCode(), task.getIntent());\n                            } else {\n                                formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.HIERARCHY, true, System.currentTimeMillis());\n                                formControllerAvailable(formController, form, instance);\n                                Intent intent = new Intent(this, FormHierarchyFragmentHostActivity.class);\n                                intent.putExtra(FormHierarchyFragmentHostActivity.EXTRA_SESSION_ID, sessionId);\n                                intent.putExtra(FormHierarchyFragmentHostActivity.SHOW_NEW_EDIT_MESSAGE, formEntryViewModel.shouldShowNewEditMessage());\n                                startActivityForResult(intent, RequestCodes.HIERARCHY_ACTIVITY);\n                            }\n                        }\n                    });\n                } else {\n                    formControllerAvailable(formController, form, instance);\n                    if (FormOpeningMode.VIEW_SENT.equalsIgnoreCase(formMode)) {\n                        Intent intent = new Intent(this, FormHierarchyFragmentHostActivity.class);\n                        intent.putExtra(FormHierarchyFragmentHostActivity.EXTRA_SESSION_ID, sessionId);\n                        intent.putExtra(FormHierarchyFragmentHostActivity.EXTRA_VIEW_ONLY, true);\n                        startActivity(intent);\n                    }\n\n                    finish();\n                }\n            }\n        } else {\n            Timber.e(new Error(\"FormController is null\"));\n            showLongToast(org.odk.collect.strings.R.string.loading_form_failed);\n            exit();\n        }\n    }\n\n    /**\n     * called by the FormLoaderTask if something goes wrong.\n     */\n    @Override\n    public void loadingError(String errorMsg) {\n        showFormLoadErrorAndExit(errorMsg);\n    }\n\n    private void showFormLoadErrorAndExit(String errorMsg) {\n        DialogFragmentUtils.dismissDialog(FormLoadingDialogFragment.class, getSupportFragmentManager());\n\n        if (errorMsg != null) {\n            createErrorDialog(new FormError.Fatal(errorMsg));\n        } else {\n            createErrorDialog(new FormError.Fatal(getString(org.odk.collect.strings.R.string.parse_error)));\n        }\n    }\n\n    public void onProgressStep(String stepMessage) {\n        showIfNotShowing(FormLoadingDialogFragment.class, getSupportFragmentManager());\n\n        FormLoadingDialogFragment dialog = getDialog(FormLoadingDialogFragment.class, getSupportFragmentManager());\n        if (dialog != null) {\n            dialog.setMessage(getString(org.odk.collect.strings.R.string.please_wait) + \"\\n\\n\" + stepMessage);\n        }\n    }\n\n    public void next() {\n        if (!swipeHandler.beenSwiped()) {\n            swipeHandler.setBeenSwiped(true);\n            onSwipeForward();\n        }\n    }\n\n    /**\n     * Returns the instance that was just filled out to the calling activity, if\n     * requested.\n     */\n    private void finishAndReturnInstance() {\n        Timber.w(\"Form saved and closed\");\n\n        String action = getIntent().getAction();\n        if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_EDIT.equals(action)) {\n            // caller is waiting on a picked form\n            Uri uri = null;\n            String path = getAbsoluteInstancePath();\n            if (path != null) {\n                if (formSaveViewModel.getInstance() != null) {\n                    uri = InstancesContract.getUri(projectsDataService.requireCurrentProject().getUuid(), formSaveViewModel.getInstance().getDbId());\n                }\n            }\n\n            if (uri != null) {\n                setResult(RESULT_OK, new Intent().setData(uri));\n            }\n        }\n\n        exit();\n    }\n\n    private void exit() {\n        backgroundLocationViewModel.activityHidden();\n        formEntryViewModel.exit();\n        finish();\n    }\n\n    @Override\n    public void advance() {\n        next();\n    }\n\n    @Override\n    public void onSavePointError(String errorMessage) {\n        if (errorMessage != null && errorMessage.trim().length() > 0) {\n            showLongToast(getString(org.odk.collect.strings.R.string.save_point_error, errorMessage));\n        }\n    }\n\n    @Override\n    public void onSaveFormIndexError(String errorMessage) {\n        if (errorMessage != null && errorMessage.trim().length() > 0) {\n            showLongToast(getString(org.odk.collect.strings.R.string.save_point_error, errorMessage));\n        }\n    }\n\n    @Override\n    public void onDateChanged(LocalDateTime selectedDate) {\n        onDataChanged(selectedDate);\n    }\n\n    @Override\n    public void onTimeChanged(DateTime selectedTime) {\n        onDataChanged(selectedTime);\n    }\n\n    @Override\n    public void onRankingChanged(List<SelectChoice> items) {\n        onDataChanged(items);\n    }\n\n    /*\n     *TODO: this is not an ideal way to solve communication between a dialog created by a widget and the widget.\n     * Instead we should use viewmodels: https://github.com/getodk/collect/pull/3964#issuecomment-670155433\n     */\n    @Override\n    public void updateSelectedItems(List<Selection> items) {\n        ODKView odkView = getCurrentViewIfODKView();\n        if (odkView != null) {\n            QuestionWidget widgetGettingNewValue = getWidgetWaitingForBinaryData();\n            setWidgetData(items);\n            widgetValueChanged(widgetGettingNewValue);\n        }\n    }\n\n    @Override\n    public void onCancelFormLoading() {\n        if (formLoaderTask != null) {\n            formLoaderTask.setFormLoaderListener(null);\n            FormLoaderTask t = formLoaderTask;\n            formLoaderTask = null;\n            t.cancel();\n            t.destroy();\n        }\n        exit();\n    }\n\n    private void onDataChanged(Object data) {\n        ODKView odkView = getCurrentViewIfODKView();\n        if (odkView != null) {\n            QuestionWidget widgetGettingNewValue = getWidgetWaitingForBinaryData();\n            setWidgetData(data);\n            widgetValueChanged(widgetGettingNewValue);\n        }\n    }\n\n    /**\n     * getter for currentView variable. This method should always be used\n     * to access currentView as an ODKView object to avoid inconsistency\n     **/\n    @Nullable\n    public ODKView getCurrentViewIfODKView() {\n        if (currentView instanceof ODKView) {\n            return (ODKView) currentView;\n        }\n        return null;\n    }\n\n    /**\n     * Used whenever we need to show empty view and be able to recognize it from the code\n     */\n    static class EmptyView extends SwipeHandler.View {\n        EmptyView(Context context) {\n            super(context);\n        }\n\n        @Override\n        public boolean shouldSuppressFlingGesture() {\n            return false;\n        }\n\n        @Nullable\n        @Override\n        public NestedScrollView verticalScrollView() {\n            return null;\n        }\n    }\n\n    private class LocationProvidersReceiver extends BroadcastReceiver {\n        @Override\n        public void onReceive(Context context, Intent intent) {\n            if (intent.getAction() != null\n                    && intent.getAction().matches(LocationManager.PROVIDERS_CHANGED_ACTION)) {\n                backgroundLocationViewModel.locationProvidersChanged();\n            }\n        }\n    }\n\n    private void activityDisplayed() {\n        displayUIFor(backgroundLocationViewModel.activityDisplayed());\n\n        if (backgroundLocationViewModel.isBackgroundLocationPermissionsCheckNeeded()) {\n            permissionsProvider.requestEnabledLocationPermissions(this, new PermissionListener() {\n                @Override\n                public void granted() {\n                    displayUIFor(backgroundLocationViewModel.locationPermissionsGranted());\n                }\n\n                @Override\n                public void denied() {\n                    backgroundLocationViewModel.locationPermissionsDenied();\n                }\n            });\n        }\n    }\n\n    /**\n     * Displays UI representing the given background location message, if there is one.\n     */\n    private void displayUIFor(@Nullable BackgroundLocationManager.BackgroundLocationMessage\n                                      backgroundLocationMessage) {\n        if (backgroundLocationMessage == null) {\n            return;\n        }\n\n        if (backgroundLocationMessage == BackgroundLocationManager.BackgroundLocationMessage.PROVIDERS_DISABLED) {\n            new LocationProvidersDisabledDialog().show(getSupportFragmentManager(), LocationProvidersDisabledDialog.LOCATION_PROVIDERS_DISABLED_DIALOG_TAG);\n            return;\n        }\n\n        String snackBarText;\n\n        if (backgroundLocationMessage.isMenuCharacterNeeded()) {\n            snackBarText = String.format(getString(backgroundLocationMessage.getMessageTextResourceId()), \"⋮\");\n        } else {\n            snackBarText = getString(backgroundLocationMessage.getMessageTextResourceId());\n        }\n\n        SnackbarUtils.showSnackbar(\n                findViewById(R.id.llParent),\n                snackBarText,\n                SnackbarUtils.DURATION_LONG,\n                findViewById(R.id.buttonholder)\n        );\n    }\n\n    @Override\n    public void widgetValueChanged(QuestionWidget changedWidget) {\n        FormController formController = getFormController();\n        if (formController == null) {\n            // TODO: As usual, no idea if/how this is possible.\n            return;\n        }\n\n        if (formController.indexIsInFieldList()) {\n            // Some widgets may call widgetValueChanged from a non-main thread but odkView can only be modified from the main thread\n            runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    try {\n                        updateFieldListQuestions(changedWidget.getFormEntryPrompt().getIndex());\n                        odkView.post(() -> {\n                            if (odkView != null && !odkView.isDisplayed(changedWidget)) {\n                                odkView.focusToTopOf(changedWidget);\n                            }\n                        });\n                    } catch (RepeatsInFieldListException e) {\n                        createErrorDialog(new FormError.NonFatal(e.getMessage()));\n                    } catch (Exception | Error e) {\n                        Timber.e(e);\n                        createErrorDialog(new FormError.Fatal(getString(org.odk.collect.strings.R.string.update_widgets_error)));\n                    }\n                }\n            });\n        }\n    }\n\n    /**\n     * Saves the form and updates displayed widgets accordingly:\n     * - removes widgets corresponding to questions that are no longer relevant\n     * - adds widgets corresponding to questions that are newly-relevant\n     * - removes and rebuilds widgets corresponding to questions that have changed in some way. For\n     * example, the question text or hint may have updated due to a value they refer to changing.\n     * <p>\n     * The widget corresponding to the {@param lastChangedIndex} is never changed.\n     */\n    private void updateFieldListQuestions(FormIndex lastChangedIndex) throws RepeatsInFieldListException {\n        ODKView odkView = getCurrentViewIfODKView();\n        if (odkView == null) {\n            return;\n        }\n\n        FormEntryPrompt[] questionsAfterSave = formEntryViewModel.saveFieldList(odkView.getAnswers());\n        odkView.onUpdated(lastChangedIndex, questionsAfterSave);\n    }\n\n    private HashMap<FormIndex, IAnswerData> getAnswers() {\n        ODKView currentViewIfODKView = getCurrentViewIfODKView();\n\n        if (currentViewIfODKView != null) {\n            return currentViewIfODKView.getAnswers();\n        } else {\n            return new HashMap<>();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/FormListActivity.java",
    "content": "package org.odk.collect.android.activities;\n\nimport org.odk.collect.android.database.forms.DatabaseFormColumns;\n\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_ASC;\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_DESC;\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_ASC;\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_DESC;\n\npublic abstract class FormListActivity extends AppListActivity {\n\n    protected static final String SORT_BY_NAME_ASC\n            = DatabaseFormColumns.DISPLAY_NAME + \" COLLATE NOCASE ASC\";\n    protected static final String SORT_BY_NAME_DESC\n            = DatabaseFormColumns.DISPLAY_NAME + \" COLLATE NOCASE DESC\";\n    protected static final String SORT_BY_DATE_ASC = DatabaseFormColumns.DATE + \" ASC\";\n    protected static final String SORT_BY_DATE_DESC = DatabaseFormColumns.DATE + \" DESC\";\n\n    protected String getSortingOrder() {\n        String sortingOrder = SORT_BY_NAME_ASC;\n        switch (getSelectedSortingOrder()) {\n            case BY_NAME_ASC:\n                sortingOrder = SORT_BY_NAME_ASC;\n                break;\n            case BY_NAME_DESC:\n                sortingOrder = SORT_BY_NAME_DESC;\n                break;\n            case BY_DATE_ASC:\n                sortingOrder = SORT_BY_DATE_ASC;\n                break;\n            case BY_DATE_DESC:\n                sortingOrder = SORT_BY_DATE_DESC;\n                break;\n        }\n        return sortingOrder;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/FormMapActivity.kt",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.activities\n\nimport android.os.Bundle\nimport androidx.activity.viewModels\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.android.R\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.android.formmanagement.FormFillingIntentFactory\nimport org.odk.collect.android.formmanagement.formmap.FormMapViewModel\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.geo.selection.SelectionMapFragment\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport javax.inject.Inject\n\n/**\n * Show a map with points representing saved instances of the selected form.\n */\nclass FormMapActivity : LocalizedActivity() {\n\n    @Inject\n    lateinit var formsRepositoryProvider: FormsRepositoryProvider\n\n    @Inject\n    lateinit var instancesRepositoryProvider: InstancesRepositoryProvider\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    private val formId by lazy { intent.getLongExtra(EXTRA_FORM_ID, -1) }\n    private val viewModel: FormMapViewModel by viewModels {\n        object : ViewModelProvider.Factory {\n            @Suppress(\"UNCHECKED_CAST\")\n            override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                return FormMapViewModel(\n                    resources,\n                    formId,\n                    formsRepositoryProvider.create(),\n                    instancesRepositoryProvider.create(),\n                    settingsProvider,\n                    scheduler\n                ) as T\n            }\n        }\n    }\n\n    public override fun onCreate(savedInstanceState: Bundle?) {\n        DaggerUtils.getComponent(this).inject(this)\n        supportFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(SelectionMapFragment::class.java) {\n                SelectionMapFragment(\n                    selectionMapData = viewModel\n                )\n            }\n            .build()\n\n        super.onCreate(savedInstanceState)\n        setView(R.layout.form_map_activity, false)\n\n        supportFragmentManager.setFragmentResultListener(\n            SelectionMapFragment.REQUEST_SELECT_ITEM,\n            this\n        ) { _: String?, result: Bundle ->\n            if (result.containsKey(SelectionMapFragment.RESULT_SELECTED_ITEM)) {\n                val instanceId = result.getLong(SelectionMapFragment.RESULT_SELECTED_ITEM)\n                startActivity(\n                    FormFillingIntentFactory.editDraftFormIntent(\n                        this,\n                        projectsDataService.requireCurrentProject().uuid,\n                        instanceId\n                    )\n                )\n            } else if (result.containsKey(SelectionMapFragment.RESULT_CREATE_NEW_ITEM)) {\n                startActivity(\n                    FormFillingIntentFactory.newFormIntent(\n                        this,\n                        FormsContract.getUri(\n                            projectsDataService.requireCurrentProject().uuid,\n                            formId\n                        )\n                    )\n                )\n            }\n        }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        viewModel.load()\n    }\n\n    companion object {\n        const val EXTRA_FORM_ID = \"form_id\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.activities;\n\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_ASC;\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_DESC;\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_ASC;\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_DESC;\nimport static org.odk.collect.androidshared.ui.EdgeToEdge.setView;\n\nimport android.content.Intent;\nimport android.database.Cursor;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.activity.result.ActivityResultLauncher;\nimport androidx.activity.result.contract.ActivityResultContracts;\nimport androidx.annotation.NonNull;\nimport androidx.loader.app.LoaderManager;\nimport androidx.loader.content.Loader;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.adapters.InstanceListCursorAdapter;\nimport org.odk.collect.android.application.CollectComposeThemeProvider;\nimport org.odk.collect.android.dao.CursorLoaderFactory;\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns;\nimport org.odk.collect.android.entities.EntitiesRepositoryProvider;\nimport org.odk.collect.android.external.FormUriActivity;\nimport org.odk.collect.android.external.InstancesContract;\nimport org.odk.collect.android.formentry.FormOpeningMode;\nimport org.odk.collect.android.formlists.sorting.FormListSortingOption;\nimport org.odk.collect.android.formmanagement.drafts.BulkFinalizationViewModel;\nimport org.odk.collect.android.formmanagement.drafts.DraftsMenuProvider;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.instancemanagement.FinalizeAllSnackbarPresenter;\nimport org.odk.collect.android.instancemanagement.InstancesDataService;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.db.sqlite.DatabaseConnection;\nimport org.odk.collect.lists.EmptyListView;\nimport org.odk.collect.material.MaterialProgressDialogFragment;\nimport org.odk.collect.settings.SettingsProvider;\n\nimport java.util.Arrays;\n\nimport javax.inject.Inject;\n\n/**\n * Responsible for displaying all the valid instances in the instance directory.\n *\n * @author Yaw Anokwa (yanokwa@gmail.com)\n * @author Carl Hartung (carlhartung@gmail.com)\n * @deprecated Uses {@link CursorLoaderFactory} and interacts with {@link DatabaseConnection} on the\n * UI thread.\n */\n@Deprecated\npublic class InstanceChooserList extends AppListActivity implements InstanceListCursorAdapter.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor>, CollectComposeThemeProvider {\n    private static final String INSTANCE_LIST_ACTIVITY_SORTING_ORDER = \"instanceListActivitySortingOrder\";\n    private static final String VIEW_SENT_FORM_SORTING_ORDER = \"ViewSentFormSortingOrder\";\n\n    private boolean editMode;\n\n    @Inject\n    ProjectsDataService projectsDataService;\n\n    @Inject\n    FormsRepositoryProvider formsRepositoryProvider;\n\n    @Inject\n    Scheduler scheduler;\n\n    @Inject\n    InstancesRepositoryProvider instancesRepositoryProvider;\n\n    @Inject\n    EntitiesRepositoryProvider entitiesRepositoryProvider;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    @Inject\n    InstancesDataService instancesDataService;\n\n    private final ActivityResultLauncher<Intent> formLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {\n        setResult(RESULT_OK, result.getData());\n        finish();\n    });\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setView(this, R.layout.form_chooser_list, false);\n        DaggerUtils.getComponent(this).inject(this);\n\n        String formMode = getIntent().getStringExtra(FormOpeningMode.FORM_MODE_KEY);\n        if (formMode == null || FormOpeningMode.EDIT_SAVED.equalsIgnoreCase(formMode)) {\n            setTitle(getString(org.odk.collect.strings.R.string.review_data));\n            editMode = true;\n        } else {\n            setTitle(getString(org.odk.collect.strings.R.string.view_sent_forms));\n            EmptyListView emptyListView = findViewById(android.R.id.empty);\n            emptyListView.setIcon(R.drawable.ic_baseline_inbox_72);\n            emptyListView.setTitle(getString(org.odk.collect.strings.R.string.empty_list_of_sent_forms_title));\n            emptyListView.setSubtitle(getString(org.odk.collect.strings.R.string.empty_list_of_sent_forms_subtitle));\n        }\n\n        sortingOptions = Arrays.asList(\n                new FormListSortingOption(\n                        R.drawable.ic_sort_by_alpha,\n                        org.odk.collect.strings.R.string.sort_by_name_asc\n                ),\n                new FormListSortingOption(\n                        R.drawable.ic_sort_by_alpha,\n                        org.odk.collect.strings.R.string.sort_by_name_desc\n                ),\n                new FormListSortingOption(\n                        R.drawable.ic_access_time,\n                        org.odk.collect.strings.R.string.sort_by_date_desc\n                ),\n                new FormListSortingOption(\n                        R.drawable.ic_access_time,\n                        org.odk.collect.strings.R.string.sort_by_date_asc\n                )\n        );\n\n        init();\n\n        BulkFinalizationViewModel bulkFinalizationViewModel = new BulkFinalizationViewModel(\n                projectsDataService.requireCurrentProject().getUuid(),\n                scheduler,\n                instancesDataService,\n                settingsProvider\n        );\n\n        MaterialProgressDialogFragment.showOn(this, bulkFinalizationViewModel.isFinalizing(), getSupportFragmentManager(), () -> {\n            MaterialProgressDialogFragment dialog = new MaterialProgressDialogFragment();\n            dialog.setMessage(\"Finalizing drafts...\");\n            return dialog;\n        });\n\n        if (bulkFinalizationViewModel.isEnabled() && editMode) {\n            DraftsMenuProvider draftsMenuProvider = new DraftsMenuProvider(this, bulkFinalizationViewModel::finalizeAllDrafts);\n            addMenuProvider(draftsMenuProvider, this);\n            bulkFinalizationViewModel.getDraftsCount().observe(this, draftsCount -> {\n                draftsMenuProvider.setDraftsCount(draftsCount);\n                invalidateMenu();\n            });\n\n            bulkFinalizationViewModel.getFinalizedForms().observe(\n                    this,\n                    new FinalizeAllSnackbarPresenter(this.findViewById(android.R.id.content), this)\n            );\n        }\n    }\n\n    private void init() {\n        setupAdapter();\n        getSupportLoaderManager().initLoader(LOADER_ID, null, this);\n    }\n\n    /**\n     * Stores the path of selected instance in the parent class and finishes.\n     */\n    @Override\n    public void onItemClick(View view, int position) {\n        if (MultiClickGuard.allowClick(getClass().getName())) {\n            if (view.isEnabled()) {\n                Cursor c = (Cursor) listView.getAdapter().getItem(position);\n                long instanceId = c.getLong(c.getColumnIndex(DatabaseInstanceColumns._ID));\n                Uri instanceUri = InstancesContract.getUri(projectsDataService.requireCurrentProject().getUuid(), instanceId);\n\n                String action = getIntent().getAction();\n                if (Intent.ACTION_PICK.equals(action)) {\n                    // caller is waiting on a picked form\n                    setResult(RESULT_OK, new Intent().setData(instanceUri));\n                    finish();\n                } else {\n                    // caller wants to view/edit a form, so launch FormFillingActivity\n                    Intent parentIntent = this.getIntent();\n                    Intent intent = new Intent(this, FormUriActivity.class);\n                    intent.setAction(Intent.ACTION_EDIT);\n                    intent.setData(instanceUri);\n                    String formMode = parentIntent.getStringExtra(FormOpeningMode.FORM_MODE_KEY);\n                    if (formMode == null || FormOpeningMode.EDIT_SAVED.equalsIgnoreCase(formMode)) {\n                        intent.putExtra(FormOpeningMode.FORM_MODE_KEY, FormOpeningMode.EDIT_SAVED);\n                        formLauncher.launch(intent);\n                    } else {\n                        intent.putExtra(FormOpeningMode.FORM_MODE_KEY, FormOpeningMode.VIEW_SENT);\n                        startActivity(intent);\n                        finish();\n                    }\n                }\n            } else {\n                TextView disabledCause = view.findViewById(R.id.form_subtitle2);\n                Toast.makeText(this, disabledCause.getText(), Toast.LENGTH_SHORT).show();\n            }\n        }\n    }\n\n    private void setupAdapter() {\n        String[] data = {DatabaseInstanceColumns.DISPLAY_NAME, DatabaseInstanceColumns.DELETED_DATE};\n        int[] view = {R.id.form_title, R.id.form_subtitle2};\n\n        boolean shouldCheckDisabled = !editMode;\n\n        InstanceListCursorAdapter instanceListCursorAdapter = new InstanceListCursorAdapter(\n                this, R.layout.form_chooser_list_item, null, data, view, shouldCheckDisabled);\n        instanceListCursorAdapter.setOnItemClickListener(this);\n        listAdapter = instanceListCursorAdapter;\n        listView.setAdapter(listAdapter);\n    }\n\n    @Override\n    protected String getSortingOrderKey() {\n        return editMode ? INSTANCE_LIST_ACTIVITY_SORTING_ORDER : VIEW_SENT_FORM_SORTING_ORDER;\n    }\n\n    @Override\n    protected void updateAdapter() {\n        getSupportLoaderManager().restartLoader(LOADER_ID, null, this);\n    }\n\n    @NonNull\n    @Override\n    public Loader<Cursor> onCreateLoader(int id, Bundle args) {\n        showProgressBar();\n        if (editMode) {\n            return new CursorLoaderFactory(projectsDataService).createEditableInstancesCursorLoader(getFilterText(), getSortingOrder());\n        } else {\n            return new CursorLoaderFactory(projectsDataService).createSentInstancesCursorLoader(getFilterText(), getSortingOrder());\n        }\n    }\n\n    @Override\n    public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {\n        hideProgressBarAndAllow();\n        listAdapter.swapCursor(cursor);\n    }\n\n    @Override\n    public void onLoaderReset(@NonNull Loader loader) {\n        listAdapter.swapCursor(null);\n    }\n\n    protected String getSortingOrder() {\n        String sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + \" COLLATE NOCASE ASC, \" + DatabaseInstanceColumns.STATUS + \" DESC\";\n        switch (getSelectedSortingOrder()) {\n            case BY_NAME_ASC:\n                sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + \" COLLATE NOCASE ASC, \" + DatabaseInstanceColumns.STATUS + \" DESC\";\n                break;\n            case BY_NAME_DESC:\n                sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + \" COLLATE NOCASE DESC, \" + DatabaseInstanceColumns.STATUS + \" DESC\";\n                break;\n            case BY_DATE_ASC:\n                sortingOrder = DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE + \" ASC\";\n                break;\n            case BY_DATE_DESC:\n                sortingOrder = DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE + \" DESC\";\n                break;\n        }\n        return sortingOrder;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/ScannerWithFlashlightActivity.kt",
    "content": "/*\n * Copyright (C) 2017 Shobhit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.activities\n\nimport android.os.Bundle\nimport android.view.WindowManager\nimport androidx.activity.enableEdgeToEdge\nimport androidx.core.view.WindowCompat\nimport androidx.core.view.WindowInsetsCompat.Type.systemBars\nimport org.odk.collect.android.R\nimport org.odk.collect.android.application.CollectComposeThemeProvider\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.strings.localization.LocalizedActivity\n\n/**\n * Custom Scannner Activity extending from Activity to display a custom layout form scanner view.\n */\nclass ScannerWithFlashlightActivity : LocalizedActivity(), CollectComposeThemeProvider {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        enableEdgeToEdge()\n\n        val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)\n        windowInsetsController.hide(systemBars())\n\n        // Remove rotation animation - we'll handle configuration changes in Fragments\n        this.window.attributes.rotationAnimation =\n            WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS\n\n        setView(R.layout.activity_custom_scanner, false)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/activities/viewmodels/FormDownloadListViewModel.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.activities.viewmodels;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.odk.collect.android.formmanagement.ServerFormDetails;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.LinkedHashSet;\n\npublic class FormDownloadListViewModel extends ViewModel {\n\n    private HashMap<String, ServerFormDetails> formDetailsByFormId = new HashMap<>();\n\n    /**\n     * List of forms from the formList response. The map acts like a DisplayableForm object with\n     * values for each component that shows up in the form list UI. See\n     * FormDownloadListActivity.formListDownloadingComplete for keys.\n     */\n    private final ArrayList<HashMap<String, String>> formList = new ArrayList<>();\n\n    private final LinkedHashSet<String> selectedFormIds = new LinkedHashSet<>();\n\n    private String alertTitle;\n    private String alertDialogMsg;\n\n    private boolean alertShowing;\n    private boolean cancelDialogShowing;\n    private boolean shouldExit;\n    private boolean loadingCanceled;\n\n    // Variables used when the activity is called from an external app\n    private boolean isDownloadOnlyMode;\n    private String[] formIdsToDownload;\n    private String url;\n    private String username;\n    private String password;\n    private final HashMap<String, Boolean> formResults = new HashMap<>();\n\n    public HashMap<String, ServerFormDetails> getFormDetailsByFormId() {\n        return formDetailsByFormId;\n    }\n\n    public void setFormDetailsByFormId(HashMap<String, ServerFormDetails> formDetailsByFormId) {\n        this.formDetailsByFormId = formDetailsByFormId;\n    }\n\n    public void clearFormDetailsByFormId() {\n        formDetailsByFormId.clear();\n    }\n\n    public String getAlertTitle() {\n        return alertTitle;\n    }\n\n    public void setAlertTitle(String alertTitle) {\n        this.alertTitle = alertTitle;\n    }\n\n    public String getAlertDialogMsg() {\n        return alertDialogMsg;\n    }\n\n    public void setAlertDialogMsg(String alertDialogMsg) {\n        this.alertDialogMsg = alertDialogMsg;\n    }\n\n    public boolean isAlertShowing() {\n        return alertShowing;\n    }\n\n    public void setAlertShowing(boolean alertShowing) {\n        this.alertShowing = alertShowing;\n    }\n\n    public boolean shouldExit() {\n        return shouldExit;\n    }\n\n    public void setShouldExit(boolean shouldExit) {\n        this.shouldExit = shouldExit;\n    }\n\n    public ArrayList<HashMap<String, String>> getFormList() {\n        return formList;\n    }\n\n    public void clearFormList() {\n        formList.clear();\n    }\n\n    public void addForm(HashMap<String, String> item) {\n        formList.add(item);\n    }\n\n    public void addForm(int index, HashMap<String, String> item) {\n        formList.add(index, item);\n    }\n\n    public LinkedHashSet<String> getSelectedFormIds() {\n        return selectedFormIds;\n    }\n\n    public void addSelectedFormId(String selectedFormId) {\n        selectedFormIds.add(selectedFormId);\n    }\n\n    public void removeSelectedFormId(String selectedFormId) {\n        selectedFormIds.remove(selectedFormId);\n    }\n\n    public void clearSelectedFormIds() {\n        selectedFormIds.clear();\n    }\n\n    public boolean isDownloadOnlyMode() {\n        return isDownloadOnlyMode;\n    }\n\n    public void setDownloadOnlyMode(boolean downloadOnlyMode) {\n        isDownloadOnlyMode = downloadOnlyMode;\n    }\n\n    public HashMap<String, Boolean> getFormResults() {\n        return formResults;\n    }\n\n    public void putFormResult(String formId, boolean result) {\n        formResults.put(formId, result);\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    public void setPassword(String password) {\n        this.password = password;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public void setUsername(String username) {\n        this.username = username;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public String[] getFormIdsToDownload() {\n        return Arrays.copyOf(formIdsToDownload, formIdsToDownload.length);\n    }\n\n    public void setFormIdsToDownload(String[] formIdsToDownload) {\n        this.formIdsToDownload = formIdsToDownload;\n    }\n\n    public boolean isCancelDialogShowing() {\n        return cancelDialogShowing;\n    }\n\n    public void setCancelDialogShowing(boolean cancelDialogShowing) {\n        this.cancelDialogShowing = cancelDialogShowing;\n    }\n\n    public boolean wasLoadingCanceled() {\n        return loadingCanceled;\n    }\n\n    public void setLoadingCanceled(boolean loadingCanceled) {\n        this.loadingCanceled = loadingCanceled;\n    }\n\n    public static class Factory implements ViewModelProvider.Factory {\n\n        @SuppressWarnings(\"unchecked\")\n        @NonNull\n        @Override\n        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {\n            return (T) new FormDownloadListViewModel();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/adapters/AboutListAdapter.kt",
    "content": "/*\n * Copyright (C) 2018 Shobhit Agarwal\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.adapters\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.recyclerview.widget.RecyclerView\nimport org.odk.collect.android.databinding.AboutItemLayoutBinding\n\nclass AboutListAdapter(\n    private val items: Array<IntArray>,\n    private val listener: AboutItemClickListener\n) : RecyclerView.Adapter<AboutListAdapter.ViewHolder>() {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        val binding = AboutItemLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        return ViewHolder(binding)\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n        holder.apply {\n            binding.root.setOnClickListener {\n                listener.onClick(position)\n            }\n            binding.imageView.setImageResource(items[position][0])\n            binding.imageView.tag = items[position][0]\n            binding.title.text = holder.binding.root.context.getString(items[position][1])\n            binding.summary.text = holder.binding.root.context.getString(items[position][2])\n        }\n    }\n\n    override fun getItemCount() = items.size\n\n    inner class ViewHolder(val binding: AboutItemLayoutBinding) : RecyclerView.ViewHolder(binding.root)\n}\n\ninterface AboutItemClickListener {\n    fun onClick(position: Int)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/adapters/AbstractSelectListAdapter.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.adapters;\n\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getClip;\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getClipID;\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getPlayableAudioURI;\n\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CompoundButton;\nimport android.widget.Filter;\nimport android.widget.Filterable;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.dynamicpreload.ExternalSelectChoice;\nimport org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel;\nimport org.odk.collect.android.formentry.questions.NoButtonsItem;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.audioclips.Clip;\nimport org.odk.collect.imageloader.GlideImageLoader;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\n\nimport timber.log.Timber;\n\npublic abstract class AbstractSelectListAdapter extends RecyclerView.Adapter<AbstractSelectListAdapter.ViewHolder>\n        implements Filterable {\n\n    private AudioPlayer audioPlayer;\n    protected Context context;\n    protected List<SelectChoice> items;\n    protected List<SelectChoice> filteredItems;\n    protected final FormEntryPrompt prompt;\n    protected final ReferenceManager referenceManager;\n    protected final int playColor;\n    protected final int numColumns;\n    protected boolean noButtonsMode;\n    private final MediaUtils mediaUtils;\n\n    AbstractSelectListAdapter(Context context, List<SelectChoice> items, FormEntryPrompt prompt,\n                              ReferenceManager referenceManager, AudioPlayer audioPlayer,\n                              int playColor, int numColumns, boolean noButtonsMode, MediaUtils mediaUtils) {\n        this.context = context;\n        this.items = items;\n        this.filteredItems = items;\n        this.prompt = prompt;\n        this.referenceManager = referenceManager;\n        this.audioPlayer = audioPlayer;\n        this.playColor = playColor;\n        this.numColumns = numColumns;\n        this.noButtonsMode = noButtonsMode;\n        this.mediaUtils = mediaUtils;\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull ViewHolder holder, int index) {\n        holder.bind(index);\n    }\n\n    @Override\n    public int getItemCount() {\n        return filteredItems.size();\n    }\n\n    @Override\n    public Filter getFilter() {\n        return new Filter() {\n            @Override\n            protected FilterResults performFiltering(CharSequence charSequence) {\n                String searchStr = charSequence.toString().toLowerCase(Locale.US);\n                FilterResults filterResults = new FilterResults();\n                if (searchStr.isEmpty()) {\n                    filterResults.values = items;\n                    filterResults.count = items.size();\n                } else {\n                    List<SelectChoice> filteredList = new ArrayList<>();\n                    for (SelectChoice item : items) {\n                        if (prompt.getSelectChoiceText(item).toLowerCase(Locale.US).contains(searchStr)) {\n                            filteredList.add(item);\n                        }\n                    }\n                    filterResults.values = filteredList;\n                    filterResults.count = filteredList.size();\n                }\n\n                return filterResults;\n            }\n\n            @Override\n            protected void publishResults(CharSequence charSequence, FilterResults filterResults) {\n                filteredItems = (List<SelectChoice>) filterResults.values;\n                notifyDataSetChanged();\n            }\n        };\n    }\n\n    abstract CompoundButton createButton(int index, ViewGroup parent);\n\n    void setUpButton(TextView button, int index) {\n        button.setTextSize(TypedValue.COMPLEX_UNIT_DIP, QuestionFontSizeUtils.getQuestionFontSize());\n        button.setText(HtmlUtils.textToHtml(prompt.getSelectChoiceText(filteredItems.get(index))));\n        button.setTag(items.indexOf(filteredItems.get(index)));\n    }\n\n    boolean isItemSelected(List<Selection> selectedItems, @NonNull Selection item) {\n        for (Selection selectedItem : selectedItems) {\n            if (item.getValue().equalsIgnoreCase(selectedItem.getValue())) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    abstract void onItemClick(Selection selection, View view);\n\n    public abstract List<Selection> getSelectedItems();\n\n    public List<SelectChoice> getFilteredItems() {\n        return filteredItems;\n    }\n\n    public AudioPlayer getAudioPlayer() {\n        return audioPlayer;\n    }\n\n    public void setContext(Context context) {\n        this.context = context;\n    }\n\n    public void setAudioPlayer(AudioPlayer audioPlayer) {\n        this.audioPlayer = audioPlayer;\n    }\n\n    abstract class ViewHolder extends RecyclerView.ViewHolder {\n        AudioVideoImageTextLabel audioVideoImageTextLabel;\n        NoButtonsItem noButtonsItem;\n\n        ViewHolder(View itemView) {\n            super(itemView);\n        }\n\n        void bind(final int index) {\n            if (noButtonsMode) {\n                File imageFile = getImageFile(index);\n                noButtonsItem.setUpNoButtonsItem(imageFile, getChoiceText(index), getErrorMsg(imageFile), numColumns > 1);\n                noButtonsItem.setOnClickListener(v -> onItemClick(filteredItems.get(index).selection(), v));\n            } else {\n                addMediaFromChoice(audioVideoImageTextLabel, index, createButton(index, audioVideoImageTextLabel), filteredItems);\n                audioVideoImageTextLabel.setEnabled(!prompt.isReadOnly());\n                enableLongClickToAllowRemovingAnswers(itemView);\n            }\n        }\n\n        private File getImageFile(int index) {\n            SelectChoice selectChoice = filteredItems.get(index);\n            String imageURI = selectChoice instanceof ExternalSelectChoice\n                    ? ((ExternalSelectChoice) selectChoice).getImage()\n                    : prompt.getSpecialFormSelectChoiceText(selectChoice, FormEntryCaption.TEXT_FORM_IMAGE);\n\n            try {\n                return new File(ReferenceManager.instance().deriveReference(imageURI).getLocalURI());\n            } catch (InvalidReferenceException e) {\n                Timber.w(e);\n            }\n            return null;\n        }\n\n        private String getChoiceText(int index) {\n            SelectChoice selectChoice = filteredItems.get(index);\n            return HtmlUtils.textToHtml(prompt.getSelectChoiceText(selectChoice)).toString();\n        }\n\n        private String getErrorMsg(File imageFile) {\n            return context.getString(org.odk.collect.strings.R.string.file_missing, imageFile);\n        }\n\n        private void enableLongClickToAllowRemovingAnswers(View view) {\n            if (view instanceof ViewGroup) {\n                view.setLongClickable(true);\n                for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {\n                    enableLongClickToAllowRemovingAnswers(((ViewGroup) view).getChildAt(i));\n                }\n            } else {\n                view.setLongClickable(true);\n            }\n        }\n\n        /**\n         * Pull media from the current item and add it to the media layout.\n         */\n        public void addMediaFromChoice(AudioVideoImageTextLabel audioVideoImageTextLabel, int index, TextView textView, List<SelectChoice> items) {\n            SelectChoice item = items.get(index);\n\n            audioVideoImageTextLabel.setTag(getClipID(prompt, item));\n            audioVideoImageTextLabel.setTextView(textView);\n            audioVideoImageTextLabel.setMediaUtils(mediaUtils);\n\n            String imageURI = getImageURI(index, items);\n            String videoURI = prompt.getSpecialFormSelectChoiceText(item, \"video\");\n            String bigImageURI = prompt.getSpecialFormSelectChoiceText(item, \"big-image\");\n            String audioURI = getPlayableAudioURI(prompt, item, referenceManager);\n            try {\n                if (imageURI != null) {\n                    audioVideoImageTextLabel.setImage(new File(referenceManager.deriveReference(imageURI).getLocalURI()), new GlideImageLoader());\n                }\n                if (bigImageURI != null) {\n                    audioVideoImageTextLabel.setBigImage(new File(referenceManager.deriveReference(bigImageURI).getLocalURI()));\n                }\n                if (videoURI != null) {\n                    audioVideoImageTextLabel.setVideo(new File(referenceManager.deriveReference(videoURI).getLocalURI()));\n                }\n                if (audioURI != null) {\n                    audioVideoImageTextLabel.setAudio(audioURI, audioPlayer);\n                }\n            } catch (InvalidReferenceException e) {\n                Timber.d(e, \"Invalid media reference due to %s \", e.getMessage());\n            }\n\n            textView.setGravity(Gravity.CENTER_VERTICAL);\n        }\n\n        private String getImageURI(int index, List<SelectChoice> items) {\n            String imageURI;\n            if (items.get(index) instanceof ExternalSelectChoice) {\n                imageURI = ((ExternalSelectChoice) items.get(index)).getImage();\n            } else {\n                imageURI = prompt.getSpecialFormSelectChoiceText(items.get(index),\n                        FormEntryCaption.TEXT_FORM_IMAGE);\n            }\n            return imageURI;\n        }\n\n        void adjustAudioVideoImageTextLabelForFlexAppearance() {\n            if (Appearances.isFlexAppearance(prompt)) {\n                RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);\n\n                audioVideoImageTextLabel.findViewById(R.id.audio_video_image_text_label_container).setLayoutParams(params);\n                audioVideoImageTextLabel.findViewById(R.id.image_text_label_container).setLayoutParams(params);\n                audioVideoImageTextLabel.getImageView().setVisibility(View.GONE);\n                audioVideoImageTextLabel.getVideoButton().setVisibility(View.GONE);\n                audioVideoImageTextLabel.getAudioButton().setVisibility(View.GONE);\n            }\n        }\n    }\n\n    public void playAudio(SelectChoice selectChoice) {\n        audioPlayer.stop();\n        Clip clip = getClip(prompt, selectChoice, referenceManager);\n        if (clip != null) {\n            audioPlayer.play(clip);\n        }\n    }\n\n    public int getNumColumns() {\n        return numColumns;\n    }\n\n    public abstract void clearAnswer();\n\n    public abstract boolean hasAnswerChanged();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/adapters/FormDownloadListAdapter.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.adapters;\n\nimport android.content.Context;\nimport androidx.annotation.NonNull;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ArrayAdapter;\nimport android.widget.TextView;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formmanagement.ServerFormDetails;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\n\nimport static org.odk.collect.android.activities.FormDownloadListActivity.FORMID_DISPLAY;\nimport static org.odk.collect.android.activities.FormDownloadListActivity.FORMNAME;\nimport static org.odk.collect.android.activities.FormDownloadListActivity.FORM_ID_KEY;\n\npublic class FormDownloadListAdapter extends ArrayAdapter {\n\n    private final ArrayList<HashMap<String, String>> filteredFormList;\n    private HashMap<String, ServerFormDetails> formIdsToDetails;\n\n    public FormDownloadListAdapter(Context context, ArrayList<HashMap<String, String>> filteredFormList,\n                                   HashMap<String, ServerFormDetails> formIdsToDetails) {\n        super(context, R.layout.form_chooser_list_item_multiple_choice, filteredFormList);\n        this.filteredFormList = filteredFormList;\n        this.formIdsToDetails = formIdsToDetails;\n    }\n\n    public void setFromIdsToDetails(HashMap<String, ServerFormDetails> formIdsToDetails) {\n        this.formIdsToDetails = formIdsToDetails;\n    }\n\n    private static class ViewHolder {\n        TextView formTitle;\n        TextView formSubtitle;\n        TextView formUpdateAlert;\n    }\n\n    public View getView(int position, View convertView, @NonNull ViewGroup parent) {\n        View row = convertView;\n        final ViewHolder holder;\n        if (row == null) {\n            holder = new ViewHolder();\n            LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);\n            row = inflater.inflate(R.layout.form_chooser_list_item_multiple_choice, parent, false);\n\n            holder.formTitle = row.findViewById(R.id.form_title);\n            holder.formSubtitle = row.findViewById(R.id.form_subtitle);\n            holder.formUpdateAlert = row.findViewById(R.id.form_update_alert);\n            row.setTag(holder);\n        } else {\n            holder = (ViewHolder) row.getTag();\n        }\n\n        final HashMap<String, String> formAtPosition = filteredFormList.get(position);\n        final String formIDAtPosition = formAtPosition.get(FORM_ID_KEY);\n\n        holder.formTitle.setText(formAtPosition.get(FORMNAME));\n        holder.formSubtitle.setText(formAtPosition.get(FORMID_DISPLAY));\n\n        if (formIdsToDetails.get(formIDAtPosition) != null\n                && formIdsToDetails.get(formIDAtPosition).isUpdated()) {\n            holder.formUpdateAlert.setVisibility(View.VISIBLE);\n        } else {\n            holder.formUpdateAlert.setVisibility(View.GONE);\n        }\n        \n        return row;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/adapters/InstanceListCursorAdapter.java",
    "content": "/*\n * Copyright 2017 SDRC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.adapters;\n\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.SimpleCursorAdapter;\n\nimport org.odk.collect.android.database.DatabaseObjectMapper;\nimport org.odk.collect.android.instancemanagement.InstanceListItemView;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.forms.instances.Instance;\n\npublic class InstanceListCursorAdapter extends SimpleCursorAdapter {\n    private final boolean shouldCheckDisabled;\n    private OnItemClickListener onItemClickListener;\n\n    public InstanceListCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, boolean shouldCheckDisabled) {\n        super(context, layout, c, from, to);\n        this.shouldCheckDisabled = shouldCheckDisabled;\n    }\n\n    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {\n        this.onItemClickListener = onItemClickListener;\n    }\n\n    @Override\n    public View getView(int position, View convertView, ViewGroup parent) {\n        View view = super.getView(position, convertView, parent);\n        Instance instance = DatabaseObjectMapper.getInstanceFromCurrentCursorPosition(\n                getCursor(),\n                new StoragePathProvider().getOdkDirPath(StorageSubdirectory.INSTANCES)\n        );\n\n        InstanceListItemView.setInstance(view, instance, shouldCheckDisabled);\n\n        view.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                onItemClickListener.onItemClick(view, position);\n            }\n        });\n\n        return view;\n    }\n\n    public interface OnItemClickListener {\n        void onItemClick(View view, int position);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/adapters/InstanceUploaderAdapter.java",
    "content": "package org.odk.collect.android.adapters;\n\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CursorAdapter;\n\nimport org.odk.collect.android.database.DatabaseObjectMapper;\nimport org.odk.collect.android.formlists.savedformlist.SelectableSavedFormListItemViewHolder;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.forms.instances.Instance;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.function.Consumer;\n\npublic class InstanceUploaderAdapter extends CursorAdapter {\n    private final Consumer<Long> onItemCheckboxClickListener;\n    private Set<Long> selected = new HashSet<>();\n\n    public InstanceUploaderAdapter(Context context, Cursor cursor, Consumer<Long> onItemCheckboxClickListener) {\n        super(context, cursor);\n        this.onItemCheckboxClickListener = onItemCheckboxClickListener;\n    }\n\n    @Override\n    public View newView(Context context, Cursor cursor, ViewGroup parent) {\n        SelectableSavedFormListItemViewHolder viewHolder = new SelectableSavedFormListItemViewHolder(parent);\n        viewHolder.itemView.setTag(viewHolder);\n        return viewHolder.itemView;\n    }\n\n    @Override\n    public void bindView(View view, Context context, Cursor cursor) {\n        SelectableSavedFormListItemViewHolder viewHolder = (SelectableSavedFormListItemViewHolder) view.getTag();\n        Instance instance = DatabaseObjectMapper.getInstanceFromCurrentCursorPosition(cursor, new StoragePathProvider().getOdkDirPath(StorageSubdirectory.INSTANCES));\n        viewHolder.setItem(instance);\n\n        long dbId = instance.getDbId();\n        viewHolder.getCheckbox().setChecked(selected.contains(dbId));\n        viewHolder.setOnDetailsClickListener(() -> {\n            onItemCheckboxClickListener.accept(dbId);\n            return null;\n        });\n    }\n\n    public void setSelected(Set<Long> ids) {\n        this.selected = ids;\n        notifyDataSetChanged();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/adapters/RankingListAdapter.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.adapters;\n\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.GradientDrawable;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView.Adapter;\nimport androidx.recyclerview.widget.RecyclerView.ViewHolder;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.adapters.RankingListAdapter.ItemViewHolder;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.utilities.ThemeUtils;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\n\nimport java.util.Collections;\nimport java.util.List;\n\npublic class RankingListAdapter extends Adapter<ItemViewHolder> {\n\n    private final List<SelectChoice> items;\n    private final FormEntryPrompt formEntryPrompt;\n\n    public RankingListAdapter(List<SelectChoice> items, FormEntryPrompt formEntryPrompt) {\n        this.items = items;\n        this.formEntryPrompt = formEntryPrompt;\n    }\n\n    @NonNull\n    @Override\n    public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new ItemViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.ranking_item, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull final ItemViewHolder holder, int position) {\n        CharSequence itemName = HtmlUtils.textToHtml(formEntryPrompt.getSelectChoiceText(items.get(position)));\n        holder.textView.setText(itemName);\n    }\n\n    public void onItemMove(int fromPosition, int toPosition) {\n        Collections.swap(items, fromPosition, toPosition);\n        notifyItemMoved(fromPosition, toPosition);\n    }\n\n    @Override\n    public int getItemCount() {\n        return items.size();\n    }\n\n    public List<SelectChoice> getItems() {\n        return items;\n    }\n\n    public static class ItemViewHolder extends ViewHolder {\n\n        final TextView textView;\n        final ThemeUtils themeUtils;\n        private final ColorDrawable background;\n\n        ItemViewHolder(View itemView) {\n            super(itemView);\n            textView = itemView.findViewById(R.id.rank_item_text);\n            textView.setTextSize(QuestionFontSizeUtils.getQuestionFontSize());\n            themeUtils = new ThemeUtils(itemView.getContext());\n            background = (ColorDrawable) itemView.getBackground();\n        }\n\n        public void onItemSelected() {\n            GradientDrawable border = new GradientDrawable();\n            border.setStroke(10, themeUtils.getColorPrimary());\n            border.setColor(background.getColor());\n            itemView.setBackground(border);\n        }\n\n        public void onItemClear() {\n            itemView.setBackground(background);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/adapters/SelectMultipleListAdapter.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.adapters;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CheckBox;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.widget.AppCompatCheckBox;\nimport androidx.core.content.ContextCompat;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel;\nimport org.odk.collect.android.formentry.questions.NoButtonsItem;\nimport org.odk.collect.android.listeners.SelectItemClickListener;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.imageloader.GlideImageLoader;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class SelectMultipleListAdapter extends AbstractSelectListAdapter {\n\n    private final List<Selection> originallySelectedItems;\n    private final List<Selection> selectedItems;\n    protected SelectItemClickListener listener;\n\n    public SelectMultipleListAdapter(List<Selection> selectedItems, SelectItemClickListener listener,\n                                     Context context, List<SelectChoice> items,\n                                     FormEntryPrompt prompt, ReferenceManager referenceManager, AudioPlayer audioPlayer,\n                                     int playColor, int numColumns, boolean noButtonsMode, MediaUtils mediaUtils) {\n        super(context, items, prompt, referenceManager, audioPlayer, playColor, numColumns, noButtonsMode, mediaUtils);\n        this.originallySelectedItems = new ArrayList<>(selectedItems);\n        this.selectedItems = selectedItems;\n        this.listener = listener;\n    }\n\n    @Override\n    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new ViewHolder(noButtonsMode\n                ? new NoButtonsItem(context, !prompt.isReadOnly(), new GlideImageLoader())\n                : new AudioVideoImageTextLabel(context));\n    }\n\n    class ViewHolder extends AbstractSelectListAdapter.ViewHolder {\n        ViewHolder(View v) {\n            super(v);\n            if (noButtonsMode) {\n                noButtonsItem = (NoButtonsItem) v;\n            } else {\n                audioVideoImageTextLabel = (AudioVideoImageTextLabel) v;\n                audioVideoImageTextLabel.setPlayTextColor(playColor);\n                audioVideoImageTextLabel.setItemClickListener(listener);\n            }\n        }\n\n        void bind(final int index) {\n            super.bind(index);\n            if (noButtonsMode) {\n                noButtonsItem.setBackground(null);\n                for (Selection selectedItem : selectedItems) {\n                    if (filteredItems.get(index).getValue().equals(selectedItem.getValue())) {\n                        noButtonsItem.setBackground(ContextCompat.getDrawable(noButtonsItem.getContext(), R.drawable.select_item_border));\n                        break;\n                    }\n                }\n            } else {\n                adjustAudioVideoImageTextLabelForFlexAppearance();\n            }\n        }\n    }\n\n    @Override\n    CheckBox createButton(final int index, ViewGroup parent) {\n        AppCompatCheckBox checkBox = (AppCompatCheckBox) LayoutInflater.from(parent.getContext()).inflate(R.layout.select_multi_item, null);\n        setUpButton(checkBox, index);\n        checkCheckBoxIfNeeded(checkBox, index); // perform before setting onCheckedChangeListener to avoid redundant calls of its body\n\n        checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {\n            if (isChecked) {\n                addItem(filteredItems.get(index).selection());\n            } else {\n                removeItem(filteredItems.get(index).selection());\n            }\n            if (listener != null) {\n                listener.onItemClicked();\n            }\n        });\n\n        return checkBox;\n    }\n\n    private void checkCheckBoxIfNeeded(CheckBox checkBox, int index) {\n        for (Selection selectedItem : selectedItems) {\n            // match based on value, not key\n            if (filteredItems.get(index).getValue().equals(selectedItem.getValue())) {\n                checkBox.setChecked(true);\n                break;\n            }\n        }\n    }\n\n    @Override\n    void onItemClick(Selection selection, View view) {\n        if (isItemSelected(selectedItems, selection)) {\n            removeItem(selection);\n            if (view != null) {\n                view.setBackground(null);\n            }\n            getAudioPlayer().stop();\n        } else {\n            addItem(selection);\n            if (view != null) {\n                view.setBackground(ContextCompat.getDrawable(view.getContext(), R.drawable.select_item_border));\n            }\n            playAudio(selection.choice);\n        }\n\n        if (listener != null) {\n            listener.onItemClicked();\n        }\n    }\n\n    public void addItem(Selection item) {\n        if (!isItemSelected(selectedItems, item)) {\n            selectedItems.add(item);\n        }\n    }\n\n    public void removeItem(Selection item) {\n        for (Selection selectedItem : selectedItems) {\n            if (selectedItem.getValue().equals(item.getValue())) {\n                selectedItems.remove(selectedItem);\n                break;\n            }\n        }\n    }\n\n    @Override\n    public void clearAnswer() {\n        selectedItems.clear();\n        notifyDataSetChanged();\n    }\n\n    @Override\n    public List<Selection> getSelectedItems() {\n        return selectedItems;\n    }\n\n    @Override\n    public boolean hasAnswerChanged() {\n        if (originallySelectedItems.size() != selectedItems.size()) {\n            return true;\n        }\n        for (Selection item : originallySelectedItems) {\n            boolean foundEqualElement = false;\n            for (Selection item2 : selectedItems) {\n                if (item.xmlValue.equals(item2.xmlValue)) {\n                    foundEqualElement = true;\n                    break;\n                }\n            }\n            if (!foundEqualElement) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/adapters/SelectOneListAdapter.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.adapters;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CompoundButton;\nimport android.widget.RadioButton;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.ContextCompat;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel;\nimport org.odk.collect.android.formentry.questions.NoButtonsItem;\nimport org.odk.collect.android.listeners.SelectItemClickListener;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.imageloader.GlideImageLoader;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class SelectOneListAdapter extends AbstractSelectListAdapter implements CompoundButton.OnCheckedChangeListener {\n    private final String originallySelectedValue;\n    private String selectedValue;\n    private RadioButton selectedRadioButton;\n    private View selectedItem;\n    private SelectItemClickListener listener;\n\n    public SelectOneListAdapter(String selectedValue, SelectItemClickListener listener, Context context,\n                                List<SelectChoice> items, FormEntryPrompt prompt, ReferenceManager referenceManager,\n                                AudioPlayer audioPlayer, int playColor, int numColumns, boolean noButtonsMode, MediaUtils mediaUtils) {\n        super(context, items, prompt, referenceManager, audioPlayer, playColor, numColumns, noButtonsMode, mediaUtils);\n        this.originallySelectedValue = selectedValue;\n        this.selectedValue = selectedValue;\n        this.listener = listener;\n    }\n\n    @Override\n    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new ViewHolder(noButtonsMode\n                ? new NoButtonsItem(context, !prompt.isReadOnly(), new GlideImageLoader())\n                : new AudioVideoImageTextLabel(context));\n    }\n\n    @Override\n    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {\n        if (isChecked) {\n            if (selectedRadioButton != null && buttonView != selectedRadioButton) {\n                selectedRadioButton.setChecked(false);\n            }\n            selectedRadioButton = (RadioButton) buttonView;\n            selectedValue = items.get((int) selectedRadioButton.getTag()).getValue();\n        }\n    }\n\n    public void setSelectItemClickListener(SelectItemClickListener listener) {\n        this.listener = listener;\n    }\n\n    class ViewHolder extends AbstractSelectListAdapter.ViewHolder {\n        ViewHolder(View v) {\n            super(v);\n            if (noButtonsMode) {\n                noButtonsItem = (NoButtonsItem) v;\n            } else {\n                audioVideoImageTextLabel = (AudioVideoImageTextLabel) v;\n                audioVideoImageTextLabel.setPlayTextColor(playColor);\n                audioVideoImageTextLabel.setItemClickListener(listener);\n            }\n        }\n\n        void bind(final int index) {\n            super.bind(index);\n            if (noButtonsMode) {\n                if (filteredItems.get(index).getValue().equals(selectedValue)) {\n                    noButtonsItem.setBackground(ContextCompat.getDrawable(noButtonsItem.getContext(), R.drawable.select_item_border));\n                    selectedItem = noButtonsItem;\n                } else {\n                    noButtonsItem.setBackground(null);\n                }\n            } else {\n                adjustAudioVideoImageTextLabelForFlexAppearance();\n            }\n        }\n    }\n\n    @Override\n    RadioButton createButton(final int index, ViewGroup parent) {\n        RadioButton radioButton = (RadioButton) LayoutInflater.from(parent.getContext()).inflate(R.layout.select_one_item, null);\n        setUpButton(radioButton, index);\n        radioButton.setOnCheckedChangeListener(this);\n\n        String value = filteredItems.get(index).getValue();\n\n        if (value != null && value.equals(selectedValue)) {\n            radioButton.setChecked(true);\n        }\n        return radioButton;\n    }\n\n    @Override\n    void onItemClick(Selection selection, View view) {\n        if (!selection.getValue().equals(selectedValue)) {\n            if (selectedItem != null) {\n                selectedItem.setBackground(null);\n            }\n            view.setBackground(ContextCompat.getDrawable(view.getContext(), R.drawable.select_item_border));\n            selectedItem = view;\n            selectedValue = selection.getValue();\n            playAudio(selection.choice);\n        }\n        listener.onItemClicked();\n    }\n\n    @Override\n    public List<Selection> getSelectedItems() {\n        return getSelectedItem() == null\n                ? new ArrayList<>()\n                : Collections.singletonList(getSelectedItem());\n    }\n\n    @Override\n    public void clearAnswer() {\n        if (selectedRadioButton != null) {\n            selectedRadioButton.setChecked(false);\n        }\n        selectedValue = null;\n        if (selectedItem != null) {\n            selectedItem.setBackground(null);\n            selectedItem = null;\n        }\n    }\n\n    public Selection getSelectedItem() {\n        if (selectedValue != null) {\n            for (SelectChoice item : items) {\n                if (selectedValue.equalsIgnoreCase(item.getValue())) {\n                    return item.selection();\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public boolean hasAnswerChanged() {\n        return !Objects.equals(originallySelectedValue, selectedValue);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt",
    "content": "package org.odk.collect.android.analytics\n\nobject AnalyticsEvents {\n    /**\n     * Track changes to the server URL setting. The action should be the scheme followed by a space\n     * followed by one of Appspot, Ona, Kobo or Other. The label should be a hash of the URL.\n     */\n    const val SET_SERVER = \"SetServer\"\n\n    /**\n     * Track submission encryption. The action should be a hash of the form definition.\n     */\n    const val ENCRYPT_SUBMISSION = \"EncryptSubmission\"\n\n    /**\n     * Track submissions. The action should describe how it's being sent and the label should be a\n     * hash of the form definition.\n     */\n    const val SUBMISSION = \"Submission\"\n\n    /**\n     * Tracks how often instances that have been deleted on disk are opened for editing/viewing\n     */\n    const val OPEN_DELETED_INSTANCE = \"OpenDeletedInstance\"\n\n    /**\n     * Tracks how often users switch between projects\n     */\n    const val SWITCH_PROJECT = \"ProjectSwitch\"\n\n    /**\n     * Tracks how often users choose to try the demo project\n     */\n    const val TRY_DEMO = \"ProjectCreateDemo\"\n\n    /**\n     * Tracks how often projects are created using QR codes.\n     */\n    const val QR_CREATE_PROJECT = \"ProjectCreateQR\"\n\n    /**\n     * Tracks how often projects are created by manually entering details.\n     */\n    const val MANUAL_CREATE_PROJECT = \"ProjectCreateManual\"\n\n    /**\n     * Tracks how often projects with the same connection settings as an existing one are attempted\n     * to be created.\n     */\n    const val DUPLICATE_PROJECT = \"ProjectCreateDuplicate\"\n\n    /**\n     * Tracks how often users try to create projects with the same connection settings but then decide\n     * to switch to an existing project instead. This will give us a sense of whether users are getting\n     * confused about project identity and trying to recreate the same one multiple times.\n     */\n    const val DUPLICATE_PROJECT_SWITCH = \"ProjectCreateDuplicateSwitch\"\n\n    /**\n     * Tracks how often users delete projects\n     */\n    const val DELETE_PROJECT = \"ProjectDelete\"\n\n    /**\n     * These events track how often users change project display settings\n     */\n    const val CHANGE_PROJECT_NAME = \"ProjectChangeName\"\n    const val CHANGE_PROJECT_ICON = \"ProjectChangeIcon\"\n    const val CHANGE_PROJECT_COLOR = \"ProjectChangeColor\"\n\n    /**\n     * Tracks how often users reconfigure a project using QR codes\n     */\n    const val RECONFIGURE_PROJECT = \"ProjectReconfigure\"\n\n    const val FORMS_PROVIDER_QUERY = \"FormsProviderQuery\"\n\n    const val FORMS_PROVIDER_DELETE = \"FormsProviderDelete\"\n\n    const val INSTANCE_PROVIDER_QUERY = \"InstanceProviderQuery\"\n\n    const val INSTANCE_PROVIDER_INSERT = \"InstanceProviderInsert\"\n\n    const val INSTANCE_PROVIDER_DELETE = \"InstanceProviderDelete\"\n\n    /**\n     * Tracks how often drafts that can't be bulk finalized are attempted to be\n     */\n    const val BULK_FINALIZE_ENCRYPTED_FORM = \"BulkFinalizeEncryptedForm\"\n    const val BULK_FINALIZE_SAVE_POINT = \"BulkFinalizeSavePoint\"\n\n    /**\n     * Tracks how often saved forms are manually deleted and in what number\n     */\n    const val DELETE_SAVED_FORM_FEW = \"DeleteSavedFormFew\" // < 10\n    const val DELETE_SAVED_FORM_TENS = \"DeleteSavedFormTens\" // >= 10\n    const val DELETE_SAVED_FORM_HUNDREDS = \"DeleteSavedFormHundreds\" // >= 100\n\n    /**\n     * Tracks how often the INSTANCE_UPLOAD action is used with a custom server URL\n     */\n    const val INSTANCE_UPLOAD_CUSTOM_SERVER = \"InstanceUploadCustomServer\"\n\n    /**\n     * Tracks how often projects are reset\n     */\n    const val RESET_PROJECT = \"ResetProject\"\n\n    /**\n     * Tracks how often finalized or sent forms are edited\n     */\n    const val EDIT_FINALIZED_OR_SENT_FORM = \"EditFinalizedOrSentForm\"\n\n    /**\n     * Tracks how often shortcuts for forms are added\n     */\n    const val ADD_SHORTCUT = \"AddShortcut\"\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsUtils.kt",
    "content": "package org.odk.collect.android.analytics\n\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\nimport org.odk.collect.shared.strings.Md5.getMd5Hash\nimport java.io.ByteArrayInputStream\n\n/**\n * This class should be used to log some specific events that require additional operations, like\n * determining form hash.\n * If you want to log a generic event use [org.odk.collect.analytics.Analytics] directly.\n */\nobject AnalyticsUtils {\n    @JvmStatic\n    fun setForm(formController: FormController) {\n        Analytics.setParam(\"form\", getFormHash(formController))\n    }\n\n    @JvmStatic\n    fun logFormEvent(event: String, formId: String?, formTitle: String?) {\n        Analytics.log(event, \"form\", getFormHash(formId, formTitle))\n    }\n\n    @JvmStatic\n    fun logServerEvent(event: String, generalSettings: Settings) {\n        Analytics.log(event, \"server\", getServerHash(generalSettings)!!)\n    }\n\n    fun logServerConfiguration(url: String) {\n        val upperCaseURL = url.uppercase()\n        val scheme = upperCaseURL.split(\":\".toRegex()).toTypedArray()[0]\n        val urlHash = getMd5Hash(ByteArrayInputStream(url.toByteArray()))\n        Analytics.log(AnalyticsEvents.SET_SERVER, scheme + \" \" + getHostFromUrl(url), urlHash!!)\n    }\n\n    private fun getHostFromUrl(url: String?): String {\n        if (url == null || url.isEmpty()) {\n            return \"\"\n        }\n        val upperCaseURL = url.uppercase()\n        var host = \"Other\"\n        if (upperCaseURL.contains(\"APPSPOT\")) {\n            host = \"Appspot\"\n        } else if (upperCaseURL.contains(\"KOBOTOOLBOX.ORG\") || upperCaseURL.contains(\"HUMANITARIANRESPONSE.INFO\")) {\n            host = \"Kobo\"\n        } else if (upperCaseURL.contains(\"ONA.IO\")) {\n            host = \"Ona\"\n        } else if (upperCaseURL.contains(\"GETODK.CLOUD\")) {\n            host = \"ODK Cloud\"\n        }\n        return host\n    }\n\n    private fun getFormHash(formController: FormController?): String {\n        return if (formController != null) {\n            val formID = formController.getFormDef()?.mainInstance?.root?.getAttributeValue(\"\", \"id\") ?: \"\"\n            getFormHash(formID, formController.getFormTitle())\n        } else {\n            \"\"\n        }\n    }\n\n    private fun getFormHash(formId: String?, formTitle: String?): String {\n        return getMd5Hash(ByteArrayInputStream(\"$formTitle $formId\".toByteArray())) ?: \"\"\n    }\n\n    private fun getServerHash(generalSettings: Settings): String? {\n        val currentServerUrl = generalSettings.getString(ProjectKeys.KEY_SERVER_URL)\n        return getMd5Hash(ByteArrayInputStream(currentServerUrl?.toByteArray()))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/Collect.java",
    "content": "/*\n * Copyright (C) 2017 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.application;\n\nimport static org.odk.collect.settings.keys.MetaKeys.KEY_GOOGLE_BUG_154855417_FIXED;\n\nimport android.app.Application;\nimport android.content.Context;\nimport android.content.res.Configuration;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.dynamicpreload.ExternalDataManager;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.injection.config.AppDependencyComponent;\nimport org.odk.collect.android.injection.config.CollectDrawDependencyModule;\nimport org.odk.collect.android.injection.config.CollectEntitiesDependencyModule;\nimport org.odk.collect.android.injection.config.CollectGeoDependencyModule;\nimport org.odk.collect.android.injection.config.CollectGoogleMapsDependencyModule;\nimport org.odk.collect.android.injection.config.CollectOsmDroidDependencyModule;\nimport org.odk.collect.android.injection.config.CollectProjectsDependencyModule;\nimport org.odk.collect.android.injection.config.CollectSelfieCameraDependencyModule;\nimport org.odk.collect.android.injection.config.DaggerAppDependencyComponent;\nimport org.odk.collect.android.utilities.CollectStrictMode;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.android.utilities.LocaleHelper;\nimport org.odk.collect.androidshared.data.AppState;\nimport org.odk.collect.androidshared.data.StateStore;\nimport org.odk.collect.androidshared.system.ExternalFilesUtils;\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.async.network.NetworkStateProvider;\nimport org.odk.collect.audiorecorder.AudioRecorderDependencyComponent;\nimport org.odk.collect.audiorecorder.AudioRecorderDependencyComponentProvider;\nimport org.odk.collect.audiorecorder.AudioRecorderDependencyModule;\nimport org.odk.collect.audiorecorder.DaggerAudioRecorderDependencyComponent;\nimport org.odk.collect.crashhandler.CrashHandler;\nimport org.odk.collect.draw.DaggerDrawDependencyComponent;\nimport org.odk.collect.draw.DrawDependencyComponent;\nimport org.odk.collect.draw.DrawDependencyComponentProvider;\nimport org.odk.collect.entities.DaggerEntitiesDependencyComponent;\nimport org.odk.collect.entities.EntitiesDependencyComponent;\nimport org.odk.collect.entities.EntitiesDependencyComponentProvider;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.geo.DaggerGeoDependencyComponent;\nimport org.odk.collect.geo.GeoDependencyComponent;\nimport org.odk.collect.geo.GeoDependencyComponentProvider;\nimport org.odk.collect.googlemaps.DaggerGoogleMapsDependencyComponent;\nimport org.odk.collect.googlemaps.GoogleMapsDependencyComponent;\nimport org.odk.collect.googlemaps.GoogleMapsDependencyComponentProvider;\nimport org.odk.collect.location.DaggerLocationDependencyComponent;\nimport org.odk.collect.location.LocationClient;\nimport org.odk.collect.location.LocationDependencyComponent;\nimport org.odk.collect.location.LocationDependencyComponentProvider;\nimport org.odk.collect.location.LocationDependencyModule;\nimport org.odk.collect.maps.layers.ReferenceLayerRepository;\nimport org.odk.collect.osmdroid.DaggerOsmDroidDependencyComponent;\nimport org.odk.collect.osmdroid.OsmDroidDependencyComponent;\nimport org.odk.collect.osmdroid.OsmDroidDependencyComponentProvider;\nimport org.odk.collect.projects.DaggerProjectsDependencyComponent;\nimport org.odk.collect.projects.ProjectsDependencyComponent;\nimport org.odk.collect.projects.ProjectsDependencyComponentProvider;\nimport org.odk.collect.qrcode.mlkit.MlKitBarcodeScannerViewFactory;\nimport org.odk.collect.selfiecamera.DaggerSelfieCameraDependencyComponent;\nimport org.odk.collect.selfiecamera.SelfieCameraDependencyComponent;\nimport org.odk.collect.selfiecamera.SelfieCameraDependencyComponentProvider;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.shared.injection.ObjectProvider;\nimport org.odk.collect.shared.injection.ObjectProviderHost;\nimport org.odk.collect.shared.injection.SupplierObjectProvider;\nimport org.odk.collect.shared.settings.Settings;\nimport org.odk.collect.shared.strings.Md5;\nimport org.odk.collect.strings.localization.LocalizedApplication;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.util.Locale;\n\npublic class Collect extends Application implements\n        LocalizedApplication,\n        AudioRecorderDependencyComponentProvider,\n        ProjectsDependencyComponentProvider,\n        GeoDependencyComponentProvider,\n        OsmDroidDependencyComponentProvider,\n        StateStore,\n        ObjectProviderHost,\n        EntitiesDependencyComponentProvider,\n        SelfieCameraDependencyComponentProvider,\n        GoogleMapsDependencyComponentProvider,\n        DrawDependencyComponentProvider,\n        LocationDependencyComponentProvider {\n\n    public static String defaultSysLanguage;\n    private static Collect singleton;\n\n    private final AppState appState = new AppState();\n    private final SupplierObjectProvider objectProvider = new SupplierObjectProvider();\n\n    private ExternalDataManager externalDataManager;\n    private AppDependencyComponent applicationComponent;\n\n    private AudioRecorderDependencyComponent audioRecorderDependencyComponent;\n    private ProjectsDependencyComponent projectsDependencyComponent;\n    private GeoDependencyComponent geoDependencyComponent;\n    private OsmDroidDependencyComponent osmDroidDependencyComponent;\n    private EntitiesDependencyComponent entitiesDependencyComponent;\n    private SelfieCameraDependencyComponent selfieCameraDependencyComponent;\n    private GoogleMapsDependencyComponent googleMapsDependencyComponent;\n    private DrawDependencyComponent drawDependencyComponent;\n\n    /**\n     * @deprecated we shouldn't have to reference a static singleton of the application. Code doing this\n     * should either have a {@link Context} instance passed to it (or have any references removed if\n     * possible).\n     */\n    @Deprecated\n    public static Collect getInstance() {\n        return singleton;\n    }\n\n    public ExternalDataManager getExternalDataManager() {\n        return externalDataManager;\n    }\n\n    public void setExternalDataManager(ExternalDataManager externalDataManager) {\n        this.externalDataManager = externalDataManager;\n    }\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        singleton = this;\n\n        CrashHandler.install(this).launchApp(\n                () -> ExternalFilesUtils.testExternalFilesAccess(this),\n                () -> {\n                    setupDagger();\n                    DaggerUtils.getComponent(this).inject(this);\n\n                    applicationComponent.applicationInitializer().initialize();\n                    fixGoogleBug154855417();\n                    CollectStrictMode.enable();\n                    MlKitBarcodeScannerViewFactory.init(this);\n                }\n        );\n    }\n\n    private void setupDagger() {\n        applicationComponent = DaggerAppDependencyComponent.builder()\n                .application(this)\n                .build();\n\n        audioRecorderDependencyComponent = DaggerAudioRecorderDependencyComponent.builder()\n                .application(this)\n                .dependencyModule(new AudioRecorderDependencyModule() {\n                    @Override\n                    public @NotNull Scheduler providesScheduler(@NotNull Application application) {\n                        return applicationComponent.scheduler();\n                    }\n\n                    @Override\n                    public @NotNull UniqueIdGenerator providesUniqueIdGenerator() {\n                        return applicationComponent.uniqueIdGenerator();\n                    }\n                })\n                .build();\n\n        projectsDependencyComponent = DaggerProjectsDependencyComponent.builder()\n                .projectsDependencyModule(new CollectProjectsDependencyModule(applicationComponent))\n                .build();\n\n        selfieCameraDependencyComponent = DaggerSelfieCameraDependencyComponent.builder()\n                .selfieCameraDependencyModule(new CollectSelfieCameraDependencyModule(applicationComponent))\n                .build();\n\n        drawDependencyComponent = DaggerDrawDependencyComponent.builder()\n                .drawDependencyModule(new CollectDrawDependencyModule(applicationComponent))\n                .build();\n\n        // Mapbox dependencies\n        objectProvider.addSupplier(SettingsProvider.class, applicationComponent::settingsProvider);\n        objectProvider.addSupplier(NetworkStateProvider.class, applicationComponent::networkStateProvider);\n        objectProvider.addSupplier(ReferenceLayerRepository.class, applicationComponent::referenceLayerRepository);\n        objectProvider.addSupplier(LocationClient.class, applicationComponent::locationClient);\n    }\n\n    @NotNull\n    @Override\n    public AudioRecorderDependencyComponent getAudioRecorderDependencyComponent() {\n        return audioRecorderDependencyComponent;\n    }\n\n    @NotNull\n    @Override\n    public ProjectsDependencyComponent getProjectsDependencyComponent() {\n        return projectsDependencyComponent;\n    }\n\n    @Override\n    public void onConfigurationChanged(Configuration newConfig) {\n        super.onConfigurationChanged(newConfig);\n\n        //noinspection deprecation\n        defaultSysLanguage = newConfig.locale.getLanguage();\n    }\n\n    @Nullable\n    public AppDependencyComponent getComponent() {\n        return applicationComponent;\n    }\n\n    public void setComponent(AppDependencyComponent applicationComponent) {\n        this.applicationComponent = applicationComponent;\n        applicationComponent.inject(this);\n    }\n\n    /**\n     * Gets a unique, privacy-preserving identifier for a form based on its id and version.\n     *\n     * @param formId      id of a form\n     * @param formVersion version of a form\n     * @return md5 hash of the form title, a space, the form ID\n     */\n    public static String getFormIdentifierHash(String formId, String formVersion) {\n        Form form = new FormsRepositoryProvider(Collect.getInstance()).create().getLatestByFormIdAndVersion(formId, formVersion);\n\n        String formTitle = form != null ? form.getDisplayName() : \"\";\n\n        String formIdentifier = formTitle + \" \" + formId;\n        return Md5.getMd5Hash(new ByteArrayInputStream(formIdentifier.getBytes()));\n    }\n\n    // https://issuetracker.google.com/issues/154855417\n    private void fixGoogleBug154855417() {\n        try {\n            Settings metaSharedPreferences = applicationComponent.settingsProvider().getMetaSettings();\n\n            boolean hasFixedGoogleBug154855417 = metaSharedPreferences.getBoolean(KEY_GOOGLE_BUG_154855417_FIXED);\n\n            if (!hasFixedGoogleBug154855417) {\n                File corruptedZoomTables = new File(getFilesDir(), \"ZoomTables.data\");\n                corruptedZoomTables.delete();\n\n                metaSharedPreferences.save(KEY_GOOGLE_BUG_154855417_FIXED, true);\n            }\n        } catch (Exception ignored) {\n            // ignored\n        }\n    }\n\n    @NotNull\n    @Override\n    public Locale getLocale() {\n        if (this.applicationComponent != null) {\n            return LocaleHelper.getLocale(applicationComponent.settingsProvider().getUnprotectedSettings().getString(ProjectKeys.KEY_APP_LANGUAGE));\n        } else {\n            return getResources().getConfiguration().locale;\n        }\n    }\n\n    @NotNull\n    @Override\n    public AppState getState() {\n        return appState;\n    }\n\n    @NonNull\n    @Override\n    public GeoDependencyComponent getGeoDependencyComponent() {\n        if (geoDependencyComponent == null) {\n            geoDependencyComponent = DaggerGeoDependencyComponent.builder()\n                    .application(this)\n                    .geoDependencyModule(new CollectGeoDependencyModule(applicationComponent))\n                    .build();\n        }\n\n        return geoDependencyComponent;\n    }\n\n    @NonNull\n    @Override\n    public OsmDroidDependencyComponent getOsmDroidDependencyComponent() {\n        if (osmDroidDependencyComponent == null) {\n            osmDroidDependencyComponent = DaggerOsmDroidDependencyComponent.builder()\n                    .osmDroidDependencyModule(new CollectOsmDroidDependencyModule(applicationComponent))\n                    .build();\n        }\n\n        return osmDroidDependencyComponent;\n    }\n\n    @NonNull\n    @Override\n    public ObjectProvider getObjectProvider() {\n        return objectProvider;\n    }\n\n    @NonNull\n    @Override\n    public EntitiesDependencyComponent getEntitiesDependencyComponent() {\n        if (entitiesDependencyComponent == null) {\n            entitiesDependencyComponent = DaggerEntitiesDependencyComponent.builder()\n                    .entitiesDependencyModule(new CollectEntitiesDependencyModule(applicationComponent))\n                    .build();\n        }\n\n        return entitiesDependencyComponent;\n    }\n\n    @NonNull\n    @Override\n    public SelfieCameraDependencyComponent getSelfieCameraDependencyComponent() {\n        return selfieCameraDependencyComponent;\n    }\n\n    @NonNull\n    @Override\n    public GoogleMapsDependencyComponent getGoogleMapsDependencyComponent() {\n        if (googleMapsDependencyComponent == null) {\n            googleMapsDependencyComponent = DaggerGoogleMapsDependencyComponent.builder()\n                    .googleMapsDependencyModule(new CollectGoogleMapsDependencyModule(applicationComponent))\n                    .build();\n        }\n\n        return googleMapsDependencyComponent;\n    }\n\n    @NonNull\n    @Override\n    public DrawDependencyComponent getDrawDependencyComponent() {\n        return drawDependencyComponent;\n    }\n\n    @Override\n    public @NotNull LocationDependencyComponent getLocationDependencyComponent() {\n        return DaggerLocationDependencyComponent.builder()\n                .locationDependencyModule(new LocationDependencyModule() {\n                    @Override\n                    public @NotNull UniqueIdGenerator providesUniqueIdGenerator() {\n                        return applicationComponent.uniqueIdGenerator();\n                    }\n                })\n                .build();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/CollectSettingsChangeHandler.kt",
    "content": "package org.odk.collect.android.application\n\nimport org.odk.collect.android.analytics.AnalyticsUtils\nimport org.odk.collect.android.backgroundwork.FormUpdateScheduler\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.metadata.PropertyManager\nimport org.odk.collect.settings.importing.SettingsChangeHandler\nimport org.odk.collect.settings.keys.ProjectKeys\n\nclass CollectSettingsChangeHandler(\n    private val propertyManager: PropertyManager,\n    private val formUpdateScheduler: FormUpdateScheduler,\n    private val formsDataService: FormsDataService\n) : SettingsChangeHandler {\n\n    override fun onSettingChanged(projectId: String, newValue: Any?, changedKey: String) {\n        propertyManager.reload()\n\n        if (changedKey == ProjectKeys.KEY_SERVER_URL) {\n            formsDataService.clear(projectId)\n        }\n\n        if (changedKey == ProjectKeys.KEY_FORM_UPDATE_MODE ||\n            changedKey == ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK\n        ) {\n            formUpdateScheduler.scheduleUpdates(projectId)\n        }\n\n        if (changedKey == ProjectKeys.KEY_SERVER_URL) {\n            AnalyticsUtils.logServerConfiguration(newValue.toString())\n        }\n    }\n\n    override fun onSettingsChanged(\n        projectId: String,\n        changedUnprotectedKeys: List<String>,\n        changedProtectedKeys: List<String>\n    ) {\n        propertyManager.reload()\n        if (changedUnprotectedKeys.contains(ProjectKeys.KEY_FORM_UPDATE_MODE) || changedUnprotectedKeys.contains(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)) {\n            formUpdateScheduler.scheduleUpdates(projectId)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/ComposeTheme.kt",
    "content": "package org.odk.collect.android.application\n\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Shapes\nimport androidx.compose.material3.Typography\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.res.colorResource\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.sp\nimport org.odk.collect.android.R\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider\n\n/**\n * Changes to this theme should also be made in `theme.xml`\n */\n@Composable\nfun CollectTheme(content: @Composable () -> Unit) {\n    val lightColors = lightColorScheme(\n        primary = colorResource(R.color.colorPrimaryLight),\n        onPrimary = colorResource(R.color.colorOnPrimaryLight),\n        surface = colorResource(R.color.colorSurfaceLight),\n        onSurface = colorResource(R.color.colorOnSurfaceLight),\n        primaryContainer = colorResource(R.color.colorPrimaryContainerLight),\n        onPrimaryContainer = colorResource(R.color.colorOnPrimaryContainerLight),\n        surfaceContainerHighest = colorResource(R.color.colorSurfaceContainerHighestLight)\n    )\n\n    val darkColors = darkColorScheme(\n        primary = colorResource(R.color.colorPrimaryDark),\n        onPrimary = colorResource(R.color.colorOnPrimaryDark),\n        surface = colorResource(R.color.colorSurfaceDark),\n        onSurface = colorResource(R.color.colorOnSurfaceDark),\n        primaryContainer = colorResource(R.color.colorPrimaryContainerDark),\n        onPrimaryContainer = colorResource(R.color.colorOnPrimaryContainerDark),\n        surfaceContainerHighest = colorResource(R.color.colorSurfaceContainerHighestDark)\n    )\n\n    val colorScheme = if (isSystemInDarkTheme()) darkColors else lightColors\n\n    val typography = Typography(\n        bodyMedium = MaterialTheme.typography.bodyMedium,\n        bodyLarge = MaterialTheme.typography.bodyLarge,\n        titleMedium = MaterialTheme.typography.titleMedium,\n        titleLarge = MaterialTheme.typography.titleLarge.copy(\n            fontSize = 20.sp,\n            lineHeight = 24.sp,\n            fontFamily = FontFamily.SansSerif,\n            fontWeight = FontWeight.Medium\n        ),\n    )\n\n    val shapes = Shapes(\n        small = MaterialTheme.shapes.small,\n        medium = RoundedCornerShape(dimensionResource(R.dimen.mediumCornerSize)),\n        large = MaterialTheme.shapes.large\n    )\n\n    MaterialTheme(\n        colorScheme = colorScheme,\n        typography = typography,\n        shapes = shapes,\n        content = content\n    )\n}\n\ninterface CollectComposeThemeProvider : ComposeThemeProvider {\n    @Composable\n    override fun Theme(content: @Composable () -> Unit) {\n        CollectTheme { content() }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/FeatureFlags.kt",
    "content": "package org.odk.collect.android.application\n\nobject FeatureFlags\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/MapboxClassInstanceCreator.kt",
    "content": "package org.odk.collect.android.application\n\nimport androidx.fragment.app.Fragment\nimport org.odk.collect.maps.MapConfigurator\nimport org.odk.collect.maps.MapFragment\n\nobject MapboxClassInstanceCreator {\n\n    private const val MAP_FRAGMENT = \"org.odk.collect.mapbox.MapboxMapFragment\"\n\n    @JvmStatic\n    fun isMapboxAvailable(): Boolean {\n        return try {\n            getClass(MAP_FRAGMENT)\n            System.loadLibrary(\"mapbox-common\")\n            true\n        } catch (e: Throwable) {\n            false\n        }\n    }\n\n    fun createMapboxMapFragment(): MapFragment {\n        return createClassInstance(MAP_FRAGMENT)\n    }\n\n    @JvmStatic\n    fun createMapBoxInitializationFragment(): Fragment {\n        return createClassInstance(\"org.odk.collect.mapbox.MapBoxInitializationFragment\")\n    }\n\n    @JvmStatic\n    fun createMapboxMapConfigurator(): MapConfigurator {\n        return createClassInstance(\"org.odk.collect.mapbox.MapboxMapConfigurator\")\n    }\n\n    private fun <T> createClassInstance(className: String): T {\n        return getClass(className).newInstance() as T\n    }\n\n    private fun getClass(className: String): Class<*> = Class.forName(className)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/AnalyticsInitializer.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\n\nclass AnalyticsInitializer(\n    private val analytics: Analytics,\n    private val versionInformation: VersionInformation,\n    private val settingsProvider: SettingsProvider\n) {\n\n    fun initialize() {\n        if (versionInformation.isBeta) {\n            analytics.setAnalyticsCollectionEnabled(true)\n        } else {\n            val analyticsEnabled = settingsProvider.getUnprotectedSettings().getBoolean(ProjectKeys.KEY_ANALYTICS)\n            analytics.setAnalyticsCollectionEnabled(analyticsEnabled)\n        }\n\n        Analytics.setInstance(analytics)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/ApplicationInitializer.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport android.app.Application\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.startup.AppInitializer\nimport net.danlew.android.joda.JodaTimeInitializer\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.application.initialization.upgrade.UpgradeInitializer\nimport org.odk.collect.android.entities.EntitiesRepositoryProvider\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.metadata.PropertyManager\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport timber.log.Timber\nimport java.util.Locale\n\nclass ApplicationInitializer(\n    private val context: Application,\n    private val propertyManager: PropertyManager,\n    private val analytics: Analytics,\n    private val upgradeInitializer: UpgradeInitializer,\n    private val analyticsInitializer: AnalyticsInitializer,\n    private val mapsInitializer: MapsInitializer,\n    private val projectsRepository: ProjectsRepository,\n    private val settingsProvider: SettingsProvider,\n    private val entitiesRepositoryProvider: EntitiesRepositoryProvider,\n    private val projectsDataService: ProjectsDataService,\n    private val scheduler: Scheduler,\n    private val instancesRepositoryProvider: ProjectDependencyFactory<InstancesRepository>,\n    private val formsRepositoryProvider: ProjectDependencyFactory<FormsRepository>\n) {\n    fun initialize() {\n        initializeLocale()\n        runInitializers()\n        initializeFrameworks()\n    }\n\n    private fun runInitializers() {\n        upgradeInitializer.initialize()\n        analyticsInitializer.initialize()\n        UserPropertiesInitializer(\n            analytics,\n            projectsRepository,\n            settingsProvider,\n            context,\n            scheduler,\n            instancesRepositoryProvider,\n            formsRepositoryProvider\n        ).initialize()\n        mapsInitializer.initialize()\n        JavaRosaInitializer(propertyManager, projectsDataService, entitiesRepositoryProvider).initialize()\n    }\n\n    private fun initializeFrameworks() {\n        ToastUtils.setApplication(context)\n        initializeLogging()\n        AppInitializer.getInstance(context).initializeComponent(JodaTimeInitializer::class.java)\n        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)\n    }\n\n    private fun initializeLocale() {\n        Collect.defaultSysLanguage = Locale.getDefault().language\n    }\n\n    private fun initializeLogging() {\n        if (BuildConfig.BUILD_TYPE == \"odkCollectRelease\") {\n            Timber.plant(CrashReportingTree(analytics))\n        } else {\n            Timber.plant(Timber.DebugTree())\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/CachedFormsCleaner.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.odk.collect.android.projects.ProjectDependencyModule\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.upgrade.Upgrade\nimport java.io.File\n\nclass CachedFormsCleaner(\n    private val projectsRepository: ProjectsRepository,\n    private val projectDependencyModuleFactory: ProjectDependencyFactory<ProjectDependencyModule>\n) : Upgrade {\n    override fun key() = null\n\n    override fun run() {\n        projectsRepository.getAll().forEach { project ->\n            val projectDependencyModule = projectDependencyModuleFactory.create(project.uuid)\n            File(projectDependencyModule.cacheDir)\n                .listFiles { file -> file.name.endsWith(\".formdef\") }\n                ?.forEach { file -> file.delete() }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/CrashReportingTree.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport android.util.Log\nimport org.odk.collect.analytics.Analytics\nimport timber.log.Timber\n\ninternal class CrashReportingTree(private val analytics: Analytics) : Timber.Tree() {\n\n    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {\n        when (priority) {\n            Log.WARN -> analytics.logMessage(\"W/$tag: $message\")\n\n            Log.ERROR -> analytics.logNonFatal(t ?: Error(\"E/$tag: $message\"))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/ExistingProjectMigrator.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport android.content.Context\nimport androidx.preference.PreferenceManager\nimport org.apache.commons.io.FileUtils\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.importing.ProjectDetailsCreator\nimport org.odk.collect.settings.keys.MetaKeys\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.upgrade.Upgrade\nimport java.io.File\nimport java.io.FileNotFoundException\n\n/**\n * This migrates from a version before Projects were introduced. Any data and settings will be\n * used to create a new project which is then set as the current one This means the user never\n * goes through the \"first launch\" experience and has their setup available to them immediately\n * after upgrade.\n */\nclass ExistingProjectMigrator(\n    private val context: Context,\n    private val storagePathProvider: StoragePathProvider,\n    private val projectsRepository: ProjectsRepository,\n    private val settingsProvider: SettingsProvider,\n    private val projectsDataService: ProjectsDataService,\n    private val projectDetailsCreator: ProjectDetailsCreator\n) : Upgrade {\n\n    override fun key(): String {\n        return MetaKeys.EXISTING_PROJECT_IMPORTED\n    }\n\n    override fun run() {\n        if (settingsProvider.getMetaSettings().contains(MetaKeys.LAST_LAUNCHED)) {\n            // We're upgrading from a version with Projects so shouldn't be running this\n            return\n        }\n\n        val generalSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)\n\n        val newProject = projectDetailsCreator.createProjectFromDetails(\n            connectionIdentifier = generalSharedPrefs.getString(\n                ProjectKeys.KEY_SERVER_URL,\n                \"\"\n            ) ?: \"\"\n        )\n        val project = projectsRepository.save(newProject)\n\n        val rootDir = storagePathProvider.odkRootDirPath\n        listOf(\n            File(rootDir, \"forms\"),\n            File(rootDir, \"instances\"),\n            File(rootDir, \"metadata\"),\n            File(rootDir, \"layers\"),\n            File(rootDir, \"settings\")\n        ).forEach {\n            try {\n                val rootPath = storagePathProvider.getProjectRootDirPath(project.uuid)\n                FileUtils.moveDirectoryToDirectory(it, File(rootPath), true)\n            } catch (_: FileNotFoundException) {\n                // Original dir doesn't exist - no  need to copy\n            }\n        }\n\n        try {\n            File(rootDir, \".cache\").deleteRecursively()\n        } catch (e: Exception) {\n            // ignore\n        }\n\n        val adminSharedPrefs = context.getSharedPreferences(\"admin_prefs\", Context.MODE_PRIVATE)\n        settingsProvider.getUnprotectedSettings(project.uuid).saveAll(generalSharedPrefs.all)\n        settingsProvider.getProtectedSettings(project.uuid).saveAll(adminSharedPrefs.all)\n\n        projectsDataService.setCurrentProject(project.uuid)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/ExistingSettingsMigrator.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.ODKAppSettingsMigrator\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.upgrade.Upgrade\n\nclass ExistingSettingsMigrator(\n    private val projectsRepository: ProjectsRepository,\n    private val settingsProvider: SettingsProvider,\n    private val settingsMigrator: ODKAppSettingsMigrator\n) : Upgrade {\n\n    override fun key(): String? {\n        return null\n    }\n\n    override fun run() {\n        projectsRepository.getAll().forEach {\n            settingsMigrator.migrate(\n                settingsProvider.getUnprotectedSettings(it.uuid),\n                settingsProvider.getProtectedSettings(it.uuid)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/GoogleDriveProjectsDeleter.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.odk.collect.android.projects.ProjectDeleter\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.upgrade.Upgrade\n\nclass GoogleDriveProjectsDeleter(\n    private val projectsRepository: ProjectsRepository,\n    private val settingsProvider: SettingsProvider,\n    private val projectDeleter: ProjectDeleter\n) : Upgrade {\n    override fun key() = null\n\n    override fun run() {\n        projectsRepository.getAll().forEach {\n            val unprotectedSettings = settingsProvider.getUnprotectedSettings(it.uuid)\n            val protocol = unprotectedSettings.getString(ProjectKeys.KEY_PROTOCOL)\n\n            if (protocol == ProjectKeys.PROTOCOL_GOOGLE_SHEETS) {\n                projectDeleter.deleteProject(it.uuid)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/JavaRosaInitializer.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.javarosa.core.model.CoreModelModule\nimport org.javarosa.core.services.PrototypeManager\nimport org.javarosa.core.util.JavaRosaCoreModule\nimport org.javarosa.model.xform.XFormsModule\nimport org.javarosa.xform.parse.XFormParser\nimport org.javarosa.xform.parse.XFormParserFactory\nimport org.javarosa.xform.util.XFormUtils\nimport org.odk.collect.android.dynamicpreload.DynamicPreloadXFormParserFactory\nimport org.odk.collect.android.logic.actions.setgeopoint.CollectSetGeopointActionHandler\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.entities.javarosa.intance.LocalEntitiesExternalInstanceParserFactory\nimport org.odk.collect.entities.javarosa.parse.EntityXFormParserFactory\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.metadata.PropertyManager\nimport org.odk.collect.projects.ProjectDependencyFactory\n\nclass JavaRosaInitializer(\n    private val propertyManager: PropertyManager,\n    private val projectsDataService: ProjectsDataService,\n    private val entitiesRepositoryProvider: ProjectDependencyFactory<EntitiesRepository>\n) {\n\n    fun initialize() {\n        propertyManager.reload()\n        org.javarosa.core.services.PropertyManager\n            .setPropertyManager(propertyManager)\n\n        // Register prototypes for classes that FormDef uses\n        PrototypeManager.registerPrototypes(JavaRosaCoreModule.classNames)\n        PrototypeManager.registerPrototypes(CoreModelModule.classNames)\n        XFormsModule().registerModule()\n\n        // When registering prototypes from Collect, a proguard exception also needs to be added\n        PrototypeManager.registerPrototype(\"org.odk.collect.android.logic.actions.setgeopoint.CollectSetGeopointAction\")\n        XFormParser.registerActionHandler(\n            CollectSetGeopointActionHandler.ELEMENT_NAME,\n            CollectSetGeopointActionHandler()\n        )\n\n        // Configure default parser factory\n        val entityXFormParserFactory =\n            EntityXFormParserFactory(XFormParserFactory())\n        val dynamicPreloadXFormParserFactory =\n            DynamicPreloadXFormParserFactory(entityXFormParserFactory)\n\n        XFormUtils.setXFormParserFactory(dynamicPreloadXFormParserFactory)\n\n        val localEntitiesExternalInstanceParserFactory = LocalEntitiesExternalInstanceParserFactory(\n            { entitiesRepositoryProvider.create(projectsDataService.requireCurrentProject().uuid) },\n            { true }\n        )\n\n        XFormUtils.setExternalInstanceParserFactory(localEntitiesExternalInstanceParserFactory)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/MapsInitializer.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport android.content.Context\nimport android.os.Handler\nimport com.google.android.gms.maps.MapView\nimport org.odk.collect.android.geo.MapConfiguratorProvider\nimport org.odk.collect.osmdroid.OsmDroidInitializer\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.utilities.UserAgentProvider\nimport timber.log.Timber\nimport javax.inject.Inject\n\nclass MapsInitializer @Inject constructor(\n    private val context: Context,\n    private val settingsProvider: SettingsProvider,\n    private val userAgentProvider: UserAgentProvider\n) {\n\n    fun initialize() {\n        resetToAvailableFramework()\n\n        if (!FRAMEWORKS_INITIALIZED) {\n            initializeFrameworks()\n        }\n    }\n\n    private fun resetToAvailableFramework() {\n        MapConfiguratorProvider.initOptions(context)\n        val availableBaseMaps = MapConfiguratorProvider.getIds()\n        val baseMapSetting =\n            settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE)\n        if (!availableBaseMaps.contains(baseMapSetting)) {\n            settingsProvider.getUnprotectedSettings().save(\n                ProjectKeys.KEY_BASEMAP_SOURCE,\n                availableBaseMaps[0]\n            )\n        }\n    }\n\n    private fun initializeFrameworks() {\n        try {\n            com.google.android.gms.maps.MapsInitializer.initialize(\n                context,\n                com.google.android.gms.maps.MapsInitializer.Renderer.LATEST\n            ) { renderer: com.google.android.gms.maps.MapsInitializer.Renderer ->\n                when (renderer) {\n                    com.google.android.gms.maps.MapsInitializer.Renderer.LATEST -> Timber.d(\"The latest version of Google Maps renderer is used.\")\n                    com.google.android.gms.maps.MapsInitializer.Renderer.LEGACY -> Timber.d(\"The legacy version of Google Maps renderer is used.\")\n                }\n            }\n            val handler = Handler(context.mainLooper)\n            handler.post {\n                // This has to happen on the main thread but we might call `initialize` from tests\n                MapView(context).onCreate(null)\n            }\n            OsmDroidInitializer.initialize(userAgentProvider.userAgent)\n        } catch (ignore: Exception) {\n            // ignored\n        } catch (ignore: Error) {\n            // ignored\n        }\n    }\n\n    companion object {\n        private var FRAMEWORKS_INITIALIZED = false\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/SavepointsImporter.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.odk.collect.android.projects.ProjectDependencyModule\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.keys.MetaKeys\nimport org.odk.collect.upgrade.Upgrade\nimport java.io.File\n\nclass SavepointsImporter(\n    private val projectsRepository: ProjectsRepository,\n    private val projectDependencyModuleFactory: ProjectDependencyFactory<ProjectDependencyModule>\n) : Upgrade {\n    override fun key(): String {\n        return MetaKeys.OLD_SAVEPOINTS_IMPORTED\n    }\n\n    override fun run() {\n        projectsRepository.getAll().forEach { project ->\n            val projectDependencyModule = projectDependencyModuleFactory.create(project.uuid)\n\n            val cacheDir = File(projectDependencyModule.cacheDir)\n            val instancesDir = File(projectDependencyModule.instancesDir)\n\n            importSavepointsThatBelongToSavedForms(projectDependencyModule.instancesRepository, projectDependencyModule.formsRepository, projectDependencyModule.savepointsRepository, cacheDir)\n\n            importSavepointsThatBelongToBlankForms(projectDependencyModule.formsRepository, projectDependencyModule.savepointsRepository, cacheDir, instancesDir)\n        }\n    }\n\n    private fun importSavepointsThatBelongToSavedForms(instancesRepository: InstancesRepository, formsRepository: FormsRepository, savepointsRepository: SavepointsRepository, cacheDir: File) {\n        instancesRepository.all.forEach { instance ->\n            if (instance.deletedDate == null) {\n                val savepointFile = File(cacheDir, File(instance.instanceFilePath).name.plus(\".save\"))\n                if (savepointFile.exists() && savepointFile.lastModified() > instance.lastStatusChangeDate) {\n                    val form = formsRepository.getAllByFormIdAndVersion(instance.formId, instance.formVersion).firstOrNull()\n                    if (form != null && !form.isDeleted) {\n                        savepointsRepository.save(Savepoint(form.dbId, instance.dbId, savepointFile.absolutePath, instance.instanceFilePath))\n                    }\n                }\n            }\n        }\n    }\n\n    private fun importSavepointsThatBelongToBlankForms(formsRepository: FormsRepository, savepointsRepository: SavepointsRepository, cacheDir: File, instancesDir: File) {\n        formsRepository.all.forEach { form ->\n            if (!form.isDeleted) {\n                val formFileName = File(form.formFilePath).name.substringBeforeLast(\".xml\")\n\n                cacheDir.listFiles { file ->\n                    val match = \"\"\"${Regex.escape(formFileName)}_(\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2})(.xml.save)\"\"\"\n                        .toRegex()\n                        .matchEntire(file.name)\n                    match != null\n                }?.forEach { savepointFile ->\n                    if (savepointFile.lastModified() > form.date) {\n                        val instanceFileName = savepointFile.name.substringBefore(\".xml.save\")\n                        savepointsRepository.save(Savepoint(form.dbId, null, savepointFile.absolutePath, File(instancesDir, \"$instanceFileName/$instanceFileName.xml\").absolutePath))\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/ScheduledWorkUpgrade.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.odk.collect.android.backgroundwork.FormUpdateScheduler\nimport org.odk.collect.android.backgroundwork.InstanceSubmitScheduler\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.upgrade.Upgrade\n\n/**\n * Reschedule all background work to prevent problems with tag or class name changes etc\n */\nclass ScheduledWorkUpgrade(\n    private val scheduler: Scheduler,\n    private val projectsRepository: ProjectsRepository,\n    private val formUpdateScheduler: FormUpdateScheduler,\n    private val instanceSubmitScheduler: InstanceSubmitScheduler\n) : Upgrade {\n\n    override fun key(): String? {\n        return null\n    }\n\n    override fun run() {\n        scheduler.cancelAllDeferred()\n\n        projectsRepository.getAll().forEach {\n            formUpdateScheduler.scheduleUpdates(it.uuid)\n            instanceSubmitScheduler.scheduleAutoSend(it.uuid)\n            instanceSubmitScheduler.scheduleFormAutoSend(it.uuid)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/UserPropertiesInitializer.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport android.content.Context\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.instancemanagement.autosend.shouldFormBeSentAutomatically\nimport org.odk.collect.android.preferences.Defaults\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.enums.AutoSend\nimport org.odk.collect.settings.enums.FormUpdateMode\nimport org.odk.collect.settings.enums.StringIdEnumUtils.getAutoSend\nimport org.odk.collect.settings.keys.ProjectKeys\nimport kotlin.time.Duration.Companion.days\n\nclass UserPropertiesInitializer(\n    private val analytics: Analytics,\n    private val projectsRepository: ProjectsRepository,\n    private val settingsProvider: SettingsProvider,\n    private val context: Context,\n    private val scheduler: Scheduler,\n    private val instancesRepositoryProvider: ProjectDependencyFactory<InstancesRepository>,\n    private val formsRepositoryProvider: ProjectDependencyFactory<FormsRepository>\n) {\n\n    fun initialize() {\n        scheduler.immediate {\n            val projects = projectsRepository.getAll()\n\n            analytics.setUserProperty(\"ProjectsCount\", projects.size.toString())\n\n            analytics.setUserProperty(\n                \"UsingLegacyFormUpdate\",\n                projects.any { isNotUsingMatchExactly(it, context) }.toString()\n            )\n\n            analytics.setUserProperty(\n                \"HasUnsentAutosendForms\",\n                projects.any { hasUnsentAutoSendForms(it) }.toString()\n            )\n        }\n    }\n\n    private fun isNotUsingMatchExactly(project: Project.Saved, context: Context): Boolean {\n        val settings = settingsProvider.getUnprotectedSettings(project.uuid)\n        val serverUrl = settings.getString(ProjectKeys.KEY_SERVER_URL)\n        val formUpdateMode = settings.getString(ProjectKeys.KEY_FORM_UPDATE_MODE)\n\n        val notUsingDefaultServer = serverUrl != Defaults.unprotected[ProjectKeys.KEY_SERVER_URL]\n        val notUsingMatchExactly = formUpdateMode != FormUpdateMode.MATCH_EXACTLY.getValue(context)\n\n        return notUsingDefaultServer && notUsingMatchExactly\n    }\n\n    private fun hasUnsentAutoSendForms(project: Project.Saved): Boolean {\n        val instancesRepository = instancesRepositoryProvider.create(project.uuid)\n        val formsRepository = formsRepositoryProvider.create(project.uuid)\n        val finalizedForms = instancesRepository.getAllByStatus(Instance.STATUS_COMPLETE)\n\n        return finalizedForms.any {\n            val form =\n                formsRepository.getLatestByFormIdAndVersion(it.formId, it.formVersion)\n\n            if (form != null) {\n                val isAutoSendEnabled = when (settingsProvider.getUnprotectedSettings().getAutoSend(context)) {\n                    AutoSend.OFF -> false\n                    AutoSend.WIFI_ONLY, AutoSend.CELLULAR_ONLY, AutoSend.WIFI_AND_CELLULAR -> true\n                }\n\n                val formShouldAutosend = form.shouldFormBeSentAutomatically(isAutoSendEnabled)\n                val timeSinceFinalize = System.currentTimeMillis() - it.lastStatusChangeDate\n                formShouldAutosend && timeSinceFinalize > 3.days.inWholeMilliseconds\n            } else {\n                false\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/upgrade/BeforeProjectsInstallDetector.kt",
    "content": "package org.odk.collect.android.application.initialization.upgrade\n\nimport android.content.Context\nimport androidx.preference.PreferenceManager\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.upgrade.AppUpgrader\nimport org.odk.collect.upgrade.InstallDetector\nimport java.io.File\n\n/**\n * Implementation of [InstallDetector] that looks for signs that a version of Collect\n * is installed from before Projects were introduced (< v2021.2). [AppUpgrader] was\n * introduced in that release as well so it and versions after it can use [AppUpgrader]'s\n * built in version tracking.\n */\nclass BeforeProjectsInstallDetector(private val context: Context) : InstallDetector {\n\n    override fun installDetected(): Boolean {\n        val legacyMetadataDir = File(context.getExternalFilesDir(null), \"metadata\")\n        val hasLegacyMetadata = FileUtils.listFiles(legacyMetadataDir).isNotEmpty()\n\n        val hasLegacyGeneralPrefs =\n            PreferenceManager.getDefaultSharedPreferences(context).all.isNotEmpty()\n        val hasLegacyAdminPrefs =\n            context.getSharedPreferences(\"admin_prefs\", Context.MODE_PRIVATE).all.isNotEmpty()\n\n        return hasLegacyMetadata || hasLegacyGeneralPrefs || hasLegacyAdminPrefs\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/application/initialization/upgrade/UpgradeInitializer.kt",
    "content": "package org.odk.collect.android.application.initialization.upgrade\n\nimport android.content.Context\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.android.application.initialization.CachedFormsCleaner\nimport org.odk.collect.android.application.initialization.ExistingProjectMigrator\nimport org.odk.collect.android.application.initialization.ExistingSettingsMigrator\nimport org.odk.collect.android.application.initialization.GoogleDriveProjectsDeleter\nimport org.odk.collect.android.application.initialization.SavepointsImporter\nimport org.odk.collect.android.application.initialization.ScheduledWorkUpgrade\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.MetaKeys\nimport org.odk.collect.upgrade.AppUpgrader\n\nclass UpgradeInitializer(\n    private val context: Context,\n    private val settingsProvider: SettingsProvider,\n    private val existingProjectMigrator: ExistingProjectMigrator,\n    private val existingSettingsMigrator: ExistingSettingsMigrator,\n    private val scheduledWorkUpgrade: ScheduledWorkUpgrade,\n    private val googleDriveProjectsDeleter: GoogleDriveProjectsDeleter,\n    private val savepointsImporter: SavepointsImporter,\n    private val cachedFormsCleaner: CachedFormsCleaner\n) {\n\n    fun initialize() {\n        AppUpgrader(\n            MetaKeys.LAST_LAUNCHED,\n            settingsProvider.getMetaSettings(),\n            BuildConfig.VERSION_CODE,\n            BeforeProjectsInstallDetector(context),\n            listOf(\n                existingProjectMigrator,\n                existingSettingsMigrator,\n                scheduledWorkUpgrade,\n                googleDriveProjectsDeleter,\n                savepointsImporter,\n                cachedFormsCleaner\n            )\n        ).upgradeIfNeeded()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/audio/AMRAppender.java",
    "content": "package org.odk.collect.android.audio;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\n\npublic class AMRAppender implements AudioFileAppender {\n\n    @Override\n    public void append(File one, File two) throws IOException {\n        FileOutputStream fos = new FileOutputStream(one, true);\n        FileInputStream fis = new FileInputStream(two);\n\n        byte[] fileContent = new byte[(int) two.length()];\n        fis.read(fileContent);\n\n        byte[] headerlessFileContent = new byte[fileContent.length - 6];\n        if (fileContent.length - 6 >= 0) {\n            System.arraycopy(fileContent, 6, headerlessFileContent, 0, fileContent.length - 6);\n        }\n\n        fileContent = headerlessFileContent;\n        fos.write(fileContent);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/audio/AudioButton.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.audio;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton;\n\n/**\n * @author ctsims\n * @author carlhartung\n */\npublic class AudioButton extends MultiClickSafeMaterialButton implements View.OnClickListener {\n\n    private Listener listener;\n\n    private Boolean playing = false;\n\n    public AudioButton(Context context) {\n        super(context, null);\n        initView();\n    }\n\n    public AudioButton(Context context, AttributeSet attrs) {\n        super(context, attrs, com.google.android.material.R.style.Widget_MaterialComponents_Button_OutlinedButton_Icon);\n        initView();\n    }\n\n    public Boolean isPlaying() {\n        return playing;\n    }\n\n    public void setPlaying(Boolean isPlaying) {\n        playing = isPlaying;\n        render();\n    }\n\n    public void setListener(Listener listener) {\n        this.listener = listener;\n    }\n\n    @Override\n    public void onClick(View view) {\n        if (playing) {\n            listener.onStopClicked();\n        } else {\n            listener.onPlayClicked();\n        }\n    }\n\n    private void initView() {\n        this.setOnClickListener(this);\n        render();\n    }\n\n    private void render() {\n        if (playing) {\n            setIconResource(R.drawable.ic_stop_black_24dp);\n        } else {\n            setIconResource(R.drawable.ic_volume_up_black_24dp);\n        }\n    }\n\n    public interface Listener {\n\n        void onPlayClicked();\n\n        void onStopClicked();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/audio/AudioControllerView.java",
    "content": "/*\n * Copyright (C) 2018 Shobhit Agarwal\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.audio;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.widget.FrameLayout;\nimport android.widget.SeekBar;\nimport android.widget.TextView;\n\nimport androidx.core.content.ContextCompat;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.databinding.AudioControllerLayoutBinding;\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton;\n\nimport static java.lang.Math.max;\nimport static java.lang.Math.min;\nimport static org.odk.collect.strings.format.LengthFormatterKt.formatLength;\n\npublic class AudioControllerView extends FrameLayout {\n\n    public final AudioControllerLayoutBinding binding;\n\n    private final TextView currentDurationLabel;\n    private final TextView totalDurationLabel;\n    private final MultiClickSafeMaterialButton playButton;\n    private final SeekBar seekBar;\n    private final SwipeListener swipeListener;\n\n    private boolean playing;\n    private int position;\n    private int duration;\n\n    private Listener listener;\n\n    public AudioControllerView(Context context) {\n        this(context, null);\n    }\n\n    public AudioControllerView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n\n        binding = AudioControllerLayoutBinding.inflate(LayoutInflater.from(context), this, true);\n        playButton = binding.play;\n        currentDurationLabel = binding.currentDuration;\n        totalDurationLabel = binding.totalDuration;\n        seekBar = binding.seekBar;\n\n        swipeListener = new SwipeListener();\n        seekBar.setOnSeekBarChangeListener(swipeListener);\n\n        binding.play.setOnClickListener(view -> playClicked());\n        binding.remove.setOnClickListener(view -> {\n            if (listener != null) {\n                listener.onRemoveClicked();\n            }\n        });\n    }\n\n    private void playClicked() {\n        if (listener == null) {\n            return;\n        }\n\n        if (playing) {\n            listener.onPauseClicked();\n        } else {\n            listener.onPlayClicked();\n        }\n    }\n\n    private void onPositionChanged(Integer newPosition) {\n        Integer correctedPosition = max(0, min(duration, newPosition));\n\n        setPosition(correctedPosition);\n        if (listener != null) {\n            listener.onPositionChanged(correctedPosition);\n        }\n    }\n\n    public void setPlaying(Boolean playing) {\n        this.playing = playing;\n\n        if (playing) {\n            playButton.setIcon(ContextCompat.getDrawable(getContext(), R.drawable.ic_pause_24dp));\n        } else {\n            playButton.setIcon(ContextCompat.getDrawable(getContext(), R.drawable.ic_play_arrow_24dp));\n        }\n    }\n\n    public void setListener(Listener listener) {\n        this.listener = listener;\n    }\n\n    public void setPosition(Integer position) {\n        if (!swipeListener.isSwiping()) {\n            renderPosition(position);\n        }\n    }\n\n    public void setDuration(Integer duration) {\n        this.duration = duration;\n\n        totalDurationLabel.setText(formatLength((long) duration));\n        seekBar.setMax(duration);\n        setPosition(0);\n    }\n\n    private void renderPosition(Integer position) {\n        this.position = position;\n\n        currentDurationLabel.setText(formatLength((long) position));\n\n        seekBar.setProgress(position, true);\n    }\n\n    public interface SwipableParent {\n        void allowSwiping(boolean allowSwiping);\n    }\n\n    public interface Listener {\n\n        void onPlayClicked();\n\n        void onPauseClicked();\n\n        void onPositionChanged(Integer newPosition);\n\n        void onRemoveClicked();\n    }\n\n    private class SwipeListener implements SeekBar.OnSeekBarChangeListener {\n\n        private Boolean wasPlaying = false;\n        private Boolean swiping = false;\n\n        @Override\n        public void onStartTrackingTouch(SeekBar seekBar) {\n            swiping = true;\n            ((SwipableParent) getContext()).allowSwiping(false);\n\n            if (playing) {\n                listener.onPauseClicked();\n                wasPlaying = true;\n            }\n        }\n\n        @Override\n        public void onProgressChanged(SeekBar seekBar, int newProgress, boolean fromUser) {\n            if (fromUser) {\n                renderPosition(newProgress);\n            }\n        }\n\n        @Override\n        public void onStopTrackingTouch(SeekBar seekBar) {\n            swiping = false;\n            ((SwipableParent) getContext()).allowSwiping(true);\n\n            onPositionChanged(position);\n\n            if (wasPlaying) {\n                listener.onPlayClicked();\n                wasPlaying = false;\n            }\n        }\n\n        Boolean isSwiping() {\n            return swiping;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/audio/AudioFileAppender.java",
    "content": "package org.odk.collect.android.audio;\n\nimport java.io.File;\nimport java.io.IOException;\n\npublic interface AudioFileAppender {\n\n    void append(File one, File two) throws IOException;\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/audio/AudioRecordingControllerFragment.java",
    "content": "package org.odk.collect.android.audio;\n\nimport static android.view.View.GONE;\nimport static android.view.View.VISIBLE;\nimport static org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing;\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.core.content.ContextCompat;\nimport androidx.fragment.app.Fragment;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.databinding.AudioRecordingControllerFragmentBinding;\nimport org.odk.collect.android.formentry.BackgroundAudioViewModel;\nimport org.odk.collect.android.formentry.FormEntryViewModel;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.androidshared.data.Consumable;\nimport org.odk.collect.androidshared.livedata.LiveDataUtils;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.recording.RecordingSession;\nimport org.odk.collect.strings.format.LengthFormatterKt;\n\nimport javax.inject.Inject;\n\npublic class AudioRecordingControllerFragment extends Fragment {\n\n    @Inject\n    AudioRecorder audioRecorder;\n\n    private final ViewModelProvider.Factory viewModelFactory;\n\n    public AudioRecordingControllerFragmentBinding binding;\n    private FormEntryViewModel formEntryViewModel;\n    private BackgroundAudioViewModel backgroundAudioViewModel;\n\n    public AudioRecordingControllerFragment(ViewModelProvider.Factory viewModelFactory) {\n        this.viewModelFactory = viewModelFactory;\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        DaggerUtils.getComponent(context).inject(this);\n\n        ViewModelProvider viewModelProvider = new ViewModelProvider(requireActivity(), viewModelFactory);\n        formEntryViewModel = viewModelProvider.get(FormEntryViewModel.class);\n        backgroundAudioViewModel = viewModelProvider.get(BackgroundAudioViewModel.class);\n    }\n\n    @Nullable\n    @Override\n    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        binding = AudioRecordingControllerFragmentBinding.inflate(inflater, container, false);\n        return binding.getRoot();\n    }\n\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        LiveDataUtils.combine4(\n                formEntryViewModel.hasBackgroundRecording(),\n                backgroundAudioViewModel.isBackgroundRecordingEnabled(),\n                audioRecorder.getCurrentSession(),\n                audioRecorder.failedToStart()\n        ).observe(getViewLifecycleOwner(), quad -> {\n            if (quad.second != null && quad.fourth != null) {\n                boolean hasBackgroundRecording = quad.first;\n                boolean isBackgroundRecordingEnabled = quad.second;\n                RecordingSession session = quad.third;\n                Consumable<Exception> failedToStart = quad.fourth;\n\n                update(hasBackgroundRecording, isBackgroundRecordingEnabled, session, failedToStart);\n            }\n        });\n\n        binding.stopRecording.setOnClickListener(v -> audioRecorder.stop());\n        binding.help.setOnClickListener(v -> {\n            showIfNotShowing(BackgroundAudioHelpDialogFragment.class, getParentFragmentManager());\n        });\n    }\n\n    private void update(boolean hasBackgroundRecording, boolean isBackgroundRecordingEnabled, RecordingSession session, Consumable<Exception> failedToStart) {\n        if (!failedToStart.isConsumed() && failedToStart.getValue() != null) {\n            showIfNotShowing(AudioRecordingErrorDialogFragment.class, getParentFragmentManager());\n        }\n\n        if (session != null) {\n            if (session.getFile() == null) {\n                binding.getRoot().setVisibility(VISIBLE);\n                renderRecordingInProgress(session, hasBackgroundRecording);\n            } else {\n                binding.getRoot().setVisibility(GONE);\n            }\n        } else {\n            if (hasBackgroundRecording && failedToStart.getValue() != null) {\n                binding.getRoot().setVisibility(VISIBLE);\n                renderRecordingProblem(getLocalizedString(requireContext(), org.odk.collect.strings.R.string.start_recording_failed));\n            } else if (hasBackgroundRecording && !isBackgroundRecordingEnabled) {\n                binding.getRoot().setVisibility(VISIBLE);\n                renderRecordingProblem(getLocalizedString(requireContext(), org.odk.collect.strings.R.string.recording_disabled, \"⋮\"));\n            } else {\n                binding.getRoot().setVisibility(GONE);\n            }\n        }\n    }\n\n    private void renderRecordingProblem(String string) {\n        binding.recordingIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), org.odk.collect.icons.R.drawable.ic_baseline_mic_off_24));\n        binding.timeCode.setText(string);\n        binding.volumeBar.setVisibility(GONE);\n        binding.controls.setVisibility(GONE);\n        binding.help.setVisibility(GONE);\n    }\n\n    private void renderRecordingInProgress(RecordingSession session, boolean hasBackgroundRecording) {\n        binding.timeCode.setText(LengthFormatterKt.formatLength(session.getDuration()));\n        binding.volumeBar.addAmplitude(session.getAmplitude());\n\n        if (hasBackgroundRecording) {\n            binding.controls.setVisibility(GONE);\n            binding.help.setVisibility(VISIBLE);\n        } else {\n            renderControls(session);\n        }\n    }\n\n    private void renderControls(RecordingSession session) {\n        binding.controls.setVisibility(VISIBLE);\n\n        if (session.getPaused()) {\n            binding.pauseRecording.setIcon(ContextCompat.getDrawable(requireContext(), org.odk.collect.icons.R.drawable.ic_baseline_mic_24));\n            binding.pauseRecording.setContentDescription(getString(org.odk.collect.strings.R.string.resume_recording));\n            binding.pauseRecording.setOnClickListener(v -> audioRecorder.resume());\n\n            binding.recordingIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_pause_24dp));\n        } else {\n            binding.pauseRecording.setIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_pause_24dp));\n            binding.pauseRecording.setContentDescription(getString(org.odk.collect.strings.R.string.pause_recording));\n            binding.pauseRecording.setOnClickListener(v -> {\n                audioRecorder.pause();\n            });\n\n            binding.recordingIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), org.odk.collect.icons.R.drawable.ic_baseline_mic_24));\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/audio/AudioRecordingErrorDialogFragment.java",
    "content": "package org.odk.collect.android.audio;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.recording.MicInUseException;\nimport org.odk.collect.androidshared.data.Consumable;\n\nimport javax.inject.Inject;\n\npublic class AudioRecordingErrorDialogFragment extends DialogFragment {\n\n    @Inject\n    AudioRecorder audioRecorder;\n\n    @Nullable\n    Consumable<Exception> exception;\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        DaggerUtils.getComponent(context).inject(this);\n        exception = audioRecorder.failedToStart().getValue();\n    }\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(requireContext())\n                .setPositiveButton(org.odk.collect.strings.R.string.ok, null);\n\n        if (exception != null && exception.getValue() instanceof MicInUseException) {\n            dialogBuilder.setMessage(org.odk.collect.strings.R.string.mic_in_use);\n        } else {\n            dialogBuilder.setMessage(org.odk.collect.strings.R.string.start_recording_failed);\n        }\n\n        return dialogBuilder.create();\n    }\n\n    @Override\n    public void onDismiss(@NonNull DialogInterface dialog) {\n        super.onDismiss(dialog);\n        if (exception != null) {\n            exception.consume();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/audio/BackgroundAudioHelpDialogFragment.java",
    "content": "package org.odk.collect.android.audio;\n\nimport android.app.Dialog;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.odk.collect.android.R;\n\npublic class BackgroundAudioHelpDialogFragment extends DialogFragment {\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        LayoutInflater inflater = LayoutInflater.from(requireContext());\n        View view = inflater.inflate(R.layout.background_audio_help_fragment_layout, null);\n        return new MaterialAlertDialogBuilder(requireContext())\n                .setView(view)\n                .setPositiveButton(org.odk.collect.strings.R.string.ok, null)\n                .create();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/audio/M4AAppender.java",
    "content": "package org.odk.collect.android.audio;\n\nimport org.mp4parser.Container;\nimport org.mp4parser.muxer.Movie;\nimport org.mp4parser.muxer.Track;\nimport org.mp4parser.muxer.builder.DefaultMp4Builder;\nimport org.mp4parser.muxer.container.mp4.MovieCreator;\nimport org.mp4parser.muxer.tracks.AppendTrack;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.nio.channels.FileChannel;\n\npublic class M4AAppender implements AudioFileAppender {\n\n    @Override\n    public void append(File one, File two) throws IOException {\n        Track existingTrack = MovieCreator.build(one.getAbsolutePath()).getTracks().get(0);\n        Track newTrack = MovieCreator.build(two.getAbsolutePath()).getTracks().get(0);\n\n        Movie movie = new Movie();\n        movie.addTrack(new AppendTrack(existingTrack, newTrack));\n\n        Container container = new DefaultMp4Builder().build(movie);\n\n        try (FileChannel fileChannel = new RandomAccessFile(one, \"rw\").getChannel()) {\n            container.writeContainer(fileChannel);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/audio/VolumeBar.java",
    "content": "package org.odk.collect.android.audio;\n\nimport android.content.Context;\nimport android.graphics.drawable.Drawable;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.widget.LinearLayout;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.R;\n\nimport static androidx.core.content.res.ResourcesCompat.getDrawable;\nimport static org.odk.collect.android.utilities.ViewUtils.dpFromPx;\nimport static org.odk.collect.android.utilities.ViewUtils.pxFromDp;\n\npublic class VolumeBar extends LinearLayout {\n\n    /**\n     * Amplitude is reported by Android as a positive Short (16 bit audio)\n     * so cannot be higher than the max value.\n     */\n    public static final int MAX_AMPLITUDE = Short.MAX_VALUE;\n\n    /**\n     * The max amount of pips we want to show on larger screens\n     */\n    public static final int MAX_PIPS = 20;\n\n    private Integer lastAmplitude;\n    private int pips;\n\n    private Drawable filledBackground;\n    private Drawable unfilledBackground;\n    private LayoutParams pipLayoutParams;\n\n    public VolumeBar(@NonNull Context context) {\n        super(context);\n        init(context);\n    }\n\n    public VolumeBar(@NonNull Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n        init(context);\n    }\n\n    public VolumeBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context);\n    }\n\n    private void init(Context context) {\n        setOrientation(LinearLayout.HORIZONTAL);\n\n        // Setup objects used during drawing/rendering amplitude\n        pipLayoutParams = new LayoutParams(0, 0);\n        filledBackground = getDrawable(context.getResources(), R.drawable.pill_filled, context.getTheme());\n        unfilledBackground = getDrawable(context.getResources(), R.drawable.pill_unfilled, context.getTheme());\n    }\n\n    @Override\n    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n        super.onLayout(changed, left, top, right, bottom);\n\n        if (changed && getHeight() > 0) {\n            int pipSize = getBestPipSize(getWidth());\n            int marginSize = pxFromDp(getContext(), 4);\n\n            int possiblePips = (getWidth() + marginSize) / (pipSize + marginSize);\n            this.pips = Math.min(possiblePips, MAX_PIPS);\n\n            this.removeAllViews();\n            for (int i = 0; i < this.pips; i++) {\n                View pip = createPipView(pipSize, marginSize, i != this.pips - 1);\n                addView(pip);\n            }\n        }\n    }\n\n    public void addAmplitude(int amplitude) {\n        lastAmplitude = amplitude;\n\n        if (pips > 0) {\n            int segmentAmplitude = MAX_AMPLITUDE / pips;\n            int adjustedAmplitude = amplitude * 6; // Optimize for voice rather than louder noises\n            int segmentsToFill = adjustedAmplitude / segmentAmplitude;\n\n            for (int i = 0; i < pips; i++) {\n                if (i < segmentsToFill) {\n                    getChildAt(i).setBackground(filledBackground);\n                } else {\n                    getChildAt(i).setBackground(unfilledBackground);\n                }\n            }\n        }\n    }\n\n    @Nullable\n    public Integer getLatestAmplitude() {\n        return lastAmplitude;\n    }\n\n    private int getBestPipSize(int availableWidth) {\n        if (dpFromPx(getContext(), availableWidth) >= 164) {\n            return pxFromDp(getContext(), 24);\n        } else {\n            return pxFromDp(getContext(), 20);\n        }\n    }\n\n    @NotNull\n    private View createPipView(int pipSize, int marginSize, boolean isLast) {\n        View pip = new View(getContext());\n\n        pipLayoutParams.width = pipSize;\n        pipLayoutParams.height = getHeight();\n\n        if (!isLast) {\n            pipLayoutParams.setMarginEnd(marginSize);\n        } else {\n            pipLayoutParams.setMarginEnd(0);\n        }\n\n        pip.setLayoutParams(pipLayoutParams);\n        return pip;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/audio/Waveform.java",
    "content": "package org.odk.collect.android.audio;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.Nullable;\n\nimport com.visualizer.amplitude.AudioRecordView;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.databinding.WaveformLayoutBinding;\n\nimport java.util.Random;\n\npublic class Waveform extends FrameLayout {\n\n    // Allows us to play with the waveform on an emulator\n    public static final boolean SIMULATED = false;\n\n    private AudioRecordView audioRecordView;\n    private Integer lastAmplitude;\n    private boolean mini;\n\n    public Waveform(@NotNull Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public Waveform(@NotNull Context context, @NotNull AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public Waveform(@NotNull Context context, @NotNull AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, @Nullable AttributeSet attrs) {\n        audioRecordView = WaveformLayoutBinding.inflate(LayoutInflater.from(context), this, true).getRoot();\n\n        if (attrs != null) {\n            TypedArray styledAttributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.Waveform, 0, 0);\n            mini = styledAttributes.getBoolean(R.styleable.Waveform_mini, false);\n        }\n    }\n\n    @Override\n    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n        super.onLayout(changed, left, top, right, bottom);\n        audioRecordView.setChunkMaxHeight(getLayoutParams().height);\n    }\n\n    public void addAmplitude(int amplitude) {\n        lastAmplitude = amplitude;\n\n        if (SIMULATED) {\n            amplitude = new Random().nextInt(22760);\n        }\n\n        if (mini) {\n            audioRecordView.update(amplitude * 6);\n        } else {\n            audioRecordView.update(amplitude);\n        }\n    }\n\n    @Nullable\n    public Integer getLatestAmplitude() {\n        return lastAmplitude;\n    }\n\n    public void clear() {\n        if (lastAmplitude != null) {\n            lastAmplitude = null;\n            audioRecordView.recreate();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoUpdateTaskSpec.kt",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.backgroundwork\n\nimport android.content.Context\nimport androidx.work.BackoffPolicy\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.async.TaskSpec\nimport java.util.function.Supplier\nimport javax.inject.Inject\n\nclass AutoUpdateTaskSpec : TaskSpec {\n    @Inject\n    lateinit var formsDataService: FormsDataService\n\n    override val maxRetries: Int? = null\n    override val backoffPolicy: BackoffPolicy? = null\n    override val backoffDelay: Long? = null\n\n    override fun getTask(\n        context: Context,\n        inputData: Map<String, String>,\n        isLastUniqueExecution: Boolean,\n        isStopped: (() -> Boolean)\n    ): Supplier<Boolean> {\n        DaggerUtils.getComponent(context).inject(this)\n        return Supplier {\n            val projectId = inputData[TaskData.DATA_PROJECT_ID]\n            if (projectId != null) {\n                formsDataService.downloadUpdates(projectId)\n                true\n            } else {\n                throw IllegalArgumentException(\"No project ID provided!\")\n            }\n        }\n    }\n\n    override fun onException(exception: Throwable) {\n        Analytics.logNonFatal(exception)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/backgroundwork/BackgroundWorkUtils.java",
    "content": "package org.odk.collect.android.backgroundwork;\n\nimport android.content.Context;\n\npublic final class BackgroundWorkUtils {\n\n    private static final long FIFTEEN_MINUTES_PERIOD = 900000;\n    private static final long ONE_HOUR_PERIOD = 3600000;\n    private static final long SIX_HOURS_PERIOD = 21600000;\n    private static final long ONE_DAY_PERIOD = 86400000;\n\n    private BackgroundWorkUtils() {\n\n    }\n\n    public static long getPeriodInMilliseconds(String period, Context context) {\n        if (period.equals(context.getString(org.odk.collect.strings.R.string.every_one_hour_value))) {\n            return ONE_HOUR_PERIOD;\n        } else if (period.equals(context.getString(org.odk.collect.strings.R.string.every_six_hours_value))) {\n            return SIX_HOURS_PERIOD;\n        } else if (period.equals(context.getString(org.odk.collect.strings.R.string.every_24_hours_value))) {\n            return ONE_DAY_PERIOD;\n        } else if (period.equals(context.getString(org.odk.collect.strings.R.string.every_fifteen_minutes_value))) {\n            return FIFTEEN_MINUTES_PERIOD;\n        } else {\n            throw new IllegalArgumentException();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/backgroundwork/FormUpdateAndInstanceSubmitScheduler.java",
    "content": "package org.odk.collect.android.backgroundwork;\n\nimport static org.odk.collect.android.backgroundwork.BackgroundWorkUtils.getPeriodInMilliseconds;\nimport static org.odk.collect.settings.enums.StringIdEnumUtils.getFormUpdateMode;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK;\n\nimport android.app.Application;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.enums.AutoSend;\nimport org.odk.collect.settings.enums.StringIdEnumUtils;\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.util.HashMap;\n\npublic class FormUpdateAndInstanceSubmitScheduler implements FormUpdateScheduler, InstanceSubmitScheduler {\n\n    private final Scheduler scheduler;\n    private final SettingsProvider settingsProvider;\n    private final Application application;\n\n    public FormUpdateAndInstanceSubmitScheduler(Scheduler scheduler, SettingsProvider settingsProvider, Application application) {\n        this.scheduler = scheduler;\n        this.settingsProvider = settingsProvider;\n        this.application = application;\n    }\n\n    @Override\n    public void scheduleUpdates(String projectId) {\n        Settings generalSettings = settingsProvider.getUnprotectedSettings(projectId);\n        String period = generalSettings.getString(KEY_PERIODIC_FORM_UPDATES_CHECK);\n        long periodInMilliseconds = getPeriodInMilliseconds(period, application);\n\n        switch (getFormUpdateMode(generalSettings, application)) {\n            case MANUAL:\n                scheduler.cancelDeferred(getMatchExactlyTag(projectId));\n                scheduler.cancelDeferred(getAutoUpdateTag(projectId));\n                break;\n            case PREVIOUSLY_DOWNLOADED_ONLY:\n                scheduler.cancelDeferred(getMatchExactlyTag(projectId));\n                scheduleAutoUpdate(periodInMilliseconds, projectId);\n                break;\n            case MATCH_EXACTLY:\n                scheduler.cancelDeferred(getAutoUpdateTag(projectId));\n                scheduleMatchExactly(periodInMilliseconds, projectId);\n                break;\n        }\n    }\n\n    private void scheduleAutoUpdate(long periodInMilliseconds, String projectId) {\n        HashMap<String, String> inputData = new HashMap<>();\n        inputData.put(TaskData.DATA_PROJECT_ID, projectId);\n        scheduler.networkDeferredRepeat(getAutoUpdateTag(projectId), new AutoUpdateTaskSpec(), periodInMilliseconds, inputData);\n    }\n\n    private void scheduleMatchExactly(long periodInMilliseconds, String projectId) {\n        HashMap<String, String> inputData = new HashMap<>();\n        inputData.put(TaskData.DATA_PROJECT_ID, projectId);\n        scheduler.networkDeferredRepeat(getMatchExactlyTag(projectId), new SyncFormsTaskSpec(), periodInMilliseconds, inputData);\n    }\n\n    @Override\n    public void cancelUpdates(String projectId) {\n        scheduler.cancelDeferred(getAutoUpdateTag(projectId));\n        scheduler.cancelDeferred(getMatchExactlyTag(projectId));\n    }\n\n    @Override\n    public void scheduleAutoSend(String projectId) {\n        Scheduler.NetworkType networkConstraint;\n        Settings settings = settingsProvider.getUnprotectedSettings(projectId);\n        AutoSend autoSendSetting = StringIdEnumUtils.getAutoSend(settings, application);\n        if (autoSendSetting == AutoSend.WIFI_ONLY) {\n            networkConstraint = Scheduler.NetworkType.WIFI;\n        } else if (autoSendSetting == AutoSend.CELLULAR_ONLY) {\n            networkConstraint = Scheduler.NetworkType.CELLULAR;\n        } else if (autoSendSetting == AutoSend.WIFI_AND_CELLULAR) {\n            networkConstraint = null;\n        } else {\n            return;\n        }\n\n        HashMap<String, String> inputData = new HashMap<>();\n        inputData.put(TaskData.DATA_PROJECT_ID, projectId);\n        scheduler.networkDeferred(getAutoSendTag(projectId), new SendFormsTaskSpec(), inputData, networkConstraint);\n    }\n\n    @Override\n    public void scheduleFormAutoSend(String projectId) {\n        HashMap<String, String> inputData = new HashMap<>();\n        inputData.put(TaskData.DATA_PROJECT_ID, projectId);\n        inputData.put(TaskData.DATA_FORM_AUTO_SEND, \"\");\n        scheduler.networkDeferred(getAutoSendFormTag(projectId), new SendFormsTaskSpec(), inputData, null);\n    }\n\n    @Override\n    public void cancelSubmit(String projectId) {\n        scheduler.cancelDeferred(getAutoSendTag(projectId));\n    }\n\n    @NotNull\n    public String getAutoSendTag(String projectId) {\n        return \"AutoSendWorker:\" + projectId;\n    }\n\n    public String getAutoSendFormTag(String projectId) {\n        return \"auto_send_form:\" + projectId;\n    }\n\n    @NotNull\n    private String getMatchExactlyTag(String projectId) {\n        return \"match_exactly:\" + projectId;\n    }\n\n    @NotNull\n    private String getAutoUpdateTag(String projectId) {\n        return \"serverPollingJob:\" + projectId;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/backgroundwork/FormUpdateScheduler.java",
    "content": "package org.odk.collect.android.backgroundwork;\n\npublic interface FormUpdateScheduler {\n\n    void scheduleUpdates(String projectId);\n\n    void cancelUpdates(String projectId);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/backgroundwork/InstanceSubmitScheduler.java",
    "content": "package org.odk.collect.android.backgroundwork;\n\npublic interface InstanceSubmitScheduler {\n\n    void scheduleAutoSend(String projectId);\n\n    void scheduleFormAutoSend(String projectId);\n\n    void cancelSubmit(String projectId);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt",
    "content": "/*\n * Copyright (C) 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.backgroundwork\n\nimport android.content.Context\nimport androidx.work.BackoffPolicy\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.async.TaskSpec\nimport java.util.function.Supplier\nimport javax.inject.Inject\n\nclass SendFormsTaskSpec : TaskSpec {\n    @Inject\n    lateinit var instancesDataService: InstancesDataService\n\n    override val maxRetries: Int = 13 // Stop trying when backoff is > 5 days\n    override val backoffPolicy = BackoffPolicy.EXPONENTIAL\n    override val backoffDelay: Long = 60_000\n\n    override fun getTask(\n        context: Context,\n        inputData: Map<String, String>,\n        isLastUniqueExecution: Boolean,\n        isStopped: (() -> Boolean)\n    ): Supplier<Boolean> {\n        DaggerUtils.getComponent(context).inject(this)\n        return Supplier {\n            val projectId = inputData[TaskData.DATA_PROJECT_ID]\n            val formAutoSend = inputData[TaskData.DATA_FORM_AUTO_SEND] != null\n            if (projectId != null) {\n                if (formAutoSend) {\n                    instancesDataService.sendInstances(projectId, formAutoSend = true)\n                } else {\n                    instancesDataService.sendInstances(projectId)\n                }\n            } else {\n                throw IllegalArgumentException(\"No project ID provided!\")\n            }\n        }\n    }\n\n    override fun onException(exception: Throwable) {\n        Analytics.logNonFatal(exception)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/backgroundwork/SyncFormsTaskSpec.kt",
    "content": "package org.odk.collect.android.backgroundwork\n\nimport android.content.Context\nimport androidx.work.BackoffPolicy\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.notifications.Notifier\nimport org.odk.collect.async.TaskSpec\nimport java.util.function.Supplier\nimport javax.inject.Inject\n\nclass SyncFormsTaskSpec : TaskSpec {\n    @Inject\n    lateinit var formsDataService: FormsDataService\n\n    @Inject\n    lateinit var notifier: Notifier\n\n    override val maxRetries = 3\n    override val backoffPolicy = BackoffPolicy.EXPONENTIAL\n    override val backoffDelay: Long = 60_000\n\n    override fun getTask(\n        context: Context,\n        inputData: Map<String, String>,\n        isLastUniqueExecution: Boolean,\n        isStopped: (() -> Boolean)\n    ): Supplier<Boolean> {\n        DaggerUtils.getComponent(context).inject(this)\n\n        return Supplier {\n            val projectId = inputData[TaskData.DATA_PROJECT_ID]\n            if (projectId != null) {\n                formsDataService.matchFormsWithServer(projectId, isLastUniqueExecution, isStopped)\n            } else {\n                throw IllegalArgumentException(\"No project ID provided!\")\n            }\n        }\n    }\n\n    override fun onException(exception: Throwable) {\n        Analytics.logNonFatal(exception)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/backgroundwork/TaskData.kt",
    "content": "package org.odk.collect.android.backgroundwork\n\nobject TaskData {\n    const val DATA_PROJECT_ID = \"projectId\"\n    const val DATA_FORM_AUTO_SEND = \"formAutoSend\"\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/configure/qr/AppConfigurationGenerator.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport org.json.JSONObject\nimport org.odk.collect.android.preferences.Defaults\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.AppConfigurationKeys\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\n\nclass AppConfigurationGenerator(\n    private val settingsProvider: SettingsProvider,\n    private val projectsDataService: ProjectsDataService\n) {\n\n    fun getAppConfigurationAsJsonWithServerDetails(\n        url: String,\n        username: String,\n        password: String\n    ): String {\n        val generalSettings = JSONObject().apply {\n            put(ProjectKeys.KEY_SERVER_URL, url)\n            put(ProjectKeys.KEY_USERNAME, username)\n            put(ProjectKeys.KEY_PASSWORD, password)\n        }\n\n        return JSONObject().apply {\n            put(AppConfigurationKeys.GENERAL, generalSettings)\n            put(AppConfigurationKeys.ADMIN, JSONObject())\n            put(AppConfigurationKeys.PROJECT, JSONObject())\n        }.toString()\n    }\n\n    fun getAppConfigurationAsJson(includedPasswordKeys: Collection<String> = emptyList()): String {\n        return JSONObject().apply {\n            put(AppConfigurationKeys.GENERAL, getUnprotectedPrefsAsJson(includedPasswordKeys))\n            put(AppConfigurationKeys.ADMIN, getProtectedPrefsAsJson(includedPasswordKeys))\n            put(AppConfigurationKeys.PROJECT, getProjectDetailsAsJson())\n        }.toString()\n    }\n\n    private fun getUnprotectedPrefsAsJson(includedPasswordKeys: Collection<String>): JSONObject {\n        val generalPrefs = JSONObject()\n\n        val generalSettings = settingsProvider.getUnprotectedSettings().getAll()\n        val defaultGeneralSettings: Map<String, *> = Defaults.unprotected\n\n        for (key in defaultGeneralSettings.keys) {\n            if (key == ProjectKeys.KEY_PASSWORD && !includedPasswordKeys.contains(ProjectKeys.KEY_PASSWORD)) {\n                continue\n            }\n            val value = generalSettings[key]\n            if (value != null && value != defaultGeneralSettings[key]) {\n                generalPrefs.put(key, value)\n            }\n        }\n        return generalPrefs\n    }\n\n    private fun getProtectedPrefsAsJson(includedPasswordKeys: Collection<String>): JSONObject {\n        val adminPrefs = JSONObject()\n\n        val adminSettings = settingsProvider.getProtectedSettings().getAll()\n        val defaultAdminSettings = Defaults.protected\n\n        for (key in ProtectedProjectKeys.allKeys()) {\n            if (\n                key == ProtectedProjectKeys.KEY_ADMIN_PW &&\n                !includedPasswordKeys.contains(ProtectedProjectKeys.KEY_ADMIN_PW)\n            ) {\n                continue\n            }\n\n            val value = adminSettings[key]\n            if (value != null && value != defaultAdminSettings[key]) {\n                adminPrefs.put(key, value)\n            }\n        }\n\n        return adminPrefs\n    }\n\n    private fun getProjectDetailsAsJson(): JSONObject {\n        val currentProject = projectsDataService.requireCurrentProject()\n\n        return JSONObject().apply {\n            put(AppConfigurationKeys.PROJECT_NAME, currentProject.name)\n            put(AppConfigurationKeys.PROJECT_ICON, currentProject.icon)\n            put(AppConfigurationKeys.PROJECT_COLOR, currentProject.color)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/configure/qr/CachingQRCodeGenerator.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport com.google.zxing.WriterException\nimport org.json.JSONException\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.storage.StorageSubdirectory\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.androidshared.bitmap.ImageFileUtils\nimport org.odk.collect.qrcode.zxing.QRCodeCreator\nimport org.odk.collect.qrcode.zxing.QRCodeDecoder\nimport timber.log.Timber\nimport java.io.File\nimport java.io.IOException\nimport java.security.MessageDigest\nimport java.security.NoSuchAlgorithmException\nimport java.util.Arrays\n\nclass CachingQRCodeGenerator(private val qrCodeCreator: QRCodeCreator) : QRCodeGenerator {\n\n    @Throws(\n        QRCodeDecoder.QRCodeInvalidException::class,\n        NoSuchAlgorithmException::class,\n        IOException::class,\n        WriterException::class\n    )\n    override fun generateQRCode(\n        selectedPasswordKeys: Collection<String>,\n        appConfigurationGenerator: AppConfigurationGenerator\n    ): String {\n        val preferencesString =\n            appConfigurationGenerator.getAppConfigurationAsJson(selectedPasswordKeys)\n        val md = MessageDigest.getInstance(\"MD5\")\n        md.update(preferencesString.toByteArray())\n        val messageDigest = md.digest()\n        var shouldWriteToDisk = true\n\n        // check if settings directory exists, if not then create one\n        val writeDir = File(StoragePathProvider().getOdkDirPath(StorageSubdirectory.SETTINGS))\n        if (!writeDir.exists()) {\n            if (!writeDir.mkdirs()) {\n                Timber.e(Error(\"Error creating directory \" + writeDir.absolutePath))\n            }\n        }\n        val mdCacheFile = File(md5CachePath)\n        if (mdCacheFile.exists()) {\n            val cachedMessageDigest = FileUtils.read(mdCacheFile)\n\n            /*\n             * If the messageDigest generated from the preferences is equal to cachedMessageDigest\n             * then don't generate QRCode and read the one saved in disk\n             */\n            if (Arrays.equals(messageDigest, cachedMessageDigest)) {\n                Timber.i(\"Loading QRCode from the disk...\")\n                shouldWriteToDisk = false\n            }\n        }\n\n        // If the file is not found in the disk or md5Hash not matched\n        if (shouldWriteToDisk) {\n            Timber.i(\"Generating QRCode...\")\n            val time = System.currentTimeMillis()\n            val bmp = qrCodeCreator.createEncoded(preferencesString)\n            Timber.i(\"QR Code generation took : %d ms\", System.currentTimeMillis() - time)\n            Timber.i(\"Saving QR Code to disk... : %s\", qRCodeFilepath)\n            ImageFileUtils.saveBitmapToFile(bmp, qRCodeFilepath)\n            FileUtils.write(mdCacheFile, messageDigest)\n            Timber.i(\"Updated %s file contents\", SETTINGS_MD5_FILE)\n        }\n        return qRCodeFilepath\n    }\n\n    private val qRCodeFilepath: String\n        get() = StoragePathProvider().getOdkDirPath(StorageSubdirectory.SETTINGS) + File.separator + \"collect-settings.png\"\n    private val md5CachePath: String\n        get() = StoragePathProvider().getOdkDirPath(StorageSubdirectory.SETTINGS) + File.separator + SETTINGS_MD5_FILE\n\n    companion object {\n        private const val SETTINGS_MD5_FILE = \".collect-settings-hash\"\n    }\n}\n\ninterface QRCodeGenerator {\n    @Throws(\n        JSONException::class,\n        NoSuchAlgorithmException::class,\n        IOException::class,\n        WriterException::class\n    )\n    fun generateQRCode(\n        selectedPasswordKeys: Collection<String>,\n        appConfigurationGenerator: AppConfigurationGenerator\n    ): String\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/configure/qr/QRCodeActivityResultDelegate.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.widget.Toast\nimport org.odk.collect.analytics.Analytics.Companion.log\nimport org.odk.collect.android.activities.ActivityUtils\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.projects.Project.Saved\nimport org.odk.collect.projects.ProjectConfigurationResult\nimport org.odk.collect.qrcode.zxing.QRCodeDecoder\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport java.io.FileNotFoundException\nimport java.io.InputStream\n\nclass QRCodeActivityResultDelegate(\n    private val activity: Activity,\n    private val settingsImporter: ODKAppSettingsImporter,\n    private val qrCodeDecoder: QRCodeDecoder,\n    private val project: Saved\n) {\n    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        if (requestCode == QRCodeMenuProvider.SELECT_PHOTO && resultCode == Activity.RESULT_OK && data != null) {\n            val imageUri = data.data\n            if (imageUri != null) {\n                val imageStream: InputStream? = try {\n                    activity.contentResolver.openInputStream(imageUri)\n                } catch (e: FileNotFoundException) {\n                    // Not sure how this could happen? If you work it out: write a test!\n                    return\n                }\n                try {\n                    val response = qrCodeDecoder.decode(imageStream)\n\n                    when (settingsImporter.fromJSON(response, project)) {\n                        ProjectConfigurationResult.SUCCESS -> {\n                            log(AnalyticsEvents.RECONFIGURE_PROJECT)\n                            showToast(org.odk.collect.strings.R.string.successfully_imported_settings)\n                            ActivityUtils.startActivityAndCloseAllOthers(\n                                activity,\n                                MainMenuActivity::class.java\n                            )\n                        }\n                        ProjectConfigurationResult.INVALID_SETTINGS -> showToast(org.odk.collect.strings.R.string.invalid_qrcode)\n                        ProjectConfigurationResult.GD_PROJECT -> showToast(org.odk.collect.strings.R.string.settings_with_gd_protocol)\n                    }\n                } catch (e: QRCodeDecoder.QRCodeInvalidException) {\n                    showToast(org.odk.collect.strings.R.string.invalid_qrcode)\n                } catch (e: QRCodeDecoder.QRCodeNotFoundException) {\n                    showToast(org.odk.collect.strings.R.string.qr_code_not_found)\n                }\n            }\n        }\n    }\n\n    private fun showToast(string: Int) {\n        Toast.makeText(activity, activity.getString(string), Toast.LENGTH_LONG).show()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/configure/qr/QRCodeMenuProvider.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport android.content.Intent\nimport android.view.Menu\nimport android.view.MenuInflater\nimport android.view.MenuItem\nimport androidx.core.view.MenuProvider\nimport androidx.fragment.app.FragmentActivity\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.android.R\nimport org.odk.collect.android.utilities.FileProvider\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidshared.ui.ToastUtils.showShortToast\nimport org.odk.collect.androidshared.ui.enableIconsVisibility\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.settings.SettingsProvider\nimport timber.log.Timber\n\nclass QRCodeMenuProvider internal constructor(\n    private val activity: FragmentActivity,\n    private val intentLauncher: IntentLauncher,\n    qrCodeGenerator: QRCodeGenerator,\n    appConfigurationGenerator: AppConfigurationGenerator,\n    private val fileProvider: FileProvider,\n    settingsProvider: SettingsProvider,\n    scheduler: Scheduler\n) : MenuProvider {\n    private var qrFilePath: String? = null\n\n    init {\n        val qrCodeViewModel = ViewModelProvider(\n            activity,\n            QRCodeViewModel.Factory(\n                qrCodeGenerator,\n                appConfigurationGenerator,\n                settingsProvider,\n                scheduler\n            )\n        )[QRCodeViewModel::class.java]\n\n        qrCodeViewModel.filePath.observe(activity) { filePath: String? ->\n            if (filePath != null) {\n                qrFilePath = filePath\n            }\n        }\n    }\n\n    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {\n        menuInflater.inflate(R.menu.qr_code_scan_menu, menu)\n        menu.enableIconsVisibility()\n    }\n\n    override fun onMenuItemSelected(item: MenuItem): Boolean {\n        if (!MultiClickGuard.allowClick(javaClass.name)) {\n            return true\n        }\n\n        when (item.itemId) {\n            R.id.menu_item_scan_sd_card -> {\n                val photoPickerIntent = Intent(Intent.ACTION_GET_CONTENT)\n                photoPickerIntent.type = \"image/*\"\n                intentLauncher.launchForResult(activity, photoPickerIntent, SELECT_PHOTO) {\n                    showShortToast(\n                        activity.getString(\n                            org.odk.collect.strings.R.string.activity_not_found,\n                            activity.getString(org.odk.collect.strings.R.string.choose_image)\n                        )\n                    )\n                    Timber.w(\n                        activity.getString(\n                            org.odk.collect.strings.R.string.activity_not_found,\n                            activity.getString(org.odk.collect.strings.R.string.choose_image)\n                        )\n                    )\n                }\n                return true\n            }\n            R.id.menu_item_share -> {\n                if (qrFilePath != null) {\n                    val intent = Intent().apply {\n                        action = Intent.ACTION_SEND\n                        type = \"image/*\"\n                        putExtra(Intent.EXTRA_STREAM, fileProvider.getURIForFile(qrFilePath))\n                    }\n                    activity.startActivity(intent)\n                }\n                return true\n            }\n        }\n        return false\n    }\n\n    companion object {\n        const val SELECT_PHOTO = 111\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/configure/qr/QRCodeScannerFragment.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport android.content.Context\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.activities.ActivityUtils\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.fragments.BarCodeScannerFragment\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.androidshared.ui.ToastUtils.showLongToast\nimport org.odk.collect.androidshared.ui.ToastUtils.showShortToast\nimport org.odk.collect.androidshared.utils.CompressionUtils\nimport org.odk.collect.projects.ProjectConfigurationResult\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.odk.collect.strings.R\nimport java.io.File\nimport javax.inject.Inject\n\nclass QRCodeScannerFragment : BarCodeScannerFragment() {\n\n    @Inject\n    lateinit var settingsImporter: ODKAppSettingsImporter\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    @Inject\n    lateinit var storagePathProvider: StoragePathProvider\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n    }\n\n    override fun handleScanningResult(result: String) {\n        val oldProjectName = projectsDataService.requireCurrentProject().name\n\n        try {\n            val settingsImportingResult = settingsImporter.fromJSON(\n                CompressionUtils.decompress(result),\n                projectsDataService.requireCurrentProject()\n            )\n\n            when (settingsImportingResult) {\n                ProjectConfigurationResult.SUCCESS -> {\n                    Analytics.log(AnalyticsEvents.RECONFIGURE_PROJECT)\n\n                    val newProjectName = projectsDataService.requireCurrentProject().name\n                    if (newProjectName != oldProjectName) {\n                        File(storagePathProvider.getProjectRootDirPath() + File.separator + oldProjectName).delete()\n                        File(storagePathProvider.getProjectRootDirPath() + File.separator + newProjectName).createNewFile()\n                    }\n\n                    showLongToast(\n                        getString(R.string.successfully_imported_settings)\n                    )\n                    ActivityUtils.startActivityAndCloseAllOthers(\n                        requireActivity(),\n                        MainMenuActivity::class.java\n                    )\n                }\n\n                ProjectConfigurationResult.INVALID_SETTINGS -> {\n                    showLongToast(\n                        getString(\n                            R.string.invalid_qrcode\n                        )\n                    )\n                    restartScanning()\n                }\n\n                ProjectConfigurationResult.GD_PROJECT -> {\n                    showLongToast(\n                        getString(R.string.settings_with_gd_protocol)\n                    )\n                    restartScanning()\n                }\n            }\n        } catch (e: Exception) {\n            showShortToast(getString(R.string.invalid_qrcode))\n            restartScanning()\n        }\n    }\n\n    override fun isQrOnly(): Boolean {\n        return true\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/configure/qr/QRCodeTabsActivity.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.viewpager2.widget.ViewPager2\nimport com.google.android.material.tabs.TabLayout\nimport com.google.android.material.tabs.TabLayoutMediator\nimport org.odk.collect.android.R\nimport org.odk.collect.android.application.CollectComposeThemeProvider\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.utilities.FileProvider\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.androidshared.ui.ListFragmentStateAdapter\nimport org.odk.collect.androidshared.utils.AppBarUtils.setupAppBarLayout\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.permissions.PermissionListener\nimport org.odk.collect.permissions.PermissionsProvider\nimport org.odk.collect.qrcode.zxing.QRCodeDecoder\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport javax.inject.Inject\n\nclass QRCodeTabsActivity : LocalizedActivity(), CollectComposeThemeProvider {\n    @Inject\n    lateinit var qrCodeGenerator: QRCodeGenerator\n\n    @Inject\n    lateinit var intentLauncher: IntentLauncher\n\n    @Inject\n    lateinit var fileProvider: FileProvider\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    @Inject\n    lateinit var qrCodeDecoder: QRCodeDecoder\n\n    @Inject\n    lateinit var settingsImporter: ODKAppSettingsImporter\n\n    @Inject\n    lateinit var appConfigurationGenerator: AppConfigurationGenerator\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    @Inject\n    lateinit var permissionsProvider: PermissionsProvider\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    private lateinit var activityResultDelegate: QRCodeActivityResultDelegate\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        DaggerUtils.getComponent(this).inject(this)\n        setView(R.layout.tabs_layout, false)\n        setupAppBarLayout(\n            this,\n            getString(org.odk.collect.strings.R.string.reconfigure_with_qr_code_settings_title)\n        )\n\n        activityResultDelegate = QRCodeActivityResultDelegate(\n            this,\n            settingsImporter,\n            qrCodeDecoder,\n            projectsDataService.requireCurrentProject()\n        )\n\n        val menuProvider = QRCodeMenuProvider(\n            this,\n            intentLauncher,\n            qrCodeGenerator,\n            appConfigurationGenerator,\n            fileProvider,\n            settingsProvider,\n            scheduler\n        )\n        addMenuProvider(menuProvider, this)\n\n        permissionsProvider.requestCameraPermission(\n            this,\n            object : PermissionListener {\n                override fun granted() {\n                    setupViewPager()\n                }\n\n                override fun additionalExplanationClosed() {\n                    finish()\n                }\n            }\n        )\n    }\n\n    private fun setupViewPager() {\n        val fragmentTitleList = arrayOf(\n            getString(org.odk.collect.strings.R.string.scan_qr_code_fragment_title),\n            getString(org.odk.collect.strings.R.string.view_qr_code_fragment_title)\n        )\n\n        val viewPager = findViewById<ViewPager2>(R.id.viewPager)\n        val tabLayout = findViewById<TabLayout>(R.id.tabLayout)\n        viewPager.adapter = ListFragmentStateAdapter(\n            this,\n            listOf(QRCodeScannerFragment::class.java, ShowQRCodeFragment::class.java)\n        )\n\n        TabLayoutMediator(tabLayout, viewPager) { tab: TabLayout.Tab, position: Int ->\n            tab.text = fragmentTitleList[position]\n        }.attach()\n    }\n\n    public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n        activityResultDelegate.onActivityResult(requestCode, resultCode, data)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/configure/qr/QRCodeViewModel.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport androidx.core.util.Pair\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.android.R\nimport org.odk.collect.androidshared.bitmap.ImageFileUtils\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\nclass QRCodeViewModel(\n    private val qrCodeGenerator: QRCodeGenerator,\n    private val appConfigurationGenerator: AppConfigurationGenerator,\n    private val generalSettings: Settings,\n    private val adminSettings: Settings,\n    private val scheduler: Scheduler\n) : ViewModel() {\n    private val qrCodeFilePath = MutableLiveData<String?>(null)\n    private val qrCodeBitmap = MutableLiveData<Bitmap?>(null)\n    private val _warning = MutableLiveData<Int?>()\n    val warning: LiveData<Int?> = _warning\n    private var includedKeys: Collection<String> = listOf(ProtectedProjectKeys.KEY_ADMIN_PW, ProjectKeys.KEY_PASSWORD)\n\n    val filePath: LiveData<String?>\n        get() = qrCodeFilePath\n    val bitmap: LiveData<Bitmap?>\n        get() = qrCodeBitmap\n\n    init {\n        generateQRCode()\n    }\n\n    fun setIncludedKeys(includedKeys: Collection<String>) {\n        this.includedKeys = includedKeys\n        generateQRCode()\n    }\n\n    private fun generateQRCode() {\n        scheduler.immediate(\n            {\n                try {\n                    val filePath =\n                        qrCodeGenerator.generateQRCode(includedKeys, appConfigurationGenerator)\n                    val options = BitmapFactory.Options()\n                    options.inPreferredConfig = Bitmap.Config.ARGB_8888\n                    val bitmap = ImageFileUtils.getBitmap(filePath, options)\n                    return@immediate Pair(filePath, bitmap)\n                } catch (ignored: Exception) {\n                    // Ignored\n                }\n                null\n            }\n        ) { qrCode: Pair<String, Bitmap>? ->\n            qrCodeFilePath.value = qrCode!!.first\n            qrCodeBitmap.value = qrCode.second\n            val serverPasswordSet = generalSettings.getString(ProjectKeys.KEY_PASSWORD)!!.isNotEmpty()\n            val adminPasswordSet = adminSettings.getString(ProtectedProjectKeys.KEY_ADMIN_PW)!!.isNotEmpty()\n            if (serverPasswordSet || adminPasswordSet) {\n                if (serverPasswordSet && includedKeys.contains(ProjectKeys.KEY_PASSWORD) && adminPasswordSet && includedKeys.contains(\n                        ProtectedProjectKeys.KEY_ADMIN_PW\n                    )\n                ) {\n                    _warning.setValue(org.odk.collect.strings.R.string.qrcode_with_both_passwords)\n                } else if (serverPasswordSet && includedKeys.contains(ProjectKeys.KEY_PASSWORD)) {\n                    _warning.setValue(org.odk.collect.strings.R.string.qrcode_with_server_password)\n                } else if (adminPasswordSet && includedKeys.contains(ProtectedProjectKeys.KEY_ADMIN_PW)) {\n                    _warning.setValue(org.odk.collect.strings.R.string.qrcode_with_admin_password)\n                } else {\n                    _warning.setValue(org.odk.collect.strings.R.string.qrcode_without_passwords)\n                }\n            } else {\n                _warning.setValue(null)\n            }\n        }\n    }\n\n    class Factory constructor(\n        private val qrCodeGenerator: QRCodeGenerator,\n        private val appConfigurationGenerator: AppConfigurationGenerator,\n        private val settingsProvider: SettingsProvider,\n        private val scheduler: Scheduler\n    ) : ViewModelProvider.Factory {\n        override fun <T : ViewModel> create(modelClass: Class<T>): T {\n            return QRCodeViewModel(\n                qrCodeGenerator,\n                appConfigurationGenerator,\n                settingsProvider.getUnprotectedSettings(),\n                settingsProvider.getProtectedSettings(),\n                scheduler\n            ) as T\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/configure/qr/SettingsBarcodeScannerViewFactory.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport android.app.Activity\nimport androidx.lifecycle.LifecycleOwner\nimport org.odk.collect.android.preferences.SettingsExt.getExperimentalOptIn\nimport org.odk.collect.qrcode.BarcodeScannerView\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.qrcode.mlkit.PlayServicesFallbackBarcodeScannerViewFactory\nimport org.odk.collect.qrcode.zxing.ZxingBarcodeScannerViewFactory\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\nclass SettingsBarcodeScannerViewFactory(\n    private val settings: Settings\n) : BarcodeScannerViewContainer.Factory {\n    private val playServicesFallbackFactory = PlayServicesFallbackBarcodeScannerViewFactory(2)\n    private val zxingFactory = ZxingBarcodeScannerViewFactory()\n\n    override fun create(\n        activity: Activity,\n        lifecycleOwner: LifecycleOwner,\n        qrOnly: Boolean,\n        useFrontCamera: Boolean\n    ): BarcodeScannerView {\n        val factory = if (settings.getExperimentalOptIn(ProjectKeys.KEY_ZXING_SCANNING)) {\n            zxingFactory\n        } else {\n            playServicesFallbackFactory\n        }\n\n        return factory.create(activity, lifecycleOwner, qrOnly, useFrontCamera)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/configure/qr/ShowQRCodeFragment.kt",
    "content": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.configure.qr\n\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.graphics.Bitmap\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.ViewGroup.OnHierarchyChangeListener\nimport androidx.appcompat.widget.AppCompatCheckedTextView\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.ViewModelProvider\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.android.R\nimport org.odk.collect.android.databinding.ShowQrcodeFragmentBinding\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport javax.inject.Inject\n\nclass ShowQRCodeFragment : Fragment() {\n    @Inject\n    lateinit var qrCodeGenerator: QRCodeGenerator\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    @Inject\n    lateinit var appConfigurationGenerator: AppConfigurationGenerator\n\n    lateinit var binding: ShowQrcodeFragmentBinding\n\n    private lateinit var qrCodeViewModel: QRCodeViewModel\n\n    private val checkedItems = booleanArrayOf(true, true)\n    private val passwordsSet = booleanArrayOf(true, true)\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        binding = ShowQrcodeFragmentBinding.inflate(inflater)\n        binding.tvPasswordWarning.setOnClickListener { passwordWarningClicked() }\n\n        setHasOptionsMenu(true)\n        passwordsSet[0] = settingsProvider.getProtectedSettings().getString(ProtectedProjectKeys.KEY_ADMIN_PW)!!.isNotEmpty()\n        passwordsSet[1] = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_PASSWORD)!!.isNotEmpty()\n\n        qrCodeViewModel.bitmap.observe(this.viewLifecycleOwner) { bitmap: Bitmap? ->\n            if (bitmap != null) {\n                binding.circularProgressBar.visibility = View.GONE\n                binding.ivQRcode.visibility = View.VISIBLE\n                binding.ivQRcode.setImageBitmap(bitmap)\n            } else {\n                binding.circularProgressBar.visibility = View.VISIBLE\n                binding.ivQRcode.visibility = View.GONE\n            }\n        }\n\n        qrCodeViewModel.warning.observe(this.viewLifecycleOwner) { warning: Int? ->\n            if (warning != null) {\n                binding.tvPasswordWarning.setText(warning)\n                binding.status.visibility = View.VISIBLE\n            } else {\n                binding.status.visibility = View.GONE\n            }\n        }\n        return binding.root\n    }\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n\n        qrCodeViewModel = ViewModelProvider(\n            requireActivity(),\n            QRCodeViewModel.Factory(\n                qrCodeGenerator,\n                appConfigurationGenerator,\n                settingsProvider,\n                scheduler\n            )\n        )[QRCodeViewModel::class.java]\n    }\n\n    private fun passwordWarningClicked() {\n        val items = arrayOf(\n            getString(org.odk.collect.strings.R.string.admin_password),\n            getString(org.odk.collect.strings.R.string.server_password)\n        )\n        MaterialAlertDialogBuilder(requireActivity())\n            .setTitle(org.odk.collect.strings.R.string.include_password_dialog)\n            .setMultiChoiceItems(\n                items,\n                checkedItems\n            ) { _: DialogInterface?, which: Int, isChecked: Boolean ->\n                checkedItems[which] = isChecked\n            }\n            .setCancelable(false)\n            .setPositiveButton(org.odk.collect.strings.R.string.generate) { dialog: DialogInterface, _: Int ->\n                qrCodeViewModel.setIncludedKeys(\n                    selectedPasswordKeys\n                )\n                dialog.dismiss()\n            }\n            .setNegativeButton(org.odk.collect.strings.R.string.cancel) { dialog: DialogInterface, _: Int -> dialog.dismiss() }\n            .create()\n            .apply {\n                // disable checkbox if password not set\n                listView.setOnHierarchyChangeListener(\n                    object : OnHierarchyChangeListener {\n                        override fun onChildViewAdded(parent: View, child: View) {\n                            val text = (child as AppCompatCheckedTextView).text\n                            val itemIndex = listOf(*items).indexOf(text)\n                            if (!passwordsSet[itemIndex]) {\n                                child.setEnabled(passwordsSet[itemIndex])\n                                child.setOnClickListener(null)\n                            }\n                        }\n\n                        override fun onChildViewRemoved(view: View, view1: View) {}\n                    }\n                )\n            }\n            .show()\n    }\n\n    // adding the selected password keys\n    private val selectedPasswordKeys: Collection<String>\n        get() {\n            val keys: MutableCollection<String> = ArrayList()\n\n            // adding the selected password keys\n            if (checkedItems[0]) {\n                keys.add(ProtectedProjectKeys.KEY_ADMIN_PW)\n            }\n            if (checkedItems[1]) {\n                keys.add(ProjectKeys.KEY_PASSWORD)\n            }\n            return keys\n        }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dao/CursorLoaderFactory.java",
    "content": "package org.odk.collect.android.dao;\n\nimport android.net.Uri;\n\nimport androidx.loader.content.CursorLoader;\n\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns;\nimport org.odk.collect.android.external.InstancesContract;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.forms.instances.Instance;\n\n@Deprecated\npublic class CursorLoaderFactory {\n\n    public static final String INTERNAL_QUERY_PARAM = \"internal\";\n    private final ProjectsDataService projectsDataService;\n\n    public CursorLoaderFactory(ProjectsDataService projectsDataService) {\n        this.projectsDataService = projectsDataService;\n    }\n\n    public CursorLoader createSentInstancesCursorLoader(CharSequence charSequence, String sortOrder) {\n        CursorLoader cursorLoader;\n        if (charSequence.length() == 0) {\n            String selection = DatabaseInstanceColumns.STATUS + \"=? or \" + DatabaseInstanceColumns.STATUS + \"=?\";\n            String[] selectionArgs = {Instance.STATUS_SUBMITTED, Instance.STATUS_SUBMISSION_FAILED};\n\n            cursorLoader = getInstancesCursorLoader(selection, selectionArgs, sortOrder);\n        } else {\n            String selection =\n                    \"(\" + DatabaseInstanceColumns.STATUS + \"=? or \"\n                            + DatabaseInstanceColumns.STATUS + \"=?) and \"\n                            + DatabaseInstanceColumns.DISPLAY_NAME + \" LIKE ?\";\n            String[] selectionArgs = {\n                    Instance.STATUS_SUBMITTED,\n                    Instance.STATUS_SUBMISSION_FAILED,\n                    \"%\" + charSequence + \"%\"};\n\n            cursorLoader = getInstancesCursorLoader(selection, selectionArgs, sortOrder);\n        }\n\n        return cursorLoader;\n    }\n\n    public CursorLoader createEditableInstancesCursorLoader(CharSequence charSequence, String sortOrder) {\n        CursorLoader cursorLoader;\n        if (charSequence.length() == 0) {\n            String selection = DatabaseInstanceColumns.STATUS + \"=? or \" + DatabaseInstanceColumns.STATUS + \"=? or \" + DatabaseInstanceColumns.STATUS + \"=? or \" + DatabaseInstanceColumns.STATUS + \"=?\";\n            String[] selectionArgs = {Instance.STATUS_INCOMPLETE, Instance.STATUS_INVALID, Instance.STATUS_VALID, Instance.STATUS_NEW_EDIT};\n\n            cursorLoader = getInstancesCursorLoader(selection, selectionArgs, sortOrder);\n        } else {\n            String selection = \"(\" + DatabaseInstanceColumns.STATUS + \"=? or \" + DatabaseInstanceColumns.STATUS + \"=? or \" + DatabaseInstanceColumns.STATUS + \"=? or \" + DatabaseInstanceColumns.STATUS + \"=?)\" +\n                    \"and \" + DatabaseInstanceColumns.DISPLAY_NAME + \" LIKE ?\";\n            String[] selectionArgs = {\n                    Instance.STATUS_INCOMPLETE, Instance.STATUS_INVALID, Instance.STATUS_VALID, Instance.STATUS_NEW_EDIT,\n                    \"%\" + charSequence + \"%\"\n            };\n\n            cursorLoader = getInstancesCursorLoader(selection, selectionArgs, sortOrder);\n        }\n\n        return cursorLoader;\n    }\n\n    public CursorLoader createFinalizedInstancesCursorLoader(CharSequence charSequence, String sortOrder) {\n        CursorLoader cursorLoader;\n        if (charSequence.length() == 0) {\n            String selection = DatabaseInstanceColumns.STATUS + \"=? or \" + DatabaseInstanceColumns.STATUS + \"=?\";\n            String[] selectionArgs = {Instance.STATUS_COMPLETE, Instance.STATUS_SUBMISSION_FAILED};\n\n            cursorLoader = getInstancesCursorLoader(selection, selectionArgs, sortOrder);\n        } else {\n            String selection =\n                    \"(\" + DatabaseInstanceColumns.STATUS + \"=? or \"\n                            + DatabaseInstanceColumns.STATUS + \"=?) and \"\n                            + DatabaseInstanceColumns.DISPLAY_NAME + \" LIKE ?\";\n            String[] selectionArgs = {\n                    Instance.STATUS_COMPLETE,\n                    Instance.STATUS_SUBMISSION_FAILED,\n                    \"%\" + charSequence + \"%\"};\n\n            cursorLoader = getInstancesCursorLoader(selection, selectionArgs, sortOrder);\n        }\n\n        return cursorLoader;\n    }\n\n    public CursorLoader createCompletedUndeletedInstancesCursorLoader(CharSequence charSequence, String sortOrder) {\n        CursorLoader cursorLoader;\n        if (charSequence.length() == 0) {\n            String selection = DatabaseInstanceColumns.DELETED_DATE + \" IS NULL and (\"\n                    + DatabaseInstanceColumns.STATUS + \"=? or \"\n                    + DatabaseInstanceColumns.STATUS + \"=? or \"\n                    + DatabaseInstanceColumns.STATUS + \"=?)\";\n\n            String[] selectionArgs = {Instance.STATUS_COMPLETE,\n                    Instance.STATUS_SUBMISSION_FAILED,\n                    Instance.STATUS_SUBMITTED};\n\n            cursorLoader = getInstancesCursorLoader(selection, selectionArgs, sortOrder);\n        } else {\n            String selection = DatabaseInstanceColumns.DELETED_DATE + \" IS NULL and (\"\n                    + DatabaseInstanceColumns.STATUS + \"=? or \"\n                    + DatabaseInstanceColumns.STATUS + \"=? or \"\n                    + DatabaseInstanceColumns.STATUS + \"=?) and \"\n                    + DatabaseInstanceColumns.DISPLAY_NAME + \" LIKE ?\";\n\n            String[] selectionArgs = {\n                    Instance.STATUS_COMPLETE,\n                    Instance.STATUS_SUBMISSION_FAILED,\n                    Instance.STATUS_SUBMITTED,\n                    \"%\" + charSequence + \"%\"};\n\n            cursorLoader = getInstancesCursorLoader(selection, selectionArgs, sortOrder);\n        }\n        return cursorLoader;\n    }\n\n    private CursorLoader getInstancesCursorLoader(String selection, String[] selectionArgs, String sortOrder) {\n        Uri uri = InstancesContract.getUri(projectsDataService.requireCurrentProject().getUuid());\n\n        return new CursorLoader(\n                Collect.getInstance(),\n                getUriWithAnalyticsParam(uri),\n                null,\n                selection,\n                selectionArgs,\n                sortOrder);\n    }\n\n    private Uri getUriWithAnalyticsParam(Uri uri) {\n        return uri.buildUpon()\n                .appendQueryParameter(INTERNAL_QUERY_PARAM, \"true\")\n                .build();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dao/helpers/InstancesDaoHelper.java",
    "content": "/*\n * Copyright (C) 2018 Shobhit Agarwal\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dao.helpers;\n\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\n\nimport timber.log.Timber;\n\n/**\n * Provides abstractions over database calls for instances.\n *\n * @deprecated to favor {@link InstancesRepository}\n */\n@Deprecated\npublic final class InstancesDaoHelper {\n\n    private InstancesDaoHelper() {\n\n    }\n\n    /**\n     * Checks the database to determine if the current instance being edited has\n     * already been 'marked completed'. A form can be 'unmarked' complete and\n     * then resaved.\n     *\n     * @return true if form has been marked completed, false otherwise.\n     * <p>\n     * TODO: replace with method in {@link InstancesRepository}\n     * that returns an {@link Instance} object from a path.\n     */\n    public static boolean isInstanceComplete(FormController formController) {\n        // default to false if we're mid form\n        boolean complete = false;\n\n        if (formController != null && formController.getInstanceFile() != null) {\n            // Then see if we've already marked this form as complete before\n            String path = formController.getInstanceFile().getAbsolutePath();\n            Instance instance = new InstancesRepositoryProvider(Collect.getInstance()).create().getOneByPath(path);\n            if (instance != null && instance.getStatus().equals(Instance.STATUS_COMPLETE)) {\n                complete = true;\n            }\n        } else {\n            Timber.w(\"FormController or its instanceFile field has a null value\");\n        }\n\n        return complete;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/DatabaseConstants.java",
    "content": "package org.odk.collect.android.database;\n\npublic final class DatabaseConstants {\n\n    public static final String FORMS_DATABASE_NAME = \"forms.db\";\n    public static final String FORMS_TABLE_NAME = \"forms\";\n    // Please always test upgrades manually when you change this value\n    public static final int FORMS_DATABASE_VERSION = 14;\n\n    public static final String INSTANCES_DATABASE_NAME = \"instances.db\";\n    public static final String INSTANCES_TABLE_NAME = \"instances\";\n    // Please always test upgrades manually when you change this value\n    public static final int INSTANCES_DATABASE_VERSION = 10;\n\n    public static final String SAVEPOINTS_DATABASE_NAME = \"savepoints.db\";\n    public static final String SAVEPOINTS_TABLE_NAME = \"savepoints\";\n    // Please always test upgrades manually when you change this value\n    public static final int SAVEPOINTS_DATABASE_VERSION = 1;\n\n    private DatabaseConstants() {\n\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/DatabaseObjectMapper.kt",
    "content": "package org.odk.collect.android.database\n\nimport android.content.ContentValues\nimport android.database.Cursor\nimport android.provider.BaseColumns\nimport androidx.core.database.getLongOrNull\nimport org.odk.collect.android.database.forms.DatabaseFormColumns\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns\nimport org.odk.collect.androidshared.utils.PathUtils.getAbsoluteFilePath\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.shared.PathUtils.getRelativeFilePath\nimport java.lang.Boolean\n\nobject DatabaseObjectMapper {\n\n    @JvmStatic\n    fun getValuesFromForm(form: Form, formsPath: String): ContentValues {\n        val formFilePath = getRelativeFilePath(formsPath, form.formFilePath)\n        val formMediaPath = form.formMediaPath?.let { getRelativeFilePath(formsPath, it) }\n\n        val values = ContentValues()\n        values.put(BaseColumns._ID, form.dbId)\n        values.put(DatabaseFormColumns.DISPLAY_NAME, form.displayName)\n        values.put(DatabaseFormColumns.DESCRIPTION, form.description)\n        values.put(DatabaseFormColumns.JR_FORM_ID, form.formId)\n        values.put(DatabaseFormColumns.JR_VERSION, form.version)\n        values.put(DatabaseFormColumns.FORM_FILE_PATH, formFilePath)\n        values.put(DatabaseFormColumns.SUBMISSION_URI, form.submissionUri)\n        values.put(DatabaseFormColumns.BASE64_RSA_PUBLIC_KEY, form.basE64RSAPublicKey)\n        values.put(DatabaseFormColumns.MD5_HASH, form.mD5Hash)\n        values.put(DatabaseFormColumns.FORM_MEDIA_PATH, formMediaPath)\n        values.put(DatabaseFormColumns.LANGUAGE, form.language)\n        values.put(DatabaseFormColumns.AUTO_SEND, form.autoSend)\n        values.put(DatabaseFormColumns.DATE, form.date)\n        values.put(DatabaseFormColumns.AUTO_DELETE, form.autoDelete)\n        values.put(DatabaseFormColumns.GEOMETRY_XPATH, form.geometryXpath)\n        values.put(DatabaseFormColumns.LAST_DETECTED_ATTACHMENTS_UPDATE_DATE, form.lastDetectedAttachmentsUpdateDate)\n        values.put(DatabaseFormColumns.USES_ENTITIES, Boolean.toString(form.usesEntities()))\n        return values\n    }\n\n    @JvmStatic\n    fun getFormFromCurrentCursorPosition(\n        cursor: Cursor,\n        formsPath: String,\n        cachePath: String\n    ): Form? {\n        val idColumnIndex = cursor.getColumnIndex(BaseColumns._ID)\n        val displayNameColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.DISPLAY_NAME)\n        val descriptionColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.DESCRIPTION)\n        val jrFormIdColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.JR_FORM_ID)\n        val jrVersionColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.JR_VERSION)\n        val formFilePathColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.FORM_FILE_PATH)\n        val submissionUriColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.SUBMISSION_URI)\n        val base64RSAPublicKeyColumnIndex =\n            cursor.getColumnIndex(DatabaseFormColumns.BASE64_RSA_PUBLIC_KEY)\n        val md5HashColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.MD5_HASH)\n        val dateColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.DATE)\n        val jrCacheFilePathColumnIndex =\n            cursor.getColumnIndex(DatabaseFormColumns.JRCACHE_FILE_PATH)\n        val formMediaPathColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.FORM_MEDIA_PATH)\n        val languageColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.LANGUAGE)\n        val autoSendColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.AUTO_SEND)\n        val autoDeleteColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.AUTO_DELETE)\n        val geometryXpathColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.GEOMETRY_XPATH)\n        val deletedDateColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.DELETED_DATE)\n        val lastDetectedAttachmentsUpdateDateColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.LAST_DETECTED_ATTACHMENTS_UPDATE_DATE)\n        val usesEntitiesColumnIndex = cursor.getColumnIndex(DatabaseFormColumns.USES_ENTITIES)\n        return Form.Builder()\n            .dbId(cursor.getLong(idColumnIndex))\n            .displayName(cursor.getString(displayNameColumnIndex))\n            .description(cursor.getString(descriptionColumnIndex))\n            .formId(cursor.getString(jrFormIdColumnIndex))\n            .version(cursor.getString(jrVersionColumnIndex))\n            .formFilePath(\n                getAbsoluteFilePath(\n                    formsPath,\n                    cursor.getString(formFilePathColumnIndex)\n                )\n            )\n            .submissionUri(cursor.getString(submissionUriColumnIndex))\n            .base64RSAPublicKey(cursor.getString(base64RSAPublicKeyColumnIndex))\n            .md5Hash(cursor.getString(md5HashColumnIndex))\n            .date(cursor.getLong(dateColumnIndex))\n            .jrCacheFilePath(\n                getAbsoluteFilePath(\n                    cachePath,\n                    cursor.getString(jrCacheFilePathColumnIndex)\n                )\n            )\n            .formMediaPath(\n                getAbsoluteFilePath(\n                    formsPath,\n                    cursor.getString(formMediaPathColumnIndex)\n                )\n            )\n            .language(cursor.getString(languageColumnIndex))\n            .autoSend(cursor.getString(autoSendColumnIndex))\n            .autoDelete(cursor.getString(autoDeleteColumnIndex))\n            .geometryXpath(cursor.getString(geometryXpathColumnIndex))\n            .deleted(!cursor.isNull(deletedDateColumnIndex))\n            .lastDetectedAttachmentsUpdateDate(cursor.getLongOrNull(lastDetectedAttachmentsUpdateDateColumnIndex))\n            .usesEntities(Boolean.valueOf(cursor.getString(usesEntitiesColumnIndex)))\n            .build()\n    }\n\n    @JvmStatic\n    fun getInstanceFromValues(values: ContentValues): Instance? {\n        return Instance.Builder()\n            .dbId(values.getAsLong(BaseColumns._ID))\n            .displayName(values.getAsString(DatabaseInstanceColumns.DISPLAY_NAME))\n            .submissionUri(values.getAsString(DatabaseInstanceColumns.SUBMISSION_URI))\n            .canEditWhenComplete(Boolean.parseBoolean(values.getAsString(DatabaseInstanceColumns.CAN_EDIT_WHEN_COMPLETE)))\n            .instanceFilePath(values.getAsString(DatabaseInstanceColumns.INSTANCE_FILE_PATH))\n            .formId(values.getAsString(DatabaseInstanceColumns.JR_FORM_ID))\n            .formVersion(values.getAsString(DatabaseInstanceColumns.JR_VERSION))\n            .status(values.getAsString(DatabaseInstanceColumns.STATUS))\n            .lastStatusChangeDate(values.getAsLong(DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE))\n            .finalizationDate(values.getAsLong(DatabaseInstanceColumns.FINALIZATION_DATE))\n            .deletedDate(values.getAsLong(DatabaseInstanceColumns.DELETED_DATE))\n            .geometry(values.getAsString(DatabaseInstanceColumns.GEOMETRY))\n            .geometryType(values.getAsString(DatabaseInstanceColumns.GEOMETRY_TYPE))\n            .editOf(values.getAsLong(DatabaseInstanceColumns.EDIT_OF))\n            .editNumber(values.getAsLong(DatabaseInstanceColumns.EDIT_NUMBER))\n            .build()\n    }\n\n    @JvmStatic\n    fun getInstanceFromCurrentCursorPosition(cursor: Cursor, instancesPath: String): Instance? {\n        val dbId = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID))\n        val displayNameColumnIndex = cursor.getColumnIndex(DatabaseInstanceColumns.DISPLAY_NAME)\n        val submissionUriColumnIndex = cursor.getColumnIndex(DatabaseInstanceColumns.SUBMISSION_URI)\n        val canEditWhenCompleteIndex =\n            cursor.getColumnIndex(DatabaseInstanceColumns.CAN_EDIT_WHEN_COMPLETE)\n        val instanceFilePathIndex =\n            cursor.getColumnIndex(DatabaseInstanceColumns.INSTANCE_FILE_PATH)\n        val jrFormIdColumnIndex = cursor.getColumnIndex(DatabaseInstanceColumns.JR_FORM_ID)\n        val jrVersionColumnIndex = cursor.getColumnIndex(DatabaseInstanceColumns.JR_VERSION)\n        val statusColumnIndex = cursor.getColumnIndex(DatabaseInstanceColumns.STATUS)\n        val lastStatusChangeDateColumnIndex =\n            cursor.getColumnIndex(DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE)\n        val finalizationDateColumnIndex =\n            cursor.getColumnIndex(DatabaseInstanceColumns.FINALIZATION_DATE)\n        val deletedDateColumnIndex = cursor.getColumnIndex(DatabaseInstanceColumns.DELETED_DATE)\n        val geometryTypeColumnIndex = cursor.getColumnIndex(DatabaseInstanceColumns.GEOMETRY_TYPE)\n        val geometryColumnIndex = cursor.getColumnIndex(DatabaseInstanceColumns.GEOMETRY)\n        val databaseIdIndex = cursor.getColumnIndex(BaseColumns._ID)\n        val canDeleteBeforeSendIndex =\n            cursor.getColumnIndex(DatabaseInstanceColumns.CAN_DELETE_BEFORE_SEND)\n        val editOfColumnIndex = cursor.getColumnIndex(DatabaseInstanceColumns.EDIT_OF)\n        val editNumberColumnIndex = cursor.getColumnIndex(DatabaseInstanceColumns.EDIT_NUMBER)\n\n        return Instance.Builder()\n            .dbId(dbId)\n            .displayName(cursor.getString(displayNameColumnIndex))\n            .submissionUri(cursor.getString(submissionUriColumnIndex))\n            .canEditWhenComplete(Boolean.valueOf(cursor.getString(canEditWhenCompleteIndex)))\n            .instanceFilePath(\n                getAbsoluteFilePath(\n                    instancesPath,\n                    cursor.getString(instanceFilePathIndex)\n                )\n            )\n            .formId(cursor.getString(jrFormIdColumnIndex))\n            .formVersion(cursor.getString(jrVersionColumnIndex))\n            .status(cursor.getString(statusColumnIndex))\n            .lastStatusChangeDate(cursor.getLong(lastStatusChangeDateColumnIndex))\n            .finalizationDate(cursor.getLongOrNull(finalizationDateColumnIndex))\n            .deletedDate(cursor.getLongOrNull(deletedDateColumnIndex))\n            .geometryType(cursor.getString(geometryTypeColumnIndex))\n            .geometry(cursor.getString(geometryColumnIndex))\n            .dbId(cursor.getLong(databaseIdIndex))\n            .canDeleteBeforeSend(Boolean.valueOf(cursor.getString(canDeleteBeforeSendIndex)))\n            .editOf(cursor.getLongOrNull(editOfColumnIndex))\n            .editNumber(cursor.getLongOrNull(editNumberColumnIndex))\n            .build()\n    }\n\n    @JvmStatic\n    fun getValuesFromInstance(instance: Instance, instancesPath: String): ContentValues {\n        val values = ContentValues()\n        values.put(BaseColumns._ID, instance.dbId)\n        values.put(DatabaseInstanceColumns.DISPLAY_NAME, instance.displayName)\n        values.put(DatabaseInstanceColumns.SUBMISSION_URI, instance.submissionUri)\n        values.put(\n            DatabaseInstanceColumns.CAN_EDIT_WHEN_COMPLETE,\n            Boolean.toString(instance.canEditWhenComplete())\n        )\n        values.put(\n            DatabaseInstanceColumns.INSTANCE_FILE_PATH,\n            getRelativeFilePath(instancesPath, instance.instanceFilePath)\n        )\n        values.put(DatabaseInstanceColumns.JR_FORM_ID, instance.formId)\n        values.put(DatabaseInstanceColumns.JR_VERSION, instance.formVersion)\n        values.put(DatabaseInstanceColumns.STATUS, instance.status)\n        values.put(DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE, instance.lastStatusChangeDate)\n        values.put(DatabaseInstanceColumns.FINALIZATION_DATE, instance.finalizationDate)\n        values.put(DatabaseInstanceColumns.DELETED_DATE, instance.deletedDate)\n        values.put(DatabaseInstanceColumns.GEOMETRY, instance.geometry)\n        values.put(DatabaseInstanceColumns.GEOMETRY_TYPE, instance.geometryType)\n        values.put(\n            DatabaseInstanceColumns.CAN_DELETE_BEFORE_SEND,\n            Boolean.toString(instance.canDeleteBeforeSend())\n        )\n        values.put(DatabaseInstanceColumns.EDIT_OF, instance.editOf)\n        values.put(DatabaseInstanceColumns.EDIT_NUMBER, instance.editNumber)\n\n        return values\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/entities/DatabaseEntitiesRepository.kt",
    "content": "package org.odk.collect.android.database.entities\n\nimport android.content.ContentValues\nimport android.content.Context\nimport android.database.Cursor\nimport android.database.sqlite.SQLiteDatabase\nimport android.database.sqlite.SQLiteException\nimport android.provider.BaseColumns._ID\nimport org.odk.collect.db.sqlite.CursorExt.first\nimport org.odk.collect.db.sqlite.CursorExt.foldAndClose\nimport org.odk.collect.db.sqlite.CursorExt.getBoolean\nimport org.odk.collect.db.sqlite.CursorExt.getInt\nimport org.odk.collect.db.sqlite.CursorExt.getLongOrNull\nimport org.odk.collect.db.sqlite.CursorExt.getString\nimport org.odk.collect.db.sqlite.CursorExt.getStringOrNull\nimport org.odk.collect.db.sqlite.CursorExt.rowToMap\nimport org.odk.collect.db.sqlite.MigrationListDatabaseMigrator\nimport org.odk.collect.db.sqlite.RowNumbers.invalidateRowNumbers\nimport org.odk.collect.db.sqlite.RowNumbers.rawQueryWithRowNumber\nimport org.odk.collect.db.sqlite.SQLiteColumns.ROW_NUMBER\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt.addColumn\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt.copyTableContent\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt.dropTable\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt.getColumnNames\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt.query\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt.renameTable\nimport org.odk.collect.db.sqlite.SynchronizedDatabaseConnection\nimport org.odk.collect.db.sqlite.toSql\nimport org.odk.collect.entities.javarosa.parse.EntitySchema\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.entities.storage.EntityList\nimport org.odk.collect.entities.storage.QueryException\nimport org.odk.collect.entities.storage.getListNames\nimport org.odk.collect.shared.Query\nimport org.odk.collect.shared.mapColumns\n\nprivate object ListsTable {\n    const val TABLE_NAME = \"lists\"\n    const val COLUMN_NAME = \"name\"\n    const val COLUMN_HASH = \"hash\"\n    const val COLUMN_NEEDS_APPROVAL = \"needs_approval\"\n    const val COLUMN_LAST_UPDATED = \"last_updated\"\n}\n\nprivate object EntitiesTable {\n    const val COLUMN_ID = \"id\"\n    const val COLUMN_LABEL = \"label\"\n    const val COLUMN_VERSION = \"version\"\n    const val COLUMN_TRUNK_VERSION = \"trunk_version\"\n    const val COLUMN_BRANCH_ID = \"branch_id\"\n    const val COLUMN_STATE = \"state\"\n    const val COLUMN_PROPERTY_PREFIX = \"p_\"\n\n    fun getPropertyColumn(property: String) = \"$COLUMN_PROPERTY_PREFIX$property\"\n}\n\nclass DatabaseEntitiesRepository(context: Context, dbPath: String, private val clock: () -> Long) :\n    EntitiesRepository {\n\n    private val databaseConnection = SynchronizedDatabaseConnection(\n        context,\n        dbPath,\n        \"entities.db\",\n        EntitiesDatabaseMigrator(DATABASE_VERSION),\n        DATABASE_VERSION\n    )\n\n    override fun save(list: String, vararg entities: Entity) {\n        if (entities.isEmpty()) {\n            return\n        }\n\n        val listExists = listExists(list)\n        if (!listExists) {\n            createList(list)\n        }\n\n        updatePropertyColumns(list, entities.first())\n\n        databaseConnection.transaction {\n            entities.forEach { entity ->\n                val existing = if (listExists) {\n                    query(\n                        quote(list),\n                        \"${EntitiesTable.COLUMN_ID} = ?\",\n                        arrayOf(entity.id)\n                    ).first { mapCursorRowToEntity(it, 0) }\n                } else {\n                    null\n                }\n\n                if (existing != null) {\n                    val state = if (existing.state == Entity.State.OFFLINE) {\n                        entity.state\n                    } else {\n                        Entity.State.ONLINE\n                    }\n\n                    val contentValues = ContentValues().also {\n                        it.put(EntitiesTable.COLUMN_ID, entity.id)\n                        it.put(EntitiesTable.COLUMN_LABEL, entity.label ?: existing.label)\n                        it.put(EntitiesTable.COLUMN_VERSION, entity.version)\n                        it.put(EntitiesTable.COLUMN_TRUNK_VERSION, entity.trunkVersion)\n                        it.put(EntitiesTable.COLUMN_BRANCH_ID, entity.branchId)\n                        it.put(EntitiesTable.COLUMN_STATE, convertStateToInt(state))\n\n                        addPropertiesToContentValues(it, entity)\n                    }\n\n                    update(\n                        quote(list),\n                        contentValues,\n                        \"${EntitiesTable.COLUMN_ID} = ?\",\n                        arrayOf(entity.id)\n                    )\n                } else {\n                    val contentValues = ContentValues().also {\n                        it.put(EntitiesTable.COLUMN_ID, entity.id)\n                        it.put(EntitiesTable.COLUMN_LABEL, entity.label)\n                        it.put(EntitiesTable.COLUMN_VERSION, entity.version)\n                        it.put(EntitiesTable.COLUMN_TRUNK_VERSION, entity.trunkVersion)\n                        it.put(EntitiesTable.COLUMN_BRANCH_ID, entity.branchId)\n                        it.put(EntitiesTable.COLUMN_STATE, convertStateToInt(entity.state))\n\n                        addPropertiesToContentValues(it, entity)\n                    }\n\n                    insertOrThrow(\n                        quote(list),\n                        null,\n                        contentValues\n                    )\n                }\n            }\n        }\n\n        invalidateRowNumbers()\n    }\n\n    override fun getLists(): List<EntityList> {\n        return databaseConnection.withConnection {\n            readableDatabase\n                .query(ListsTable.TABLE_NAME)\n                .foldAndClose(emptyList()) { list, cursor ->\n                    list + mapCursorRowToEntityList(cursor)\n                }\n        }\n    }\n\n    override fun updateList(list: String, hash: String, needsApproval: Boolean) {\n        if (!listExists(list)) {\n            createList(list)\n        }\n\n        val contentValues = ContentValues().also {\n            it.put(ListsTable.COLUMN_NAME, list)\n            it.put(ListsTable.COLUMN_HASH, hash)\n            it.put(ListsTable.COLUMN_NEEDS_APPROVAL, if (needsApproval) 1 else 0)\n            it.put(ListsTable.COLUMN_LAST_UPDATED, clock())\n        }\n\n        databaseConnection.withConnection {\n            writableDatabase.update(\n                ListsTable.TABLE_NAME,\n                contentValues,\n                \"${ListsTable.COLUMN_NAME} = ?\",\n                arrayOf(list)\n            )\n        }\n    }\n\n    override fun getList(list: String): EntityList? {\n        return databaseConnection.withConnection {\n            readableDatabase\n                .query(ListsTable.TABLE_NAME, \"${ListsTable.COLUMN_NAME} = ?\", arrayOf(list))\n                .first { mapCursorRowToEntityList(it) }\n        }\n    }\n\n    override fun getCount(list: String): Int {\n        if (!listExists(list)) {\n            return 0\n        }\n\n        return databaseConnection.withConnection {\n            readableDatabase.rawQuery(\n                \"\"\"\n                SELECT COUNT(*)\n                FROM \"$list\"\n                \"\"\".trimIndent(),\n                null\n            ).first {\n                it.getInt(0)\n            }!!\n        }\n    }\n\n    override fun addList(list: String) {\n        if (!listExists(list)) {\n            createList(list)\n            invalidateRowNumbers()\n        }\n    }\n\n    override fun delete(list: String, id: String) {\n        databaseConnection.withConnection {\n            writableDatabase.delete(quote(list), \"${EntitiesTable.COLUMN_ID} = ?\", arrayOf(id))\n        }\n\n        invalidateRowNumbers()\n    }\n\n    override fun query(list: String, query: Query?): List<Entity.Saved> {\n        if (!listExists(list)) {\n            return emptyList()\n        }\n\n        return queryWithAttachedRowNumber(list, query?.mapColumns { columnName ->\n            when (columnName) {\n                EntitySchema.ID -> EntitiesTable.COLUMN_ID\n                EntitySchema.LABEL -> EntitiesTable.COLUMN_LABEL\n                EntitySchema.VERSION -> EntitiesTable.COLUMN_VERSION\n                else -> EntitiesTable.getPropertyColumn(columnName)\n            }\n        })\n    }\n\n    override fun getByIndex(list: String, index: Int): Entity.Saved? {\n        if (!listExists(list)) {\n            return null\n        }\n\n        val query = Query.StringEq(ROW_NUMBER, (index + 1).toString())\n        return queryWithAttachedRowNumber(list, query).firstOrNull()\n    }\n\n    private fun queryWithAttachedRowNumber(list: String, query: Query?): List<Entity.Saved> {\n        try {\n            val cursorMapper = { cursor: Cursor ->\n                cursor.foldAndClose {\n                    mapCursorRowToEntity(it, it.getInt(ROW_NUMBER))\n                }\n            }\n\n            return if (query == null) {\n                databaseConnection.rawQueryWithRowNumber(list, cursorMapper = cursorMapper)\n            } else {\n                val sqlQuery = query.toSql()\n                databaseConnection.rawQueryWithRowNumber(\n                    list,\n                    sqlQuery.selection,\n                    sqlQuery.selectionArgs,\n                    cursorMapper = cursorMapper\n                )\n            }\n        } catch (e: SQLiteException) {\n            throw QueryException(e.message)\n        }\n    }\n\n    private fun invalidateRowNumbers() {\n        getListNames().forEach {\n            databaseConnection.invalidateRowNumbers(it)\n        }\n    }\n\n    private fun listExists(list: String): Boolean {\n        return databaseConnection.withConnection {\n            readableDatabase\n                .query(\n                    ListsTable.TABLE_NAME,\n                    selection = \"${ListsTable.COLUMN_NAME} = ?\",\n                    selectionArgs = arrayOf(list)\n                ).use { it.count } > 0\n        }\n    }\n\n    private fun createList(list: String) {\n        databaseConnection.resetTransaction {\n            createList(this, list)\n        }\n    }\n\n    private fun createList(db: SQLiteDatabase, list: String) {\n        val contentValues = ContentValues()\n        contentValues.put(ListsTable.COLUMN_NAME, list)\n        db.insertOrThrow(\n            ListsTable.TABLE_NAME,\n            null,\n            contentValues\n        )\n\n        db.execSQL(\n            \"\"\"\n                CREATE TABLE IF NOT EXISTS \"$list\" (\n                    $_ID integer PRIMARY KEY,\n                    ${EntitiesTable.COLUMN_ID} text,\n                    ${EntitiesTable.COLUMN_LABEL} text,\n                    ${EntitiesTable.COLUMN_VERSION} integer,\n                    ${EntitiesTable.COLUMN_TRUNK_VERSION} integer,\n                    ${EntitiesTable.COLUMN_BRANCH_ID} text,\n                    ${EntitiesTable.COLUMN_STATE} integer NOT NULL\n                );\n                \"\"\".trimIndent()\n        )\n\n        db.execSQL(\n            \"\"\"\n                CREATE UNIQUE INDEX IF NOT EXISTS \"${list}_unique_id_index\" ON \"$list\" (${EntitiesTable.COLUMN_ID});\n                \"\"\".trimIndent()\n        )\n    }\n\n    private fun updatePropertyColumns(list: String, entity: Entity) {\n        val columnNames = databaseConnection.withConnection {\n            readableDatabase.getColumnNames(quote(list))\n        }\n\n        val expectedColumns = entity.properties\n            .map { EntitiesTable.getPropertyColumn(it.first) }\n            .distinctBy { it.lowercase() }\n\n        val removedColumns = columnNames\n            .filter { it.startsWith(EntitiesTable.COLUMN_PROPERTY_PREFIX, ignoreCase = true) }\n            .filterNot { columnName ->\n                expectedColumns.any {\n                    it.equals(\n                        columnName,\n                        ignoreCase = true\n                    )\n                }\n            }\n\n        val newColumns = expectedColumns\n            .filterNot { columnName ->\n                columnNames.any {\n                    it.equals(\n                        columnName,\n                        ignoreCase = true\n                    )\n                }\n            }\n\n        databaseConnection.resetTransaction {\n            if (removedColumns.isNotEmpty()) {\n                val remainingColumns = columnNames - removedColumns.toSet()\n                val tempTable = \"${list}_temp\"\n\n                createList(this, tempTable)\n                addPropertyColumns(\n                    tempTable,\n                    remainingColumns.filter {\n                        it.startsWith(EntitiesTable.COLUMN_PROPERTY_PREFIX, ignoreCase = true)\n                    }\n                )\n                copyTableContent(list, tempTable, remainingColumns)\n                dropTable(list)\n                renameTable(tempTable, list)\n            }\n\n            if (newColumns.isNotEmpty()) {\n                addPropertyColumns(list, newColumns)\n            }\n        }\n    }\n\n    private fun addPropertyColumns(list: String, columns: List<String>) {\n        databaseConnection.withConnection {\n            columns.forEach {\n                writableDatabase.addColumn(list, it, \"text NOT NULL\", default = \"''\")\n            }\n        }\n    }\n\n    private fun addPropertiesToContentValues(contentValues: ContentValues, entity: Entity) {\n        entity.properties.forEach { (name, value) ->\n            contentValues.put(quote(EntitiesTable.getPropertyColumn(name)), value)\n        }\n    }\n\n    private fun mapCursorRowToEntityList(cursor: Cursor): EntityList {\n        return EntityList(\n            cursor.getString(ListsTable.COLUMN_NAME),\n            cursor.getStringOrNull(ListsTable.COLUMN_HASH),\n            cursor.getBoolean(ListsTable.COLUMN_NEEDS_APPROVAL),\n            cursor.getLongOrNull(ListsTable.COLUMN_LAST_UPDATED)\n        )\n    }\n\n    private fun mapCursorRowToEntity(\n        cursor: Cursor,\n        rowId: Int\n    ): Entity.Saved {\n        val map = cursor.rowToMap()\n\n        val propertyColumns = map.keys.filter {\n            it.startsWith(EntitiesTable.COLUMN_PROPERTY_PREFIX)\n        }\n\n        val properties =\n            propertyColumns.fold(emptyList<Pair<String, String>>()) { accum, property ->\n                accum + Pair(\n                    property.removePrefix(EntitiesTable.COLUMN_PROPERTY_PREFIX),\n                    map[property] ?: \"\"\n                )\n            }\n\n        val state = if (map[EntitiesTable.COLUMN_STATE]!!.toInt() == 0) {\n            Entity.State.OFFLINE\n        } else {\n            Entity.State.ONLINE\n        }\n\n        return Entity.Saved(\n            map[EntitiesTable.COLUMN_ID]!!,\n            map[EntitiesTable.COLUMN_LABEL],\n            map[EntitiesTable.COLUMN_VERSION]!!.toInt(),\n            properties,\n            state,\n            rowId - 1,\n            map[EntitiesTable.COLUMN_TRUNK_VERSION]?.toInt(),\n            map[EntitiesTable.COLUMN_BRANCH_ID]!!\n        )\n    }\n\n    /**\n     * Store state as an Int rather than a string to avoid increasing the storage needed for\n     * entities.\n     */\n    private fun convertStateToInt(state: Entity.State): Int {\n        return when (state) {\n            Entity.State.OFFLINE -> 0\n            Entity.State.ONLINE -> 1\n        }\n    }\n\n    private fun quote(text: String) = \"\\\"$text\\\"\"\n\n    companion object {\n        const val DATABASE_VERSION = 4\n    }\n}\n\nclass EntitiesDatabaseMigrator(databaseVersion: Int) : MigrationListDatabaseMigrator(\n    databaseVersion,\n    {\n        throw IllegalStateException(\"Cannot upgrade from this beta version. Please reinstall Collect!\")\n    },\n    {\n        it.addColumn(\n            ListsTable.TABLE_NAME,\n            ListsTable.COLUMN_NEEDS_APPROVAL,\n            \"integer\",\n            default = \"0\"\n        )\n    },\n    {\n        it.addColumn(\n            ListsTable.TABLE_NAME,\n            ListsTable.COLUMN_LAST_UPDATED,\n            \"date\"\n        )\n    }\n) {\n    override fun createDbForVersion(db: SQLiteDatabase, version: Int) {\n        if (version == 2) {\n            db.execSQL(\n                \"\"\"\n                CREATE TABLE IF NOT EXISTS lists (\n                        _id integer PRIMARY KEY,\n                        name text NOT NULL,\n                        hash text\n                );\n                \"\"\".trimIndent()\n            )\n        } else if (version == 3) {\n            db.execSQL(\n                \"\"\"\n                CREATE TABLE IF NOT EXISTS ${ListsTable.TABLE_NAME} (\n                        $_ID integer PRIMARY KEY,\n                        ${ListsTable.COLUMN_NAME} text NOT NULL,\n                        ${ListsTable.COLUMN_HASH} text,\n                        ${ListsTable.COLUMN_NEEDS_APPROVAL} integer DEFAULT 0\n                );\n                \"\"\".trimIndent()\n            )\n        } else if (version == 4) {\n            db.execSQL(\n                \"\"\"\n                CREATE TABLE IF NOT EXISTS ${ListsTable.TABLE_NAME} (\n                        $_ID integer PRIMARY KEY,\n                        ${ListsTable.COLUMN_NAME} text NOT NULL,\n                        ${ListsTable.COLUMN_HASH} text,\n                        ${ListsTable.COLUMN_NEEDS_APPROVAL} integer DEFAULT 0,\n                        ${ListsTable.COLUMN_LAST_UPDATED} date\n                );\n                \"\"\".trimIndent()\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/forms/DatabaseFormColumns.kt",
    "content": "package org.odk.collect.android.database.forms\n\nimport android.provider.BaseColumns\n\n/**\n * Columns for the Forms table.\n */\nobject DatabaseFormColumns : BaseColumns {\n    // These are the only things needed for an insert\n    const val DISPLAY_NAME = \"displayName\"\n    const val DESCRIPTION = \"description\" // can be null\n    const val JR_FORM_ID = \"jrFormId\"\n    const val JR_VERSION = \"jrVersion\" // can be null\n    const val FORM_FILE_PATH = \"formFilePath\"\n    const val SUBMISSION_URI = \"submissionUri\" // can be null\n    const val BASE64_RSA_PUBLIC_KEY = \"base64RsaPublicKey\" // can be null\n    const val AUTO_DELETE = \"autoDelete\" // can be null\n    const val USES_ENTITIES = \"usesEntities\" // can be null\n\n    // Column is called autoSubmit for legacy support but the attribute is auto-send\n    const val AUTO_SEND = \"autoSubmit\" // can be null\n    const val GEOMETRY_XPATH = \"geometryXpath\" // can be null\n\n    // these are generated for you (but you can insert something else if you want)\n    const val MD5_HASH = \"md5Hash\"\n    const val DATE = \"date\"\n    const val JRCACHE_FILE_PATH = \"jrcacheFilePath\"\n    const val FORM_MEDIA_PATH = \"formMediaPath\"\n\n    // this is null on create, and can only be set on an update.\n    const val LANGUAGE = \"language\"\n    const val DELETED_DATE = \"deleted_date\"\n    const val LAST_DETECTED_ATTACHMENTS_UPDATE_DATE = \"lastDetectedAttachmentsUpdateDate\"\n\n    // not used in the newest database version\n    const val DISPLAY_SUBTEXT = \"displaySubtext\"\n    const val LAST_DETECTED_FORM_VERSION_HASH = \"lastDetectedFormVersionHash\"\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/forms/DatabaseFormsRepository.java",
    "content": "package org.odk.collect.android.database.forms;\n\nimport static android.provider.BaseColumns._ID;\nimport static org.odk.collect.android.database.DatabaseConstants.FORMS_TABLE_NAME;\nimport static org.odk.collect.android.database.DatabaseObjectMapper.getFormFromCurrentCursorPosition;\nimport static org.odk.collect.android.database.DatabaseObjectMapper.getValuesFromForm;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_DELETE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_SEND;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.BASE64_RSA_PUBLIC_KEY;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DELETED_DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DESCRIPTION;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DISPLAY_NAME;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.USES_ENTITIES;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_FILE_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_MEDIA_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.GEOMETRY_XPATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JRCACHE_FILE_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_FORM_ID;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_VERSION;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.LANGUAGE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.LAST_DETECTED_ATTACHMENTS_UPDATE_DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.MD5_HASH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.SUBMISSION_URI;\nimport static org.odk.collect.shared.PathUtils.getRelativeFilePath;\n\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.database.CursorWindow;\nimport android.database.sqlite.SQLiteBlobTooBigException;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteQueryBuilder;\nimport android.os.StrictMode;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.db.sqlite.DatabaseConnection;\nimport org.odk.collect.android.database.DatabaseConstants;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.forms.savepoints.SavepointsRepository;\nimport org.odk.collect.shared.files.FileExt;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.File;\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Supplier;\n\nimport javax.annotation.Nullable;\n\nimport timber.log.Timber;\n\npublic class DatabaseFormsRepository implements FormsRepository {\n\n    private final DatabaseConnection databaseConnection;\n    private final String formsPath;\n    private final String cachePath;\n    private final Supplier<Long> clock;\n    private final SavepointsRepository savepointsRepository;\n\n    public DatabaseFormsRepository(Context context, String dbPath, String formsPath, String cachePath, Supplier<Long> clock, SavepointsRepository savepointsRepository) {\n        this.formsPath = formsPath;\n        this.cachePath = cachePath;\n        this.clock = clock;\n        this.databaseConnection = new DatabaseConnection(\n                context,\n                dbPath,\n                DatabaseConstants.FORMS_DATABASE_NAME,\n                new FormDatabaseMigrator(),\n                DatabaseConstants.FORMS_DATABASE_VERSION\n        );\n        this.savepointsRepository = savepointsRepository;\n    }\n\n    @Nullable\n    @Override\n    public Form get(Long id) {\n        return queryForForm(_ID + \"=?\", new String[]{id.toString()});\n    }\n\n    @Nullable\n    @Override\n    public Form getLatestByFormIdAndVersion(String jrFormId, @Nullable String jrVersion) {\n        List<Form> all = getAllByFormIdAndVersion(jrFormId, jrVersion);\n        if (!all.isEmpty()) {\n            return all.stream().max(Comparator.comparingLong(Form::getDate)).get();\n        } else {\n            return null;\n        }\n    }\n\n    @Nullable\n    @Override\n    public Form getOneByPath(String path) {\n        String selection = FORM_FILE_PATH + \"=?\";\n        String[] selectionArgs = {getRelativeFilePath(formsPath, path)};\n        return queryForForm(selection, selectionArgs);\n    }\n\n    @Nullable\n    @Override\n    public Form getOneByMd5Hash(@NotNull String hash) {\n        if (hash == null) {\n            throw new IllegalArgumentException(\"Missing form hash. ODK-compatible servers must include form hashes in their form lists. Please talk to the person who asked you to collect data.\");\n        }\n\n        String selection = DatabaseFormColumns.MD5_HASH + \"=?\";\n        String[] selectionArgs = {hash};\n        return queryForForm(selection, selectionArgs);\n    }\n\n    @Override\n    public List<Form> getAll() {\n        StrictMode.noteSlowCall(\"Accessing readable DB\");\n        return queryForForms(null, null);\n    }\n\n    @Override\n    public List<Form> getAllByFormIdAndVersion(String jrFormId, @Nullable String jrVersion) {\n        if (jrVersion != null) {\n            return queryForForms(JR_FORM_ID + \"=? AND \" + JR_VERSION + \"=?\", new String[]{jrFormId, jrVersion});\n        } else {\n            return queryForForms(JR_FORM_ID + \"=? AND \" + JR_VERSION + \" IS NULL\", new String[]{jrFormId});\n        }\n    }\n\n    @Override\n    public List<Form> getAllByFormId(String formId) {\n        StrictMode.noteSlowCall(\"Accessing readable DB\");\n        return queryForForms(JR_FORM_ID + \"=?\", new String[]{formId});\n    }\n\n    @Override\n    public List<Form> getAllNotDeletedByFormId(String jrFormId) {\n        StrictMode.noteSlowCall(\"Accessing readable DB\");\n        return queryForForms(JR_FORM_ID + \"=? AND \" + DELETED_DATE + \" IS NULL\", new String[]{jrFormId});\n    }\n\n\n    @Override\n    public List<Form> getAllNotDeletedByFormIdAndVersion(String jrFormId, @Nullable String jrVersion) {\n        StrictMode.noteSlowCall(\"Accessing readable DB\");\n\n        if (jrVersion != null) {\n            return queryForForms(DELETED_DATE + \" IS NULL AND \" + JR_FORM_ID + \"=? AND \" + JR_VERSION + \"=?\", new String[]{jrFormId, jrVersion});\n        } else {\n            return queryForForms(DELETED_DATE + \" IS NULL AND \" + JR_FORM_ID + \"=? AND \" + JR_VERSION + \" IS NULL\", new String[]{jrFormId});\n        }\n    }\n\n    @Override\n    public Form save(@NotNull Form form) {\n        final ContentValues values = getValuesFromForm(form, formsPath);\n\n        String md5Hash = Md5.getMd5Hash(new File(form.getFormFilePath()));\n        values.put(MD5_HASH, md5Hash);\n        values.put(FORM_MEDIA_PATH, getRelativeFilePath(formsPath, FileUtils.constructMediaPath(form.getFormFilePath())));\n        values.put(JRCACHE_FILE_PATH, md5Hash + \".formdef\");\n\n        if (form.isDeleted()) {\n            values.put(DELETED_DATE, 0L);\n        } else {\n            values.putNull(DELETED_DATE);\n        }\n\n        if (form.getDbId() == null) {\n            values.put(DATE, clock.get());\n\n            List<Form> existingFormsWithSameFormId = getAllByFormId(form.getFormId());\n            if (!existingFormsWithSameFormId.isEmpty()) {\n                Form latestFormWithSameFormId = Collections.max(existingFormsWithSameFormId, Comparator.comparing(Form::getDate));\n                values.put(LANGUAGE, latestFormWithSameFormId.getLanguage());\n            }\n\n            Long idFromUri = insertForm(values);\n            if (idFromUri == -1) {\n                return getOneByMd5Hash(md5Hash);\n            }\n            return get(idFromUri);\n        } else {\n            updateForm(form.getDbId(), values);\n            return get(form.getDbId());\n        }\n    }\n\n    @Override\n    public void delete(Long id) {\n        String selection = _ID + \"=?\";\n        String[] selectionArgs = {String.valueOf(id)};\n\n        deleteForms(selection, selectionArgs);\n    }\n\n    @Override\n    public void softDelete(Long id) {\n        ContentValues values = new ContentValues();\n        values.put(DELETED_DATE, System.currentTimeMillis());\n        updateForm(id, values);\n        savepointsRepository.delete(id, null);\n    }\n\n    @Override\n    public void deleteByMd5Hash(@NotNull String md5Hash) {\n        String selection = DatabaseFormColumns.MD5_HASH + \"=?\";\n        String[] selectionArgs = {md5Hash};\n\n        deleteForms(selection, selectionArgs);\n    }\n\n    @Override\n    public void deleteAll() {\n        deleteForms(null, null);\n    }\n\n    @Override\n    public void restore(Long id) {\n        ContentValues values = new ContentValues();\n        values.putNull(DELETED_DATE);\n        updateForm(id, values);\n    }\n\n    public Cursor rawQuery(Map<String, String> projectionMap, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy) {\n        return queryAndReturnCursor(projectionMap, projection, selection, selectionArgs, sortOrder, groupBy);\n    }\n\n    @Nullable\n    private Form queryForForm(String selection, String[] selectionArgs) {\n        StrictMode.noteSlowCall(\"Accessing readable DB\");\n        List<Form> forms = queryForForms(selection, selectionArgs);\n        return !forms.isEmpty() ? forms.get(0) : null;\n    }\n\n    private List<Form> queryForForms(String selection, String[] selectionArgs) {\n        try (Cursor cursor = queryAndReturnCursor(null, null, selection, selectionArgs, null, null)) {\n            return getFormsFromCursor(cursor, formsPath, cachePath);\n        }\n    }\n\n    private Cursor queryAndReturnCursor(Map<String, String> projectionMap, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy) {\n        SQLiteDatabase readableDatabase = databaseConnection.getReadableDatabase();\n        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();\n        qb.setTables(FORMS_TABLE_NAME);\n\n        if (projection == null) {\n            /*\n             For some reason passing null as the projection doesn't always give us all the\n             columns so we hardcode them here so it's explicit that we need these all back.\n             The problem can occur, for example, when a new column is added to a database and the\n             database needs to be updated. After the upgrade, the new column might not be returned,\n             even though it already exists.\n             */\n            projection = new String[]{\n                    _ID,\n                    DISPLAY_NAME,\n                    DESCRIPTION,\n                    JR_FORM_ID,\n                    JR_VERSION,\n                    MD5_HASH,\n                    DATE,\n                    FORM_MEDIA_PATH,\n                    FORM_FILE_PATH,\n                    LANGUAGE,\n                    SUBMISSION_URI,\n                    BASE64_RSA_PUBLIC_KEY,\n                    JRCACHE_FILE_PATH,\n                    AUTO_SEND,\n                    AUTO_DELETE,\n                    GEOMETRY_XPATH,\n                    DELETED_DATE,\n                    LAST_DETECTED_ATTACHMENTS_UPDATE_DATE,\n                    USES_ENTITIES\n            };\n        }\n\n        if (projectionMap != null) {\n            qb.setProjectionMap(projectionMap);\n        }\n\n        return qb.query(readableDatabase, projection, selection, selectionArgs, groupBy, null, sortOrder);\n    }\n\n    private Long insertForm(ContentValues values) {\n        SQLiteDatabase writableDatabase = databaseConnection.getWritableDatabase();\n        return writableDatabase.insertOrThrow(FORMS_TABLE_NAME, null, values);\n    }\n\n    private void updateForm(Long id, ContentValues values) {\n        SQLiteDatabase writableDatabase = databaseConnection.getWritableDatabase();\n        writableDatabase.update(FORMS_TABLE_NAME, values, _ID + \"=?\", new String[]{String.valueOf(id)});\n    }\n\n    private void deleteForms(String selection, String[] selectionArgs) {\n        StrictMode.noteSlowCall(\"Accessing readable DB\");\n\n        List<Form> forms = queryForForms(selection, selectionArgs);\n        for (Form form : forms) {\n            deleteFilesForForm(form);\n        }\n\n        SQLiteDatabase writableDatabase = databaseConnection.getWritableDatabase();\n        writableDatabase.delete(FORMS_TABLE_NAME, selection, selectionArgs);\n    }\n\n    @NotNull\n    private static List<Form> getFormsFromCursor(Cursor cursor, String formsPath, String cachePath) {\n        List<Form> forms = new ArrayList<>();\n        if (cursor != null) {\n            Object cursorSize = null;\n            try {\n                Field field = CursorWindow.class.getDeclaredField(\"sCursorWindowSize\");\n                field.setAccessible(true);\n                cursorSize = field.get(null);\n            } catch (Throwable e) {\n                // ignore\n            }\n\n            try {\n                cursor.moveToPosition(-1);\n                while (cursor.moveToNext()) {\n                    Form form = getFormFromCurrentCursorPosition(cursor, formsPath, cachePath);\n                    forms.add(form);\n                }\n            } catch (SQLiteBlobTooBigException e) {\n                Timber.w(\"SQLiteBlobTooBigException, sCursorWindowSize: %sB\", cursorSize != null ? cursorSize : \"?\");\n                throw e;\n            }\n        }\n        return forms;\n    }\n\n    private void deleteFilesForForm(Form form) {\n        // Delete form file\n        if (form.getFormFilePath() != null) {\n            new File(form.getFormFilePath()).delete();\n        }\n\n        // Delete cache file\n        if (form.getJrCacheFilePath() != null) {\n            new File(form.getJrCacheFilePath()).delete();\n        }\n\n        // Delete media files\n        if (form.getFormMediaPath() != null) {\n            File mediaDir = new File(form.getFormMediaPath());\n\n            if (mediaDir.isDirectory()) {\n                FileExt.deleteDirectory(mediaDir);\n            } else {\n                mediaDir.delete();\n            }\n        }\n\n        savepointsRepository.delete(form.getDbId(), null);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/forms/FormDatabaseMigrator.java",
    "content": "package org.odk.collect.android.database.forms;\n\nimport android.database.SQLException;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport org.odk.collect.db.sqlite.DatabaseMigrator;\nimport org.odk.collect.db.sqlite.SQLiteUtils;\n\nimport static android.provider.BaseColumns._ID;\nimport static org.odk.collect.android.database.DatabaseConstants.FORMS_TABLE_NAME;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_DELETE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_SEND;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.BASE64_RSA_PUBLIC_KEY;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DELETED_DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DESCRIPTION;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DISPLAY_NAME;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DISPLAY_SUBTEXT;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.USES_ENTITIES;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_FILE_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_MEDIA_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.GEOMETRY_XPATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JRCACHE_FILE_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_FORM_ID;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_VERSION;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.LANGUAGE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.LAST_DETECTED_ATTACHMENTS_UPDATE_DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.LAST_DETECTED_FORM_VERSION_HASH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.MD5_HASH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.SUBMISSION_URI;\nimport static org.odk.collect.db.sqlite.SQLiteDatabaseExt.addColumn;\n\nimport timber.log.Timber;\n\npublic class FormDatabaseMigrator implements DatabaseMigrator {\n\n    private static final String[] COLUMN_NAMES_V7 = {_ID, DISPLAY_NAME, DESCRIPTION,\n            JR_FORM_ID, JR_VERSION, MD5_HASH, DATE, FORM_MEDIA_PATH, FORM_FILE_PATH, LANGUAGE,\n            SUBMISSION_URI, BASE64_RSA_PUBLIC_KEY, JRCACHE_FILE_PATH, AUTO_SEND, AUTO_DELETE,\n            LAST_DETECTED_FORM_VERSION_HASH};\n\n    // These exist in database versions 2 and 3, but not in 4...\n    private static final String TEMP_FORMS_TABLE_NAME = \"forms_v4\";\n    private static final String MODEL_VERSION = \"modelVersion\";\n\n    public void onCreate(SQLiteDatabase db) {\n        createFormsTableV14(db);\n    }\n\n    @SuppressWarnings({\"checkstyle:FallThrough\"})\n    public void onUpgrade(SQLiteDatabase db, int oldVersion) throws SQLException {\n        Timber.w(\"Forms db upgrade from version: %s\", oldVersion);\n        switch (oldVersion) {\n            case 1:\n                upgradeToVersion2(db);\n            case 2:\n            case 3:\n                upgradeToVersion4(db, oldVersion);\n            case 4:\n                upgradeToVersion5(db);\n            case 5:\n                upgradeToVersion6(db);\n            case 6:\n                upgradeToVersion7(db);\n            case 7:\n                upgradeToVersion8(db);\n            case 8:\n                upgradeToVersion9(db);\n            case 9:\n                upgradeToVersion10(db);\n            case 10:\n                upgradeToVersion11(db);\n            case 11:\n                upgradeToVersion12(db);\n            case 12:\n                upgradeToVersion13(db);\n            case 13:\n                upgradeToVersion14(db);\n            case 14:\n                // Remember to bump the database version number in {@link org.odk.collect.android.database.DatabaseConstants}\n                // upgradeToVersion15(db);\n        }\n    }\n\n    private void upgradeToVersion2(SQLiteDatabase db) {\n        SQLiteUtils.dropTable(db, FORMS_TABLE_NAME);\n        onCreate(db);\n    }\n\n    private void upgradeToVersion4(SQLiteDatabase db, int oldVersion) {\n        // adding BASE64_RSA_PUBLIC_KEY and changing type and name of\n        // integer MODEL_VERSION to text VERSION\n        SQLiteUtils.dropTable(db, TEMP_FORMS_TABLE_NAME);\n        createFormsTableV4(db, TEMP_FORMS_TABLE_NAME);\n        db.execSQL(\"INSERT INTO \"\n                + TEMP_FORMS_TABLE_NAME\n                + \" (\"\n                + _ID\n                + \", \"\n                + DISPLAY_NAME\n                + \", \"\n                + DISPLAY_SUBTEXT\n                + \", \"\n                + DESCRIPTION\n                + \", \"\n                + JR_FORM_ID\n                + \", \"\n                + MD5_HASH\n                + \", \"\n                + DATE\n                + \", \" // milliseconds\n                + FORM_MEDIA_PATH\n                + \", \"\n                + FORM_FILE_PATH\n                + \", \"\n                + LANGUAGE\n                + \", \"\n                + SUBMISSION_URI\n                + \", \"\n                + JR_VERSION\n                + \", \"\n                + ((oldVersion != 3) ? \"\"\n                : (BASE64_RSA_PUBLIC_KEY + \", \"))\n                + JRCACHE_FILE_PATH\n                + \") SELECT \"\n                + _ID\n                + \", \"\n                + DISPLAY_NAME\n                + \", \"\n                + DISPLAY_SUBTEXT\n                + \", \"\n                + DESCRIPTION\n                + \", \"\n                + JR_FORM_ID\n                + \", \"\n                + MD5_HASH\n                + \", \"\n                + DATE\n                + \", \" // milliseconds\n                + FORM_MEDIA_PATH\n                + \", \"\n                + FORM_FILE_PATH\n                + \", \"\n                + LANGUAGE\n                + \", \"\n                + SUBMISSION_URI\n                + \", \"\n                + \"CASE WHEN \"\n                + MODEL_VERSION\n                + \" IS NOT NULL THEN \"\n                + \"CAST(\"\n                + MODEL_VERSION\n                + \" AS TEXT) ELSE NULL END, \"\n                + ((oldVersion != 3) ? \"\"\n                : (BASE64_RSA_PUBLIC_KEY + \", \"))\n                + JRCACHE_FILE_PATH + \" FROM \"\n                + FORMS_TABLE_NAME);\n\n        // risky failures here...\n        SQLiteUtils.dropTable(db, FORMS_TABLE_NAME);\n        createFormsTableV4(db, FORMS_TABLE_NAME);\n        db.execSQL(\"INSERT INTO \"\n                + FORMS_TABLE_NAME\n                + \" (\"\n                + _ID\n                + \", \"\n                + DISPLAY_NAME\n                + \", \"\n                + DISPLAY_SUBTEXT\n                + \", \"\n                + DESCRIPTION\n                + \", \"\n                + JR_FORM_ID\n                + \", \"\n                + MD5_HASH\n                + \", \"\n                + DATE\n                + \", \" // milliseconds\n                + FORM_MEDIA_PATH + \", \"\n                + FORM_FILE_PATH + \", \"\n                + LANGUAGE + \", \"\n                + SUBMISSION_URI + \", \"\n                + JR_VERSION + \", \"\n                + BASE64_RSA_PUBLIC_KEY + \", \"\n                + JRCACHE_FILE_PATH + \") SELECT \"\n                + _ID + \", \"\n                + DISPLAY_NAME\n                + \", \"\n                + DISPLAY_SUBTEXT\n                + \", \"\n                + DESCRIPTION\n                + \", \"\n                + JR_FORM_ID\n                + \", \"\n                + MD5_HASH\n                + \", \"\n                + DATE\n                + \", \" // milliseconds\n                + FORM_MEDIA_PATH + \", \"\n                + FORM_FILE_PATH + \", \"\n                + LANGUAGE + \", \"\n                + SUBMISSION_URI + \", \"\n                + JR_VERSION + \", \"\n                + BASE64_RSA_PUBLIC_KEY + \", \"\n                + JRCACHE_FILE_PATH + \" FROM \"\n                + TEMP_FORMS_TABLE_NAME);\n        SQLiteUtils.dropTable(db, TEMP_FORMS_TABLE_NAME);\n    }\n\n    private void upgradeToVersion5(SQLiteDatabase db) {\n        addColumn(db, FORMS_TABLE_NAME, AUTO_SEND, \"text\");\n        addColumn(db, FORMS_TABLE_NAME, AUTO_DELETE, \"text\");\n    }\n\n    private void upgradeToVersion6(SQLiteDatabase db) {\n        addColumn(db, FORMS_TABLE_NAME, LAST_DETECTED_FORM_VERSION_HASH, \"text\");\n    }\n\n    private void upgradeToVersion7(SQLiteDatabase db) {\n        String temporaryTable = FORMS_TABLE_NAME + \"_tmp\";\n        SQLiteUtils.renameTable(db, FORMS_TABLE_NAME, temporaryTable);\n        createFormsTableV7(db);\n        SQLiteUtils.copyRows(db, temporaryTable, COLUMN_NAMES_V7, FORMS_TABLE_NAME);\n        SQLiteUtils.dropTable(db, temporaryTable);\n    }\n\n    private void upgradeToVersion8(SQLiteDatabase db) {\n        addColumn(db, FORMS_TABLE_NAME, GEOMETRY_XPATH, \"text\");\n    }\n\n    private void upgradeToVersion9(SQLiteDatabase db) {\n        String temporaryTable = FORMS_TABLE_NAME + \"_tmp\";\n        SQLiteUtils.renameTable(db, FORMS_TABLE_NAME, temporaryTable);\n        createFormsTableV9(db);\n        SQLiteUtils.copyRows(db, temporaryTable, new String[]{_ID, DISPLAY_NAME, DESCRIPTION,\n                JR_FORM_ID, JR_VERSION, MD5_HASH, DATE, FORM_MEDIA_PATH, FORM_FILE_PATH, LANGUAGE,\n                SUBMISSION_URI, BASE64_RSA_PUBLIC_KEY, JRCACHE_FILE_PATH, AUTO_SEND, AUTO_DELETE,\n                GEOMETRY_XPATH}, FORMS_TABLE_NAME);\n        SQLiteUtils.dropTable(db, temporaryTable);\n    }\n\n    private void upgradeToVersion10(SQLiteDatabase db) {\n        String temporaryTable = FORMS_TABLE_NAME + \"_tmp\";\n        SQLiteUtils.renameTable(db, FORMS_TABLE_NAME, temporaryTable);\n        createFormsTableV10(db);\n        SQLiteUtils.copyRows(db, temporaryTable, new String[]{_ID, DISPLAY_NAME, DESCRIPTION,\n                JR_FORM_ID, JR_VERSION, MD5_HASH, DATE, FORM_MEDIA_PATH, FORM_FILE_PATH, LANGUAGE,\n                SUBMISSION_URI, BASE64_RSA_PUBLIC_KEY, JRCACHE_FILE_PATH, AUTO_SEND, AUTO_DELETE,\n                GEOMETRY_XPATH}, FORMS_TABLE_NAME);\n        SQLiteUtils.dropTable(db, temporaryTable);\n    }\n\n    private void upgradeToVersion11(SQLiteDatabase db) {\n        String temporaryTable = FORMS_TABLE_NAME + \"_tmp\";\n        SQLiteUtils.renameTable(db, FORMS_TABLE_NAME, temporaryTable);\n        createFormsTableV11(db);\n        SQLiteUtils.copyRows(db, temporaryTable, new String[]{_ID, DISPLAY_NAME, DESCRIPTION,\n                JR_FORM_ID, JR_VERSION, MD5_HASH, DATE, FORM_MEDIA_PATH, FORM_FILE_PATH, LANGUAGE,\n                SUBMISSION_URI, BASE64_RSA_PUBLIC_KEY, JRCACHE_FILE_PATH, AUTO_SEND, AUTO_DELETE,\n                GEOMETRY_XPATH, DELETED_DATE}, FORMS_TABLE_NAME);\n        SQLiteUtils.dropTable(db, temporaryTable);\n    }\n\n    private void upgradeToVersion12(SQLiteDatabase db) {\n        addColumn(db, FORMS_TABLE_NAME, LAST_DETECTED_ATTACHMENTS_UPDATE_DATE, \"integer\");\n    }\n\n    private void upgradeToVersion13(SQLiteDatabase db) {\n        String temporaryTable = FORMS_TABLE_NAME + \"_tmp\";\n        SQLiteUtils.renameTable(db, FORMS_TABLE_NAME, temporaryTable);\n        createFormsTableV13(db);\n        SQLiteUtils.copyRows(db, temporaryTable, new String[]{_ID, DISPLAY_NAME, DESCRIPTION,\n                JR_FORM_ID, JR_VERSION, MD5_HASH, DATE, FORM_MEDIA_PATH, FORM_FILE_PATH, LANGUAGE,\n                SUBMISSION_URI, BASE64_RSA_PUBLIC_KEY, JRCACHE_FILE_PATH, AUTO_SEND, AUTO_DELETE,\n                GEOMETRY_XPATH, DELETED_DATE, LAST_DETECTED_ATTACHMENTS_UPDATE_DATE}, FORMS_TABLE_NAME);\n        SQLiteUtils.dropTable(db, temporaryTable);\n    }\n\n    private void upgradeToVersion14(SQLiteDatabase db) {\n        addColumn(db, FORMS_TABLE_NAME, USES_ENTITIES, \"text\");\n    }\n\n    private void createFormsTableV4(SQLiteDatabase db, String tableName) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + tableName + \" (\"\n                + _ID + \" integer primary key, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + DISPLAY_SUBTEXT + \" text not null, \"\n                + DESCRIPTION + \" text, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + MD5_HASH + \" text not null, \"\n                + DATE + \" integer not null, \" // milliseconds\n                + FORM_MEDIA_PATH + \" text not null, \"\n                + FORM_FILE_PATH + \" text not null, \"\n                + LANGUAGE + \" text, \"\n                + SUBMISSION_URI + \" text, \"\n                + BASE64_RSA_PUBLIC_KEY + \" text, \"\n                + JRCACHE_FILE_PATH + \" text not null, \"\n                + AUTO_SEND + \" text, \"\n                + AUTO_DELETE + \" text, \"\n                + LAST_DETECTED_FORM_VERSION_HASH + \" text);\");\n    }\n\n    public void createFormsTableV7(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + FORMS_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + DESCRIPTION + \" text, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + MD5_HASH + \" text not null, \"\n                + DATE + \" integer not null, \" // milliseconds\n                + FORM_MEDIA_PATH + \" text not null, \"\n                + FORM_FILE_PATH + \" text not null, \"\n                + LANGUAGE + \" text, \"\n                + SUBMISSION_URI + \" text, \"\n                + BASE64_RSA_PUBLIC_KEY + \" text, \"\n                + JRCACHE_FILE_PATH + \" text not null, \"\n                + AUTO_SEND + \" text, \"\n                + AUTO_DELETE + \" text, \"\n                + LAST_DETECTED_FORM_VERSION_HASH + \" text);\");\n    }\n\n    public void createFormsTableV8(SQLiteDatabase database) {\n        database.execSQL(\"CREATE TABLE IF NOT EXISTS \" + FORMS_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + DESCRIPTION + \" text, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + MD5_HASH + \" text not null, \"\n                + DATE + \" integer not null, \"\n                + FORM_MEDIA_PATH + \" text not null, \"\n                + FORM_FILE_PATH + \" text not null, \"\n                + LANGUAGE + \" text, \"\n                + SUBMISSION_URI + \" text, \"\n                + BASE64_RSA_PUBLIC_KEY + \" text, \"\n                + JRCACHE_FILE_PATH + \" text not null, \"\n                + AUTO_SEND + \" text, \"\n                + AUTO_DELETE + \" text, \"\n                + LAST_DETECTED_FORM_VERSION_HASH + \" text, \"\n                + GEOMETRY_XPATH + \" text);\");\n    }\n\n    public void createFormsTableV9(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + FORMS_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + DESCRIPTION + \" text, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + MD5_HASH + \" text not null, \"\n                + DATE + \" integer not null, \" // milliseconds\n                + FORM_MEDIA_PATH + \" text not null, \"\n                + FORM_FILE_PATH + \" text not null, \"\n                + LANGUAGE + \" text, \"\n                + SUBMISSION_URI + \" text, \"\n                + BASE64_RSA_PUBLIC_KEY + \" text, \"\n                + JRCACHE_FILE_PATH + \" text not null, \"\n                + AUTO_SEND + \" text, \"\n                + AUTO_DELETE + \" text, \"\n                + GEOMETRY_XPATH + \" text, \"\n                + \"deleted\" + \" boolean default(0));\");\n    }\n\n    public void createFormsTableV10(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + FORMS_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + DESCRIPTION + \" text, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + MD5_HASH + \" text not null, \"\n                + DATE + \" integer not null, \" // milliseconds\n                + FORM_MEDIA_PATH + \" text not null, \"\n                + FORM_FILE_PATH + \" text not null, \"\n                + LANGUAGE + \" text, \"\n                + SUBMISSION_URI + \" text, \"\n                + BASE64_RSA_PUBLIC_KEY + \" text, \"\n                + JRCACHE_FILE_PATH + \" text not null, \"\n                + AUTO_SEND + \" text, \"\n                + AUTO_DELETE + \" text, \"\n                + GEOMETRY_XPATH + \" text, \"\n                + DELETED_DATE + \" integer);\");\n    }\n\n    public void createFormsTableV11(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + FORMS_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + DESCRIPTION + \" text, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + MD5_HASH + \" text not null UNIQUE ON CONFLICT IGNORE, \"\n                + DATE + \" integer not null, \" // milliseconds\n                + FORM_MEDIA_PATH + \" text not null, \"\n                + FORM_FILE_PATH + \" text not null, \"\n                + LANGUAGE + \" text, \"\n                + SUBMISSION_URI + \" text, \"\n                + BASE64_RSA_PUBLIC_KEY + \" text, \"\n                + JRCACHE_FILE_PATH + \" text not null, \"\n                + AUTO_SEND + \" text, \"\n                + AUTO_DELETE + \" text, \"\n                + GEOMETRY_XPATH + \" text, \"\n                + DELETED_DATE + \" integer);\");\n    }\n\n    public void createFormsTableV12(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + FORMS_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + DESCRIPTION + \" text, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + MD5_HASH + \" text not null UNIQUE ON CONFLICT IGNORE, \"\n                + DATE + \" integer not null, \" // milliseconds\n                + FORM_MEDIA_PATH + \" text not null, \"\n                + FORM_FILE_PATH + \" text not null, \"\n                + LANGUAGE + \" text, \"\n                + SUBMISSION_URI + \" text, \"\n                + BASE64_RSA_PUBLIC_KEY + \" text, \"\n                + JRCACHE_FILE_PATH + \" text not null, \"\n                + AUTO_SEND + \" text, \"\n                + AUTO_DELETE + \" text, \"\n                + GEOMETRY_XPATH + \" text, \"\n                + DELETED_DATE + \" integer, \"\n                + LAST_DETECTED_ATTACHMENTS_UPDATE_DATE + \" integer);\"); // milliseconds\n    }\n\n    public void createFormsTableV13(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + FORMS_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key autoincrement, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + DESCRIPTION + \" text, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + MD5_HASH + \" text not null UNIQUE ON CONFLICT IGNORE, \"\n                + DATE + \" integer not null, \" // milliseconds\n                + FORM_MEDIA_PATH + \" text not null, \"\n                + FORM_FILE_PATH + \" text not null, \"\n                + LANGUAGE + \" text, \"\n                + SUBMISSION_URI + \" text, \"\n                + BASE64_RSA_PUBLIC_KEY + \" text, \"\n                + JRCACHE_FILE_PATH + \" text not null, \"\n                + AUTO_SEND + \" text, \"\n                + AUTO_DELETE + \" text, \"\n                + GEOMETRY_XPATH + \" text, \"\n                + DELETED_DATE + \" integer, \"\n                + LAST_DETECTED_ATTACHMENTS_UPDATE_DATE + \" integer);\"); // milliseconds\n    }\n\n    private void createFormsTableV14(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + FORMS_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key autoincrement, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + DESCRIPTION + \" text, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + MD5_HASH + \" text not null UNIQUE ON CONFLICT IGNORE, \"\n                + DATE + \" integer not null, \" // milliseconds\n                + FORM_MEDIA_PATH + \" text not null, \"\n                + FORM_FILE_PATH + \" text not null, \"\n                + LANGUAGE + \" text, \"\n                + SUBMISSION_URI + \" text, \"\n                + BASE64_RSA_PUBLIC_KEY + \" text, \"\n                + JRCACHE_FILE_PATH + \" text not null, \"\n                + AUTO_SEND + \" text, \"\n                + AUTO_DELETE + \" text, \"\n                + GEOMETRY_XPATH + \" text, \"\n                + DELETED_DATE + \" integer, \"\n                + LAST_DETECTED_ATTACHMENTS_UPDATE_DATE + \" integer, \" // milliseconds\n                + USES_ENTITIES + \" text);\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/instances/DatabaseInstanceColumns.kt",
    "content": "package org.odk.collect.android.database.instances\n\nimport android.provider.BaseColumns\n\nobject DatabaseInstanceColumns : BaseColumns {\n\n    // instance column names\n    const val DISPLAY_NAME = \"displayName\"\n    const val SUBMISSION_URI = \"submissionUri\"\n    const val INSTANCE_FILE_PATH = \"instanceFilePath\"\n    const val JR_FORM_ID = \"jrFormId\"\n    const val JR_VERSION = \"jrVersion\"\n    const val STATUS = \"status\"\n    const val CAN_EDIT_WHEN_COMPLETE = \"canEditWhenComplete\" // the only reason why a finalized form should not be opened for review is that it is encrypted\n    const val LAST_STATUS_CHANGE_DATE = \"date\"\n    const val FINALIZATION_DATE = \"finalizationDate\"\n    const val DELETED_DATE = \"deletedDate\"\n    const val GEOMETRY = \"geometry\"\n    const val GEOMETRY_TYPE = \"geometryType\"\n    const val CAN_DELETE_BEFORE_SEND = \"canDeleteBeforeSend\"\n    const val EDIT_OF = \"editOf\"\n    const val EDIT_NUMBER = \"editNumber\"\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/instances/DatabaseInstancesRepository.java",
    "content": "package org.odk.collect.android.database.instances;\n\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteConstraintException;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteQueryBuilder;\nimport android.os.StrictMode;\n\nimport org.odk.collect.db.sqlite.DatabaseConnection;\nimport org.odk.collect.android.database.DatabaseConstants;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.shared.files.FileExt;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Supplier;\n\nimport static android.provider.BaseColumns._ID;\nimport static org.odk.collect.android.database.DatabaseConstants.INSTANCES_TABLE_NAME;\nimport static org.odk.collect.android.database.DatabaseObjectMapper.getInstanceFromCurrentCursorPosition;\nimport static org.odk.collect.android.database.DatabaseObjectMapper.getValuesFromInstance;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.CAN_DELETE_BEFORE_SEND;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.CAN_EDIT_WHEN_COMPLETE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.DELETED_DATE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.DISPLAY_NAME;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.EDIT_NUMBER;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.EDIT_OF;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.FINALIZATION_DATE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.GEOMETRY;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.GEOMETRY_TYPE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.INSTANCE_FILE_PATH;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.JR_FORM_ID;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.JR_VERSION;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.STATUS;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.SUBMISSION_URI;\nimport static org.odk.collect.shared.PathUtils.getRelativeFilePath;\n\n/**\n * Mediates between {@link Instance} objects and the underlying SQLite database that stores them.\n */\npublic final class DatabaseInstancesRepository implements InstancesRepository {\n\n    private final DatabaseConnection databaseConnection;\n    private final Supplier<Long> clock;\n    private final String instancesPath;\n\n    public DatabaseInstancesRepository(Context context, String dbPath, String instancesPath, Supplier<Long> clock) {\n        this.databaseConnection = new DatabaseConnection(\n                context,\n                dbPath,\n                DatabaseConstants.INSTANCES_DATABASE_NAME,\n                new InstanceDatabaseMigrator(),\n                DatabaseConstants.INSTANCES_DATABASE_VERSION\n        );\n\n        this.clock = clock;\n        this.instancesPath = instancesPath;\n    }\n\n    @Override\n    public Instance get(Long databaseId) {\n        String selection = _ID + \"=?\";\n        String[] selectionArgs = {Long.toString(databaseId)};\n\n        try (Cursor cursor = query(null, selection, selectionArgs, null)) {\n            List<Instance> result = getInstancesFromCursor(cursor, instancesPath);\n            return !result.isEmpty() ? result.get(0) : null;\n        }\n    }\n\n    @Override\n    public Instance getOneByPath(String instancePath) {\n        String selection = INSTANCE_FILE_PATH + \"=?\";\n        String[] args = {getRelativeFilePath(instancesPath, instancePath)};\n        try (Cursor cursor = query(null, selection, args, null)) {\n            List<Instance> instances = getInstancesFromCursor(cursor, instancesPath);\n            if (instances.size() == 1) {\n                return instances.get(0);\n            } else {\n                return null;\n            }\n        }\n    }\n\n    @Override\n    public List<Instance> getAll() {\n        StrictMode.noteSlowCall(\"Accessing readable DB\");\n\n        try (Cursor cursor = query(null, null, null, null)) {\n            return getInstancesFromCursor(cursor, instancesPath);\n        }\n    }\n\n    @Override\n    public List<Instance> getAllNotDeleted() {\n        StrictMode.noteSlowCall(\"Accessing readable DB\");\n\n        try (Cursor cursor = query(null, DELETED_DATE + \" IS NULL \", null, null)) {\n            return getInstancesFromCursor(cursor, instancesPath);\n        }\n    }\n\n    @Override\n    public List<Instance> getAllByStatus(String... status) {\n        try (Cursor instancesCursor = getCursorForAllByStatus(status)) {\n            return getInstancesFromCursor(instancesCursor, instancesPath);\n        }\n    }\n\n    @Override\n    public int getCountByStatus(String... status) {\n        try (Cursor cursorForAllByStatus = getCursorForAllByStatus(status)) {\n            return cursorForAllByStatus.getCount();\n        }\n    }\n\n\n    @Override\n    public List<Instance> getAllByFormId(String formId) {\n        StrictMode.noteSlowCall(\"Accessing readable DB\");\n\n        try (Cursor c = query(null, JR_FORM_ID + \" = ?\", new String[]{formId}, null)) {\n            return getInstancesFromCursor(c, instancesPath);\n        }\n    }\n\n    @Override\n    public List<Instance> getAllNotDeletedByFormIdAndVersion(String jrFormId, String jrVersion) {\n        StrictMode.noteSlowCall(\"Accessing readable DB\");\n\n        if (jrVersion != null) {\n            try (Cursor cursor = query(null, JR_FORM_ID + \" = ? AND \" + JR_VERSION + \" = ? AND \" + DELETED_DATE + \" IS NULL\", new String[]{jrFormId, jrVersion}, null)) {\n                return getInstancesFromCursor(cursor, instancesPath);\n            }\n        } else {\n            try (Cursor cursor = query(null, JR_FORM_ID + \" = ? AND \" + JR_VERSION + \" IS NULL AND \" + DELETED_DATE + \" IS NULL\", new String[]{jrFormId}, null)) {\n                return getInstancesFromCursor(cursor, instancesPath);\n            }\n        }\n    }\n\n    @Override\n    public void delete(Long id) {\n        try {\n            Instance instance = get(id);\n\n            databaseConnection.getWritableDatabase().delete(\n                    INSTANCES_TABLE_NAME,\n                    _ID + \"=?\",\n                    new String[]{String.valueOf(id)}\n            );\n\n            deleteInstanceFiles(instance);\n        } catch (SQLiteConstraintException e) {\n            throw new IntegrityException(e.getMessage());\n        }\n    }\n\n    @Override\n    public void deleteAll() {\n        try {\n            List<Instance> instances = getAll();\n\n            databaseConnection.getWritableDatabase().delete(\n                    INSTANCES_TABLE_NAME,\n                    null,\n                    null\n            );\n\n            for (Instance instance : instances) {\n                deleteInstanceFiles(instance);\n            }\n        } catch (SQLiteConstraintException e) {\n            throw new IntegrityException(e.getMessage());\n        }\n    }\n\n    @Override\n    public Instance save(Instance instance) {\n        if (instance.getStatus() == null) {\n            instance = new Instance.Builder(instance)\n                    .status(Instance.STATUS_INCOMPLETE)\n                    .build();\n        }\n\n        Long currentTime = clock.get();\n\n        if (instance.getStatus().equals(Instance.STATUS_COMPLETE) && instance.getFinalizationDate() == null) {\n            instance = new Instance.Builder(instance)\n                    .finalizationDate(currentTime)\n                    .build();\n        }\n\n        if (instance.getDbId() == null) {\n            if (instance.getLastStatusChangeDate() == null) {\n                instance = new Instance.Builder(instance)\n                        .lastStatusChangeDate(currentTime)\n                        .build();\n            }\n\n            long insertId = insert(getValuesFromInstance(instance, instancesPath));\n            return get(insertId);\n        } else {\n            if (instance.getDeletedDate() == null) {\n                instance = new Instance.Builder(instance)\n                        .lastStatusChangeDate(currentTime)\n                        .build();\n            }\n\n            update(instance.getDbId(), getValuesFromInstance(instance, instancesPath));\n            return get(instance.getDbId());\n        }\n    }\n\n    @Override\n    public void deleteWithLogging(Long id) {\n        ContentValues values = new ContentValues();\n        values.putNull(GEOMETRY);\n        values.putNull(GEOMETRY_TYPE);\n        values.put(DELETED_DATE, clock.get());\n        update(id, values);\n\n        Instance instance = get(id);\n        deleteInstanceFiles(instance);\n    }\n\n    public Cursor rawQuery(String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy) {\n        return query(projection, selection, selectionArgs, sortOrder);\n    }\n\n    private Cursor getCursorForAllByStatus(String[] status) {\n        StringBuilder selection = new StringBuilder(STATUS + \"=?\");\n        for (int i = 1; i < status.length; i++) {\n            selection.append(\" or \").append(STATUS).append(\"=?\");\n        }\n\n        return query(null, selection.toString(), status, null);\n    }\n\n    private Cursor query(String[] projection, String selection, String[] selectionArgs, String sortOrder) {\n        SQLiteDatabase readableDatabase = databaseConnection.getReadableDatabase();\n        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();\n        qb.setTables(INSTANCES_TABLE_NAME);\n\n        if (projection == null) {\n            /*\n             For some reason passing null as the projection doesn't always give us all the\n             columns so we hardcode them here so it's explicit that we need these all back.\n             The problem can occur, for example, when a new column is added to a database and the\n             database needs to be updated. After the upgrade, the new column might not be returned,\n             even though it already exists.\n             */\n            projection = new String[]{\n                    _ID,\n                    DISPLAY_NAME,\n                    SUBMISSION_URI,\n                    CAN_EDIT_WHEN_COMPLETE,\n                    INSTANCE_FILE_PATH,\n                    JR_FORM_ID,\n                    JR_VERSION,\n                    STATUS,\n                    LAST_STATUS_CHANGE_DATE,\n                    FINALIZATION_DATE,\n                    DELETED_DATE,\n                    GEOMETRY,\n                    GEOMETRY_TYPE,\n                    CAN_DELETE_BEFORE_SEND,\n                    EDIT_OF,\n                    EDIT_NUMBER\n            };\n        }\n\n        return qb.query(readableDatabase, projection, selection, selectionArgs, null, null, sortOrder);\n    }\n\n    private long insert(ContentValues values) throws IntegrityException {\n        try {\n            return databaseConnection.getWritableDatabase().insertOrThrow(\n                    INSTANCES_TABLE_NAME,\n                    null,\n                    values\n            );\n        } catch (SQLiteConstraintException e) {\n            throw new IntegrityException(e.getMessage());\n        }\n    }\n\n    private void update(Long instanceId, ContentValues values) {\n        databaseConnection.getWritableDatabase().update(\n                INSTANCES_TABLE_NAME,\n                values,\n                _ID + \"=?\",\n                new String[]{instanceId.toString()}\n        );\n    }\n\n    private void deleteInstanceFiles(Instance instance) {\n        FileExt.deleteDirectory(new File(instance.getInstanceFilePath()).getParentFile());\n    }\n\n    private static List<Instance> getInstancesFromCursor(Cursor cursor, String instancesPath) {\n        List<Instance> instances = new ArrayList<>();\n        cursor.moveToPosition(-1);\n        while (cursor.moveToNext()) {\n            Instance instance = getInstanceFromCurrentCursorPosition(cursor, instancesPath);\n            instances.add(instance);\n        }\n\n        return instances;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/instances/InstanceDatabaseMigrator.java",
    "content": "package org.odk.collect.android.database.instances;\n\nimport static android.provider.BaseColumns._ID;\nimport static org.odk.collect.android.database.DatabaseConstants.INSTANCES_TABLE_NAME;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.CAN_DELETE_BEFORE_SEND;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.CAN_EDIT_WHEN_COMPLETE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.DELETED_DATE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.DISPLAY_NAME;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.EDIT_NUMBER;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.EDIT_OF;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.FINALIZATION_DATE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.GEOMETRY;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.GEOMETRY_TYPE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.INSTANCE_FILE_PATH;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.JR_FORM_ID;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.JR_VERSION;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.STATUS;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.SUBMISSION_URI;\nimport static org.odk.collect.db.sqlite.SQLiteDatabaseExt.addColumn;\nimport static org.odk.collect.db.sqlite.SQLiteDatabaseExt.doesColumnExist;\n\nimport android.database.sqlite.SQLiteDatabase;\n\nimport org.odk.collect.db.sqlite.DatabaseMigrator;\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt;\nimport org.odk.collect.db.sqlite.SQLiteUtils;\nimport org.odk.collect.forms.instances.Instance;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport timber.log.Timber;\n\npublic class InstanceDatabaseMigrator implements DatabaseMigrator {\n    private static final String[] COLUMN_NAMES_V5 = {_ID, DISPLAY_NAME, SUBMISSION_URI, CAN_EDIT_WHEN_COMPLETE,\n            INSTANCE_FILE_PATH, JR_FORM_ID, JR_VERSION, STATUS, LAST_STATUS_CHANGE_DATE, DELETED_DATE};\n\n    private static final String[] COLUMN_NAMES_V6 = {_ID, DISPLAY_NAME, SUBMISSION_URI,\n            CAN_EDIT_WHEN_COMPLETE, INSTANCE_FILE_PATH, JR_FORM_ID, JR_VERSION, STATUS,\n            LAST_STATUS_CHANGE_DATE, DELETED_DATE, GEOMETRY, GEOMETRY_TYPE};\n\n    public void onCreate(SQLiteDatabase db) {\n        createInstancesTableV10(db);\n    }\n\n    @SuppressWarnings({\"checkstyle:FallThrough\"})\n    public void onUpgrade(SQLiteDatabase db, int oldVersion) {\n        Timber.w(\"Instances db upgrade from version: %s\", oldVersion);\n        switch (oldVersion) {\n            case 1:\n                upgradeToVersion2(db);\n            case 2:\n                upgradeToVersion3(db);\n            case 3:\n                upgradeToVersion4(db);\n            case 4:\n                upgradeToVersion5(db);\n            case 5:\n                upgradeToVersion6(db, INSTANCES_TABLE_NAME);\n            case 6:\n                upgradeToVersion7(db);\n            case 7:\n                upgradeToVersion8(db);\n            case 8:\n                upgradeToVersion9(db);\n            case 9:\n                upgradeToVersion10(db);\n            case 10:\n                // Remember to bump the database version number in {@link org.odk.collect.android.database.DatabaseConstants}\n                // upgradeToVersion11(db);\n        }\n    }\n\n    private void upgradeToVersion2(SQLiteDatabase db) {\n        if (!doesColumnExist(db, INSTANCES_TABLE_NAME, CAN_EDIT_WHEN_COMPLETE)) {\n            addColumn(db, INSTANCES_TABLE_NAME, CAN_EDIT_WHEN_COMPLETE, \"text\");\n\n            db.execSQL(\"UPDATE \" + INSTANCES_TABLE_NAME + \" SET \"\n                    + CAN_EDIT_WHEN_COMPLETE + \" = '\" + true\n                    + \"' WHERE \" + STATUS + \" IS NOT NULL AND \"\n                    + STATUS + \" != '\" + Instance.STATUS_INCOMPLETE\n                    + \"'\");\n        }\n    }\n\n    private void upgradeToVersion3(SQLiteDatabase db) {\n        addColumn(db, INSTANCES_TABLE_NAME, JR_VERSION, \"text\");\n    }\n\n    private void upgradeToVersion4(SQLiteDatabase db) {\n        addColumn(db, INSTANCES_TABLE_NAME, DELETED_DATE, \"date\");\n    }\n\n    /**\n     * Upgrade to version 5. Prior versions of the instances table included a {@code displaySubtext}\n     * column which was redundant with the {@link DatabaseInstanceColumns#STATUS} and\n     * {@link DatabaseInstanceColumns#LAST_STATUS_CHANGE_DATE} columns and included\n     * unlocalized text. Version 5 removes this column.\n     */\n    private void upgradeToVersion5(SQLiteDatabase db) {\n        String temporaryTableName = INSTANCES_TABLE_NAME + \"_tmp\";\n\n        // onDowngrade in Collect v1.22 always failed to clean up the temporary table so remove it now.\n        // Going from v1.23 to v1.22 and back to v1.23 will result in instance status information\n        // being lost.\n        SQLiteUtils.dropTable(db, temporaryTableName);\n\n        createInstancesTableV5(db, temporaryTableName);\n        dropObsoleteColumns(db, COLUMN_NAMES_V5, temporaryTableName);\n    }\n\n    /**\n     * Use the existing temporary table with the provided name to only keep the given relevant\n     * columns, dropping all others.\n     *\n     * NOTE: the temporary table with the name provided is dropped.\n     *\n     * The move and copy strategy is used to overcome the fact that SQLITE does not directly support\n     * removing a column. See https://sqlite.org/lang_altertable.html\n     *\n     * @param db                    the database to operate on\n     * @param relevantColumns       the columns relevant to the current version\n     * @param temporaryTableName    the name of the temporary table to use and then drop\n     */\n    private void dropObsoleteColumns(SQLiteDatabase db, String[] relevantColumns, String temporaryTableName) {\n        List<String> columns = SQLiteDatabaseExt.getColumnNames(db, INSTANCES_TABLE_NAME);\n        columns.retainAll(Arrays.asList(relevantColumns));\n        String[] columnsToKeep = columns.toArray(new String[0]);\n\n        SQLiteUtils.copyRows(db, INSTANCES_TABLE_NAME, columnsToKeep, temporaryTableName);\n        SQLiteUtils.dropTable(db, INSTANCES_TABLE_NAME);\n        SQLiteUtils.renameTable(db, temporaryTableName, INSTANCES_TABLE_NAME);\n    }\n\n    private void upgradeToVersion6(SQLiteDatabase db, String name) {\n        addColumn(db, name, GEOMETRY, \"text\");\n        addColumn(db, name, GEOMETRY_TYPE, \"text\");\n    }\n\n    private void upgradeToVersion7(SQLiteDatabase db) {\n        String temporaryTable = INSTANCES_TABLE_NAME + \"_tmp\";\n        SQLiteUtils.renameTable(db, INSTANCES_TABLE_NAME, temporaryTable);\n        createInstancesTableV7(db);\n        SQLiteUtils.copyRows(db, temporaryTable, COLUMN_NAMES_V6, INSTANCES_TABLE_NAME);\n        SQLiteUtils.dropTable(db, temporaryTable);\n    }\n\n    private void upgradeToVersion8(SQLiteDatabase db) {\n        addColumn(db, INSTANCES_TABLE_NAME, CAN_DELETE_BEFORE_SEND, \"text\");\n        db.execSQL(\"UPDATE \" + INSTANCES_TABLE_NAME + \" SET \" + CAN_DELETE_BEFORE_SEND + \" = 'true';\");\n    }\n\n    private void upgradeToVersion9(SQLiteDatabase db) {\n        db.execSQL(\"ALTER TABLE \" + INSTANCES_TABLE_NAME + \" ADD COLUMN \" + EDIT_OF + \" integer REFERENCES \" + INSTANCES_TABLE_NAME + \"(\" + _ID + \") CHECK (\" + EDIT_OF + \" != \" + _ID + \")\");\n        db.execSQL(\"ALTER TABLE \" + INSTANCES_TABLE_NAME + \" ADD COLUMN \" + EDIT_NUMBER + \" integer CHECK ((\" + EDIT_OF + \" IS NULL AND \" + EDIT_NUMBER + \" IS NULL) OR + (\" + EDIT_OF + \" IS NOT NULL AND + \" + EDIT_NUMBER + \" IS NOT NULL))\");\n    }\n\n    private void upgradeToVersion10(SQLiteDatabase db) {\n        db.execSQL(\"ALTER TABLE \" + INSTANCES_TABLE_NAME + \" ADD COLUMN \" + FINALIZATION_DATE + \" date\");\n        db.execSQL(\n                \"UPDATE \" + INSTANCES_TABLE_NAME + \" SET \" + FINALIZATION_DATE + \" = \" + LAST_STATUS_CHANGE_DATE + \" WHERE \" + STATUS + \" IN (?, ?, ?);\",\n                new Object[] {Instance.STATUS_COMPLETE, Instance.STATUS_SUBMITTED, Instance.STATUS_SUBMISSION_FAILED}\n        );\n    }\n\n    private void createInstancesTableV5(SQLiteDatabase db, String name) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + name + \" (\"\n                + _ID + \" integer primary key, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + SUBMISSION_URI + \" text, \"\n                + CAN_EDIT_WHEN_COMPLETE + \" text, \"\n                + INSTANCE_FILE_PATH + \" text not null, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + STATUS + \" text not null, \"\n                + LAST_STATUS_CHANGE_DATE + \" date not null, \"\n                + DELETED_DATE + \" date );\");\n    }\n\n    public void createInstancesTableV6(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + INSTANCES_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + SUBMISSION_URI + \" text, \"\n                + CAN_EDIT_WHEN_COMPLETE + \" text, \"\n                + INSTANCE_FILE_PATH + \" text not null, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + STATUS + \" text not null, \"\n                + LAST_STATUS_CHANGE_DATE + \" date not null, \"\n                + DELETED_DATE + \" date, \"\n                + GEOMETRY + \" text, \"\n                + GEOMETRY_TYPE + \" text);\");\n    }\n\n    public void createInstancesTableV7(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + INSTANCES_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key autoincrement, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + SUBMISSION_URI + \" text, \"\n                + CAN_EDIT_WHEN_COMPLETE + \" text, \"\n                + INSTANCE_FILE_PATH + \" text not null, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + STATUS + \" text not null, \"\n                + LAST_STATUS_CHANGE_DATE + \" date not null, \"\n                + DELETED_DATE + \" date, \"\n                + GEOMETRY + \" text, \"\n                + GEOMETRY_TYPE + \" text);\");\n    }\n\n    public void createInstancesTableV8(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + INSTANCES_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key autoincrement, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + SUBMISSION_URI + \" text, \"\n                + CAN_EDIT_WHEN_COMPLETE + \" text, \"\n                + CAN_DELETE_BEFORE_SEND + \" text, \"\n                + INSTANCE_FILE_PATH + \" text not null, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + STATUS + \" text not null, \"\n                + LAST_STATUS_CHANGE_DATE + \" date not null, \"\n                + DELETED_DATE + \" date, \"\n                + GEOMETRY + \" text, \"\n                + GEOMETRY_TYPE + \" text);\");\n    }\n\n    public void createInstancesTableV9(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + INSTANCES_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key autoincrement, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + SUBMISSION_URI + \" text, \"\n                + CAN_EDIT_WHEN_COMPLETE + \" text, \"\n                + CAN_DELETE_BEFORE_SEND + \" text, \"\n                + INSTANCE_FILE_PATH + \" text not null, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + STATUS + \" text not null, \"\n                + LAST_STATUS_CHANGE_DATE + \" date not null, \"\n                + DELETED_DATE + \" date, \"\n                + GEOMETRY + \" text, \"\n                + GEOMETRY_TYPE + \" text, \"\n                + EDIT_OF + \" integer REFERENCES \" + INSTANCES_TABLE_NAME + \"(\" + _ID + \") CHECK (\" + EDIT_OF + \" != \" + _ID + \"),\"\n                + EDIT_NUMBER + \" integer CHECK ((\" + EDIT_OF + \" IS NULL AND \" + EDIT_NUMBER + \" IS NULL) OR + (\" + EDIT_OF + \" IS NOT NULL AND + \" + EDIT_NUMBER + \" IS NOT NULL))\"\n                + \");\"\n        );\n    }\n\n    public void createInstancesTableV10(SQLiteDatabase db) {\n        db.execSQL(\"CREATE TABLE IF NOT EXISTS \" + INSTANCES_TABLE_NAME + \" (\"\n                + _ID + \" integer primary key autoincrement, \"\n                + DISPLAY_NAME + \" text not null, \"\n                + SUBMISSION_URI + \" text, \"\n                + CAN_EDIT_WHEN_COMPLETE + \" text, \"\n                + CAN_DELETE_BEFORE_SEND + \" text, \"\n                + INSTANCE_FILE_PATH + \" text not null, \"\n                + JR_FORM_ID + \" text not null, \"\n                + JR_VERSION + \" text, \"\n                + STATUS + \" text not null, \"\n                + LAST_STATUS_CHANGE_DATE + \" date not null, \"\n                + FINALIZATION_DATE + \" date, \"\n                + DELETED_DATE + \" date, \"\n                + GEOMETRY + \" text, \"\n                + GEOMETRY_TYPE + \" text, \"\n                + EDIT_OF + \" integer REFERENCES \" + INSTANCES_TABLE_NAME + \"(\" + _ID + \") CHECK (\" + EDIT_OF + \" != \" + _ID + \"),\"\n                + EDIT_NUMBER + \" integer CHECK ((\" + EDIT_OF + \" IS NULL AND \" + EDIT_NUMBER + \" IS NULL) OR + (\" + EDIT_OF + \" IS NOT NULL AND + \" + EDIT_NUMBER + \" IS NOT NULL))\"\n                + \");\"\n        );\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/itemsets/DatabaseFastExternalItemsetsRepository.kt",
    "content": "package org.odk.collect.android.database.itemsets\n\nimport org.odk.collect.android.fastexternalitemset.ItemsetDbAdapter\nimport org.odk.collect.android.itemsets.FastExternalItemsetsRepository\n\nclass DatabaseFastExternalItemsetsRepository : FastExternalItemsetsRepository {\n\n    override fun deleteAllByCsvPath(path: String) {\n        ItemsetDbAdapter().open().use {\n            it.delete(path)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/savepoints/DatabaseSavepointsColumns.kt",
    "content": "package org.odk.collect.android.database.savepoints\n\nimport android.provider.BaseColumns\n\nobject DatabaseSavepointsColumns : BaseColumns {\n    /**\n     * The form db id of the blank form that the savepoint belongs to.\n     */\n    const val FORM_DB_ID = \"fromDbId\"\n\n    /**\n     * The instance db id of the saved form that the savepoint belongs to.\n     */\n    const val INSTANCE_DB_ID = \"instanceDbId\"\n\n    /**\n     * The relative path to the file containing a savepoint.\n     */\n    const val SAVEPOINT_FILE_PATH = \"savepointFilePath\"\n\n    /**\n     * The relative path to the instance file.\n     */\n    const val INSTANCE_FILE_PATH = \"instanceFilePath\"\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/savepoints/DatabaseSavepointsRepository.kt",
    "content": "package org.odk.collect.android.database.savepoints\n\nimport android.content.ContentValues\nimport android.content.Context\nimport android.database.Cursor\nimport androidx.core.database.getLongOrNull\nimport org.odk.collect.android.database.DatabaseConstants.SAVEPOINTS_DATABASE_NAME\nimport org.odk.collect.android.database.DatabaseConstants.SAVEPOINTS_DATABASE_VERSION\nimport org.odk.collect.android.database.DatabaseConstants.SAVEPOINTS_TABLE_NAME\nimport org.odk.collect.android.database.savepoints.DatabaseSavepointsColumns.FORM_DB_ID\nimport org.odk.collect.android.database.savepoints.DatabaseSavepointsColumns.INSTANCE_DB_ID\nimport org.odk.collect.androidshared.utils.PathUtils.getAbsoluteFilePath\nimport org.odk.collect.db.sqlite.CursorExt.foldAndClose\nimport org.odk.collect.db.sqlite.DatabaseConnection\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt.delete\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt.query\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport org.odk.collect.shared.PathUtils\nimport java.io.File\n\nclass DatabaseSavepointsRepository(\n    context: Context,\n    dbPath: String,\n    private val cachePath: String,\n    private val instancesPath: String\n) : SavepointsRepository {\n    private val databaseConnection: DatabaseConnection = DatabaseConnection(\n        context,\n        dbPath,\n        SAVEPOINTS_DATABASE_NAME,\n        SavepointsDatabaseMigrator(SAVEPOINTS_DATABASE_VERSION),\n        SAVEPOINTS_DATABASE_VERSION,\n        true\n    )\n\n    override fun get(formDbId: Long, instanceDbId: Long?): Savepoint? {\n        val cursor = if (instanceDbId == null) {\n            databaseConnection.readableDatabase.query(\n                SAVEPOINTS_TABLE_NAME,\n                \"$FORM_DB_ID=? AND $INSTANCE_DB_ID IS NULL\",\n                arrayOf(formDbId.toString())\n            )\n        } else {\n            databaseConnection.readableDatabase.query(\n                SAVEPOINTS_TABLE_NAME,\n                \"$FORM_DB_ID=? AND $INSTANCE_DB_ID=?\",\n                arrayOf(formDbId.toString(), instanceDbId.toString())\n            )\n        }\n        val savepoints = getSavepointsFromCursor(cursor)\n\n        return if (savepoints.isNotEmpty()) savepoints[0] else null\n    }\n\n    override fun getAll(): List<Savepoint> {\n        return getSavepointsFromCursor(\n            databaseConnection.readableDatabase.query(SAVEPOINTS_TABLE_NAME)\n        )\n    }\n\n    override fun save(savepoint: Savepoint) {\n        if (get(savepoint.formDbId, savepoint.instanceDbId) != null) {\n            return\n        }\n\n        val values = getValuesFromSavepoint(savepoint, cachePath, instancesPath)\n\n        databaseConnection\n            .writableDatabase\n            .insertOrThrow(SAVEPOINTS_TABLE_NAME, null, values)\n    }\n\n    override fun delete(formDbId: Long, instanceDbId: Long?) {\n        val savepoint = get(formDbId, instanceDbId) ?: return\n\n        val (selection, selectionArgs) = if (savepoint.instanceDbId == null) {\n            Pair(\n                \"$FORM_DB_ID=? AND $INSTANCE_DB_ID IS NULL\",\n                arrayOf(savepoint.formDbId.toString())\n            )\n        } else {\n            Pair(\n                \"$FORM_DB_ID=? AND $INSTANCE_DB_ID=?\",\n                arrayOf(savepoint.formDbId.toString(), savepoint.instanceDbId.toString())\n            )\n        }\n\n        databaseConnection\n            .writableDatabase\n            .delete(SAVEPOINTS_TABLE_NAME, selection, selectionArgs)\n\n        File(savepoint.savepointFilePath).delete()\n    }\n\n    override fun deleteAll() {\n        getAll().forEach {\n            File(it.savepointFilePath).delete()\n        }\n\n        databaseConnection\n            .writableDatabase\n            .delete(SAVEPOINTS_TABLE_NAME)\n    }\n\n    private fun getSavepointsFromCursor(cursor: Cursor?): List<Savepoint> {\n        return cursor?.foldAndClose(emptyList()) { list, cursor ->\n            list + getSavepointFromCurrentCursorPosition(cursor, cachePath, instancesPath)\n        } ?: emptyList()\n    }\n\n    private fun getSavepointFromCurrentCursorPosition(\n        cursor: Cursor,\n        cachePath: String,\n        instancesPath: String\n    ): Savepoint {\n        val formDbIdColumnIndex = cursor.getColumnIndex(FORM_DB_ID)\n        val instanceDbIdColumnIndex = cursor.getColumnIndex(INSTANCE_DB_ID)\n        val savepointFilePathColumnIndex = cursor.getColumnIndex(DatabaseSavepointsColumns.SAVEPOINT_FILE_PATH)\n        val instanceDirPathColumnIndex = cursor.getColumnIndex(DatabaseSavepointsColumns.INSTANCE_FILE_PATH)\n\n        return Savepoint(\n            cursor.getLong(formDbIdColumnIndex),\n            cursor.getLongOrNull(instanceDbIdColumnIndex),\n            getAbsoluteFilePath(\n                cachePath,\n                cursor.getString(savepointFilePathColumnIndex)\n            ),\n            getAbsoluteFilePath(\n                instancesPath,\n                cursor.getString(instanceDirPathColumnIndex)\n            )\n        )\n    }\n\n    private fun getValuesFromSavepoint(savepoint: Savepoint, cachePath: String, instancesPath: String): ContentValues {\n        return ContentValues().apply {\n            put(FORM_DB_ID, savepoint.formDbId)\n            put(INSTANCE_DB_ID, savepoint.instanceDbId)\n            put(DatabaseSavepointsColumns.SAVEPOINT_FILE_PATH, PathUtils.getRelativeFilePath(cachePath, savepoint.savepointFilePath))\n            put(DatabaseSavepointsColumns.INSTANCE_FILE_PATH, PathUtils.getRelativeFilePath(instancesPath, savepoint.instanceFilePath))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/database/savepoints/SavepointsDatabaseMigrator.kt",
    "content": "package org.odk.collect.android.database.savepoints\n\nimport android.database.sqlite.SQLiteDatabase\nimport android.provider.BaseColumns._ID\nimport org.odk.collect.android.database.DatabaseConstants.SAVEPOINTS_TABLE_NAME\nimport org.odk.collect.android.database.savepoints.DatabaseSavepointsColumns.FORM_DB_ID\nimport org.odk.collect.android.database.savepoints.DatabaseSavepointsColumns.INSTANCE_DB_ID\nimport org.odk.collect.android.database.savepoints.DatabaseSavepointsColumns.INSTANCE_FILE_PATH\nimport org.odk.collect.android.database.savepoints.DatabaseSavepointsColumns.SAVEPOINT_FILE_PATH\nimport org.odk.collect.db.sqlite.MigrationListDatabaseMigrator\n\nclass SavepointsDatabaseMigrator(databaseVersion: Int) : MigrationListDatabaseMigrator(databaseVersion) {\n    override fun createDbForVersion(db: SQLiteDatabase, version: Int) {\n        if (version == 1) {\n            db.execSQL(\n                \"CREATE TABLE IF NOT EXISTS $SAVEPOINTS_TABLE_NAME (\" +\n                    \"$_ID integer PRIMARY KEY, \" +\n                    \"$FORM_DB_ID integer NOT NULL, \" +\n                    \"$INSTANCE_DB_ID integer, \" +\n                    \"$SAVEPOINT_FILE_PATH text NOT NULL, \" +\n                    \"$INSTANCE_FILE_PATH text NOT NULL);\"\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/DynamicPreloadXFormParserFactory.kt",
    "content": "package org.odk.collect.android.dynamicpreload\n\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.QuestionDef\nimport org.javarosa.core.util.externalizable.Externalizable\nimport org.javarosa.core.util.externalizable.PrototypeFactory\nimport org.javarosa.xform.parse.IXFormParserFactory\nimport org.javarosa.xform.parse.XFormParser\nimport org.javarosa.xpath.expr.XPathExpression\nimport java.io.DataInputStream\nimport java.io.DataOutputStream\n\nclass DynamicPreloadXFormParserFactory(base: IXFormParserFactory) :\n    IXFormParserFactory.Wrapper(base) {\n\n    override fun apply(xFormParser: XFormParser): XFormParser {\n        return xFormParser.also {\n            it.addProcessor(DynamicPreloadParseProcessor())\n        }\n    }\n}\n\nclass DynamicPreloadParseProcessor :\n    XFormParser.XPathProcessor,\n    XFormParser.QuestionProcessor,\n    XFormParser.FormDefProcessor {\n\n    private var containsPullData = false\n    private var containsSearch = false\n\n    override fun processXPath(xPathExpression: XPathExpression) {\n        if (containsPullData) {\n            return // No need to search if we already found pulldata\n        }\n\n        if (xPathExpression.containsFunc(\"pulldata\")) {\n            containsPullData = true\n        }\n    }\n\n    override fun processQuestion(question: QuestionDef) {\n        if (containsSearch) {\n            return // No need to search if we already found search\n        }\n\n        if (ExternalDataUtil.getSearchXPathExpression(question.appearanceAttr) != null) {\n            containsSearch = true\n        }\n    }\n\n    override fun processFormDef(formDef: FormDef) {\n        if (containsPullData || containsSearch) {\n            formDef.extras.put(DynamicPreloadExtra())\n        }\n    }\n}\n\nclass DynamicPreloadExtra : Externalizable {\n    override fun readExternal(`in`: DataInputStream?, pf: PrototypeFactory?) = Unit\n\n    override fun writeExternal(out: DataOutputStream?) = Unit\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalAnswerResolver.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload;\n\nimport org.javarosa.core.model.Constants;\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectMultiData;\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.model.instance.TreeElement;\nimport org.javarosa.core.model.instance.utils.DefaultAnswerResolver;\nimport org.javarosa.core.model.utils.DateUtils;\nimport org.javarosa.xform.parse.XFormParser;\nimport org.javarosa.xform.util.XFormAnswerDataSerializer;\nimport org.javarosa.xpath.expr.XPathFuncExpr;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport timber.log.Timber;\n\n/**\n * Author: Meletis Margaritis\n * Date: 17/05/13\n * Time: 16:51\n */\npublic class ExternalAnswerResolver extends DefaultAnswerResolver {\n\n    @Override\n    public IAnswerData resolveAnswer(String textVal, TreeElement treeElement, FormDef formDef) {\n        QuestionDef questionDef = XFormParser.ghettoGetQuestionDef(treeElement.getDataType(),\n                formDef, treeElement.getRef());\n        if (questionDef != null && (questionDef.getControlType() == Constants.CONTROL_SELECT_ONE\n                || questionDef.getControlType() == Constants.CONTROL_SELECT_MULTI\n                || questionDef.getControlType() == Constants.CONTROL_RANK)) {\n            boolean containsSearchExpression = false;\n\n            XPathFuncExpr xpathExpression = null;\n            try {\n                xpathExpression = ExternalDataUtil.getSearchXPathExpression(\n                        questionDef.getAppearanceAttr());\n            } catch (Exception e) {\n                Timber.e(e);\n                // there is a search expression, but has syntax errors\n                containsSearchExpression = true;\n            }\n\n            if (xpathExpression != null || containsSearchExpression) {\n                // that means that we have dynamic selects\n\n                // read the static choices from the options sheet\n                List<SelectChoice> staticChoices = questionDef.getChoices();\n                for (int index = 0; index < staticChoices.size(); index++) {\n                    SelectChoice selectChoice = staticChoices.get(index);\n                    String selectChoiceValue = selectChoice.getValue();\n                    if (ExternalDataUtil.isAnInteger(selectChoiceValue)) {\n\n                        Selection selection = selectChoice.selection();\n\n                        switch (questionDef.getControlType()) {\n                            case Constants.CONTROL_SELECT_ONE: {\n                                if (selectChoiceValue.equals(textVal)) {\n                                    // This means that the user selected a static selection.\n                                    //\n                                    // Although (for select1 fields) the default implementation\n                                    // will catch this and return the right thing\n                                    // (if we call super.resolveAnswer(textVal, treeElement,\n                                    // formDef))\n                                    // we just need to make sure, so we will override that.\n                                    if (questionDef.getControlType()\n                                            == Constants.CONTROL_SELECT_ONE) {\n                                        // we don't need another, just return the static choice.\n                                        return new SelectOneData(selection);\n                                    }\n                                }\n                                break;\n                            }\n                            case Constants.CONTROL_SELECT_MULTI:\n                            case Constants.CONTROL_RANK: {\n                                // we should search in a potential comma-separated string of\n                                // values for a match\n                                // copied from org.javarosa.xform.util.XFormAnswerDataParser\n                                // .getSelections()\n                                List<String> textValues = DateUtils.split(textVal,\n                                        XFormAnswerDataSerializer.DELIMITER, true);\n                                if (textValues.contains(textVal)) {\n                                    // this means that the user has selected AT LEAST the static\n                                    // choice.\n                                    if (selectChoiceValue.equals(textVal)) {\n                                        // this means that the user selected ONLY the static\n                                        // answer, so just return that\n                                        List<Selection> customSelections = new ArrayList<>();\n                                        customSelections.add(selection);\n                                        return new SelectMultiData(customSelections);\n                                    } else {\n                                        // we will ignore it for now since we will return that\n                                        // selection together with the dynamic ones.\n                                    }\n                                }\n                                break;\n                            }\n                            default: {\n                                // There is a bug if we get here, so let's throw an Exception\n                                throw createBugRuntimeException(treeElement, textVal);\n                            }\n                        }\n\n                    } else {\n                        switch (questionDef.getControlType()) {\n                            case Constants.CONTROL_SELECT_ONE: {\n                                // the default implementation will search for the \"textVal\"\n                                // (saved answer) inside the static choices.\n                                // Since we know that there isn't such, we just wrap the textVal\n                                // in a virtual choice in order to\n                                // create a SelectOneData object to be used as the IAnswer to the\n                                // TreeElement.\n                                // (the caller of this function is searching for such an answer\n                                // to populate the in-memory model.)\n                                SelectChoice customSelectChoice = new SelectChoice(textVal, textVal,\n                                        false);\n                                customSelectChoice.setIndex(index);\n                                return new SelectOneData(customSelectChoice.selection());\n                            }\n                            case Constants.CONTROL_SELECT_MULTI:\n                            case Constants.CONTROL_RANK: {\n                                // we should create multiple selections and add them to the pile\n                                List<SelectChoice> customSelectChoices = createCustomSelectChoices(\n                                        textVal);\n                                List<Selection> customSelections = new ArrayList<>();\n                                for (SelectChoice customSelectChoice : customSelectChoices) {\n                                    customSelections.add(customSelectChoice.selection());\n                                }\n                                return new SelectMultiData(customSelections);\n                            }\n                            default: {\n                                // There is a bug if we get here, so let's throw an Exception\n                                throw createBugRuntimeException(treeElement, textVal);\n                            }\n                        }\n\n                    }\n                }\n\n                // if we get there then that means that we have a bug\n                throw createBugRuntimeException(treeElement, textVal);\n            }\n        }\n        // default behavior matches original behavior (for static selects, etc.)\n        return super.resolveAnswer(textVal, treeElement, formDef);\n    }\n\n    private RuntimeException createBugRuntimeException(TreeElement treeElement, String textVal) {\n        return new RuntimeException(\"The appearance column of the field \" + treeElement.getName()\n                + \" contains a search() call and the field type is \" + treeElement.getDataType()\n                + \" and the saved answer is \" + textVal);\n    }\n\n    protected List<SelectChoice> createCustomSelectChoices(String completeTextValue) {\n        // copied from org.javarosa.xform.util.XFormAnswerDataParser.getSelections()\n        List<String> textValues = DateUtils.split(completeTextValue,\n                XFormAnswerDataSerializer.DELIMITER, true);\n\n        int index = 0;\n        List<SelectChoice> customSelectChoices = new ArrayList<>();\n        for (String textValue : textValues) {\n            SelectChoice selectChoice = new SelectChoice(textValue, textValue, false);\n            selectChoice.setIndex(index++);\n            customSelectChoices.add(selectChoice);\n        }\n\n        return customSelectChoices;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalAppsUtils.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload;\n\nimport android.content.Intent;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.condition.EvaluationContext;\nimport org.javarosa.core.model.data.DecimalData;\nimport org.javarosa.core.model.data.IntegerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.model.instance.FormInstance;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.javarosa.model.xform.XPathReference;\nimport org.javarosa.xpath.XPathNodeset;\nimport org.javarosa.xpath.XPathParseTool;\nimport org.javarosa.xpath.expr.XPathExpression;\nimport org.javarosa.xpath.expr.XPathFuncExpr;\nimport org.javarosa.xpath.expr.XPathPathExpr;\nimport org.javarosa.xpath.parser.XPathSyntaxException;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.exception.ExternalParamsException;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Author: Meletis Margaritis\n * Date: 30/07/13\n * Time: 10:44\n */\npublic final class ExternalAppsUtils {\n\n    private static final String LEFT_PARENTHESIS = \"(\";\n    private static final String RIGHT_PARENTHESIS = \")\";\n\n    private ExternalAppsUtils() {\n    }\n\n    public static String extractIntentName(String exString) {\n        if (!exString.contains(LEFT_PARENTHESIS)) {\n            return exString.contains(RIGHT_PARENTHESIS)\n                    ? exString.substring(0, exString.indexOf(RIGHT_PARENTHESIS)).trim()\n                    : exString;\n        }\n\n        return exString.substring(0, exString.indexOf(LEFT_PARENTHESIS)).trim();\n    }\n\n    public static Map<String, String> extractParameters(String exString) {\n        exString = exString.trim();\n\n        int leftParIndex = exString.indexOf(LEFT_PARENTHESIS);\n        if (leftParIndex == -1) {\n            return Collections.emptyMap();\n        }\n\n        String paramsStr = exString.endsWith(\")\")\n                ? exString.substring(leftParIndex + 1, exString.lastIndexOf(')'))\n                : exString.substring(leftParIndex + 1);\n\n        Map<String, String> parameters = new LinkedHashMap<>();\n        List<String> paramsPairs = getParamPairs(paramsStr.trim());\n        for (String paramsPair : paramsPairs) {\n            String[] keyValue = paramsPair.trim().split(\"=\");\n            if (keyValue.length == 2) {\n                parameters.put(keyValue[0].trim(), keyValue[1].trim());\n            }\n        }\n        return parameters;\n    }\n\n    private static List<String> getParamPairs(String paramsStr) {\n        List<String> paramPairs = new ArrayList<>();\n        int startPos = 0;\n        boolean inQuotes = false;\n        for (int current = 0; current < paramsStr.length(); current++) {\n            if (paramsStr.charAt(current) == '\\'') {\n                inQuotes = !inQuotes;\n            }\n\n            if (current == paramsStr.length() - 1) {\n                paramPairs.add(paramsStr.substring(startPos));\n            } else if (paramsStr.charAt(current) == ',' && !inQuotes) {\n                paramPairs.add(paramsStr.substring(startPos, current));\n                startPos = current + 1;\n            }\n        }\n\n        return paramPairs;\n    }\n\n    public static void populateParameters(Intent intent, Map<String, String> exParams,\n                                          TreeReference reference, FormController formController) throws ExternalParamsException {\n        if (exParams != null) {\n            for (Map.Entry<String, String> paramEntry : exParams.entrySet()) {\n                String paramEntryValue = paramEntry.getValue();\n                try {\n                    Object result = getValueRepresentedBy(paramEntry.getValue(), reference, formController);\n\n                    if (result instanceof Serializable) {\n                        intent.putExtra(paramEntry.getKey(), (Serializable) result);\n                    }\n                } catch (Exception e) {\n                    throw new ExternalParamsException(\n                            \"Could not evaluate '\" + paramEntryValue + \"'\", e);\n                }\n            }\n        }\n    }\n\n    public static Object getValueRepresentedBy(String text, TreeReference reference, FormController formController) throws XPathSyntaxException {\n        if (text.startsWith(\"'\")) {\n            // treat this as a constant parameter but not require an ending quote\n            return text.endsWith(\"'\") ? text.substring(1, text.length() - 1) : text.substring(1);\n        }\n\n        FormDef formDef = formController.getFormDef();\n        FormInstance formInstance = formDef.getInstance();\n        EvaluationContext evaluationContext = new EvaluationContext(formDef.getEvaluationContext(), reference);\n        if (text.startsWith(\"/\")) {\n            // treat this is an xpath\n            XPathPathExpr pathExpr = XPathReference.getPathExpr(text);\n            XPathNodeset xpathNodeset = pathExpr.eval(formInstance, evaluationContext);\n            return XPathFuncExpr.unpack(xpathNodeset);\n        } else if (text.equals(\"instanceProviderID()\")) {\n            // instanceProviderID returns -1 if the current instance has not been saved to disk already\n            String path = formController.getInstanceFile().getAbsolutePath();\n\n            String instanceProviderID = \"-1\";\n            Instance instance = new InstancesRepositoryProvider(Collect.getInstance()).create().getOneByPath(path);\n            if (instance != null) {\n                instanceProviderID = instance.getDbId().toString();\n            }\n\n            return instanceProviderID;\n        } else {\n            // treat this as a function\n            XPathExpression xpathExpression = XPathParseTool.parseXPath(text);\n            return xpathExpression.eval(formInstance, evaluationContext);\n        }\n    }\n\n    public static StringData asStringData(Object value) {\n        return value == null ? null : new StringData(value.toString());\n    }\n\n    public static IntegerData asIntegerData(Object value) {\n        if (value == null) {\n            return null;\n        } else {\n            try {\n                String s = value.toString();\n                int i = Integer.parseInt(s);\n                return new IntegerData(i);\n            } catch (NumberFormatException e) {\n                return null;\n            }\n        }\n    }\n\n    public static DecimalData asDecimalData(Object value) {\n        if (value == null) {\n            return null;\n        } else {\n            try {\n                String s = value.toString();\n                double d = Double.parseDouble(s);\n                return new DecimalData(d);\n            } catch (NumberFormatException e) {\n                return null;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataHandler.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload;\n\nimport org.javarosa.core.model.condition.IFunctionHandler;\n\n/**\n * Author: Meletis Margaritis\n * Date: 30/04/13\n * Time: 09:42\n */\npublic interface ExternalDataHandler extends IFunctionHandler {\n\n    void setExternalDataManager(ExternalDataManager externalDataManager);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataManager.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload;\n\n/**\n * This class handles all DB connections for the function handlers\n * <p/>\n * Author: Meletis Margaritis\n * Date: 14/05/13\n * Time: 17:17\n */\npublic interface ExternalDataManager {\n\n    /**\n     * Returns an object of {@link ExternalSQLiteOpenHelper}\n     *\n     * @param dataSetName the name of the imported .csv\n     * @param required    if true, a runtime exception\n     *                    ({@link org.odk.collect.android.exception.ExternalDataException})\n     *                    will be thrown.\n     *                    if false null will be returned\n     * @return an object of {@link ExternalSQLiteOpenHelper}\n     */\n    ExternalSQLiteOpenHelper getDatabase(String dataSetName, boolean required);\n\n    void close();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataManagerImpl.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload;\n\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.exception.ExternalDataException;\n\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport timber.log.Timber;\n\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\n/**\n * Author: Meletis Margaritis\n * Date: 14/05/13\n * Time: 17:19\n */\npublic class ExternalDataManagerImpl implements ExternalDataManager {\n\n    private final Map<String, ExternalSQLiteOpenHelper> dbMap = new HashMap<>();\n\n    private final File mediaFolder;\n\n    public ExternalDataManagerImpl(File mediaFolder) {\n        this.mediaFolder = mediaFolder;\n    }\n\n    @Override\n    public ExternalSQLiteOpenHelper getDatabase(String dataSetName, boolean required) {\n        ExternalSQLiteOpenHelper sqLiteOpenHelper = dbMap.get(dataSetName);\n        if (sqLiteOpenHelper == null) {\n            if (mediaFolder == null) {\n                String msg = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_not_initialized_error);\n                Timber.e(new Error(msg));\n                if (required) {\n                    throw new ExternalDataException(msg);\n                } else {\n                    return null;\n                }\n            } else {\n                sqLiteOpenHelper = new ExternalSQLiteOpenHelper(new File(mediaFolder, dataSetName + \".db\"));\n                dbMap.put(dataSetName, sqLiteOpenHelper);\n            }\n        }\n        return sqLiteOpenHelper;\n    }\n\n    @Override\n    public void close() {\n        if (dbMap != null) {\n            for (ExternalSQLiteOpenHelper externalSQLiteOpenHelper : dbMap.values()) {\n                Timber.w(\"Closing database handler:%s\", externalSQLiteOpenHelper.toString());\n                externalSQLiteOpenHelper.close();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataReader.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload;\n\nimport java.io.File;\nimport java.util.Map;\n\n/**\n * Author: Meletis Margaritis\n * Date: 30/04/13\n * Time: 13:52\n */\npublic interface ExternalDataReader {\n\n    void doImport(Map<String, File> externalDataMap);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataReaderImpl.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload;\n\nimport android.content.res.Resources;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport org.apache.commons.io.FileUtils;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\nimport timber.log.Timber;\n\n/**\n * Author: Meletis Margaritis\n * Date: 30/04/13\n * Time: 09:32\n */\npublic class ExternalDataReaderImpl implements ExternalDataReader {\n\n    private final Supplier<Boolean> isCancelled;\n    private final Consumer<Function<Resources, String>> progressReporter;\n\n    public ExternalDataReaderImpl(Supplier<Boolean> isCancelled, Consumer<Function<Resources, String>> progressReporter) {\n        this.isCancelled = isCancelled;\n        this.progressReporter = progressReporter;\n    }\n\n    @Override\n    public void doImport(Map<String, File> externalDataMap) {\n        for (Map.Entry<String, File> stringFileEntry : externalDataMap.entrySet()) {\n            String dataSetName = stringFileEntry.getKey();\n            File dataSetFile = stringFileEntry.getValue();\n            if (!dataSetFile.exists()) {\n                continue;\n            }\n            if (!doImportDataSetAndContinue(dataSetName, dataSetFile)) {\n                return; // halt if import was cancelled\n            }\n        }\n    }\n\n    private boolean doImportDataSetAndContinue(String dataSetName, File dataSetFile) {\n        File dbFile = new File(dataSetFile.getParentFile().getAbsolutePath(),\n                dataSetName + \".db\");\n        if (dbFile.exists()) {\n            // Determine if we need to reimport\n            if (ExternalSQLiteOpenHelper.shouldUpdateDBforDataSet(dbFile, dataSetFile)) {\n                boolean deleted = dbFile.delete();\n                if (!deleted) {\n                    Timber.e(new Error(dataSetFile.getName() + \" has changed but we could not delete the previous DB at \" + dbFile.getAbsolutePath()));\n                    return true;\n                }\n            } else {\n                return true;\n            }\n        }\n        ExternalSQLiteOpenHelper externalSQLiteOpenHelper = new ExternalSQLiteOpenHelper(\n                dbFile);\n        externalSQLiteOpenHelper.importFromCSV(dataSetFile, this, isCancelled, progressReporter);\n\n        if (isCancelled.get()) {\n            Timber.w(\n                    \"The import was cancelled, so we need to rollback.\");\n\n            // we need to drop the database file since it might be partially populated.\n            // It will be re-created next time.\n\n            Timber.w(\"Closing database to be deleted %s\", dbFile.toString());\n\n            // then close the database\n            SQLiteDatabase db = externalSQLiteOpenHelper.getReadableDatabase();\n            db.close();\n\n            // the physically delete the db.\n            try {\n                FileUtils.forceDelete(dbFile);\n                Timber.w(\"Deleted %s\", dbFile.getName());\n            } catch (IOException e) {\n                Timber.e(e);\n            }\n\n            // then just exit and do not process any other CSVs.\n            return false;\n\n        }\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataUseCases.kt",
    "content": "package org.odk.collect.android.dynamicpreload\n\nimport android.content.res.Resources\nimport org.javarosa.core.model.FormDef\nimport java.io.File\nimport java.util.function.Consumer\nimport java.util.function.Function\nimport java.util.function.Supplier\n\nobject ExternalDataUseCases {\n\n    @JvmStatic\n    fun create(\n        form: FormDef,\n        mediaDir: File,\n        isCancelled: Supplier<Boolean>,\n        progressReporter: Consumer<Function<Resources, String>>\n    ) {\n        if (form.extras.get(DynamicPreloadExtra::class.java) == null) {\n            return\n        }\n\n        val csvFiles = mediaDir.listFiles { file ->\n            val lowerCaseName = file.name.lowercase()\n            lowerCaseName.endsWith(\".csv\") && !lowerCaseName.equals(\n                \"itemsets.csv\",\n                ignoreCase = true\n            )\n        }\n\n        val externalDataMap: MutableMap<String, File> = HashMap()\n\n        if (csvFiles != null) {\n            for (csvFile in csvFiles) {\n                val dataSetName = csvFile.name.substring(\n                    0,\n                    csvFile.name.lastIndexOf(\".\")\n                )\n                externalDataMap[dataSetName] = csvFile\n            }\n            if (externalDataMap.isNotEmpty()) {\n                progressReporter.accept { resources ->\n                    resources.getString(org.odk.collect.strings.R.string.survey_loading_reading_csv_message)\n                }\n                val externalDataReader: ExternalDataReader =\n                    ExternalDataReaderImpl(isCancelled, progressReporter)\n                externalDataReader.doImport(externalDataMap)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalDataUtil.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload;\n\nimport android.widget.Toast;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.condition.EvaluationContext;\nimport org.javarosa.core.model.instance.FormInstance;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.javarosa.xpath.XPathParseTool;\nimport org.javarosa.xpath.expr.XPathExpression;\nimport org.javarosa.xpath.expr.XPathFuncExpr;\nimport org.javarosa.xpath.parser.XPathSyntaxException;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.exception.ExternalDataException;\nimport org.odk.collect.android.dynamicpreload.handler.ExternalDataHandlerSearch;\nimport org.odk.collect.android.javarosawrapper.FormController;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.StringTokenizer;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport timber.log.Timber;\n\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\n/**\n * Author: Meletis Margaritis\n * Date: 30/04/13\n * Time: 09:29\n */\n\npublic final class ExternalDataUtil {\n\n    public static final String EXTERNAL_DATA_TABLE_NAME = \"externalData\";\n    public static final String EXTERNAL_METADATA_TABLE_NAME = \"externalMetadata\";\n    public static final String SORT_COLUMN_NAME = \"c_sortby\";\n    public static final String COLUMN_DATASET_FILENAME = \"dataSetFilename\";\n    public static final String COLUMN_MD5_HASH = \"md5Hash\";\n\n    public static final Pattern SEARCH_FUNCTION_REGEX = Pattern.compile(\"search\\\\(.+\\\\)\");\n    private static final String COLUMN_SEPARATOR = \",\";\n    private static final String FALLBACK_COLUMN_SEPARATOR = \" \";\n    public static final String JR_IMAGES_PREFIX = \"jr://images/\";\n\n    private ExternalDataUtil() {\n\n    }\n\n    public static String toSafeColumnName(String columnName, Map<String, String> cache) {\n        String cachedName = cache.get(columnName);\n        if (cachedName == null) {\n            String safeColumnName = toSafeColumnName(columnName);\n            cache.put(columnName, safeColumnName);\n            return safeColumnName;\n        } else {\n            return cachedName;\n        }\n    }\n\n    public static String toSafeColumnName(String columnName) {\n        // SCTO-567 - begin all column names with \"c_\" to avoid possible conflicts with\n        // reserved keywords; also, escape any potentially-illegal characters\n        return \"c_\" + columnName.trim().replaceAll(\"[^A-Za-z0-9_]\", \"_\").toLowerCase(\n                Locale.ENGLISH);\n    }\n\n    public static List<String> findMatchingColumnsAfterSafeningNames(String[] columnNames) {\n        // key is the safe, value is the unsafe\n        Map<String, String> map = new HashMap<>();\n        for (String columnName : columnNames) {\n            if (columnName.trim().length() > 0) {\n                String safeColumn = toSafeColumnName(columnName);\n                if (!map.containsKey(safeColumn)) {\n                    map.put(safeColumn, columnName);\n                } else {\n                    return Arrays.asList(map.get(safeColumn), columnName);\n                }\n            }\n        }\n        return null;\n    }\n\n    public static XPathFuncExpr getSearchXPathExpression(String appearance) {\n        if (appearance == null) {\n            appearance = \"\";\n        }\n        appearance = appearance.trim();\n\n        Matcher matcher = SEARCH_FUNCTION_REGEX.matcher(appearance);\n        if (matcher.find()) {\n\n            String function = matcher.group(0);\n            try {\n                XPathExpression xpathExpression = XPathParseTool.parseXPath(function);\n                if (XPathFuncExpr.class.isAssignableFrom(xpathExpression.getClass())) {\n                    XPathFuncExpr xpathFuncExpr = (XPathFuncExpr) xpathExpression;\n                    if (xpathFuncExpr.id.name.equalsIgnoreCase(\n                            ExternalDataHandlerSearch.HANDLER_NAME)) {\n                        // also check that the args are either 1, 4 or 6.\n                        if (xpathFuncExpr.args.length == 1 || xpathFuncExpr.args.length == 4\n                                || xpathFuncExpr.args.length == 6) {\n                            return xpathFuncExpr;\n                        } else {\n                            Toast.makeText(Collect.getInstance(),\n                                    getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_search_wrong_arguments_error),\n                                    Toast.LENGTH_SHORT).show();\n                            Timber.i(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_search_wrong_arguments_error));\n                            return null;\n                        }\n                    } else {\n                        // this might mean a problem in the regex above. Unit tests required.\n                        Toast.makeText(Collect.getInstance(),\n                                getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_search_wrong_function_error, xpathFuncExpr.id.name),\n                                Toast.LENGTH_SHORT).show();\n                        Timber.i(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_search_wrong_function_error, xpathFuncExpr.id.name));\n                        return null;\n                    }\n                } else {\n                    // this might mean a problem in the regex above. Unit tests required.\n                    Toast.makeText(Collect.getInstance(),\n                            getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_search_bad_function_error, function),\n                            Toast.LENGTH_SHORT).show();\n                    Timber.i(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_search_bad_function_error, function));\n                    return null;\n                }\n            } catch (XPathSyntaxException e) {\n                Toast.makeText(Collect.getInstance(),\n                        getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_search_generic_error, appearance),\n                        Toast.LENGTH_SHORT).show();\n                Timber.i(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_search_generic_error, appearance));\n                return null;\n            }\n        } else {\n            return null;\n        }\n    }\n\n    public static ArrayList<SelectChoice> populateExternalChoices(FormEntryPrompt formEntryPrompt,\n            XPathFuncExpr xpathfuncexpr, FormController formController) throws FileNotFoundException {\n        try {\n            List<SelectChoice> selectChoices = formEntryPrompt.getSelectChoices();\n            if (!containsConfigurationChoice(selectChoices)) {\n                String filePath = getFilePath(xpathfuncexpr, formController);\n                throw new FileNotFoundException(filePath);\n            }\n            ArrayList<SelectChoice> returnedChoices = new ArrayList<>();\n            for (SelectChoice selectChoice : selectChoices) {\n                String value = selectChoice.getValue();\n                if (isAnInteger(value)) {\n                    // treat this as a static choice\n                    returnedChoices.add(selectChoice);\n                } else {\n                    String displayColumns = formEntryPrompt.getSelectChoiceText(selectChoice);\n                    String imageColumn = formEntryPrompt.getSpecialFormSelectChoiceText(\n                            selectChoice, FormEntryCaption.TEXT_FORM_IMAGE);\n                    if (imageColumn != null && imageColumn.startsWith(JR_IMAGES_PREFIX)) {\n                        imageColumn = imageColumn.substring(JR_IMAGES_PREFIX.length());\n                    }\n                    //                    if (displayColumns == null || displayColumns.trim().length() == 0) {\n                    //                        throw new InvalidSyntaxException(\"The label column in the choices sheet\n                    // appears to be empty (or has been calculated as empty).\");\n                    //                    }\n\n                    ExternalDataManager externalDataManager =\n                            Collect.getInstance().getExternalDataManager();\n                    FormInstance formInstance = formController.getFormDef().getInstance();\n                    EvaluationContext baseEvaluationContext = new EvaluationContext(formInstance);\n                    EvaluationContext evaluationContext = new EvaluationContext(\n                            baseEvaluationContext, formEntryPrompt.getIndex().getReference());\n                    // we can only add only the appropriate by querying the xPathFuncExpr.id.name\n                    evaluationContext.addFunctionHandler(\n                            new ExternalDataHandlerSearch(externalDataManager, displayColumns,\n                                    value, imageColumn));\n\n                    Object eval = xpathfuncexpr.eval(formInstance, evaluationContext);\n                    if (eval.getClass().isAssignableFrom(ArrayList.class)) {\n                        @SuppressWarnings(\"unchecked\")\n                        List<SelectChoice> dynamicChoices = (ArrayList<SelectChoice>) eval;\n                        for (SelectChoice dynamicChoice : dynamicChoices) {\n                            returnedChoices.add(dynamicChoice);\n                        }\n                    } else {\n                        throw new ExternalDataException(\n                                getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_search_return_error,\n                                        eval.getClass().getName()));\n                    }\n                }\n            }\n            return returnedChoices;\n        } catch (Exception e) {\n            String filePath = getFilePath(xpathfuncexpr, formController);\n            if (!new File(filePath).exists()) {\n                throw new FileNotFoundException(filePath);\n            }\n\n            throw new ExternalDataException(e.getMessage(), e);\n        }\n    }\n\n    /**\n     * We could simple return new String(displayColumns + \",\" + valueColumn) but we want to handle\n     * the cases\n     * where the displayColumns (valueColumn) contain more commas than needed, in the middle, start\n     * or end.\n     *\n     * @param valueColumn    single string to appear first.\n     * @param displayColumns comma-separated string\n     * @return A {@link java.util.LinkedHashMap} that contains the SQL columns as keys, and the CSV\n     * columns as values\n     */\n    public static LinkedHashMap<String, String> createMapWithDisplayingColumns(String valueColumn,\n            String displayColumns) {\n        valueColumn = valueColumn.trim();\n\n        LinkedHashMap<String, String> columns = new LinkedHashMap<>();\n\n        columns.put(toSafeColumnName(valueColumn), valueColumn);\n\n        if (displayColumns != null && displayColumns.trim().length() > 0) {\n            displayColumns = displayColumns.trim();\n\n            List<String> commaSplitParts = splitTrimmed(displayColumns, COLUMN_SEPARATOR,\n                    FALLBACK_COLUMN_SEPARATOR);\n\n            for (String commaSplitPart : commaSplitParts) {\n                columns.put(toSafeColumnName(commaSplitPart), commaSplitPart);\n            }\n        }\n\n        return columns;\n    }\n\n    public static List<String> createListOfColumns(String columnString) {\n        List<String> values = new ArrayList<>();\n\n        List<String> commaSplitParts = splitTrimmed(columnString, COLUMN_SEPARATOR,\n                FALLBACK_COLUMN_SEPARATOR);\n\n        for (String commaSplitPart : commaSplitParts) {\n            values.add(toSafeColumnName(commaSplitPart));\n        }\n\n        return values;\n    }\n\n    private static List<String> splitTrimmed(String displayColumns, String separator,\n            String fallbackSeparator) {\n        List<String> commaSplitParts = splitTrimmed(displayColumns, separator);\n\n        // SCTO-584: Fall back to a space-separated list\n        if (commaSplitParts.size() == 1 && displayColumns.contains(fallbackSeparator)) {\n            commaSplitParts = splitTrimmed(displayColumns, fallbackSeparator);\n        }\n        return commaSplitParts;\n    }\n\n    private static List<String> splitTrimmed(String text, String separator) {\n        List<String> parts = new ArrayList<>();\n        StringTokenizer st = new StringTokenizer(text, separator);\n        while (st.hasMoreTokens()) {\n            String token = st.nextToken().trim();\n            if (token.length() > 0) {\n                parts.add(token);\n            }\n        }\n        return parts;\n    }\n\n    public static boolean containsAnyData(String[] row) {\n        if (row == null || row.length == 0) {\n            return false;\n        }\n        for (String value : row) {\n            if (value != null && value.trim().length() > 0) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public static String[] fillUpNullValues(String[] row, String[] headerRow) {\n        String[] fullRow = new String[headerRow.length];\n\n        for (int i = 0; i < fullRow.length; i++) {\n            if (i < row.length) {\n                String value = row[i];\n                if (value == null) {\n                    value = \"\";\n                }\n                fullRow[i] = value;\n            } else {\n                fullRow[i] = \"\";\n            }\n        }\n\n        return fullRow;\n    }\n\n    public static String nullSafe(String value) {\n        return value == null ? \"\" : value;\n    }\n\n    public static boolean isAnInteger(String value) {\n        if (value == null) {\n            return false;\n        }\n\n        value = value.trim();\n\n        try {\n            Integer.parseInt(value);\n            return true;\n        } catch (NumberFormatException e) {\n            return false;\n        }\n    }\n\n    /**\n     * The config row is a special row in the choices worksheet that defines how\n     * choices are mapped from an external .csv file with list_name, name and label,\n     * where these correspond to columns in the .csv file.\n     */\n    private static boolean containsConfigurationChoice(List<SelectChoice> selectChoices) {\n        for (SelectChoice choice : selectChoices) {\n            if (!isAnInteger(choice.getValue())) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private static String getFilePath(XPathFuncExpr xpathfuncexpr, FormController formController) {\n        String fileName = String.valueOf(xpathfuncexpr.args[0].eval(null, null));\n        if (!fileName.endsWith(\".csv\")) {\n            fileName = fileName + \".csv\";\n        }\n        String filePath = fileName;\n        if (formController != null) {\n            filePath = formController.getMediaFolder() + File.separator + fileName;\n        }\n        return filePath;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalSQLiteOpenHelper.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload;\n\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.content.ContentValues;\nimport android.content.res.Resources;\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteOpenHelper;\n\nimport com.opencsv.CSVParserBuilder;\nimport com.opencsv.CSVReader;\nimport com.opencsv.CSVReaderBuilder;\n\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.db.sqlite.AltDatabasePathContext;\nimport org.odk.collect.android.exception.ExternalDataException;\nimport org.odk.collect.db.sqlite.CustomSQLiteQueryBuilder;\nimport org.odk.collect.db.sqlite.CustomSQLiteQueryExecutor;\nimport org.odk.collect.db.sqlite.SQLiteUtils;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.File;\nimport java.io.FileReader;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\nimport timber.log.Timber;\n\n/**\n * Author: Meletis Margaritis\n * Date: 30/04/13\n * Time: 09:36\n */\npublic class ExternalSQLiteOpenHelper extends SQLiteOpenHelper {\n\n    private static final int VERSION = 1;\n    private static final char DELIMITING_CHAR = \",\".charAt(0);\n    private static final char QUOTE_CHAR = \"\\\"\".charAt(0);\n    private static final char ESCAPE_CHAR = \"\\0\".charAt(0);\n\n    private File dataSetFile;\n    private ExternalDataReader externalDataReader;\n    private Supplier<Boolean> isCancelled;\n    private Consumer<Function<Resources, String>> progressReporter;\n\n    ExternalSQLiteOpenHelper(File dbFile) {\n        super(new AltDatabasePathContext(dbFile.getParentFile().getAbsolutePath(), Collect.getInstance()), dbFile.getName(), null, VERSION);\n    }\n\n    void importFromCSV(File dataSetFile, ExternalDataReader externalDataReader,\n                       Supplier<Boolean> isCancelled, Consumer<Function<Resources, String>> progressReporter) {\n        this.dataSetFile = dataSetFile;\n        this.externalDataReader = externalDataReader;\n        this.isCancelled = isCancelled;\n        this.progressReporter = progressReporter;\n\n        SQLiteDatabase writableDatabase = null;\n        try {\n            writableDatabase = getWritableDatabase();\n        } finally {\n            if (writableDatabase != null) {\n                writableDatabase.close();\n            }\n        }\n    }\n\n    @Override\n    public void onCreate(SQLiteDatabase db) {\n        if (externalDataReader == null) {\n            // this means that the function handler needed the database through calling\n            // getReadableDatabase() --> getWritableDatabase(),\n            // but this is not allowed, so just return;\n            Timber.e(new Error(\"The function handler triggered this external data population. This is not good.\"));\n            return;\n        }\n\n        try {\n            if (shouldUpdateDBforDataSet(db, ExternalDataUtil.EXTERNAL_DATA_TABLE_NAME, ExternalDataUtil.EXTERNAL_METADATA_TABLE_NAME, dataSetFile)) {\n                onCreateNamed(db, ExternalDataUtil.EXTERNAL_DATA_TABLE_NAME);\n                createAndPopulateMetadataTable(db, ExternalDataUtil.EXTERNAL_METADATA_TABLE_NAME, dataSetFile);\n            }\n        } catch (Exception e) {\n            throw new ExternalDataException(\n                    getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_import_generic_error,\n                            dataSetFile.getName(), e.getMessage()), e);\n        }\n    }\n\n    private void onCreateNamed(SQLiteDatabase db, String tableName) throws Exception {\n        Timber.w(\"Reading data from '%s\", dataSetFile.toString());\n\n        onProgress(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_import_progress_message,\n                dataSetFile.getName(), \"\"));\n\n        CSVReader reader = null;\n        try {\n            reader = new CSVReaderBuilder(new FileReader(dataSetFile))\n                    .withCSVParser(new CSVParserBuilder()\n                            .withSeparator(DELIMITING_CHAR)\n                            .withQuoteChar(QUOTE_CHAR)\n                            .withEscapeChar(ESCAPE_CHAR)\n                            .build())\n                    .build();\n            String[] headerRow = reader.readNext();\n\n            headerRow[0] = removeByteOrderMark(headerRow[0]);\n\n            if (!ExternalDataUtil.containsAnyData(headerRow)) {\n                throw new ExternalDataException(\n                        getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_file_no_data_error));\n            }\n\n            List<String> conflictingColumns =\n                    ExternalDataUtil.findMatchingColumnsAfterSafeningNames(headerRow);\n\n            if (conflictingColumns != null && !conflictingColumns.isEmpty()) {\n                // this means that after removing invalid characters, some column names resulted\n                // with the same name,\n                // so the create table query will fail with \"duplicate column\" error.\n                throw new ExternalDataException(\n                        getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_conflicting_columns_error,\n                                conflictingColumns));\n            }\n\n            Map<String, String> columnNamesCache = new HashMap<>();\n\n            StringBuilder sb = new StringBuilder();\n\n            boolean sortColumnAlreadyPresent = false;\n\n            sb\n                    .append(\"CREATE TABLE IF NOT EXISTS \")\n                    .append(tableName)\n                    .append(\" ( \");\n\n            for (int i = 0; i < headerRow.length; i++) {\n                String columnName = headerRow[i].trim();\n                if (columnName.length() == 0) {\n                    continue;\n                }\n                if (i != 0) {\n                    sb.append(\", \");\n                }\n                String safeColumnName = ExternalDataUtil.toSafeColumnName(columnName,\n                        columnNamesCache);\n                if (safeColumnName.equals(ExternalDataUtil.SORT_COLUMN_NAME)) {\n                    sortColumnAlreadyPresent = true;\n                    sb.append(safeColumnName).append(\" real \");\n                } else {\n                    sb.append(safeColumnName).append(\" text collate nocase \");\n                }\n            }\n            if (!sortColumnAlreadyPresent) {\n                sb.append(\", \");\n                sb.append(ExternalDataUtil.SORT_COLUMN_NAME).append(\" real \");\n            }\n\n            sb.append(\" );\");\n            String sql = sb.toString();\n\n            Timber.w(\"Creating database for %s with query: %s\", dataSetFile, sql);\n            db.execSQL(sql);\n\n            // create the indexes.\n            // save the sql for later because inserts will be much faster if we don't have\n            // indexes already.\n            List<String> createIndexesCommands = new ArrayList<>();\n            for (String header : headerRow) {\n                if (header.endsWith(\"_key\")) {\n                    String indexSQL = \"CREATE INDEX \" + header + \"_idx ON \" + tableName + \" (\"\n                            + ExternalDataUtil.toSafeColumnName(header, columnNamesCache) + \");\";\n                    createIndexesCommands.add(indexSQL);\n                    Timber.w(\"Will create an index on %s later.\", header);\n                }\n            }\n\n            // populate the database\n            String[] row = reader.readNext();\n            int rowCount = 0;\n            while (row != null && !isCancelled()) {\n                // SCTO-894 - first we should make sure that this is not an empty line\n                if (!ExternalDataUtil.containsAnyData(row)) {\n                    // yes, that is an empty row, ignore it\n                    row = reader.readNext();\n                    continue;\n                }\n\n                // SCTO-894 - then check if the row contains less values than the header\n                // we should not ignore the existing values in the row,\n                // we will just fill up the rest with empty strings\n                if (row.length < headerRow.length) {\n                    row = ExternalDataUtil.fillUpNullValues(row, headerRow);\n                }\n\n                ContentValues values = new ContentValues();\n                if (!sortColumnAlreadyPresent) {\n                    values.put(ExternalDataUtil.SORT_COLUMN_NAME, rowCount + 1);\n                }\n\n                for (int i = 0; i < row.length && i < headerRow.length; i++) {\n                    String columnName = headerRow[i].trim();\n                    String columnValue = row[i];\n                    if (columnName.length() == 0) {\n                        continue;\n                    }\n                    String safeColumnName = ExternalDataUtil.toSafeColumnName(columnName,\n                            columnNamesCache);\n                    if (safeColumnName.equals(ExternalDataUtil.SORT_COLUMN_NAME)) {\n                        try {\n                            values.put(safeColumnName, Double.parseDouble(columnValue));\n                        } catch (NumberFormatException e) {\n                            throw new ExternalDataException(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_sortBy_numeric_error, columnValue));\n                        }\n                    } else {\n                        values.put(safeColumnName, columnValue);\n                    }\n                }\n                db.insertOrThrow(tableName, null, values);\n                row = reader.readNext();\n                rowCount++;\n                if (rowCount % 100 == 0) {\n                    onProgress(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_import_progress_message,\n                            dataSetFile.getName(), \" (\" + rowCount + \" records so far)\"));\n                }\n            }\n\n            if (isCancelled()) {\n                Timber.w(\"User canceled reading data from %s\", dataSetFile.toString());\n                onProgress(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_import_cancelled_message));\n            } else {\n\n                onProgress(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_import_finalizing_message));\n\n                // now create the indexes\n                for (String createIndexCommand : createIndexesCommands) {\n                    Timber.w(createIndexCommand);\n                    db.execSQL(createIndexCommand);\n                }\n\n                Timber.w(\"Read all data from %s\", dataSetFile.toString());\n                onProgress(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_import_completed_message));\n            }\n        } finally {\n            if (reader != null) {\n                try {\n                    reader.close();\n                } catch (IOException e) {\n                    Timber.e(e);\n                }\n            }\n        }\n    }\n\n    protected boolean isCancelled() {\n        return isCancelled.get();\n    }\n\n    // Create a metadata table with a single column that keeps track of the date of the last import\n    // of this data set.\n    static void createAndPopulateMetadataTable(SQLiteDatabase db, String metadataTableName, File dataSetFile) {\n        final String dataSetFilenameColumn = CustomSQLiteQueryBuilder.quoteIdentifier(ExternalDataUtil.COLUMN_DATASET_FILENAME);\n        final String md5HashColumn = CustomSQLiteQueryBuilder.quoteIdentifier(ExternalDataUtil.COLUMN_MD5_HASH);\n\n        List<String> columnDefinitions = new ArrayList<>();\n        columnDefinitions.add(CustomSQLiteQueryBuilder.formatColumnDefinition(dataSetFilenameColumn, \"TEXT\"));\n        columnDefinitions.add(CustomSQLiteQueryBuilder.formatColumnDefinition(md5HashColumn, \"TEXT NOT NULL\"));\n\n        CustomSQLiteQueryExecutor.begin(db).createTable(metadataTableName).columnsForCreate(columnDefinitions).end();\n\n        ContentValues metadata = new ContentValues();\n        metadata.put(ExternalDataUtil.COLUMN_DATASET_FILENAME, dataSetFile.getName());\n        metadata.put(ExternalDataUtil.COLUMN_MD5_HASH, Md5.getMd5Hash(dataSetFile));\n        db.insertOrThrow(metadataTableName, null, metadata);\n    }\n\n    static String getLastMd5Hash(SQLiteDatabase db, String metadataTableName, File dataSetFile) {\n        final String dataSetFilenameColumn = CustomSQLiteQueryBuilder.quoteIdentifier(ExternalDataUtil.COLUMN_DATASET_FILENAME);\n        final String md5HashColumn = CustomSQLiteQueryBuilder.quoteIdentifier(ExternalDataUtil.COLUMN_MD5_HASH);\n        final String dataSetFilenameLiteral = CustomSQLiteQueryBuilder.quoteStringLiteral(dataSetFile.getName());\n\n        String[] columns = {md5HashColumn};\n        String selectionCriteria = CustomSQLiteQueryBuilder.formatCompareEquals(dataSetFilenameColumn, dataSetFilenameLiteral);\n        Cursor cursor = db.query(metadataTableName, columns, selectionCriteria, null, null, null, null);\n\n        String lastImportMd5 = \"\";\n        if (cursor != null && cursor.getCount() == 1) {\n            cursor.moveToFirst();\n            lastImportMd5 = cursor.getString(0);\n        }\n        cursor.close();\n        return lastImportMd5;\n    }\n\n    static boolean shouldUpdateDBforDataSet(File dbFile, File dataSetFile) {\n        SQLiteDatabase db = SQLiteDatabase.openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY);\n        return shouldUpdateDBforDataSet(db, ExternalDataUtil.EXTERNAL_DATA_TABLE_NAME, ExternalDataUtil.EXTERNAL_METADATA_TABLE_NAME, dataSetFile);\n    }\n\n    static boolean shouldUpdateDBforDataSet(SQLiteDatabase db, String dataTableName, String metadataTableName, File dataSetFile) {\n        if (!SQLiteUtils.doesTableExist(db, dataTableName)) {\n            return true;\n        }\n        if (!SQLiteUtils.doesTableExist(db, metadataTableName)) {\n            return true;\n        }\n        // Import if the CSV file has been updated\n        String priorImportMd5 = getLastMd5Hash(db, metadataTableName, dataSetFile);\n        String newFileMd5 = Md5.getMd5Hash(dataSetFile);\n        return newFileMd5 == null || !newFileMd5.equals(priorImportMd5);\n    }\n\n    @Override\n    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {\n    }\n\n    private void onProgress(String message) {\n        progressReporter.accept(resources -> message);\n    }\n\n    /**\n     * Removes a Byte Order Mark (BOM) from the start of a String.\n     *\n     * @param bomCheckString is checked to see if it starts with a Byte Order Mark.\n     * @return bomCheckString without a Byte Order Mark.\n     */\n    private String removeByteOrderMark(String bomCheckString) {\n        return bomCheckString.startsWith(\"\\uFEFF\") ? bomCheckString.substring(1) : bomCheckString;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/ExternalSelectChoice.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload;\n\nimport org.javarosa.core.model.SelectChoice;\n\n/**\n * Author: Meletis Margaritis\n * Date: 18/11/2013\n * Time: 2:08 μμ\n */\npublic class ExternalSelectChoice extends SelectChoice {\n\n    String image;\n\n    public ExternalSelectChoice(String labelOrID, String value, boolean isLocalizable) {\n        super(labelOrID, value, isLocalizable);\n    }\n\n    public String getImage() {\n        return image;\n    }\n\n    public void setImage(String image) {\n        this.image = image;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/handler/ExternalDataHandlerBase.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload.handler;\n\nimport org.odk.collect.android.dynamicpreload.ExternalDataHandler;\nimport org.odk.collect.android.dynamicpreload.ExternalDataManager;\n\nimport java.util.Locale;\n\n/**\n * Author: Meletis Margaritis\n * Date: 16/05/13\n * Time: 10:42\n */\npublic abstract class ExternalDataHandlerBase implements ExternalDataHandler {\n\n    private ExternalDataManager externalDataManager;\n\n    public ExternalDataManager getExternalDataManager() {\n        return externalDataManager;\n    }\n\n    public void setExternalDataManager(ExternalDataManager externalDataManager) {\n        this.externalDataManager = externalDataManager;\n    }\n\n    protected ExternalDataHandlerBase(ExternalDataManager externalDataManager) {\n        this.setExternalDataManager(externalDataManager);\n    }\n\n    /**\n     * SCTO-545\n     *\n     * @param dataSetName the user-supplied data-set in the function\n     * @return the normalized data-set name.\n     */\n    protected String normalize(String dataSetName) {\n        dataSetName = dataSetName.toLowerCase(Locale.US);\n        if (dataSetName.endsWith(\".csv\")) {\n            dataSetName = dataSetName.substring(0, dataSetName.lastIndexOf(\".csv\"));\n        }\n        return dataSetName;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/handler/ExternalDataHandlerPull.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload.handler;\n\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteException;\n\nimport org.javarosa.core.model.condition.EvaluationContext;\nimport org.javarosa.xpath.expr.XPathFuncExpr;\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.dynamicpreload.ExternalDataManager;\nimport org.odk.collect.android.dynamicpreload.ExternalDataUtil;\nimport org.odk.collect.android.dynamicpreload.ExternalSQLiteOpenHelper;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport timber.log.Timber;\n\n/**\n * Author: Meletis Margaritis\n * Date: 25/04/13\n * Time: 13:50\n */\npublic class ExternalDataHandlerPull extends ExternalDataHandlerBase {\n\n    public static final String HANDLER_NAME = \"pulldata\";\n\n    public ExternalDataHandlerPull(ExternalDataManager externalDataManager) {\n        super(externalDataManager);\n    }\n\n    @Override\n    public String getName() {\n        return HANDLER_NAME;\n    }\n\n    @Override\n    public @NotNull List<@NotNull Class<?>@NotNull []> getPrototypes() {\n        return new ArrayList<>();\n    }\n\n    @Override\n    public boolean rawArgs() {\n        return true;\n    }\n\n    @Override\n    public boolean realTime() {\n        return false;\n    }\n\n    @Override\n    public Object eval(Object[] args, EvaluationContext ec) {\n\n        if (args.length != 4) {\n            Timber.e(new Error(\"4 arguments are needed to evaluate the \" + HANDLER_NAME + \" function\"));\n            return \"\";\n        }\n\n        String dataSetName = XPathFuncExpr.toString(args[0]);\n        String queriedColumn = XPathFuncExpr.toString(args[1]);\n        String referenceColumn = XPathFuncExpr.toString(args[2]);\n        String referenceValue = XPathFuncExpr.toString(args[3]);\n\n        // SCTO-545\n        dataSetName = normalize(dataSetName);\n\n        Cursor c = null;\n        try {\n            ExternalSQLiteOpenHelper sqLiteOpenHelper = getExternalDataManager().getDatabase(\n                    dataSetName, false);\n            if (sqLiteOpenHelper == null) {\n                return \"\";\n            }\n\n            SQLiteDatabase db = sqLiteOpenHelper.getReadableDatabase();\n            String[] columns = {ExternalDataUtil.toSafeColumnName(queriedColumn)};\n            String selection = ExternalDataUtil.toSafeColumnName(referenceColumn) + \"=?\";\n            String[] selectionArgs = {referenceValue};\n\n            c = db.query(ExternalDataUtil.EXTERNAL_DATA_TABLE_NAME, columns, selection,\n                    selectionArgs, null, null, null);\n            if (c.getCount() > 0) {\n                c.moveToFirst();\n                return ExternalDataUtil.nullSafe(c.getString(0));\n            } else {\n                Timber.i(\"Could not find a value in %s where the column %s has the value %s\",\n                        queriedColumn, referenceColumn, referenceValue);\n                return \"\";\n            }\n        } catch (SQLiteException e) {\n            Timber.i(e);\n            return \"\";\n        } finally {\n            if (c != null) {\n                c.close();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/handler/ExternalDataHandlerSearch.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload.handler;\n\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.condition.EvaluationContext;\nimport org.javarosa.xpath.expr.XPathFuncExpr;\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.dynamicpreload.ExternalDataManager;\nimport org.odk.collect.android.dynamicpreload.ExternalDataUtil;\nimport org.odk.collect.android.dynamicpreload.ExternalSQLiteOpenHelper;\nimport org.odk.collect.android.dynamicpreload.ExternalSelectChoice;\nimport org.odk.collect.android.exception.ExternalDataException;\nimport org.odk.collect.shared.strings.StringUtils;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Set;\n\nimport timber.log.Timber;\n\n/**\n * Author: Meletis Margaritis\n * Date: 16/05/13\n * Time: 10:42\n */\npublic class ExternalDataHandlerSearch extends ExternalDataHandlerBase {\n\n    public static final String HANDLER_NAME = \"search\";\n\n    private final String displayColumns;\n    private final String valueColumn;\n    private final String imageColumn;\n\n    public ExternalDataHandlerSearch(ExternalDataManager externalDataManager, String displayColumns,\n            String valueColumn, String imageColumn) {\n        super(externalDataManager);\n        this.displayColumns = displayColumns;\n        this.valueColumn = valueColumn;\n        this.imageColumn = imageColumn;\n    }\n\n    public String getDisplayColumns() {\n        return displayColumns;\n    }\n\n    public String getValueColumn() {\n        return valueColumn;\n    }\n\n    public String getImageColumn() {\n        return imageColumn;\n    }\n\n    @Override\n    public String getName() {\n        return HANDLER_NAME;\n    }\n\n    @Override\n    public @NotNull List<@NotNull Class<?>@NotNull []> getPrototypes() {\n        return new ArrayList<>();\n    }\n\n    @Override\n    public boolean rawArgs() {\n        return true;\n    }\n\n    @Override\n    public boolean realTime() {\n        return false;\n    }\n\n    @Override\n    public Object eval(Object[] args, EvaluationContext ec) {\n        if (args == null || (args.length != 1 && args.length != 4 && args.length != 6)) {\n            // we should never get here since it is already handled in ExternalDataUtil\n            // .getSearchXPathExpression(String appearance)\n            throw new ExternalDataException(\n                    getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_search_wrong_arguments_error));\n        }\n\n        String searchType = null;\n\n        String queriedColumnsParam = null;\n        List<String> queriedColumns = null;\n        String queriedValue = null;\n        if (args.length >= 4) {\n            searchType = XPathFuncExpr.toString(args[1]);\n            queriedColumnsParam = XPathFuncExpr.toString(args[2]);\n            queriedValue = XPathFuncExpr.toString(args[3]);\n        }\n\n        ExternalDataSearchType externalDataSearchType = ExternalDataSearchType.getByKeyword(\n                searchType, ExternalDataSearchType.CONTAINS);\n\n        boolean searchRows = false;\n        boolean useFilter = false;\n\n        if (queriedColumnsParam != null && queriedColumnsParam.trim().length() > 0) {\n            searchRows = true;\n            queriedColumns = ExternalDataUtil.createListOfColumns(queriedColumnsParam);\n        }\n\n        String filterColumn = null;\n        String filterValue = null;\n        if (args.length == 6) {\n            filterColumn = XPathFuncExpr.toString(args[4]);\n            filterValue = XPathFuncExpr.toString(args[5]);\n            useFilter = true;\n        }\n\n        // SCTO-545\n        String dataSetName = normalize(XPathFuncExpr.toString(args[0]));\n\n        Cursor c = null;\n        try {\n            ExternalSQLiteOpenHelper sqLiteOpenHelper = getExternalDataManager().getDatabase(\n                    dataSetName, true);\n\n            SQLiteDatabase db = sqLiteOpenHelper.getReadableDatabase();\n            LinkedHashMap<String, String> selectColumnMap =\n                    ExternalDataUtil.createMapWithDisplayingColumns(getValueColumn(),\n                            getDisplayColumns());\n\n            List<String> columnsToFetch = new ArrayList<>(selectColumnMap.keySet());\n            String safeImageColumn = null;\n            if (getImageColumn() != null && getImageColumn().trim().length() > 0) {\n                safeImageColumn = ExternalDataUtil.toSafeColumnName(getImageColumn());\n                columnsToFetch.add(safeImageColumn);\n            }\n\n            String[] sqlColumns = columnsToFetch.toArray(new String[0]);\n\n            String selection;\n            String[] selectionArgs;\n\n            if (searchRows && useFilter) {\n                selection = \"( \" + createLikeExpression(queriedColumns) + \" ) AND \"\n                        + ExternalDataUtil.toSafeColumnName(filterColumn) + \"=? \";\n                String[] likeArgs = externalDataSearchType.constructLikeArguments(queriedValue,\n                        queriedColumns.size());\n                selectionArgs = new String[likeArgs.length + 1];\n                System.arraycopy(likeArgs, 0, selectionArgs, 0, likeArgs.length);\n                selectionArgs[selectionArgs.length - 1] = filterValue;\n            } else if (searchRows) {\n                selection = createLikeExpression(queriedColumns);\n                selectionArgs = externalDataSearchType.constructLikeArguments(queriedValue,\n                        queriedColumns.size());\n            } else if (useFilter) {\n                selection = ExternalDataUtil.toSafeColumnName(filterColumn) + \"=? \";\n                selectionArgs = new String[]{filterValue};\n            } else {\n                selection = null;\n                selectionArgs = null;\n            }\n\n            try {\n                c = db.query(ExternalDataUtil.EXTERNAL_DATA_TABLE_NAME, sqlColumns, selection,\n                        selectionArgs, null, null, ExternalDataUtil.SORT_COLUMN_NAME);\n            } catch (Exception e) {\n                Timber.e(new Error(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.ext_import_csv_missing_error, dataSetName, dataSetName)));\n                c = db.query(ExternalDataUtil.EXTERNAL_DATA_TABLE_NAME, sqlColumns, selection,\n                        selectionArgs, null, null, null);\n            }\n\n            return createDynamicSelectChoices(c, selectColumnMap, safeImageColumn);\n        } finally {\n            if (c != null) {\n                c.close();\n            }\n        }\n    }\n\n    protected ArrayList<SelectChoice> createDynamicSelectChoices(Cursor c,\n            LinkedHashMap<String, String> selectColumnMap, String safeImageColumn) {\n        List<String> columnsToExcludeFromLabels = new ArrayList<>();\n        if (safeImageColumn != null) {\n            columnsToExcludeFromLabels.add(safeImageColumn);\n        }\n\n        ArrayList<SelectChoice> selectChoices = new ArrayList<>();\n        if (c.getCount() > 0) {\n            c.moveToPosition(-1);\n            int index = 0;\n            Set<String> uniqueValues = new HashSet<>();\n            while (c.moveToNext()) {\n\n                // the value is always the first column\n                String value = c.getString(0);\n                if (!uniqueValues.contains(value)) {\n                    String label = buildLabel(c, selectColumnMap, columnsToExcludeFromLabels);\n\n                    ExternalSelectChoice selectChoice;\n                    if (StringUtils.isBlank(label)) {\n                        selectChoice = new ExternalSelectChoice(value, value, false);\n                    } else {\n                        selectChoice = new ExternalSelectChoice(label, value, false);\n                    }\n                    selectChoice.setIndex(index);\n\n                    if (safeImageColumn != null && safeImageColumn.trim().length() > 0) {\n                        String image = c.getString(c.getColumnIndex(safeImageColumn));\n                        if (image != null && image.trim().length() > 0) {\n                            selectChoice.setImage(ExternalDataUtil.JR_IMAGES_PREFIX + image);\n                        }\n                    }\n\n                    selectChoices.add(selectChoice);\n\n                    index++;\n\n                    uniqueValues.add(value);\n                }\n            }\n        }\n        return selectChoices;\n    }\n\n    protected String createLikeExpression(List<String> queriedColumns) {\n        StringBuilder sb = new StringBuilder();\n        for (String queriedColumn : queriedColumns) {\n            if (sb.length() > 0) {\n                sb.append(\" OR \");\n            }\n            sb.append(queriedColumn).append(\" LIKE ? \");\n        }\n        return sb.toString();\n    }\n\n    /**\n     * So here are examples of labels with one, two, and three columns:\n     * <p/>\n     * col1value\n     * col1value (col2name: col2value)\n     * col1value (col2name: col2value) (col3name: col3value)\n     */\n    protected String buildLabel(Cursor c, LinkedHashMap<String, String> selectColumnMap,\n            List<String> columnsToExcludeFromLabels) {\n        StringBuilder sb = new StringBuilder();\n        // we start at 1 since 0 is the \"value\" column\n        for (int columnIndex = 1; columnIndex < c.getColumnCount(); columnIndex++) {\n            String columnName = c.getColumnName(columnIndex);\n            if (columnsToExcludeFromLabels.contains(columnName)) {\n                continue;\n            }\n\n            String value = c.getString(columnIndex);\n\n            if (columnIndex == 1) {\n                sb.append(value);\n                continue;\n            }\n            if (c.getColumnCount() - columnsToExcludeFromLabels.size() == 2) {\n                break;\n            }\n            if (columnIndex > 1) {\n                sb.append(' ');\n            }\n            sb.append('(')\n                .append(selectColumnMap.get(columnName))\n                .append(\": \")\n                .append(value)\n                .append(')');\n        }\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/dynamicpreload/handler/ExternalDataSearchType.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.dynamicpreload.handler;\n\n/**\n * Author: Meletis Margaritis\n * Date: 20/05/13\n * Time: 12:22\n */\nenum ExternalDataSearchType {\n\n    CONTAINS(\"contains\") {\n        @Override\n        protected String getSingleLikeArgument(String queriedValue) {\n            return '%' + queriedValue + '%';\n        }\n    },\n\n    MATCHES(\"matches\") {\n        @Override\n        protected String getSingleLikeArgument(String queriedValue) {\n            return queriedValue;\n        }\n    },\n\n    STARTS(\"startsWith\") {\n        @Override\n        protected String getSingleLikeArgument(String queriedValue) {\n            return queriedValue + '%';\n        }\n    },\n\n    ENDS(\"endsWith\") {\n        @Override\n        protected String getSingleLikeArgument(String queriedValue) {\n            return '%' + queriedValue;\n        }\n    };\n\n    private final String keyword;\n\n    ExternalDataSearchType(String keyword) {\n        this.keyword = keyword;\n    }\n\n    public String getKeyword() {\n        return keyword;\n    }\n\n    public static ExternalDataSearchType getByKeyword(String keyword,\n            ExternalDataSearchType fallback) {\n        if (keyword == null) {\n            return fallback;\n        }\n        for (ExternalDataSearchType externalDataSearchType : ExternalDataSearchType.values()) {\n            if (externalDataSearchType.getKeyword().trim().equalsIgnoreCase(keyword.trim())) {\n                return externalDataSearchType;\n            }\n        }\n\n        return fallback;\n    }\n\n    public String[] constructLikeArguments(String queriedValue, int times) {\n        String[] args = new String[times];\n        for (int i = 0; i < times; i++) {\n            args[i] = getSingleLikeArgument(queriedValue);\n        }\n        return args;\n    }\n\n    protected abstract String getSingleLikeArgument(String queriedValue);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/entities/EntitiesRepositoryProvider.kt",
    "content": "package org.odk.collect.android.entities\n\nimport android.content.Context\nimport org.odk.collect.android.database.entities.DatabaseEntitiesRepository\nimport org.odk.collect.android.storage.StoragePaths\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.projects.ProjectDependencyFactory\n\nclass EntitiesRepositoryProvider(\n    private val context: Context,\n    private val storagePathFactory: ProjectDependencyFactory<StoragePaths>\n) :\n    ProjectDependencyFactory<EntitiesRepository> {\n\n    override fun create(projectId: String): EntitiesRepository {\n        return DatabaseEntitiesRepository(\n            context,\n            storagePathFactory.create(projectId).metaDir,\n            System::currentTimeMillis\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/exception/EncryptionException.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.exception;\n\n/**\n * Author: Meletis Margaritis\n * Date: 30/09/13\n * Time: 11:39\n */\npublic class EncryptionException extends Exception {\n\n    public EncryptionException(String detailMessage, Throwable throwable) {\n        super(detailMessage, throwable);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/exception/ExternalDataException.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.exception;\n\n/**\n * Author: Meletis Margaritis\n * Date: 01/05/13\n * Time: 10:57\n */\npublic class ExternalDataException extends RuntimeException {\n\n    public ExternalDataException(String detailMessage) {\n        super(detailMessage);\n    }\n\n    public ExternalDataException(String detailMessage, Throwable throwable) {\n        super(detailMessage, throwable);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/exception/ExternalParamsException.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.exception;\n\n/**\n * Author: Meletis Margaritis\n * Date: 30/07/13\n * Time: 12:27\n */\npublic class ExternalParamsException extends Exception {\n\n    public ExternalParamsException(String detailMessage, Throwable throwable) {\n        super(detailMessage, throwable);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/exception/JavaRosaException.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.exception;\n\n/**\n * This exception wraps all exceptions that come from JR.\n *\n * Author: Meletis Margaritis\n * Date: 2/13/14\n * Time: 11:39 AM\n */\npublic class JavaRosaException extends Exception {\n\n    public JavaRosaException(Throwable throwable) {\n        super(throwable.getMessage() == null ? throwable.getClass().getSimpleName()\n                : throwable.getMessage(), throwable);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/external/AndroidShortcutsActivity.kt",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.external\n\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.Parcelable\nimport androidx.activity.viewModels\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.pm.ShortcutInfoCompat\nimport androidx.core.content.pm.ShortcutManagerCompat\nimport androidx.core.graphics.drawable.IconCompat\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.R\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListItem\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListViewModel\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.settings.SettingsProvider\nimport java.util.UUID\nimport javax.inject.Inject\n\n/**\n * Allows the user to create desktop shortcuts to any form currently available to Collect\n */\nclass AndroidShortcutsActivity : AppCompatActivity() {\n    @Inject\n    lateinit var viewModelFactory: BlankFormListViewModel.Factory\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    private val viewModel: BlankFormListViewModel by viewModels { viewModelFactory }\n\n    private var dialog: AlertDialog? = null\n\n    public override fun onCreate(bundle: Bundle?) {\n        super.onCreate(bundle)\n        DaggerUtils.getComponent(this).inject(this)\n\n        viewModel.formsToDisplay.observe(this) { forms ->\n            showFormListDialog(forms)\n        }\n    }\n\n    private fun showFormListDialog(blankFormListItems: List<BlankFormListItem>) {\n        dialog?.dismiss()\n        dialog = MaterialAlertDialogBuilder(this)\n            .setTitle(org.odk.collect.strings.R.string.select_odk_shortcut)\n            .setItems(\n                blankFormListItems\n                    .map { it.formName }\n                    .toTypedArray()\n            ) { _: DialogInterface?, item: Int ->\n                Analytics.log(AnalyticsEvents.ADD_SHORTCUT)\n\n                val intent = getShortcutIntent(blankFormListItems, item)\n                setResult(RESULT_OK, intent)\n                finish()\n            }\n            .setOnCancelListener {\n                setResult(RESULT_CANCELED)\n                finish()\n            }\n            .create()\n            .also {\n                it.show()\n            }\n    }\n\n    private fun getShortcutIntent(forms: List<BlankFormListItem>, item: Int): Intent {\n        val shortcutIntent = Intent(Intent.ACTION_EDIT).apply {\n            data = forms[item].contentUri\n        }\n\n        return if (Build.VERSION.SDK_INT >= 36) {\n            ShortcutManagerCompat.createShortcutResultIntent(\n                this,\n                ShortcutInfoCompat.Builder(this, UUID.randomUUID().toString())\n                    .setIntent(shortcutIntent)\n                    .setShortLabel(forms[item].formName)\n                    .setIcon(IconCompat.createWithResource(this, R.drawable.notes))\n                    .build()\n            )\n        } else {\n            Intent().apply {\n                putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent)\n                putExtra(Intent.EXTRA_SHORTCUT_NAME, forms[item].formName)\n                val iconResource: Parcelable =\n                    Intent.ShortcutIconResource.fromContext(\n                        this@AndroidShortcutsActivity,\n                        R.drawable.notes\n                    )\n                putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/external/FormUriActivity.kt",
    "content": "package org.odk.collect.android.external\n\nimport android.content.ContentResolver\nimport android.content.Intent\nimport android.content.res.Resources\nimport android.net.Uri\nimport android.os.Bundle\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.activity.viewModels\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewmodel.CreationExtras\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.R\nimport org.odk.collect.android.activities.FormFillingActivity\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.formentry.FormOpeningMode\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.instancemanagement.InstanceDeleter\nimport org.odk.collect.android.instancemanagement.canBeEdited\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.savepoints.SavepointUseCases\nimport org.odk.collect.android.utilities.ChangeLockProvider\nimport org.odk.collect.android.utilities.ContentUriHelper\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.utilities.SavepointsRepositoryProvider\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.Locale\nimport javax.inject.Inject\n\nconst val FORM_ENTRY_TOKEN = \"form_entry_token\"\n\n/**\n * This class serves as a firewall for starting form filling. It should be used to do that\n * rather than [FormFillingActivity] directly as it ensures that the required data is valid.\n */\nclass FormUriActivity : LocalizedActivity() {\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    @Inject\n    lateinit var projectsRepository: ProjectsRepository\n\n    @Inject\n    lateinit var formsRepositoryProvider: FormsRepositoryProvider\n\n    @Inject\n    lateinit var instanceRepositoryProvider: InstancesRepositoryProvider\n\n    @Inject\n    lateinit var savepointsRepositoryProvider: SavepointsRepositoryProvider\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    @Inject\n    lateinit var changeLockProvider: ChangeLockProvider\n\n    private var formFillingAlreadyStarted = false\n\n    private val openForm =\n        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {\n            setResult(it.resultCode, it.data)\n            finish()\n        }\n\n    private val formUriViewModel by viewModels<FormUriViewModel> {\n        object : ViewModelProvider.Factory {\n            override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {\n                return FormUriViewModel(\n                    intent,\n                    scheduler,\n                    projectsRepository,\n                    projectsDataService,\n                    contentResolver,\n                    formsRepositoryProvider,\n                    instanceRepositoryProvider,\n                    settingsProvider,\n                    savepointsRepositoryProvider,\n                    changeLockProvider,\n                    resources\n                ) as T\n            }\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        DaggerUtils.getComponent(this).inject(this)\n        setView(R.layout.circular_progress_indicator, false)\n\n        if (savedInstanceState?.getBoolean(FORM_FILLING_ALREADY_STARTED) == true) {\n            return\n        }\n\n        formUriViewModel.formInspectionResult.observe(this) {\n            when (it) {\n                is FormInspectionResult.Error -> displayErrorDialog(it.error)\n                is FormInspectionResult.Savepoint -> displaySavePointRecoveryDialog(it.savepoint)\n                is FormInspectionResult.Valid -> startForm(intent.data!!)\n            }\n        }\n    }\n\n    private fun displaySavePointRecoveryDialog(savepoint: Savepoint) {\n        MaterialAlertDialogBuilder(this)\n            .setTitle(string.savepoint_recovery_dialog_title)\n            .setMessage(SimpleDateFormat(getString(string.savepoint_recovery_dialog_message), Locale.getDefault()).format(File(savepoint.savepointFilePath).lastModified()))\n            .setCancelable(false)\n            .setPositiveButton(string.recover) { _, _ ->\n                val uri = intent.data!!\n                val uriMimeType = contentResolver.getType(uri)!!\n                if (uriMimeType == FormsContract.CONTENT_ITEM_TYPE) {\n                    startForm(FormsContract.getUri(projectsDataService.requireCurrentProject().uuid, savepoint.formDbId))\n                } else {\n                    startForm(intent.data!!)\n                }\n            }\n            .setNegativeButton(string.do_not_recover) { _, _ ->\n                formUriViewModel.deleteSavepoint(savepoint)\n            }\n            .create()\n            .show()\n    }\n\n    private fun startForm(uri: Uri) {\n        formFillingAlreadyStarted = true\n        openForm.launch(\n            Intent(this, FormFillingActivity::class.java).apply {\n                action = intent.action\n                data = uri\n                intent.extras?.let { sourceExtras -> putExtras(sourceExtras) }\n            }\n        )\n    }\n\n    private fun displayErrorDialog(message: String) {\n        MaterialAlertDialogBuilder(this)\n            .setTitle(string.form_cannot_be_used)\n            .setMessage(message)\n            .setPositiveButton(string.ok) { _, _ -> finish() }\n            .setOnCancelListener { finish() }\n            .create()\n            .show()\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        outState.putBoolean(FORM_FILLING_ALREADY_STARTED, formFillingAlreadyStarted)\n        super.onSaveInstanceState(outState)\n    }\n\n    companion object {\n        private const val FORM_FILLING_ALREADY_STARTED = \"FORM_FILLING_ALREADY_STARTED\"\n    }\n}\n\nprivate class FormUriViewModel(\n    private val intent: Intent,\n    private val scheduler: Scheduler,\n    private val projectsRepository: ProjectsRepository,\n    private val projectsDataService: ProjectsDataService,\n    private val contentResolver: ContentResolver,\n    private val formsRepositoryProvider: FormsRepositoryProvider,\n    private val instancesRepositoryProvider: InstancesRepositoryProvider,\n    private val settingsProvider: SettingsProvider,\n    private val savepointsRepositoryProvider: SavepointsRepositoryProvider,\n    private val changeLockProvider: ChangeLockProvider,\n    private val resources: Resources\n) : ViewModel() {\n    private val uri: Uri? = intent.data\n\n    private val _formInspectionResult = MutableLiveData<FormInspectionResult>()\n    val formInspectionResult: LiveData<FormInspectionResult> = _formInspectionResult\n\n    init {\n        scheduler.immediate(\n            background = {\n                val error = assertProjectListNotEmpty()\n                    ?: assertCurrentProjectUsed()\n                    ?: assertValidUri()\n                    ?: assertFormExists()\n                    ?: assertFormNotEncrypted()\n                    ?: assertNonEditableFormsAreStartedWithCorrectMode()\n                    ?: assertDoesNotUseEntitiesOrFormsUpdateNotInProgress()\n                if (error != null) {\n                    FormInspectionResult.Error(error)\n                } else {\n                    getSavePoint()?.let {\n                        FormInspectionResult.Savepoint(it)\n                    } ?: FormInspectionResult.Valid\n                }\n            },\n            foreground = {\n                _formInspectionResult.value = it\n            }\n        )\n    }\n\n    private fun assertProjectListNotEmpty(): String? {\n        val projects = projectsRepository.getAll()\n        return if (projects.isEmpty()) {\n            resources.getString(string.app_not_configured)\n        } else {\n            null\n        }\n    }\n\n    private fun assertCurrentProjectUsed(): String? {\n        val projects = projectsRepository.getAll()\n        val firstProject = projects.first()\n        val uriProjectId = uri?.getQueryParameter(\"projectId\")\n        val projectId = uriProjectId ?: firstProject.uuid\n\n        return if (projectId != projectsDataService.requireCurrentProject().uuid) {\n            resources.getString(string.wrong_project_selected_for_form)\n        } else {\n            null\n        }\n    }\n\n    private fun assertValidUri(): String? {\n        val isUriValid = uri?.let {\n            val uriMimeType = contentResolver.getType(it)\n            if (uriMimeType == null) {\n                false\n            } else {\n                uriMimeType == FormsContract.CONTENT_ITEM_TYPE || uriMimeType == InstancesContract.CONTENT_ITEM_TYPE\n            }\n        } ?: false\n\n        return if (!isUriValid) {\n            resources.getString(string.unrecognized_uri)\n        } else {\n            null\n        }\n    }\n\n    private fun assertFormExists(): String? {\n        val uriMimeType = contentResolver.getType(uri!!)\n\n        return if (uriMimeType == FormsContract.CONTENT_ITEM_TYPE) {\n            val formExists =\n                formsRepositoryProvider.create().get(ContentUriHelper.getIdFromUri(uri))?.let {\n                    File(it.formFilePath).exists()\n                } ?: false\n\n            if (formExists) {\n                null\n            } else {\n                resources.getString(string.bad_uri)\n            }\n        } else {\n            val instance = instancesRepositoryProvider.create().get(ContentUriHelper.getIdFromUri(uri))\n            if (instance == null) {\n                resources.getString(string.bad_uri)\n            } else if (!File(instance.instanceFilePath).exists()) {\n                Analytics.log(AnalyticsEvents.OPEN_DELETED_INSTANCE)\n                InstanceDeleter(\n                    instancesRepositoryProvider.create(),\n                    formsRepositoryProvider.create()\n                ).delete(instance.dbId)\n                resources.getString(string.instance_deleted_message)\n            } else {\n                val candidateForms = formsRepositoryProvider.create()\n                    .getAllByFormIdAndVersion(instance.formId, instance.formVersion)\n\n                if (candidateForms.isEmpty()) {\n                    val version = if (instance.formVersion == null) {\n                        \"\"\n                    } else {\n                        \"\\n${resources.getString(string.version)} ${instance.formVersion}\"\n                    }\n\n                    resources.getString(string.parent_form_not_present, \"${instance.formId}$version\")\n                } else if (candidateForms.filter { !it.isDeleted }.size > 1) {\n                    resources.getString(string.survey_multiple_forms_error)\n                } else {\n                    null\n                }\n            }\n        }\n    }\n\n    private fun assertFormNotEncrypted(): String? {\n        val uriMimeType = contentResolver.getType(uri!!)\n\n        return if (uriMimeType == InstancesContract.CONTENT_ITEM_TYPE) {\n            val instance = instancesRepositoryProvider.create().get(ContentUriHelper.getIdFromUri(uri))\n            if (instance!!.canEditWhenComplete()) {\n                null\n            } else {\n                resources.getString(string.encrypted_form)\n            }\n        } else {\n            null\n        }\n    }\n\n    private fun assertNonEditableFormsAreStartedWithCorrectMode(): String? {\n        if (!canFormBeEdited()) {\n            intent.putExtra(\n                FormOpeningMode.FORM_MODE_KEY,\n                FormOpeningMode.VIEW_SENT\n            )\n        }\n        return null\n    }\n\n    private fun assertDoesNotUseEntitiesOrFormsUpdateNotInProgress(): String? {\n        val uriMimeType = contentResolver.getType(uri!!)\n        val projectId = projectsDataService.requireCurrentProject().uuid\n\n        if (intent.extras?.getString(FormOpeningMode.FORM_MODE_KEY) == FormOpeningMode.VIEW_SENT) {\n            return null\n        }\n\n        val form = if (uriMimeType == FormsContract.CONTENT_ITEM_TYPE) {\n            formsRepositoryProvider.create().get(ContentUriHelper.getIdFromUri(uri))!!\n        } else {\n            val instance = instancesRepositoryProvider.create().get(ContentUriHelper.getIdFromUri(uri))!!\n            formsRepositoryProvider.create().getAllByFormIdAndVersion(instance.formId, instance.formVersion).first()\n        }\n\n        if (form.usesEntities()) {\n            val formsLock = changeLockProvider.create(projectId).formsLock\n            val isLocAcquired = formsLock.tryLock(FORM_ENTRY_TOKEN)\n\n            return if (isLocAcquired) {\n                null\n            } else {\n                resources.getString(string.cannot_open_form_because_of_forms_update)\n            }\n        } else {\n            return null\n        }\n    }\n\n    private fun getSavePoint(): Savepoint? {\n        val uriMimeType = contentResolver.getType(uri!!)!!\n\n        return SavepointUseCases.getSavepoint(\n            uri,\n            uriMimeType,\n            formsRepositoryProvider.create(),\n            instancesRepositoryProvider.create(),\n            savepointsRepositoryProvider.create()\n        )\n    }\n\n    fun deleteSavepoint(savepoint: Savepoint) {\n        scheduler.immediate(\n            background = {\n                if (savepoint.instanceDbId == null) {\n                    File(savepoint.instanceFilePath).parentFile?.deleteRecursively()\n                }\n                savepointsRepositoryProvider.create().delete(savepoint.formDbId, savepoint.instanceDbId)\n            },\n            foreground = {\n                _formInspectionResult.value = FormInspectionResult.Valid\n            }\n        )\n    }\n\n    private fun canFormBeEdited(): Boolean {\n        val uriMimeType = contentResolver.getType(uri!!)\n\n        val formEditingEnabled = if (uriMimeType == InstancesContract.CONTENT_ITEM_TYPE) {\n            val instance = instancesRepositoryProvider.create().get(ContentUriHelper.getIdFromUri(uri))\n            instance!!.canBeEdited(settingsProvider)\n        } else {\n            true\n        }\n\n        return formEditingEnabled\n    }\n}\n\nprivate sealed class FormInspectionResult {\n    data class Error(val error: String) : FormInspectionResult()\n    data class Savepoint(val savepoint: org.odk.collect.forms.savepoints.Savepoint) : FormInspectionResult()\n    data object Valid : FormInspectionResult()\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/external/FormsContract.java",
    "content": "/*\n * Copyright (C) 2007 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.external;\n\nimport android.net.Uri;\n\nimport org.odk.collect.android.database.forms.DatabaseFormColumns;\n\n/**\n * Contract between the forms provider and applications. Contains definitions for the supported\n * URIs. Data columns are defined at {@link DatabaseFormColumns}.\n * <p>\n * This defines the data model for blank forms. Blank forms are unique by\n * {@link DatabaseFormColumns#JR_FORM_ID} unless multiple {@link DatabaseFormColumns#JR_VERSION}s are defined.\n */\npublic final class FormsContract {\n\n    static final String AUTHORITY = \"org.odk.collect.android.provider.odk.forms\";\n    public static final String CONTENT_TYPE = \"vnd.android.cursor.dir/vnd.odk.form\";\n    public static final String CONTENT_ITEM_TYPE = \"vnd.android.cursor.item/vnd.odk.form\";\n\n    /**\n     * The content:// style URL for accessing Forms.\n     */\n    public static Uri getUri(String projectId, Long formDbId) {\n        return Uri.parse(\"content://\" + AUTHORITY + \"/forms/\" + formDbId + \"?projectId=\" + projectId);\n    }\n\n    public static Uri getUri(String projectId) {\n        return Uri.parse(\"content://\" + AUTHORITY + \"/forms?projectId=\" + projectId);\n    }\n\n    /**\n     * The content:// style URL for accessing the newest versions of Forms. For each\n     * {@link DatabaseFormColumns#JR_FORM_ID}, only the version with the most recent\n     * {@link DatabaseFormColumns#DATE} is included.\n     *\n     * @deprecated This should be implemented as part of the UI/app code and should not be\n     * available as a special content URI case.\n     */\n    @Deprecated\n    public static Uri getContentNewestFormsByFormIdUri(String projectId) {\n        return Uri.parse(\"content://\" + AUTHORITY + \"/newest_forms_by_form_id?projectId=\" + projectId);\n    }\n\n    private FormsContract() {\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/external/FormsProvider.java",
    "content": "/*\n * Copyright (C) 2007 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.external;\n\nimport static android.provider.BaseColumns._ID;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_DELETE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_SEND;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.BASE64_RSA_PUBLIC_KEY;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DELETED_DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DESCRIPTION;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DISPLAY_NAME;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_FILE_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_MEDIA_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.GEOMETRY_XPATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JRCACHE_FILE_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_FORM_ID;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_VERSION;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.LANGUAGE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.MD5_HASH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.SUBMISSION_URI;\n\nimport android.content.ContentProvider;\nimport android.content.ContentValues;\nimport android.content.UriMatcher;\nimport android.database.Cursor;\nimport android.net.Uri;\n\nimport androidx.annotation.NonNull;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.analytics.AnalyticsEvents;\nimport org.odk.collect.android.analytics.AnalyticsUtils;\nimport org.odk.collect.android.dao.CursorLoaderFactory;\nimport org.odk.collect.android.database.forms.DatabaseFormsRepository;\nimport org.odk.collect.android.formmanagement.LocalFormUseCases;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.itemsets.FastExternalItemsetsRepository;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.utilities.ContentUriHelper;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.projects.ProjectsRepository;\nimport org.odk.collect.settings.SettingsProvider;\n\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\nimport javax.inject.Inject;\n\npublic class FormsProvider extends ContentProvider {\n\n    private static final int FORMS = 1;\n    private static final int FORM_ID = 2;\n    // Forms unique by ID, keeping only the latest one downloaded\n    private static final int NEWEST_FORMS_BY_FORM_ID = 3;\n\n    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);\n\n    @Inject\n    FormsRepositoryProvider formsRepositoryProvider;\n\n    @Inject\n    InstancesRepositoryProvider instancesRepositoryProvider;\n\n    @Inject\n    FastExternalItemsetsRepository fastExternalItemsetsRepository;\n\n    @Inject\n    StoragePathProvider storagePathProvider;\n\n    @Inject\n    ProjectsRepository projectsRepository;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    // Do not call it in onCreate() https://stackoverflow.com/questions/23521083/inject-database-in-a-contentprovider-with-dagger\n    private void deferDaggerInit() {\n        DaggerUtils.getComponent(getContext()).inject(this);\n    }\n\n    @Override\n    public boolean onCreate() {\n        return true;\n    }\n\n    @Override\n    public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {\n        deferDaggerInit();\n\n        String projectId = getProjectId(uri);\n\n        // We only want to log external calls to the content provider\n        if (uri.getQueryParameter(CursorLoaderFactory.INTERNAL_QUERY_PARAM) == null) {\n            logServerEvent(projectId, AnalyticsEvents.FORMS_PROVIDER_QUERY);\n        }\n\n        Cursor cursor;\n        switch (URI_MATCHER.match(uri)) {\n            case FORMS:\n                cursor = databaseQuery(projectId, projection, selection, selectionArgs, sortOrder, null, null);\n                cursor.setNotificationUri(getContext().getContentResolver(), FormsContract.getUri(projectId));\n                break;\n\n            case NEWEST_FORMS_BY_FORM_ID:\n                Set<String> maxDateColumns = new HashSet<>();\n                maxDateColumns.add(_ID);\n                maxDateColumns.add(DISPLAY_NAME);\n                maxDateColumns.add(DESCRIPTION);\n                maxDateColumns.add(JR_FORM_ID);\n                maxDateColumns.add(JR_VERSION);\n                maxDateColumns.add(SUBMISSION_URI);\n                maxDateColumns.add(BASE64_RSA_PUBLIC_KEY);\n                maxDateColumns.add(MD5_HASH);\n                maxDateColumns.add(FORM_MEDIA_PATH);\n                maxDateColumns.add(FORM_FILE_PATH);\n                maxDateColumns.add(JRCACHE_FILE_PATH);\n                maxDateColumns.add(LANGUAGE);\n                maxDateColumns.add(AUTO_DELETE);\n                maxDateColumns.add(AUTO_SEND);\n                maxDateColumns.add(GEOMETRY_XPATH);\n                maxDateColumns.add(DELETED_DATE);\n                maxDateColumns.add(\"MAX(date)\");\n\n                Map<String, String> maxDateProjectionMap = new HashMap<>();\n                for (String column : maxDateColumns) {\n                    if (column.equals(\"MAX(date)\")) {\n                        maxDateProjectionMap.put(\"MAX(date)\", \"MAX(date) AS \" + DATE);\n                    } else {\n                        maxDateProjectionMap.put(column, column);\n                    }\n                }\n\n                cursor = databaseQuery(projectId, maxDateColumns.toArray(new String[0]), selection, selectionArgs, sortOrder, JR_FORM_ID, maxDateProjectionMap);\n                cursor.setNotificationUri(getContext().getContentResolver(), FormsContract.getUri(projectId));\n                break;\n\n            case FORM_ID:\n                String formId = String.valueOf(ContentUriHelper.getIdFromUri(uri));\n                cursor = databaseQuery(projectId, null, _ID + \"=?\", new String[]{formId}, null, null, null);\n                cursor.setNotificationUri(getContext().getContentResolver(), uri);\n                break;\n\n            // Only include the latest form that was downloaded with each form_id\n\n            default:\n                throw new IllegalArgumentException(\"Unknown URI \" + uri);\n        }\n\n        return cursor;\n    }\n\n    @Override\n    public String getType(@NonNull Uri uri) {\n        switch (URI_MATCHER.match(uri)) {\n            case FORMS:\n            case NEWEST_FORMS_BY_FORM_ID:\n                return FormsContract.CONTENT_TYPE;\n\n            case FORM_ID:\n                return FormsContract.CONTENT_ITEM_TYPE;\n\n            default:\n                throw new IllegalArgumentException(\"Unknown URI \" + uri);\n        }\n    }\n\n    @Override\n    public synchronized Uri insert(@NonNull Uri uri, ContentValues initialValues) {\n        return null;\n    }\n\n    /**\n     * This method removes the entry from the content provider, and also removes\n     * any associated files. files: form.xml, [formmd5].formdef, formname-media\n     * {directory}\n     */\n    @Override\n    public int delete(@NonNull Uri uri, String where, String[] whereArgs) {\n        deferDaggerInit();\n\n        int count;\n\n        String projectId = getProjectId(uri);\n        logServerEvent(projectId, AnalyticsEvents.FORMS_PROVIDER_DELETE);\n\n        FormsRepository formsRepository = getFormsRepository(projectId);\n        InstancesRepository instancesRepository = instancesRepositoryProvider.create(projectId);\n\n        switch (URI_MATCHER.match(uri)) {\n            case FORMS:\n                try (Cursor cursor = databaseQuery(projectId, null, where, whereArgs, null, null, null)) {\n                    while (cursor.moveToNext()) {\n                        LocalFormUseCases.deleteForm(formsRepository, instancesRepository, cursor.getLong(cursor.getColumnIndex(_ID)));\n                    }\n\n                    count = cursor.getCount();\n                }\n                break;\n\n            case FORM_ID:\n                LocalFormUseCases.deleteForm(formsRepository, instancesRepository, ContentUriHelper.getIdFromUri(uri));\n                count = 1;\n                break;\n\n            default:\n                throw new IllegalArgumentException(\"Unknown URI \" + uri);\n        }\n\n        getContext().getContentResolver().notifyChange(uri, null);\n        return count;\n    }\n\n    @Override\n    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {\n        return 0;\n    }\n\n    @NotNull\n    private FormsRepository getFormsRepository(String projectId) {\n        return formsRepositoryProvider.create(projectId);\n    }\n\n    private String getProjectId(@NonNull Uri uri) {\n        String queryParam = uri.getQueryParameter(\"projectId\");\n\n        if (queryParam != null) {\n            return queryParam;\n        } else {\n            return projectsRepository.getAll().get(0).getUuid();\n        }\n    }\n\n    private Cursor databaseQuery(String projectId, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, Map<String, String> projectionMap) {\n        return ((DatabaseFormsRepository) getFormsRepository(projectId)).rawQuery(projectionMap, projection, selection, selectionArgs, sortOrder, groupBy);\n    }\n\n    private void logServerEvent(String projectId, String event) {\n        AnalyticsUtils.logServerEvent(event, settingsProvider.getUnprotectedSettings(projectId));\n    }\n\n    static {\n        URI_MATCHER.addURI(FormsContract.AUTHORITY, \"forms\", FORMS);\n        URI_MATCHER.addURI(FormsContract.AUTHORITY, \"forms/#\", FORM_ID);\n        // Only available for query and type\n        URI_MATCHER.addURI(FormsContract.AUTHORITY, \"newest_forms_by_form_id\", NEWEST_FORMS_BY_FORM_ID);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/external/InstanceProvider.java",
    "content": "/*\n * Copyright (C) 2007 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.external;\n\nimport static org.odk.collect.android.database.DatabaseObjectMapper.getInstanceFromValues;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns._ID;\nimport static org.odk.collect.android.external.InstancesContract.CONTENT_ITEM_TYPE;\nimport static org.odk.collect.android.external.InstancesContract.CONTENT_TYPE;\nimport static org.odk.collect.android.external.InstancesContract.getUri;\n\nimport android.content.ContentProvider;\nimport android.content.ContentValues;\nimport android.content.UriMatcher;\nimport android.database.Cursor;\nimport android.net.Uri;\n\nimport androidx.annotation.NonNull;\n\nimport org.odk.collect.android.analytics.AnalyticsEvents;\nimport org.odk.collect.android.analytics.AnalyticsUtils;\nimport org.odk.collect.android.dao.CursorLoaderFactory;\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns;\nimport org.odk.collect.android.database.instances.DatabaseInstancesRepository;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.instancemanagement.InstanceDeleter;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.utilities.ContentUriHelper;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.projects.ProjectsRepository;\nimport org.odk.collect.settings.SettingsProvider;\n\nimport javax.inject.Inject;\n\npublic class InstanceProvider extends ContentProvider {\n\n    private static final int INSTANCES = 1;\n    private static final int INSTANCE_ID = 2;\n    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);\n\n    @Inject\n    InstancesRepositoryProvider instancesRepositoryProvider;\n\n    @Inject\n    FormsRepositoryProvider formsRepositoryProvider;\n\n    @Inject\n    StoragePathProvider storagePathProvider;\n\n    @Inject\n    ProjectsRepository projectsRepository;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    @Override\n    public boolean onCreate() {\n        return true;\n    }\n\n    @Override\n    public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs,\n                        String sortOrder) {\n        DaggerUtils.getComponent(getContext()).inject(this);\n\n        String projectId = getProjectId(uri);\n\n        // We only want to log external calls to the content provider\n        if (uri.getQueryParameter(CursorLoaderFactory.INTERNAL_QUERY_PARAM) == null) {\n            logServerEvent(projectId, AnalyticsEvents.INSTANCE_PROVIDER_QUERY);\n        }\n\n        Cursor c;\n        switch (URI_MATCHER.match(uri)) {\n            case INSTANCES:\n                c = dbQuery(projectId, projection, selection, selectionArgs, sortOrder);\n                break;\n\n            case INSTANCE_ID:\n                String id = String.valueOf(ContentUriHelper.getIdFromUri(uri));\n                c = dbQuery(projectId, projection, _ID + \"=?\", new String[]{id}, null);\n                break;\n\n            default:\n                throw new IllegalArgumentException(\"Unknown URI \" + uri);\n        }\n\n        // Tell the cursor what uri to watch, so it knows when its source data changes\n        c.setNotificationUri(getContext().getContentResolver(), uri);\n        return c;\n    }\n\n    private Cursor dbQuery(String projectId, String[] projection, String selection, String[] selectionArgs, String sortOrder) {\n        return ((DatabaseInstancesRepository) instancesRepositoryProvider.create(projectId)).rawQuery(projection, selection, selectionArgs, sortOrder, null);\n    }\n\n    @Override\n    public String getType(@NonNull Uri uri) {\n        switch (URI_MATCHER.match(uri)) {\n            case INSTANCES:\n                return CONTENT_TYPE;\n\n            case INSTANCE_ID:\n                return CONTENT_ITEM_TYPE;\n\n            default:\n                throw new IllegalArgumentException(\"Unknown URI \" + uri);\n        }\n    }\n\n    @Override\n    public Uri insert(@NonNull Uri uri, ContentValues initialValues) {\n        DaggerUtils.getComponent(getContext()).inject(this);\n\n        String projectId = getProjectId(uri);\n        logServerEvent(projectId, AnalyticsEvents.INSTANCE_PROVIDER_INSERT);\n\n        // Validate the requested uri\n        if (URI_MATCHER.match(uri) != INSTANCES) {\n            throw new IllegalArgumentException(\"Unknown URI \" + uri);\n        }\n\n        if (initialValues.containsKey(DatabaseInstanceColumns.SUBMISSION_URI)) {\n            throw new SecurityException();\n        }\n\n        Instance newInstance = instancesRepositoryProvider.create(projectId).save(getInstanceFromValues(initialValues));\n        return getUri(projectId, newInstance.getDbId());\n    }\n\n    /**\n     * This method removes the entry from the content provider, and also removes any associated\n     * files.\n     * files:  form.xml, [formmd5].formdef, formname-media {directory}\n     */\n    @Override\n    public int delete(@NonNull Uri uri, String where, String[] whereArgs) {\n        DaggerUtils.getComponent(getContext()).inject(this);\n\n        String projectId = getProjectId(uri);\n        logServerEvent(projectId, AnalyticsEvents.INSTANCE_PROVIDER_DELETE);\n\n        int count;\n\n        switch (URI_MATCHER.match(uri)) {\n            case INSTANCES:\n                try (Cursor cursor = dbQuery(projectId, new String[]{_ID}, where, whereArgs, null)) {\n                    while (cursor.moveToNext()) {\n                        long id = cursor.getLong(cursor.getColumnIndex(_ID));\n                        new InstanceDeleter(instancesRepositoryProvider.create(projectId), formsRepositoryProvider.create(projectId)).delete(id);\n                    }\n\n                    count = cursor.getCount();\n                }\n\n                break;\n\n            case INSTANCE_ID:\n                long id = ContentUriHelper.getIdFromUri(uri);\n\n                if (where == null) {\n                    new InstanceDeleter(instancesRepositoryProvider.create(projectId), formsRepositoryProvider.create(projectId)).delete(id);\n                } else {\n                    try (Cursor cursor = dbQuery(projectId, new String[]{_ID}, where, whereArgs, null)) {\n                        while (cursor.moveToNext()) {\n                            if (cursor.getLong(cursor.getColumnIndex(_ID)) == id) {\n                                new InstanceDeleter(instancesRepositoryProvider.create(), formsRepositoryProvider.create()).delete(id);\n                                break;\n                            }\n                        }\n                    }\n                }\n\n                count = 1;\n                break;\n\n            default:\n                throw new IllegalArgumentException(\"Unknown URI \" + uri);\n        }\n\n        getContext().getContentResolver().notifyChange(uri, null);\n\n        return count;\n    }\n\n    @Override\n    public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) {\n        return 0;\n    }\n\n    private String getProjectId(@NonNull Uri uri) {\n        String queryParam = uri.getQueryParameter(\"projectId\");\n\n        if (queryParam != null) {\n            return queryParam;\n        } else {\n            return projectsRepository.getAll().get(0).getUuid();\n        }\n    }\n\n    private void logServerEvent(String projectId, String event) {\n        AnalyticsUtils.logServerEvent(event, settingsProvider.getUnprotectedSettings(projectId));\n    }\n\n    static {\n        URI_MATCHER.addURI(InstancesContract.AUTHORITY, \"instances\", INSTANCES);\n        URI_MATCHER.addURI(InstancesContract.AUTHORITY, \"instances/#\", INSTANCE_ID);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/external/InstancesContract.java",
    "content": "/*\n * Copyright (C) 2007 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.external;\n\nimport android.net.Uri;\n\npublic final class InstancesContract {\n\n    public static final String AUTHORITY = \"org.odk.collect.android.provider.odk.instances\";\n    public static final String CONTENT_TYPE = \"vnd.android.cursor.dir/vnd.odk.instance\";\n    public static final String CONTENT_ITEM_TYPE = \"vnd.android.cursor.item/vnd.odk.instance\";\n\n    public static Uri getUri(String projectId) {\n        return Uri.parse(\"content://\" + AUTHORITY + \"/instances?projectId=\" + projectId);\n    }\n\n    public static Uri getUri(String projectId, Long instanceDbId) {\n        return Uri.parse(\"content://\" + AUTHORITY + \"/instances/\" + instanceDbId + \"?projectId=\" + projectId);\n    }\n\n    // This class cannot be instantiated\n    private InstancesContract() {\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fastexternalitemset/ItemsetDao.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.fastexternalitemset;\n\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteException;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.condition.EvaluationContext;\nimport org.javarosa.core.model.instance.TreeElement;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.javarosa.xpath.XPathNodeset;\nimport org.javarosa.xpath.expr.XPathExpression;\nimport org.javarosa.xpath.parser.XPathSyntaxException;\nimport org.odk.collect.android.javarosawrapper.FormController;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport timber.log.Timber;\n\npublic class ItemsetDao {\n    private static final String QUOTATION_MARK = \"\\\"\";\n\n    private final ItemsetDbAdapter adapter;\n\n    public ItemsetDao(ItemsetDbAdapter adapter) {\n        this.adapter = adapter;\n    }\n\n    public String getItemLabel(String itemName, String mediaFolderPath, String language) {\n        String itemLabel = null;\n\n        File itemsetFile = getItemsetFile(mediaFolderPath);\n        if (itemsetFile.exists()) {\n            adapter.open();\n\n            // name of the itemset table for this form\n            String pathHash = ItemsetDbAdapter.getMd5FromString(itemsetFile.getAbsolutePath());\n            try {\n                String selection = \"name=?\";\n                String[] selectionArgs = {itemName};\n\n                Cursor c = adapter.query(pathHash, selection, selectionArgs);\n                if (c != null) {\n                    c.move(-1);\n                    while (c.moveToNext()) {\n                        // apparently you only need the double quotes in the\n                        // column name when creating the column with a : included\n                        String labelLang = \"label\" + \"::\" + language;\n                        int langCol = c.getColumnIndex(labelLang);\n                        if (langCol == -1) {\n                            itemLabel = c.getString(c.getColumnIndex(\"label\"));\n                        } else {\n                            itemLabel = c.getString(c.getColumnIndex(labelLang));\n                        }\n\n                    }\n                    c.close();\n                }\n            } catch (SQLiteException e) {\n                Timber.i(e);\n            } finally {\n                adapter.close();\n            }\n        }\n\n        return itemLabel;\n    }\n\n    public List<SelectChoice> getItems(FormEntryPrompt formEntryPrompt, XPathParseTool pathParseTool, FormController formController) throws FileNotFoundException, XPathSyntaxException {\n        String nodesetString = getNodesetString(formEntryPrompt);\n\n        List<String> arguments = new ArrayList<>();\n        String selectionString = getSelectionStringAndPopulateArguments(getQueryString(nodesetString), arguments);\n\n        String[] selectionArgs = getSelectionArgs(arguments, nodesetString, formController, pathParseTool, formEntryPrompt);\n        return selectionArgs == null ? null : getItemsFromDatabase(selectionString, selectionArgs, formController, adapter);\n    }\n\n    private String getNodesetString(FormEntryPrompt formEntryPrompt) {\n        // the format of the query should be something like this:\n        // query=\"instance('cities')/root/item[state=/data/state and county=/data/county]\"\n        // \"query\" is what we're using to notify that this is an itemset widget.\n        return formEntryPrompt.getQuestion().getAdditionalAttribute(null, \"query\");\n    }\n\n    private String getQueryString(String nodesetStr) {\n        // isolate the string between between the [ ] characters\n        return nodesetStr.substring(nodesetStr.indexOf('[') + 1, nodesetStr.lastIndexOf(']'));\n    }\n\n    private String getSelectionStringAndPopulateArguments(String queryString, List<String> arguments) {\n        StringBuilder selectionString = new StringBuilder();\n        // add the list name as the first argument, which will always be there\n        selectionString.append(\"list_name=?\");\n\n        // check to see if there are any arguments\n        if (queryString.indexOf('=') != -1) {\n            selectionString.append(\" and \");\n        }\n\n        // can't just split on 'and' or 'or' because they have different\n        // behavior, so loop through and break them off until we don't have any more\n        // must include the spaces in indexOf so we don't match words like \"land\"\n        int andIndex;\n        int orIndex = -1;\n\n        while ((andIndex = queryString.indexOf(\" and \")) != -1 || (orIndex = queryString.indexOf(\" or \")) != -1) {\n            if (andIndex != -1) {\n                String[] pair = queryString\n                        .substring(0, andIndex)\n                        .split(\"=\");\n\n                if (pair.length == 2) {\n                    selectionString\n                            .append(QUOTATION_MARK)\n                            .append(pair[0].trim())\n                            .append(QUOTATION_MARK)\n                            .append(\"=? and \");\n\n                    arguments\n                            .add(pair[1]\n                                    .trim());\n                }\n                // move string forward to after \" and \"\n                queryString = queryString.substring(andIndex + 5, queryString.length());\n            } else {\n                String subString = queryString.substring(0, orIndex);\n                String[] pair = subString.split(\"=\");\n\n                if (pair.length == 2) {\n                    selectionString\n                            .append(QUOTATION_MARK)\n                            .append(pair[0].trim())\n                            .append(QUOTATION_MARK)\n                            .append(\"=? or \");\n                    arguments.add(pair[1].trim());\n                }\n                // move string forward to after \" or \"\n                queryString = queryString.substring(orIndex + 4, queryString.length());\n            }\n        }\n\n        // parse the last segment (or only segment if there are no 'and' or 'or' clauses\n        String[] pair = queryString.split(\"=\");\n        if (pair.length == 2) {\n            selectionString\n                    .append(QUOTATION_MARK)\n                    .append(pair[0].trim())\n                    .append(QUOTATION_MARK)\n                    .append(\"=?\");\n            arguments.add(pair[1].trim());\n        }\n        return selectionString.toString();\n    }\n\n    @SuppressWarnings(\"PMD.AvoidThrowingNewInstanceOfSameException\")\n    private String[] getSelectionArgs(List<String> arguments, String nodesetStr, FormController formController, XPathParseTool pathParseTool, FormEntryPrompt formEntryPrompt) throws XPathSyntaxException {\n        // +1 is for the list_name\n        String[] selectionArgs = new String[arguments.size() + 1];\n\n        // parse out the list name, between the ''\n        String listName = nodesetStr.substring(nodesetStr.indexOf('\\'') + 1, nodesetStr.lastIndexOf('\\''));\n\n        selectionArgs[0] = listName; // first argument is always listname\n\n        if (formController == null) {\n            Timber.w(\"Can't instantiate ItemsetWidget with a null FormController.\");\n            return null;\n        }\n\n        // loop through the arguments, evaluate any expressions and build the query string for the DB\n        for (int i = 0; i < arguments.size(); i++) {\n            XPathExpression xpr;\n            try {\n                xpr = pathParseTool.parseXPath(arguments.get(i));\n            } catch (XPathSyntaxException e) {\n                throw new XPathSyntaxException(arguments.get(i));\n            }\n\n            if (xpr != null) {\n                FormDef form = formController.getFormDef();\n                TreeElement treeElement = form.getMainInstance().resolveReference(\n                        formEntryPrompt.getIndex().getReference());\n                EvaluationContext ec = new EvaluationContext(form.getEvaluationContext(),\n                        treeElement.getRef());\n                Object value = xpr.eval(form.getMainInstance(), ec);\n\n                if (value == null) {\n                    return null;\n                } else {\n                    if (value instanceof XPathNodeset) {\n                        value = ((XPathNodeset) value).getValAt(0);\n                    }\n                    selectionArgs[i + 1] = value.toString();\n                }\n            }\n        }\n        return selectionArgs;\n    }\n\n    private List<SelectChoice> getItemsFromDatabase(String selection, String[] selectionArgs, FormController formController, ItemsetDbAdapter adapter) throws FileNotFoundException {\n        List<SelectChoice> items = new ArrayList<>();\n\n        File itemsetFile = getItemsetFile(formController.getMediaFolder().getAbsolutePath());\n\n        if (itemsetFile.exists()) {\n            adapter.open();\n\n            // name of the itemset table for this form\n            String pathHash = ItemsetDbAdapter.getMd5FromString(itemsetFile.getAbsolutePath());\n            try {\n                Cursor c = adapter.query(pathHash, selection, selectionArgs);\n                if (c != null) {\n                    c.move(-1);\n                    int index = 0;\n                    while (c.moveToNext()) {\n                        String label;\n                        String val;\n\n                        // try to get the value associated with the label:lang\n                        // string if that doen't exist, then just use label\n                        String lang = \"\";\n                        if (formController.getLanguages() != null && formController.getLanguages().length > 0) {\n                            lang = formController.getLanguage();\n                        }\n\n                        // apparently you only need the double quotes in the\n                        // column name when creating the column with a : included\n                        String labelLang = \"label\" + \"::\" + lang;\n                        int langCol = c.getColumnIndex(labelLang);\n                        if (langCol == -1) {\n                            label = c.getString(c.getColumnIndex(\"label\"));\n                        } else {\n                            label = c.getString(c.getColumnIndex(labelLang));\n                        }\n\n                        val = c.getString(c.getColumnIndex(\"name\"));\n                        SelectChoice selectChoice = new SelectChoice(null, label, val, false);\n                        selectChoice.setIndex(index);\n                        items.add(selectChoice);\n                        index++;\n                    }\n                    c.close();\n                }\n            } catch (SQLiteException e) {\n                Timber.i(e);\n            } finally {\n                adapter.close();\n            }\n        } else {\n            throw new FileNotFoundException(itemsetFile.getAbsolutePath());\n        }\n        return items;\n    }\n\n    public File getItemsetFile(String mediaFolderPath) {\n        return new File(mediaFolderPath + \"/itemsets.csv\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fastexternalitemset/ItemsetDbAdapter.java",
    "content": "package org.odk.collect.android.fastexternalitemset;\n\nimport static org.odk.collect.androidshared.utils.PathUtils.getAbsoluteFilePath;\n\nimport android.content.ContentValues;\nimport android.database.Cursor;\nimport android.database.SQLException;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteOpenHelper;\n\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.db.sqlite.AltDatabasePathContext;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.shared.PathUtils;\n\nimport java.io.Closeable;\nimport java.math.BigInteger;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\n\nimport timber.log.Timber;\n\npublic class ItemsetDbAdapter implements Closeable {\n\n    private DatabaseHelper dbHelper;\n    private SQLiteDatabase db;\n\n    public static final String DATABASE_NAME = \"itemsets.db\";\n    private static final String DATABASE_TABLE = \"itemset_\";\n    private static final int DATABASE_VERSION = 3;\n\n    private static final String ITEMSET_TABLE = \"itemsets\";\n    public static final String KEY_ITEMSET_HASH = \"hash\";\n    public static final String KEY_PATH = \"path\";\n\n    private static final String CREATE_ITEMSET_TABLE =\n            \"CREATE TABLE IF NOT EXISTS \" + ITEMSET_TABLE + \" (_id integer primary key autoincrement, \"\n                    + KEY_ITEMSET_HASH + \" text, \"\n                    + KEY_PATH + \" text \"\n                    + \");\";\n\n    /**\n     * This class helps open, create, and upgrade the database file.\n     */\n    private static class DatabaseHelper extends SQLiteOpenHelper {\n        DatabaseHelper() {\n            super(new AltDatabasePathContext(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.METADATA), Collect.getInstance()), DATABASE_NAME, null, DATABASE_VERSION);\n        }\n\n        @Override\n        public void onCreate(SQLiteDatabase db) {\n            // create table to keep track of the itemsets\n            db.execSQL(CREATE_ITEMSET_TABLE);\n\n        }\n\n        @Override\n        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {\n            Timber.w(\"Upgrading database from version %d to %d, which will destroy all old data\", oldVersion, newVersion);\n            // first drop all of our generated itemset tables\n            Cursor c = db.query(ITEMSET_TABLE, null, null, null, null, null, null);\n            if (c != null) {\n                c.move(-1);\n                while (c.moveToNext()) {\n                    String table = c.getString(c.getColumnIndex(KEY_ITEMSET_HASH));\n                    db.execSQL(\"DROP TABLE IF EXISTS \" + DATABASE_TABLE + table);\n                }\n                c.close();\n            }\n\n            // then drop the table tracking itemsets itself\n            db.execSQL(\"DROP TABLE IF EXISTS \" + ITEMSET_TABLE);\n            onCreate(db);\n        }\n    }\n\n    /**\n     * Open the database. If it cannot be opened, try to create a new instance\n     * of the database. If it cannot be created, throw an exception to signal\n     * the failure\n     *\n     * @return this (self reference, allowing this to be chained in an\n     * initialization call)\n     * @throws SQLException if the database could be neither opened or created\n     */\n    public ItemsetDbAdapter open() throws SQLException {\n        dbHelper = new DatabaseHelper();\n        db = dbHelper.getWritableDatabase();\n        return this;\n    }\n\n    @Override\n    public void close() {\n        dbHelper.close();\n    }\n\n    public boolean createTable(String formHash, String pathHash, String[] columns, String path) {\n        StringBuilder sb = new StringBuilder();\n\n        // get md5 of the path to itemset.csv, which is unique per form\n        // the md5 is easier to use because it doesn't have chars like '/'\n\n        sb.append(\"create table \")\n                .append(DATABASE_TABLE)\n                .append(pathHash)\n                .append(\" (_id integer primary key autoincrement \");\n\n        for (String column : columns) {\n            if (!column.isEmpty()) {\n                // add double quotes in case the column is of label:lang\n                sb\n                        .append(\" , \\\"\")\n                        .append(column)\n                        .append(\"\\\" text \");\n                // create database with first line\n            }\n        }\n        sb.append(\");\");\n\n        String tableCreate = sb.toString();\n        Timber.i(\"create string: %s\", tableCreate);\n        db.execSQL(tableCreate);\n\n        ContentValues cv = new ContentValues();\n        cv.put(KEY_ITEMSET_HASH, formHash);\n        cv.put(KEY_PATH, PathUtils.getRelativeFilePath(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS), path));\n        db.insert(ITEMSET_TABLE, null, cv);\n\n        return true;\n    }\n\n    public boolean addRow(String tableName, String[] columns, String[] newRow) {\n        ContentValues cv = new ContentValues();\n\n        // rows don't necessarily use all the columns\n        // but a column is guaranteed to exist for a row (or else blow up)\n        for (int i = 0; i < newRow.length; i++) {\n            if (!columns[i].isEmpty()) {\n                cv.put(\"\\\"\" + columns[i] + \"\\\"\", newRow[i]);\n            }\n        }\n        db.insert(DATABASE_TABLE + tableName, null, cv);\n        return true;\n    }\n\n    public void beginTransaction() {\n        db.execSQL(\"BEGIN\");\n    }\n\n    public void commit() {\n        db.execSQL(\"COMMIT\");\n    }\n\n    public Cursor query(String hash, String selection, String[] selectionArgs) throws SQLException {\n        return db.query(true, DATABASE_TABLE + hash, null, selection, selectionArgs,\n                null, null, null, null);\n    }\n\n    public void dropTable(String pathHash, String path) {\n        // drop the table\n        db.execSQL(\"DROP TABLE IF EXISTS \" + DATABASE_TABLE + pathHash);\n\n        // and remove the entry from the itemsets table\n        String where = KEY_PATH + \"=?\";\n        String[] whereArgs = {\n                PathUtils.getRelativeFilePath(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS), path)\n        };\n        db.delete(ITEMSET_TABLE, where, whereArgs);\n    }\n\n    public Cursor getItemsets(String path) {\n        String selection = KEY_PATH + \"=?\";\n        String[] selectionArgs = {\n                PathUtils.getRelativeFilePath(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS), path)\n        };\n        return db.query(ITEMSET_TABLE, null, selection, selectionArgs, null, null, null);\n    }\n\n    public Cursor getItemsets() {\n        return db.query(ITEMSET_TABLE, null, null, null, null, null, null);\n    }\n\n    public void update(ContentValues values, String where, String[] whereArgs) {\n        db.update(ITEMSET_TABLE, values, where, whereArgs);\n    }\n\n    public void delete(String path) {\n        StoragePathProvider storagePathProvider = new StoragePathProvider();\n        Cursor c = getItemsets(path);\n        if (c != null) {\n            if (c.getCount() == 1) {\n                c.moveToFirst();\n                String table = getMd5FromString(getAbsoluteFilePath(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS), c.getString(c.getColumnIndex(KEY_PATH))));\n                db.execSQL(\"DROP TABLE IF EXISTS \" + DATABASE_TABLE + table);\n            }\n            c.close();\n        }\n\n        String where = KEY_PATH + \"=?\";\n        String[] whereArgs = {\n                PathUtils.getRelativeFilePath(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS), path)\n        };\n        db.delete(ITEMSET_TABLE, where, whereArgs);\n    }\n\n    public static String getMd5FromString(String toEncode) {\n        MessageDigest md;\n        try {\n            md = MessageDigest.getInstance(\"MD5\");\n        } catch (NoSuchAlgorithmException e) {\n            Timber.e(e, \"Unable to get MD5 algorithm due to : %s \", e.getMessage());\n            return null;\n        }\n\n        md.update(toEncode.getBytes());\n        byte[] digest = md.digest();\n        BigInteger bigInt = new BigInteger(1, digest);\n        return bigInt.toString(16);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fastexternalitemset/XPathParseTool.java",
    "content": "package org.odk.collect.android.fastexternalitemset;\n\nimport org.javarosa.xpath.expr.XPathExpression;\nimport org.javarosa.xpath.parser.Lexer;\nimport org.javarosa.xpath.parser.Parser;\nimport org.javarosa.xpath.parser.XPathSyntaxException;\n\n/**\n * @author James Knight\n */\npublic class XPathParseTool {\n    public XPathExpression parseXPath(String xpath) throws XPathSyntaxException {\n        return Parser.parse(Lexer.lex(xpath));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/BackgroundAudioPermissionDialogFragment.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.widget.Toast;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.permissions.PermissionListener;\nimport org.odk.collect.permissions.PermissionsProvider;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\npublic class BackgroundAudioPermissionDialogFragment extends DialogFragment {\n\n    @Inject\n    PermissionsProvider permissionsProvider;\n\n    private final ViewModelProvider.Factory viewModelFactory;\n\n    BackgroundAudioViewModel viewModel;\n\n    public BackgroundAudioPermissionDialogFragment(ViewModelProvider.Factory viewModelFactory) {\n        this.viewModelFactory = viewModelFactory;\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        DaggerUtils.getComponent(context).inject(this);\n        viewModel = new ViewModelProvider(requireActivity(), viewModelFactory).get(BackgroundAudioViewModel.class);\n    }\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        setCancelable(false);\n\n        final FragmentActivity activity = requireActivity();\n        return new MaterialAlertDialogBuilder(requireContext())\n                .setMessage(org.odk.collect.strings.R.string.background_audio_permission_explanation)\n                .setPositiveButton(org.odk.collect.strings.R.string.ok, (dialog, which) -> {\n                    onOKClicked(activity);\n                })\n                .create();\n    }\n\n    private void onOKClicked(FragmentActivity activity) {\n        permissionsProvider.requestRecordAudioPermission(activity, new PermissionListener() {\n            @Override\n            public void granted() {\n                try {\n                    viewModel.grantAudioPermission();\n                } catch (IllegalStateException e) {\n                    Timber.e(e);\n\n                    Toast.makeText(\n                            activity,\n                            \"Could not start recording. Please reopen form.\",\n                            Toast.LENGTH_LONG\n                    ).show();\n                    activity.finish();\n                }\n            }\n\n            @Override\n            public void additionalExplanationClosed() {\n                activity.finish();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/BackgroundAudioViewModel.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_BACKGROUND_RECORDING;\n\nimport android.Manifest;\nimport android.os.Handler;\nimport android.os.Looper;\n\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.ViewModel;\n\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.odk.collect.android.formentry.audit.AuditEvent;\nimport org.odk.collect.android.formentry.audit.AuditEventLogger;\nimport org.odk.collect.androidshared.livedata.LiveDataUtils;\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData;\nimport org.odk.collect.androidshared.livedata.NonNullLiveData;\nimport org.odk.collect.async.Cancellable;\nimport org.odk.collect.audiorecorder.recorder.Output;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.recording.RecordingSession;\nimport org.odk.collect.permissions.PermissionsChecker;\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\npublic class BackgroundAudioViewModel extends ViewModel {\n\n    private final AudioRecorder audioRecorder;\n    private final Settings generalSettings;\n    private final RecordAudioActionRegistry recordAudioActionRegistry;\n    private final PermissionsChecker permissionsChecker;\n    private final Supplier<Long> clock;\n\n    private final MutableNonNullLiveData<Boolean> isPermissionRequired = new MutableNonNullLiveData<>(false);\n    private final MutableNonNullLiveData<Boolean> isBackgroundRecordingEnabled;\n\n    // These fields handle storing record action details while we're granting permissions\n    private final HashSet<TreeReference> tempTreeReferences = new HashSet<>();\n    private final Cancellable formSessionObserver;\n    private String tempQuality;\n\n    @Nullable\n    private AuditEventLogger auditEventLogger;\n\n    public BackgroundAudioViewModel(AudioRecorder audioRecorder, Settings generalSettings, RecordAudioActionRegistry recordAudioActionRegistry, PermissionsChecker permissionsChecker, Supplier<Long> clock, LiveData<FormSession> formSession) {\n        this.audioRecorder = audioRecorder;\n        this.generalSettings = generalSettings;\n        this.recordAudioActionRegistry = recordAudioActionRegistry;\n        this.permissionsChecker = permissionsChecker;\n        this.clock = clock;\n\n        this.recordAudioActionRegistry.register((treeReference, quality) -> {\n            new Handler(Looper.getMainLooper()).post(() -> handleRecordAction(treeReference, quality));\n        });\n\n        isBackgroundRecordingEnabled = new MutableNonNullLiveData<>(generalSettings.getBoolean(KEY_BACKGROUND_RECORDING));\n\n        formSessionObserver = LiveDataUtils.observe(formSession, it -> this.auditEventLogger = it.getFormController().getAuditEventLogger());\n    }\n\n    @Override\n    protected void onCleared() {\n        recordAudioActionRegistry.unregister();\n        formSessionObserver.cancel();\n    }\n\n    public LiveData<Boolean> isPermissionRequired() {\n        return isPermissionRequired;\n    }\n\n    public NonNullLiveData<Boolean> isBackgroundRecordingEnabled() {\n        return isBackgroundRecordingEnabled;\n    }\n\n    public void setBackgroundRecordingEnabled(boolean enabled) {\n        if (enabled) {\n            if (auditEventLogger != null) {\n                auditEventLogger.logEvent(AuditEvent.AuditEventType.BACKGROUND_AUDIO_ENABLED, true, clock.get());\n            }\n        } else {\n            audioRecorder.cleanUp();\n\n            if (auditEventLogger != null) {\n                auditEventLogger.logEvent(AuditEvent.AuditEventType.BACKGROUND_AUDIO_DISABLED, true, clock.get());\n            }\n        }\n\n        generalSettings.save(KEY_BACKGROUND_RECORDING, enabled);\n        isBackgroundRecordingEnabled.postValue(enabled);\n    }\n\n    public boolean isBackgroundRecording() {\n        return audioRecorder.isRecording() && audioRecorder.getCurrentSession().getValue().getId() instanceof Set;\n    }\n\n    public void grantAudioPermission() {\n        if (tempTreeReferences.isEmpty()) {\n            throw new IllegalStateException(\"No TreeReferences to start recording with!\");\n        }\n\n        isPermissionRequired.setValue(false);\n        startBackgroundRecording(tempQuality, new HashSet<>(tempTreeReferences));\n\n        tempTreeReferences.clear();\n        tempQuality = null;\n    }\n\n    private void handleRecordAction(TreeReference treeReference, String quality) {\n        if (isBackgroundRecordingEnabled.getValue()) {\n            if (permissionsChecker.isPermissionGranted(Manifest.permission.RECORD_AUDIO)) {\n                if (isBackgroundRecording()) {\n                    RecordingSession session = audioRecorder.getCurrentSession().getValue();\n                    HashSet<TreeReference> treeReferences = (HashSet<TreeReference>) session.getId();\n                    treeReferences.add(treeReference);\n                } else {\n                    HashSet<TreeReference> treeReferences = new HashSet<>();\n                    treeReferences.add(treeReference);\n\n                    startBackgroundRecording(quality, treeReferences);\n                }\n            } else {\n                isPermissionRequired.setValue(true);\n\n                tempTreeReferences.add(treeReference);\n                if (tempQuality == null) {\n                    tempQuality = quality;\n                }\n            }\n        }\n    }\n\n    private void startBackgroundRecording(String quality, HashSet<TreeReference> treeReferences) {\n        Output output = Output.AMR;\n        if (\"low\".equals(quality)) {\n            output = Output.AAC_LOW;\n        } else if (\"normal\".equals(quality)) {\n            output = Output.AAC;\n        }\n\n        audioRecorder.start(treeReferences, output);\n    }\n\n    public interface RecordAudioActionRegistry {\n\n        void register(BiConsumer<TreeReference, String> listener);\n\n        void unregister();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/CurrentFormIndex.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport org.javarosa.core.model.FormIndex\nimport org.odk.collect.android.javarosawrapper.ValidationResult\n\ndata class CurrentFormIndex(\n    /**\n     * The index of the screen to navigate to or refresh.\n     */\n    val screenIndex: FormIndex,\n    /**\n     * The index of the particular question to focus on and scroll to within a screen\n     * containing multiple questions. Can be null if navigation is intended\n     * only to the screen without focusing on a specific question.\n     */\n    val questionIndex: FormIndex?,\n    /**\n     * The result of validating the current form state.\n     * Contains information necessary to highlight a question with an invalid\n     * answer if validation was performed and failed. Can be null if validation\n     * has not been executed.\n     */\n    val validationResult: ValidationResult?\n)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormAnimation.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.content.Context\nimport org.odk.collect.strings.localization.isLTR\n\nenum class FormAnimationType {\n    LEFT, RIGHT, FADE\n}\n\nobject FormAnimation {\n    @JvmStatic\n    fun getAnimationTypeBasedOnLanguageDirection(context: Context, formAnimationType: FormAnimationType): FormAnimationType {\n        return if (context.isLTR()) {\n            formAnimationType\n        } else {\n            when (formAnimationType) {\n                FormAnimationType.LEFT -> FormAnimationType.RIGHT\n                FormAnimationType.RIGHT -> FormAnimationType.LEFT\n                else -> FormAnimationType.FADE\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormDefCache.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport org.javarosa.core.model.FormDef\nimport java.io.File\nimport java.io.IOException\n\ninterface FormDefCache {\n\n    @Throws(IOException::class)\n    fun writeCache(formDef: FormDef?, formPath: String?)\n    fun readCache(formXml: File?): FormDef?\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormEnd.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport androidx.annotation.StringRes\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.Send\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material.icons.filled.EditOff\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.platform.testTag\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.semantics.SemanticsPropertyKey\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport org.odk.collect.android.application.CollectTheme\nimport org.odk.collect.androidshared.ui.compose.marginExtraSmall\nimport org.odk.collect.androidshared.ui.compose.marginSmall\nimport org.odk.collect.androidshared.ui.compose.marginStandard\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\nimport org.odk.collect.strings.R.string\n\n@Composable\nfun FormEnd(\n    formTitle: String,\n    isEditableAfterFinalization: Boolean,\n    shouldBeSentAutomatically: Boolean,\n    saveAsDraftEnabled: Boolean,\n    finalizeEnabled: Boolean,\n    onSave: (Boolean) -> Unit = {}\n) {\n    Column(\n        modifier = Modifier\n            .fillMaxSize()\n            .padding(marginStandard()),\n        verticalArrangement = Arrangement.Center\n    ) {\n        Text(\n            text = stringResource(string.save_enter_data_description, formTitle),\n            style = MaterialTheme.typography.titleLarge\n        )\n\n        if (finalizeEnabled) {\n            val (icon, title, message) = getWarning(\n                isEditableAfterFinalization,\n                shouldBeSentAutomatically,\n                saveAsDraftEnabled\n            )\n\n            EditWarning(\n                icon = icon,\n                title = title,\n                message = message,\n                errorBackground = !isEditableAfterFinalization\n            )\n        }\n\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(top = 64.dp),\n            horizontalArrangement = Arrangement.spacedBy(marginStandard())\n        ) {\n            val screenName = stringResource(org.odk.collect.android.R.string.form_end_screen)\n\n            if (saveAsDraftEnabled) {\n                OutlinedButton(\n                    modifier = Modifier.weight(1f),\n                    onClick = {\n                        if (MultiClickGuard.allowClick(screenName)) {\n                            onSave(false)\n                        }\n                    }\n                ) {\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Icon(\n                            Icons.Default.Save,\n                            contentDescription = null\n                        )\n\n                        Text(\n                            text = stringResource(string.save_as_draft),\n                            modifier = Modifier.padding(start = marginExtraSmall())\n                        )\n                    }\n                }\n            }\n\n            if (finalizeEnabled) {\n                val finalizeText = if (shouldBeSentAutomatically) {\n                    string.send\n                } else {\n                    string.finalize\n                }\n\n                Button(\n                    modifier = Modifier.weight(1f),\n                    onClick = {\n                        if (MultiClickGuard.allowClick(screenName)) {\n                            onSave(true)\n                        }\n                    }\n                ) {\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Icon(\n                            Icons.AutoMirrored.Default.Send,\n                            contentDescription = null\n                        )\n\n                        Text(\n                            text = stringResource(finalizeText),\n                            modifier = Modifier.padding(start = marginExtraSmall())\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun EditWarning(\n    icon: ImageVector,\n    @StringRes title: Int,\n    @StringRes message: Int?,\n    errorBackground: Boolean\n) {\n    val titleText = stringResource(title)\n    val messageText = message?.let { stringResource(message) }\n\n    val cardColors = if (errorBackground) {\n        CardDefaults.cardColors().copy(\n            containerColor = MaterialTheme.colorScheme.errorContainer,\n            contentColor = MaterialTheme.colorScheme.onErrorContainer\n        )\n    } else {\n        CardDefaults.cardColors()\n    }\n\n    Card(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(top = marginStandard())\n            .testTag(EditWarningSemantics.TAG)\n            .clearAndSetSemantics {\n                contentDescription = if (messageText != null) {\n                    \"$titleText: $messageText\"\n                } else {\n                    titleText\n                }\n\n                set(EditWarningSemantics.iconProperty, icon.name)\n                set(EditWarningSemantics.errorBackgroundProperty, errorBackground)\n            },\n        colors = cardColors\n    ) {\n        Row(modifier = Modifier.padding(marginStandard())) {\n            Icon(\n                icon,\n                contentDescription = null\n            )\n\n            Column(modifier = Modifier.padding(start = marginStandard())) {\n                Text(\n                    text = titleText,\n                    style = MaterialTheme.typography.titleMedium\n                )\n\n                if (messageText != null) {\n                    Text(\n                        text = messageText,\n                        style = MaterialTheme.typography.bodyLarge,\n                        modifier = Modifier.padding(top = marginSmall())\n                    )\n                }\n            }\n        }\n    }\n}\n\nprivate fun getWarning(\n    isEditableAfterFinalization: Boolean,\n    shouldBeSentAutomatically: Boolean,\n    saveAsDraftEnabled: Boolean\n): Triple<ImageVector, Int, Int?> {\n    return if (isEditableAfterFinalization) {\n        if (shouldBeSentAutomatically) {\n            Triple(\n                Icons.Default.Edit,\n                string.form_editing_enabled_after_sending,\n                string.form_editing_enabled_after_sending_hint\n            )\n        } else {\n            Triple(\n                Icons.Default.Edit,\n                string.form_editing_enabled_after_finalizing,\n                string.form_editing_enabled_after_finalizing_hint\n            )\n        }\n    } else {\n        val icon = Icons.Default.EditOff\n        val message = if (saveAsDraftEnabled) {\n            string.form_editing_disabled_hint\n        } else {\n            null\n        }\n\n        if (shouldBeSentAutomatically) {\n            Triple(\n                icon,\n                string.form_editing_disabled_after_sending,\n                message\n            )\n        } else {\n            Triple(\n                icon,\n                string.form_editing_disabled_after_finalizing,\n                message\n            )\n        }\n    }\n}\n\nobject EditWarningSemantics {\n    const val TAG = \"EditWarning\"\n    val iconProperty = SemanticsPropertyKey<String>(\"icon\")\n    val errorBackgroundProperty = SemanticsPropertyKey<Boolean>(\"errorBackground\")\n}\n\n@Preview\n@Composable\nprivate fun PreviewFormEnd(saveAsDraftEnabled: Boolean = true, finalizeEnabled: Boolean = true) {\n    CollectTheme {\n        FormEnd(\n            formTitle = \"My form\",\n            isEditableAfterFinalization = false,\n            shouldBeSentAutomatically = false,\n            saveAsDraftEnabled = saveAsDraftEnabled,\n            finalizeEnabled = finalizeEnabled\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun PreviewFormEndSaveAsDraftDisabled() {\n    PreviewFormEnd(saveAsDraftEnabled = false)\n}\n\n@Preview\n@Composable\nprivate fun PreviewFormEndFinalizeDisabled() {\n    PreviewFormEnd(finalizeEnabled = false)\n}"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormEndView.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport androidx.core.widget.NestedScrollView\nimport org.odk.collect.android.R\nimport org.odk.collect.android.databinding.FormEntryEndBinding\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\n\nclass FormEndView(\n    context: Context,\n    formTitle: String,\n    isFormEditableAfterFinalization: Boolean,\n    formEndViewModel: FormEndViewModel,\n    private val listener: Listener\n) : SwipeHandler.View(context) {\n\n    private val binding = FormEntryEndBinding.inflate(LayoutInflater.from(context), this, true)\n\n    init {\n        binding.composeView.setContextThemedContent { \n            FormEnd(\n                formTitle = formTitle,\n                isEditableAfterFinalization = isFormEditableAfterFinalization,\n                shouldBeSentAutomatically = formEndViewModel.shouldFormBeSentAutomatically(),\n                saveAsDraftEnabled = formEndViewModel.isSaveDraftEnabled(),\n                finalizeEnabled = formEndViewModel.isFinalizeEnabled(),\n                onSave = { listener.onSaveClicked(it) }\n            )\n        }\n    }\n\n    override fun shouldSuppressFlingGesture() = false\n\n    override fun verticalScrollView(): NestedScrollView? {\n        return findViewById(R.id.scroll_view)\n    }\n\n    interface Listener {\n        fun onSaveClicked(markAsFinalized: Boolean)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormEndViewModel.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport androidx.lifecycle.ViewModel\nimport org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider\nimport org.odk.collect.android.instancemanagement.autosend.shouldFormBeSentAutomatically\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\n\nclass FormEndViewModel(\n    private val formSessionRepository: FormSessionRepository,\n    private val sessionId: String,\n    private val settingsProvider: SettingsProvider,\n    private val autoSendSettingsProvider: AutoSendSettingsProvider\n) : ViewModel() {\n\n    fun isSaveDraftEnabled(): Boolean {\n        return settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT)\n    }\n\n    fun isFinalizeEnabled(): Boolean {\n        return settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY)\n    }\n\n    fun shouldFormBeSentAutomatically(): Boolean {\n        val form = formSessionRepository.get(sessionId).value?.form\n        return form?.shouldFormBeSentAutomatically(autoSendSettingsProvider.isAutoSendEnabledInSettings()) ?: false\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryMenuProvider.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.view.Menu\nimport android.view.MenuInflater\nimport android.view.MenuItem\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.ContextCompat\nimport androidx.core.view.MenuProvider\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formentry.backgroundlocation.BackgroundLocationViewModel\nimport org.odk.collect.android.formentry.questions.AnswersProvider\nimport org.odk.collect.android.formhierarchy.FormHierarchyFragmentHostActivity\nimport org.odk.collect.android.preferences.screens.ProjectPreferencesActivity\nimport org.odk.collect.android.utilities.ActionRegister\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.androidshared.system.PlayServicesChecker\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing\nimport org.odk.collect.androidshared.ui.enableIconsVisibility\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard.allowClick\nimport org.odk.collect.audiorecorder.recording.AudioRecorder\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.strings.localization.getLocalizedString\n\nclass FormEntryMenuProvider(\n    private val activity: AppCompatActivity,\n    private val answersProvider: AnswersProvider,\n    private val formEntryViewModel: FormEntryViewModel,\n    private val audioRecorder: AudioRecorder,\n    private val backgroundLocationViewModel: BackgroundLocationViewModel,\n    private val backgroundAudioViewModel: BackgroundAudioViewModel,\n    private val settingsProvider: SettingsProvider,\n    private val formEntryMenuClickListener: FormEntryMenuClickListener,\n    private val beforeMenuItemClick: () -> Boolean\n) : MenuProvider {\n    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {\n        menuInflater.inflate(R.menu.form_menu, menu)\n        menu.enableIconsVisibility()\n    }\n\n    override fun onPrepareMenu(menu: Menu) {\n        val formController = formEntryViewModel.formController\n\n        var useability: Boolean = settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_SAVE_MID)\n        menu.findItem(R.id.menu_save).isVisible = useability\n\n        useability = settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_JUMP_TO)\n        menu.findItem(R.id.menu_goto).isVisible = useability\n\n        useability = settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_CHANGE_LANGUAGE) && formController != null && formController.getLanguages() != null && formController.getLanguages()!!.size > 1\n        menu.findItem(R.id.menu_languages).isVisible = useability\n\n        useability = settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_ACCESS_SETTINGS)\n        menu.findItem(R.id.menu_preferences).isVisible = useability\n\n        if (formController != null &&\n            formController.currentFormCollectsBackgroundLocation() &&\n            PlayServicesChecker().isGooglePlayServicesAvailable(activity)\n        ) {\n            val backgroundLocation = menu.findItem(R.id.track_location)\n            backgroundLocation.isVisible = true\n            val isBackgroundLocationEnabled = settingsProvider.getUnprotectedSettings().getBoolean(ProjectKeys.KEY_BACKGROUND_LOCATION)\n            menu.findItem(R.id.track_location).icon = if (isBackgroundLocationEnabled) {\n                ContextCompat.getDrawable(activity, org.odk.collect.icons.R.drawable.ic_baseline_location_on_24)\n            } else {\n                ContextCompat.getDrawable(activity, org.odk.collect.icons.R.drawable.ic_baseline_location_off_24)\n            }\n            menu.findItem(R.id.track_location).title = if (isBackgroundLocationEnabled) {\n                activity.getLocalizedString(org.odk.collect.strings.R.string.track_location_on)\n            } else {\n                activity.getLocalizedString(org.odk.collect.strings.R.string.track_location_off)\n            }\n        }\n\n        menu.findItem(R.id.menu_add_repeat).isVisible = formEntryViewModel.canAddRepeat()\n\n        menu.findItem(R.id.menu_record_audio).isVisible = formEntryViewModel.hasBackgroundRecording().value\n        val isRecordingAudioEnabled = backgroundAudioViewModel.isBackgroundRecordingEnabled.value\n        menu.findItem(R.id.menu_record_audio).icon = if (isRecordingAudioEnabled) {\n            ContextCompat.getDrawable(activity, org.odk.collect.icons.R.drawable.ic_baseline_mic_24)\n        } else {\n            ContextCompat.getDrawable(activity, org.odk.collect.icons.R.drawable.ic_baseline_mic_off_24)\n        }\n        menu.findItem(R.id.menu_record_audio).title = if (isRecordingAudioEnabled) {\n            activity.getLocalizedString(org.odk.collect.strings.R.string.record_audio_on)\n        } else {\n            activity.getLocalizedString(org.odk.collect.strings.R.string.record_audio_off)\n        }\n    }\n\n    override fun onMenuItemSelected(item: MenuItem): Boolean {\n        if (!allowClick(javaClass.name)) {\n            return true\n        }\n\n        if (!beforeMenuItemClick()) {\n            return true\n        }\n\n        return when (item.itemId) {\n            R.id.menu_add_repeat -> {\n                if (audioRecorder.isRecording() && !backgroundAudioViewModel.isBackgroundRecording) {\n                    showIfNotShowing(RecordingWarningDialogFragment::class.java, activity.supportFragmentManager)\n                } else {\n                    formEntryViewModel.updateAnswersForScreen(answersProvider.answers, false)\n                    formEntryViewModel.promptForNewRepeat()\n                }\n                true\n            }\n            R.id.menu_preferences -> {\n                if (audioRecorder.isRecording()) {\n                    showIfNotShowing(RecordingWarningDialogFragment::class.java, activity.supportFragmentManager)\n                } else {\n                    formEntryViewModel.updateAnswersForScreen(answersProvider.answers, false)\n                    val intent = Intent(activity, ProjectPreferencesActivity::class.java)\n                    intent.putExtra(ProjectPreferencesActivity.EXTRA_IN_FORM_ENTRY, true)\n                    activity.startActivityForResult(intent, ApplicationConstants.RequestCodes.CHANGE_SETTINGS)\n                }\n                true\n            }\n            R.id.track_location -> {\n                backgroundLocationViewModel.backgroundLocationPreferenceToggled(settingsProvider.getUnprotectedSettings())\n                true\n            }\n            R.id.menu_goto -> {\n                ActionRegister.actionDetected()\n\n                if (audioRecorder.isRecording() && !backgroundAudioViewModel.isBackgroundRecording) {\n                    showIfNotShowing(RecordingWarningDialogFragment::class.java, activity.supportFragmentManager)\n                } else {\n                    formEntryViewModel.updateAnswersForScreen(answersProvider.answers, false)\n                    formEntryViewModel.openHierarchy()\n                    val i = Intent(activity, FormHierarchyFragmentHostActivity::class.java)\n                    i.putExtra(FormHierarchyFragmentHostActivity.EXTRA_SESSION_ID, formEntryViewModel.sessionId)\n                    activity.startActivityForResult(i, ApplicationConstants.RequestCodes.HIERARCHY_ACTIVITY)\n                }\n                true\n            }\n            R.id.menu_record_audio -> {\n                val enabled = item.title == activity.getLocalizedString(org.odk.collect.strings.R.string.record_audio_off)\n                if (enabled) {\n                    MaterialAlertDialogBuilder(activity)\n                        .setMessage(org.odk.collect.strings.R.string.background_audio_recording_enabled_explanation)\n                        .setCancelable(false)\n                        .setPositiveButton(org.odk.collect.strings.R.string.ok, null)\n                        .create()\n                        .show()\n                    backgroundAudioViewModel.setBackgroundRecordingEnabled(true)\n                } else {\n                    MaterialAlertDialogBuilder(activity)\n                        .setMessage(org.odk.collect.strings.R.string.stop_recording_confirmation)\n                        .setPositiveButton(org.odk.collect.strings.R.string.disable_recording) { _: DialogInterface?, _: Int -> backgroundAudioViewModel.setBackgroundRecordingEnabled(false) }\n                        .setNegativeButton(org.odk.collect.strings.R.string.cancel, null)\n                        .create()\n                        .show()\n                }\n                true\n            }\n            R.id.menu_validate -> {\n                formEntryViewModel.saveScreenAnswersToFormController(answersProvider.answers, false)\n                formEntryViewModel.validateForm()\n                true\n            }\n            R.id.menu_languages -> {\n                formEntryMenuClickListener.changeLanguage()\n                true\n            }\n            R.id.menu_save -> {\n                formEntryMenuClickListener.save()\n                true\n            }\n            else -> {\n                false\n            }\n        }\n    }\n\n    interface FormEntryMenuClickListener {\n        fun changeLanguage()\n        fun save()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport org.apache.commons.io.FileUtils.readFileToByteArray\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.instance.InstanceInitializationFactory\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.core.model.instance.utils.DefaultAnswerResolver\nimport org.javarosa.core.reference.ReferenceManager\nimport org.javarosa.form.api.FormEntryController\nimport org.javarosa.xform.parse.XFormParser\nimport org.javarosa.xform.util.XFormUtils\nimport org.odk.collect.android.dynamicpreload.ExternalAnswerResolver\nimport org.odk.collect.android.instancemanagement.isEdit\nimport org.odk.collect.android.javarosawrapper.FailedValidationResult\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.javarosawrapper.JavaRosaFormController\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.android.utilities.FormUtils\nimport org.odk.collect.entities.LocalEntityUseCases\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.shared.DebugLogger\nimport java.io.File\n\nobject FormEntryUseCases {\n\n    fun loadFormDef(\n        instance: Instance,\n        formsRepository: FormsRepository,\n        projectRootDir: File,\n        formDefCache: FormDefCache\n    ): Pair<FormDef, Form>? {\n        val form =\n            formsRepository.getAllByFormIdAndVersion(instance.formId, instance.formVersion)\n                .firstOrNull()\n        return if (form == null) {\n            null\n        } else {\n            val formDef = loadFormDef(form, projectRootDir, formDefCache)\n            return if (formDef == null) {\n                null\n            } else {\n                Pair(formDef, form)\n            }\n        }\n    }\n\n    fun loadFormDef(\n        form: Form,\n        projectRootDir: File,\n        formDefCache: FormDefCache\n    ): FormDef? {\n        val xForm = File(form.formFilePath)\n        if (!xForm.exists()) {\n            return null\n        }\n        val formMediaDir = File(form.formMediaPath)\n\n        FormUtils.setupReferenceManagerForForm(\n            ReferenceManager.instance(),\n            projectRootDir,\n            formMediaDir\n        )\n\n        return createFormDefFromCacheOrXml(xForm, formDefCache)!!\n    }\n\n    fun loadBlankForm(\n        form: Form,\n        formEntryController: FormEntryController,\n        instanceFile: File\n    ): FormController {\n        formEntryController.model.form.initialize(true, InstanceInitializationFactory())\n\n        return JavaRosaFormController(\n            File(form.formMediaPath),\n            formEntryController,\n            instanceFile\n        )\n    }\n\n    @JvmStatic\n    fun loadDraft(\n        form: Form,\n        instance: Instance,\n        formEntryController: FormEntryController\n    ): FormController? {\n        val instanceFile = File(instance.instanceFilePath)\n        if (!instanceFile.exists()) {\n            return null\n        }\n\n        importInstance(instanceFile, formEntryController)\n        formEntryController.model.form.initialize(false, InstanceInitializationFactory())\n\n        return JavaRosaFormController(\n            File(form.formMediaPath),\n            formEntryController,\n            instanceFile\n        )\n    }\n\n    fun saveDraft(\n        form: Form,\n        formController: FormController,\n        instancesRepository: InstancesRepository\n    ): Instance {\n        saveInstanceToDisk(formController)\n        return instancesRepository.save(\n            Instance.Builder()\n                .formId(form.formId)\n                .formVersion(form.version)\n                .instanceFilePath(formController.getInstanceFile()!!.absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n    }\n\n    @JvmStatic\n    fun finalizeDraft(\n        formController: FormController,\n        instancesRepository: InstancesRepository,\n        entitiesRepository: EntitiesRepository,\n        debugLogger: DebugLogger\n    ): Instance? {\n        val instance =\n            getInstanceFromFormController(formController, instancesRepository)!!\n\n        val validationResult = formController.validateAnswers(false)\n        val valid = validationResult !is FailedValidationResult\n\n        return if (valid) {\n            val newInstance = finalizeFormController(\n                instance,\n                formController,\n                instancesRepository,\n                entitiesRepository,\n                debugLogger\n            )\n            saveInstanceToDisk(formController)\n            newInstance\n        } else {\n            instancesRepository.save(\n                Instance.Builder(instance)\n                    .status(Instance.STATUS_INVALID)\n                    .build()\n            )\n\n            null\n        }\n    }\n\n    @JvmStatic\n    fun finalizeFormController(\n        instance: Instance,\n        formController: FormController,\n        instancesRepository: InstancesRepository,\n        entitiesRepository: EntitiesRepository,\n        debugLogger: DebugLogger\n    ): Instance? {\n        formController.finalizeForm()\n\n        val formEntities = formController.getEntities()\n        if (!instance.isEdit()) {\n            LocalEntityUseCases.updateLocalEntitiesFromForm(\n                formEntities,\n                entitiesRepository,\n                debugLogger\n            )\n        }\n\n        val instanceName = formController.getSubmissionMetadata()?.instanceName\n        return instancesRepository.save(\n            Instance.Builder(instance)\n                .status(Instance.STATUS_COMPLETE)\n                .canEditWhenComplete(formController.isSubmissionEntireForm())\n                .displayName(instanceName ?: instance.displayName)\n                .canDeleteBeforeSend(formEntities == null)\n                .build()\n        )\n    }\n\n    @JvmStatic\n    private fun saveInstanceToDisk(formController: FormController) {\n        val payload = formController.getSubmissionXml()\n        FileUtils.write(formController.getInstanceFile(), payload!!.payloadBytes)\n    }\n\n    private fun getInstanceFromFormController(\n        formController: FormController,\n        instancesRepository: InstancesRepository\n    ): Instance? {\n        val instancePath = formController.getInstanceFile()!!.absolutePath\n        return instancesRepository.getOneByPath(instancePath)\n    }\n\n    private fun createFormDefFromCacheOrXml(xForm: File, formDefCache: FormDefCache): FormDef? {\n        val formDefFromCache = formDefCache.readCache(xForm)\n        if (formDefFromCache != null) {\n            return formDefFromCache\n        }\n\n        val lastSavedSrc = FileUtils.getOrCreateLastSavedSrc(xForm)\n        return XFormUtils.getFormFromFormXml(xForm.absolutePath, lastSavedSrc)?.also {\n            formDefCache.writeCache(it, xForm.path)\n        }\n    }\n\n    private fun importInstance(instanceFile: File, formEntryController: FormEntryController) {\n        // convert files into a byte array\n        val fileBytes = readFileToByteArray(instanceFile)\n\n        // get the root of the saved and template instances\n        val savedRoot = XFormParser.restoreDataModel(fileBytes, null).root\n        val templateRoot = formEntryController.model.form.instance.root.deepCopy(true)\n\n        // weak check for matching forms\n        if (savedRoot.name != templateRoot.name || savedRoot.mult != 0) {\n            return\n        }\n\n        // populate the data model\n        val tr = TreeReference.rootRef()\n        tr.add(templateRoot.name, TreeReference.INDEX_UNBOUND)\n\n        // Here we set the Collect's implementation of the IAnswerResolver.\n        // We set it back to the default after select choices have been populated.\n        XFormParser.setAnswerResolver(ExternalAnswerResolver())\n        templateRoot.populate(savedRoot, formEntryController.model.form)\n        XFormParser.setAnswerResolver(DefaultAnswerResolver())\n\n        // FormInstanceParser.parseInstance is responsible for initial creation of instances. It explicitly sets the\n        // main instance name to null so we force this again on deserialization because some code paths rely on the main\n        // instance not having a name. Must be before the call on setRoot because setRoot also sets the root's name.\n        formEntryController.model.form.instance.name = null\n\n        // populated model to current form\n        formEntryController.model.form.instance.root = templateRoot\n\n        // fix any language issues\n        // :\n        // http://bitbucket.org/javarosa/main/issue/5/itext-n-appearing-in-restored-instances\n        if (formEntryController.model.languages != null) {\n            formEntryController.model.form\n                .localeChanged(\n                    formEntryController.model.language,\n                    formEntryController.model.form.localizer\n                )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryViewModel.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport static org.odk.collect.android.external.FormUriActivityKt.FORM_ENTRY_TOKEN;\nimport static org.odk.collect.android.javarosawrapper.FormControllerExt.getQuestionPrompts;\nimport static org.odk.collect.android.javarosawrapper.FormIndexUtils.getRepeatGroupIndex;\nimport static org.odk.collect.androidshared.livedata.LiveDataUtils.observe;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.lifecycle.ViewModel;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.GroupDef;\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.SubmissionProfile;\nimport org.javarosa.core.model.actions.recordaudio.RecordAudioActionHandler;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryController;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.javarosa.xpath.parser.XPathSyntaxException;\nimport org.odk.collect.android.exception.ExternalDataException;\nimport org.odk.collect.android.exception.JavaRosaException;\nimport org.odk.collect.android.formentry.audit.AuditEvent;\nimport org.odk.collect.android.formentry.audit.AuditUtils;\nimport org.odk.collect.android.formentry.questions.SelectChoiceUtils;\nimport org.odk.collect.android.javarosawrapper.FailedValidationResult;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.javarosawrapper.RepeatsInFieldListException;\nimport org.odk.collect.android.javarosawrapper.ValidationResult;\nimport org.odk.collect.android.logic.ImmutableDisplayableQuestion;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.ChangeLocks;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.androidshared.async.TrackableWorker;\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData;\nimport org.odk.collect.androidshared.livedata.NonNullLiveData;\nimport org.odk.collect.async.Cancellable;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.forms.instances.Instance;\n\nimport java.io.FileNotFoundException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.Supplier;\n\nimport timber.log.Timber;\n\npublic class FormEntryViewModel extends ViewModel implements SelectChoiceLoader {\n\n    private final Supplier<Long> clock;\n\n    private final MutableLiveData<FormError> error = new MutableLiveData<>(null);\n    private final MutableNonNullLiveData<Boolean> hasBackgroundRecording = new MutableNonNullLiveData<>(false);\n    private final MutableLiveData<CurrentFormIndex> currentIndex = new MutableLiveData<>(null);\n    @NonNull\n    private final FormSessionRepository formSessionRepository;\n    private final String sessionId;\n    private Form form;\n    private Instance instance;\n\n    @Nullable\n    private FormController formController;\n\n    @Nullable\n    private FormIndex jumpBackIndex;\n\n    private final Cancellable formSessionObserver;\n    private final FormsRepository formsRepository;\n    private final ChangeLocks changeLocks;\n\n    private final Map<FormIndex, List<SelectChoice>> choices = new HashMap<>();\n\n    private final TrackableWorker worker;\n    private boolean newEditMessageAlreadyShown;\n\n    @SuppressWarnings(\"WeakerAccess\")\n    public FormEntryViewModel(Supplier<Long> clock, Scheduler scheduler, FormSessionRepository formSessionRepository, String sessionId, FormsRepository formsRepository, ChangeLocks changeLocks) {\n        this.clock = clock;\n        this.formSessionRepository = formSessionRepository;\n        worker = new TrackableWorker(scheduler);\n\n        this.sessionId = sessionId;\n        formSessionObserver = observe(formSessionRepository.get(this.sessionId), formSession -> {\n            this.formController = formSession.getFormController();\n            this.form = formSession.getForm();\n            this.instance = formSession.getInstance();\n\n            boolean hasBackgroundRecording = formController.getFormDef().hasAction(RecordAudioActionHandler.ELEMENT_NAME);\n            this.hasBackgroundRecording.setValue(hasBackgroundRecording);\n        });\n        this.formsRepository = formsRepository;\n        this.changeLocks = changeLocks;\n    }\n\n    public String getSessionId() {\n        return sessionId;\n    }\n\n    /**\n     * @deprecated this should not be exposed\n     */\n    @Deprecated\n    public FormController getFormController() {\n        return formController;\n    }\n\n    public LiveData<CurrentFormIndex> getCurrentIndex() {\n        return currentIndex;\n    }\n\n    public LiveData<FormError> getError() {\n        return error;\n    }\n\n    public NonNullLiveData<Boolean> isLoading() {\n        return worker.isWorking();\n    }\n\n    @SuppressWarnings(\"WeakerAccess\")\n    public void promptForNewRepeat() {\n        if (formController == null) {\n            return;\n        }\n\n        jumpBackIndex = formController.getFormIndex();\n        jumpToNewRepeat();\n        updateIndex(false, null);\n    }\n\n    public void jumpToNewRepeat() {\n        formController.jumpToNewRepeatPrompt();\n    }\n\n    public void addRepeat() {\n        if (formController == null) {\n            return;\n        }\n\n        jumpBackIndex = null;\n\n        try {\n            formController.newRepeat();\n        } catch (RuntimeException e) {\n            error.setValue(new FormError.NonFatal(e.getCause() != null ? e.getCause().getMessage() : e.getMessage()));\n        }\n\n        if (!formController.indexIsInFieldList()) {\n            try {\n                formController.stepToNextScreenEvent();\n            } catch (JavaRosaException e) {\n                error.setValue(new FormError.NonFatal(e.getCause() != null ? e.getCause().getMessage() : e.getMessage()));\n            }\n        }\n\n        updateIndex(false, null);\n    }\n\n    public void cancelRepeatPrompt() {\n        if (formController == null) {\n            return;\n        }\n\n        worker.immediate((Supplier<Void>) () -> {\n            if (jumpBackIndex != null) {\n                formController.jumpToIndex(jumpBackIndex);\n                jumpBackIndex = null;\n            } else {\n                try {\n                    this.formController.stepToNextScreenEvent();\n                } catch (JavaRosaException exception) {\n                    error.postValue(new FormError.NonFatal(exception.getCause().getMessage()));\n                }\n            }\n\n            updateIndex(true, null);\n            return null;\n        }, ignored -> {});\n    }\n\n    public void errorDisplayed() {\n        error.setValue(null);\n    }\n\n    public boolean canAddRepeat() {\n        if (formController != null && formController.indexContainsRepeatableGroup()) {\n            FormDef formDef = formController.getFormDef();\n            FormIndex repeatGroupIndex = getRepeatGroupIndex(formController.getFormIndex(), formDef);\n            return !((GroupDef) formDef.getChild(repeatGroupIndex)).noAddRemove;\n        } else {\n            return false;\n        }\n    }\n\n    public void moveForward(HashMap<FormIndex, IAnswerData> answers) {\n        moveForward(answers, false);\n    }\n\n    public void moveForward(HashMap<FormIndex, IAnswerData> answers, Boolean evaluateConstraints) {\n        worker.immediate(() -> {\n            boolean updateSuccess = saveScreenAnswersToFormController(answers, evaluateConstraints);\n            if (updateSuccess) {\n                try {\n                    formController.stepToNextScreenEvent();\n                    formController.getAuditEventLogger().flush(); // Close events waiting for an end time\n                    updateIndex(true, null);\n                } catch (JavaRosaException e) {\n                    error.postValue(new FormError.NonFatal(e.getCause().getMessage()));\n                }\n            }\n\n            return null;\n        }, ignored -> {\n\n        });\n    }\n\n    public void moveBackward(HashMap<FormIndex, IAnswerData> answers) {\n        worker.immediate((Supplier<Boolean>) () -> {\n            boolean updateSuccess = saveScreenAnswersToFormController(answers, false);\n            if (updateSuccess) {\n                try {\n                    formController.stepToPreviousScreenEvent();\n                    formController.getAuditEventLogger().flush(); // Close events waiting for an end time\n                    updateIndex(true, null);\n                } catch (JavaRosaException e) {\n                    error.postValue(new FormError.NonFatal(e.getCause().getMessage()));\n                }\n            }\n\n            return updateSuccess;\n        }, ignored -> {\n\n        });\n    }\n\n    public boolean updateAnswersForScreen(HashMap<FormIndex, IAnswerData> answers, Boolean evaluateConstraints) {\n        boolean success = saveScreenAnswersToFormController(answers, evaluateConstraints);\n        formController.getAuditEventLogger().flush();\n\n        return success;\n    }\n\n    public boolean saveScreenAnswersToFormController(HashMap<FormIndex, IAnswerData> answers, Boolean evaluateConstraints) {\n        if (formController == null) {\n            return false;\n        }\n\n        try {\n            ValidationResult result = formController.saveAllScreenAnswers(answers, evaluateConstraints);\n            if (result instanceof FailedValidationResult) {\n                updateIndex(true, result);\n                return false;\n            }\n        } catch (JavaRosaException e) {\n            error.postValue(new FormError.NonFatal(e.getMessage()));\n            return false;\n        }\n\n        return true;\n    }\n\n    public void openHierarchy() {\n        formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.HIERARCHY, true, clock.get());\n    }\n\n    public NonNullLiveData<Boolean> hasBackgroundRecording() {\n        return hasBackgroundRecording;\n    }\n\n    public FormEntryPrompt getQuestionPrompt(FormIndex formIndex) {\n        return formController.getQuestionPrompt(formIndex);\n    }\n\n    public void answerQuestion(FormIndex index, IAnswerData answer) {\n        worker.immediate(() -> {\n            try {\n                FormEntryPrompt prompt = formController.getQuestionPrompt(index);\n                boolean autoAdvance = Appearances.isQuick(prompt);\n                ValidationResult result = formController.saveOneScreenAnswer(index, answer, autoAdvance);\n\n                if (result instanceof FailedValidationResult) {\n                    updateIndex(true, result);\n                } else {\n                    if (autoAdvance) {\n                        formController.stepToNextScreenEvent();\n                        updateIndex(true, null);\n                    } else {\n                        updateIndex(true, null, index);\n                    }\n                }\n            } catch (JavaRosaException e) {\n                throw new RuntimeException(e);\n            }\n        });\n    }\n\n    @NonNull\n    @Override\n    public List<SelectChoice> loadSelectChoices(@NonNull FormEntryPrompt prompt) throws FileNotFoundException, XPathSyntaxException, ExternalDataException {\n        List<SelectChoice> selectChoices = choices.get(prompt.getIndex());\n\n        if (selectChoices != null) {\n            return selectChoices;\n        } else {\n            // Choice lists from some questions aren't preloaded yet\n            return SelectChoiceUtils.loadSelectChoices(prompt, formController);\n        }\n    }\n\n    @Override\n    protected void onCleared() {\n        formSessionObserver.cancel();\n    }\n\n    /**\n     * Use {@link #refresh()} instead.\n     */\n    @Deprecated\n    public void refreshSync() {\n        updateIndex(false, null);\n    }\n\n    public void refresh() {\n        worker.immediate((Supplier<Void>) () -> {\n            updateIndex(true, null);\n            return null;\n        }, ignored -> {});\n    }\n\n    private void updateIndex(boolean isAsync, @Nullable ValidationResult validationResult) {\n        updateIndex(isAsync, validationResult, null);\n    }\n\n    private void updateIndex(boolean isAsync, @Nullable ValidationResult validationResult, @Nullable FormIndex questionIndex) {\n        choices.clear();\n\n        if (formController != null) {\n            if (formController.getEvent() == FormEntryController.EVENT_BEGINNING_OF_FORM) {\n                try {\n                    formController.stepToNextScreenEvent();\n                } catch (JavaRosaException e) {\n                    if (isAsync) {\n                        error.postValue(new FormError.NonFatal(e.getCause() != null ? e.getCause().getMessage() : e.getMessage()));\n                    } else {\n                        error.setValue(new FormError.NonFatal(e.getCause() != null ? e.getCause().getMessage() : e.getMessage()));\n                    }\n                }\n            }\n            try {\n                /*\n                 We can't load for field lists as their choices might change on screen (before\n                 updateIndex is called again).\n                */\n                if (!formController.indexIsInFieldList()) {\n                    preloadSelectChoices();\n                }\n            } catch (RepeatsInFieldListException | XPathSyntaxException |\n                     FileNotFoundException e) {\n                // Ignored\n            }\n\n            AuditUtils.logCurrentScreen(formController, formController.getAuditEventLogger(), clock.get());\n            if (isAsync) {\n                currentIndex.postValue(new CurrentFormIndex(formController.getFormIndex(), questionIndex, validationResult));\n            } else {\n                currentIndex.setValue(new CurrentFormIndex(formController.getFormIndex(), questionIndex, validationResult));\n            }\n        }\n    }\n\n    public void exit() {\n        formSessionRepository.clear(sessionId);\n        ReferenceManager.instance().reset();\n        changeLocks.getFormsLock().unlock(FORM_ENTRY_TOKEN);\n    }\n\n    public void validateForm() {\n        worker.immediate(\n                () -> {\n                    ValidationResult result = null;\n                    try {\n                        result = formController.validateAnswers(true);\n                    } catch (JavaRosaException e) {\n                        error.postValue(new FormError.NonFatal(e.getMessage()));\n                    }\n\n                    updateIndex(true, result);\n                    return null;\n                }, ignored -> {}\n        );\n    }\n\n    private void preloadSelectChoices() throws RepeatsInFieldListException, FileNotFoundException, XPathSyntaxException {\n        int event = formController.getEvent();\n        if (event == FormEntryController.EVENT_QUESTION) {\n            FormEntryPrompt prompt = formController.getQuestionPrompt();\n\n            if (prompt != null) {\n                try {\n                    List<SelectChoice> selectChoices = SelectChoiceUtils.loadSelectChoices(prompt, formController);\n                    choices.put(prompt.getIndex(), selectChoices);\n                } catch (Exception e) {\n                    // Let the widget load choices and handle the error\n                }\n            }\n        }\n    }\n\n    public void changeLanguage(String newLanguage) {\n        formController.setLanguage(newLanguage);\n\n        worker.immediate(() -> {\n            formsRepository.save(new Form.Builder(form)\n                    .language(newLanguage)\n                    .build()\n            );\n        });\n    }\n\n    public boolean isFormEditableAfterFinalization() {\n        SubmissionProfile submissionProfile = formController.getFormDef().getSubmissionProfile();\n        if (submissionProfile == null) {\n            return false;\n        }\n        String clientEditableAttribute = submissionProfile.getAttribute(\"client-editable\");\n        return Boolean.parseBoolean(clientEditableAttribute);\n    }\n\n    public boolean shouldShowNewEditMessage() {\n        boolean shouldShowNewEditMessage =  instance != null &&\n                instance.getStatus().equals(Instance.STATUS_NEW_EDIT) &&\n                !newEditMessageAlreadyShown;\n\n        newEditMessageAlreadyShown = true;\n\n        return shouldShowNewEditMessage;\n    }\n\n    // The method saves questions one by one in order to support calculations in field-list groups\n    public FormEntryPrompt[] saveFieldList(HashMap<FormIndex, IAnswerData> answers) throws RepeatsInFieldListException {\n        FormEntryPrompt[] questionsBeforeSave = getQuestionPrompts(getFormController());\n        List<ImmutableDisplayableQuestion> immutableQuestionsBeforeSave = new ArrayList<>();\n        for (FormEntryPrompt questionBeforeSave : questionsBeforeSave) {\n            immutableQuestionsBeforeSave.add(new ImmutableDisplayableQuestion(questionBeforeSave));\n        }\n\n        int index = 0;\n        for (Map.Entry<FormIndex, IAnswerData> answer : answers.entrySet()) {\n            // Questions with calculates will have their answers updated as the questions they depend on are saved\n            if (!isQuestionRecalculated(questionsBeforeSave[index], immutableQuestionsBeforeSave.get(index))) {\n                try {\n                    formController.saveOneScreenAnswer(answer.getKey(), answer.getValue(), false);\n                } catch (JavaRosaException e) {\n                    Timber.e(e);\n                }\n            }\n\n            index++;\n        }\n\n        return getQuestionPrompts(formController);\n    }\n\n    private boolean isQuestionRecalculated(FormEntryPrompt mutableQuestionBeforeSave, ImmutableDisplayableQuestion immutableQuestionBeforeSave) {\n        return !Objects.equals(mutableQuestionBeforeSave.getAnswerText(), immutableQuestionBeforeSave.getAnswerText());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormError.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.os.Parcelable\nimport kotlinx.android.parcel.Parcelize\n\n@Parcelize\nsealed class FormError : Parcelable {\n    abstract val message: String?\n\n    data class NonFatal(override val message: String?) : FormError()\n\n    data class Fatal(override val message: String?) : FormError()\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormIndexAnimationHandler.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport org.javarosa.core.model.FormIndex\n\n/**\n * Responsible for determining how a new \"screen\" in a form should be animated to based on\n * the [FormIndex].\n */\nclass FormIndexAnimationHandler(private val listener: Listener) {\n\n    enum class Direction {\n        FORWARDS, BACKWARDS\n    }\n\n    private var lastIndex: FormIndex? = null\n\n    fun handle(index: FormIndex) {\n        if (lastIndex == null) {\n            listener.onScreenRefresh(true)\n        } else {\n            if (index > lastIndex) {\n                listener.onScreenChange(Direction.FORWARDS)\n            } else if (index < lastIndex) {\n                listener.onScreenChange(Direction.BACKWARDS)\n            } else {\n                listener.onScreenRefresh(false)\n            }\n        }\n\n        lastIndex = index\n    }\n\n    /**\n     * Can be used to update the handler on the starting index in situations\n     * where [.handle] isn't be called.\n     */\n    fun setLastIndex(lastIndex: FormIndex?) {\n        this.lastIndex = lastIndex\n    }\n\n    interface Listener {\n        fun onScreenChange(direction: Direction?)\n        fun onScreenRefresh(isFormStart: Boolean)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormLoadingDialogFragment.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.formentry;\n\nimport android.content.Context;\n\nimport org.odk.collect.android.formentry.saving.SaveFormProgressDialogFragment;\nimport org.odk.collect.material.MaterialProgressDialogFragment;\n\npublic class FormLoadingDialogFragment extends MaterialProgressDialogFragment {\n\n    public interface FormLoadingDialogFragmentListener {\n        void onCancelFormLoading();\n    }\n\n    /**\n     * Using a listener like this requires an Activity to implement the interface. We could\n     * use a similar approach as that used in {@link SaveFormProgressDialogFragment} and grab\n     * a ViewModel to cancel.\n     */\n    @Deprecated\n    private FormLoadingDialogFragmentListener listener;\n\n    @Override\n    public void onAttach(Context context) {\n        super.onAttach(context);\n\n        setTitle(getString(org.odk.collect.strings.R.string.loading_form));\n        setMessage(getString(org.odk.collect.strings.R.string.please_wait));\n        setCancelable(false);\n\n        if (context instanceof FormLoadingDialogFragmentListener) {\n            listener = (FormLoadingDialogFragmentListener) context;\n        }\n    }\n\n    @Override\n    protected String getCancelButtonText() {\n        return getString(org.odk.collect.strings.R.string.cancel_loading_form);\n    }\n\n    @Override\n    protected OnCancelCallback getOnCancelCallback() {\n        return () -> {\n            listener.onCancelFormLoading();\n            return true;\n        };\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormOpeningMode.kt",
    "content": "package org.odk.collect.android.formentry\n\nobject FormOpeningMode {\n    const val FORM_MODE_KEY = \"formMode\"\n\n    const val EDIT_SAVED = \"editSaved\"\n    const val VIEW_SENT = \"viewSent\"\n\n    @JvmStatic\n    fun isEditableMode(mode: String?): Boolean {\n        return mode == null || mode.equals(EDIT_SAVED, true)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/FormSessionRepository.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.app.Application\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.androidshared.data.getState\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.shared.strings.UUIDGenerator\n\ninterface FormSessionRepository {\n    fun create(): String\n    fun get(id: String): LiveData<FormSession>\n    fun clear(id: String)\n\n    fun set(id: String, formController: FormController, form: Form) {\n        set(id, formController, form, null)\n    }\n\n    fun set(id: String, formController: FormController, form: Form, instance: Instance?)\n\n    fun update(id: String, instance: Instance?)\n}\n\nclass AppStateFormSessionRepository(application: Application) : FormSessionRepository {\n\n    private val appState = application.getState()\n\n    override fun create(): String {\n        return UUIDGenerator().generateUUID()\n    }\n\n    override fun get(id: String): LiveData<FormSession> {\n        return getLiveData(id)\n    }\n\n    override fun set(id: String, formController: FormController, form: Form, instance: Instance?) {\n        getLiveData(id).value = FormSession(formController, form, instance)\n    }\n\n    override fun update(id: String, instance: Instance?) {\n        val liveData = getLiveData(id)\n        liveData.value?.let {\n            liveData.value = it.copy(instance = instance)\n        }\n    }\n\n    /**\n     * Ensure the object gets completely removed. Simply nullifying it might cause memory leaks.\n     * See: https://github.com/getodk/collect/issues/5777\n     */\n    override fun clear(id: String) {\n        getLiveData(id).value = null\n        appState.clear(getKey(id))\n    }\n\n    private fun getLiveData(id: String) =\n        appState.get(getKey(id), MutableLiveData<FormSession>(null))\n\n    private fun getKey(id: String) = \"$KEY_PREFIX:$id\"\n\n    companion object {\n        const val KEY_PREFIX = \"formSession\"\n    }\n}\n\ndata class FormSession @JvmOverloads constructor(\n    val formController: FormController,\n    val form: Form,\n    val instance: Instance? = null\n)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.formentry;\n\nimport static org.odk.collect.android.injection.DaggerUtils.getComponent;\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_EXTERNAL_APP_RECORDING;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.ActivityNotFoundException;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Rect;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.text.TextUtils;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.View.OnLongClickListener;\nimport android.view.ViewGroup;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.activity.ComponentActivity;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.core.widget.NestedScrollView;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.lifecycle.LifecycleOwner;\n\nimport org.javarosa.core.model.Constants;\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.IFormElement;\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.dynamicpreload.ExternalAppsUtils;\nimport org.odk.collect.android.exception.ExternalParamsException;\nimport org.odk.collect.android.exception.JavaRosaException;\nimport org.odk.collect.android.formentry.media.PromptAutoplayer;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.javarosawrapper.RepeatsInFieldListException;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.logic.ImmutableDisplayableQuestion;\nimport org.odk.collect.android.utilities.ContentUriHelper;\nimport org.odk.collect.android.utilities.ExternalAppIntentProvider;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.utilities.ThemeUtils;\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.StringWidget;\nimport org.odk.collect.android.widgets.WidgetFactory;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.android.widgets.utilities.ExternalAppRecordingRequester;\nimport org.odk.collect.android.widgets.utilities.FileRequesterImpl;\nimport org.odk.collect.android.widgets.utilities.InternalRecordingRequester;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.android.widgets.utilities.RecordingRequesterProvider;\nimport org.odk.collect.android.widgets.utilities.StringRequesterImpl;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.androidshared.system.IntentLauncher;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton;\nimport org.odk.collect.audioclips.PlaybackFailedException;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.permissions.PermissionListener;\nimport org.odk.collect.permissions.PermissionsProvider;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.timedgrid.NavigationAwareWidget;\nimport org.odk.collect.timedgrid.NavigationWarning;\n\nimport java.io.File;\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\n/**\n * Contains either one {@link QuestionWidget} if the current form element is a question or\n * multiple {@link QuestionWidget}s if the current form element is a group with the\n * {@code field-list} appearance.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class ODKView extends SwipeHandler.View implements OnLongClickListener, WidgetValueChangedListener {\n\n    private final LinearLayout widgetsList;\n    private final LinearLayout.LayoutParams layout;\n    private final ArrayList<QuestionWidget> widgets;\n    FormEntryCaption intentGroup;\n    int intentGroupStartIndex = -1;\n\n    private WidgetValueChangedListener widgetValueChangedListener;\n\n    @Inject\n    PermissionsProvider permissionsProvider;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    @Inject\n    ExternalAppIntentProvider externalAppIntentProvider;\n\n    @Inject\n    IntentLauncher intentLauncher;\n\n    private final WidgetFactory widgetFactory;\n    @NonNull\n    private List<ImmutableDisplayableQuestion> questions = new ArrayList<>();\n    private final LifecycleOwner viewLifecycle;\n    private final AudioPlayer audioPlayer;\n    private final FormController formController;\n\n    /**\n     * Builds the view for a specified question or field-list of questions.\n     * @param context         the activity creating this view\n     * @param questionPrompts the questions to be included in this view\n     * @param groups          the group hierarchy that this question or field list is in\n     * @param advancingPage   whether this view is being created after a forward swipe through the\n     */\n    public ODKView(\n            FragmentActivity context,\n            final FormEntryPrompt[] questionPrompts,\n            FormEntryCaption[] groups,\n            boolean advancingPage,\n            QuestionMediaManager questionMediaManager,\n            WaitingForDataRegistry waitingForDataRegistry,\n            AudioPlayer audioPlayer,\n            AudioRecorder audioRecorder,\n            FormEntryViewModel formEntryViewModel,\n            PrinterWidgetViewModel printerWidgetViewModel,\n            InternalRecordingRequester internalRecordingRequester,\n            ExternalAppRecordingRequester externalAppRecordingRequester,\n            LifecycleOwner viewLifecycle,\n            MediaWidgetAnswerViewModel mediaWidgetAnswerViewModel\n    ) {\n        super(context);\n        updateQuestions(questionPrompts);\n\n        this.viewLifecycle = viewLifecycle;\n        this.audioPlayer = audioPlayer;\n\n        getComponent(context).inject(this);\n        inflate(getContext(), R.layout.odk_view, this); // keep in an xml file to enable the vertical scrollbar\n\n        formController = formEntryViewModel.getFormController();\n\n        this.widgetFactory = new WidgetFactory(\n                context,\n                settingsProvider.getUnprotectedSettings().getBoolean(KEY_EXTERNAL_APP_RECORDING),\n                waitingForDataRegistry,\n                questionMediaManager,\n                audioPlayer,\n                new RecordingRequesterProvider(\n                        internalRecordingRequester,\n                        externalAppRecordingRequester\n                ),\n                formEntryViewModel,\n                printerWidgetViewModel,\n                audioRecorder,\n                this.viewLifecycle,\n                new FileRequesterImpl(intentLauncher, externalAppIntentProvider, formController),\n                new StringRequesterImpl(intentLauncher, externalAppIntentProvider, formController),\n                formController,\n                mediaWidgetAnswerViewModel\n        );\n\n        widgets = new ArrayList<>();\n        widgetsList = findViewById(R.id.widgets);\n\n        layout = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT);\n        // display which group you are in as well as the question\n        setGroupText(groups);\n\n        for (int questionIndex = 0; questionIndex < questionPrompts.length; questionIndex++) {\n            FormEntryPrompt questionPrompt = questionPrompts[questionIndex];\n            if (groups != null && groups.length > 0) {\n                configureIntentGroup(context, questionPrompt, questionIndex);\n            }\n            addWidgetForQuestion(questionPrompt);\n        }\n\n        setupAudioErrors();\n        autoplayIfNeeded(advancingPage);\n    }\n\n    private void setupAudioErrors() {\n        audioPlayer.onPlaybackError(e -> {\n            if (e instanceof PlaybackFailedException) {\n                final PlaybackFailedException playbackFailedException = (PlaybackFailedException) e;\n                Toast.makeText(\n                        getContext(),\n                        getContext().getString(playbackFailedException.getExceptionMsg() == 0 ? org.odk.collect.strings.R.string.file_missing : org.odk.collect.strings.R.string.file_invalid, playbackFailedException.getURI()),\n                        Toast.LENGTH_SHORT\n                ).show();\n            }\n        });\n    }\n\n    private void autoplayIfNeeded(boolean advancingPage) {\n        // see if there is an autoplay option.\n        // Only execute it during forward swipes through the form\n        if (advancingPage && widgets.size() == 1) {\n            FormEntryPrompt firstPrompt = widgets.get(0).getFormEntryPrompt();\n            Boolean autoplayedAudio = autoplayAudio(firstPrompt);\n\n            if (!autoplayedAudio) {\n                autoplayVideo(firstPrompt);\n            }\n\n        }\n    }\n\n    private Boolean autoplayAudio(FormEntryPrompt firstPrompt) {\n        PromptAutoplayer promptAutoplayer = new PromptAutoplayer(\n                audioPlayer,\n                ReferenceManager.instance()\n        );\n\n        return promptAutoplayer.autoplayIfNeeded(firstPrompt);\n    }\n\n    private void autoplayVideo(FormEntryPrompt prompt) {\n        final String autoplayOption = prompt.getFormElement().getAdditionalAttribute(null, \"autoplay\");\n\n        if (autoplayOption != null) {\n            if (autoplayOption.equalsIgnoreCase(\"video\")) {\n                new Handler().postDelayed(() -> {\n                    widgets.get(0).getAudioVideoImageTextLabel().playVideo();\n                }, 150);\n            }\n        }\n    }\n\n    /**\n     * Called while iterating over questions for this screen and before displaying the current\n     * question. If the intent group hasn't been identified yet, check to see if the current\n     * question's closest containing group is the intent group. If it is, place the intent launch\n     * button.\n     */\n    private void configureIntentGroup(ComponentActivity context, FormEntryPrompt questionPrompt, int questionIndex) {\n        if (intentGroup == null) { // there can only be one intent group in a field list so if it's been identified, skip this\n            FormEntryCaption[] captions = formController.getGroupsForIndex(questionPrompt.getIndex());\n\n            if (captions != null) {\n                FormEntryCaption closestParent = captions[captions.length - 1];\n\n                String intentString = closestParent.getFormElement().getAdditionalAttribute(null, \"intent\");\n\n                if (intentString != null && !intentString.isEmpty()) {\n                    intentGroup = closestParent;\n                    intentGroupStartIndex = questionIndex;\n                    try {\n                        addIntentLaunchButton(context, formController.getQuestionPrompts(closestParent.getIndex()), intentGroup, intentString, -1);\n                    } catch (RepeatsInFieldListException e) {\n                        // ignore because it would have been handled as part of building the view initially\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Creates a {@link QuestionWidget} for the given {@link FormEntryPrompt}, sets its listeners,\n     * and adds it to the end of the view. If this widget is not the first one, add a divider above\n     * it.\n     */\n    private void addWidgetForQuestion(FormEntryPrompt question) {\n        QuestionWidget qw = configureWidgetForQuestion(question);\n\n        widgets.add(qw);\n\n        if (widgets.size() > 1) {\n            widgetsList.addView(getDividerView());\n        }\n        widgetsList.addView(qw, layout);\n    }\n\n    /**\n     * Creates a {@link QuestionWidget} for the given {@link FormEntryPrompt}, sets its listeners,\n     * and adds it to the view at the specified {@code index}. If this widget is not the first one,\n     * add a divider above it. If the specified {@code index} is beyond the end of the widget list,\n     * add it to the end.\n     */\n    private void addWidgetForQuestion(FormEntryPrompt question, int index) {\n        if (index > widgets.size() - 1) {\n            addWidgetForQuestion(question);\n            return;\n        }\n\n        QuestionWidget qw = configureWidgetForQuestion(question);\n\n        widgets.add(index, qw);\n\n        int indexAccountingForDividers = index * 2;\n\n        if (intentGroup != null) {\n            if (intentGroupStartIndex == -1) {\n                if (isInIntentGroup(question)) {\n                    // find the first question in the intent group to add the launch button there\n                    for (int i = 0; i < widgets.size(); i++) {\n                        if (isInIntentGroup(widgets.get(i).getFormEntryPrompt())) {\n                            String intentString = intentGroup.getFormElement().getAdditionalAttribute(null, \"intent\");\n                            try {\n                                addIntentLaunchButton(getContext(), formController.getQuestionPrompts(intentGroup.getIndex()), intentGroup, intentString, i * 2 - 1);\n                            } catch (RepeatsInFieldListException e) {\n                                // ignore because it would have been handled as part of building the view initially\n                            }\n                            intentGroupStartIndex = i;\n                            break;\n                        }\n                    }\n                    indexAccountingForDividers += 1;\n                }\n            } else if (index > intentGroupStartIndex) {\n                indexAccountingForDividers += 1;\n            } else if (index < intentGroupStartIndex) {\n                intentGroupStartIndex += 1;\n            }\n        }\n\n        if (index > 0) {\n            widgetsList.addView(getDividerView(), indexAccountingForDividers - 1);\n        }\n\n        widgetsList.addView(qw, indexAccountingForDividers, layout);\n    }\n\n    private boolean isInIntentGroup(FormEntryPrompt question) {\n        FormEntryCaption[] groupsForQuestion = formController.getGroupsForIndex(question.getIndex());\n        if (groupsForQuestion != null) {\n            FormEntryCaption closestParent = groupsForQuestion[groupsForQuestion.length - 1];\n            return closestParent.getIndex().equals(intentGroup.getIndex());\n        } else {\n            return false;\n        }\n    }\n\n    /**\n     * Creates and configures a {@link QuestionWidget} for the given {@link FormEntryPrompt}.\n     * <p>\n     * Note: if the given question is of an unsupported type, a text widget will be created.\n     */\n    private QuestionWidget configureWidgetForQuestion(FormEntryPrompt question) {\n        boolean forceReadOnly = false;\n\n        if (intentGroup != null) {\n            forceReadOnly = isInIntentGroup(question);\n        }\n\n        QuestionWidget qw = widgetFactory.createWidgetFromPrompt(question, permissionsProvider, forceReadOnly);\n        qw.setOnLongClickListener(this);\n        qw.setValueChangedListener(this);\n\n        return qw;\n    }\n\n    private View getDividerView() {\n        View divider = new View(getContext());\n        divider.setBackgroundResource(new ThemeUtils(getContext()).getDivider());\n        divider.setMinimumHeight(3);\n\n        ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(\n                ViewGroup.LayoutParams.WRAP_CONTENT,\n                ViewGroup.LayoutParams.MATCH_PARENT\n        );\n        int marginVertical = (int) getContext().getResources().getDimension(org.odk.collect.androidshared.R.dimen.margin_extra_small);\n        params.setMargins(0, marginVertical, 0, marginVertical);\n        divider.setLayoutParams(params);\n\n        return divider;\n    }\n\n    /**\n     * @return a HashMap of answers entered by the user for this set of widgets\n     */\n    public HashMap<FormIndex, IAnswerData> getAnswers() {\n        HashMap<FormIndex, IAnswerData> answers = new LinkedHashMap<>();\n        for (QuestionWidget q : widgets) {\n            /*\n             * The FormEntryPrompt has the FormIndex, which is where the answer gets stored. The\n             * QuestionWidget has the answer the user has entered.\n             */\n            FormEntryPrompt p = q.getFormEntryPrompt();\n            answers.put(p.getIndex(), q.getAnswer());\n        }\n\n        return answers;\n    }\n\n    /**\n     * Add a TextView containing the hierarchy of groups to which the question belongs.\n     */\n    private void setGroupText(FormEntryCaption[] groups) {\n        CharSequence path = getGroupsPath(groups);\n\n        if (path.length() > 0) {\n            TextView tv = findViewById(R.id.group_text);\n            tv.setText(path);\n\n            int fontSize = QuestionFontSizeUtils.getFontSize(settingsProvider.getUnprotectedSettings(), QuestionFontSizeUtils.FontSize.SUBTITLE_1);\n            tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);\n\n            tv.setVisibility(VISIBLE);\n        }\n    }\n\n    /**\n     * @see #getGroupsPath(FormEntryCaption[], boolean)\n     */\n    @NonNull\n    public static CharSequence getGroupsPath(FormEntryCaption[] groups) {\n        return getGroupsPath(groups, false);\n    }\n\n    /**\n     * Builds a string representing the 'path' of the list of groups.\n     * Each level is separated by `>`.\n     * <p>\n     * Some views (e.g. the repeat picker) may want to hide the multiplicity of the last item,\n     * i.e. show `Friends` instead of `Friends > 1`.\n     */\n    @NonNull\n    public static CharSequence getGroupsPath(FormEntryCaption[] groups, boolean hideLastMultiplicity) {\n        if (groups == null) {\n            return \"\";\n        }\n\n        List<String> segments = new ArrayList<>();\n        int index = 1;\n        for (FormEntryCaption group : groups) {\n            String text = group.getLongText();\n\n            if (text != null) {\n                segments.add(text);\n\n                boolean isMultiplicityAllowed = !(hideLastMultiplicity && index == groups.length);\n                if (group.repeats() && isMultiplicityAllowed) {\n                    segments.add(Integer.toString(group.getMultiplicity() + 1));\n                }\n            }\n\n            index++;\n        }\n\n        return HtmlUtils.textToHtml(TextUtils.join(\" > \", segments));\n    }\n\n    /**\n     * Adds a button to launch an intent at the end of the widgets list. Should be called at the start\n     * of the intent group for the current field list.\n     */\n    private void addIntentLaunchButton(Context context, FormEntryPrompt[] questionPrompts,\n                                       FormEntryCaption c, String intentString, int viewIndex) {\n        String v = c.getSpecialFormQuestionText(\"buttonText\");\n        final String buttonText = (v != null) ? v : context.getString(org.odk.collect.strings.R.string.launch_app);\n\n        View buttonLayout = LayoutInflater.from(context).inflate(R.layout.launch_intent_button_layout, widgetsList, false);\n        MultiClickSafeMaterialButton launchIntentButton = buttonLayout.findViewById(R.id.launch_intent);\n\n        launchIntentButton.setText(buttonText);\n        launchIntentButton.setTextSize(QuestionFontSizeUtils.getFontSize(settingsProvider.getUnprotectedSettings(), QuestionFontSizeUtils.FontSize.BODY_LARGE));\n\n        if (viewIndex == -1) {\n            widgetsList.addView(buttonLayout);\n        } else {\n            widgetsList.addView(buttonLayout, viewIndex);\n        }\n\n        launchIntentButton.setOnClickListener(view -> {\n            String intentName = ExternalAppsUtils.extractIntentName(intentString);\n            Map<String, String> parameters = ExternalAppsUtils.extractParameters(intentString);\n\n            Intent i = new Intent(intentName);\n            if (i.resolveActivity(Collect.getInstance().getPackageManager()) == null) {\n                Intent launchIntent = Collect.getInstance().getPackageManager().getLaunchIntentForPackage(intentName);\n\n                if (launchIntent != null) {\n                    // Make sure FLAG_ACTIVITY_NEW_TASK is not set because it doesn't work with startActivityForResult\n                    launchIntent.setFlags(0);\n                    i = launchIntent;\n                }\n            }\n\n            try {\n                ExternalAppsUtils.populateParameters(i, parameters,\n                        c.getIndex().getReference(), formController);\n\n                for (FormEntryPrompt p : questionPrompts) {\n                    IFormElement formElement = p.getFormElement();\n                    if (formElement instanceof QuestionDef) {\n                        TreeReference reference =\n                                (TreeReference) formElement.getBind().getReference();\n                        IAnswerData answerValue = p.getAnswerValue();\n                        Object value =\n                                answerValue == null ? null : answerValue.getValue();\n                        switch (p.getDataType()) {\n                            case Constants.DATATYPE_TEXT:\n                            case Constants.DATATYPE_INTEGER:\n                            case Constants.DATATYPE_DECIMAL:\n                            case Constants.DATATYPE_BINARY:\n                                i.putExtra(reference.getNameLast(),\n                                        (Serializable) value);\n                                break;\n                        }\n                    }\n                }\n\n                ((Activity) getContext()).startActivityForResult(i, RequestCodes.EX_GROUP_CAPTURE);\n            } catch (ExternalParamsException e) {\n                Timber.e(e, \"ExternalParamsException\");\n\n                ToastUtils.showShortToast(e.getMessage());\n            } catch (ActivityNotFoundException e) {\n                Timber.d(e, \"ActivityNotFoundExcept\");\n\n                String formErrorText = c.getSpecialFormQuestionText(\"noAppErrorString\");\n                final String errorString = (formErrorText != null) ? formErrorText : context.getString(org.odk.collect.strings.R.string.no_app);\n                ToastUtils.showShortToast(errorString);\n            }\n        });\n    }\n\n    public void setFocus(Context context) {\n        if (!widgets.isEmpty()) {\n            widgets.get(0).setFocus(context);\n        }\n    }\n\n    /**\n     * Returns true if any part of the question widget is currently on the screen or false otherwise.\n     */\n    public boolean isDisplayed(QuestionWidget qw) {\n        Rect scrollBounds = new Rect();\n        findViewById(R.id.odk_view_container).getHitRect(scrollBounds);\n        return qw.getLocalVisibleRect(scrollBounds);\n    }\n\n    public void focusToTopOf(FormIndex index) {\n        for (QuestionWidget widget : widgets) {\n            if (widget.getFormEntryPrompt().getIndex().equals(index)) {\n                focusToTopOf(widget);\n                break;\n            }\n        }\n    }\n\n    public void focusToTopOf(@Nullable QuestionWidget qw) {\n        if (qw != null && widgets.contains(qw)) {\n            findViewById(R.id.odk_view_container).scrollTo(0, qw.getTop());\n            qw.setFocus(getContext());\n        }\n    }\n\n    /**\n     * Saves answers for the widgets in this view. Called when the widgets are in an intent group.\n     */\n    public void setDataForFields(Bundle bundle) throws JavaRosaException {\n        if (bundle != null) {\n            Set<String> keys = bundle.keySet();\n            for (String key : keys) {\n                Object answer = bundle.get(key);\n                if (answer == null) {\n                    continue;\n                }\n                for (QuestionWidget questionWidget : widgets) {\n                    FormEntryPrompt prompt = questionWidget.getFormEntryPrompt();\n                    TreeReference treeReference =\n                            (TreeReference) prompt.getFormElement().getBind().getReference();\n\n                    if (treeReference.getNameLast().equals(key)) {\n                        switch (prompt.getDataType()) {\n                            case Constants.DATATYPE_TEXT:\n                            case Constants.DATATYPE_INTEGER:\n                            case Constants.DATATYPE_DECIMAL:\n                                ((StringWidget) questionWidget).widgetAnswerText.setAnswer(answer.toString());\n                                questionWidget.showAnswerContainer();\n                                widgetValueChanged(questionWidget);\n                                break;\n                            case Constants.DATATYPE_BINARY:\n                                try {\n                                    Uri uri;\n                                    if (answer instanceof Uri) {\n                                        uri = (Uri) answer;\n                                    } else if (answer instanceof String) {\n                                        uri = Uri.parse(bundle.getString(key));\n                                    } else {\n                                        throw new RuntimeException(\"The value for \" + key + \" must be a URI but it is \" + answer);\n                                    }\n\n                                    permissionsProvider.requestReadUriPermission((Activity) getContext(), uri, getContext().getContentResolver(), new PermissionListener() {\n                                        @Override\n                                        public void granted() {\n                                            File destFile = FileUtils.createDestinationMediaFile(formController.getInstanceFile().getParent(), ContentUriHelper.getFileExtensionFromUri(uri));\n                                            //TODO might be better to use QuestionMediaManager in the future\n                                            FileUtils.saveAnswerFileFromUri(uri, destFile, getContext());\n                                            ((WidgetDataReceiver) questionWidget).setData(destFile);\n\n                                            questionWidget.showAnswerContainer();\n                                            widgetValueChanged(questionWidget);\n                                        }\n                                    });\n                                } catch (Exception | Error e) {\n                                    Timber.w(e);\n                                }\n                                break;\n                            default:\n                                throw new RuntimeException(\n                                        getContext().getString(org.odk.collect.strings.R.string.ext_assign_value_error,\n                                                treeReference.toString(false)));\n                        }\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    public boolean shouldSuppressFlingGesture() {\n        for (QuestionWidget q : widgets) {\n            if (q.shouldSuppressFlingGesture()) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Nullable\n    @Override\n    public NestedScrollView verticalScrollView() {\n        return findViewById(R.id.odk_view_container);\n    }\n\n    /**\n     * @return true if the answer was cleared, false otherwise.\n     */\n    public boolean clearAnswer() {\n        // If there's only one widget, clear the answer.\n        // If there are more, then force a long-press to clear the answer.\n        if (widgets.size() == 1 && !widgets.get(0).getFormEntryPrompt().isReadOnly()) {\n            widgets.get(0).clearAnswer();\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    public ArrayList<QuestionWidget> getWidgets() {\n        return widgets;\n    }\n\n    @Override\n    public void setOnFocusChangeListener(OnFocusChangeListener l) {\n        for (int i = 0; i < widgets.size(); i++) {\n            QuestionWidget qw = widgets.get(i);\n            qw.setOnFocusChangeListener(l);\n        }\n    }\n\n    @Override\n    public boolean onLongClick(View v) {\n        return false;\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        for (QuestionWidget qw : widgets) {\n            qw.cancelLongPress();\n        }\n    }\n\n    public void setErrorForQuestionWithIndex(FormIndex formIndex, String errorMessage) {\n        for (QuestionWidget questionWidget : getWidgets()) {\n            if (formIndex.equals(questionWidget.getFormEntryPrompt().getIndex())) {\n                questionWidget.displayError(errorMessage);\n                focusToTopOf(questionWidget);\n            } else {\n                questionWidget.hideError();\n            }\n        }\n    }\n\n    /**\n     * Removes the widget and corresponding divider at a particular index.\n     */\n    private void removeWidgetAt(int index) {\n        int indexAccountingForDividers = index * 2;\n        int intentGroupStartIndexWithDividers = intentGroupStartIndex * 2;\n\n        // There may be a first TextView to display the group path. See addGroupText(FormEntryCaption[])\n        if (widgetsList.getChildCount() > 0 && widgetsList.getChildAt(0) instanceof TextView) {\n            indexAccountingForDividers += 1;\n            intentGroupStartIndexWithDividers += 1;\n        }\n\n        // There may be an app launch button for an intent group\n        if (intentGroupStartIndex != -1) {\n            // There must be at least one field in an intent group and relevance must be applied at\n            // the group level so remove the button if the field immediately after it is removed\n            if (index == intentGroupStartIndex) {\n                widgetsList.removeViewAt(intentGroupStartIndexWithDividers);\n                intentGroupStartIndex = -1;\n            } else if (index > intentGroupStartIndex) {\n                indexAccountingForDividers += 1;\n            } else {\n                intentGroupStartIndex -= 1;\n            }\n        }\n\n        widgetsList.removeViewAt(indexAccountingForDividers);\n\n        if (index > 0) {\n            widgetsList.removeViewAt(indexAccountingForDividers - 1);\n        }\n\n        widgets.remove(index);\n    }\n\n    public void setWidgetValueChangedListener(WidgetValueChangedListener listener) {\n        widgetValueChangedListener = listener;\n    }\n\n    public void widgetValueChanged(QuestionWidget changedWidget) {\n        if (widgetValueChangedListener != null) {\n            widgetValueChangedListener.widgetValueChanged(changedWidget);\n        }\n    }\n\n    public void onUpdated(FormIndex lastChangedIndex, FormEntryPrompt[] questionsAfterSave) {\n        Map<FormIndex, FormEntryPrompt> questionsAfterSaveByIndex = new HashMap<>();\n        for (FormEntryPrompt question : questionsAfterSave) {\n            questionsAfterSaveByIndex.put(question.getIndex(), question);\n        }\n\n        // Identify widgets to remove or rebuild (by removing and re-adding). We'd like to do the\n        // identification and removal in the same pass but removal has to be done in a loop that\n        // starts from the end and itemset-based select choices will only be correctly recomputed\n        // if accessed from beginning to end because the call on sameAs is what calls\n        // populateDynamicChoices. See https://github.com/getodk/javarosa/issues/436\n        List<FormEntryPrompt> questionsThatHaveNotChanged = new ArrayList<>();\n        List<FormIndex> formIndexesToRemove = new ArrayList<>();\n        for (ImmutableDisplayableQuestion questionBeforeSave : questions) {\n            FormEntryPrompt questionAtSameFormIndex = questionsAfterSaveByIndex.get(questionBeforeSave.getFormIndex());\n\n            // Always rebuild questions that use database-driven external data features since they\n            // bypass SelectChoices stored in ImmutableDisplayableQuestion\n            if (questionBeforeSave.sameAs(questionAtSameFormIndex)\n                    && !formController.usesDatabaseExternalDataFeature(questionBeforeSave.getFormIndex())) {\n                questionsThatHaveNotChanged.add(questionAtSameFormIndex);\n            } else if (!lastChangedIndex.equals(questionBeforeSave.getFormIndex())) {\n                formIndexesToRemove.add(questionBeforeSave.getFormIndex());\n            }\n        }\n\n        for (int i = questions.size() - 1; i >= 0; i--) {\n            ImmutableDisplayableQuestion questionBeforeSave = questions.get(i);\n\n            if (formIndexesToRemove.contains(questionBeforeSave.getFormIndex())) {\n                removeWidgetAt(i);\n            }\n        }\n\n        for (int i = 0; i < questionsAfterSave.length; i++) {\n            if (!questionsThatHaveNotChanged.contains(questionsAfterSave[i])\n                    && !questionsAfterSave[i].getIndex().equals(lastChangedIndex)) {\n                addWidgetForQuestion(questionsAfterSave[i], i);\n            }\n        }\n\n        updateQuestions(questionsAfterSave);\n    }\n\n    private void updateQuestions(FormEntryPrompt[] prompts) {\n        questions.clear();\n        for (FormEntryPrompt questionAfterSave : prompts) {\n            this.questions.add(new ImmutableDisplayableQuestion(questionAfterSave));\n        }\n    }\n\n    @Nullable\n    public NavigationWarning isNavigationBlocked() {\n        return widgets.stream()\n                .filter(widget -> widget instanceof NavigationAwareWidget)\n                .map(widget -> (NavigationAwareWidget) widget)\n                .map(NavigationAwareWidget::shouldBlockNavigation)\n                .filter(Objects::nonNull)\n                .findFirst()\n                .orElse(null);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/PrinterWidgetViewModel.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.util.Base64\nimport androidx.lifecycle.ViewModel\nimport org.jsoup.Jsoup\nimport org.jsoup.nodes.Document\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.android.widgets.interfaces.Printer\nimport org.odk.collect.androidshared.async.TrackableWorker\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.printer.HtmlPrinter\nimport org.odk.collect.qrcode.zxing.QRCodeCreator\nimport java.io.ByteArrayOutputStream\n\nclass PrinterWidgetViewModel(\n    private val scheduler: Scheduler,\n    private val qrCodeCreator: QRCodeCreator,\n    private val htmlPrinter: HtmlPrinter\n) : ViewModel(), Printer {\n    private val trackableWorker = TrackableWorker(scheduler)\n\n    @Override\n    override fun parseAndPrint(\n        htmlDocument: String,\n        questionMediaManager: QuestionMediaManager,\n        context: Context\n    ) {\n        trackableWorker.immediate(\n            background = {\n                val document = Jsoup.parse(htmlDocument)\n\n                parseImages(document, questionMediaManager)\n                parseQRCodes(document, qrCodeCreator)\n\n                document.html()\n            },\n            foreground = { content ->\n                htmlPrinter.print(context, content)\n            }\n        )\n    }\n\n    override fun isLoading(): NonNullLiveData<Boolean> {\n        return trackableWorker.isWorking\n    }\n\n    private fun parseImages(document: Document, questionMediaManager: QuestionMediaManager) {\n        for (imgElement in document.getElementsByTag(\"img\")) {\n            val file = questionMediaManager.getAnswerFile(imgElement.attributes().get(\"src\"))\n            if (file != null && file.exists()) {\n                imgElement.attr(\"src\", \"file://${file.absolutePath}\")\n            }\n        }\n    }\n\n    private fun parseQRCodes(document: Document, qrCodeCreator: QRCodeCreator) {\n        for (qrcodeElement in document.getElementsByTag(\"qrcode\")) {\n            val newElement = document.createElement(\"img\").apply {\n                attributes().addAll(qrcodeElement.attributes())\n                val qrCodeData = bitmapToBase64(qrCodeCreator.create(qrcodeElement.text()))\n                attr(\"src\", \"data:image/png;base64,$qrCodeData\")\n            }\n            qrcodeElement.replaceWith(newElement)\n        }\n    }\n\n    private fun bitmapToBase64(bitmap: Bitmap): String {\n        val byteArrayOutputStream = ByteArrayOutputStream()\n        bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)\n        val byteArray = byteArrayOutputStream.toByteArray()\n\n        return Base64.encodeToString(byteArray, Base64.DEFAULT)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/QuitFormDialog.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.app.Activity\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.view.isVisible\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.android.R\nimport org.odk.collect.android.databinding.QuitFormDialogLayoutBinding\nimport org.odk.collect.android.formentry.saving.FormSaveViewModel\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport java.text.SimpleDateFormat\nimport java.util.Locale\n\nobject QuitFormDialog {\n\n    @JvmStatic\n    fun show(\n        activity: Activity,\n        formSaveViewModel: FormSaveViewModel,\n        formEntryViewModel: FormEntryViewModel,\n        settingsProvider: SettingsProvider,\n        onSaveChangesClicked: Runnable?\n    ): AlertDialog {\n        return create(\n            activity,\n            formSaveViewModel,\n            formEntryViewModel,\n            settingsProvider,\n            onSaveChangesClicked\n        ).also {\n            it.show()\n        }\n    }\n\n    private fun create(\n        activity: Activity,\n        formSaveViewModel: FormSaveViewModel,\n        formEntryViewModel: FormEntryViewModel,\n        settingsProvider: SettingsProvider,\n        onSaveChangesClicked: Runnable?\n    ): AlertDialog {\n        val saveAsDraft = settingsProvider.getProtectedSettings()\n            .getBoolean(ProtectedProjectKeys.KEY_SAVE_MID)\n        val canBeFullyDiscarded = formSaveViewModel.canBeFullyDiscarded()\n\n        val binding = QuitFormDialogLayoutBinding.inflate(activity.layoutInflater)\n        val dialog = MaterialAlertDialogBuilder(activity)\n            .setTitle(\n                if (saveAsDraft) {\n                    org.odk.collect.strings.R.string.quit_form_title\n                } else {\n                    org.odk.collect.strings.R.string.quit_form_continue_title\n                }\n            )\n            .setView(binding.root)\n            .create()\n\n        binding.saveExplanation.text = if (!saveAsDraft) {\n            if (canBeFullyDiscarded) {\n                activity.getString(org.odk.collect.strings.R.string.discard_form_warning)\n            } else {\n                val string = activity.getString(org.odk.collect.strings.R.string.discard_changes_warning)\n                SimpleDateFormat(string, Locale.getDefault()).format(formSaveViewModel.lastSavedTime)\n            }\n        } else if (canBeFullyDiscarded) {\n            activity.getString(org.odk.collect.strings.R.string.save_explanation)\n        } else {\n            val string = activity.getString(org.odk.collect.strings.R.string.save_explanation_with_last_saved)\n            SimpleDateFormat(string, Locale.getDefault()).format(formSaveViewModel.lastSavedTime)\n        }\n\n        binding.discardChanges.setText(\n            if (canBeFullyDiscarded) {\n                org.odk.collect.strings.R.string.do_not_save\n            } else {\n                org.odk.collect.strings.R.string.discard_changes\n            }\n        )\n\n        binding.discardChanges.setOnClickListener {\n            formSaveViewModel.ignoreChanges()\n            formEntryViewModel.exit()\n            activity.finish()\n            dialog.dismiss()\n        }\n\n        binding.keepEditingOutlined.isVisible = saveAsDraft\n        binding.keepEditingFilled.isVisible = !saveAsDraft\n\n        binding.keepEditingOutlined.setOnClickListener {\n            dialog.dismiss()\n        }\n\n        binding.keepEditingFilled.setOnClickListener {\n            dialog.dismiss()\n        }\n\n        binding.saveChanges.isVisible = saveAsDraft\n        binding.saveChanges.setOnClickListener {\n            onSaveChangesClicked?.run()\n        }\n\n        return dialog\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/RecordingHandler.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport androidx.lifecycle.LifecycleOwner;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.odk.collect.android.audio.AudioFileAppender;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.recording.RecordingSession;\nimport org.odk.collect.utilities.Result;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.HashSet;\nimport java.util.function.Consumer;\n\nimport timber.log.Timber;\n\npublic class RecordingHandler {\n\n    private final QuestionMediaManager questionMediaManager;\n    private final LifecycleOwner lifecycleOwner;\n    private final AudioRecorder audioRecorder;\n    private final AudioFileAppender amrAppender;\n    private final AudioFileAppender m4aAppender;\n\n    public RecordingHandler(QuestionMediaManager questionMediaManager, LifecycleOwner lifecycleOwner, AudioRecorder audioRecorder, AudioFileAppender amrAppender, AudioFileAppender m4aAppender) {\n        this.questionMediaManager = questionMediaManager;\n        this.lifecycleOwner = lifecycleOwner;\n        this.audioRecorder = audioRecorder;\n        this.amrAppender = amrAppender;\n        this.m4aAppender = m4aAppender;\n    }\n\n    public void handle(FormController formController, RecordingSession session, Consumer<File> onRecordingHandled) {\n        questionMediaManager.createAnswerFile(session.getFile()).observe(lifecycleOwner, result -> {\n            if (result != null && result.isSuccess()) {\n                try {\n                    if (session.getId() instanceof HashSet) {\n                        handleBackgroundRecording(formController, session, result);\n                    }\n\n                    onRecordingHandled.accept(result.getOrNull());\n                } catch (IOException e) {\n                    Timber.e(e);\n                    onRecordingHandled.accept(null);\n                }\n                audioRecorder.cleanUp();\n            }\n        });\n    }\n\n    private void handleBackgroundRecording(FormController formController, RecordingSession session, Result<File> result) throws IOException {\n        HashSet<TreeReference> treeReferences = (HashSet<TreeReference>) session.getId();\n\n        TreeReference firstReference = treeReferences.iterator().next();\n        StringData answer = (StringData) formController.getAnswer(firstReference);\n\n        if (answer != null) {\n            File existingAnswerFile = questionMediaManager.getAnswerFile((String) answer.getValue());\n            if (existingAnswerFile != null && existingAnswerFile.exists()) {\n                File newAnswerFile = result.getOrNull();\n\n                if (newAnswerFile.getName().endsWith(\".m4a\")) {\n                    m4aAppender.append(existingAnswerFile, newAnswerFile);\n                } else {\n                    amrAppender.append(existingAnswerFile, newAnswerFile);\n                }\n\n                newAnswerFile.delete();\n            } else {\n                for (TreeReference treeReference : treeReferences) {\n                    formController.getFormDef().setValue(new StringData(result.getOrNull().getName()), treeReference, false);\n                }\n            }\n        } else {\n            for (TreeReference treeReference : treeReferences) {\n                formController.getFormDef().setValue(new StringData(result.getOrNull().getName()), treeReference, false);\n            }\n        }\n\n        session.getFile().delete();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/RecordingWarningDialogFragment.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport android.app.Dialog;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\npublic class RecordingWarningDialogFragment extends DialogFragment {\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        return new MaterialAlertDialogBuilder(requireActivity())\n                .setTitle(org.odk.collect.strings.R.string.recording)\n                .setMessage(org.odk.collect.strings.R.string.recording_warning)\n                .setPositiveButton(org.odk.collect.strings.R.string.ok, null)\n                .create();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/RefreshFormListDialogFragment.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport android.content.Context;\n\nimport androidx.annotation.NonNull;\n\nimport org.odk.collect.material.MaterialProgressDialogFragment;\n\npublic class RefreshFormListDialogFragment extends MaterialProgressDialogFragment {\n\n    protected RefreshFormListDialogFragmentListener listener;\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n\n        if (context instanceof RefreshFormListDialogFragmentListener) {\n            listener = (RefreshFormListDialogFragmentListener) context;\n        }\n        setTitle(getString(org.odk.collect.strings.R.string.downloading_data));\n        setMessage(getString(org.odk.collect.strings.R.string.please_wait));\n        setCancelable(false);\n    }\n\n    @Override\n    protected String getCancelButtonText() {\n        return getString(org.odk.collect.strings.R.string.cancel_loading_form);\n    }\n\n    @Override\n    protected OnCancelCallback getOnCancelCallback() {\n        return () -> {\n            listener.onCancelFormLoading();\n            dismiss();\n            return true;\n        };\n    }\n\n    public interface RefreshFormListDialogFragmentListener {\n            void onCancelFormLoading();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/SwipeHandler.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.content.Context\nimport android.view.GestureDetector\nimport android.view.MotionEvent\nimport android.widget.FrameLayout\nimport androidx.core.widget.NestedScrollView\nimport org.odk.collect.android.utilities.ActionRegister\nimport org.odk.collect.androidshared.utils.ScreenUtils\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\nimport org.odk.collect.strings.localization.isLTR\nimport kotlin.math.abs\nimport kotlin.math.atan2\n\nclass SwipeHandler(context: Context, generalSettings: Settings) {\n    val gestureDetector: GestureDetector\n    private val onSwipe: OnSwipeListener\n    private var view: View? = null\n    private var allowSwiping = true\n    private var beenSwiped = false\n    private val generalSettings: Settings\n\n    interface OnSwipeListener {\n        fun onSwipeBackward()\n        fun onSwipeForward()\n    }\n\n    init {\n        gestureDetector = GestureDetector(context, GestureListener())\n        onSwipe = context as OnSwipeListener\n        this.generalSettings = generalSettings\n    }\n\n    fun setView(view: View?) {\n        this.view = view\n    }\n\n    fun setAllowSwiping(allowSwiping: Boolean) {\n        this.allowSwiping = allowSwiping\n    }\n\n    fun setBeenSwiped(beenSwiped: Boolean) {\n        this.beenSwiped = beenSwiped\n    }\n\n    fun beenSwiped() = beenSwiped\n\n    inner class GestureListener : GestureDetector.OnGestureListener {\n        override fun onDown(event: MotionEvent) = false\n        override fun onSingleTapUp(e: MotionEvent) = false\n\n        override fun onShowPress(e: MotionEvent) = Unit\n        override fun onLongPress(e: MotionEvent) = Unit\n\n        override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {\n            // The onFling() captures the 'up' event so our view thinks it gets long pressed. We don't want that, so cancel it.\n            view?.cancelLongPress()\n            return false\n        }\n\n        override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {\n            if (view == null) {\n                return false\n            }\n\n            ActionRegister.actionDetected()\n\n            if (e1 != null && generalSettings.getString(ProjectKeys.KEY_NAVIGATION)!!.contains(ProjectKeys.NAVIGATION_SWIPE) && allowSwiping) {\n                // Looks for user swipes. If the user has swiped, move to the appropriate screen.\n\n                // For all screens a swipe is left/right of at least .25\" and up/down of less than .25\" OR left/right of > .5\"\n                val xpixellimit = (ScreenUtils.xdpi(view!!.context) * .25).toInt()\n                val ypixellimit = (ScreenUtils.ydpi(view!!.context) * .25).toInt()\n\n                if (view != null && view!!.shouldSuppressFlingGesture()) {\n                    return false\n                }\n\n                if (beenSwiped) {\n                    return false\n                }\n\n                val diffX = abs(e1.x - e2.x)\n                val diffY = abs(e1.y - e2.y)\n\n                if (view != null && canScrollVertically() && getGestureAngle(diffX, diffY) > 30) {\n                    return false\n                }\n\n                if (diffX > xpixellimit && diffY < ypixellimit || diffX > xpixellimit * 2) {\n                    beenSwiped = true\n                    if (e1.x > e2.x) {\n                        if (view!!.context.isLTR()) {\n                            onSwipe.onSwipeForward()\n                        } else {\n                            onSwipe.onSwipeBackward()\n                        }\n                    } else {\n                        if (view!!.context.isLTR()) {\n                            onSwipe.onSwipeBackward()\n                        } else {\n                            onSwipe.onSwipeForward()\n                        }\n                    }\n                    return true\n                }\n            }\n            return false\n        }\n\n        private fun getGestureAngle(diffX: Float, diffY: Float): Double {\n            return Math.toDegrees(atan2(diffY.toDouble(), diffX.toDouble()))\n        }\n\n        private fun canScrollVertically(): Boolean {\n            val scrollView = view!!.verticalScrollView()\n\n            return if (scrollView != null) {\n                val screenHeight = scrollView.height\n                val viewHeight = scrollView.getChildAt(0).height\n                viewHeight > screenHeight\n            } else {\n                false\n            }\n        }\n    }\n\n    abstract class View(context: Context) : FrameLayout(context) {\n        abstract fun shouldSuppressFlingGesture(): Boolean\n        abstract fun verticalScrollView(): NestedScrollView?\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/audit/AsyncTaskAuditEventWriter.java",
    "content": "package org.odk.collect.android.formentry.audit;\n\nimport android.os.AsyncTask;\n\nimport androidx.annotation.NonNull;\n\nimport java.io.File;\nimport java.util.List;\n\npublic class AsyncTaskAuditEventWriter implements AuditEventLogger.AuditEventWriter {\n\n    private static AsyncTask saveTask;\n    private final File file;\n    private final boolean isLocationEnabled;\n    private final boolean isTrackingChangesEnabled;\n    private final boolean isUserRequired;\n    private final boolean isTrackChangesReasonEnabled;\n\n    public AsyncTaskAuditEventWriter(@NonNull File file, boolean isLocationEnabled, boolean isTrackingChangesEnabled, boolean isUserRequired, boolean isTrackChangesReasonEnabled) {\n        this.file = file;\n        this.isLocationEnabled = isLocationEnabled;\n        this.isTrackingChangesEnabled = isTrackingChangesEnabled;\n        this.isUserRequired = isUserRequired;\n        this.isTrackChangesReasonEnabled = isTrackChangesReasonEnabled;\n    }\n\n    @Override\n    public void writeEvents(List<AuditEvent> auditEvents) {\n        AuditEvent[] auditEventArray = auditEvents.toArray(new AuditEvent[0]);\n        saveTask = new AuditEventSaveTask(file, isLocationEnabled, isTrackingChangesEnabled, isUserRequired, isTrackChangesReasonEnabled).execute(auditEventArray);\n    }\n\n    @Override\n    public boolean isWriting() {\n        return saveTask != null && saveTask.getStatus() != AsyncTask.Status.FINISHED;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/audit/AuditConfig.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.formentry.audit;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport org.odk.collect.location.LocationClient;\n\nimport java.util.Locale;\n\n/**\n * This class is responsible for storing the current audit configuration, which contains three\n * parameters: locationPriority, locationMinInterval and locationMaxAge.\n */\npublic class AuditConfig {\n\n    private static final long MIN_ALLOWED_LOCATION_MIN_INTERVAL = 1000;\n\n    /**\n     * The locationPriority of location requests\n     */\n    private final LocationClient.Priority locationPriority;\n\n    /**\n     * The desired minimum interval in milliseconds that the location will be fetched\n     */\n    private final Long locationMinInterval;\n\n    /**\n     * The time in milliseconds that location will be valid\n     */\n    private final Long locationMaxAge;\n\n    /**\n     * True if new answers should be added in the audit file\n     */\n    private final boolean isTrackingChangesEnabled;\n\n    private final boolean isIdentifyUserEnabled;\n    private final boolean isTrackChangesReasonEnabled;\n\n    public AuditConfig(String mode, String locationMinInterval, String locationMaxAge, boolean isTrackingChangesEnabled, boolean isIdentifyUserEnabled, boolean isTrackChangesReasonEnabled) {\n        this.locationPriority = mode != null ? getMode(mode) : null;\n        this.locationMinInterval = locationMinInterval != null ? Long.parseLong(locationMinInterval) * 1000 : null;\n        this.locationMaxAge = locationMaxAge != null ? Long.parseLong(locationMaxAge) * 1000 : null;\n        this.isTrackingChangesEnabled = isTrackingChangesEnabled;\n        this.isIdentifyUserEnabled = isIdentifyUserEnabled;\n        this.isTrackChangesReasonEnabled = isTrackChangesReasonEnabled;\n    }\n\n    private LocationClient.Priority getMode(@NonNull String mode) {\n        switch (mode.toLowerCase(Locale.US)) {\n            case \"balanced\":\n                return LocationClient.Priority.PRIORITY_BALANCED_POWER_ACCURACY;\n            case \"low_power\":\n            case \"low-power\":\n                return LocationClient.Priority.PRIORITY_LOW_POWER;\n            case \"no_power\":\n            case \"no-power\":\n                return LocationClient.Priority.PRIORITY_NO_POWER;\n            default:\n                return LocationClient.Priority.PRIORITY_HIGH_ACCURACY;\n        }\n    }\n\n    @Nullable\n    public LocationClient.Priority getLocationPriority() {\n        return locationPriority;\n    }\n\n    @Nullable\n    public Long getLocationMinInterval() {\n        return locationMinInterval == null\n                ? null\n                : locationMinInterval > MIN_ALLOWED_LOCATION_MIN_INTERVAL\n                    ? locationMinInterval\n                    : MIN_ALLOWED_LOCATION_MIN_INTERVAL;\n    }\n\n    @Nullable\n    public Long getLocationMaxAge() {\n        return locationMaxAge;\n    }\n\n    public boolean isLocationEnabled() {\n        return locationPriority != null && locationMinInterval != null && locationMaxAge != null;\n    }\n\n    public boolean isTrackingChangesEnabled() {\n        return isTrackingChangesEnabled;\n    }\n\n    public boolean isIdentifyUserEnabled() {\n        return isIdentifyUserEnabled;\n    }\n\n    public boolean isTrackChangesReasonEnabled() {\n        return isTrackChangesReasonEnabled;\n    }\n\n    public static class Builder {\n        private String mode;\n        private String locationMinInterval;\n        private String locationMaxAge;\n        private boolean isTrackingChangesEnabled;\n        private boolean isIdentifyUserEnabled;\n        private boolean isTrackChangesReasonEnabled;\n\n        public Builder setMode(String mode) {\n            this.mode = mode;\n            return this;\n        }\n\n        public Builder setLocationMinInterval(String locationMinInterval) {\n            this.locationMinInterval = locationMinInterval;\n            return this;\n        }\n\n        public Builder setLocationMaxAge(String locationMaxAge) {\n            this.locationMaxAge = locationMaxAge;\n            return this;\n        }\n\n        public Builder setIsTrackingChangesEnabled(boolean isTrackingChangesEnabled) {\n            this.isTrackingChangesEnabled = isTrackingChangesEnabled;\n            return this;\n        }\n\n        public Builder setIsIdentifyUserEnabled(boolean isIdentifyUserEnabled) {\n            this.isIdentifyUserEnabled = isIdentifyUserEnabled;\n            return this;\n        }\n\n        public Builder setIsTrackChangesReasonEnabled(boolean isTrackChangesReasonEnabled) {\n            this.isTrackChangesReasonEnabled = isTrackChangesReasonEnabled;\n            return this;\n        }\n\n        public AuditConfig createAuditConfig() {\n            return new AuditConfig(mode, locationMinInterval, locationMaxAge, isTrackingChangesEnabled, isIdentifyUserEnabled, isTrackChangesReasonEnabled);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/audit/AuditEvent.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.formentry.audit;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.form.api.FormEntryController;\n\npublic class AuditEvent {\n\n    public enum AuditEventType {\n        // Beginning of the form\n        BEGINNING_OF_FORM(\"beginning of form\", false, false, false),\n        // Create a question\n        QUESTION(\"question\", true),\n        // Create a group\n        GROUP(\"group questions\", true),\n        // Prompt to add a new repeat\n        PROMPT_NEW_REPEAT(\"add repeat\", true),\n        // Repeat group\n        REPEAT(\"repeat\", false, false, false),\n        // Show the \"end of form\" view\n        END_OF_FORM(\"end screen\", true),\n        // Start filling in the form\n        FORM_START(\"form start\"),\n        // Exit the form\n        FORM_EXIT(\"form exit\"),\n        // Resume filling in the form after previously exiting\n        FORM_RESUME(\"form resume\"),\n        // Save the form\n        FORM_SAVE(\"form save\"),\n        // Finalize the form\n        FORM_FINALIZE(\"form finalize\"),\n        // Jump to a question\n        HIERARCHY(\"jump\", true),\n        // Error in save\n        SAVE_ERROR(\"save error\"),\n        // Error in finalize\n        FINALIZE_ERROR(\"finalize error\"),\n        // Constraint or missing answer error on save\n        CONSTRAINT_ERROR(\"constraint error\"),\n        // Delete a repeat group\n        DELETE_REPEAT(\"delete repeat\"),\n\n        CHANGE_REASON(\"change reason\"),\n\n        BACKGROUND_AUDIO_DISABLED(\"background audio disabled\"),\n\n        BACKGROUND_AUDIO_ENABLED(\"background audio enabled\"),\n\n        // Google Play Services are not available\n        GOOGLE_PLAY_SERVICES_NOT_AVAILABLE(\"google play services not available\", true, false, true),\n        // Location permissions are granted\n        LOCATION_PERMISSIONS_GRANTED(\"location permissions granted\", true, false, true),\n        // Location permissions are not granted\n        LOCATION_PERMISSIONS_NOT_GRANTED(\"location permissions not granted\", true, false, true),\n        // Location tracking option is enabled\n        LOCATION_TRACKING_ENABLED(\"location tracking enabled\", true, false, true),\n        // Location tracking option is disabled\n        LOCATION_TRACKING_DISABLED(\"location tracking disabled\", true, false, true),\n        // Location providers are enabled\n        LOCATION_PROVIDERS_ENABLED(\"location providers enabled\", true, false, true),\n        // Location providers are disabled\n        LOCATION_PROVIDERS_DISABLED(\"location providers disabled\", true, false, true),\n        // Unknown event type\n        UNKNOWN_EVENT_TYPE(\"Unknown AuditEvent Type\");\n\n        private final String value;\n        private final boolean isLogged;\n        private final boolean isInterval;\n        private final boolean isLocationRelated;\n\n        AuditEventType(String value, boolean isLogged, boolean isInterval, boolean isLocationRelated) {\n            this.value = value;\n\n            this.isLogged = isLogged;\n            this.isInterval = isInterval;\n            this.isLocationRelated = isLocationRelated;\n        }\n\n        AuditEventType(String value, boolean isInterval) {\n            this(value, true, isInterval, false);\n        }\n\n        AuditEventType(String value) {\n            this(value, true, false, false);\n        }\n\n        public String getValue() {\n            return value;\n        }\n\n        public boolean isLogged() {\n            return isLogged;\n        }\n\n        /**\n         * @return true if events of this type have both a start and an end time, false otherwise.\n         */\n        public boolean isInterval() {\n            return isInterval;\n        }\n\n        public boolean isLocationRelated() {\n            return isLocationRelated;\n        }\n    }\n\n    private final long start;\n    private AuditEventType auditEventType;\n    private String latitude;\n    private String longitude;\n    private String accuracy;\n    @NonNull\n    private String oldValue;\n    private String user;\n    private final String changeReason;\n    @NonNull\n    private String newValue = \"\";\n    private long end;\n    private boolean endTimeSet;\n    private FormIndex formIndex;\n\n    /*\n     * Create a new event\n     */\n    public AuditEvent(long start, AuditEventType auditEventType) {\n        this(start, auditEventType, null, null, null, null);\n    }\n\n    public AuditEvent(long start, AuditEventType auditEventType,\n                      FormIndex formIndex, String oldValue, String user, String changeReason) {\n        this.start = start;\n        this.auditEventType = auditEventType;\n        this.formIndex = formIndex;\n        this.oldValue = oldValue == null ? \"\" : oldValue;\n        this.user = user;\n        this.changeReason = changeReason;\n    }\n\n    /**\n     * @return true if this event's type is an interval event type.\n     */\n    public boolean isIntervalAuditEventType() {\n        return auditEventType.isInterval();\n    }\n\n    /*\n     * Mark the end of an interval event\n     */\n    public void setEnd(long endTime) {\n        this.end = endTime;\n        this.endTimeSet = true;\n    }\n\n    public boolean isEndTimeSet() {\n        return endTimeSet;\n    }\n\n    public AuditEventType getAuditEventType() {\n        return auditEventType;\n    }\n\n    public FormIndex getFormIndex() {\n        return formIndex;\n    }\n\n    public boolean hasNewAnswer() {\n        return !oldValue.equals(newValue);\n    }\n\n    public boolean isLocationAlreadySet() {\n        return latitude != null && !latitude.isEmpty()\n                && longitude != null && !longitude.isEmpty()\n                && accuracy != null && !accuracy.isEmpty();\n    }\n\n    public void setLocationCoordinates(String latitude, String longitude, String accuracy) {\n        this.latitude = latitude;\n        this.longitude = longitude;\n        this.accuracy = accuracy;\n    }\n\n    public void setUser(String user) {\n        this.user = user;\n    }\n\n    public boolean recordValueChange(String newValue) {\n        this.newValue = newValue != null ? newValue : \"\";\n\n        // Clear values if they are equal\n        if (this.oldValue.equals(this.newValue)) {\n            this.oldValue = \"\";\n            this.newValue = \"\";\n            return false;\n        }\n\n        return true;\n    }\n\n    public String getChangeReason() {\n        return changeReason;\n    }\n\n    public String getLatitude() {\n        return latitude;\n    }\n\n    public String getLongitude() {\n        return longitude;\n    }\n\n    public String getAccuracy() {\n        return accuracy;\n    }\n\n    public String getUser() {\n        return user;\n    }\n\n    public long getStart() {\n        return start;\n    }\n\n    @NonNull\n    public String getOldValue() {\n        return oldValue;\n    }\n\n    @NonNull\n    public String getNewValue() {\n        return newValue;\n    }\n\n    public long getEnd() {\n        return end;\n    }\n\n    // Get event type based on a Form Controller event\n    public static AuditEventType getAuditEventTypeFromFecType(int fcEvent) {\n        AuditEventType auditEventType;\n        switch (fcEvent) {\n            case FormEntryController.EVENT_BEGINNING_OF_FORM:\n                auditEventType = AuditEventType.BEGINNING_OF_FORM;\n                break;\n            case FormEntryController.EVENT_GROUP:\n                auditEventType = AuditEventType.GROUP;\n                break;\n            case FormEntryController.EVENT_REPEAT:\n                auditEventType = AuditEventType.REPEAT;\n                break;\n            case FormEntryController.EVENT_PROMPT_NEW_REPEAT:\n                auditEventType = AuditEventType.PROMPT_NEW_REPEAT;\n                break;\n            case FormEntryController.EVENT_END_OF_FORM:\n                auditEventType = AuditEventType.END_OF_FORM;\n                break;\n            default:\n                auditEventType = AuditEventType.UNKNOWN_EVENT_TYPE;\n        }\n        return auditEventType;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/audit/AuditEventCSVLine.java",
    "content": "package org.odk.collect.android.formentry.audit;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.odk.collect.shared.strings.StringUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport timber.log.Timber;\n\nimport static org.odk.collect.android.utilities.CSVUtils.getEscapedValueForCsv;\n\npublic final class AuditEventCSVLine {\n\n    private AuditEventCSVLine() {\n\n    }\n\n    public static String toCSVLine(AuditEvent auditEvent, boolean isTrackingLocationsEnabled, boolean isTrackingChangesEnabled, boolean isTrackingChangesReasonEnabled) {\n        FormIndex formIndex = auditEvent.getFormIndex();\n        AuditEvent.AuditEventType auditEventType = auditEvent.getAuditEventType();\n        long start = auditEvent.getStart();\n        long end = auditEvent.getEnd();\n        String latitude = auditEvent.getLatitude();\n        String longitude = auditEvent.getLongitude();\n        String accuracy = auditEvent.getAccuracy();\n        String oldValue = auditEvent.getOldValue();\n        String newValue = auditEvent.getNewValue();\n        String user = auditEvent.getUser();\n        String changeReason = auditEvent.getChangeReason();\n\n        String node = formIndex == null || formIndex.getReference() == null ? \"\" : getXPathPath(formIndex);\n\n        String string = String.format(\"%s,%s,%s,%s\", auditEventType.getValue(), node, start, end != 0 ? end : \"\");\n\n        if (isTrackingLocationsEnabled) {\n            string += String.format(\",%s,%s,%s\", latitude, longitude, accuracy);\n        }\n\n        if (isTrackingChangesEnabled) {\n            string += String.format(\",%s,%s\", getEscapedValueForCsv(oldValue), getEscapedValueForCsv(newValue));\n        }\n\n        if (user != null) {\n            string += String.format(\",%s\", getEscapedValueForCsv(user));\n        }\n\n        if (isTrackingChangesReasonEnabled) {\n            if (changeReason != null) {\n                string += String.format(\",%s\", getEscapedValueForCsv(changeReason));\n            } else {\n                string += \",\";\n            }\n        }\n\n        return string;\n    }\n\n    /**\n     * Get the XPath path of the node at a particular {@link FormIndex}.\n     * <p>\n     * Differs from {@link TreeReference#toString()} in that position predicates are only\n     * included for repeats. For example, given a group named {@code my-group} that contains a\n     * repeat named {@code my-repeat} which in turn contains a question named {@code my-question},\n     * {@link TreeReference#toString()} would return paths that look like\n     * {@code /my-group[1]/my-repeat[3]/my-question[1]}. In contrast, this method would return\n     * {@code /my-group/my-repeat[3]/my-question}.\n     * <p>\n     * TODO: consider moving to {@link FormIndex}\n     */\n    private static String getXPathPath(FormIndex formIndex) {\n        List<String> nodeNames = new ArrayList<>();\n        nodeNames.add(formIndex.getReference().getName(0));\n\n        FormIndex walker = formIndex;\n        int i = 1;\n        while (walker != null) {\n            try {\n                String currentNodeName = formIndex.getReference().getName(i);\n                if (walker.getInstanceIndex() != -1) {\n                    currentNodeName = currentNodeName + \"[\" + (walker.getInstanceIndex() + 1) + \"]\";\n                }\n                nodeNames.add(currentNodeName);\n            } catch (IndexOutOfBoundsException e) {\n                Timber.i(e);\n            }\n\n            walker = walker.getNextLevel();\n            i++;\n        }\n        return \"/\" + StringUtils.join(\"/\", nodeNames);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/audit/AuditEventLogger.java",
    "content": "\npackage org.odk.collect.android.formentry.audit;\n\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PROVIDERS_DISABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PROVIDERS_ENABLED;\n\nimport android.location.Location;\nimport android.os.SystemClock;\n\nimport androidx.annotation.Nullable;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.odk.collect.android.javarosawrapper.FormController;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport timber.log.Timber;\n\n/**\n * Handle logging of auditEvents (which contain time and might contain location coordinates),\n * and pass them to an Async task to append to a file\n * Notes:\n * 1) If the user has saved the form, then resumes editing, then exits without saving then the timing data during the\n * second editing session will be saved.  This is OK as it records user activity.  However if the user exits\n * without saving and they have never saved the form then the timing data is lost as the form editing will be\n * restarted from scratch.\n * 2) The auditEvents for questions in a field-list group are not shown.  Only the event for the group is shown.\n */\npublic class AuditEventLogger {\n\n    private final AuditEventWriter writer;\n    private List<Location> locations = new ArrayList<>();\n\n    private ArrayList<AuditEvent> auditEvents = new ArrayList<>();\n    private long surveyOpenTime;\n    private long surveyOpenElapsedTime;\n\n    private final AuditConfig auditConfig;\n    private final FormController formController;\n    private String user;\n\n    public AuditEventLogger(AuditConfig auditConfig, AuditEventWriter writer, FormController formController) {\n        this.auditConfig = auditConfig;\n        this.writer = writer;\n        this.formController = formController;\n    }\n\n    public void logEvent(AuditEvent.AuditEventType eventType, boolean writeImmediatelyToDisk, long currentTime) {\n        logEvent(eventType, null, writeImmediatelyToDisk, null, currentTime, null);\n    }\n\n    /**\n     * Logs events to the audit log. Can safely be used on a background thread, but should not be\n     * used on the UI thread.\n     */\n    public void logEvent(AuditEvent.AuditEventType eventType, FormIndex formIndex,\n                         boolean writeImmediatelyToDisk, String questionAnswer, long currentTime, String changeReason) {\n        internalLog(eventType, formIndex, writeImmediatelyToDisk, questionAnswer, currentTime, changeReason);\n    }\n\n    private synchronized void internalLog(AuditEvent.AuditEventType eventType, FormIndex formIndex, boolean writeImmediatelyToDisk, String questionAnswer, long currentTime, String changeReason) {\n        if (!isAuditEnabled() || shouldBeIgnored(eventType)) {\n            return;\n        }\n\n        Timber.i(\"AuditEvent recorded: %s\", eventType);\n\n        AuditEvent newAuditEvent = new AuditEvent(\n                getEventTime(),\n                eventType,\n                formIndex,\n                questionAnswer,\n                user,\n                changeReason\n        );\n\n        if (isDuplicatedIntervalEvent(newAuditEvent)) {\n            return;\n        }\n\n        if (auditConfig.isLocationEnabled()) {\n            addLocationCoordinatesToAuditEvent(newAuditEvent, currentTime);\n        }\n\n        /*\n         * Close any existing interval events if the view is being exited\n         */\n        if (eventType == AuditEvent.AuditEventType.FORM_EXIT) {\n            finalizeEvents();\n        }\n\n        auditEvents.add(newAuditEvent);\n\n        /*\n         * Write the event unless it is an interval event in which case we need to wait for the end of that event\n         */\n        if (writeImmediatelyToDisk && !newAuditEvent.isIntervalAuditEventType()) {\n            writeEvents();\n        }\n    }\n\n    /*\n     * Finalizes and writes events. Can safely be used on a background thread, but should not be\n     * used on the UI thread.\n     */\n    public synchronized void flush() {\n        if (isAuditEnabled()) {\n            finalizeEvents();\n            writeEvents();\n        }\n    }\n\n    private void addLocationCoordinatesToAuditEvent(AuditEvent auditEvent, long currentTime) {\n        Location location = getMostAccurateLocation(currentTime);\n        String latitude = location != null ? Double.toString(location.getLatitude()) : \"\";\n        String longitude = location != null ? Double.toString(location.getLongitude()) : \"\";\n        String accuracy = location != null ? Double.toString(location.getAccuracy()) : \"\";\n        auditEvent.setLocationCoordinates(latitude, longitude, accuracy);\n    }\n\n    private void addNewValueToQuestionAuditEvent(AuditEvent aev, FormController formController) {\n        IAnswerData answerData = formController.getQuestionPrompt(aev.getFormIndex()).getAnswerValue();\n        aev.recordValueChange(answerData != null ? answerData.getDisplayText() : null);\n    }\n\n    // If location provider are enabled/disabled it sometimes fires the BroadcastReceiver multiple\n    // times what tries to add duplicated logs\n    boolean isDuplicateOfLastLocationEvent(AuditEvent.AuditEventType eventType) {\n        return (eventType.equals(LOCATION_PROVIDERS_ENABLED) || eventType.equals(LOCATION_PROVIDERS_DISABLED))\n                && !auditEvents.isEmpty() && eventType.equals(auditEvents.get(auditEvents.size() - 1).getAuditEventType());\n    }\n\n    /*\n     * Ignore the event if we are already in an interval view event or have jumped\n     * This can happen if the user is on a question page and the page gets refreshed\n     * The exception is hierarchy events since they interrupt an existing interval event\n     */\n    private boolean isDuplicatedIntervalEvent(AuditEvent newAuditEvent) {\n        if (newAuditEvent.isIntervalAuditEventType()) {\n            for (AuditEvent aev : auditEvents) {\n                if (aev.isIntervalAuditEventType()\n                        && newAuditEvent.getAuditEventType().equals(aev.getAuditEventType())\n                        && newAuditEvent.getFormIndex().equals(aev.getFormIndex())) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    // Filter all events and set final parameters of interval events\n    private void finalizeEvents() {\n        // Calculate the time and add the event to the auditEvents array\n        long end = getEventTime();\n        ArrayList<AuditEvent> filteredAuditEvents = new ArrayList<>();\n        for (AuditEvent aev : auditEvents) {\n            if (aev.isIntervalAuditEventType()) {\n                setIntervalEventFinalParameters(aev, end, formController);\n            }\n            if (shouldEventBeLogged(aev)) {\n                filteredAuditEvents.add(aev);\n            }\n        }\n\n        auditEvents.clear();\n        auditEvents.addAll(filteredAuditEvents);\n    }\n\n    private void setIntervalEventFinalParameters(AuditEvent aev, long end, FormController formController) {\n        // Set location parameters.\n        // We try to add them here again (first attempt takes place when an event is being created),\n        // because coordinates might be not available at that time, so now we have another (last) chance.\n        if (auditConfig.isLocationEnabled() && !aev.isLocationAlreadySet()) {\n            addLocationCoordinatesToAuditEvent(aev, System.currentTimeMillis());\n        }\n\n        // Set answers\n        if (aev.getAuditEventType().equals(AuditEvent.AuditEventType.QUESTION) && formController != null) {\n            addNewValueToQuestionAuditEvent(aev, formController);\n        }\n\n        // Set end time\n        if (!aev.isEndTimeSet()) {\n            aev.setEnd(end);\n        }\n    }\n\n    /**\n     * @return true if an event of this type should be ignored given the current audit configuration\n     * and previous events, false otherwise.\n     */\n    private boolean shouldBeIgnored(AuditEvent.AuditEventType eventType) {\n        return !eventType.isLogged()\n                || eventType.isLocationRelated() && !auditConfig.isLocationEnabled()\n                || isDuplicateOfLastLocationEvent(eventType);\n    }\n\n    /*\n    Question which is in field-list group should be logged only if tracking changes option is\n    enabled and its answer has changed\n    */\n    private boolean shouldEventBeLogged(AuditEvent aev) {\n        if (aev.getAuditEventType().equals(AuditEvent.AuditEventType.QUESTION) && formController != null) {\n            return !formController.indexIsInFieldList(aev.getFormIndex())\n                    || (aev.hasNewAnswer() && auditConfig.isTrackingChangesEnabled());\n        }\n        return true;\n    }\n\n    private void writeEvents() {\n        if (!writer.isWriting()) {\n            writer.writeEvents(auditEvents);\n            auditEvents = new ArrayList<>();\n        } else {\n            Timber.i(\"Queueing AuditEvent\");\n        }\n    }\n\n    /*\n     * Use the time the survey was opened as a consistent value for wall clock time\n     */\n    private long getEventTime() {\n        if (surveyOpenTime == 0) {\n            surveyOpenTime = System.currentTimeMillis();\n            surveyOpenElapsedTime = SystemClock.elapsedRealtime();\n        }\n        return surveyOpenTime + (SystemClock.elapsedRealtime() - surveyOpenElapsedTime);\n    }\n\n    public void addLocation(Location location) {\n        locations.add(location);\n    }\n\n    @Nullable\n    private Location getMostAccurateLocation(long currentTime) {\n        removeExpiredLocations(currentTime);\n\n        Location bestLocation = null;\n        if (!locations.isEmpty()) {\n            for (Location location : locations) {\n                if (bestLocation == null || location.getAccuracy() < bestLocation.getAccuracy()) {\n                    bestLocation = location;\n                }\n            }\n        }\n        return bestLocation;\n    }\n\n    private void removeExpiredLocations(long currentTime) {\n        if (!locations.isEmpty()) {\n            List<Location> unexpiredLocations = new ArrayList<>();\n            for (Location location : locations) {\n                if (currentTime <= location.getTime() + auditConfig.getLocationMaxAge()) {\n                    unexpiredLocations.add(location);\n                }\n            }\n            locations = unexpiredLocations;\n        }\n    }\n\n    /*\n     * The event logger is enabled if the meta section of the form contains a logging entry\n     *      <orx:audit />\n     */\n    boolean isAuditEnabled() {\n        return auditConfig != null;\n    }\n\n    List<Location> getLocations() {\n        return locations;\n    }\n\n    public boolean isUserRequired() {\n        return auditConfig != null && auditConfig.isIdentifyUserEnabled();\n    }\n\n    public void setUser(String user) {\n        this.user = user;\n    }\n\n    public String getUser() {\n        return user;\n    }\n\n    public boolean isChangeReasonRequired() {\n        return auditConfig != null && auditConfig.isTrackChangesReasonEnabled();\n    }\n\n    public interface AuditEventWriter {\n\n        void writeEvents(List<AuditEvent> auditEvents);\n\n        boolean isWriting();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/audit/AuditEventSaveTask.java",
    "content": "\npackage org.odk.collect.android.formentry.audit;\n\nimport android.os.AsyncTask;\n\nimport androidx.annotation.NonNull;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileReader;\nimport java.io.FileWriter;\nimport java.io.IOException;\n\nimport timber.log.Timber;\n\nimport static org.odk.collect.android.formentry.audit.AuditEventCSVLine.toCSVLine;\n\n/**\n * Background task for appending events to the event log\n */\npublic class AuditEventSaveTask extends AsyncTask<AuditEvent, Void, Void> {\n    private final @NonNull\n    File file;\n    private final boolean isLocationEnabled;\n    private final boolean isTrackingChangesEnabled;\n    private final boolean isUserRequired;\n    private final boolean isTrackChangesReasonEnabled;\n\n    private static final String DEFAULT_COLUMNS = \"event,node,start,end\";\n    private static final String LOCATION_COORDINATES_COLUMNS = \",latitude,longitude,accuracy\";\n    private static final String ANSWER_VALUES_COLUMNS = \",old-value,new-value\";\n    private static final String USER_COLUMNS = \",user\";\n    private static final String CHANGE_REASON_COLUMNS = \",change-reason\";\n\n    public AuditEventSaveTask(@NonNull File file, boolean isLocationEnabled, boolean isTrackingChangesEnabled, boolean isUserRequired, boolean isTrackChangesReasonEnabled) {\n        this.file = file;\n        this.isLocationEnabled = isLocationEnabled;\n        this.isTrackingChangesEnabled = isTrackingChangesEnabled;\n        this.isUserRequired = isUserRequired;\n        this.isTrackChangesReasonEnabled = isTrackChangesReasonEnabled;\n    }\n\n    @Override\n    protected Void doInBackground(AuditEvent... params) {\n        FileWriter fw = null;\n        try {\n            boolean newFile = !file.exists();\n            fw = new FileWriter(file, true);\n            if (newFile) {\n                fw.write(getHeader() + \"\\n\");\n            } else if (updateHeaderIfNeeded()) {\n                fw.close();\n                fw = new FileWriter(file.getAbsolutePath(), true);\n            }\n            if (params.length > 0) {\n                for (AuditEvent aev : params) {\n                    String csvLine = toCSVLine(aev, isLocationEnabled, isTrackingChangesEnabled, isTrackChangesReasonEnabled);\n                    fw.write(csvLine + \"\\n\");\n                    Timber.i(\"Log audit event: %s\", csvLine);\n                }\n            }\n        } catch (IOException e) {\n            Timber.e(e);\n        } finally {\n            try {\n                if (fw != null) {\n                    fw.close();\n                }\n            } catch (Exception e) {\n                Timber.e(e);\n            }\n        }\n        return null;\n    }\n\n    private boolean updateHeaderIfNeeded() {\n        boolean headerUpdated = false;\n        FileWriter tfw = null;\n        BufferedReader br = null;\n        try {\n            br = new BufferedReader(new FileReader(file));\n            if (shouldHeaderBeUpdated(br.readLine())) { // update header\n                File temporaryFile = new File(file.getParentFile().getAbsolutePath() + \"/temporaryAudit.csv\");\n                tfw = new FileWriter(temporaryFile, true);\n                tfw.write(getHeader() + \"\\n\");\n                String line;\n                while ((line = br.readLine()) != null) {\n                    tfw.write(line + \"\\n\");\n                }\n                temporaryFile.renameTo(file);\n                headerUpdated = true;\n            }\n        } catch (IOException e) {\n            Timber.e(e);\n        } finally {\n            try {\n                if (tfw != null) {\n                    tfw.close();\n                }\n                if (br != null) {\n                    br.close();\n                }\n            } catch (Exception e) {\n                Timber.e(e);\n            }\n        }\n        return headerUpdated;\n    }\n\n    private boolean shouldHeaderBeUpdated(String header) {\n        return header == null\n                || (isLocationEnabled && !header.contains(LOCATION_COORDINATES_COLUMNS))\n                || (isTrackingChangesEnabled && !header.contains(ANSWER_VALUES_COLUMNS))\n                || (isUserRequired && !header.contains(USER_COLUMNS));\n    }\n\n    private String getHeader() {\n        String header = DEFAULT_COLUMNS;\n        if (isLocationEnabled) {\n            header += LOCATION_COORDINATES_COLUMNS;\n        }\n        if (isTrackingChangesEnabled) {\n            header += ANSWER_VALUES_COLUMNS;\n        }\n        if (isUserRequired) {\n            header += USER_COLUMNS;\n        }\n        if (isTrackChangesReasonEnabled) {\n            header += CHANGE_REASON_COLUMNS;\n        }\n        return header;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/audit/AuditUtils.kt",
    "content": "package org.odk.collect.android.formentry.audit\n\nimport org.javarosa.form.api.FormEntryController\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.javarosawrapper.FormControllerExt.getQuestionPrompts\nimport org.odk.collect.android.javarosawrapper.RepeatsInFieldListException\n\nobject AuditUtils {\n    @JvmStatic\n    fun logCurrentScreen(\n        formController: FormController,\n        auditEventLogger: AuditEventLogger,\n        currentTime: Long\n    ) {\n        if (formController.getEvent() == FormEntryController.EVENT_QUESTION ||\n            formController.getEvent() == FormEntryController.EVENT_GROUP ||\n            formController.getEvent() == FormEntryController.EVENT_REPEAT\n        ) {\n            try {\n                for (question in formController.getQuestionPrompts()) {\n                    val answer =\n                        if (question.answerValue != null) {\n                            question.answerValue!!.displayText\n                        } else {\n                            null\n                        }\n\n                    auditEventLogger.logEvent(\n                        AuditEvent.AuditEventType.QUESTION,\n                        question.index,\n                        true,\n                        answer,\n                        currentTime,\n                        null\n                    )\n                }\n            } catch (e: RepeatsInFieldListException) {\n                // ignore\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/audit/ChangesReasonPromptDialogFragment.java",
    "content": "package org.odk.collect.android.formentry.audit;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.EditText;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.saving.FormSaveViewModel;\nimport org.odk.collect.material.MaterialFullScreenDialogFragment;\n\npublic class ChangesReasonPromptDialogFragment extends MaterialFullScreenDialogFragment {\n\n    private FormSaveViewModel viewModel;\n\n    @Nullable\n    @Override\n    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        return inflater.inflate(R.layout.changes_reason_dialog, container, false);\n    }\n\n    @Override\n    public void onViewCreated(View view, Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n\n        Toolbar toolbar = getToolbar();\n        toolbar.setTitle(viewModel.getFormName());\n        toolbar.setNavigationIcon(org.odk.collect.icons.R.drawable.ic_close);\n        toolbar.setNavigationContentDescription(org.odk.collect.strings.R.string.close);\n        toolbar.inflateMenu(R.menu.changes_reason_dialog);\n\n        EditText reasonField = view.findViewById(R.id.reason);\n        reasonField.setText(viewModel.getReason());\n        reasonField.addTextChangedListener(new TextWatcher() {\n            @Override\n            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {\n\n            }\n\n            @Override\n            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {\n\n            }\n\n            @Override\n            public void afterTextChanged(Editable editable) {\n                viewModel.setReason(editable.toString());\n            }\n        });\n\n        toolbar.setOnMenuItemClickListener(item -> {\n            viewModel.resumeSave();\n            return true;\n        });\n\n        reasonField.requestFocus();\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        viewModel = new ViewModelProvider(requireActivity()).get(FormSaveViewModel.class);\n    }\n\n    @Override\n    protected void onBackPressed() {\n        dismiss();\n    }\n\n    @Override\n    protected Toolbar getToolbar() {\n        return getView().findViewById(org.odk.collect.androidshared.R.id.toolbar);\n    }\n\n    @Override\n    protected void onCloseClicked() {\n        dismiss();\n    }\n\n    @Override\n    protected boolean shouldShowSoftKeyboard() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/audit/IdentifyUserPromptDialogFragment.java",
    "content": "package org.odk.collect.android.formentry.audit;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.EditText;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.material.MaterialFullScreenDialogFragment;\n\npublic class IdentifyUserPromptDialogFragment extends MaterialFullScreenDialogFragment {\n\n    private IdentityPromptViewModel viewModel;\n\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {\n        return inflater.inflate(R.layout.identify_user_dialog, container, false);\n    }\n\n    @Override\n    public void onViewCreated(View view, Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n\n        Toolbar toolbar = getToolbar();\n        toolbar.setTitle(viewModel.getFormTitle());\n        toolbar.setNavigationIcon(org.odk.collect.icons.R.drawable.ic_close);\n        toolbar.setNavigationContentDescription(org.odk.collect.strings.R.string.close);\n\n        EditText identityField = view.findViewById(R.id.identity);\n        identityField.setText(viewModel.getUser());\n\n        identityField.addTextChangedListener(new TextWatcher() {\n            @Override\n            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {\n\n            }\n\n            @Override\n            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {\n\n            }\n\n            @Override\n            public void afterTextChanged(Editable editable) {\n                viewModel.setIdentity(editable.toString());\n            }\n        });\n\n        identityField.setOnEditorActionListener((textView, i, keyEvent) -> {\n            viewModel.done();\n            return true;\n        });\n\n        identityField.requestFocus();\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n\n        viewModel = new ViewModelProvider(requireActivity()).get(IdentityPromptViewModel.class);\n        viewModel.requiresIdentityToContinue().observe(this, requiresIdentity -> {\n            if (!requiresIdentity) {\n                dismiss();\n            }\n        });\n    }\n\n    @Override\n    protected void onCloseClicked() {\n        dismiss();\n        viewModel.promptDismissed();\n    }\n\n    @Override\n    protected void onBackPressed() {\n        dismiss();\n        viewModel.promptDismissed();\n    }\n\n    @Override\n    protected Toolbar getToolbar() {\n        return getView().findViewById(org.odk.collect.androidshared.R.id.toolbar);\n    }\n\n    @Override\n    protected boolean shouldShowSoftKeyboard() {\n        return true;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/audit/IdentityPromptViewModel.java",
    "content": "package org.odk.collect.android.formentry.audit;\n\nimport static org.odk.collect.shared.strings.StringUtils.isBlank;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.lifecycle.ViewModel;\n\nimport org.odk.collect.android.javarosawrapper.FormController;\n\npublic class IdentityPromptViewModel extends ViewModel {\n\n    private final MutableLiveData<Boolean> formEntryCancelled = new MutableLiveData<>(false);\n    private final MutableLiveData<Boolean> requiresIdentity = new MutableLiveData<>(false);\n\n    @Nullable\n    private AuditEventLogger auditEventLogger;\n\n    private String identity = \"\";\n    private String formName;\n\n    public IdentityPromptViewModel() {\n        updateRequiresIdentity();\n    }\n\n    public void formLoaded(@NonNull FormController formController) {\n        this.formName = formController.getFormTitle();\n        this.auditEventLogger = formController.getAuditEventLogger();\n        updateRequiresIdentity();\n    }\n\n    public LiveData<Boolean> requiresIdentityToContinue() {\n        return requiresIdentity;\n    }\n\n    public LiveData<Boolean> isFormEntryCancelled() {\n        return formEntryCancelled;\n    }\n\n    public String getUser() {\n        return identity;\n    }\n\n    public void setIdentity(String identity) {\n        this.identity = identity;\n    }\n\n    public void done() {\n        if (auditEventLogger != null) {\n            auditEventLogger.setUser(identity);\n        }\n        \n        updateRequiresIdentity();\n    }\n\n    public void promptDismissed() {\n        formEntryCancelled.setValue(true);\n    }\n\n    private void updateRequiresIdentity() {\n        this.requiresIdentity.setValue(\n                auditEventLogger != null && auditEventLogger.isUserRequired() && !userIsValid(auditEventLogger.getUser())\n        );\n    }\n\n    private static boolean userIsValid(String user) {\n        return user != null && !user.isEmpty() && !isBlank(user);\n    }\n\n    public String getFormTitle() {\n        return formName;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/backgroundlocation/BackgroundLocationHelper.java",
    "content": "package org.odk.collect.android.formentry.backgroundlocation;\n\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_BACKGROUND_LOCATION;\n\nimport android.location.Location;\n\nimport org.odk.collect.android.activities.FormFillingActivity;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.formentry.FormSession;\nimport org.odk.collect.android.formentry.FormSessionRepository;\nimport org.odk.collect.android.formentry.audit.AuditConfig;\nimport org.odk.collect.android.formentry.audit.AuditEvent;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.androidshared.system.PlayServicesChecker;\nimport org.odk.collect.permissions.PermissionsProvider;\nimport org.odk.collect.shared.settings.Settings;\n\nimport javax.annotation.Nullable;\n\n/**\n * Wrapper on resources needed by {@link BackgroundLocationManager} to make testing easier.\n *\n * Ideally this would be replaced by more coherent abstractions in the future.\n *\n * The methods on the {@link FormController} are wrapped here rather\n * than the form controller being passed in when constructing the {@link BackgroundLocationManager}\n * because the form controller isn't set until\n * {@link FormFillingActivity}'s onCreate.\n */\npublic class BackgroundLocationHelper {\n\n    private final PermissionsProvider permissionsProvider;\n    private final Settings generalSettings;\n    private final FormSessionRepository formSessionRepository;\n    private final String sessionId;\n\n    public BackgroundLocationHelper(\n            PermissionsProvider permissionsProvider,\n            Settings generalSettings,\n            FormSessionRepository formSessionRepository,\n            String sessionId\n    ) {\n        this.permissionsProvider = permissionsProvider;\n        this.generalSettings = generalSettings;\n        this.formSessionRepository = formSessionRepository;\n        this.sessionId = sessionId;\n    }\n\n    boolean isAndroidLocationPermissionGranted() {\n        return permissionsProvider.areLocationPermissionsGranted();\n    }\n\n    boolean isBackgroundLocationPreferenceEnabled() {\n        return generalSettings.getBoolean(KEY_BACKGROUND_LOCATION);\n    }\n\n    boolean arePlayServicesAvailable() {\n        return new PlayServicesChecker().isGooglePlayServicesAvailable(Collect.getInstance().getApplicationContext());\n    }\n\n    /**\n     * @return true if the global form controller has been initialized.\n     */\n    boolean isCurrentFormSet() {\n        return getFormController() != null;\n    }\n\n    /**\n     * @return true if the current form definition requests any kind of background location.\n     *\n     * Precondition: the global form controller has been initialized.\n     */\n    boolean currentFormCollectsBackgroundLocation() {\n        return getFormController().currentFormCollectsBackgroundLocation();\n    }\n\n    /**\n     * @return true if the current form definition requests a background location audit, false\n     * otherwise.\n     *\n     * Precondition: the global form controller has been initialized.\n     */\n    boolean currentFormAuditsLocation() {\n        return getFormController().currentFormAuditsLocation();\n    }\n\n    /**\n     * @return the configuration for the audit requested by the current form definition.\n     *\n     * Precondition: the global form controller has been initialized.\n     */\n    AuditConfig getCurrentFormAuditConfig() {\n        return getFormController().getSubmissionMetadata().auditConfig;\n    }\n\n    /**\n     * Logs an audit event of the given type.\n     *\n     * Precondition: the global form controller has been initialized.\n     */\n    void logAuditEvent(AuditEvent.AuditEventType eventType) {\n        getFormController().getAuditEventLogger().logEvent(eventType, false, System.currentTimeMillis());\n    }\n\n    /**\n     * Provides the location to the global audit event logger.\n     *\n     * Precondition: the global form controller has been initialized.\n     */\n    void provideLocationToAuditLogger(Location location) {\n        getFormController().getAuditEventLogger().addLocation(location);\n    }\n\n    @Nullable\n    private FormController getFormController() {\n        FormSession formSession = formSessionRepository.get(sessionId).getValue();\n        return formSession == null ? null : formSession.getFormController();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/backgroundlocation/BackgroundLocationManager.java",
    "content": "package org.odk.collect.android.formentry.backgroundlocation;\n\nimport android.location.Location;\n\nimport androidx.annotation.NonNull;\n\nimport com.google.android.gms.location.LocationListener;\n\nimport org.odk.collect.location.LocationClient;\nimport org.odk.collect.android.formentry.audit.AuditConfig;\nimport org.odk.collect.android.formentry.audit.AuditEvent;\nimport org.odk.collect.android.logic.actions.setgeopoint.CollectSetGeopointAction;\n\n/**\n * Manages background location for the location audit logging and odk:setgeopoint action features.\n * Provides precondition checking and user feedback for both features.\n *\n * For location audit logging, manages all the audit logging as well as fetching the location using\n * Google Play Services.\n *\n * {@link CollectSetGeopointAction} fetches location for odk:setgeopoint actions.\n *\n * The implementation uses a state machine concept. Public methods represent user or system actions\n * that clients of this class react to. Based on those actions and various preconditions (Google Play\n * Services available, location permissions granted, etc), the manager's state changes.\n */\npublic class BackgroundLocationManager implements LocationClient.LocationClientListener, LocationListener {\n    @NonNull\n    private BackgroundLocationState currentState;\n\n    @NonNull\n    private LocationClient locationClient;\n\n    @NonNull\n    private LocationListener locationListener;\n\n    @NonNull\n    private BackgroundLocationHelper helper;\n\n    public BackgroundLocationManager(LocationClient locationClient, BackgroundLocationHelper helper) {\n        currentState = BackgroundLocationState.NO_BACKGROUND_LOCATION_NEEDED;\n        this.locationClient = locationClient;\n\n        this.helper = helper;\n\n        this.locationListener = this;\n    }\n\n    public void formFinishedLoading() {\n        switch (currentState) {\n            case NO_BACKGROUND_LOCATION_NEEDED:\n                if (helper.currentFormCollectsBackgroundLocation()) {\n                    currentState = BackgroundLocationState.PENDING_PRECONDITION_CHECKS;\n                }\n        }\n    }\n\n    public BackgroundLocationMessage activityDisplayed() {\n        switch (currentState) {\n            case NO_BACKGROUND_LOCATION_NEEDED:\n                // After system-initiated process death, state is reset. The form did not get\n                // reloaded and user messaging has already been displayed so go straight to\n                // requesting location.\n                if (helper.isCurrentFormSet() && helper.currentFormAuditsLocation()) {\n                    startLocationRequests();\n\n                    if (currentState != BackgroundLocationState.RECEIVING_LOCATIONS) {\n                        // The form requests background location and some precondition failed. Change\n                        // the state to STOPPED so that if preconditions change, location tracking\n                        // will resume.\n                        currentState = BackgroundLocationState.STOPPED;\n                    }\n                }\n                break;\n\n            case PENDING_PRECONDITION_CHECKS:\n                // Separate out user message so that any failed precondition is written to the audit\n                // log. If Play Services are unavailable AND the location preference is disabled,\n                // show the Play Services unavailable message only.\n                BackgroundLocationMessage userMessage = null;\n\n                if (!helper.isBackgroundLocationPreferenceEnabled()) {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_TRACKING_DISABLED);\n                    userMessage = BackgroundLocationMessage.LOCATION_PREF_DISABLED;\n                }\n\n                if (!helper.arePlayServicesAvailable()) {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.GOOGLE_PLAY_SERVICES_NOT_AVAILABLE);\n                    userMessage = BackgroundLocationMessage.PLAY_SERVICES_UNAVAILABLE;\n                }\n\n                if (userMessage == null) {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_TRACKING_ENABLED);\n                    currentState = BackgroundLocationState.PENDING_PERMISSION_CHECK;\n                } else {\n                    currentState = BackgroundLocationState.STOPPED;\n                }\n\n                return userMessage;\n\n            case STOPPED:\n                // All preconditions could be met either because we were collecting location, hid\n                // the activity and showed it again or because a precondition became met.\n                startLocationRequests();\n                break;\n        }\n\n        return null;\n    }\n\n    public void activityHidden() {\n        switch (currentState) {\n            case RECEIVING_LOCATIONS:\n            case STOPPED:\n                stopLocationRequests();\n        }\n    }\n\n    public boolean isPendingPermissionCheck() {\n        return currentState == BackgroundLocationState.PENDING_PERMISSION_CHECK;\n    }\n\n    public BackgroundLocationMessage locationPermissionGranted() {\n        switch (currentState) {\n            case PENDING_PERMISSION_CHECK:\n                if (!helper.currentFormAuditsLocation()) {\n                    // Since setgeopoint actions manage their own location clients, we don't need to configure\n                    // location requests here before asking isLocationAvailable()\n                    currentState = BackgroundLocationState.SETGEOPOINT_ONLY;\n                    if (locationClient.isLocationAvailable()) {\n                        return BackgroundLocationMessage.COLLECTING_LOCATION;\n                    } else {\n                        return BackgroundLocationMessage.PROVIDERS_DISABLED;\n                    }\n                }\n\n                startLocationRequests();\n\n                if (currentState != BackgroundLocationState.RECEIVING_LOCATIONS) {\n                    // one of the preconditions became false; we don't want to stay PENDING_PERMISSION_CHECK\n                    currentState = BackgroundLocationState.STOPPED;\n                }\n\n                helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_PERMISSIONS_GRANTED);\n\n                // TODO: isLocationAvailable must be called after location request made but there's no\n                // guarantee since request updates are called onClientStart()\n                if (locationClient.isLocationAvailable()) {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_PROVIDERS_ENABLED);\n                } else {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_PROVIDERS_DISABLED);\n                    return BackgroundLocationMessage.PROVIDERS_DISABLED;\n                }\n\n                return BackgroundLocationMessage.COLLECTING_LOCATION;\n            default:\n                return null;\n        }\n    }\n\n    public void locationPermissionDenied() {\n        switch (currentState) {\n            case PENDING_PERMISSION_CHECK:\n                if (!helper.currentFormAuditsLocation()) {\n                    currentState = BackgroundLocationState.SETGEOPOINT_ONLY;\n                    return;\n                }\n\n                helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_PERMISSIONS_NOT_GRANTED);\n                currentState = BackgroundLocationState.STOPPED;\n        }\n    }\n\n    public void backgroundLocationPreferenceToggled() {\n        switch (currentState) {\n            case RECEIVING_LOCATIONS:\n                if (!helper.isBackgroundLocationPreferenceEnabled()) {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_TRACKING_DISABLED);\n                    stopLocationRequests();\n                }\n                break;\n\n            case STOPPED:\n                if (helper.isBackgroundLocationPreferenceEnabled()) {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_TRACKING_ENABLED);\n                    startLocationRequests();\n                }\n                break;\n        }\n    }\n\n    public void locationPermissionChanged() {\n        switch (currentState) {\n            case STOPPED:\n                if (helper.isAndroidLocationPermissionGranted()) {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_PERMISSIONS_GRANTED);\n                } else {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_PERMISSIONS_NOT_GRANTED);\n                }\n                break;\n        }\n    }\n\n    public void locationProvidersChanged() {\n        switch (currentState) {\n            case RECEIVING_LOCATIONS:\n            case STOPPED:\n                if (locationClient.isLocationAvailable()) {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_PROVIDERS_ENABLED);\n                } else {\n                    helper.logAuditEvent(AuditEvent.AuditEventType.LOCATION_PROVIDERS_DISABLED);\n                }\n                break;\n        }\n    }\n\n    private void startLocationRequests() {\n        if (helper.currentFormAuditsLocation()\n                && helper.isBackgroundLocationPreferenceEnabled()\n                && helper.arePlayServicesAvailable()\n                && helper.isAndroidLocationPermissionGranted()) {\n            AuditConfig auditConfig = helper.getCurrentFormAuditConfig();\n\n            locationClient.setPriority(auditConfig.getLocationPriority());\n            locationClient.setUpdateInterval(auditConfig.getLocationMinInterval());\n            locationClient.start(this);\n\n            currentState = BackgroundLocationState.RECEIVING_LOCATIONS;\n        }\n    }\n\n    private void stopLocationRequests() {\n        locationClient.stop();\n\n        currentState = BackgroundLocationState.STOPPED;\n    }\n\n    @Override\n    public void onLocationChanged(Location location) {\n        switch (currentState) {\n            case RECEIVING_LOCATIONS:\n                helper.provideLocationToAuditLogger(location);\n        }\n    }\n\n    @Override\n    public void onClientStart() {\n        locationClient.requestLocationUpdates(locationListener);\n    }\n\n    @Override\n    public void onClientStartFailure() {\n\n    }\n\n    @Override\n    public void onClientStop() {\n\n    }\n\n    private enum BackgroundLocationState {\n        /** The current form does not track background location (also the case if the current form\n         * is not set yet */\n        NO_BACKGROUND_LOCATION_NEEDED,\n\n        /** The current form tracks background location and a message hasn't been shown to the user **/\n        PENDING_PRECONDITION_CHECKS,\n\n        /** An Android location permission check must be performed */\n        PENDING_PERMISSION_CHECK,\n\n        /** Terminal state: all checks have been performed and messaging has been displayed to the\n         * user, it's now up to the setgeopoint action implementation to manage location fetching */\n        SETGEOPOINT_ONLY,\n\n        /** The current form requests location audits but some preconditions to location capture are\n         * currently unmet. Once this state is reached, it's only possible to go between it and\n         * {@link #RECEIVING_LOCATIONS} */\n        STOPPED,\n\n        /** The current form audits location and all preconditions to location capture have been\n         * met. Once this state is reached, it's only possible to go between it and\n         * {@link #STOPPED} */\n        RECEIVING_LOCATIONS\n    }\n\n    public enum BackgroundLocationMessage {\n        LOCATION_PREF_DISABLED(org.odk.collect.strings.R.string.background_location_disabled, true),\n        PLAY_SERVICES_UNAVAILABLE(org.odk.collect.strings.R.string.google_play_services_not_available, false),\n        COLLECTING_LOCATION(org.odk.collect.strings.R.string.background_location_enabled, true),\n        PROVIDERS_DISABLED(-1, false);\n\n        private int messageTextResourceId;\n\n        // Indicates whether the message text needs a \"⋮\" character inserted\n        private boolean isMenuCharacterNeeded;\n\n        BackgroundLocationMessage(int messageTextResourceId, boolean isMenuCharacterNeeded) {\n            this.messageTextResourceId = messageTextResourceId;\n            this.isMenuCharacterNeeded = isMenuCharacterNeeded;\n        }\n\n        public int getMessageTextResourceId() {\n            return messageTextResourceId;\n        }\n\n        public boolean isMenuCharacterNeeded() {\n            return isMenuCharacterNeeded;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/backgroundlocation/BackgroundLocationViewModel.java",
    "content": "package org.odk.collect.android.formentry.backgroundlocation;\n\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_BACKGROUND_LOCATION;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.ViewModel;\n\nimport org.odk.collect.android.activities.FormFillingActivity;\nimport org.odk.collect.shared.settings.Settings;\n\n/**\n * Ensures that background location tracking continues throughout the activity lifecycle. Builds\n * location-related dependency, receives activity events from #{@link FormFillingActivity} and\n * forwards those events to the location manager.\n *\n * The current goal is to keep this component very thin but this may evolve as it is involved in\n * managing more model objects.\n */\npublic class BackgroundLocationViewModel extends ViewModel {\n    @NonNull\n    private final BackgroundLocationManager locationManager;\n\n    public BackgroundLocationViewModel(BackgroundLocationManager locationManager) {\n        this.locationManager = locationManager;\n    }\n\n    public void formFinishedLoading() {\n        locationManager.formFinishedLoading();\n    }\n\n    public BackgroundLocationManager.BackgroundLocationMessage activityDisplayed() {\n        return locationManager.activityDisplayed();\n    }\n\n    public void activityHidden() {\n        locationManager.activityHidden();\n    }\n\n    public boolean isBackgroundLocationPermissionsCheckNeeded() {\n        return locationManager.isPendingPermissionCheck();\n    }\n\n    public BackgroundLocationManager.BackgroundLocationMessage locationPermissionsGranted() {\n        return locationManager.locationPermissionGranted();\n    }\n\n    public void locationPermissionsDenied() {\n        locationManager.locationPermissionDenied();\n    }\n\n    public void locationPermissionChanged() {\n        locationManager.locationPermissionChanged();\n    }\n\n    public void locationProvidersChanged() {\n        locationManager.locationProvidersChanged();\n    }\n\n    public void backgroundLocationPreferenceToggled(Settings generalSettings) {\n        generalSettings.save(KEY_BACKGROUND_LOCATION, !generalSettings.getBoolean(KEY_BACKGROUND_LOCATION));\n        locationManager.backgroundLocationPreferenceToggled();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/media/FormMediaUtils.java",
    "content": "package org.odk.collect.android.formentry.media;\n\nimport android.graphics.Color;\n\nimport androidx.annotation.Nullable;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.utilities.ThemeUtils;\nimport org.odk.collect.audioclips.Clip;\n\nimport timber.log.Timber;\n\npublic final class FormMediaUtils {\n\n    private FormMediaUtils() {\n\n    }\n\n    @Nullable\n    public static Clip getClip(FormEntryPrompt prompt, SelectChoice selectChoice, ReferenceManager referenceManager) {\n        String playableAudioURI = getPlayableAudioURI(prompt, selectChoice, referenceManager);\n\n        if (playableAudioURI != null) {\n            return new Clip(getClipID(prompt, selectChoice), playableAudioURI);\n        } else {\n            return null;\n        }\n    }\n\n    public static String getClipID(FormEntryPrompt prompt) {\n        return prompt.getIndex().toString();\n    }\n\n    public static String getClipID(FormEntryPrompt prompt, SelectChoice selectChoice) {\n        return prompt.getIndex().toString() + \" \" + selectChoice.getIndex();\n    }\n\n    @Nullable\n    public static String getPlayableAudioURI(FormEntryPrompt prompt, ReferenceManager referenceManager) {\n        return deriveReference(prompt.getAudioText(), referenceManager);\n    }\n\n    @Nullable\n    public static String getPlayableAudioURI(FormEntryPrompt prompt, SelectChoice selectChoice, ReferenceManager referenceManager) {\n        String selectAudioURI = prompt.getSpecialFormSelectChoiceText(\n                selectChoice,\n                FormEntryCaption.TEXT_FORM_AUDIO\n        );\n\n        return deriveReference(selectAudioURI, referenceManager);\n    }\n\n    @Nullable\n    private static String deriveReference(String originalURI, ReferenceManager referenceManager) {\n        if (originalURI == null) {\n            return null;\n        }\n\n        try {\n            return referenceManager.deriveReference(originalURI).getLocalURI();\n        } catch (InvalidReferenceException e) {\n            Timber.e(e);\n            return null;\n        }\n    }\n\n    public static int getPlayColor(FormEntryPrompt prompt, ThemeUtils themeUtils) {\n        int playColor = themeUtils.getColorPrimary();\n\n        String playColorString = prompt.getFormElement().getAdditionalAttribute(null, \"playColor\");\n        if (playColorString != null) {\n            try {\n                playColor = Color.parseColor(playColorString);\n            } catch (IllegalArgumentException e) {\n                Timber.e(e, \"Argument %s is incorrect\", playColorString);\n            }\n        }\n\n        return playColor;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/media/PromptAutoplayer.java",
    "content": "package org.odk.collect.android.formentry.media;\n\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getClipID;\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getPlayableAudioURI;\nimport static org.odk.collect.android.utilities.Appearances.NO_BUTTONS;\nimport static java.util.Collections.emptyList;\n\nimport org.javarosa.core.model.Constants;\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.FormEntryPromptUtils;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.audioclips.Clip;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class PromptAutoplayer {\n\n    private static final String AUTOPLAY_ATTRIBUTE = \"autoplay\";\n    private static final String AUDIO_OPTION = \"audio\";\n\n    private final AudioPlayer audioPlayer;\n    private final ReferenceManager referenceManager;\n\n    public PromptAutoplayer(AudioPlayer audioPlayer, ReferenceManager referenceManager) {\n        this.audioPlayer = audioPlayer;\n        this.referenceManager = referenceManager;\n    }\n\n    public Boolean autoplayIfNeeded(FormEntryPrompt prompt) {\n        String autoplayOption = FormEntryPromptUtils.getAdditionalAttribute(prompt, AUTOPLAY_ATTRIBUTE);\n\n        if (hasAudioAutoplay(autoplayOption)) {\n            List<Clip> clipsToPlay = new ArrayList<>();\n\n            Clip promptClip = getPromptClip(prompt);\n            if (promptClip != null) {\n                clipsToPlay.add(promptClip);\n            }\n\n            List<Clip> selectClips = getSelectClips(prompt);\n            if (!selectClips.isEmpty()) {\n                clipsToPlay.addAll(selectClips);\n            }\n\n            if (clipsToPlay.isEmpty()) {\n                return false;\n            } else {\n                audioPlayer.playInOrder(clipsToPlay);\n                return true;\n            }\n        } else {\n            return false;\n        }\n    }\n\n    private boolean hasAudioAutoplay(String autoplayOption) {\n        return autoplayOption != null && autoplayOption.equalsIgnoreCase(AUDIO_OPTION);\n    }\n\n    private List<Clip> getSelectClips(FormEntryPrompt prompt) {\n        if (appearanceDoesNotShowControls(Appearances.getSanitizedAppearanceHint(prompt))) {\n            return emptyList();\n        }\n\n        List<Clip> selectClips = new ArrayList<>();\n\n        int controlType = prompt.getControlType();\n        if (controlType == Constants.CONTROL_SELECT_ONE || controlType == Constants.CONTROL_SELECT_MULTI) {\n\n            List<SelectChoice> selectChoices = prompt.getSelectChoices();\n\n            for (SelectChoice choice : selectChoices) {\n                String selectURI = getPlayableAudioURI(prompt, choice, referenceManager);\n\n                if (selectURI != null) {\n                    Clip clip = new Clip(getClipID(prompt, choice), selectURI);\n                    selectClips.add(clip);\n                }\n            }\n        }\n\n        return selectClips;\n    }\n\n    private boolean appearanceDoesNotShowControls(String appearance) {\n        return appearance.startsWith(Appearances.MINIMAL) ||\n                appearance.startsWith(Appearances.COMPACT) ||\n                appearance.contains(NO_BUTTONS);\n    }\n\n    private Clip getPromptClip(FormEntryPrompt prompt) {\n        String uri = getPlayableAudioURI(prompt, referenceManager);\n        if (uri != null) {\n            return new Clip(\n                    getClipID(prompt),\n                    getPlayableAudioURI(prompt, referenceManager)\n            );\n        } else {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/questions/AnswersProvider.java",
    "content": "package org.odk.collect.android.formentry.questions;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.data.IAnswerData;\n\nimport java.util.HashMap;\n\npublic interface AnswersProvider {\n    HashMap<FormIndex, IAnswerData> getAnswers();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/questions/AudioVideoImageTextLabel.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.formentry.questions;\n\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.text.method.LinkMovementMethod;\nimport android.util.AttributeSet;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.CheckBox;\nimport android.widget.ImageView;\nimport android.widget.RadioButton;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.audio.AudioButton;\nimport org.odk.collect.android.databinding.AudioVideoImageTextLabelBinding;\nimport org.odk.collect.android.listeners.SelectItemClickListener;\nimport org.odk.collect.android.utilities.FormEntryPromptUtils;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.audioclips.Clip;\nimport org.odk.collect.imageloader.ImageLoader;\n\nimport java.io.File;\n\n/**\n * Represents a label for a prompt/question or a select choice. The label can have media\n * attached to it as well as text (such as audio, video or an image).\n */\npublic class AudioVideoImageTextLabel extends RelativeLayout implements View.OnClickListener {\n    AudioVideoImageTextLabelBinding binding;\n\n    private TextView textLabel;\n    private int originalTextColor;\n    private int playTextColor = Color.BLUE;\n    private SelectItemClickListener listener;\n    private File videoFile;\n    private File bigImageFile;\n    private MediaUtils mediaUtils;\n\n    public AudioVideoImageTextLabel(Context context) {\n        super(context);\n        binding = AudioVideoImageTextLabelBinding.inflate(LayoutInflater.from(context), this, true);\n        textLabel = binding.textLabel;\n    }\n\n    public AudioVideoImageTextLabel(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        binding = AudioVideoImageTextLabelBinding.inflate(LayoutInflater.from(context), this, true);\n        textLabel = binding.textLabel;\n    }\n\n    public void setTextView(TextView questionText) {\n        textLabel = questionText;\n        textLabel.setId(R.id.text_label);\n        textLabel.setOnClickListener(v -> {\n            if (listener != null) {\n                listener.onItemClicked();\n            }\n        });\n\n        binding.textContainer.removeAllViews();\n        binding.textContainer.addView(textLabel);\n    }\n\n    public void setText(String questionText, boolean isRequiredQuestion, float fontSize) {\n        if (questionText != null && !questionText.isEmpty()) {\n            textLabel.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);\n            textLabel.setText(FormEntryPromptUtils.styledQuestionText(questionText, isRequiredQuestion));\n            textLabel.setMovementMethod(LinkMovementMethod.getInstance());\n\n            // Wrap to the size of the parent view\n            textLabel.setHorizontallyScrolling(false);\n        } else {\n            textLabel.setVisibility(View.GONE);\n        }\n    }\n\n    public void setAudio(String audioURI, AudioPlayer audioPlayer) {\n        String clipID = getTag() != null ? getTag().toString() : \"\";\n\n        originalTextColor = textLabel.getTextColors().getDefaultColor();\n        audioPlayer.onPlayingChanged(clipID, isPlaying -> {\n            binding.audioButton.setPlaying(isPlaying);\n\n            if (isPlaying) {\n                textLabel.setTextColor(playTextColor);\n            } else {\n                textLabel.setTextColor(originalTextColor);\n            }\n        });\n\n        binding.audioButton.setListener(new AudioButton.Listener() {\n            @Override\n            public void onPlayClicked() {\n                audioPlayer.play(new Clip(clipID, audioURI));\n            }\n\n            @Override\n            public void onStopClicked() {\n                audioPlayer.stop();\n            }\n        });\n\n        binding.audioButton.setVisibility(VISIBLE);\n        binding.mediaButtons.setVisibility(VISIBLE);\n    }\n\n    public void setImage(@NonNull File imageFile, ImageLoader imageLoader) {\n        if (imageFile.exists()) {\n            binding.imageView.layout(0, 0, 0, 0);\n\n            imageLoader.loadImage(binding.imageView, imageFile, ImageView.ScaleType.CENTER_INSIDE, null);\n            binding.imageView.setVisibility(VISIBLE);\n            binding.imageView.setOnClickListener(this);\n        } else {\n            binding.missingImage.setVisibility(VISIBLE);\n            binding.missingImage.setText(getContext().getString(org.odk.collect.strings.R.string.file_missing, imageFile));\n        }\n    }\n\n    public void setBigImage(@NonNull File bigImageFile) {\n        this.bigImageFile = bigImageFile;\n    }\n\n    public void setVideo(@NonNull File videoFile) {\n        this.videoFile = videoFile;\n\n        binding.videoButton.setVisibility(VISIBLE);\n        binding.mediaButtons.setVisibility(VISIBLE);\n        binding.videoButton.setOnClickListener(this);\n    }\n\n    public void setPlayTextColor(int textColor) {\n        playTextColor = textColor;\n    }\n\n    public void setMediaUtils(MediaUtils mediaUtils) {\n        this.mediaUtils = mediaUtils;\n    }\n\n    public void playVideo() {\n        mediaUtils.openFile(getContext(), videoFile, \"video/*\");\n    }\n\n    public TextView getLabelTextView() {\n        return textLabel;\n    }\n\n    public ImageView getImageView() {\n        return binding.imageView;\n    }\n\n    public TextView getMissingImage() {\n        return binding.missingImage;\n    }\n\n    public Button getVideoButton() {\n        return binding.videoButton;\n    }\n\n    public Button getAudioButton() {\n        return binding.audioButton;\n    }\n\n    @Override\n    public void onClick(View v) {\n        if (v.getId() == R.id.videoButton) {\n            playVideo();\n        } else if (v.getId() == R.id.imageView) {\n            onImageClick();\n        }\n    }\n\n    @Override\n    public void setEnabled(boolean enabled) {\n        textLabel.setEnabled(enabled);\n        binding.imageView.setEnabled(enabled);\n    }\n\n    @Override\n    public boolean isEnabled() {\n        return textLabel.isEnabled() && binding.imageView.isEnabled();\n    }\n\n    private void onImageClick() {\n        if (bigImageFile != null) {\n            mediaUtils.openFile(getContext(), bigImageFile, \"image/*\");\n        } else {\n            selectItem();\n        }\n    }\n\n    private void selectItem() {\n        if (textLabel instanceof RadioButton) {\n            ((RadioButton) textLabel).setChecked(true);\n        } else if (textLabel instanceof CheckBox) {\n            CheckBox checkbox = (CheckBox) textLabel;\n            checkbox.setChecked(!checkbox.isChecked());\n        }\n        if (listener != null) {\n            listener.onItemClicked();\n        }\n    }\n\n    public void setItemClickListener(SelectItemClickListener listener) {\n        this.listener = listener;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/questions/NoButtonsItem.java",
    "content": "package org.odk.collect.android.formentry.questions;\n\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.FrameLayout;\nimport android.widget.ImageView;\n\nimport org.odk.collect.android.databinding.NoButtonsItemLayoutBinding;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.imageloader.ImageLoader;\n\nimport java.io.File;\n\npublic class NoButtonsItem extends FrameLayout {\n    NoButtonsItemLayoutBinding binding;\n    private final ImageLoader imageLoader;\n\n    public NoButtonsItem(Context context, boolean enabled, ImageLoader imageLoader) {\n        super(context);\n        binding = NoButtonsItemLayoutBinding.inflate(LayoutInflater.from(context), this, true);\n        this.imageLoader = imageLoader;\n\n        setLongClickable(true);\n        setEnabled(enabled);\n    }\n\n    public void setUpNoButtonsItem(File imageFile, String choiceText, String errorMsg, boolean isInGridView) {\n        if (imageFile != null && imageFile.exists()) {\n            binding.imageView.layout(0, 0, 0, 0);\n            binding.imageView.setVisibility(View.VISIBLE);\n            if (isInGridView) {\n                imageLoader.loadImage(binding.imageView, imageFile, ImageView.ScaleType.FIT_CENTER, null);\n            } else {\n                imageLoader.loadImage(binding.imageView, imageFile, ImageView.ScaleType.CENTER_INSIDE, null);\n            }\n        } else {\n            binding.label.setVisibility(View.VISIBLE);\n            binding.label.setTextSize(TypedValue.COMPLEX_UNIT_DIP, QuestionFontSizeUtils.getQuestionFontSize());\n            binding.label.setText(choiceText == null || choiceText.isEmpty() ? errorMsg : choiceText);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/questions/QuestionDetails.java",
    "content": "package org.odk.collect.android.formentry.questions;\n\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.widgets.QuestionWidget;\n\n/**\n * Data class representing a \"question\" for use with {@link QuestionWidget}\n * and its subclasses\n */\npublic class QuestionDetails {\n\n    private final FormEntryPrompt prompt;\n    private final boolean isReadOnly;\n\n    public QuestionDetails(FormEntryPrompt prompt) {\n        this(prompt, false);\n    }\n\n    public QuestionDetails(FormEntryPrompt prompt, boolean readOnlyOverride) {\n        this.prompt = prompt;\n        this.isReadOnly = readOnlyOverride || prompt.isReadOnly();\n    }\n\n    public FormEntryPrompt getPrompt() {\n        return prompt;\n    }\n\n    public boolean isReadOnly() {\n        return isReadOnly;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/questions/SelectChoiceUtils.kt",
    "content": "package org.odk.collect.android.formentry.questions\n\nimport org.javarosa.core.model.SelectChoice\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.javarosa.measure.Measure\nimport org.javarosa.xpath.parser.XPathSyntaxException\nimport org.odk.collect.android.dynamicpreload.ExternalDataUtil\nimport org.odk.collect.android.exception.ExternalDataException\nimport org.odk.collect.android.fastexternalitemset.ItemsetDao\nimport org.odk.collect.android.fastexternalitemset.ItemsetDbAdapter\nimport org.odk.collect.android.fastexternalitemset.XPathParseTool\nimport org.odk.collect.android.javarosawrapper.FormController\nimport java.io.FileNotFoundException\n\nobject SelectChoiceUtils {\n\n    @JvmStatic\n    @Throws(FileNotFoundException::class, XPathSyntaxException::class, ExternalDataException::class)\n    fun loadSelectChoices(prompt: FormEntryPrompt, formController: FormController): List<SelectChoice> {\n        Measure.log(\"LoadSelectChoices\")\n\n        return when {\n            isFastExternalItemsetUsed(prompt) -> readFastExternalItems(prompt, formController)\n            isSearchPulldataItemsetUsed(prompt) -> readSearchPulldataItems(prompt, formController)\n            else -> prompt.selectChoices ?: emptyList()\n        }\n    }\n\n    private fun isFastExternalItemsetUsed(prompt: FormEntryPrompt): Boolean {\n        val questionDef = prompt.question\n        return questionDef?.getAdditionalAttribute(null, \"query\") != null\n    }\n\n    private fun isSearchPulldataItemsetUsed(prompt: FormEntryPrompt): Boolean {\n        return ExternalDataUtil.getSearchXPathExpression(prompt.appearanceHint) != null\n    }\n\n    @Throws(FileNotFoundException::class, XPathSyntaxException::class)\n    private fun readFastExternalItems(prompt: FormEntryPrompt, formController: FormController): List<SelectChoice> {\n        return ItemsetDao(ItemsetDbAdapter()).getItems(prompt, XPathParseTool(), formController)\n    }\n\n    @Throws(FileNotFoundException::class, ExternalDataException::class)\n    private fun readSearchPulldataItems(prompt: FormEntryPrompt, formController: FormController): List<SelectChoice> {\n        // SurveyCTO-added support for dynamic select content (from .csv files)\n        val xpathFuncExpr =\n            ExternalDataUtil.getSearchXPathExpression(prompt.appearanceHint)\n        return ExternalDataUtil.populateExternalChoices(prompt, xpathFuncExpr, formController)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/repeats/AddRepeatDialog.kt",
    "content": "package org.odk.collect.android.formentry.repeats\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.google.android.material.textview.MaterialTextView\nimport org.odk.collect.android.R\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\nimport org.odk.collect.strings.R.string\n\nobject AddRepeatDialog {\n\n    @JvmStatic\n    fun show(context: Context, groupLabel: String?, listener: Listener) {\n        val view = LayoutInflater.from(context).inflate(R.layout.add_repeat_dialog_layout, null)\n\n        view.findViewById<MaterialTextView>(R.id.message).text = if (groupLabel.isNullOrBlank()) {\n            context.getString(string.add_another_question)\n        } else {\n            context.getString(string.add_repeat_question, groupLabel)\n        }\n\n        val alertDialog = MaterialAlertDialogBuilder(context)\n            .setView(view)\n            .setCancelable(false)\n            .create()\n\n        view.findViewById<MultiClickSafeMaterialButton>(R.id.add_button).setOnClickListener {\n            listener.onAddRepeatClicked()\n            alertDialog.dismiss()\n        }\n\n        view.findViewById<MultiClickSafeMaterialButton>(R.id.do_not_add_button).setOnClickListener {\n            listener.onCancelClicked()\n            alertDialog.dismiss()\n        }\n\n        alertDialog.show()\n    }\n\n    interface Listener {\n        fun onAddRepeatClicked()\n\n        fun onCancelClicked()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/repeats/DeleteRepeatDialogFragment.java",
    "content": "package org.odk.collect.android.formentry.repeats;\n\nimport static android.content.DialogInterface.BUTTON_NEGATIVE;\nimport static android.content.DialogInterface.BUTTON_POSITIVE;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.odk.collect.android.formentry.FormEntryViewModel;\nimport org.odk.collect.android.formentry.audit.AuditEvent;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.javarosawrapper.FormController;\n\npublic class DeleteRepeatDialogFragment extends DialogFragment {\n\n    public static final String REQUEST_DELETE_REPEAT = \"DELETE_REPEAT\";\n\n    private final ViewModelProvider.Factory viewModelFactory;\n    private FormEntryViewModel formEntryViewModel;\n\n    public DeleteRepeatDialogFragment(ViewModelProvider.Factory viewModelFactory) {\n        this.viewModelFactory = viewModelFactory;\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        DaggerUtils.getComponent(context).inject(this);\n\n        formEntryViewModel = new ViewModelProvider(requireActivity(), viewModelFactory).get(FormEntryViewModel.class);\n    }\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        super.onCreateDialog(savedInstanceState);\n\n        FormController formController = formEntryViewModel.getFormController();\n\n        String name = formController.getLastRepeatedGroupName();\n        int repeatCount = formController.getLastRepeatedGroupRepeatCount();\n        if (repeatCount != -1) {\n            name += \" (\" + (repeatCount + 1) + \")\";\n        }\n\n        AlertDialog alertDialog = new MaterialAlertDialogBuilder(getActivity()).create();\n        alertDialog.setTitle(getActivity().getString(org.odk.collect.strings.R.string.delete_repeat_ask));\n        alertDialog.setMessage(getActivity().getString(org.odk.collect.strings.R.string.delete_repeat_confirm, name));\n        DialogInterface.OnClickListener quitListener = (dialog, i) -> {\n            if (i == BUTTON_POSITIVE) { // yes\n                formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.DELETE_REPEAT, true, System.currentTimeMillis());\n                formController.deleteRepeat();\n                getParentFragmentManager().setFragmentResult(REQUEST_DELETE_REPEAT, new Bundle());\n            }\n            alertDialog.cancel();\n            dismiss();\n        };\n        setCancelable(false);\n        alertDialog.setCancelable(false);\n        alertDialog.setButton(BUTTON_POSITIVE, getActivity().getString(org.odk.collect.strings.R.string.discard_group), quitListener);\n        alertDialog.setButton(BUTTON_NEGATIVE, getActivity().getString(org.odk.collect.strings.R.string.delete_repeat_no), quitListener);\n\n        return alertDialog;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/saving/DiskFormSaver.java",
    "content": "package org.odk.collect.android.formentry.saving;\n\nimport android.net.Uri;\n\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.tasks.SaveFormToDisk;\nimport org.odk.collect.android.tasks.SaveToDiskResult;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.entities.storage.EntitiesRepository;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.shared.DebugLogger;\n\nimport java.util.ArrayList;\n\npublic class DiskFormSaver implements FormSaver {\n\n    @Override\n    public SaveToDiskResult save(Uri instanceContentURI, FormController formController, MediaUtils mediaUtils, boolean shouldFinalize, boolean exitAfter,\n                                 String updatedSaveName, ProgressListener progressListener, ArrayList<String> tempFiles, String currentProjectId, EntitiesRepository entitiesRepository, InstancesRepository instancesRepository, DebugLogger debugLogger) {\n        SaveFormToDisk saveFormToDisk = new SaveFormToDisk(formController, mediaUtils, exitAfter, shouldFinalize,\n                updatedSaveName, instanceContentURI, tempFiles, currentProjectId, entitiesRepository, instancesRepository, debugLogger);\n        return saveFormToDisk.saveForm(progressListener);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java",
    "content": "package org.odk.collect.android.formentry.saving;\n\nimport static org.odk.collect.android.tasks.SaveFormToDisk.SAVED;\nimport static org.odk.collect.android.tasks.SaveFormToDisk.SAVED_AND_EXIT;\nimport static org.odk.collect.android.tasks.SaveFormToDisk.SAVE_ERROR;\nimport static org.odk.collect.shared.strings.StringUtils.isBlank;\n\nimport android.net.Uri;\nimport android.os.AsyncTask;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.lifecycle.SavedStateHandle;\nimport androidx.lifecycle.ViewModel;\n\nimport org.apache.commons.io.IOUtils;\nimport org.javarosa.form.api.FormEntryController;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.dynamicpreload.ExternalDataManager;\nimport org.odk.collect.android.formentry.FormSession;\nimport org.odk.collect.android.formentry.audit.AuditEvent;\nimport org.odk.collect.android.formentry.audit.AuditUtils;\nimport org.odk.collect.android.instancemanagement.InstancesDataService;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.android.tasks.SaveFormToDisk;\nimport org.odk.collect.android.tasks.SaveToDiskResult;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.androidshared.livedata.LiveDataUtils;\nimport org.odk.collect.async.Cancellable;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.entities.storage.EntitiesRepository;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.forms.savepoints.SavepointsRepository;\nimport org.odk.collect.material.MaterialProgressDialogFragment;\nimport org.odk.collect.shared.DebugLogger;\nimport org.odk.collect.shared.strings.Md5;\nimport org.odk.collect.utilities.Result;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Supplier;\n\nimport timber.log.Timber;\n\npublic class FormSaveViewModel extends ViewModel implements MaterialProgressDialogFragment.OnCancelCallback, QuestionMediaManager {\n\n    public static final String ORIGINAL_FILES = \"originalFiles\";\n    public static final String RECENT_FILES = \"recentFiles\";\n\n    private final SavedStateHandle stateHandle;\n    private final Supplier<Long> clock;\n    private final FormSaver formSaver;\n    private final MediaUtils mediaUtils;\n\n    private final MutableLiveData<SaveResult> saveResult = new MutableLiveData<>(null);\n\n    private String reason = \"\";\n\n    private Map<String, String> originalFiles = new HashMap<>();\n    private Map<String, String> recentFiles = new HashMap<>();\n    private final MutableLiveData<Boolean> isSavingAnswerFile = new MutableLiveData<>(false);\n    private final MutableLiveData<String> answerFileError = new MutableLiveData<>(null);\n\n\n    @Nullable\n    private FormController formController;\n\n    @Nullable\n    private AsyncTask<Void, String, SaveToDiskResult> saveTask;\n\n    private final Scheduler scheduler;\n    private final AudioRecorder audioRecorder;\n    private final ProjectsDataService projectsDataService;\n    private final EntitiesRepository entitiesRepository;\n    private final InstancesRepository instancesRepository;\n    private final SavepointsRepository savepointsRepository;\n    private Form form;\n    private Instance instance;\n    private final Cancellable formSessionObserver;\n    private InstancesDataService instancesDataService;\n    private DebugLogger debugLogger;\n\n    public FormSaveViewModel(SavedStateHandle stateHandle, Supplier<Long> clock, FormSaver formSaver,\n                             MediaUtils mediaUtils, Scheduler scheduler, AudioRecorder audioRecorder,\n                             ProjectsDataService projectsDataService, LiveData<FormSession> formSession,\n                             EntitiesRepository entitiesRepository, InstancesRepository instancesRepository,\n                             SavepointsRepository savepointsRepository, InstancesDataService instancesDataService,\n                             DebugLogger debugLogger\n    ) {\n        this.stateHandle = stateHandle;\n        this.clock = clock;\n        this.formSaver = formSaver;\n        this.mediaUtils = mediaUtils;\n        this.scheduler = scheduler;\n        this.audioRecorder = audioRecorder;\n        this.projectsDataService = projectsDataService;\n        this.entitiesRepository = entitiesRepository;\n        this.instancesRepository = instancesRepository;\n        this.savepointsRepository = savepointsRepository;\n        this.instancesDataService = instancesDataService;\n        this.debugLogger = debugLogger;\n\n        if (stateHandle.get(ORIGINAL_FILES) != null) {\n            originalFiles = stateHandle.get(ORIGINAL_FILES);\n        }\n        if (stateHandle.get(RECENT_FILES) != null) {\n            recentFiles = stateHandle.get(RECENT_FILES);\n        }\n\n        formSessionObserver = LiveDataUtils.observe(formSession, it -> {\n            formController = it.getFormController();\n            form = it.getForm();\n            instance = it.getInstance();\n        });\n    }\n\n    @Override\n    protected void onCleared() {\n        formSessionObserver.cancel();\n    }\n\n    public void saveForm(Uri instanceContentURI, boolean shouldFinalize, String updatedSaveName, boolean viewExiting) {\n        if (isSaving() || formController == null) {\n            return;\n        }\n\n        SaveRequest saveRequest = new SaveRequest(instanceContentURI, viewExiting, updatedSaveName, shouldFinalize);\n        formController.getAuditEventLogger().flush();\n\n        if (requiresReasonToSave()) {\n            this.saveResult.setValue(new SaveResult(SaveResult.State.CHANGE_REASON_REQUIRED, saveRequest));\n        } else if (viewExiting && audioRecorder.isRecording()) {\n            this.saveResult.setValue(new SaveResult(SaveResult.State.WAITING_TO_SAVE, saveRequest));\n            audioRecorder.stop();\n        } else {\n            this.saveResult.setValue(new SaveResult(SaveResult.State.SAVING, saveRequest));\n            saveToDisk(saveRequest);\n        }\n    }\n\n    // Cleanup when user exits a form without saving\n    public void ignoreChanges() {\n        if (audioRecorder.isRecording()) {\n            audioRecorder.cleanUp();\n        }\n\n        ExternalDataManager manager = Collect.getInstance().getExternalDataManager();\n        if (manager != null) {\n            manager.close();\n        }\n\n        if (formController != null) {\n            formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_EXIT, true, System.currentTimeMillis());\n\n            if (formController.getInstanceFile() != null) {\n                removeSavepoint(form.getDbId(), instance != null ? instance.getDbId() : null);\n                SaveFormToDisk.removeIndexFile(formController.getInstanceFile().getName());\n\n                if (canBeFullyDiscarded()) {\n                    if (instance != null) {\n                        scheduler.immediate(() -> {\n                            instancesDataService.deleteInstances(\n                                    projectsDataService.getCurrentProject().getValue().getUuid(),\n                                    new long[] {instance.getDbId()}\n                            );\n                            return null;\n                        }, result -> {\n                        });\n                    }\n                    String instanceFolder = formController.getInstanceFile().getParent();\n                    FileUtils.purgeMediaPath(instanceFolder);\n                }\n            }\n        }\n\n        for (String filePath : recentFiles.values()) {\n            mediaUtils.deleteMediaFile(filePath);\n        }\n\n        clearMediaFiles();\n    }\n\n    public void resumeSave() {\n        if (saveResult.getValue() != null) {\n            SaveRequest saveRequest = saveResult.getValue().request;\n\n            if (saveResult.getValue().getState() == SaveResult.State.CHANGE_REASON_REQUIRED) {\n                if (!saveReason()) {\n                    return;\n                } else if (saveRequest.viewExiting && audioRecorder.isRecording()) {\n                    this.saveResult.setValue(new SaveResult(SaveResult.State.WAITING_TO_SAVE, saveRequest));\n                    audioRecorder.stop();\n                    return;\n                }\n            }\n\n            this.saveResult.setValue(new SaveResult(SaveResult.State.SAVING, saveRequest));\n            saveToDisk(saveRequest);\n        }\n    }\n\n    @Nullable\n    public String getAbsoluteInstancePath() {\n        return formController != null ? formController.getAbsoluteInstancePath() : null;\n    }\n\n    public boolean isSaving() {\n        return saveResult.getValue() != null && saveResult.getValue().getState().equals(SaveResult.State.SAVING);\n    }\n\n    @Override\n    public boolean cancel() {\n        if (saveTask != null) {\n            return saveTask.cancel(true);\n        } else {\n            return false;\n        }\n    }\n\n    public void setReason(@NonNull String reason) {\n        this.reason = reason;\n    }\n\n    public String getReason() {\n        return reason;\n    }\n\n    private boolean saveReason() {\n        if (reason == null || isBlank(reason) || formController == null) {\n            return false;\n        }\n\n        formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.CHANGE_REASON, null, true, null, clock.get(), reason);\n        return true;\n    }\n\n    private void saveToDisk(SaveRequest saveRequest) {\n        saveTask = new SaveTask(saveRequest, formSaver, formController, mediaUtils, new SaveTask.Listener() {\n            @Override\n            public void onProgressPublished(String progress) {\n                saveResult.setValue(new SaveResult(SaveResult.State.SAVING, saveRequest, progress));\n            }\n\n            @Override\n            public void onComplete(SaveToDiskResult saveToDiskResult) {\n                handleTaskResult(saveToDiskResult, saveRequest);\n                clearMediaFiles();\n            }\n        }, new ArrayList<>(originalFiles.values()), projectsDataService.requireCurrentProject().getUuid(), entitiesRepository, instancesRepository, debugLogger).execute();\n    }\n\n    private void handleTaskResult(SaveToDiskResult taskResult, SaveRequest saveRequest) {\n        if (formController == null) {\n            return;\n        }\n\n        if (taskResult.getSaveResult() != SAVE_ERROR) {\n            removeSavepoint(form.getDbId(), instance != null ? instance.getDbId() : null);\n        }\n\n        instance = taskResult.getInstance();\n\n        switch (taskResult.getSaveResult()) {\n            case SAVED:\n            case SAVED_AND_EXIT: {\n                formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_SAVE, false, clock.get());\n\n                if (saveRequest.viewExiting) {\n                    if (saveRequest.shouldFinalize) {\n                        formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_EXIT, false, clock.get());\n                        formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_FINALIZE, true, clock.get());\n\n                        instancesDataService.instanceFinalized(projectsDataService.requireCurrentProject().getUuid(), form);\n                    } else {\n                        formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_EXIT, true, clock.get());\n                    }\n                } else {\n                    AuditUtils.logCurrentScreen(formController, formController.getAuditEventLogger(), clock.get());\n                }\n\n                saveResult.setValue(new SaveResult(SaveResult.State.SAVED, saveRequest, taskResult.getSaveErrorMessage()));\n                break;\n            }\n\n            case SAVE_ERROR: {\n                formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.SAVE_ERROR, true, clock.get());\n                saveResult.setValue(new SaveResult(SaveResult.State.SAVE_ERROR, saveRequest, taskResult.getSaveErrorMessage()));\n                break;\n            }\n\n            case SaveFormToDisk.ENCRYPTION_ERROR: {\n                formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FINALIZE_ERROR, true, clock.get());\n                saveResult.setValue(new SaveResult(SaveResult.State.FINALIZE_ERROR, saveRequest, taskResult.getSaveErrorMessage()));\n                break;\n            }\n\n            case FormEntryController.ANSWER_CONSTRAINT_VIOLATED:\n            case FormEntryController.ANSWER_REQUIRED_BUT_EMPTY: {\n                formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.CONSTRAINT_ERROR, true, clock.get());\n                saveResult.setValue(new SaveResult(SaveResult.State.CONSTRAINT_ERROR, saveRequest, taskResult.getSaveErrorMessage()));\n                break;\n            }\n        }\n    }\n\n    public LiveData<SaveResult> getSaveResult() {\n        return saveResult;\n    }\n\n    public void resumeFormEntry() {\n        saveResult.setValue(null);\n    }\n\n    private boolean requiresReasonToSave() {\n        return formController != null\n                && formController.isEditing()\n                && formController.getAuditEventLogger().isChangeReasonRequired();\n    }\n\n    public String getFormName() {\n        if (formController == null) {\n            return null;\n        }\n        return formController.getFormTitle();\n    }\n\n    @Override\n    public void deleteAnswerFile(String questionIndex, String fileName) {\n        if (questionIndex != null && fileName != null) {\n            // We don't want to delete the \"original\" answer file as we might need to restore it\n            // but we can delete any follow up deletions\n            if (originalFiles.containsKey(questionIndex)) {\n                mediaUtils.deleteMediaFile(fileName);\n            } else {\n                originalFiles.put(questionIndex, fileName);\n                stateHandle.set(ORIGINAL_FILES, originalFiles);\n            }\n        }\n    }\n\n    @Override\n    public void replaceAnswerFile(String questionIndex, String fileName) {\n        if (questionIndex != null && fileName != null) {\n            // If we're replacing an answer's file for a second time we can just get rid of the\n            // first (replacement) file we were going to use\n            if (recentFiles.containsKey(questionIndex)) {\n                mediaUtils.deleteMediaFile(recentFiles.get(questionIndex));\n            }\n            recentFiles.put(questionIndex, fileName);\n            stateHandle.set(RECENT_FILES, recentFiles);\n        }\n    }\n\n    @Override\n    public LiveData<Result<File>> createAnswerFile(File file) {\n        MutableLiveData<Result<File>> liveData = new MutableLiveData<>(null);\n\n        isSavingAnswerFile.setValue(true);\n        scheduler.immediate(() -> {\n            String newFileHash = Md5.getMd5Hash(file);\n            String instanceDir = formController.getInstanceFile().getParent();\n\n            File[] answerFiles = new File(instanceDir).listFiles();\n            for (File answerFile : answerFiles) {\n                if (Md5.getMd5Hash(answerFile).equals(newFileHash)) {\n                    return answerFile;\n                }\n            }\n\n            String fileName = file.getName();\n            String extension = fileName.substring(fileName.lastIndexOf('.') + 1);\n            String newFileName = System.currentTimeMillis() + \".\" + extension;\n            String newFilePath = instanceDir + File.separator + newFileName;\n\n            try (InputStream inputStream = new FileInputStream(file)) {\n                try (OutputStream outputStream = new FileOutputStream(newFilePath)) {\n                    IOUtils.copy(inputStream, outputStream);\n                }\n            } catch (IOException e) {\n                Timber.e(e);\n                return null;\n            }\n\n            return new File(newFilePath);\n        }, answerFile -> {\n            liveData.setValue(new Result<>(answerFile));\n            isSavingAnswerFile.setValue(false);\n\n            if (answerFile == null) {\n                answerFileError.setValue(file.getAbsolutePath());\n            }\n        });\n\n        return liveData;\n    }\n\n    @Override\n    @Nullable\n    public File getAnswerFile(@Nullable String fileName) {\n        if (fileName != null && formController != null && formController.getInstanceFile() != null) {\n            return new File(formController.getInstanceFile().getParent(), fileName);\n        } else {\n            return null;\n        }\n    }\n\n    public LiveData<Boolean> isSavingAnswerFile() {\n        return isSavingAnswerFile;\n    }\n\n    private void clearMediaFiles() {\n        originalFiles.clear();\n        recentFiles.clear();\n    }\n\n    public LiveData<String> getAnswerFileError() {\n        return answerFileError;\n    }\n\n    public void answerFileErrorDisplayed() {\n        answerFileError.setValue(null);\n    }\n\n    public boolean canBeFullyDiscarded() {\n        return instance == null || instance.getStatus().equals(Instance.STATUS_NEW_EDIT);\n    }\n\n    public Long getLastSavedTime() {\n        return instance != null ? instance.getLastStatusChangeDate() : null;\n    }\n\n    @Nullable\n    public Instance getInstance() {\n        return instance;\n    }\n\n    private void removeSavepoint(long formDbId, @Nullable Long instanceDbId) {\n        scheduler.immediate(() -> {\n            savepointsRepository.delete(formDbId, instanceDbId);\n            return null;\n        }, result -> {\n        });\n    }\n\n    public static class SaveResult {\n        private final State state;\n        private final String message;\n        private final SaveRequest request;\n\n        SaveResult(State state, SaveRequest request) {\n            this(state, request, null);\n        }\n\n        SaveResult(State state, SaveRequest request, String message) {\n            this.state = state;\n            this.message = message;\n            this.request = request;\n        }\n\n        public State getState() {\n            return state;\n        }\n\n        public String getMessage() {\n            return message;\n        }\n\n        public enum State {\n            CHANGE_REASON_REQUIRED,\n            SAVING,\n            SAVED,\n            SAVE_ERROR,\n            FINALIZE_ERROR,\n            CONSTRAINT_ERROR,\n            WAITING_TO_SAVE\n        }\n\n        public SaveRequest getRequest() {\n            return request;\n        }\n    }\n\n    public static class SaveRequest {\n\n        private final boolean shouldFinalize;\n        private final boolean viewExiting;\n        private final String updatedSaveName;\n        private final Uri uri;\n\n        SaveRequest(Uri instanceContentURI, boolean viewExiting, String updatedSaveName, boolean shouldFinalize) {\n            this.shouldFinalize = shouldFinalize;\n            this.viewExiting = viewExiting;\n            this.updatedSaveName = updatedSaveName;\n            this.uri = instanceContentURI;\n        }\n\n        public boolean shouldFinalize() {\n            return shouldFinalize;\n        }\n\n        public boolean viewExiting() {\n            return viewExiting;\n        }\n    }\n\n    private static class SaveTask extends AsyncTask<Void, String, SaveToDiskResult> {\n\n        private final SaveRequest saveRequest;\n        private final FormSaver formSaver;\n\n        private final Listener listener;\n        private final FormController formController;\n        private final MediaUtils mediaUtils;\n        private final ArrayList<String> tempFiles;\n        private final String currentProjectId;\n        private final EntitiesRepository entitiesRepository;\n        private final InstancesRepository instancesRepository;\n        private final DebugLogger debugLogger;\n\n        SaveTask(SaveRequest saveRequest, FormSaver formSaver, FormController formController, MediaUtils mediaUtils,\n                 Listener listener, ArrayList<String> tempFiles, String currentProjectId, EntitiesRepository entitiesRepository, InstancesRepository instancesRepository, DebugLogger debugLogger) {\n            this.saveRequest = saveRequest;\n            this.formSaver = formSaver;\n            this.listener = listener;\n            this.formController = formController;\n            this.mediaUtils = mediaUtils;\n            this.tempFiles = tempFiles;\n            this.currentProjectId = currentProjectId;\n            this.entitiesRepository = entitiesRepository;\n            this.instancesRepository = instancesRepository;\n            this.debugLogger = debugLogger;\n        }\n\n        @Override\n        protected SaveToDiskResult doInBackground(Void... voids) {\n            return formSaver.save(saveRequest.uri, formController,\n                    mediaUtils, saveRequest.shouldFinalize,\n                    saveRequest.viewExiting, saveRequest.updatedSaveName,\n                    this::publishProgress, tempFiles,\n                    currentProjectId, entitiesRepository, instancesRepository, debugLogger);\n        }\n\n        @Override\n        protected void onProgressUpdate(String... values) {\n            listener.onProgressPublished(values[0]);\n        }\n\n        @Override\n        protected void onPostExecute(SaveToDiskResult saveToDiskResult) {\n            listener.onComplete(saveToDiskResult);\n        }\n\n        interface Listener {\n            void onProgressPublished(String progress);\n\n            void onComplete(SaveToDiskResult saveToDiskResult);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaver.java",
    "content": "package org.odk.collect.android.formentry.saving;\n\nimport android.net.Uri;\n\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.tasks.SaveToDiskResult;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.entities.storage.EntitiesRepository;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.shared.DebugLogger;\n\nimport java.util.ArrayList;\n\npublic interface FormSaver {\n    SaveToDiskResult save(Uri instanceContentURI, FormController formController, MediaUtils mediaUtils, boolean shouldFinalize, boolean exitAfter,\n                          String updatedSaveName, ProgressListener progressListener, ArrayList<String> tempFiles, String currentProjectId, EntitiesRepository entitiesRepository, InstancesRepository instancesRepository, DebugLogger debugLogger);\n\n    interface ProgressListener {\n        void onProgressUpdate(String message);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/saving/SaveAnswerFileErrorDialogFragment.java",
    "content": "package org.odk.collect.android.formentry.saving;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\npublic class SaveAnswerFileErrorDialogFragment extends DialogFragment {\n\n    private final ViewModelProvider.Factory viewModelFactory;\n    private FormSaveViewModel formSaveViewModel;\n\n    public SaveAnswerFileErrorDialogFragment(ViewModelProvider.Factory viewModelFactory) {\n        this.viewModelFactory = viewModelFactory;\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n\n        ViewModelProvider viewModelProvider = new ViewModelProvider(requireActivity(), viewModelFactory);\n        formSaveViewModel = viewModelProvider.get(FormSaveViewModel.class);\n    }\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        return new MaterialAlertDialogBuilder(requireContext())\n                .setTitle(org.odk.collect.strings.R.string.error_occured)\n                .setMessage(getString(org.odk.collect.strings.R.string.answer_file_copy_failed_message, formSaveViewModel.getAnswerFileError().getValue()))\n                .setPositiveButton(org.odk.collect.strings.R.string.ok, null)\n                .create();\n    }\n\n    @Override\n    public void onDismiss(@NonNull DialogInterface dialog) {\n        super.onDismiss(dialog);\n        formSaveViewModel.answerFileErrorDisplayed();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/saving/SaveAnswerFileProgressDialogFragment.java",
    "content": "package org.odk.collect.android.formentry.saving;\n\nimport android.content.Context;\n\nimport androidx.annotation.NonNull;\n\nimport org.odk.collect.material.MaterialProgressDialogFragment;\n\npublic class SaveAnswerFileProgressDialogFragment extends MaterialProgressDialogFragment {\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        setMessage(getString(org.odk.collect.strings.R.string.saving_file));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formentry/saving/SaveFormProgressDialogFragment.java",
    "content": "package org.odk.collect.android.formentry.saving;\n\nimport static org.odk.collect.android.formentry.saving.FormSaveViewModel.SaveResult.State.SAVING;\n\nimport android.content.Context;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.odk.collect.material.MaterialProgressDialogFragment;\n\npublic class SaveFormProgressDialogFragment extends MaterialProgressDialogFragment {\n\n    private final ViewModelProvider.Factory viewModelFactory;\n    private FormSaveViewModel viewModel;\n\n    public SaveFormProgressDialogFragment(ViewModelProvider.Factory viewModelFactory) {\n        this.viewModelFactory = viewModelFactory;\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n\n        viewModel = new ViewModelProvider(requireActivity(), viewModelFactory).get(FormSaveViewModel.class);\n\n        setCancelable(false);\n        setTitle(getString(org.odk.collect.strings.R.string.saving_form));\n\n        viewModel.getSaveResult().observe(this, result -> {\n            if (result != null && result.getState() == SAVING && result.getMessage() != null) {\n                setMessage(getString(org.odk.collect.strings.R.string.please_wait) + \"\\n\\n\" + result.getMessage());\n            } else {\n                setMessage(getString(org.odk.collect.strings.R.string.please_wait));\n            }\n        });\n    }\n\n    @Override\n    protected OnCancelCallback getOnCancelCallback() {\n        return viewModel;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formhierarchy/FormHierarchyFragment.java",
    "content": "package org.odk.collect.android.formhierarchy;\n\nimport static android.app.Activity.RESULT_OK;\nimport static org.odk.collect.android.formentry.repeats.DeleteRepeatDialogFragment.REQUEST_DELETE_REPEAT;\nimport static org.odk.collect.android.javarosawrapper.FormIndexUtils.getPreviousLevel;\nimport static org.odk.collect.androidshared.ui.SnackbarUtils.showSnackbar;\n\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.Menu;\nimport android.view.MenuInflater;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.activity.OnBackPressedCallback;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.core.content.ContextCompat;\nimport androidx.core.view.MenuHost;\nimport androidx.core.view.MenuProvider;\nimport androidx.fragment.app.Fragment;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.recyclerview.widget.DividerItemDecoration;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.GroupDef;\nimport org.javarosa.core.model.IFormElement;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.javarosa.form.api.FormEntryController;\nimport org.javarosa.form.api.FormEntryModel;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.databinding.FormHierarchyLayoutBinding;\nimport org.odk.collect.android.exception.JavaRosaException;\nimport org.odk.collect.android.formentry.FormEntryViewModel;\nimport org.odk.collect.android.formentry.ODKView;\nimport org.odk.collect.android.formentry.repeats.DeleteRepeatDialogFragment;\nimport org.odk.collect.android.formmanagement.FormFillingIntentFactory;\nimport org.odk.collect.android.instancemanagement.InstanceEditResult;\nimport org.odk.collect.android.instancemanagement.InstanceExtKt;\nimport org.odk.collect.android.instancemanagement.InstancesDataService;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.javarosawrapper.JavaRosaFormController;\nimport org.odk.collect.android.utilities.FormEntryPromptUtils;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\nimport org.odk.collect.androidshared.ui.SnackbarUtils;\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.material.MaterialProgressDialogFragment;\n\nimport java.text.SimpleDateFormat;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\n\nimport timber.log.Timber;\n\npublic class FormHierarchyFragment extends Fragment {\n\n    private final boolean viewOnly;\n    private final ViewModelProvider.Factory viewModelFactory;\n    private final MenuHost menuHost;\n    private FormHiearchyMenuProvider menuProvider;\n    private FormEntryViewModel formEntryViewModel;\n    private FormHierarchyViewModel formHierarchyViewModel;\n    /**\n     * The index of the question or the field list the FormController was set to when the hierarchy\n     * was accessed. Used to jump the user back to where they were if applicable.\n     */\n    private FormIndex startIndex;\n    private final Scheduler scheduler;\n    private final InstancesDataService instancesDataService;\n    private final String currentProjectId;\n\n    public FormHierarchyFragment(\n            boolean viewOnly,\n            ViewModelProvider.Factory viewModelFactory,\n            MenuHost menuHost,\n            Scheduler scheduler,\n            InstancesDataService instancesDataService,\n            String currentProjectId\n    ) {\n        super(R.layout.form_hierarchy_layout);\n        this.viewOnly = viewOnly;\n        this.viewModelFactory = viewModelFactory;\n        this.menuHost = menuHost;\n        this.scheduler = scheduler;\n        this.instancesDataService = instancesDataService;\n        this.currentProjectId = currentProjectId;\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n\n        formEntryViewModel = new ViewModelProvider(requireActivity(), viewModelFactory).get(FormEntryViewModel.class);\n        formHierarchyViewModel = new ViewModelProvider(\n                this,\n                new FormHierarchyViewModel.Factory(scheduler)\n        ).get(FormHierarchyViewModel.class);\n        requireActivity().setTitle(formEntryViewModel.getFormController().getFormTitle());\n        startIndex = formEntryViewModel.getFormController().getFormIndex();\n\n        formHierarchyViewModel.getInstanceEditResult().observe(this, instanceEditResult -> {\n            if (!instanceEditResult.isConsumed()) {\n                instanceEditResult.consume();\n                handleInstanceEditResult(instanceEditResult.getValue());\n            }\n        });\n        MaterialProgressDialogFragment.showOn(this, formHierarchyViewModel.isEditingInstance(), getParentFragmentManager(), () -> {\n            MaterialProgressDialogFragment dialog = new MaterialProgressDialogFragment();\n            dialog.setMessage(getString(org.odk.collect.strings.R.string.preparing_form_edit));\n            return dialog;\n        });\n\n        menuProvider = new FormHiearchyMenuProvider(formEntryViewModel, formHierarchyViewModel, viewOnly, context.getString(R.string.form_entry_screen), new FormHiearchyMenuProvider.OnClickListener() {\n            @Override\n            public void onEditClicked() {\n                formHierarchyViewModel.editInstance(\n                        formEntryViewModel.getFormController().getAbsoluteInstancePath(),\n                        instancesDataService,\n                        currentProjectId\n                );\n            }\n\n            @Override\n            public void onGoUpClicked() {\n                FormController formController = formEntryViewModel.getFormController();\n\n                // If `repeatGroupPickerIndex` is set it means we're currently displaying\n                // a list of repeat instances. If we unset `repeatGroupPickerIndex`,\n                // we will go back up to the previous screen.\n                if (formHierarchyViewModel.shouldShowRepeatGroupPicker()) {\n                    // Exit the picker.\n                    formHierarchyViewModel.setRepeatGroupPickerIndex(null);\n                } else {\n                    // Enter the picker if coming from a repeat group.\n                    FormIndex screenIndex = formHierarchyViewModel.getScreenIndex();\n                    int event = formController.getEvent(screenIndex);\n                    if (event == FormEntryController.EVENT_REPEAT || event == FormEntryController.EVENT_PROMPT_NEW_REPEAT) {\n                        formHierarchyViewModel.setRepeatGroupPickerIndex(screenIndex);\n                    }\n\n                    formController.stepToOuterScreenEvent();\n                }\n\n                refreshView(true);\n            }\n\n            @Override\n            public void onAddRepeatClicked() {\n                formEntryViewModel.getFormController().jumpToIndex(formHierarchyViewModel.getRepeatGroupPickerIndex());\n                formEntryViewModel.jumpToNewRepeat();\n                formEntryViewModel.addRepeat();\n\n                requireActivity().finish();\n            }\n\n            @Override\n            public void onDeleteRepeatClicked() {\n                DialogFragmentUtils.showIfNotShowing(DeleteRepeatDialogFragment.class, getChildFragmentManager());\n            }\n        });\n    }\n\n    private void handleInstanceEditResult(InstanceEditResult result) {\n        Instance instance = result.getInstance();\n\n        if (result instanceof InstanceEditResult.EditCompleted) {\n            openEditedInstance(instance.getDbId());\n        } else if (result instanceof InstanceEditResult.EditBlockedByNewerExistingEdit) {\n            if (InstanceExtKt.isDraft(instance)) {\n                showOpenDraftEditDialog(instance);\n            } else {\n                showOpenFinalizedEditDialog(instance);\n            }\n        }\n    }\n\n    private void openEditedInstance(long dbId) {\n        Intent intent = FormFillingIntentFactory.editDraftFormIntent(requireContext(), currentProjectId, dbId);\n        startActivity(intent);\n        requireActivity().getOnBackPressedDispatcher().onBackPressed();\n    }\n\n    private void showOpenDraftEditDialog(Instance instance) {\n        String dialogMessage = new SimpleDateFormat(\n                getString(org.odk.collect.strings.R.string.newer_draft_edit_found_dialog_message),\n                Locale.getDefault()\n        ).format(instance.getLastStatusChangeDate());\n\n        new MaterialAlertDialogBuilder(requireContext())\n                .setTitle(org.odk.collect.strings.R.string.newer_draft_edit_found_dialog_title)\n                .setMessage(dialogMessage)\n                .setPositiveButton(org.odk.collect.strings.R.string.newer_draft_edit_found_dialog_positive_button, (dialog, which) -> {\n                    Intent intent = FormFillingIntentFactory.editDraftFormIntent(requireContext(), currentProjectId, instance.getDbId());\n                    startActivity(intent);\n                    requireActivity().getOnBackPressedDispatcher().onBackPressed();\n                })\n                .setNegativeButton(org.odk.collect.strings.R.string.cancel, (dialog, which) -> {})\n                .setCancelable(false)\n                .show();\n    }\n\n    private void showOpenFinalizedEditDialog(Instance instance) {\n        String dialogMessage = new SimpleDateFormat(\n                getString(org.odk.collect.strings.R.string.newer_finalized_edit_found_dialog_message),\n                Locale.getDefault()\n        ).format(instance.getLastStatusChangeDate());\n\n        new MaterialAlertDialogBuilder(requireContext())\n                .setTitle(org.odk.collect.strings.R.string.newer_finalized_edit_found_dialog_title)\n                .setMessage(dialogMessage)\n                .setPositiveButton(org.odk.collect.strings.R.string.newer_finalized_edit_found_dialog_positive_button, (dialog, which) -> {\n                    formHierarchyViewModel.editInstance(\n                            instance.getInstanceFilePath(),\n                            instancesDataService,\n                            currentProjectId\n                    );\n                })\n                .setNegativeButton(org.odk.collect.strings.R.string.cancel, (dialog, which) -> {})\n                .setCancelable(false)\n                .show();\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        requireActivity().getOnBackPressedDispatcher().addCallback(\n                getViewLifecycleOwner(),\n                new OnBackPressedCallback(true) {\n                    @Override\n                    public void handleOnBackPressed() {\n                        if (viewOnly) {\n                            formEntryViewModel.exit();\n                        } else {\n                            FormController formController = formEntryViewModel.getFormController();\n                            if (formController != null) {\n                                formController.getAuditEventLogger().flush();\n                                navigateToTheLastRelevantIndex(formController);\n                            }\n                        }\n\n                        requireActivity().finish();\n                    }\n                }\n        );\n\n        formHierarchyViewModel.setStartIndex(formEntryViewModel.getFormController().getFormIndex());\n\n        menuHost.addMenuProvider(menuProvider, getViewLifecycleOwner());\n\n        FormHierarchyLayoutBinding binding = FormHierarchyLayoutBinding.bind(view);\n\n        RecyclerView recyclerView = binding.list;\n        recyclerView.setHasFixedSize(true);\n        recyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));\n        recyclerView.addItemDecoration(new DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL));\n\n        configureButtons(binding, formEntryViewModel.getFormController());\n        refreshView();\n\n        // Scroll to the last question the user was looking at\n        // TODO: avoid another iteration through all displayed elements\n        if (recyclerView != null && recyclerView.getAdapter() != null && recyclerView.getAdapter().getItemCount() > 0) {\n            binding.empty.setVisibility(View.GONE);\n            recyclerView.post(() -> {\n                int position = 0;\n                // Iterate over all the elements currently displayed looking for a match with the\n                // startIndex which can either represent a question or a field list.\n                List<HierarchyItem> elementsToDisplay = formHierarchyViewModel.getElementsToDisplay();\n                for (HierarchyItem hierarchyItem : elementsToDisplay) {\n                    FormIndex startIndex = formHierarchyViewModel.getStartIndex();\n                    FormIndex indexToCheck = hierarchyItem.getFormIndex();\n                    boolean indexIsInFieldList = formEntryViewModel.getFormController().indexIsInFieldList(startIndex);\n                    if (startIndex.equals(indexToCheck)\n                            || (indexIsInFieldList && indexToCheck.toString().startsWith(startIndex.toString()))) {\n                        position = elementsToDisplay.indexOf(hierarchyItem);\n                        break;\n                    }\n                }\n                ((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(position, 0);\n            });\n        }\n\n        getChildFragmentManager().setFragmentResultListener(REQUEST_DELETE_REPEAT, getViewLifecycleOwner(), (requestKey, result) -> onRepeatDeleted());\n\n        boolean newEdit = FormHierarchyFragmentArgs.fromBundle(getArguments()).getNewEdit();\n        if (newEdit) {\n            showSnackbar(\n                    view,\n                    getString(org.odk.collect.strings.R.string.finalized_form_edit_started),\n                    SnackbarUtils.DURATION_LONG,\n                    null,\n                    null,\n                    true\n            );\n        }\n    }\n\n    public void refreshView() {\n        refreshView(false);\n    }\n\n    /**\n     * @see #refreshView()\n     */\n    private void refreshView(boolean isGoingUp) {\n        FormHierarchyLayoutBinding binding = FormHierarchyLayoutBinding.bind(requireView());\n        ImageView groupIcon = binding.groupIcon;\n        TextView groupPathTextView = binding.pathtext;\n        RecyclerView recyclerView = binding.list;\n\n        try {\n            FormController formController = formEntryViewModel.getFormController();\n\n            // Save the current index so we can return to the problematic question\n            // in the event of an error.\n            formHierarchyViewModel.setCurrentIndex(formController.getFormIndex());\n\n            calculateElementsToDisplay(formController, groupIcon, groupPathTextView);\n            ViewModelProvider viewModelProvider = new ViewModelProvider(this, viewModelFactory);\n            recyclerView.setAdapter(new HierarchyListAdapter(\n                    formHierarchyViewModel.getElementsToDisplay(),\n                    viewModelProvider.get(MediaWidgetAnswerViewModel.class),\n                    this::onElementClick)\n            );\n\n            formController.jumpToIndex(formHierarchyViewModel.getCurrentIndex());\n\n            // Prevent a redundant middle screen (common on many forms\n            // that use presentation groups to display labels).\n            if (isDisplayingSingleGroup() && !formHierarchyViewModel.getScreenIndex().isBeginningOfFormIndex()) {\n                if (isGoingUp) {\n                    // Back out once more.\n                    goUpLevel();\n                } else {\n                    // Enter automatically.\n                    formController.jumpToIndex(formHierarchyViewModel.getElementsToDisplay().get(0).getFormIndex());\n                    refreshView();\n                }\n            }\n        } catch (Exception e) {\n            Timber.e(e);\n            createErrorDialog(e.getMessage());\n        }\n    }\n\n    private void calculateElementsToDisplay(FormController formController, ImageView groupIcon, TextView groupPathTextView) {\n        List<HierarchyItem> elementsToDisplay = new ArrayList<>();\n\n        jumpToHierarchyStartIndex();\n\n        int event = formController.getEvent();\n\n        if (event == FormEntryController.EVENT_BEGINNING_OF_FORM && !formHierarchyViewModel.shouldShowRepeatGroupPicker()) {\n            // The beginning of form has no valid prompt to display.\n            groupIcon.setVisibility(View.GONE);\n            groupPathTextView.setVisibility(View.GONE);\n        } else {\n            groupIcon.setVisibility(View.VISIBLE);\n            groupPathTextView.setVisibility(View.VISIBLE);\n            groupPathTextView.setText(getCurrentPath());\n\n            if (formController.indexContainsRepeatableGroup(formHierarchyViewModel.getScreenIndex()) || formHierarchyViewModel.shouldShowRepeatGroupPicker()) {\n                groupIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_repeat));\n            } else {\n                groupIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_folder_open));\n            }\n        }\n\n        // Refresh the current event in case we did step forward.\n        event = formController.getEvent();\n\n        // Ref to the parent group that's currently being displayed.\n        //\n        // Because of the guard conditions below, we will skip\n        // everything until we exit this group.\n        TreeReference visibleGroupRef = null;\n\n        while (event != FormEntryController.EVENT_END_OF_FORM) {\n            // get the ref to this element\n            TreeReference currentRef = formController.getFormIndex().getReference();\n\n            // retrieve the current group\n            TreeReference curGroup = (visibleGroupRef == null) ? formHierarchyViewModel.getContextGroupRef() : visibleGroupRef;\n\n            if (curGroup != null && !curGroup.isParentOf(currentRef, false)) {\n                // We have left the current group\n                if (visibleGroupRef == null) {\n                    // We are done.\n                    break;\n                } else {\n                    // exit the inner group\n                    visibleGroupRef = null;\n                }\n            }\n\n            if (visibleGroupRef != null) {\n                // We're in a group within the one we want to list\n                // skip this question/group/repeat and move to the next index.\n                event =\n                        formController.stepToNextEvent(JavaRosaFormController.STEP_INTO_GROUP);\n                continue;\n            }\n\n            switch (event) {\n                case FormEntryController.EVENT_QUESTION: {\n                    // Nothing but repeat group instances should show up in the picker.\n                    if (formHierarchyViewModel.shouldShowRepeatGroupPicker()) {\n                        break;\n                    }\n\n                    FormEntryPrompt fp = formController.getQuestionPrompt();\n                    String label = fp.getShortText();\n                    String answerDisplay = QuestionAnswerProcessor.getQuestionAnswer(fp, requireContext(), formController);\n                    elementsToDisplay.add(\n                            new HierarchyItem.Question(\n                                    fp.getIndex(),\n                                    FormEntryPromptUtils.styledQuestionText(label, fp.isRequired()),\n                                    answerDisplay,\n                                    fp\n                            )\n                    );\n                    break;\n                }\n                case FormEntryController.EVENT_GROUP: {\n                    if (!formController.isGroupRelevant()) {\n                        break;\n                    }\n                    // Nothing but repeat group instances should show up in the picker.\n                    if (formHierarchyViewModel.shouldShowRepeatGroupPicker()) {\n                        break;\n                    }\n\n                    FormIndex index = formController.getFormIndex();\n\n                    // Only display groups with a specific appearance attribute.\n                    if (!formController.isDisplayableGroup(index)) {\n                        break;\n                    }\n\n                    // Don't render other groups' children.\n                    TreeReference contextGroupRef = formHierarchyViewModel.getContextGroupRef();\n                    if (contextGroupRef != null && !contextGroupRef.isParentOf(currentRef, false)) {\n                        break;\n                    }\n\n                    visibleGroupRef = currentRef;\n\n                    FormEntryCaption caption = formController.getCaptionPrompt();\n\n                    elementsToDisplay.add(\n                            new HierarchyItem.VisibleGroup(\n                                    caption.getIndex(),\n                                    HtmlUtils.textToHtml(caption.getShortText())\n                            )\n                    );\n\n                    // Skip to the next item outside the group.\n                    event = formController.stepOverGroup();\n                    continue;\n                }\n                case FormEntryController.EVENT_PROMPT_NEW_REPEAT: {\n                    // this would display the 'add new repeat' dialog\n                    // ignore it.\n                    break;\n                }\n                case FormEntryController.EVENT_REPEAT: {\n                    boolean forPicker = formHierarchyViewModel.shouldShowRepeatGroupPicker();\n                    // Only break to exclude non-relevant repeat from picker\n                    if (!formController.isGroupRelevant() && forPicker) {\n                        break;\n                    }\n\n                    visibleGroupRef = currentRef;\n\n                    // Don't render other groups' children.\n                    TreeReference contextGroupRef = formHierarchyViewModel.getContextGroupRef();\n                    if (contextGroupRef != null && !contextGroupRef.isParentOf(currentRef, false)) {\n                        break;\n                    }\n\n                    FormEntryCaption fc = formController.getCaptionPrompt();\n\n                    if (forPicker) {\n                        // Don't render other groups' instances.\n                        String repeatGroupPickerRef = formHierarchyViewModel.getRepeatGroupPickerIndex().getReference().toString(false);\n                        if (!currentRef.toString(false).equals(repeatGroupPickerRef)) {\n                            break;\n                        }\n\n                        int itemNumber = fc.getMultiplicity() + 1;\n\n                        // e.g. `friends > 1`\n                        String repeatLabel = fc.getShortText() + \" > \" + itemNumber;\n\n                        // If the child of the group has a more descriptive label, use that instead.\n                        if (fc.getFormElement().getChildren().size() == 1 && fc.getFormElement().getChild(0) instanceof GroupDef) {\n                            formController.stepToNextEvent(JavaRosaFormController.STEP_INTO_GROUP);\n                            String itemLabel = formController.getCaptionPrompt().getShortText();\n                            if (itemLabel != null) {\n                                // e.g. `1. Alice`\n                                repeatLabel = itemNumber + \".\\u200E \" + itemLabel;\n                            }\n                        }\n\n                        elementsToDisplay.add(\n                                new HierarchyItem.RepeatInstance(\n                                        fc.getIndex(),\n                                        HtmlUtils.textToHtml(repeatLabel)\n                                )\n                        );\n                    } else if (fc.getMultiplicity() == 0) {\n                        elementsToDisplay.add(\n                                new HierarchyItem.RepeatableGroup(\n                                        fc.getIndex(),\n                                        HtmlUtils.textToHtml(fc.getShortText())\n                                )\n                        );\n                    }\n\n                    break;\n                }\n            }\n\n            event = formController.stepToNextEvent(JavaRosaFormController.STEP_INTO_GROUP);\n        }\n\n        formHierarchyViewModel.setElementsToDisplay(elementsToDisplay);\n    }\n\n    /**\n     * Goes to the start of the hierarchy view based on where the user came from.\n     * Backs out until the index is at the beginning of a repeat group or the beginning of the form.\n     */\n    private void jumpToHierarchyStartIndex() {\n        FormController formController = formEntryViewModel.getFormController();\n        FormIndex startIndex = formController.getFormIndex();\n\n        // If we're not at the first level, we're inside a repeated group so we want to only\n        // display everything enclosed within that group.\n        formHierarchyViewModel.setContextGroupRef(null);\n\n        // Save the index to the screen itself, before potentially moving into it.\n        formHierarchyViewModel.setScreenIndex(startIndex);\n\n        // If we're currently at a displayable group, record the name of the node and step to the next\n        // node to display.\n        if (formController.isDisplayableGroup(startIndex)) {\n            formHierarchyViewModel.setContextGroupRef(formController.getFormIndex().getReference());\n            formController.stepToNextEvent(JavaRosaFormController.STEP_INTO_GROUP);\n        } else {\n            FormIndex potentialStartIndex = getPreviousLevel(startIndex);\n            // Step back until we hit a displayable group or the beginning.\n            while (!isScreenEvent(formController, potentialStartIndex)) {\n                potentialStartIndex = getPreviousLevel(potentialStartIndex);\n            }\n\n            formHierarchyViewModel.setScreenIndex(potentialStartIndex);\n\n            // Check to see if the question is at the first level of the hierarchy.\n            // If it is, display the root level from the beginning.\n            // Otherwise we're at a displayable group.\n            if (formHierarchyViewModel.getScreenIndex() == null) {\n                formHierarchyViewModel.setScreenIndex(FormIndex.createBeginningOfFormIndex());\n            }\n\n            formController.jumpToIndex(formHierarchyViewModel.getScreenIndex());\n\n            // Now test again. This should be true at this point or we're at the beginning.\n            if (formController.isDisplayableGroup(formController.getFormIndex())) {\n                formHierarchyViewModel.setContextGroupRef(formController.getFormIndex().getReference());\n                formController.stepToNextEvent(JavaRosaFormController.STEP_INTO_GROUP);\n            } else {\n                // Let contextGroupRef be null.\n            }\n        }\n\n        menuHost.invalidateMenu();\n    }\n\n    /**\n     * Returns true if the event is a displayable group or the start of the form.\n     * See {@link FormController#stepToOuterScreenEvent} for more context.\n     */\n    private boolean isScreenEvent(FormController formController, FormIndex index) {\n        // Beginning of form.\n        if (index == null) {\n            return true;\n        }\n\n        return formController.isDisplayableGroup(index);\n    }\n\n    /**\n     * Navigates \"up\" in the form hierarchy.\n     */\n    protected void goUpLevel() {\n        FormController formController = formEntryViewModel.getFormController();\n\n        // If `repeatGroupPickerIndex` is set it means we're currently displaying\n        // a list of repeat instances. If we unset `repeatGroupPickerIndex`,\n        // we will go back up to the previous screen.\n        if (formHierarchyViewModel.shouldShowRepeatGroupPicker()) {\n            // Exit the picker.\n            formHierarchyViewModel.setRepeatGroupPickerIndex(null);\n        } else {\n            // Enter the picker if coming from a repeat group.\n            int event = formController.getEvent(formHierarchyViewModel.getScreenIndex());\n            if (event == FormEntryController.EVENT_REPEAT || event == FormEntryController.EVENT_PROMPT_NEW_REPEAT) {\n                formHierarchyViewModel.setRepeatGroupPickerIndex(formHierarchyViewModel.getScreenIndex());\n            }\n\n            formController.stepToOuterScreenEvent();\n        }\n\n        refreshView(true);\n    }\n\n    /**\n     * Returns a string representing the 'path' of the current screen.\n     * Each level is separated by `>`.\n     */\n    private CharSequence getCurrentPath() {\n        FormController formController = formEntryViewModel.getFormController();\n        FormIndex index = formHierarchyViewModel.getScreenIndex();\n\n        List<FormEntryCaption> groups = new ArrayList<>();\n\n        if (formHierarchyViewModel.shouldShowRepeatGroupPicker()) {\n            groups.add(formController.getCaptionPrompt(formHierarchyViewModel.getRepeatGroupPickerIndex()));\n        }\n\n        while (index != null) {\n            groups.add(0, formController.getCaptionPrompt(index));\n            index = getPreviousLevel(index);\n        }\n\n        // If the repeat picker is showing, don't show an item number for the current index.\n        boolean hideLastMultiplicity = formHierarchyViewModel.shouldShowRepeatGroupPicker();\n\n        return ODKView.getGroupsPath(groups.toArray(new FormEntryCaption[0]), hideLastMultiplicity);\n    }\n\n    /**\n     * Handles clicks on a specific row in the hierarchy view.\n     */\n    private void onElementClick(HierarchyItem item) {\n        FormIndex index = item.getFormIndex();\n        if (item instanceof HierarchyItem.Question) {\n            onQuestionClicked(index);\n        } else if (item instanceof HierarchyItem.RepeatableGroup) {\n            // Show the picker.\n            formHierarchyViewModel.setRepeatGroupPickerIndex(index);\n            refreshView();\n        } else {\n            // Hide the picker.\n            formHierarchyViewModel.setRepeatGroupPickerIndex(null);\n            formEntryViewModel.getFormController().jumpToIndex(index);\n            requireActivity().setResult(RESULT_OK);\n            refreshView();\n        }\n    }\n\n    /**\n     * Handles clicks on a question. Jumps to the form filling view with the selected question shown.\n     * If the selected question is in a field list, show the entire field list.\n     */\n    void onQuestionClicked(FormIndex index) {\n        if (viewOnly) {\n            return;\n        }\n\n        formEntryViewModel.getFormController().jumpToIndex(index);\n        if (formEntryViewModel.getFormController().indexIsInFieldList()) {\n            try {\n                formEntryViewModel.getFormController().stepToPreviousScreenEvent();\n            } catch (JavaRosaException e) {\n                Timber.d(e);\n                createErrorDialog(e.getCause().getMessage());\n                return;\n            }\n        }\n        requireActivity().setResult(RESULT_OK);\n        requireActivity().finish();\n    }\n\n    /**\n     * Creates and displays dialog with the given errorMsg.\n     */\n    protected void createErrorDialog(String errorMsg) {\n        AlertDialog alertDialog = new MaterialAlertDialogBuilder(requireContext()).create();\n\n        alertDialog.setTitle(getString(org.odk.collect.strings.R.string.error_occured));\n        alertDialog.setMessage(errorMsg);\n        DialogInterface.OnClickListener errorListener = new DialogInterface.OnClickListener() {\n            @Override\n            public void onClick(DialogInterface dialog, int i) {\n                switch (i) {\n                    case DialogInterface.BUTTON_POSITIVE:\n                        FormController formController = formEntryViewModel.getFormController();\n                        formController.jumpToIndex(formHierarchyViewModel.getCurrentIndex());\n                        break;\n                }\n            }\n        };\n        alertDialog.setCancelable(false);\n        alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, getString(org.odk.collect.strings.R.string.ok), errorListener);\n        alertDialog.show();\n    }\n\n    /**\n     * Returns true if there's only one item being displayed, and it's a group.\n     * Groups like this are often used to display a label in the hierarchy path.\n     */\n    private boolean isDisplayingSingleGroup() {\n        return formHierarchyViewModel.getElementsToDisplay().size() == 1\n                && formHierarchyViewModel.getElementsToDisplay().get(0) instanceof HierarchyItem.VisibleGroup;\n    }\n\n    private void configureButtons(FormHierarchyLayoutBinding binding, FormController formController) {\n        Button exitButton = binding.exitButton;\n        Button jumpBeginningButton = binding.jumpBeginningButton;\n        Button jumpEndButton = binding.jumpEndButton;\n\n        if (viewOnly) {\n            exitButton.setOnClickListener(v -> requireActivity().getOnBackPressedDispatcher().onBackPressed());\n            exitButton.setVisibility(View.VISIBLE);\n            jumpBeginningButton.setVisibility(View.GONE);\n            jumpEndButton.setVisibility(View.GONE);\n        } else {\n            jumpBeginningButton.setOnClickListener(v -> {\n                formController.getAuditEventLogger().flush();\n                formController.jumpToIndex(FormIndex.createBeginningOfFormIndex());\n\n                requireActivity().setResult(RESULT_OK);\n                requireActivity().finish();\n            });\n\n            jumpEndButton.setOnClickListener(v -> {\n                formController.getAuditEventLogger().flush();\n                formController.jumpToIndex(FormIndex.createEndOfFormIndex());\n\n                requireActivity().setResult(RESULT_OK);\n                requireActivity().finish();\n            });\n        }\n\n    }\n\n    /**\n     * After having deleted the current index,\n     * returns true if the current index was the only item in the repeat group.\n     */\n    private boolean didDeleteLastRepeatItem() {\n        FormController formController = formEntryViewModel.getFormController();\n        FormIndex index = formController.getFormIndex();\n        int event = formController.getEvent(index);\n\n        // If we're on item 0, but we will be prompted to add another item next,\n        // it must be the last remaining item.\n        return event == FormEntryController.EVENT_PROMPT_NEW_REPEAT\n                && index.getElementMultiplicity() == 0;\n    }\n\n    private boolean didDeleteFirstRepeatItem() {\n        return formEntryViewModel\n                .getFormController()\n                .getFormIndex()\n                .getElementMultiplicity() == 0;\n    }\n\n    private void onRepeatDeleted() {\n        if (didDeleteLastRepeatItem()) {\n            // goUpLevel would put us in a weird state after deleting the last item;\n            // just go back one event instead.\n            //\n            // TODO: This works well in most cases, but if there are 2 repeats in a row,\n            //   and you delete an item from the second repeat, it will send you into the\n            //   first repeat instead of going back a level as expected.\n            goToPreviousEvent();\n        } else if (didDeleteFirstRepeatItem()) {\n            goUpLevel();\n        } else {\n            goToPreviousEvent();\n            goUpLevel();\n        }\n    }\n\n    /**\n     * Similar to {@link #goUpLevel}, but makes a less significant step backward.\n     * This is only used when the caller knows where to go back to,\n     * e.g. after deleting the final remaining item in a repeat group.\n     */\n    private void goToPreviousEvent() {\n        FormController formController = formEntryViewModel.getFormController();\n        try {\n            formController.stepToPreviousScreenEvent();\n        } catch (JavaRosaException e) {\n            Timber.d(e);\n            createErrorDialog(e.getCause().getMessage());\n            return;\n        }\n\n        refreshView();\n    }\n\n    private void navigateToTheLastRelevantIndex(FormController formController) {\n        FormEntryController fec = new FormEntryController(new FormEntryModel(formController.getFormDef()));\n        formController.jumpToIndex(startIndex);\n\n        // startIndex might no longer exist if it was a part of repeat group that has been removed\n        while (true) {\n            boolean isBeginningOfFormIndex = formController.getFormIndex().isBeginningOfFormIndex();\n            boolean isEndOfFormIndex = formController.getFormIndex().isEndOfFormIndex();\n            boolean isIndexRelevant = isBeginningOfFormIndex\n                    || isEndOfFormIndex\n                    || fec.getModel().isIndexRelevant(formController.getFormIndex());\n            boolean isPromptNewRepeatEvent = formController.getEvent() == FormEntryController.EVENT_PROMPT_NEW_REPEAT;\n\n            boolean shouldNavigateBack = !isIndexRelevant || isPromptNewRepeatEvent;\n\n            if (shouldNavigateBack) {\n                formController.stepToPreviousEvent();\n            } else {\n                break;\n            }\n        }\n    }\n\n    private static class FormHiearchyMenuProvider implements MenuProvider {\n\n        private final FormEntryViewModel formEntryViewModel;\n        private final FormHierarchyViewModel formHierarchyViewModel;\n        private final boolean viewOnly;\n        private final OnClickListener onClickListener;\n        private final String screenName;\n\n        FormHiearchyMenuProvider(FormEntryViewModel formEntryViewModel, FormHierarchyViewModel formHierarchyViewModel, boolean viewOnly, String screenName, OnClickListener goUpClicked) {\n            this.formEntryViewModel = formEntryViewModel;\n            this.formHierarchyViewModel = formHierarchyViewModel;\n            this.viewOnly = viewOnly;\n            this.onClickListener = goUpClicked;\n            this.screenName = screenName;\n        }\n\n        @Override\n        public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {\n            menuInflater.inflate(R.menu.form_hierarchy_menu, menu);\n        }\n\n        @Override\n        public void onPrepareMenu(@NonNull Menu menu) {\n            FormIndex screenIndex = formHierarchyViewModel.getScreenIndex();\n            boolean isAtBeginning = screenIndex.isBeginningOfFormIndex() && !formHierarchyViewModel.shouldShowRepeatGroupPicker();\n            boolean shouldShowPicker = formHierarchyViewModel.shouldShowRepeatGroupPicker();\n            boolean isInRepeat = formEntryViewModel.getFormController().indexContainsRepeatableGroup(screenIndex);\n            boolean isGroupSizeLocked = shouldShowPicker\n                    ? isGroupSizeLocked(formHierarchyViewModel.getRepeatGroupPickerIndex()) : isGroupSizeLocked(screenIndex);\n\n            menu.findItem(R.id.menu_edit).setVisible(viewOnly && formEntryViewModel.isFormEditableAfterFinalization());\n            menu.findItem(R.id.menu_add_repeat).setVisible(shouldShowPicker && !isGroupSizeLocked && !viewOnly);\n            menu.findItem(R.id.menu_delete_child).setVisible(isInRepeat && !shouldShowPicker && !isGroupSizeLocked && !viewOnly);\n            menu.findItem(R.id.menu_go_up).setVisible(!isAtBeginning);\n        }\n\n        @Override\n        public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {\n            if (!MultiClickGuard.allowClick(screenName)) {\n                return false;\n            }\n\n            if (menuItem.getItemId() == R.id.menu_edit) {\n                onClickListener.onEditClicked();\n                return true;\n            } else if (menuItem.getItemId() == R.id.menu_delete_child) {\n                onClickListener.onDeleteRepeatClicked();\n                return true;\n            } else if (menuItem.getItemId() == R.id.menu_add_repeat) {\n                onClickListener.onAddRepeatClicked();\n                return true;\n            } else if (menuItem.getItemId() == R.id.menu_go_up) {\n                onClickListener.onGoUpClicked();\n                return true;\n            } else {\n                return false;\n            }\n        }\n\n        private boolean isGroupSizeLocked(FormIndex index) {\n            FormController formController = formEntryViewModel.getFormController();\n            IFormElement element = formController.getCaptionPrompt(index).getFormElement();\n            return element instanceof GroupDef && ((GroupDef) element).noAddRemove;\n        }\n\n        interface OnClickListener {\n            void onEditClicked();\n\n            void onGoUpClicked();\n\n            void onAddRepeatClicked();\n\n            void onDeleteRepeatClicked();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formhierarchy/FormHierarchyFragmentHostActivity.kt",
    "content": "package org.odk.collect.android.formhierarchy\n\nimport android.os.Bundle\nimport androidx.navigation.fragment.NavHostFragment\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.R\nimport org.odk.collect.android.activities.FormEntryViewModelFactory\nimport org.odk.collect.android.application.CollectComposeThemeProvider\nimport org.odk.collect.android.entities.EntitiesRepositoryProvider\nimport org.odk.collect.android.formentry.FormOpeningMode\nimport org.odk.collect.android.formentry.FormSessionRepository\nimport org.odk.collect.android.formentry.repeats.DeleteRepeatDialogFragment\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.injection.config.ProjectDependencyModuleFactory\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.utilities.ChangeLockProvider\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.utilities.MediaUtils\nimport org.odk.collect.android.utilities.SavepointsRepositoryProvider\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.audiorecorder.recording.AudioRecorder\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.permissions.PermissionsProvider\nimport org.odk.collect.printer.HtmlPrinter\nimport org.odk.collect.qrcode.zxing.QRCodeCreatorImpl\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport javax.inject.Inject\n\nclass FormHierarchyFragmentHostActivity : LocalizedActivity(), CollectComposeThemeProvider {\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    @Inject\n    lateinit var formSessionRepository: FormSessionRepository\n\n    @Inject\n    lateinit var mediaUtils: MediaUtils\n\n    @Inject\n    lateinit var analytics: Analytics\n\n    @Inject\n    lateinit var audioRecorder: AudioRecorder\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    @Inject\n    lateinit var entitiesRepositoryProvider: EntitiesRepositoryProvider\n\n    @Inject\n    lateinit var permissionsChecker: PermissionsChecker\n\n    @Inject\n    lateinit var fusedLocationClient: LocationClient\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    @Inject\n    lateinit var permissionsProvider: PermissionsProvider\n\n    @Inject\n    lateinit var autoSendSettingsProvider: AutoSendSettingsProvider\n\n    @Inject\n    lateinit var instancesRepositoryProvider: InstancesRepositoryProvider\n\n    @Inject\n    lateinit var formsRepositoryProvider: FormsRepositoryProvider\n\n    @Inject\n    lateinit var savepointsRepositoryProvider: SavepointsRepositoryProvider\n\n    @Inject\n    lateinit var instancesDataService: InstancesDataService\n\n    @Inject\n    lateinit var changeLockProvider: ChangeLockProvider\n\n    @Inject\n    lateinit var projectDependencyModuleFactory: ProjectDependencyModuleFactory\n\n    private val sessionId by lazy { intent.getStringExtra(EXTRA_SESSION_ID)!! }\n    private val viewModelFactory by lazy {\n        FormEntryViewModelFactory(\n            FormOpeningMode.EDIT_SAVED,\n            sessionId,\n            scheduler,\n            formSessionRepository,\n            mediaUtils,\n            audioRecorder,\n            projectsDataService,\n            entitiesRepositoryProvider,\n            settingsProvider,\n            permissionsChecker,\n            fusedLocationClient,\n            permissionsProvider,\n            autoSendSettingsProvider,\n            formsRepositoryProvider,\n            instancesRepositoryProvider,\n            savepointsRepositoryProvider,\n            QRCodeCreatorImpl(),\n            HtmlPrinter(),\n            instancesDataService,\n            changeLockProvider,\n            projectDependencyModuleFactory\n        )\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        DaggerUtils.getComponent(this).inject(this)\n\n        val viewOnly = intent.getBooleanExtra(EXTRA_VIEW_ONLY, false)\n        supportFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(FormHierarchyFragment::class) {\n                FormHierarchyFragment(\n                    viewOnly,\n                    viewModelFactory,\n                    this,\n                    scheduler,\n                    instancesDataService,\n                    projectsDataService.getCurrentProject().value!!.uuid\n                )\n            }\n            .forClass(DeleteRepeatDialogFragment::class) {\n                DeleteRepeatDialogFragment(viewModelFactory)\n            }\n            .build()\n\n        if (formSessionRepository.get(sessionId).value == null) {\n            super.onCreate(null)\n            finish()\n            return\n        } else {\n            super.onCreate(savedInstanceState)\n            setView(R.layout.hierarchy_host_layout, false)\n\n            val navHostFragment =\n                supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment\n            val navController = navHostFragment.navController\n\n            val shouldShowNewEditMessage = intent.getBooleanExtra(SHOW_NEW_EDIT_MESSAGE, false)\n            navController.setGraph(\n                R.navigation.form_entry,\n                FormHierarchyFragmentArgs.Builder(shouldShowNewEditMessage)\n                    .build()\n                    .toBundle()\n            )\n\n            setSupportActionBar(findViewById(org.odk.collect.androidshared.R.id.toolbar))\n        }\n    }\n\n    companion object {\n        const val EXTRA_SESSION_ID = \"session_id\"\n        const val EXTRA_VIEW_ONLY = \"view_only\"\n        const val SHOW_NEW_EDIT_MESSAGE = \"show_new_edit_message\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formhierarchy/FormHierarchyViewModel.kt",
    "content": "package org.odk.collect.android.formhierarchy\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.core.model.instance.TreeReference\nimport org.odk.collect.android.instancemanagement.InstanceEditResult\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.androidshared.async.TrackableWorker\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.async.Scheduler\n\nclass FormHierarchyViewModel(scheduler: Scheduler) : ViewModel() {\n    private val trackableWorker = TrackableWorker(scheduler)\n    val isEditingInstance: LiveData<Boolean> = trackableWorker.isWorking\n\n    private val _instanceEditResult = MutableLiveData<Consumable<InstanceEditResult>>()\n    val instanceEditResult: LiveData<Consumable<InstanceEditResult>> = _instanceEditResult\n\n    var contextGroupRef: TreeReference? = null\n    var screenIndex: FormIndex? = null\n    var repeatGroupPickerIndex: FormIndex? = null\n    var currentIndex: FormIndex? = null\n    var elementsToDisplay: List<HierarchyItem>? = null\n    var startIndex: FormIndex? = null\n\n    fun shouldShowRepeatGroupPicker() = repeatGroupPickerIndex != null\n\n    fun editInstance(\n        instanceFilePath: String,\n        instancesDataService: InstancesDataService,\n        projectId: String\n    ) {\n        trackableWorker.immediate(\n            background = {\n                instancesDataService.editInstance(instanceFilePath, projectId)\n            },\n            foreground = { instanceEditResult ->\n                _instanceEditResult.value = Consumable(instanceEditResult)\n            }\n        )\n    }\n\n    class Factory(private val scheduler: Scheduler) : ViewModelProvider.Factory {\n        override fun <T : ViewModel> create(modelClass: Class<T>): T {\n            return FormHierarchyViewModel(scheduler) as T\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formhierarchy/HierarchyItem.kt",
    "content": "package org.odk.collect.android.formhierarchy\n\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.form.api.FormEntryPrompt\n\nsealed class HierarchyItem {\n    abstract val formIndex: FormIndex\n    abstract val primaryText: CharSequence\n\n    data class Question(\n        override val formIndex: FormIndex,\n        override val primaryText: CharSequence,\n        val secondaryText: String,\n        val formEntryPrompt: FormEntryPrompt\n    ) : HierarchyItem()\n\n    data class VisibleGroup(\n        override val formIndex: FormIndex,\n        override val primaryText: CharSequence,\n    ) : HierarchyItem()\n\n    data class RepeatableGroup(\n        override val formIndex: FormIndex,\n        override val primaryText: CharSequence,\n    ) : HierarchyItem()\n\n    data class RepeatInstance(\n        override val formIndex: FormIndex,\n        override val primaryText: CharSequence,\n    ) : HierarchyItem()\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formhierarchy/HierarchyListAdapter.kt",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.formhierarchy\n\nimport android.view.ViewGroup\nimport androidx.recyclerview.widget.RecyclerView\nimport org.odk.collect.android.R\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel\n\nclass HierarchyListAdapter(\n    private val hierarchyItems: List<HierarchyItem>,\n    private val mediaWidgetAnswerViewModel: MediaWidgetAnswerViewModel,\n    private val listener: OnElementClickListener\n) : RecyclerView.Adapter<HierarchyListAdapter.ViewHolder>() {\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        val item = HierarchyListItemView(parent.context, viewType).apply {\n            layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)\n        }\n        return ViewHolder(item)\n    }\n\n    override fun getItemViewType(position: Int): Int {\n        return when (hierarchyItems[position]) {\n            is HierarchyItem.Question -> R.layout.hierarchy_question_item\n            is HierarchyItem.VisibleGroup -> R.layout.hierarchy_group_item\n            is HierarchyItem.RepeatableGroup -> R.layout.hierarchy_repeatable_group_item\n            is HierarchyItem.RepeatInstance -> R.layout.hierarchy_repeatable_group_instance_item\n        }\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n        val element = hierarchyItems[position]\n\n        holder.view.setElement(element, mediaWidgetAnswerViewModel) {\n            listener.onElementClick(element)\n        }\n\n        holder.view.setOnClickListener {\n            listener.onElementClick(element)\n        }\n    }\n\n    override fun getItemCount(): Int {\n        return hierarchyItems.size\n    }\n\n    class ViewHolder(val view: HierarchyListItemView) : RecyclerView.ViewHolder(view)\n\n    interface OnElementClickListener {\n        fun onElementClick(element: HierarchyItem?)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formhierarchy/HierarchyListItemView.kt",
    "content": "package org.odk.collect.android.formhierarchy\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Text\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.ComposeView\nimport androidx.compose.ui.res.dimensionResource\nimport com.google.android.material.textview.MaterialTextView\nimport org.odk.collect.android.R\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel\nimport org.odk.collect.android.widgets.WidgetAnswer\nimport org.odk.collect.androidshared.R.dimen\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\nimport org.odk.collect.androidshared.ui.compose.marginSmall\nimport org.odk.collect.androidshared.ui.compose.marginStandard\n\nclass HierarchyListItemView(context: Context, layoutResId: Int) : FrameLayout(context) {\n    init {\n        LayoutInflater.from(context).inflate(layoutResId, this, true)\n    }\n\n    fun setElement(\n        item: HierarchyItem,\n        mediaWidgetAnswerViewModel: MediaWidgetAnswerViewModel,\n        onCLick: () -> Unit\n    ) {\n        findViewById<MaterialTextView>(R.id.primary_text).text = item.primaryText\n        if (item is HierarchyItem.Question) {\n            findViewById<ComposeView>(R.id.answer_view).setContextThemedContent {\n                WidgetAnswer(\n                    modifier = Modifier.padding(top = marginStandard()),\n                    prompt = item.formEntryPrompt,\n                    answer = item.secondaryText,\n                    summaryView = true,\n                    mediaWidgetAnswerViewModel = mediaWidgetAnswerViewModel,\n                    onClick = onCLick\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessor.kt",
    "content": "package org.odk.collect.android.formhierarchy\n\nimport android.content.Context\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.data.DateData\nimport org.javarosa.core.model.data.DateTimeData\nimport org.javarosa.core.model.data.MultipleItemsData\nimport org.javarosa.core.model.data.helper.Selection\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.fastexternalitemset.ItemsetDao\nimport org.odk.collect.android.fastexternalitemset.ItemsetDbAdapter\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.utilities.Appearances\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils\nimport java.math.BigDecimal\nimport java.text.DecimalFormat\nimport java.text.DecimalFormatSymbols\nimport java.util.Date\n\nobject QuestionAnswerProcessor {\n    @JvmStatic\n    fun getQuestionAnswer(fep: FormEntryPrompt, context: Context, formController: FormController): String {\n        val appearance: String? = fep.appearanceHint\n        if (appearance == Appearances.PRINTER) {\n            return \"\"\n        }\n\n        if (!fep.answerText.isNullOrBlank() &&\n            Appearances.isMasked(fep) &&\n            fep.controlType == Constants.CONTROL_INPUT &&\n            fep.dataType == Constants.DATATYPE_TEXT\n        ) {\n            return \"••••••••••\"\n        }\n\n        val data = fep.answerValue\n        if (data is MultipleItemsData) {\n            val answerText = StringBuilder()\n            val values = data.getValue() as List<Selection>\n            for (value in values) {\n                if (fep.controlType == Constants.CONTROL_RANK) {\n                    answerText\n                        .append(values.indexOf(value) + 1)\n                        .append(\". \")\n                }\n                answerText.append(fep.getSelectItemText(value))\n\n                if (values.size - 1 > values.indexOf(value)) {\n                    answerText.append(\", \")\n                }\n            }\n            return answerText.toString()\n        }\n\n        if (data is DateTimeData) {\n            return DateTimeWidgetUtils.getDateTimeLabel(\n                data.getValue() as Date,\n                DateTimeWidgetUtils.getDatePickerDetails(appearance),\n                true,\n                context\n            )\n        }\n\n        if (data is DateData) {\n            return DateTimeWidgetUtils.getDateTimeLabel(\n                data.getValue() as Date,\n                DateTimeWidgetUtils.getDatePickerDetails(appearance),\n                false,\n                context\n            )\n        }\n\n        if (data != null && appearance != null && appearance.contains(Appearances.THOUSANDS_SEP)) {\n            return try {\n                val answerAsDecimal = BigDecimal(fep.answerText)\n\n                val df = DecimalFormat()\n                df.groupingSize = 3\n                df.isGroupingUsed = true\n                df.maximumFractionDigits = Int.MAX_VALUE\n\n                // Use . as decimal marker for consistency with DecimalWidget\n                val customFormat = DecimalFormatSymbols()\n                customFormat.decimalSeparator = '.'\n\n                if (df.decimalFormatSymbols.groupingSeparator == '.') {\n                    customFormat.groupingSeparator = ' '\n                }\n\n                df.decimalFormatSymbols = customFormat\n                df.format(answerAsDecimal)\n            } catch (e: NumberFormatException) {\n                fep.answerText\n            }\n        }\n\n        if (data != null && fep.dataType == Constants.DATATYPE_TEXT && fep.question.getAdditionalAttribute(null, \"query\") != null) { // ItemsetWidget\n            var language: String? = \"\"\n            if (formController.getLanguages() != null && formController.getLanguages()!!.isNotEmpty()) {\n                language = formController.getLanguage()\n            }\n            return ItemsetDao(ItemsetDbAdapter()).getItemLabel(fep.answerValue!!.displayText, formController.getMediaFolder()!!.absolutePath, language)\n        }\n        return fep.answerText ?: \"\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListActivity.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.view.View\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.activity.viewModels\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport org.odk.collect.android.R\nimport org.odk.collect.android.activities.FormMapActivity\nimport org.odk.collect.android.formmanagement.FormFillingIntentFactory\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.preferences.dialogs.ServerAuthDialogFragment\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.androidshared.ui.ObviousProgressBar\nimport org.odk.collect.androidshared.ui.SnackbarUtils\nimport org.odk.collect.androidshared.ui.SnackbarUtils.DURATION_SHORT\nimport org.odk.collect.async.network.NetworkStateProvider\nimport org.odk.collect.lists.EmptyListView\nimport org.odk.collect.lists.RecyclerViewUtils\nimport org.odk.collect.permissions.PermissionListener\nimport org.odk.collect.permissions.PermissionsProvider\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport javax.inject.Inject\n\nclass BlankFormListActivity : LocalizedActivity(), OnFormItemClickListener {\n\n    @Inject\n    lateinit var viewModelFactory: BlankFormListViewModel.Factory\n\n    @Inject\n    lateinit var networkStateProvider: NetworkStateProvider\n\n    @Inject\n    lateinit var permissionsProvider: PermissionsProvider\n\n    private val viewModel: BlankFormListViewModel by viewModels { viewModelFactory }\n\n    private val adapter: BlankFormListAdapter = BlankFormListAdapter(this)\n\n    private val formLauncher =\n        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {\n            setResult(RESULT_OK, it.data)\n            finish()\n        }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        DaggerUtils.getComponent(this).inject(this)\n        setView(R.layout.activity_blank_form_list, false)\n        title = getString(org.odk.collect.strings.R.string.enter_data)\n        setSupportActionBar(findViewById(org.odk.collect.androidshared.R.id.toolbar))\n\n        val menuProvider = BlankFormListMenuProvider(this, viewModel, networkStateProvider)\n        addMenuProvider(menuProvider, this)\n\n        val list = findViewById<RecyclerView>(R.id.form_list)\n        list.layoutManager = LinearLayoutManager(this)\n        list.addItemDecoration(RecyclerViewUtils.verticalLineDivider(this))\n        list.adapter = adapter\n\n        initObservers()\n    }\n\n    override fun onFormClick(formUri: Uri) {\n        if (Intent.ACTION_PICK == intent.action) {\n            // caller is waiting on a picked form\n            setResult(RESULT_OK, Intent().setData(formUri))\n            finish()\n        } else {\n            // caller wants to view/edit a form, so launch FormFillingActivity\n            formLauncher.launch(FormFillingIntentFactory.newFormIntent(this, formUri))\n        }\n    }\n\n    override fun onMapButtonClick(id: Long) {\n        permissionsProvider.requestEnabledLocationPermissions(\n            this,\n            object : PermissionListener {\n                override fun granted() {\n                    startActivity(\n                        Intent(this@BlankFormListActivity, FormMapActivity::class.java).also {\n                            it.putExtra(FormMapActivity.EXTRA_FORM_ID, id)\n                        }\n                    )\n                }\n            }\n        )\n    }\n\n    private fun initObservers() {\n        viewModel.isLoading.observe(this) { isLoading ->\n            if (isLoading) {\n                findViewById<ObviousProgressBar>(org.odk.collect.androidshared.R.id.progressBar).show()\n            } else {\n                findViewById<ObviousProgressBar>(org.odk.collect.androidshared.R.id.progressBar).hide()\n            }\n        }\n\n        viewModel.syncResult.observe(this) { result ->\n            if (result != null) {\n                SnackbarUtils.showSnackbar(findViewById(R.id.form_list), result, DURATION_SHORT)\n            }\n        }\n\n        viewModel.formsToDisplay.observe(this) { forms ->\n            findViewById<RecyclerView>(R.id.form_list).visibility =\n                if (forms.isEmpty()) View.GONE else View.VISIBLE\n\n            findViewById<EmptyListView>(R.id.empty_list_message).visibility =\n                if (forms.isEmpty()) View.VISIBLE else View.GONE\n\n            adapter.setData(forms)\n        }\n\n        viewModel.isAuthenticationRequired().observe(this) { authenticationRequired ->\n            if (authenticationRequired) {\n                DialogFragmentUtils.showIfNotShowing(\n                    ServerAuthDialogFragment::class.java,\n                    supportFragmentManager\n                )\n            } else {\n                DialogFragmentUtils.dismissDialog(\n                    ServerAuthDialogFragment::class.java,\n                    supportFragmentManager\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListAdapter.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.net.Uri\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.Button\nimport androidx.recyclerview.widget.RecyclerView\nimport org.odk.collect.android.R\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\nimport org.odk.collect.lists.RecyclerViewUtils.matchParentWidth\n\nclass BlankFormListAdapter(\n    val listener: OnFormItemClickListener\n) : RecyclerView.Adapter<BlankFormListAdapter.BlankFormListItemWithMapViewHolder>() {\n\n    private var formItems = emptyList<BlankFormListItem>()\n\n    override fun onCreateViewHolder(\n        parent: ViewGroup,\n        viewType: Int\n    ): BlankFormListItemWithMapViewHolder {\n        return BlankFormListItemWithMapViewHolder(parent)\n    }\n\n    override fun onBindViewHolder(holder: BlankFormListItemWithMapViewHolder, position: Int) {\n        val item = formItems[position]\n        holder.setItem(item)\n\n        holder.itemView.setOnClickListener {\n            if (MultiClickGuard.allowClick(javaClass.name)) {\n                listener.onFormClick(item.contentUri)\n            }\n        }\n\n        val mapButton = holder.itemView.findViewById<Button>(R.id.map_button)\n\n        mapButton.visibility = if (item.geometryPath.isNotBlank()) {\n            View.VISIBLE\n        } else {\n            View.GONE\n        }\n\n        mapButton.setOnClickListener {\n            if (MultiClickGuard.allowClick(javaClass.name)) {\n                listener.onMapButtonClick(item.databaseId)\n            }\n        }\n    }\n\n    override fun getItemCount() = formItems.size\n\n    fun setData(blankFormItems: List<BlankFormListItem>) {\n        this.formItems = blankFormItems.toList()\n        notifyDataSetChanged()\n    }\n\n    class BlankFormListItemWithMapViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(\n        BlankFormListItemView(parent.context).also {\n            it.setTrailingView(R.layout.map_button)\n        }\n    ) {\n        fun setItem(item: BlankFormListItem) {\n            (itemView as BlankFormListItemView).setItem(item)\n        }\n\n        init {\n            matchParentWidth()\n        }\n    }\n}\n\ninterface OnFormItemClickListener {\n    fun onFormClick(formUri: Uri)\n\n    fun onMapButtonClick(id: Long)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItem.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.net.Uri\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.InstancesRepository\n\ndata class BlankFormListItem(\n    val databaseId: Long,\n    val formId: String,\n    val formName: String,\n    val formVersion: String,\n    val geometryPath: String,\n    val dateOfCreation: Long,\n    val dateOfLastUsage: Long,\n    val dateOfLastDetectedAttachmentsUpdate: Long?,\n    val contentUri: Uri\n)\n\nfun Form.toBlankFormListItem(projectId: String, instancesRepository: InstancesRepository) = BlankFormListItem(\n    databaseId = this.dbId,\n    formId = this.formId,\n    formName = this.displayName,\n    formVersion = this.version ?: \"\",\n    geometryPath = this.geometryXpath ?: \"\",\n    dateOfCreation = this.date,\n    dateOfLastUsage = instancesRepository\n        .getAllByFormId(this.formId)\n        .filter { it.formVersion == this.version }\n        .maxByOrNull { it.lastStatusChangeDate }?.lastStatusChangeDate ?: 0L,\n    dateOfLastDetectedAttachmentsUpdate = this.lastDetectedAttachmentsUpdateDate,\n    contentUri = FormsContract.getUri(projectId, this.dbId)\n)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItemView.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport org.odk.collect.android.databinding.BlankFormListItemBinding\nimport org.odk.collect.strings.R.string\nimport timber.log.Timber\nimport java.text.SimpleDateFormat\nimport java.util.Locale\n\nclass BlankFormListItemView(context: Context) : FrameLayout(context) {\n\n    val binding = BlankFormListItemBinding.inflate(LayoutInflater.from(context), this, true)\n\n    fun setItem(item: BlankFormListItem) {\n        binding.formTitle.text = item.formName\n\n        binding.formVersion.text =\n            binding.root.context.getString(\n                string.version_number,\n                item.formVersion\n            )\n        binding.formVersion.visibility =\n            if (item.formVersion.isNotBlank()) VISIBLE else GONE\n\n        binding.formId.text =\n            binding.root.context.getString(\n                string.id_number,\n                item.formId\n            )\n\n        binding.formHistory.text = try {\n            if (item.dateOfLastDetectedAttachmentsUpdate != null) {\n                SimpleDateFormat(\n                    binding.root.context.getString(string.updated_on_date_at_time),\n                    Locale.getDefault()\n                ).format(item.dateOfLastDetectedAttachmentsUpdate)\n            } else {\n                SimpleDateFormat(\n                    binding.root.context.getString(string.added_on_date_at_time),\n                    Locale.getDefault()\n                ).format(item.dateOfCreation)\n            }\n        } catch (e: IllegalArgumentException) {\n            Timber.e(e)\n            \"\"\n        }\n    }\n\n    fun setTrailingView(layoutId: Int) {\n        inflate(context, layoutId, binding.trailingView)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuProvider.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.view.Menu\nimport android.view.MenuInflater\nimport android.view.MenuItem\nimport androidx.activity.ComponentActivity\nimport androidx.appcompat.widget.SearchView\nimport androidx.core.view.MenuProvider\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog\nimport org.odk.collect.android.formlists.sorting.FormListSortingOption\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\nimport org.odk.collect.async.network.NetworkStateProvider\n\nclass BlankFormListMenuProvider(\n    private val activity: ComponentActivity,\n    private val viewModel: BlankFormListViewModel,\n    private val networkStateProvider: NetworkStateProvider? = null\n) : MenuProvider {\n\n    private var outOfSync = false\n    private var syncing = false\n\n    init {\n        viewModel.isLoading.observe(activity) { isLoading: Boolean ->\n            this.syncing = isLoading\n            activity.invalidateOptionsMenu()\n        }\n\n        viewModel.isOutOfSyncWithServer().observe(activity) { outOfSync: Boolean ->\n            this.outOfSync = outOfSync\n            activity.invalidateOptionsMenu()\n        }\n    }\n\n    override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {\n        inflater.inflate(R.menu.blank_form_list_menu, menu)\n\n        menu.findItem(R.id.menu_filter).apply {\n            setOnActionExpandListener(object : MenuItem.OnActionExpandListener {\n                override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {\n                    menu.findItem(R.id.menu_refresh).isVisible = false\n                    menu.findItem(R.id.menu_sort).isVisible = false\n                    return true\n                }\n\n                override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean {\n                    menu.findItem(R.id.menu_refresh).isVisible = viewModel.isMatchExactlyEnabled()\n                    menu.findItem(R.id.menu_sort).isVisible = true\n                    return true\n                }\n            })\n\n            (actionView as SearchView).apply {\n                queryHint = activity.resources.getString(org.odk.collect.strings.R.string.search)\n                maxWidth = Int.MAX_VALUE\n                setOnQueryTextListener(object : SearchView.OnQueryTextListener {\n                    override fun onQueryTextSubmit(query: String) = false\n\n                    override fun onQueryTextChange(newText: String): Boolean {\n                        viewModel.filterText = newText\n                        return false\n                    }\n                })\n            }\n            viewModel.filterText = \"\"\n        }\n    }\n\n    override fun onPrepareMenu(menu: Menu) {\n        val refreshItem = menu.findItem(R.id.menu_refresh)\n        refreshItem.isVisible = viewModel.isMatchExactlyEnabled()\n        refreshItem.isEnabled = !syncing\n        if (outOfSync) {\n            refreshItem.setIcon(R.drawable.ic_baseline_refresh_error_24)\n        } else {\n            refreshItem.setIcon(R.drawable.ic_baseline_refresh_24)\n        }\n    }\n\n    override fun onMenuItemSelected(item: MenuItem): Boolean {\n        if (!MultiClickGuard.allowClick(javaClass.name)) {\n            return true\n        }\n\n        return when (item.itemId) {\n            R.id.menu_refresh -> {\n                if (networkStateProvider?.isDeviceOnline == true) {\n                    viewModel.syncWithServer()\n                } else {\n                    ToastUtils.showShortToast(org.odk.collect.strings.R.string.no_connection)\n                }\n                true\n            }\n\n            R.id.menu_sort -> {\n                FormListSortingBottomSheetDialog(\n                    activity,\n                    BlankFormListViewModel.SortOrder.entries.map { getForListSortingOption(it) },\n                    viewModel.sortingOrder.ordinal\n                ) { newSortingOrder ->\n                    viewModel.sortingOrder =\n                        BlankFormListViewModel.SortOrder.entries[newSortingOrder]\n                }.show()\n\n                true\n            }\n            else -> false\n        }\n    }\n\n    private fun getForListSortingOption(it: BlankFormListViewModel.SortOrder) =\n        when (it) {\n            BlankFormListViewModel.SortOrder.NAME_ASC -> FormListSortingOption(\n                R.drawable.ic_sort_by_alpha,\n                org.odk.collect.strings.R.string.sort_by_name_asc\n            )\n\n            BlankFormListViewModel.SortOrder.NAME_DESC -> FormListSortingOption(\n                R.drawable.ic_sort_by_alpha,\n                org.odk.collect.strings.R.string.sort_by_name_desc\n            )\n\n            BlankFormListViewModel.SortOrder.DATE_DESC -> FormListSortingOption(\n                R.drawable.ic_access_time,\n                org.odk.collect.strings.R.string.sort_by_date_desc\n            )\n\n            BlankFormListViewModel.SortOrder.DATE_ASC -> FormListSortingOption(\n                R.drawable.ic_access_time,\n                org.odk.collect.strings.R.string.sort_by_date_asc\n            )\n\n            BlankFormListViewModel.SortOrder.LAST_SAVED -> FormListSortingOption(\n                R.drawable.ic_sort_by_last_saved,\n                org.odk.collect.strings.R.string.sort_by_last_saved\n            )\n        }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListViewModel.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.app.Application\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.asLiveData\nimport androidx.lifecycle.map\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.map\nimport org.odk.collect.android.backgroundwork.SyncFormsTaskSpec\nimport org.odk.collect.android.backgroundwork.TaskData\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.async.NotificationInfo\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.flowOnBackground\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.forms.FormSourceException.AuthRequired\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.settings.enums.FormUpdateMode\nimport org.odk.collect.settings.enums.StringIdEnumUtils.getFormUpdateMode\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\nclass BlankFormListViewModel(\n    private val instancesRepository: InstancesRepository,\n    private val application: Application,\n    private val formsDataService: FormsDataService,\n    private val scheduler: Scheduler,\n    private val generalSettings: Settings,\n    private val projectId: String,\n    private val showAllVersions: Boolean = false,\n    private val uniqueIdGenerator: UniqueIdGenerator\n) : ViewModel() {\n\n    private val _filterText = MutableStateFlow(\"\")\n    private val _sortingOrder = MutableStateFlow(getSortOrder())\n    private val filteredForms = formsDataService.getForms(projectId)\n        .combine(_filterText) { forms, filter ->\n            Pair(forms, filter)\n        }.combine(_sortingOrder) { (forms, filter), sort ->\n            Triple(forms, filter, sort)\n        }\n\n    val formsToDisplay: LiveData<List<BlankFormListItem>> =\n        filteredForms.map { (forms, filter, sort) ->\n            filterAndSortForms(forms, sort, filter)\n        }.flowOnBackground(scheduler).asLiveData()\n\n    val syncResult: LiveData<String?> = formsDataService.getDiskError(projectId)\n    val isLoading: LiveData<Boolean> = formsDataService.isSyncing(projectId)\n\n    var sortingOrder: SortOrder = getSortOrder()\n        get() { return getSortOrder() }\n\n        set(value) {\n            field = value\n            generalSettings.save(ProjectKeys.KEY_BLANK_FORM_SORT_ORDER, value.ordinal)\n            _sortingOrder.value = value\n        }\n\n    var filterText: String = \"\"\n        set(value) {\n            field = value\n            _filterText.value = value\n        }\n\n    init {\n        scheduler.immediate(\n            background = {\n                formsDataService.refresh(projectId)\n            },\n            foreground = {}\n        )\n    }\n\n    fun syncWithServer() {\n        scheduler.immediate(\n            getSyncTag(projectId),\n            SyncFormsTaskSpec(),\n            mapOf(TaskData.DATA_PROJECT_ID to projectId),\n            NotificationInfo(\n                uniqueIdGenerator.getInt(SYNC_NOTIFICATION_IDENTIFIER),\n                SYNC_NOTIFICATION_CHANNEL_NAME,\n                SYNC_NOTIFICATION_CHANNEL,\n                org.odk.collect.strings.R.string.form_update_notification_title\n            )\n        )\n    }\n\n    fun isMatchExactlyEnabled(): Boolean {\n        return generalSettings.getFormUpdateMode(application) == FormUpdateMode.MATCH_EXACTLY\n    }\n\n    fun isOutOfSyncWithServer(): LiveData<Boolean> {\n        return formsDataService.getServerError(projectId).map { obj: FormSourceException? ->\n            obj != null\n        }\n    }\n\n    fun isAuthenticationRequired(): LiveData<Boolean> {\n        return formsDataService.getServerError(projectId).map { error: FormSourceException? ->\n            if (error != null) {\n                error is AuthRequired\n            } else {\n                false\n            }\n        }\n    }\n\n    fun deleteForms(vararg databaseIds: Long) {\n        scheduler.immediate(\n            background = {\n                databaseIds.forEach {\n                    formsDataService.deleteForm(projectId, it)\n                }\n            },\n            foreground = {}\n        )\n    }\n\n    private fun filterAndSortForms(\n        forms: List<Form>,\n        sort: SortOrder,\n        filter: String\n    ): List<BlankFormListItem> {\n        var newListOfForms = forms\n            .filter {\n                !it.isDeleted\n            }.map { form ->\n                form.toBlankFormListItem(projectId, instancesRepository)\n            }\n\n        if (!showAllVersions) {\n            newListOfForms = newListOfForms.groupBy {\n                it.formId\n            }.map { (_, itemsWithSameId) ->\n                itemsWithSameId.sortedBy {\n                    it.dateOfCreation\n                }.last()\n            }\n        }\n\n        return when (sort) {\n            SortOrder.NAME_ASC -> newListOfForms.sortedBy { it.formName.lowercase() }\n            SortOrder.NAME_DESC -> newListOfForms.sortedByDescending { it.formName.lowercase() }\n            SortOrder.DATE_DESC -> newListOfForms.sortedByDescending {\n                it.dateOfLastDetectedAttachmentsUpdate ?: it.dateOfCreation\n            }\n            SortOrder.DATE_ASC -> newListOfForms.sortedBy {\n                it.dateOfLastDetectedAttachmentsUpdate ?: it.dateOfCreation\n            }\n            SortOrder.LAST_SAVED -> newListOfForms.sortedByDescending { it.dateOfLastUsage }\n        }.filter {\n            filter.isBlank() || it.formName.contains(filter, true)\n        }\n    }\n\n    private fun getSortOrder() =\n        SortOrder.entries[generalSettings.getInt(ProjectKeys.KEY_BLANK_FORM_SORT_ORDER)]\n\n    open class Factory(\n        private val instancesRepository: InstancesRepository,\n        private val application: Application,\n        private val formsDataService: FormsDataService,\n        private val scheduler: Scheduler,\n        private val generalSettings: Settings,\n        private val projectId: String,\n        private val uniqueIdGenerator: UniqueIdGenerator\n    ) : ViewModelProvider.Factory {\n\n        override fun <T : ViewModel> create(modelClass: Class<T>): T {\n            return BlankFormListViewModel(\n                instancesRepository,\n                application,\n                formsDataService,\n                scheduler,\n                generalSettings,\n                projectId,\n                !generalSettings.getBoolean(ProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS),\n                uniqueIdGenerator\n            ) as T\n        }\n    }\n\n    enum class SortOrder {\n        NAME_ASC,\n        NAME_DESC,\n        DATE_DESC,\n        DATE_ASC,\n        LAST_SAVED\n    }\n\n    companion object {\n        private const val SYNC_NOTIFICATION_CHANNEL = \"form_updates\"\n        private const val SYNC_NOTIFICATION_CHANNEL_NAME = \"Form updates\"\n        private const val SYNC_NOTIFICATION_IDENTIFIER = \"form_sync\"\n\n        private fun getSyncTag(projectId: String): String {\n            return \"match_exactly_foreground:$projectId\"\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/DeleteBlankFormFragment.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.CheckBox\nimport androidx.core.view.MenuHost\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.Lifecycle.State\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.map\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.android.R\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.lists.RecyclerViewUtils\nimport org.odk.collect.lists.RecyclerViewUtils.matchParentWidth\nimport org.odk.collect.lists.selects.MultiSelectAdapter\nimport org.odk.collect.lists.selects.MultiSelectControlsFragment\nimport org.odk.collect.lists.selects.MultiSelectListFragment\nimport org.odk.collect.lists.selects.MultiSelectViewModel\nimport org.odk.collect.lists.selects.SelectItem\nimport org.odk.collect.strings.R.string\n\nclass DeleteBlankFormFragment(\n    private val viewModelFactory: ViewModelProvider.Factory,\n    private val menuHost: MenuHost\n) : Fragment() {\n\n    private val blankFormListViewModel: BlankFormListViewModel by viewModels { viewModelFactory }\n    private val multiSelectViewModel: MultiSelectViewModel<BlankFormListItem> by viewModels {\n        MultiSelectViewModel.Factory(\n            blankFormListViewModel.formsToDisplay.map {\n                it.map { blankForm ->\n                    SelectItem(\n                        blankForm.databaseId.toString(),\n                        blankForm\n                    )\n                }\n            }\n        )\n    }\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n\n        childFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(MultiSelectListFragment::class) {\n                MultiSelectListFragment(\n                    getString(string.delete_file),\n                    multiSelectViewModel,\n                    ::SelectableBlankFormListItemViewHolder\n                ) {\n                    it.empty.setIcon(R.drawable.ic_baseline_delete_72)\n                    it.empty.setTitle(getString(string.empty_list_of_forms_to_delete_title))\n                    it.empty.setSubtitle(getString(string.empty_list_of_blank_forms_to_delete_subtitle))\n\n                    it.list.addItemDecoration(RecyclerViewUtils.verticalLineDivider(context))\n                }\n            }\n            .build()\n\n        childFragmentManager.setFragmentResultListener(\n            MultiSelectControlsFragment.REQUEST_ACTION,\n            this\n        ) { _, result ->\n            val selected = result.getStringArray(MultiSelectControlsFragment.RESULT_SELECTED)!!\n            onDeleteSelected(selected.map { it.toLong() }.toLongArray())\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return inflater.inflate(R.layout.delete_form_layout, container, false)\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        val blankFormListMenuProvider =\n            BlankFormListMenuProvider(requireActivity(), blankFormListViewModel)\n        menuHost.addMenuProvider(blankFormListMenuProvider, viewLifecycleOwner, State.RESUMED)\n    }\n\n    private fun onDeleteSelected(selected: LongArray) {\n        MaterialAlertDialogBuilder(requireContext())\n            .setTitle(string.delete_file)\n            .setMessage(\n                getString(\n                    string.delete_confirm,\n                    selected.size.toString()\n                )\n            )\n            .setPositiveButton(getString(string.delete_yes)) { _, _ ->\n                blankFormListViewModel.deleteForms(*selected)\n                multiSelectViewModel.unselectAll()\n            }\n            .setNegativeButton(getString(string.delete_no), null)\n            .show()\n    }\n}\n\nprivate class SelectableBlankFormListItemViewHolder(parent: ViewGroup) :\n    MultiSelectAdapter.ViewHolder<BlankFormListItem>(\n        BlankFormListItemView(parent.context).also {\n            it.setTrailingView(R.layout.checkbox)\n        }\n    ) {\n\n    init {\n        matchParentWidth()\n    }\n\n    override fun setItem(item: BlankFormListItem) {\n        (itemView as BlankFormListItemView).setItem(item)\n    }\n\n    override fun getCheckbox(): CheckBox {\n        return itemView.findViewById(R.id.checkbox)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragment.kt",
    "content": "package org.odk.collect.android.formlists.savedformlist\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.view.MenuHost\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.map\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.R\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.instancemanagement.isDeletable\nimport org.odk.collect.android.instancemanagement.isEdit\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.androidshared.ui.SnackbarUtils\nimport org.odk.collect.androidshared.ui.SnackbarUtils.SnackbarPresenterObserver\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.lists.RecyclerViewUtils\nimport org.odk.collect.lists.selects.MultiSelectControlsFragment\nimport org.odk.collect.lists.selects.MultiSelectListFragment\nimport org.odk.collect.lists.selects.MultiSelectViewModel\nimport org.odk.collect.lists.selects.SelectItem\nimport org.odk.collect.material.MaterialProgressDialogFragment\nimport org.odk.collect.strings.R.string\n\nclass DeleteSavedFormFragment(\n    private val viewModelFactory: ViewModelProvider.Factory,\n    private val menuHost: MenuHost? = null\n) : Fragment() {\n\n    private val savedFormListViewModel: SavedFormListViewModel by viewModels { viewModelFactory }\n    private val multiSelectViewModel: MultiSelectViewModel<Instance> by viewModels {\n        MultiSelectViewModel.Factory(\n            savedFormListViewModel.formsToDisplay.map { instanceList ->\n                val unsentEditsById = instanceList\n                    .filter { it.isEdit() }\n                    .groupBy { it.editOf }\n\n                instanceList\n                    .filter { it.isDeletable() }\n                    .filter { instance ->\n                        if (instance.status == Instance.STATUS_SUBMITTED) {\n                            true\n                        } else {\n                            val originalId = instance.editOf ?: instance.dbId\n\n                            unsentEditsById[originalId].orEmpty().none { edit ->\n                                edit.editNumber!! > (instance.editNumber ?: 0)\n                            }\n                        }\n                    }\n                    .map { instance ->\n                        SelectItem(\n                            instance.dbId.toString(),\n                            instance\n                        )\n                    }\n            }\n        )\n    }\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n\n        childFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(MultiSelectListFragment::class) {\n                MultiSelectListFragment(\n                    getString(string.delete_file),\n                    multiSelectViewModel,\n                    ::SelectableSavedFormListItemViewHolder\n                ) {\n                    it.empty.setIcon(R.drawable.ic_baseline_delete_72)\n                    it.empty.setTitle(getString(string.empty_list_of_forms_to_delete_title))\n                    it.empty.setSubtitle(getString(string.empty_list_of_saved_forms_to_delete_subtitle))\n\n                    it.list.addItemDecoration(RecyclerViewUtils.verticalLineDivider(context))\n                }\n            }\n            .build()\n\n        childFragmentManager.setFragmentResultListener(\n            MultiSelectControlsFragment.REQUEST_ACTION,\n            this\n        ) { _, result ->\n            val selected = result.getStringArray(MultiSelectControlsFragment.RESULT_SELECTED)!!\n            onDeleteSelected(selected.map { it.toLong() }.toLongArray())\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return inflater.inflate(\n            R.layout.delete_form_layout,\n            container,\n            false\n        )\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        menuHost?.addMenuProvider(\n            SavedFormListListMenuProvider(requireContext(), savedFormListViewModel),\n            viewLifecycleOwner,\n            Lifecycle.State.RESUMED\n        )\n\n        MaterialProgressDialogFragment.showOn(\n            viewLifecycleOwner,\n            savedFormListViewModel.isDeleting,\n            childFragmentManager\n        ) {\n            MaterialProgressDialogFragment().also {\n                it.message = getString(string.form_delete_message)\n            }\n        }\n    }\n\n    private fun onDeleteSelected(selected: LongArray) {\n        MaterialAlertDialogBuilder(requireContext())\n            .setTitle(string.delete_file)\n            .setMessage(\n                getString(\n                    string.delete_confirm,\n                    selected.size.toString()\n                )\n            )\n            .setPositiveButton(getString(string.delete_yes)) { _, _ ->\n                logDelete(selected.size)\n\n                multiSelectViewModel.unselectAll()\n                savedFormListViewModel.deleteForms(selected).observe(\n                    viewLifecycleOwner,\n                    object : SnackbarPresenterObserver<Int>(requireView()) {\n                        override fun getSnackbarDetails(value: Int): SnackbarUtils.SnackbarDetails {\n                            return SnackbarUtils.SnackbarDetails(\n                                getString(\n                                    string.file_deleted_ok,\n                                    value.toString()\n                                )\n                            )\n                        }\n                    }\n                )\n            }\n            .setNegativeButton(getString(string.delete_no), null)\n            .show()\n    }\n\n    private fun logDelete(size: Int) {\n        val event = when {\n            size >= 100 -> AnalyticsEvents.DELETE_SAVED_FORM_HUNDREDS\n            size >= 10 -> AnalyticsEvents.DELETE_SAVED_FORM_TENS\n            else -> AnalyticsEvents.DELETE_SAVED_FORM_FEW\n        }\n\n        Analytics.log(event)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SavedFormListItemView.kt",
    "content": "package org.odk.collect.android.formlists.savedformlist\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport android.widget.ImageView\nimport android.widget.TextView\nimport org.odk.collect.android.R\nimport org.odk.collect.android.databinding.FormChooserListItemMultipleChoiceBinding\nimport org.odk.collect.android.instancemanagement.getIcon\nimport org.odk.collect.android.instancemanagement.getStatusDescription\nimport org.odk.collect.android.instancemanagement.userVisibleInstanceName\nimport org.odk.collect.forms.instances.Instance\nimport java.util.Date\n\nclass SavedFormListItemView(context: Context) : FrameLayout(context) {\n\n    val binding =\n        FormChooserListItemMultipleChoiceBinding.inflate(LayoutInflater.from(context), this, true)\n\n    fun setItem(value: Instance) {\n        val lastStatusChangeDate = value.lastStatusChangeDate\n        val status = value.status\n\n        binding.root.findViewById<TextView>(R.id.form_title).text = value.userVisibleInstanceName(context.resources)\n        binding.root.findViewById<TextView>(R.id.form_subtitle).text =\n            getStatusDescription(context, status, Date(lastStatusChangeDate))\n\n        val statusIcon = binding.root.findViewById<ImageView>(R.id.image)\n        statusIcon.setImageResource(value.getIcon())\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SavedFormListListMenuProvider.kt",
    "content": "package org.odk.collect.android.formlists.savedformlist\n\nimport android.content.Context\nimport android.view.Menu\nimport android.view.MenuInflater\nimport android.view.MenuItem\nimport android.view.MenuItem.OnActionExpandListener\nimport androidx.appcompat.widget.SearchView\nimport androidx.core.view.MenuProvider\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog\nimport org.odk.collect.android.formlists.sorting.FormListSortingOption\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\nclass SavedFormListListMenuProvider(private val context: Context, private val viewModel: SavedFormListViewModel) : MenuProvider {\n    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {\n        menuInflater.inflate(R.menu.saved_form_list_menu, menu)\n\n        menu.findItem(R.id.menu_filter).apply {\n            setOnActionExpandListener(object : OnActionExpandListener {\n                override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {\n                    menu.findItem(R.id.menu_sort).isVisible = false\n                    return true\n                }\n\n                override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean {\n                    menu.findItem(R.id.menu_sort).isVisible = true\n                    return true\n                }\n            })\n\n            (actionView as SearchView).apply {\n                setOnQueryTextListener(object : SearchView.OnQueryTextListener {\n                    override fun onQueryTextSubmit(query: String) = false\n\n                    override fun onQueryTextChange(newText: String): Boolean {\n                        viewModel.filterText = newText\n                        return false\n                    }\n                })\n            }\n        }\n    }\n\n    override fun onMenuItemSelected(menuItem: MenuItem): Boolean {\n        if (!MultiClickGuard.allowClick(javaClass.name)) {\n            return true\n        }\n\n        return when (menuItem.itemId) {\n            R.id.menu_sort -> {\n                FormListSortingBottomSheetDialog(\n                    context,\n                    SavedFormListViewModel.SortOrder.entries.map { getFormListSortingOption(it) },\n                    viewModel.sortOrder.ordinal\n                ) {\n                    viewModel.sortOrder = SavedFormListViewModel.SortOrder.entries[it]\n                }.show()\n                true\n            }\n\n            else -> false\n        }\n    }\n\n    private fun getFormListSortingOption(it: SavedFormListViewModel.SortOrder) =\n        when (it) {\n            SavedFormListViewModel.SortOrder.NAME_ASC -> FormListSortingOption(\n                R.drawable.ic_sort_by_alpha,\n                org.odk.collect.strings.R.string.sort_by_name_asc\n            )\n\n            SavedFormListViewModel.SortOrder.NAME_DESC -> FormListSortingOption(\n                R.drawable.ic_sort_by_alpha,\n                org.odk.collect.strings.R.string.sort_by_name_desc\n            )\n\n            SavedFormListViewModel.SortOrder.DATE_DESC -> FormListSortingOption(\n                R.drawable.ic_access_time,\n                org.odk.collect.strings.R.string.sort_by_date_desc\n            )\n\n            SavedFormListViewModel.SortOrder.DATE_ASC -> FormListSortingOption(\n                R.drawable.ic_access_time,\n                org.odk.collect.strings.R.string.sort_by_date_asc\n            )\n        }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SavedFormListViewModel.kt",
    "content": "package org.odk.collect.android.formlists.savedformlist\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.asLiveData\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.map\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.instancemanagement.userVisibleInstanceName\nimport org.odk.collect.androidshared.async.TrackableWorker\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.flowOnBackground\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\nclass SavedFormListViewModel(\n    scheduler: Scheduler,\n    private val settings: Settings,\n    private val instancesDataService: InstancesDataService,\n    val projectId: String\n) : ViewModel() {\n\n    private val _sortOrder =\n        MutableStateFlow(SortOrder.entries[settings.getInt(ProjectKeys.KEY_SAVED_FORM_SORT_ORDER)])\n    var sortOrder: SortOrder = _sortOrder.value\n        set(value) {\n            settings.save(ProjectKeys.KEY_SAVED_FORM_SORT_ORDER, value.ordinal)\n            _sortOrder.value = value\n            field = value\n        }\n\n    private val _filterText = MutableStateFlow(\"\")\n    var filterText: String = \"\"\n        set(value) {\n            field = value\n            _filterText.value = value\n        }\n\n    val formsToDisplay: LiveData<List<Instance>> = instancesDataService.getInstances(projectId)\n        .map { instances -> instances.filter { instance -> instance.deletedDate == null } }\n        .combine(_sortOrder) { instances, order ->\n            when (order) {\n                SortOrder.NAME_DESC -> instances.sortedByDescending { it.userVisibleInstanceName().lowercase() }\n                SortOrder.DATE_DESC -> instances.sortedByDescending { it.lastStatusChangeDate }\n                SortOrder.NAME_ASC -> instances.sortedBy { it.userVisibleInstanceName().lowercase() }\n                SortOrder.DATE_ASC -> instances.sortedBy { it.lastStatusChangeDate }\n            }\n        }.combine(_filterText) { instances, filter ->\n            instances.filter { it.userVisibleInstanceName().contains(filter, ignoreCase = true) }\n        }.flowOnBackground(scheduler).asLiveData()\n\n    private val worker = TrackableWorker(scheduler)\n    val isDeleting: LiveData<Boolean> = worker.isWorking\n\n    fun deleteForms(databaseIds: LongArray): LiveData<Consumable<Int>?> {\n        val result = MutableLiveData<Consumable<Int>?>(null)\n        worker.immediate(\n            background = {\n                instancesDataService.deleteInstances(projectId, databaseIds)\n            },\n            foreground = { instancesDeleted ->\n                if (instancesDeleted) {\n                    result.value = Consumable(databaseIds.count())\n                } else {\n                    result.value = Consumable(0)\n                }\n            }\n        )\n\n        return result\n    }\n\n    enum class SortOrder {\n        NAME_ASC,\n        NAME_DESC,\n        DATE_DESC,\n        DATE_ASC\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/savedformlist/SelectableSavedFormListItemViewHolder.kt",
    "content": "package org.odk.collect.android.formlists.savedformlist\n\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.CheckBox\nimport org.odk.collect.android.R\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.lists.RecyclerViewUtils.matchParentWidth\nimport org.odk.collect.lists.selects.MultiSelectAdapter\n\nclass SelectableSavedFormListItemViewHolder(parent: ViewGroup) :\n    MultiSelectAdapter.ViewHolder<Instance>(\n        SavedFormListItemView(parent.context)\n    ) {\n    private var selectView = itemView\n\n    init {\n        matchParentWidth()\n    }\n\n    override fun setItem(item: Instance) {\n        (itemView as SavedFormListItemView).setItem(item)\n    }\n\n    override fun getCheckbox(): CheckBox {\n        return (itemView as SavedFormListItemView).binding.checkbox\n    }\n\n    override fun getSelectArea(): View {\n        return selectView\n    }\n\n    fun setOnDetailsClickListener(listener: () -> Unit) {\n        selectView = itemView.findViewById(R.id.selectView)\n        selectView.setOnClickListener { listener() }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingAdapter.kt",
    "content": "package org.odk.collect.android.formlists.sorting\n\nimport android.content.res.ColorStateList\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.core.graphics.drawable.DrawableCompat\nimport androidx.recyclerview.widget.RecyclerView\nimport org.odk.collect.android.databinding.SortItemLayoutBinding\nimport org.odk.collect.androidshared.system.ContextExt.getThemeAttributeValue\nimport java.util.function.Consumer\n\nclass FormListSortingAdapter(\n    private val sortingOptions: List<FormListSortingOption>,\n    private val selectedSortingOrder: Int,\n    private val listener: Consumer<Int>\n) : RecyclerView.Adapter<FormListSortingAdapter.ViewHolder>() {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        val binding = SortItemLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        return ViewHolder(binding)\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n        holder.binding.root.setOnClickListener {\n            listener.accept(position)\n            selectItem(holder.binding)\n        }\n\n        val sortingOption = sortingOptions[position]\n\n        holder.binding.title.setText(sortingOption.text)\n        holder.binding.icon.setImageResource(sortingOption.icon)\n        holder.binding.icon.tag = sortingOption.icon\n        holder.binding.icon.setImageDrawable(\n            DrawableCompat.wrap(holder.binding.icon.drawable).mutate()\n        )\n\n        if (position == selectedSortingOrder) {\n            selectItem(holder.binding)\n        }\n    }\n\n    private fun selectItem(binding: SortItemLayoutBinding) {\n        binding.title.setTextColor(getThemeAttributeValue(binding.root.context, androidx.appcompat.R.attr.colorPrimary))\n        DrawableCompat.setTintList(\n            binding.icon.drawable,\n            ColorStateList.valueOf(getThemeAttributeValue(binding.root.context, androidx.appcompat.R.attr.colorPrimary))\n        )\n    }\n\n    override fun getItemCount() = sortingOptions.size\n\n    class ViewHolder(val binding: SortItemLayoutBinding) : RecyclerView.ViewHolder(binding.root)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingBottomSheetDialog.kt",
    "content": "package org.odk.collect.android.formlists.sorting\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport androidx.recyclerview.widget.DefaultItemAnimator\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.bottomsheet.BottomSheetDialog\nimport org.odk.collect.android.R\nimport java.util.function.Consumer\n\nclass FormListSortingBottomSheetDialog(\n    context: Context,\n    val options: List<FormListSortingOption>,\n    val selectedOption: Int,\n    val onSelectedOptionChanged: Consumer<Int>\n) : BottomSheetDialog(context) {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        setContentView(LayoutInflater.from(context).inflate(R.layout.bottom_sheet, null))\n\n        findViewById<RecyclerView>(R.id.recyclerView)?.apply {\n            adapter = FormListSortingAdapter(\n                options,\n                selectedOption\n            ) { position ->\n                onSelectedOptionChanged.accept(position)\n                dismiss()\n            }\n            layoutManager = LinearLayoutManager(context)\n            itemAnimator = DefaultItemAnimator()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formlists/sorting/FormListSortingOption.kt",
    "content": "package org.odk.collect.android.formlists.sorting\n\ndata class FormListSortingOption(\n    val icon: Int,\n    val text: Int\n)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/CollectFormEntryControllerFactory.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport android.os.Handler\nimport android.os.Looper\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.condition.EvaluationContext\nimport org.javarosa.core.model.condition.FilterStrategy\nimport org.javarosa.core.model.instance.DataInstance\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.form.api.FormEntryController\nimport org.javarosa.form.api.FormEntryModel\nimport org.javarosa.xpath.expr.XPathExpression\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.dynamicpreload.ExternalDataManagerImpl\nimport org.odk.collect.android.dynamicpreload.handler.ExternalDataHandlerPull\nimport org.odk.collect.android.formmanagement.finalization.EditedFormFinalizationProcessor\nimport org.odk.collect.android.preferences.SettingsExt.getExperimentalOptIn\nimport org.odk.collect.android.tasks.FormLoaderTask.FormEntryControllerFactory\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.entities.javarosa.filter.LocalEntitiesFilterStrategy\nimport org.odk.collect.entities.javarosa.filter.PullDataFunctionHandler\nimport org.odk.collect.entities.javarosa.finalization.EntityFormFinalizationProcessor\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.geo.javarosa.IntersectsFunctionHandler\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\nimport java.io.File\nimport java.util.function.Supplier\nimport kotlin.system.measureTimeMillis\n\nclass CollectFormEntryControllerFactory(\n    private val entitiesRepository: EntitiesRepository,\n    private val settings: Settings\n) :\n    FormEntryControllerFactory {\n    override fun create(formDef: FormDef, formMediaDir: File, instance: Instance?): FormEntryController {\n        val externalDataManager = ExternalDataManagerImpl(formMediaDir).also {\n            Collect.getInstance().externalDataManager = it\n        }\n\n        return FormEntryController(FormEntryModel(formDef)).also {\n            val externalDataHandlerPull = ExternalDataHandlerPull(externalDataManager)\n            it.addFunctionHandler(\n                PullDataFunctionHandler(\n                    entitiesRepository,\n                    externalDataHandlerPull\n                )\n            )\n\n            it.addFunctionHandler(IntersectsFunctionHandler())\n\n            it.addPostProcessor(EntityFormFinalizationProcessor())\n            it.addPostProcessor(EditedFormFinalizationProcessor(instance))\n\n            if (settings.getExperimentalOptIn(ProjectKeys.KEY_DEBUG_FILTERS)) {\n                it.addFilterStrategy(LoggingFilterStrategy())\n            }\n\n            it.addFilterStrategy(LocalEntitiesFilterStrategy(entitiesRepository))\n        }\n    }\n}\n\nprivate class LoggingFilterStrategy : FilterStrategy {\n    override fun filter(\n        sourceInstance: DataInstance<*>,\n        nodeSet: TreeReference,\n        predicate: XPathExpression,\n        children: MutableList<TreeReference>,\n        evaluationContext: EvaluationContext,\n        next: Supplier<MutableList<TreeReference>>\n    ): MutableList<TreeReference> {\n        val result: MutableList<TreeReference>\n        val filterTime = measureTimeMillis { result = next.get() }\n        Handler(Looper.getMainLooper()).post {\n            ToastUtils.showShortToast(\"Filter took ${filterTime / 1000.0}s\")\n        }\n\n        return result\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/FormFillingIntentFactory.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport org.odk.collect.android.external.FormUriActivity\nimport org.odk.collect.android.external.InstancesContract\nimport kotlin.reflect.KClass\n\nobject FormFillingIntentFactory {\n    fun newFormIntent(\n        context: Context,\n        uri: Uri?,\n        clazz: KClass<out Activity> = FormUriActivity::class\n    ): Intent {\n        return Intent(context, clazz.java).also {\n            it.action = Intent.ACTION_EDIT\n            it.data = uri\n        }\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun editDraftFormIntent(\n        context: Context,\n        uri: Uri?,\n        clazz: KClass<out Activity> = FormUriActivity::class\n    ): Intent {\n        return Intent(context, clazz.java).also {\n            it.action = Intent.ACTION_EDIT\n            it.data = uri\n        }\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun editDraftFormIntent(\n        context: Context,\n        projectId: String,\n        instanceId: Long,\n        clazz: KClass<out Activity> = FormUriActivity::class\n    ): Intent {\n        return Intent(context, clazz.java).also {\n            it.action = Intent.ACTION_EDIT\n            it.data = InstancesContract.getUri(projectId, instanceId)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/FormSourceExceptionMapper.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport android.content.Context\nimport org.odk.collect.android.R\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.strings.localization.getLocalizedString\n\nclass FormSourceExceptionMapper(private val context: Context) {\n    fun getMessage(exception: FormSourceException?): String {\n        return when (exception) {\n            is FormSourceException.Unreachable -> {\n                context.getLocalizedString(\n                    org.odk.collect.strings.R.string.unreachable_error,\n                    exception.serverUrl\n                ) + \" \" + context.getLocalizedString(\n                    org.odk.collect.strings.R.string.report_to_project_lead\n                )\n            }\n            is FormSourceException.SecurityError -> {\n                context.getLocalizedString(\n                    org.odk.collect.strings.R.string.security_error,\n                    exception.serverUrl\n                ) + \" \" + context.getLocalizedString(\n                    org.odk.collect.strings.R.string.report_to_project_lead\n                )\n            }\n            is FormSourceException.ServerError -> {\n                context.getLocalizedString(\n                    org.odk.collect.strings.R.string.server_error,\n                    exception.serverUrl,\n                    exception.statusCode\n                ) + \" \" + context.getLocalizedString(\n                    org.odk.collect.strings.R.string.report_to_project_lead\n                )\n            }\n            is FormSourceException.ParseError -> {\n                context.getLocalizedString(\n                    org.odk.collect.strings.R.string.invalid_response,\n                    exception.serverUrl\n                ) + \" \" + context.getLocalizedString(\n                    org.odk.collect.strings.R.string.report_to_project_lead\n                )\n            }\n            is FormSourceException.ServerNotOpenRosaError -> {\n                \"This server does not correctly implement the OpenRosa formList API.\" + \" \" + context.getLocalizedString(\n                    org.odk.collect.strings.R.string.report_to_project_lead\n                )\n            }\n            else -> {\n                context.getLocalizedString(org.odk.collect.strings.R.string.report_to_project_lead)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.asLiveData\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.StateFlow\nimport org.odk.collect.android.formmanagement.download.FormDownloadException\nimport org.odk.collect.android.formmanagement.download.ServerFormDownloader\nimport org.odk.collect.android.formmanagement.matchexactly.ServerFormsSynchronizer\nimport org.odk.collect.android.formmanagement.metadata.FormMetadataParser\nimport org.odk.collect.android.notifications.Notifier\nimport org.odk.collect.android.projects.ProjectDependencyModule\nimport org.odk.collect.android.state.DataKeys\nimport org.odk.collect.androidshared.data.AppState\nimport org.odk.collect.androidshared.data.DataService\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.settings.keys.ProjectKeys\nimport java.io.File\nimport java.util.function.Supplier\n\nclass FormsDataService(\n    appState: AppState,\n    private val notifier: Notifier,\n    private val projectDependencyModuleFactory: ProjectDependencyFactory<ProjectDependencyModule>,\n    private val clock: Supplier<Long>\n) : DataService(appState) {\n\n    private val forms by qualifiedData(DataKeys.FORMS, emptyList<Form>()) { projectId ->\n        val projectDependencies = projectDependencyModuleFactory.create(projectId)\n        projectDependencies.formsRepository.all\n    }\n\n    private val formsCount by qualifiedData(DataKeys.FORMS_COUNT, 0) { projectId ->\n        val projectDependencies = projectDependencyModuleFactory.create(projectId)\n        projectDependencies.formsRepository.all.filter { !it.isDeleted }.size\n    }\n\n    private val syncing by qualifiedData(DataKeys.SYNC_STATUS_SYNCING, false)\n    private val serverError by qualifiedData<FormSourceException?>(DataKeys.SYNC_STATUS_ERROR, null)\n    private val diskError by qualifiedData<String?>(DataKeys.DISK_ERROR, null)\n\n    fun getForms(projectId: String): Flow<List<Form>> {\n        return forms.flow(projectId)\n    }\n\n    fun getFormsCount(projectId: String): StateFlow<Int> = formsCount.flow(projectId)\n\n    fun isSyncing(projectId: String): LiveData<Boolean> {\n        return syncing.flow(projectId).asLiveData()\n    }\n\n    fun getServerError(projectId: String): LiveData<FormSourceException?> {\n        return serverError.flow(projectId).asLiveData()\n    }\n\n    fun getDiskError(projectId: String): LiveData<String?> {\n        return diskError.flow(projectId).asLiveData()\n    }\n\n    fun clear(projectId: String) {\n        serverError.set(projectId, null)\n    }\n\n    fun refresh(projectId: String) {\n        val projectDependencies = projectDependencyModuleFactory.create(projectId)\n        projectDependencies.formsLock.withLock { acquiredLock ->\n            if (acquiredLock) {\n                startSync(projectId)\n                syncWithStorage(projectId)\n                update(projectId)\n                finishSyncWithStorage(projectId)\n            }\n        }\n    }\n\n    fun downloadForms(\n        projectId: String,\n        forms: List<ServerFormDetails>,\n        progressReporter: (Int, Int) -> Unit,\n        isCancelled: () -> Boolean\n    ): Map<ServerFormDetails, FormDownloadException?> {\n        val results = mutableMapOf<ServerFormDetails, FormDownloadException?>()\n\n        val projectDependencyModule = projectDependencyModuleFactory.create(projectId)\n        projectDependencyModule.formsLock.withLock { acquiredLock ->\n            if (acquiredLock) {\n                val formDownloader =\n                    formDownloader(projectDependencyModule, clock)\n\n                results.putAll(ServerFormUseCases.downloadForms(\n                    forms,\n                    formDownloader,\n                    progressReporter,\n                    isCancelled\n                ))\n            }\n        }\n        return results\n    }\n\n    /**\n     * Downloads updates for the project's already downloaded forms. If Automatic download is\n     * disabled the user will just be notified that there are updates available.\n     */\n    fun downloadUpdates(projectId: String) {\n        val projectDependencies = projectDependencyModuleFactory.create(projectId)\n        projectDependencies.formsLock.withLock { acquiredLock ->\n            if (acquiredLock) {\n                syncWithStorage(projectId)\n\n                val formDownloader = formDownloader(projectDependencies, clock)\n\n                try {\n                    val serverForms = ServerFormUseCases.fetchFormDetails(\n                        projectDependencies.formsRepository,\n                        projectDependencies.formSource\n                    )\n                    val updatedForms = serverForms.filter { it.isUpdated() }\n                    if (updatedForms.isNotEmpty()) {\n                        if (projectDependencies.generalSettings.getBoolean(ProjectKeys.KEY_AUTOMATIC_UPDATE)) {\n                            val results = ServerFormUseCases.downloadForms(\n                                updatedForms,\n                                formDownloader\n                            )\n\n                            notifier.onUpdatesDownloaded(results, projectId)\n                        } else {\n                            notifier.onUpdatesAvailable(updatedForms, projectId)\n                        }\n                    }\n\n                    update(projectId)\n                } catch (_: FormSourceException) {\n                    // Ignored\n                }\n            }\n        }\n    }\n\n    /**\n     * Downloads new forms, updates existing forms and deletes forms that are no longer part of\n     * the project's form list.\n     */\n    @JvmOverloads\n    fun matchFormsWithServer(\n        projectId: String,\n        notify: Boolean = true,\n        isStopped: (() -> Boolean) = { false }\n    ): Boolean {\n        val projectDependencies = projectDependencyModuleFactory.create(projectId)\n        return projectDependencies.formsLock.withLock { acquiredLock ->\n            if (acquiredLock) {\n                startSync(projectId)\n                syncWithStorage(projectId)\n\n                val formDownloader = formDownloader(projectDependencies, clock)\n\n                val serverFormsSynchronizer = ServerFormsSynchronizer(\n                    { repository, source -> ServerFormUseCases.fetchFormDetails(repository, source) },\n                    projectDependencies.formsRepository,\n                    projectDependencies.instancesRepository,\n                    formDownloader,\n                    projectDependencies.formSource\n                )\n\n                val exception = try {\n                    serverFormsSynchronizer.synchronize()\n                    if (notify) {\n                        notifier.onSync(null, projectId)\n                    }\n\n                    null\n                } catch (e: FormSourceException) {\n                    if (notify) {\n                        notifier.onSync(e, projectId)\n                    } else if (isStopped()) {\n                        notifier.onSyncStopped(projectId)\n                    }\n\n                    e\n                }\n\n                update(projectId)\n                finishSyncWithServer(projectId, exception)\n                exception == null\n            } else {\n                false\n            }\n        }\n    }\n\n    fun deleteForm(projectId: String, formId: Long) {\n        val projectDependencies = projectDependencyModuleFactory.create(projectId)\n        LocalFormUseCases.deleteForm(\n            projectDependencies.formsRepository,\n            projectDependencies.instancesRepository,\n            formId\n        )\n\n        update(projectId)\n    }\n\n    private fun syncWithStorage(projectId: String) {\n        val projectDependencies = projectDependencyModuleFactory.create(projectId)\n        val error = LocalFormUseCases.synchronizeWithDisk(\n            projectDependencies.formsRepository,\n            projectDependencies.formsDir\n        )\n\n        diskError.set(projectId, error)\n    }\n\n    private fun startSync(projectId: String) {\n        syncing.set(projectId, true)\n    }\n\n    private fun finishSyncWithServer(projectId: String, exception: FormSourceException? = null) {\n        serverError.set(projectId, exception)\n        syncing.set(projectId, false)\n    }\n\n    private fun finishSyncWithStorage(projectId: String) {\n        syncing.set(projectId, false)\n    }\n}\n\nprivate fun formDownloader(\n    projectDependencyModule: ProjectDependencyModule,\n    clock: Supplier<Long>\n): ServerFormDownloader {\n    return ServerFormDownloader(\n        projectDependencyModule.formSource,\n        projectDependencyModule.formsRepository,\n        File(projectDependencyModule.cacheDir),\n        projectDependencyModule.formsDir,\n        FormMetadataParser,\n        clock,\n        projectDependencyModule.entitiesRepository,\n        projectDependencyModule.entitySource\n    )\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/LocalFormUseCases.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport android.database.SQLException\nimport org.javarosa.xform.parse.XFormParser\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.formmanagement.metadata.FormMetadata\nimport org.odk.collect.android.formmanagement.metadata.FormMetadataParser\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.androidshared.utils.Validator\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.shared.strings.Md5.getMd5Hash\nimport org.odk.collect.strings.localization.getLocalizedString\nimport timber.log.Timber\nimport java.io.File\nimport java.util.Collections\nimport java.util.LinkedList\n\nobject LocalFormUseCases {\n\n    private var counter = 0\n\n    @JvmStatic\n    fun deleteForm(\n        formsRepository: FormsRepository,\n        instancesRepository: InstancesRepository,\n        id: Long\n    ) {\n        val form = formsRepository[id]\n        val instancesForVersion = instancesRepository.getAllNotDeletedByFormIdAndVersion(\n            form!!.formId,\n            form.version\n        )\n\n        // If there's more than one form with the same formid/version, trust the user that they want to truly delete this one\n        // because otherwise it may not ever be removed (instance deletion only deletes one corresponding form).\n        val formsWithSameFormIdVersion = formsRepository.getAllByFormIdAndVersion(\n            form.formId,\n            form.version\n        )\n\n        if (instancesForVersion.isEmpty() || formsWithSameFormIdVersion.size > 1) {\n            formsRepository.delete(id)\n        } else {\n            formsRepository.softDelete(form.dbId)\n        }\n    }\n\n    fun synchronizeWithDisk(formsRepository: FormsRepository, formsDir: String?): String {\n        var statusMessage = \"\"\n        val instance = ++counter\n        Timber.i(\"[%d] doInBackground begins!\", instance)\n        val idsToDelete: MutableList<Long> = ArrayList()\n        return try {\n            // Process everything then report what didn't work.\n            val errors = StringBuilder()\n            val formDir = File(formsDir)\n            if (formDir.exists() && formDir.isDirectory) {\n                // Get all the files in the /odk/foms directory\n                val formDefs = formDir.listFiles()\n\n                // Step 1: assemble the candidate form files\n                val formsToAdd = filterFormsToAdd(formDefs, instance)\n\n                // Step 2: quickly run through and figure out what files we need to\n                // parse and update; this is quick, as we only calculate the md5\n                // and see if it has changed.\n                val uriToUpdate: MutableList<IdFile?> = ArrayList()\n                val forms = formsRepository.all\n                for (form in forms) {\n                    // For each element in the provider, see if the file already exists\n                    val sqlFilename = form.formFilePath\n                    val md5 = form.mD5Hash\n                    val sqlFile = File(sqlFilename)\n                    if (sqlFile.exists()) {\n                        // remove it from the list of forms (we only want forms\n                        // we haven't added at the end)\n                        formsToAdd.remove(sqlFile)\n                        val md5Computed = sqlFile.getMd5Hash()\n                        if (md5Computed == null || md5 == null || md5Computed != md5) {\n                            // Probably someone overwrite the file on the sdcard\n                            // So re-parse it and update it's information\n                            val id = form.dbId\n                            uriToUpdate.add(IdFile(id, sqlFile))\n                        }\n                    } else {\n                        // File not found in sdcard but file path found in database\n                        // probably because the file has been deleted or filename was changed in sdcard\n                        // Add the ID to list so that they could be deleted all together\n                        val id = form.dbId\n                        idsToDelete.add(id)\n                    }\n                }\n\n                // Delete the forms not found in sdcard from the database\n                for (id in idsToDelete) {\n                    formsRepository.delete(id)\n                }\n\n                // Step3: go through uriToUpdate to parse and update each in turn.\n                // Note: buildContentValues calls getMetadataFromFormDefinition which parses the\n                // form XML. This takes time for large forms and/or slow devices.\n                Collections.shuffle(uriToUpdate) // Big win if multiple DiskSyncTasks running\n                for (entry in uriToUpdate) {\n                    val formDefFile = entry!!.file\n                    // Probably someone overwrite the file on the sdcard\n                    // So re-parse it and update it's information\n                    var form: Form\n                    form = try {\n                        parseForm(formDefFile)\n                    } catch (e: IllegalArgumentException) {\n                        errors.append(e.message).append(\"\\r\\n\")\n                        val badFile = File(\n                            formDefFile.parentFile,\n                            formDefFile.name + \".bad\"\n                        )\n                        badFile.delete()\n                        formDefFile.renameTo(badFile)\n                        continue\n                    }\n                    formsRepository.save(\n                        Form.Builder(form)\n                            .dbId(entry.id)\n                            .build()\n                    )\n                }\n                uriToUpdate.clear()\n\n                // Step 4: go through the newly-discovered files in xFormsToAdd and add them.\n                // This is slow because buildContentValues(...) is slow.\n                //\n                Collections.shuffle(formsToAdd) // Big win if multiple DiskSyncTasks running\n                while (!formsToAdd.isEmpty()) {\n                    val formDefFile = formsToAdd.removeAt(0)\n\n                    // Since parsing is so slow, if there are multiple tasks,\n                    // they may have already updated the database.\n                    // Skip this file if that is the case.\n                    if (formsRepository.getOneByPath(formDefFile!!.absolutePath) != null) {\n                        Timber.i(\n                            \"[%d] skipping -- definition already recorded: %s\",\n                            instance,\n                            formDefFile.absolutePath\n                        )\n                        continue\n                    }\n\n                    // Parse it for the first time...\n                    var form: Form\n                    form = try {\n                        parseForm(formDefFile)\n                    } catch (e: IllegalArgumentException) {\n                        errors.append(e.message).append(\"\\r\\n\")\n                        val badFile = File(\n                            formDefFile.parentFile,\n                            formDefFile.name + \".bad\"\n                        )\n                        badFile.delete()\n                        formDefFile.renameTo(badFile)\n                        continue\n                    }\n\n                    // insert into content provider\n                    try {\n                        // insert failures are OK and expected if multiple\n                        // DiskSync scanners are active.\n                        formsRepository.save(form)\n                    } catch (e: SQLException) {\n                        Timber.i(\"[%d] %s\", instance, e.toString())\n                    }\n                }\n            }\n            if (errors.length != 0) {\n                statusMessage = errors.toString()\n            } else {\n                Timber.d(\n                    Collect.getInstance()\n                        .getLocalizedString(org.odk.collect.strings.R.string.finished_disk_scan)\n                )\n            }\n            statusMessage\n        } finally {\n            Timber.i(\"[%d] doInBackground ends!\", instance)\n        }\n    }\n\n    @JvmStatic\n    fun filterFormsToAdd(formDefs: Array<File>?, backgroundInstanceId: Int): MutableList<File?> {\n        val formsToAdd: MutableList<File?> = LinkedList()\n        if (formDefs != null) {\n            for (candidate in formDefs) {\n                if (shouldAddFormFile(candidate.name)) {\n                    formsToAdd.add(candidate)\n                } else {\n                    Timber.i(\"[%d] Ignoring: %s\", backgroundInstanceId, candidate.absolutePath)\n                }\n            }\n        }\n        return formsToAdd\n    }\n\n    @JvmStatic\n    fun shouldAddFormFile(fileName: String): Boolean {\n        // discard files beginning with \".\"\n        // discard files not ending with \".xml\" or \".xhtml\"\n        val ignoredFile = fileName.startsWith(\".\")\n        val xmlFile = fileName.endsWith(\".xml\")\n        val xhtmlFile = fileName.endsWith(\".xhtml\")\n        return !ignoredFile && (xmlFile || xhtmlFile)\n    }\n\n    @Throws(IllegalArgumentException::class)\n    private fun parseForm(formDefFile: File?): Form {\n        // Probably someone overwrite the file on the sdcard\n        // So re-parse it and update it's information\n        val builder = Form.Builder()\n        val formMetadata: FormMetadata = try {\n            FormMetadataParser.readMetadata(formDefFile!!)\n        } catch (e: RuntimeException) {\n            throw IllegalArgumentException(formDefFile!!.name + \" :: \" + e.toString())\n        } catch (e: XFormParser.ParseException) {\n            throw IllegalArgumentException(formDefFile!!.name + \" :: \" + e.toString())\n        }\n\n        // update date\n        val now = System.currentTimeMillis()\n        builder.date(now)\n        val title = formMetadata.title\n        if (title != null) {\n            builder.displayName(title)\n        } else {\n            throw IllegalArgumentException(\n                Collect.getInstance()\n                    .getLocalizedString(\n                        org.odk.collect.strings.R.string.xform_parse_error,\n                        formDefFile!!.name,\n                        \"title\"\n                    )\n            )\n        }\n        val formid = formMetadata.id\n        if (formid != null) {\n            builder.formId(formid)\n        } else {\n            throw IllegalArgumentException(\n                Collect.getInstance()\n                    .getLocalizedString(\n                        org.odk.collect.strings.R.string.xform_parse_error,\n                        formDefFile!!.name,\n                        \"id\"\n                    )\n            )\n        }\n        val version = formMetadata.version\n        if (version != null) {\n            builder.version(version)\n        }\n        val submission = formMetadata.submissionUri\n        if (submission != null) {\n            if (Validator.isUrlValid(submission)) {\n                builder.submissionUri(submission)\n            } else {\n                throw IllegalArgumentException(\n                    Collect.getInstance().getLocalizedString(\n                        org.odk.collect.strings.R.string.xform_parse_error,\n                        formDefFile!!.name,\n                        \"submission url\"\n                    )\n                )\n            }\n        }\n        val base64RsaPublicKey = formMetadata.base64RsaPublicKey\n        if (base64RsaPublicKey != null) {\n            builder.base64RSAPublicKey(base64RsaPublicKey)\n        }\n        builder.autoDelete(formMetadata.autoDelete)\n        builder.autoSend(formMetadata.autoSend)\n        builder.geometryXpath(formMetadata.geometryXPath)\n        builder.usesEntities(formMetadata.isEntityForm)\n\n        // Note, the path doesn't change here, but it needs to be included so the\n        // update will automatically update the .md5 and the cache path.\n        builder.formFilePath(formDefFile!!.absolutePath)\n        builder.formMediaPath(\n            FileUtils.constructMediaPath(\n                formDefFile.absolutePath\n            )\n        )\n        return builder.build()\n    }\n\n    private class IdFile(val id: Long, val file: File)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/OpenRosaClientProvider.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport org.odk.collect.android.utilities.WebCredentialsUtils\nimport org.odk.collect.openrosa.forms.OpenRosaClient\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface\nimport org.odk.collect.openrosa.parse.Kxml2OpenRosaResponseParser\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\nclass OpenRosaClientProvider(\n    private val settingsFactory: ProjectDependencyFactory<Settings>,\n    private val openRosaHttpInterface: OpenRosaHttpInterface\n) {\n\n    fun create(projectId: String): OpenRosaClient {\n        val settings = settingsFactory.create(projectId)\n        val serverURL = settings.getString(ProjectKeys.KEY_SERVER_URL)!!\n\n        return OpenRosaClient(\n            serverURL,\n            openRosaHttpInterface,\n            WebCredentialsUtils(settings),\n            Kxml2OpenRosaResponseParser\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.formmanagement\n\nimport org.odk.collect.forms.ManifestFile\nimport java.io.Serializable\n\ndata class ServerFormDetails @JvmOverloads constructor(\n    val formName: String?,\n    val downloadUrl: String?,\n    val formId: String?,\n    val formVersion: String?,\n    val hash: String?,\n    val manifest: ManifestFile?,\n    val type: Type\n) : Serializable {\n\n    @Deprecated(message = \"Use primary constructor instead\")\n    constructor(\n        formName: String?,\n        downloadUrl: String?,\n        formId: String?,\n        formVersion: String?,\n        hash: String?,\n        isNotOnDevice: Boolean,\n        isUpdated: Boolean,\n        manifest: ManifestFile?,\n    ) : this(\n        formName,\n        downloadUrl,\n        formId,\n        formVersion,\n        hash,\n        manifest,\n        if (isNotOnDevice) {\n            Type.New\n        } else if (isUpdated) {\n            Type.UpdatedVersion\n        } else {\n            Type.OnDevice\n        }\n    )\n\n    fun isUpdated(): Boolean {\n        return type != Type.OnDevice && type != Type.New\n    }\n\n    companion object {\n        private const val serialVersionUID = 4L\n    }\n\n    enum class Type {\n        /**\n         * The form is on the device already\n         */\n        OnDevice,\n\n        /**\n         * The form is not on the device\n         */\n        New,\n\n        /**\n         * The form is on the device, but this is a new version with a new form version and hash\n         */\n        UpdatedVersion,\n\n        /**\n         * The form is on the device, but this is a new version with a new hash\n         */\n        UpdatedHash,\n\n        /**\n         * This version of the form is on the device, but new/updated media files are available\n         */\n        UpdatedMedia\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport org.apache.commons.io.FileUtils.copyFileToDirectory\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.formmanagement.download.FormDownloadException\nimport org.odk.collect.android.formmanagement.download.FormDownloader\nimport org.odk.collect.android.instancemanagement.autosend.getLastUpdated\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.android.utilities.FormUtils\nimport org.odk.collect.async.OngoingWorkListener\nimport org.odk.collect.entities.LocalEntityUseCases\nimport org.odk.collect.entities.server.EntitySource\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormSource\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.ManifestFile\nimport org.odk.collect.forms.MediaFile\nimport org.odk.collect.shared.strings.Md5.getMd5Hash\nimport timber.log.Timber\nimport java.io.File\nimport java.io.IOException\n\nobject ServerFormUseCases {\n\n    @JvmStatic\n    @Throws(FormSourceException::class)\n    fun fetchFormDetails(\n        formsRepository: FormsRepository,\n        formSource: FormSource\n    ): List<ServerFormDetails> {\n        val formList = formSource.fetchFormList()\n        return formList.map { listItem ->\n            val manifestFile = listItem.manifestURL?.let {\n                getManifestFile(formSource, it)\n            }\n\n            val forms = formsRepository.getAllNotDeletedByFormId(listItem.formID)\n            val thisFormAlreadyDownloaded = forms.isNotEmpty()\n\n            val formHash = listItem.hash\n            val existingForm = if (formHash != null) {\n                formsRepository.getOneByMd5Hash(formHash)\n            } else {\n                null\n            }\n\n            val areNewerMediaFilesAvailable = if (existingForm != null && manifestFile != null) {\n                areNewerMediaFilesAvailable(existingForm, manifestFile.mediaFiles)\n            } else {\n                false\n            }\n\n            val type = if (existingForm != null) {\n                if (existingForm.isDeleted) {\n                    ServerFormDetails.Type.New\n                } else if (areNewerMediaFilesAvailable) {\n                    ServerFormDetails.Type.UpdatedMedia\n                } else {\n                    ServerFormDetails.Type.OnDevice\n                }\n            } else if (thisFormAlreadyDownloaded) {\n                if (listItem.hash == null) {\n                    ServerFormDetails.Type.OnDevice\n                } else if (forms.any { it.version == listItem.version }) {\n                    ServerFormDetails.Type.UpdatedHash\n                } else {\n                    ServerFormDetails.Type.UpdatedVersion\n                }\n            } else {\n                ServerFormDetails.Type.New\n            }\n\n            ServerFormDetails(\n                listItem.name,\n                listItem.downloadURL,\n                listItem.formID,\n                listItem.version,\n                listItem.hash,\n                manifestFile,\n                type\n            )\n        }\n    }\n\n    fun downloadForms(\n        forms: List<ServerFormDetails>,\n        formDownloader: FormDownloader,\n        progressReporter: ((Int, Int) -> Unit)? = null,\n        isCancelled: (() -> Boolean)? = null,\n    ): Map<ServerFormDetails, FormDownloadException?> {\n        val results = mutableMapOf<ServerFormDetails, FormDownloadException?>()\n        for (index in forms.indices) {\n            val form = forms[index]\n\n            try {\n                formDownloader.downloadForm(\n                    form,\n                    object : FormDownloader.ProgressReporter {\n                        override fun onDownloadingMediaFile(count: Int) {\n                            progressReporter?.invoke(index, count)\n                        }\n                    },\n                    { isCancelled?.invoke() ?: false }\n                )\n\n                results[form] = null\n            } catch (e: FormDownloadException.DownloadingInterrupted) {\n                break\n            } catch (e: FormDownloadException) {\n                results[form] = e\n            }\n        }\n\n        return results\n    }\n\n    @JvmStatic\n    fun copySavedFileFromPreviousFormVersionIfExists(\n        formsRepository: FormsRepository,\n        formId: String,\n        mediaDirPath: String\n    ) {\n        val lastSavedFile: File? = formsRepository\n            .getAllByFormId(formId)\n            .maxByOrNull { form -> form.date }\n            ?.let {\n                File(it.formMediaPath, FileUtils.LAST_SAVED_FILENAME)\n            }\n\n        if (lastSavedFile != null && lastSavedFile.exists()) {\n            File(mediaDirPath).mkdir()\n            FileUtils.copyFile(lastSavedFile, File(mediaDirPath, FileUtils.LAST_SAVED_FILENAME))\n        }\n    }\n\n    @JvmStatic\n    @Throws(IOException::class, FormSourceException::class, InterruptedException::class)\n    fun downloadMediaFiles(\n        formToDownload: ServerFormDetails,\n        formSource: FormSource,\n        formsRepository: FormsRepository,\n        tempMediaPath: String,\n        tempDir: File,\n        entitiesRepository: EntitiesRepository,\n        entitySource: EntitySource,\n        stateListener: OngoingWorkListener\n    ): MediaFilesDownloadResult {\n        var newAttachmentsDownloaded = false\n        var entitiesDownloaded = false\n\n        val tempMediaDir = File(tempMediaPath).also { it.mkdir() }\n\n        val existingForm = formsRepository.getAllByFormIdAndVersion(formToDownload.formId, formToDownload.formVersion).firstOrNull()\n        val allFormVersionsSorted = formsRepository.getAllByFormId(formToDownload.formId).sortedByDescending { it.date }\n        val currentOrLastFormVersion = existingForm ?: allFormVersionsSorted.firstOrNull()\n\n        formToDownload.manifest!!.mediaFiles.forEachIndexed { i, mediaFile ->\n            stateListener.progressUpdate(i + 1)\n\n            val tempMediaFile = File(tempMediaDir, mediaFile.filename)\n\n            val isEntityList = mediaFile.type != null\n            if (isEntityList) {\n                val entityListName = getEntityListFromFileName(mediaFile)\n                val localEntityList = entitiesRepository.getList(entityListName)\n\n                entitiesDownloaded = true\n\n                if (localEntityList == null || mediaFile.hash != localEntityList.hash) {\n                    newAttachmentsDownloaded = true\n                    downloadMediaFile(formSource, mediaFile, tempMediaFile, tempDir, stateListener)\n\n                    LocalEntityUseCases.updateLocalEntitiesFromServer(\n                        entityListName,\n                        tempMediaFile,\n                        entitiesRepository,\n                        mediaFile\n                    )\n                } else {\n                    val existingForm = formsRepository.getAllByFormIdAndVersion(\n                        formToDownload.formId,\n                        formToDownload.formVersion\n                    ).getOrNull(0)\n\n                    if (existingForm != null) {\n                        val entityListLastUpdated = localEntityList.lastUpdated\n                        if (entityListLastUpdated != null && entityListLastUpdated > existingForm.getLastUpdated()) {\n                            newAttachmentsDownloaded = true\n                        }\n                    }\n                }\n\n                /**\n                 * Ensures local offline Entities are cleaned up when they have been deleted on the server.\n                 *\n                 * Normally this cleanup is triggered during sync as part of a full update with the server\n                 * whenever the Entity list hash from Central changes.\n                 * However, there is a case where the hash stays the same:\n                 *  - a sync happens and the current hash is stored,\n                 *  - an Entity is created locally and a form is uploaded, creating the Entity on the server,\n                 *  - the Entity is then deleted on the server,\n                 *  - another sync occurs, but the hash is the same as the stored one because the list\n                 *    contents are identical to before the local Entity was added.\n                 *\n                 * In this case, the usual hash-based update will not detect the deletion. Collect must\n                 * use the integrityUrl to check for missing Entities and remove them locally.\n                 */\n                LocalEntityUseCases.cleanUpDeletedOfflineEntities(\n                    entityListName,\n                    entitiesRepository,\n                    entitySource,\n                    mediaFile\n                )\n            } else {\n                val existingFile = searchForExistingMediaFile(currentOrLastFormVersion, mediaFile)\n                if (existingFile != null) {\n                    val existingFileHash = existingFile.getMd5Hash()\n\n                    if (existingFileHash.contentEquals(mediaFile.hash)) {\n                        if (formToDownload.type != ServerFormDetails.Type.UpdatedMedia) {\n                            copyFileToDirectory(existingFile, tempMediaDir)\n                        }\n                    } else {\n                        downloadMediaFile(\n                            formSource,\n                            mediaFile,\n                            tempMediaFile,\n                            tempDir,\n                            stateListener\n                        )\n\n                        if (!tempMediaFile.getMd5Hash().contentEquals(existingFileHash)) {\n                            newAttachmentsDownloaded = true\n                        }\n                    }\n                } else {\n                    downloadMediaFile(\n                        formSource,\n                        mediaFile,\n                        tempMediaFile,\n                        tempDir,\n                        stateListener\n                    )\n                    newAttachmentsDownloaded = true\n                }\n\n                logEntityListClashes(mediaFile, entitiesRepository)\n            }\n        }\n\n        return MediaFilesDownloadResult(newAttachmentsDownloaded, entitiesDownloaded)\n    }\n\n    private fun downloadMediaFile(\n        formSource: FormSource,\n        mediaFile: MediaFile,\n        tempMediaFile: File,\n        tempDir: File,\n        stateListener: OngoingWorkListener\n    ) {\n        val file = formSource.fetchMediaFile(mediaFile.downloadUrl)\n        FileUtils.interuptablyWriteFile(file, tempMediaFile, tempDir, stateListener)\n    }\n\n    /**\n     * Track CSVs that have names that clash with entity lists in the project. If\n     * these CSVs are being used as part of a `select_one_from_file` question, the\n     * instance ID will be the file name with the extension removed.\n     */\n    private fun logEntityListClashes(\n        mediaFile: MediaFile,\n        entitiesRepository: EntitiesRepository\n    ) {\n        val isCsv = mediaFile.filename.endsWith(\".csv\")\n        val mostLikelyInstanceId = getEntityListFromFileName(mediaFile)\n        if (isCsv && entitiesRepository.getList(mostLikelyInstanceId) != null) {\n            Analytics.setUserProperty(\"HasEntityListCollision\", \"true\")\n        }\n    }\n\n    private fun getEntityListFromFileName(mediaFile: MediaFile) =\n        mediaFile.filename.substringBefore(\".csv\")\n\n    private fun searchForExistingMediaFile(\n        currentOrLastFormVersion: Form?,\n        mediaFile: MediaFile\n    ): File? {\n        return if (currentOrLastFormVersion != null) {\n            val candidate = File(currentOrLastFormVersion.formMediaPath, mediaFile.filename)\n            if (candidate.exists()) {\n                candidate\n            } else {\n                null\n            }\n        } else {\n            null\n        }\n    }\n\n    private fun getManifestFile(formSource: FormSource, manifestUrl: String): ManifestFile? {\n        return try {\n            formSource.fetchManifest(manifestUrl)\n        } catch (formSourceException: FormSourceException) {\n            Timber.w(formSourceException)\n            null\n        }\n    }\n\n    private fun areNewerMediaFilesAvailable(\n        existingForm: Form,\n        newMediaFiles: List<MediaFile>\n    ): Boolean {\n        if (newMediaFiles.isEmpty()) {\n            return false\n        }\n\n        val localMediaHashes = FormUtils.getMediaFiles(existingForm)\n            .map { it.getMd5Hash() }\n            .toSet()\n\n        return newMediaFiles.any {\n            !it.filename.endsWith(\".zip\") && it.hash !in localMediaHashes\n        }\n    }\n}\n\nclass EntityListUpdateException(cause: Throwable) : Exception(cause)\n\ndata class MediaFilesDownloadResult(\n    val newAttachmentsDownloaded: Boolean,\n    val entitiesDownloaded: Boolean\n)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/download/FormDownloadException.kt",
    "content": "package org.odk.collect.android.formmanagement.download\n\nimport org.odk.collect.forms.FormSourceException\n\nsealed class FormDownloadException : Exception() {\n    class DownloadingInterrupted : FormDownloadException()\n    class FormWithNoHash : FormDownloadException()\n    class FormParsingError(val original: Exception) : FormDownloadException()\n    class DiskError : FormDownloadException()\n    class InvalidSubmission : FormDownloadException()\n    class FormSourceError(val exception: FormSourceException) : FormDownloadException()\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/download/FormDownloadExceptionMapper.kt",
    "content": "package org.odk.collect.android.formmanagement.download\n\nimport android.content.Context\nimport org.odk.collect.android.formmanagement.FormSourceExceptionMapper\nimport org.odk.collect.strings.localization.getLocalizedString\n\nclass FormDownloadExceptionMapper(private val context: Context) {\n    fun getMessage(exception: FormDownloadException?): String {\n        return when (exception) {\n            is FormDownloadException.FormWithNoHash -> {\n                context.getLocalizedString(\n                    org.odk.collect.strings.R.string.form_with_no_hash_error\n                ) + \" \" + context.getLocalizedString(\n                    org.odk.collect.strings.R.string.report_to_project_lead\n                )\n            }\n            is FormDownloadException.FormParsingError -> {\n                context.getLocalizedString(org.odk.collect.strings.R.string.form_parsing_error) +\n                    \"\\n\\n${exception.original.getStackTraceString(1)}\\n\\n\" +\n                    context.getLocalizedString(org.odk.collect.strings.R.string.report_to_project_lead)\n            }\n            is FormDownloadException.DiskError -> {\n                context.getLocalizedString(\n                    org.odk.collect.strings.R.string.form_save_disk_error\n                ) + \" \" + context.getLocalizedString(\n                    org.odk.collect.strings.R.string.report_to_project_lead\n                )\n            }\n            is FormDownloadException.InvalidSubmission -> {\n                context.getLocalizedString(\n                    org.odk.collect.strings.R.string.form_with_invalid_submission_error\n                ) + \" \" + context.getLocalizedString(\n                    org.odk.collect.strings.R.string.report_to_project_lead\n                )\n            }\n            is FormDownloadException.FormSourceError -> {\n                FormSourceExceptionMapper(context).getMessage(exception.exception)\n            }\n            else -> {\n                context.getLocalizedString(org.odk.collect.strings.R.string.report_to_project_lead)\n            }\n        }\n    }\n\n    private fun Exception.getStackTraceString(lines: Int): String {\n        val stackTrace = this.stackTrace\n        return \"${this.message}\\n${stackTrace.take(lines).joinToString(\"\\n\") { it.toString() }}\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/download/FormDownloader.kt",
    "content": "package org.odk.collect.android.formmanagement.download\n\nimport org.odk.collect.android.formmanagement.ServerFormDetails\nimport java.util.function.Supplier\n\ninterface FormDownloader {\n\n    @Throws(FormDownloadException::class)\n    fun downloadForm(\n        form: ServerFormDetails?,\n        progressReporter: ProgressReporter?,\n        isCancelled: Supplier<Boolean?>?\n    )\n\n    interface ProgressReporter {\n        fun onDownloadingMediaFile(count: Int)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/download/ServerFormDownloader.java",
    "content": "package org.odk.collect.android.formmanagement.download;\n\nimport static org.odk.collect.android.utilities.FileUtils.interuptablyWriteFile;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.formmanagement.MediaFilesDownloadResult;\nimport org.odk.collect.android.formmanagement.metadata.FormMetadata;\nimport org.odk.collect.android.formmanagement.metadata.FormMetadataParser;\nimport org.odk.collect.android.formmanagement.ServerFormDetails;\nimport org.odk.collect.android.formmanagement.ServerFormUseCases;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.FormNameUtils;\nimport org.odk.collect.androidshared.utils.Validator;\nimport org.odk.collect.async.OngoingWorkListener;\nimport org.odk.collect.entities.server.EntitySource;\nimport org.odk.collect.entities.storage.EntitiesRepository;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.FormSource;\nimport org.odk.collect.forms.FormSourceException;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.shared.files.FileExt;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.function.Supplier;\n\nimport javax.annotation.Nullable;\n\nimport timber.log.Timber;\n\npublic class ServerFormDownloader implements FormDownloader {\n\n    private final FormsRepository formsRepository;\n    private final FormSource formSource;\n    private final File cacheDir;\n    private final String formsDirPath;\n    private final FormMetadataParser formMetadataParser;\n    private final Supplier<Long> clock;\n    private final EntitiesRepository entitiesRepository;\n    private final EntitySource entitySource;\n\n    public ServerFormDownloader(FormSource formSource, FormsRepository formsRepository, File cacheDir, String formsDirPath, FormMetadataParser formMetadataParser, Supplier<Long> clock, EntitiesRepository entitiesRepository, EntitySource entitySource) {\n        this.formSource = formSource;\n        this.cacheDir = cacheDir;\n        this.formsDirPath = formsDirPath;\n        this.formsRepository = formsRepository;\n        this.formMetadataParser = formMetadataParser;\n        this.clock = clock;\n        this.entitiesRepository = entitiesRepository;\n        this.entitySource = entitySource;\n    }\n\n    @Override\n    public void downloadForm(ServerFormDetails form, @Nullable ProgressReporter progressReporter, @Nullable Supplier<Boolean> isCancelled) throws FormDownloadException {\n        Form formOnDevice;\n        List<Form> preExistingFormsWithSameIdAndVersion = new ArrayList<>();\n        try {\n            formOnDevice = formsRepository.getOneByMd5Hash(validateHash(form.getHash()));\n        } catch (IllegalArgumentException e) {\n            throw new FormDownloadException.FormWithNoHash();\n        }\n\n        if (formOnDevice != null) {\n            if (formOnDevice.isDeleted()) {\n                formsRepository.restore(formOnDevice.getDbId());\n            }\n        } else {\n            preExistingFormsWithSameIdAndVersion = formsRepository.getAllByFormIdAndVersion(form.getFormId(), form.getFormVersion());\n        }\n\n        File tempDir = new File(cacheDir, \"download-\" + UUID.randomUUID().toString());\n        tempDir.mkdirs();\n\n        try {\n            OngoingWorkListener stateListener = new ProgressReporterAndSupplierStateListener(progressReporter, isCancelled);\n            processOneForm(form, stateListener, tempDir, formsDirPath, formMetadataParser);\n        } catch (FormSourceException e) {\n            throw new FormDownloadException.FormSourceError(e);\n        } finally {\n            FileExt.deleteDirectory(tempDir);\n            for (Form formToDelete : preExistingFormsWithSameIdAndVersion) {\n                formsRepository.delete(formToDelete.getDbId());\n            }\n        }\n    }\n\n    private void processOneForm(ServerFormDetails fd, OngoingWorkListener stateListener, File tempDir, String formsDirPath, FormMetadataParser formMetadataParser) throws FormDownloadException, FormSourceException {\n        // use a temporary media path until everything is ok.\n        String tempMediaPath = new File(tempDir, \"media\").getAbsolutePath();\n        FileResult fileResult = null;\n        MediaFilesDownloadResult mediaFilesDownloadResult;\n\n        try {\n            // get the xml file\n            // if we've downloaded a duplicate, this gives us the file\n            fileResult = downloadXform(fd.getFormName(), fd.getDownloadUrl(), stateListener, tempDir, formsDirPath);\n\n            // download media files if there are any\n            if (fd.getManifest() != null && !fd.getManifest().getMediaFiles().isEmpty()) {\n                mediaFilesDownloadResult = ServerFormUseCases.downloadMediaFiles(fd, formSource, formsRepository, tempMediaPath, tempDir, entitiesRepository, entitySource, stateListener);\n            } else {\n                mediaFilesDownloadResult = new MediaFilesDownloadResult(false, false);\n            }\n\n            ServerFormUseCases.copySavedFileFromPreviousFormVersionIfExists(formsRepository, fd.getFormId(), tempMediaPath);\n        } catch (FormDownloadException.DownloadingInterrupted | InterruptedException e) {\n            Timber.i(e);\n            cleanUp(fileResult, tempMediaPath);\n            throw new FormDownloadException.DownloadingInterrupted();\n        } catch (IOException e) {\n            throw new FormDownloadException.DiskError();\n        }\n\n        if (stateListener != null && stateListener.isCancelled()) {\n            cleanUp(fileResult, tempMediaPath);\n            throw new FormDownloadException.DownloadingInterrupted();\n        }\n\n        FormMetadata formMetadata = null;\n        if (fileResult.isNew) {\n            try {\n                final long start = System.currentTimeMillis();\n                Timber.i(\"Parsing document %s\", fileResult.file.getAbsolutePath());\n\n                formMetadata = formMetadataParser.readMetadata(fileResult.file);\n\n                Timber.i(\"Parse finished in %.3f seconds.\", (System.currentTimeMillis() - start) / 1000F);\n            } catch (RuntimeException e) {\n                throw new FormDownloadException.FormParsingError(e);\n            }\n        }\n\n        if (stateListener != null && stateListener.isCancelled()) {\n            throw new FormDownloadException.DownloadingInterrupted();\n        }\n\n        if (fileResult.isNew && !isSubmissionOk(formMetadata)) {\n            throw new FormDownloadException.InvalidSubmission();\n        }\n\n        try {\n            installEverything(tempMediaPath, fileResult, formMetadata, formsDirPath, mediaFilesDownloadResult);\n        } catch (FormDownloadException.DiskError e) {\n            cleanUp(fileResult, tempMediaPath);\n            throw e;\n        }\n    }\n\n    private boolean isSubmissionOk(FormMetadata formMetadata) {\n        String submission = formMetadata.getSubmissionUri();\n        return submission == null || Validator.isUrlValid(submission);\n    }\n\n    private void installEverything(String tempMediaPath, FileResult fileResult, FormMetadata formMetadata, String formsDirPath, MediaFilesDownloadResult mediaFilesDownloadResult) throws FormDownloadException.DiskError {\n        FormResult formResult;\n\n        File formFile;\n\n        if (fileResult.isNew()) {\n            // Copy form to forms dir\n            formFile = new File(formsDirPath, fileResult.file.getName());\n            FileUtils.copyFile(fileResult.file, formFile);\n        } else {\n            formFile = fileResult.file;\n\n            if (mediaFilesDownloadResult.getNewAttachmentsDownloaded()) {\n                Form existingForm = formsRepository.getOneByPath(formFile.getAbsolutePath());\n                if (existingForm != null) {\n                    formsRepository.save(new Form.Builder(existingForm)\n                            .lastDetectedAttachmentsUpdateDate(clock.get())\n                            .build()\n                    );\n                }\n            }\n\n            if (mediaFilesDownloadResult.getEntitiesDownloaded()) {\n                Form existingForm = formsRepository.getOneByPath(formFile.getAbsolutePath());\n                if (existingForm != null) {\n                    formsRepository.save(new Form.Builder(existingForm)\n                            .usesEntities(true)\n                            .build()\n                    );\n                }\n            }\n        }\n\n        // Save form in database\n        formResult = findOrCreateForm(formFile, formMetadata, mediaFilesDownloadResult);\n\n        // move the media files in the media folder\n        if (tempMediaPath != null) {\n            File formMediaDir = new File(formResult.form.getFormMediaPath());\n\n            try {\n                moveMediaFiles(tempMediaPath, formMediaDir);\n            } catch (IOException e) {\n                Timber.e(e);\n\n                if (formResult.isNew() && fileResult.isNew()) {\n                    // this means we should delete the entire form together with the metadata\n                    formsRepository.delete(formResult.form.getDbId());\n                }\n\n                throw new FormDownloadException.DiskError();\n            }\n        }\n    }\n\n    private void cleanUp(FileResult fileResult, String tempMediaPath) {\n        if (fileResult == null) {\n            Timber.d(\"The user cancelled (or an exception happened) the download of a form at the very beginning.\");\n        } else {\n            String md5Hash = Md5.getMd5Hash(fileResult.file);\n            if (md5Hash != null) {\n                formsRepository.deleteByMd5Hash(md5Hash);\n            }\n            FileUtils.deleteAndReport(fileResult.getFile());\n        }\n\n        if (tempMediaPath != null) {\n            FileUtils.purgeMediaPath(tempMediaPath);\n        }\n    }\n\n    private FormResult findOrCreateForm(File formFile, FormMetadata formMetadata, MediaFilesDownloadResult mediaFilesDownloadResult) {\n        final String formFilePath = formFile.getAbsolutePath();\n        String mediaPath = FileUtils.constructMediaPath(formFilePath);\n\n        Form existingForm = formsRepository.getOneByPath(formFile.getAbsolutePath());\n\n        if (existingForm == null) {\n            Form newForm = saveNewForm(formMetadata, formFile, mediaPath, mediaFilesDownloadResult.getEntitiesDownloaded());\n            return new FormResult(newForm, true);\n        } else {\n            return new FormResult(existingForm, false);\n        }\n    }\n\n    private Form saveNewForm(FormMetadata formMetadata, File formFile, String mediaPath, boolean entityAttachmentsDetected) {\n        Form form = new Form.Builder()\n                .formFilePath(formFile.getAbsolutePath())\n                .formMediaPath(mediaPath)\n                .displayName(formMetadata.getTitle())\n                .version(formMetadata.getVersion())\n                .formId(formMetadata.getId())\n                .submissionUri(formMetadata.getSubmissionUri())\n                .base64RSAPublicKey(formMetadata.getBase64RsaPublicKey())\n                .autoDelete(formMetadata.getAutoDelete())\n                .autoSend(formMetadata.getAutoSend())\n                .geometryXpath(formMetadata.getGeometryXPath())\n                .usesEntities(formMetadata.isEntityForm() || entityAttachmentsDetected)\n                .build();\n\n        return formsRepository.save(form);\n    }\n\n    /**\n     * Takes the formName and the URL and attempts to download the specified file. Returns a file\n     * object representing the downloaded file.\n     */\n    private FileResult downloadXform(String formName, String url, OngoingWorkListener stateListener, File tempDir, String formsDirPath) throws FormSourceException, IOException, FormDownloadException.DownloadingInterrupted, InterruptedException {\n        InputStream xform = formSource.fetchForm(url);\n\n        String fileName = getFormFileName(formName, formsDirPath);\n        File tempFormFile = new File(tempDir + File.separator + fileName);\n        interuptablyWriteFile(xform, tempFormFile, tempDir, stateListener);\n\n        // we've downloaded the file, and we may have renamed it\n        // make sure it's not the same as a file we already have\n        Form form = formsRepository.getOneByMd5Hash(Md5.getMd5Hash(tempFormFile));\n        if (form != null) {\n            // delete the file we just downloaded, because it's a duplicate\n            FileUtils.deleteAndReport(tempFormFile);\n\n            // set the file returned to the file we already had\n            return new FileResult(new File(form.getFormFilePath()), false);\n        } else {\n            return new FileResult(tempFormFile, true);\n        }\n    }\n\n    @NotNull\n    private static String getFormFileName(String formName, String formsDirPath) {\n        String formattedFormName = FormNameUtils.formatFilenameFromFormName(formName);\n        String fileName = formattedFormName + \".xml\";\n        int i = 2;\n        while (new File(formsDirPath + File.separator + fileName).exists()) {\n            fileName = formattedFormName + \"_\" + i + \".xml\";\n            i++;\n        }\n        return fileName;\n    }\n\n    private static String validateHash(String hash) {\n        return hash == null || hash.isEmpty() ? null : hash;\n    }\n\n    private static void moveMediaFiles(String tempMediaPath, File formMediaPath) throws IOException {\n        File tempMediaFolder = new File(tempMediaPath);\n        File[] mediaFiles = tempMediaFolder.listFiles();\n\n        if (mediaFiles != null && mediaFiles.length != 0) {\n            for (File mediaFile : mediaFiles) {\n                try {\n                    org.apache.commons.io.FileUtils.copyFileToDirectory(mediaFile, formMediaPath);\n                } catch (IllegalArgumentException e) {\n                    // This can happen if copyFileToDirectory is pointed at a file instead of a dir\n                    throw new IOException(e);\n                }\n\n            }\n        }\n    }\n\n    private static class FormResult {\n\n        private final Form form;\n        private final boolean isNew;\n\n        private FormResult(Form form, boolean isNew) {\n            this.form = form;\n            this.isNew = isNew;\n        }\n\n        private boolean isNew() {\n            return isNew;\n        }\n\n        public Form getForm() {\n            return form;\n        }\n    }\n\n    private static class FileResult {\n\n        private final File file;\n        private final boolean isNew;\n\n        FileResult(File file, boolean isNew) {\n            this.file = file;\n            this.isNew = isNew;\n        }\n\n        private File getFile() {\n            return file;\n        }\n\n        private boolean isNew() {\n            return isNew;\n        }\n    }\n\n    private static class ProgressReporterAndSupplierStateListener implements OngoingWorkListener {\n        private final ProgressReporter progressReporter;\n        private final Supplier<Boolean> isCancelled;\n\n        ProgressReporterAndSupplierStateListener(ProgressReporter progressReporter, Supplier<Boolean> isCancelled) {\n            this.progressReporter = progressReporter;\n            this.isCancelled = isCancelled;\n        }\n\n        @Override\n        public void progressUpdate(int progress) {\n            if (progressReporter != null) {\n                progressReporter.onDownloadingMediaFile(progress);\n            }\n        }\n\n        @Override\n        public boolean isCancelled() {\n            if (isCancelled != null) {\n                return isCancelled.get();\n            } else {\n                return false;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt",
    "content": "package org.odk.collect.android.formmanagement.drafts\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.asLiveData\nimport org.odk.collect.android.instancemanagement.FinalizeAllResult\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\n\nclass BulkFinalizationViewModel(\n    private val projectId: String,\n    private val scheduler: Scheduler,\n    private val instancesDataService: InstancesDataService,\n    settingsProvider: SettingsProvider\n) {\n    private val _finalizedForms = MutableLiveData<Consumable<FinalizeAllResult>>()\n    val finalizedForms: LiveData<Consumable<FinalizeAllResult>> = _finalizedForms\n\n    private val _isFinalizing = MutableNonNullLiveData(false)\n    val isFinalizing: NonNullLiveData<Boolean> = _isFinalizing\n\n    val draftsCount = instancesDataService.getEditableCount(projectId).asLiveData()\n    val isEnabled =\n        settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_BULK_FINALIZE)\n\n    fun finalizeAllDrafts() {\n        _isFinalizing.value = true\n\n        scheduler.immediate(\n            background = {\n                instancesDataService.finalizeAllDrafts(projectId)\n            },\n            foreground = {\n                _isFinalizing.value = false\n                _finalizedForms.value = Consumable(it)\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt",
    "content": "package org.odk.collect.android.formmanagement.drafts\n\nimport android.content.Context\nimport android.view.Menu\nimport android.view.MenuInflater\nimport android.view.MenuItem\nimport androidx.core.view.MenuProvider\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.android.R\nimport org.odk.collect.androidshared.ui.enableIconsVisibility\nimport org.odk.collect.strings.R.plurals\nimport org.odk.collect.strings.R.string\n\nclass DraftsMenuProvider(\n    private val context: Context,\n    private val onFinalizeAll: Runnable\n) : MenuProvider {\n\n    var draftsCount: Int? = null\n\n    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {\n        menuInflater.inflate(R.menu.drafts, menu)\n        menu.enableIconsVisibility()\n    }\n\n    override fun onPrepareMenu(menu: Menu) {\n        if (draftsCount == null || draftsCount == 0) {\n            menu.findItem(R.id.bulk_finalize).isVisible = false\n        }\n    }\n\n    override fun onMenuItemSelected(menuItem: MenuItem): Boolean {\n        if (menuItem.itemId == R.id.bulk_finalize) {\n            draftsCount?.also {\n                val dialogTitle = context.resources.getQuantityString(\n                    plurals.bulk_finalize_confirmation,\n                    it,\n                    it\n                )\n\n                MaterialAlertDialogBuilder(context)\n                    .setTitle(dialogTitle)\n                    .setMessage(string.bulk_finalize_explanation)\n                    .setPositiveButton(string.finalize) { _, _ ->\n                        onFinalizeAll.run()\n                    }\n                    .setNegativeButton(string.cancel, null)\n                    .show()\n            }\n\n            return true\n        }\n\n        return false\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/finalization/EditedFormFinalizationProcessor.kt",
    "content": "package org.odk.collect.android.formmanagement.finalization\n\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.core.model.instance.TreeElement\nimport org.javarosa.core.util.PropertyUtils\nimport org.javarosa.form.api.FormEntryFinalizationProcessor\nimport org.javarosa.form.api.FormEntryModel\nimport org.odk.collect.android.instancemanagement.isEdit\nimport org.odk.collect.forms.instances.Instance\n\nclass EditedFormFinalizationProcessor(\n    private val instance: Instance?\n) : FormEntryFinalizationProcessor {\n    override fun processForm(formEntryModel: FormEntryModel) {\n        if (instance?.isEdit() == true) {\n            addDeprecatedId(formEntryModel.form)\n        }\n    }\n\n    private fun addDeprecatedId(formDef: FormDef) {\n        val mainInstance = formDef.mainInstance\n        val metaSection = mainInstance.root.getFirstChild(\"meta\")!!\n        val instanceId = metaSection.getFirstChild(\"instanceID\")!!\n        val deprecatedId = TreeElement(\"deprecatedID\")\n\n        metaSection.addChild(deprecatedId)\n        deprecatedId.setAnswer(instanceId.value)\n        instanceId.setAnswer(StringData(\"uuid:\" + PropertyUtils.genUUID()))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/formmap/FormMapViewModel.kt",
    "content": "package org.odk.collect.android.formmanagement.formmap\n\nimport android.content.res.Resources\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport org.json.JSONException\nimport org.json.JSONObject\nimport org.odk.collect.android.R\nimport org.odk.collect.android.instancemanagement.getStatusDescription\nimport org.odk.collect.android.instancemanagement.showAsEditable\nimport org.odk.collect.android.instancemanagement.userVisibleInstanceName\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.geo.selection.IconifiedText\nimport org.odk.collect.geo.selection.MappableSelectItem\nimport org.odk.collect.geo.selection.SelectionMapData\nimport org.odk.collect.geo.selection.Status\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport timber.log.Timber\nimport java.text.SimpleDateFormat\nimport java.util.Locale\n\nclass FormMapViewModel(\n    private val resources: Resources,\n    private val formId: Long,\n    private val formsRepository: FormsRepository,\n    private val instancesRepository: InstancesRepository,\n    private val settingsProvider: SettingsProvider,\n    private val scheduler: Scheduler\n) : ViewModel(), SelectionMapData {\n\n    private var _form: Form? = null\n\n    private val mapTitle = MutableLiveData<String?>()\n    private var mappableItems = MutableLiveData<List<MappableSelectItem>>(null)\n    private var itemCount = MutableNonNullLiveData(0)\n    private val isLoading = MutableNonNullLiveData(false)\n\n    override fun getMapTitle(): LiveData<String?> {\n        return mapTitle\n    }\n\n    override fun getItemType(): String {\n        return resources.getString(org.odk.collect.strings.R.string.saved_forms)\n    }\n\n    override fun getItemCount(): NonNullLiveData<Int> {\n        return itemCount\n    }\n\n    override fun getMappableItems(): LiveData<List<MappableSelectItem>?> {\n        return mappableItems\n    }\n\n    override fun isLoading(): NonNullLiveData<Boolean> {\n        return isLoading\n    }\n\n    fun load() {\n        isLoading.value = true\n\n        scheduler.immediate(\n            background = {\n                val form = _form ?: formsRepository.get(formId)!!.also { _form = it }\n                val instances = instancesRepository.getAllByFormId(form.formId)\n                val items = mutableListOf<MappableSelectItem>()\n\n                for (instance in instances) {\n                    if (instance.geometry != null && instance.geometryType == Instance.GEOMETRY_TYPE_POINT) {\n                        try {\n                            val geometry = JSONObject(instance.geometry)\n                            val coordinates = geometry.getJSONArray(\"coordinates\")\n\n                            // In GeoJSON, longitude comes before latitude.\n                            val lon = coordinates.getDouble(0)\n                            val lat = coordinates.getDouble(1)\n\n                            items.add(createItem(instance, lat, lon))\n                        } catch (e: JSONException) {\n                            Timber.w(\"Invalid JSON in instances table: %s\", instance.geometry)\n                        }\n                    }\n                }\n\n                Triple(form.displayName, items, instances.size)\n            },\n            foreground = {\n                mapTitle.value = it.first\n                mappableItems.value = it.second as List<MappableSelectItem>\n                itemCount.value = it.third\n                isLoading.value = false\n            }\n        )\n    }\n\n    private fun createItem(\n        instance: Instance,\n        latitude: Double,\n        longitude: Double\n    ): MappableSelectItem {\n        val instanceLastStatusChangeDate = instance.getStatusDescription(resources)\n\n        return if (instance.deletedDate != null) {\n            val deletedTime = resources.getString(org.odk.collect.strings.R.string.deleted_on_date_at_time)\n            val dateFormat = SimpleDateFormat(\n                deletedTime,\n                Locale.getDefault()\n            )\n\n            val info = \"$instanceLastStatusChangeDate\\n${dateFormat.format(instance.deletedDate)}\"\n            MappableSelectItem.MappableSelectPoint(\n                instance.dbId,\n                instance.userVisibleInstanceName(resources),\n                point = MapPoint(latitude, longitude),\n                smallIcon = getDrawableIdForStatus(instance.status, false),\n                largeIcon = getDrawableIdForStatus(instance.status, true),\n                info = info\n            )\n        } else if (!instance.canEditWhenComplete() && listOf(\n                Instance.STATUS_COMPLETE,\n                Instance.STATUS_SUBMISSION_FAILED,\n                Instance.STATUS_SUBMITTED\n            ).contains(instance.status)\n        ) {\n            val info = \"$instanceLastStatusChangeDate\\n${resources.getString(org.odk.collect.strings.R.string.cannot_edit_completed_form)}\"\n            MappableSelectItem.MappableSelectPoint(\n                instance.dbId,\n                instance.userVisibleInstanceName(resources),\n                point = MapPoint(latitude, longitude),\n                smallIcon = getDrawableIdForStatus(instance.status, false),\n                largeIcon = getDrawableIdForStatus(instance.status, true),\n                info = info\n            )\n        } else {\n            val action =\n                if (instance.showAsEditable(settingsProvider)) {\n                    createEditAction()\n                } else {\n                    createViewAction()\n                }\n\n            MappableSelectItem.MappableSelectPoint(\n                instance.dbId,\n                instance.userVisibleInstanceName(resources),\n                point = MapPoint(latitude, longitude),\n                smallIcon = getDrawableIdForStatus(instance.status, false),\n                largeIcon = getDrawableIdForStatus(instance.status, true),\n                info = instanceLastStatusChangeDate,\n                action = action,\n                status = instanceStatusToMappableSelectionItemStatus(instance)\n            )\n        }\n    }\n\n    private fun instanceStatusToMappableSelectionItemStatus(instance: Instance): Status? {\n        return when (instance.status) {\n            Instance.STATUS_INVALID, Instance.STATUS_INCOMPLETE -> Status.ERRORS\n            Instance.STATUS_VALID, Instance.STATUS_NEW_EDIT -> Status.NO_ERRORS\n            else -> null\n        }\n    }\n\n    private fun createViewAction(): IconifiedText {\n        return IconifiedText(\n            R.drawable.ic_visibility,\n            resources.getString(org.odk.collect.strings.R.string.view_data)\n        )\n    }\n\n    private fun createEditAction(): IconifiedText {\n        val canEditSaved = settingsProvider.getProtectedSettings()\n            .getBoolean(ProtectedProjectKeys.KEY_EDIT_SAVED)\n\n        return IconifiedText(\n            if (canEditSaved) org.odk.collect.icons.R.drawable.ic_edit else R.drawable.ic_visibility,\n            resources.getString(if (canEditSaved) org.odk.collect.strings.R.string.edit_data else org.odk.collect.strings.R.string.view_data)\n        )\n    }\n\n    private fun getDrawableIdForStatus(status: String, enlarged: Boolean): Int {\n        return when (status) {\n            Instance.STATUS_INCOMPLETE, Instance.STATUS_VALID, Instance.STATUS_INVALID, Instance.STATUS_NEW_EDIT -> if (enlarged) R.drawable.ic_room_form_state_incomplete_48dp else R.drawable.ic_room_form_state_incomplete_24dp\n            Instance.STATUS_COMPLETE -> if (enlarged) R.drawable.ic_room_form_state_complete_48dp else R.drawable.ic_room_form_state_complete_24dp\n            Instance.STATUS_SUBMITTED -> if (enlarged) R.drawable.ic_room_form_state_submitted_48dp else R.drawable.ic_room_form_state_submitted_24dp\n            Instance.STATUS_SUBMISSION_FAILED -> if (enlarged) R.drawable.ic_room_form_state_submission_failed_48dp else R.drawable.ic_room_form_state_submission_failed_24dp\n            else -> org.odk.collect.icons.R.drawable.ic_map_point\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java",
    "content": "package org.odk.collect.android.formmanagement.matchexactly;\n\nimport org.odk.collect.android.formmanagement.LocalFormUseCases;\nimport org.odk.collect.android.formmanagement.ServerFormDetails;\nimport org.odk.collect.android.formmanagement.download.FormDownloadException;\nimport org.odk.collect.android.formmanagement.download.FormDownloader;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.FormSource;\nimport org.odk.collect.forms.FormSourceException;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.forms.instances.InstancesRepository;\n\nimport java.util.List;\n\npublic class ServerFormsSynchronizer {\n\n    private final FormsRepository formsRepository;\n    private final InstancesRepository instancesRepository;\n    private final FormDownloader formDownloader;\n    private final ServerFormsDetailsFetcher serverFormsDetailsFetcher;\n    private final FormSource formSource;\n\n    public ServerFormsSynchronizer(ServerFormsDetailsFetcher serverFormsDetailsFetcher, FormsRepository formsRepository, InstancesRepository instancesRepository, FormDownloader formDownloader, FormSource formSource) {\n        this.serverFormsDetailsFetcher = serverFormsDetailsFetcher;\n        this.formsRepository = formsRepository;\n        this.instancesRepository = instancesRepository;\n        this.formDownloader = formDownloader;\n        this.formSource = formSource;\n    }\n\n    public void synchronize() throws FormSourceException {\n        List<ServerFormDetails> formList = serverFormsDetailsFetcher.fetchFormDetails(formsRepository, formSource);\n        List<Form> formsOnDevice = formsRepository.getAll();\n        formsOnDevice.stream().forEach(form -> {\n            if (formList.stream().noneMatch(f -> form.getFormId().equals(f.getFormId()))) {\n                LocalFormUseCases.deleteForm(formsRepository, instancesRepository, form.getDbId());\n            }\n        });\n\n        boolean downloadException = false;\n\n        for (ServerFormDetails form : formList) {\n            if (form.getType() != ServerFormDetails.Type.OnDevice) {\n                try {\n                    formDownloader.downloadForm(form, null, null);\n                } catch (FormDownloadException.DownloadingInterrupted e) {\n                    return;\n                } catch (FormDownloadException e) {\n                    downloadException = true;\n                }\n            }\n        }\n\n        if (downloadException) {\n            throw new FormSourceException.FetchError();\n        }\n    }\n\n    public interface ServerFormsDetailsFetcher {\n        List<ServerFormDetails> fetchFormDetails(FormsRepository formsRepository, FormSource formSource) throws FormSourceException;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/metadata/FormMetadata.kt",
    "content": "package org.odk.collect.android.formmanagement.metadata\n\ndata class FormMetadata(\n    val title: String?,\n    val id: String?,\n    val version: String?,\n    val submissionUri: String?,\n    val base64RsaPublicKey: String?,\n    val autoDelete: String?,\n    val autoSend: String?,\n    val geometryXPath: String?,\n    val isEntityForm: Boolean\n)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/formmanagement/metadata/FormMetadataParser.kt",
    "content": "package org.odk.collect.android.formmanagement.metadata\n\nimport org.javarosa.xform.parse.XFormParser\nimport org.kxml2.kdom.Element\nimport java.io.File\nimport java.io.InputStream\n\nobject FormMetadataParser {\n    @JvmStatic\n    fun readMetadata(formFile: File): FormMetadata {\n        return readMetadata(formFile.inputStream())\n    }\n\n    @JvmStatic\n    fun readMetadata(formFile: InputStream): FormMetadata {\n        val doc = XFormParser.getXMLDocument(formFile.reader())\n        val head = doc.getRootElement().getElement(null, \"head\")\n        val model = head.getElement(null, \"model\")\n        val body = doc.getRootElement().getElement(null, \"body\")\n        val title = head.getElement(null, \"title\").getChild(0).toString()\n\n        val modelElements = getChildren(model)\n        val mainInstance = modelElements.first { it.name == \"instance\" }\n        val mainInstanceRoot = getChildren(mainInstance).first()\n        val submission = modelElements.firstOrNull { it.name == \"submission\" }\n\n        val id = mainInstanceRoot.getAttributeValue(null, \"id\")\n        val version = mainInstanceRoot.getAttributeValue(null, \"version\")\n        val submissionUri = submission?.getAttributeValue(null, \"action\")\n        val base64RsaPublicKey = submission?.getAttributeValue(null, \"base64RsaPublicKey\")\n        val autoDelete = submission?.getAttributeValue(null, \"auto-delete\")\n        val autoSend = submission?.getAttributeValue(null, \"auto-send\")\n        val geometryXPath = getFirstGeopointXPath(model, mainInstanceRoot, body)\n        val isEntityForm = model.getAttributeValue(null, \"entities-version\") != null\n\n        return FormMetadata(\n            title,\n            id,\n            if (version.isNullOrBlank()) null else version,\n            submissionUri,\n            base64RsaPublicKey,\n            autoDelete,\n            autoSend,\n            geometryXPath,\n            isEntityForm\n        )\n    }\n\n    private fun getChildren(element: Element): List<Element> {\n        return 0.until(element.childCount).fold(emptyList()) { list, i ->\n            val child = element.getElement(i)\n            if (child != null) {\n                list + listOf(child)\n            } else {\n                list\n            }\n        }\n    }\n\n    /**\n     * Finds the first geopoint reference in the primary instance by:\n     * 1. Retrieving all geopoint binds from the model.\n     * 2. Iterating through the elements of the primary instance root.\n     * 3. Returning the first reference found in the primary instance that matches one of\n     *    the geopoint binds and is not inside a repeat.\n     *\n     * This solution is not perfect because it assumes that the references in the model\n     * appear in the same order as in the body, which is not guaranteed by XForms.\n     * However, in practice, this is typically the case.\n     *\n     */\n    private fun getFirstGeopointXPath(model: Element, mainInstanceRoot: Element, body: Element): String? {\n        val geopointXPaths = getGeopointXPaths(model)\n        return if (geopointXPaths.isEmpty()) {\n            null\n        } else {\n            val repeatXPaths = getRepeatXPaths(body)\n            getFirstPrimaryInstanceGeopointXPath(geopointXPaths, repeatXPaths, mainInstanceRoot, null)\n        }\n    }\n\n    private fun getGeopointXPaths(model: Element): List<String> {\n        val geopointXPaths = mutableListOf<String>()\n        for (position in 0 until model.childCount) {\n            val child = model.getElement(position) ?: continue\n            if (child.name == \"bind\" && child.getAttributeValue(null, \"type\") == \"geopoint\") {\n                geopointXPaths.add(child.getAttributeValue(null, \"nodeset\"))\n            }\n        }\n        return geopointXPaths\n    }\n\n    private fun getRepeatXPaths(body: Element): List<String> {\n        val repeatXPaths = mutableListOf<String>()\n        for (position in 0 until body.childCount) {\n            val child = body.getElement(position) ?: continue\n            if (child.name == \"repeat\") {\n                repeatXPaths.add(child.getAttributeValue(null, \"nodeset\"))\n            } else if (child.childCount > 0) {\n                repeatXPaths.addAll(getRepeatXPaths(child))\n            }\n        }\n        return repeatXPaths\n    }\n\n    private fun getFirstPrimaryInstanceGeopointXPath(\n        geopointXPaths: List<String>,\n        repeatXPaths: List<String>,\n        parentRoot: Element,\n        parentXPath: String?\n    ): String? {\n        for (position in 0 until parentRoot.childCount) {\n            val child = parentRoot.getElement(position) ?: continue\n            val currentXPath = if (parentXPath == null) {\n                \"/${parentRoot.name}/${child.name}\"\n            } else {\n                \"$parentXPath/${child.name}\"\n            }\n            if (geopointXPaths.contains(currentXPath)) {\n                return currentXPath\n            } else if (child.childCount > 0 && !repeatXPaths.contains(currentXPath)) {\n                val nestedXPath = getFirstPrimaryInstanceGeopointXPath(geopointXPaths, repeatXPaths, child, currentXPath)\n                if (nestedXPath != null) {\n                    return nestedXPath\n                }\n            }\n        }\n        return null\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/BarCodeScannerFragment.kt",
    "content": "/* Copyright (C) 2017 Shobhit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.fragments\n\nimport android.content.Context\nimport android.content.pm.ActivityInfo\nimport android.content.pm.PackageManager\nimport android.content.res.Configuration\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.fragment.app.Fragment\nimport com.google.zxing.client.android.BeepManager\nimport org.odk.collect.android.databinding.FragmentScanBinding\nimport org.odk.collect.android.injection.DaggerUtils.getComponent\nimport org.odk.collect.android.utilities.Appearances\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.qrcode.BarcodeScannerView\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.qrcode.ScannerControls\nimport javax.inject.Inject\nimport kotlin.time.Duration.Companion.seconds\n\nabstract class BarCodeScannerFragment : Fragment() {\n\n    @Inject\n    lateinit var barcodeScannerViewFactory: BarcodeScannerViewContainer.Factory\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    private val beepManager: BeepManager by lazy { BeepManager(requireActivity()) }\n    private val fullScreenState = mutableStateOf(false)\n    private val fullScreenToggleExtendedState = mutableStateOf(false)\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        getComponent(context).inject(this)\n\n        scheduler.immediate(delay = 10.seconds.inWholeMilliseconds) {\n            fullScreenToggleExtendedState.value = true\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        val binding = FragmentScanBinding.inflate(inflater, container, false)\n\n        binding.barcodeView.setup(\n            barcodeScannerViewFactory,\n            requireActivity(),\n            getViewLifecycleOwner(),\n            isQrOnly(),\n            frontCameraUsed()\n        )\n\n        val flashlightOnState = mutableStateOf(false)\n        binding.barcodeView.barcodeScannerView.setTorchListener(object :\n            BarcodeScannerView.TorchListener {\n            override fun onTorchOn() {\n                flashlightOnState.value = true\n            }\n\n            override fun onTorchOff() {\n                flashlightOnState.value = false\n            }\n        })\n\n        val supportsFullScreen =\n            binding.barcodeView.barcodeScannerView.supportsFullScreenViewFinder()\n        binding.composeView.setContextThemedContent {\n            ScannerControls(\n                showFlashLight = hasFlash() && !frontCameraUsed(),\n                flashlightOn = flashlightOnState.value,\n                fullScreenViewFinder = supportsFullScreen && fullScreenState.value,\n                showFullScreenToggle = supportsFullScreen,\n                fullScreenToggleExtended = fullScreenToggleExtendedState.value,\n                onFullScreenToggled = {\n                    fullScreenState.value = !fullScreenState.value\n                    updateFullScreen(binding, fullScreenState.value)\n                },\n                onFlashlightToggled = {\n                    binding.barcodeView.barcodeScannerView.setTorchOn(!flashlightOnState.value)\n                }\n            )\n        }\n\n        binding.barcodeView.barcodeScannerView.latestBarcode\n            .observe(getViewLifecycleOwner()) { result: String ->\n                try {\n                    beepManager.playBeepSoundAndVibrate()\n                } catch (_: Exception) {\n                    // ignored\n                }\n\n                handleScanningResult(result)\n            }\n\n        binding.barcodeView.barcodeScannerView.start()\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        val binding = FragmentScanBinding.bind(view)\n        if (!binding.barcodeView.barcodeScannerView.supportsFullScreenViewFinder()) {\n            requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT\n        }\n\n        updateConfiguration(resources.configuration)\n    }\n\n    override fun onConfigurationChanged(newConfig: Configuration) {\n        super.onConfigurationChanged(newConfig)\n        updateConfiguration(newConfig)\n    }\n\n    private fun updateConfiguration(config: Configuration) {\n        val binding = FragmentScanBinding.bind(requireView())\n\n        val isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE\n        updateFullScreen(binding, isLandscape)\n    }\n\n    private fun updateFullScreen(binding: FragmentScanBinding, isFullScreen: Boolean) {\n        binding.barcodeView.barcodeScannerView.setFullScreenViewFinder(isFullScreen)\n        fullScreenState.value = isFullScreen\n    }\n\n    private fun hasFlash(): Boolean {\n        return requireActivity().applicationContext.packageManager\n            .hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)\n    }\n\n    private fun frontCameraUsed(): Boolean {\n        val bundle = requireActivity().intent.extras\n        return bundle != null && bundle.getBoolean(Appearances.FRONT)\n    }\n\n    protected fun restartScanning() {\n        val binding = FragmentScanBinding.bind(requireView())\n        scheduler.immediate(true, 2000L) {\n            binding.barcodeView.barcodeScannerView.start()\n        }\n    }\n\n    protected abstract fun isQrOnly(): Boolean\n\n    protected abstract fun handleScanningResult(result: String)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/BarcodeWidgetScannerFragment.kt",
    "content": "package org.odk.collect.android.fragments\n\nimport org.odk.collect.externalapp.ExternalAppUtils.returnSingleValue\n\nclass BarcodeWidgetScannerFragment : BarCodeScannerFragment() {\n    override fun isQrOnly(): Boolean {\n        return false\n    }\n\n    override fun handleScanningResult(result: String) {\n        if (isAdded) {\n            returnSingleValue(requireActivity(), result)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/MediaLoadingFragment.java",
    "content": "package org.odk.collect.android.fragments;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Bundle;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.Fragment;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.activities.FormFillingActivity;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.tasks.MediaLoadingTask;\n\npublic class MediaLoadingFragment extends Fragment {\n\n    private MediaLoadingTask mediaLoadingTask;\n\n    public void beginMediaLoadingTask(Uri uri, FormController formController) {\n        mediaLoadingTask = new MediaLoadingTask((FormFillingActivity) getActivity(), formController.getInstanceFile());\n        mediaLoadingTask.execute(uri);\n    }\n\n    @Override\n    public void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setRetainInstance(true);\n    }\n\n    @Override\n    public void onAttach(@NotNull Context context) {\n        super.onAttach(context);\n        if (mediaLoadingTask != null) {\n            mediaLoadingTask.onAttach((FormFillingActivity) getActivity());\n        }\n    }\n\n    public boolean isMediaLoadingTaskRunning() {\n        return mediaLoadingTask != null && mediaLoadingTask.getStatus() == AsyncTask.Status.RUNNING;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/dialogs/FormsDownloadResultDialog.kt",
    "content": "package org.odk.collect.android.fragments.dialogs\n\nimport android.app.Dialog\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.fragment.app.DialogFragment\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.android.formmanagement.ServerFormDetails\nimport org.odk.collect.android.formmanagement.download.FormDownloadException\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.utilities.FormsDownloadResultInterpreter\nimport org.odk.collect.errors.ErrorActivity\nimport java.io.Serializable\n\nclass FormsDownloadResultDialog : DialogFragment() {\n    private lateinit var result: Map<ServerFormDetails, FormDownloadException?>\n\n    var listener: FormDownloadResultDialogListener? = null\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n        if (context is FormDownloadResultDialogListener) {\n            listener = context\n        }\n    }\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        result = arguments?.getSerializable(ARG_RESULT) as Map<ServerFormDetails, FormDownloadException?>\n\n        val builder = MaterialAlertDialogBuilder(requireContext())\n            .setMessage(getMessage())\n            .setPositiveButton(getString(org.odk.collect.strings.R.string.ok)) { _, _ ->\n                listener?.onCloseDownloadingResult()\n            }\n\n        if (!FormsDownloadResultInterpreter.allFormsDownloadedSuccessfully(result)) {\n            builder.setNegativeButton(getString(org.odk.collect.strings.R.string.show_details)) { _, _ ->\n                val intent = Intent(context, ErrorActivity::class.java).apply {\n                    putExtra(ErrorActivity.EXTRA_ERRORS, FormsDownloadResultInterpreter.getFailures(result, requireContext()) as Serializable)\n                }\n                startActivity(intent)\n                listener?.onCloseDownloadingResult()\n            }\n        }\n\n        return builder.create()\n    }\n\n    private fun getMessage(): String {\n        return if (FormsDownloadResultInterpreter.allFormsDownloadedSuccessfully(result)) {\n            getString(org.odk.collect.strings.R.string.all_downloads_succeeded)\n        } else {\n            getString(org.odk.collect.strings.R.string.some_downloads_failed, FormsDownloadResultInterpreter.getNumberOfFailures(result).toString(), result.size.toString())\n        }\n    }\n\n    interface FormDownloadResultDialogListener {\n        fun onCloseDownloadingResult()\n    }\n\n    companion object {\n        const val ARG_RESULT = \"RESULT\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/dialogs/LocationProvidersDisabledDialog.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.fragments.dialogs;\n\nimport android.app.Dialog;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.provider.Settings;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.fragment.app.FragmentManager;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport timber.log.Timber;\n\npublic class LocationProvidersDisabledDialog extends DialogFragment {\n\n    public static final String LOCATION_PROVIDERS_DISABLED_DIALOG_TAG = \"locationProvidersDisabledDialogTag\";\n\n    public static LocationProvidersDisabledDialog newInstance() {\n        return new LocationProvidersDisabledDialog();\n    }\n\n    /*\n    We keep this just in case to avoid problems if someone tries to show a dialog after\n    the activity’s state have been saved. Basically it shouldn't take place since we should control\n    the activity state if we want to show a dialog (especially after long tasks).\n     */\n    @Override\n    public void show(FragmentManager manager, String tag) {\n        try {\n            manager\n                    .beginTransaction()\n                    .add(this, tag)\n                    .commit();\n        } catch (IllegalStateException e) {\n            Timber.w(e);\n        }\n    }\n\n    @Override\n    public Dialog onCreateDialog(Bundle savedInstanceState) {\n        setCancelable(false);\n\n        return new MaterialAlertDialogBuilder(getActivity())\n                .setIcon(org.odk.collect.permissions.R.drawable.ic_room_24dp)\n                .setTitle(org.odk.collect.strings.R.string.provider_disabled_error)\n                .setMessage(org.odk.collect.strings.R.string.location_providers_disabled_dialog_message)\n                .setPositiveButton(org.odk.collect.strings.R.string.go_to_settings, (dialog, id) -> {\n                    startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));\n                })\n                .setNegativeButton(org.odk.collect.strings.R.string.cancel, (dialog, id) -> {\n                })\n                .create();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/dialogs/MovingBackwardsDialog.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.fragments.dialogs;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.os.Bundle;\nimport androidx.fragment.app.DialogFragment;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\npublic class MovingBackwardsDialog extends DialogFragment {\n\n    public static final String MOVING_BACKWARDS_DIALOG_TAG = \"movingBackwardsDialogTag\";\n\n    public interface MovingBackwardsDialogListener {\n        void preventOtherWaysOfEditingForm();\n    }\n\n    private MovingBackwardsDialogListener listener;\n\n    @Override\n    public void onAttach(Context context) {\n        super.onAttach(context);\n        if (context instanceof MovingBackwardsDialogListener) {\n            listener = (MovingBackwardsDialogListener) context;\n        }\n    }\n\n    @Override\n    public Dialog onCreateDialog(Bundle savedInstanceState) {\n        setCancelable(false);\n\n        return new MaterialAlertDialogBuilder(getActivity())\n                .setTitle(org.odk.collect.strings.R.string.moving_backwards_disabled_title)\n                .setMessage(org.odk.collect.strings.R.string.moving_backwards_disabled_message)\n                .setPositiveButton(org.odk.collect.strings.R.string.yes, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int id) {\n                        listener.preventOtherWaysOfEditingForm();\n                    }\n                })\n                .setNegativeButton(org.odk.collect.strings.R.string.no, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int id) {\n                    }\n                })\n                .create();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/dialogs/RangePickerDialogFragment.kt",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.fragments.dialogs\n\nimport android.app.Dialog\nimport android.os.Bundle\nimport android.widget.NumberPicker\nimport androidx.fragment.app.DialogFragment\nimport androidx.fragment.app.activityViewModels\nimport androidx.lifecycle.ViewModelProvider\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.core.model.data.DecimalData\nimport org.javarosa.core.model.data.IntegerData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formentry.FormEntryViewModel\n\nclass RangePickerDialogFragment(private val viewModelFactory: ViewModelProvider.Factory) :\n    DialogFragment() {\n\n    private val formEntryViewModel: FormEntryViewModel by activityViewModels { viewModelFactory }\n    private val prompt: FormEntryPrompt by lazy {\n        formEntryViewModel.getQuestionPrompt(requireArguments().getSerializable(ARG_FORM_INDEX) as FormIndex)\n    }\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        val view = layoutInflater.inflate(R.layout.number_picker_dialog, null)\n\n        val numbers = requireArguments().getSerializable(ARG_VALUES) as Array<String>\n        val selected = requireArguments().getInt(ARG_SELECTED)\n        val decimal = requireArguments().getBoolean(ARG_DECIMAL)\n\n        val numberPicker = view.findViewById<NumberPicker>(R.id.number_picker).apply {\n            maxValue = numbers.size - 1\n            minValue = 0\n            wrapSelectorWheel = false\n            displayedValues = numbers\n            value = selected\n        }\n\n        return MaterialAlertDialogBuilder(requireActivity())\n            .setTitle(org.odk.collect.strings.R.string.number_picker_title)\n            .setView(view)\n            .setPositiveButton(org.odk.collect.strings.R.string.ok) { _, _ ->\n                val value = numbers[numberPicker.value]\n                val answerData = if (decimal) {\n                    DecimalData(value.toDouble())\n                } else {\n                    IntegerData(value.toInt())\n                }\n\n                formEntryViewModel.answerQuestion(prompt.index, answerData)\n            }\n            .setNegativeButton(org.odk.collect.strings.R.string.cancel) { _, _ -> }\n            .create()\n    }\n\n    companion object {\n        const val ARG_VALUES = \"values\"\n        const val ARG_SELECTED = \"selected\"\n        const val ARG_FORM_INDEX = \"formIndex\"\n        const val ARG_DECIMAL = \"decimal\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/dialogs/RankingWidgetDialog.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.fragments.dialogs;\n\nimport static org.odk.collect.android.utilities.ViewUtils.pxFromDp;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\nimport android.widget.LinearLayout;\nimport android.widget.LinearLayout.LayoutParams;\nimport android.widget.TextView;\n\nimport androidx.core.widget.NestedScrollView;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport androidx.recyclerview.widget.ItemTouchHelper.Callback;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.adapters.RankingListAdapter;\nimport org.odk.collect.android.fragments.viewmodels.RankingViewModel;\nimport org.odk.collect.android.utilities.RankingItemTouchHelperCallback;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class RankingWidgetDialog extends DialogFragment {\n    private RankingListener listener;\n    private RankingListAdapter rankingListAdapter;\n    private List<SelectChoice> items;\n    private FormEntryPrompt formEntryPrompt;\n    private RankingViewModel viewModel;\n\n    public interface RankingListener {\n        void onRankingChanged(List<SelectChoice> items);\n    }\n\n    public RankingWidgetDialog() {\n    }\n\n    public RankingWidgetDialog(List<SelectChoice> items, FormEntryPrompt formEntryPrompt) {\n        this.items = new ArrayList<>(items);\n        this.formEntryPrompt = formEntryPrompt;\n    }\n\n    @Override\n    public void onAttach(@NotNull Context context) {\n        super.onAttach(context);\n        if (context instanceof RankingListener) {\n            listener = (RankingListener) context;\n        }\n        viewModel = new ViewModelProvider(this, new RankingViewModel.Factory(items, formEntryPrompt)).get(RankingViewModel.class);\n        if (viewModel.getItems() == null) {\n            dismiss();\n        }\n    }\n\n    @Override\n    public Dialog onCreateDialog(Bundle savedInstanceState) {\n        return new MaterialAlertDialogBuilder(getActivity())\n                .setView(setUpRankingLayout())\n                .setPositiveButton(org.odk.collect.strings.R.string.ok, (dialog, id) -> {\n                    listener.onRankingChanged(rankingListAdapter.getItems());\n                    dismiss();\n                })\n                .setNegativeButton(org.odk.collect.strings.R.string.cancel, (dialog, id) -> dismiss())\n                .create();\n    }\n\n    private NestedScrollView setUpRankingLayout() {\n        LinearLayout rankingLayout = new LinearLayout(getContext());\n        rankingLayout.setOrientation(LinearLayout.HORIZONTAL);\n        rankingLayout.addView(setUpPositionsLayout());\n        rankingLayout.addView(setUpRecyclerView());\n\n        float standardMargin = requireContext()\n                .getResources()\n                .getDimension(org.odk.collect.androidshared.R.dimen.margin_standard);\n        int standardMarginPx = pxFromDp(requireContext(), standardMargin);\n        rankingLayout.setPadding(standardMarginPx, standardMarginPx, standardMarginPx, standardMarginPx);\n\n        NestedScrollView scrollView = new NestedScrollView(getContext());\n        scrollView.addView(rankingLayout);\n        return scrollView;\n    }\n\n    private LinearLayout setUpPositionsLayout() {\n        LinearLayout positionsLayout = new LinearLayout(getContext());\n        positionsLayout.setOrientation(LinearLayout.VERTICAL);\n\n        LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);\n        layoutParams.setMargins(0, 0, 10, 0);\n        positionsLayout.setLayoutParams(layoutParams);\n\n        for (SelectChoice item : viewModel.getItems()) {\n            FrameLayout positionLayout = (FrameLayout) LayoutInflater.from(getContext()).inflate(R.layout.ranking_item, positionsLayout, false);\n            TextView textView = positionLayout.findViewById(R.id.rank_item_text);\n            textView.setText(String.valueOf(viewModel.getItems().indexOf(item) + 1));\n            textView.setTextSize(QuestionFontSizeUtils.getQuestionFontSize());\n\n            positionsLayout.addView(positionLayout);\n        }\n        return positionsLayout;\n    }\n\n    private RecyclerView setUpRecyclerView() {\n        rankingListAdapter = new RankingListAdapter(viewModel.getItems(), viewModel.getFormEntryPrompt());\n\n        RecyclerView recyclerView = new RecyclerView(getContext());\n        recyclerView.setHasFixedSize(true);\n        recyclerView.setAdapter(rankingListAdapter);\n        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));\n        recyclerView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));\n\n        Callback callback = new RankingItemTouchHelperCallback(rankingListAdapter);\n        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);\n        itemTouchHelper.attachToRecyclerView(recyclerView);\n\n        return recyclerView;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/dialogs/ResetSettingsResultDialog.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.fragments.dialogs;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.os.Bundle;\nimport androidx.fragment.app.DialogFragment;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\npublic class ResetSettingsResultDialog extends DialogFragment {\n    public static final String RESET_SETTINGS_RESULT_DIALOG_TAG = \"resetSettingsResultDialogTag\";\n\n    private static final String MESSAGE = \"message\";\n\n    public interface ResetSettingsResultDialogListener {\n        void onDialogClosed();\n    }\n\n    private ResetSettingsResultDialogListener listener;\n\n    public static ResetSettingsResultDialog newInstance(String dialogMessage) {\n        Bundle bundle = new Bundle();\n        bundle.putString(MESSAGE, dialogMessage);\n\n        ResetSettingsResultDialog dialogFragment = new ResetSettingsResultDialog();\n        dialogFragment.setArguments(bundle);\n        return dialogFragment;\n    }\n\n    @Override\n    public void onAttach(Context context) {\n        super.onAttach(context);\n        if (context instanceof ResetSettingsResultDialogListener) {\n            listener = (ResetSettingsResultDialogListener) context;\n        }\n    }\n\n    @Override\n    public Dialog onCreateDialog(Bundle savedInstanceState) {\n        setCancelable(false);\n\n        String message = getArguments() != null ? getArguments().getString(MESSAGE) : \"\";\n\n        return new MaterialAlertDialogBuilder(getActivity())\n                .setTitle(org.odk.collect.strings.R.string.reset_app_state_result)\n                .setMessage(message)\n                .setPositiveButton(org.odk.collect.strings.R.string.ok, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int id) {\n                        listener.onDialogClosed();\n                    }\n                })\n                .create();\n    }\n}"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/dialogs/SelectMinimalDialog.java",
    "content": "package org.odk.collect.android.fragments.dialogs;\n\nimport static org.odk.collect.android.injection.DaggerUtils.getComponent;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.SearchView;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.adapters.AbstractSelectListAdapter;\nimport org.odk.collect.android.databinding.SelectMinimalDialogLayoutBinding;\nimport org.odk.collect.android.fragments.viewmodels.SelectMinimalViewModel;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.audioclips.AudioPlayerFactory;\nimport org.odk.collect.material.MaterialFullScreenDialogFragment;\n\nimport java.util.List;\n\nimport javax.inject.Inject;\n\npublic abstract class SelectMinimalDialog extends MaterialFullScreenDialogFragment {\n\n    @Inject\n    public AudioPlayerFactory audioPlayerFactory;\n\n    private SelectMinimalDialogLayoutBinding binding;\n\n    private boolean isFlex;\n    private boolean isAutocomplete;\n\n    protected SelectMinimalViewModel viewModel;\n    protected SelectMinimalDialogListener listener;\n    protected AbstractSelectListAdapter adapter;\n\n    public interface SelectMinimalDialogListener {\n        void updateSelectedItems(List<Selection> items);\n    }\n\n    public SelectMinimalDialog() {\n    }\n\n    public SelectMinimalDialog(boolean isFlex, boolean isAutoComplete) {\n        this.isFlex = isFlex;\n        this.isAutocomplete = isAutoComplete;\n    }\n\n    @Override\n    public void onAttach(@NotNull Context context) {\n        super.onAttach(context);\n        getComponent(context).inject(this);\n        if (context instanceof SelectMinimalDialogListener) {\n            listener = (SelectMinimalDialogListener) context;\n        }\n        viewModel = new ViewModelProvider(this, new SelectMinimalViewModel.Factory(adapter, isFlex, isAutocomplete)).get(SelectMinimalViewModel.class);\n        if (viewModel.getSelectListAdapter() == null) {\n            dismiss();\n        }\n    }\n\n    @Override\n    public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {\n        binding = SelectMinimalDialogLayoutBinding.inflate(inflater, container, false);\n        return binding.getRoot();\n    }\n\n    @Override\n    public void onViewCreated(@NotNull View view, Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        initRecyclerView();\n        initToolbar();\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        viewModel.getSelectListAdapter().getAudioPlayer().stop();\n        binding = null;\n    }\n\n    @Override\n    protected void onCloseClicked() {\n        closeDialogAndSaveAnswers();\n    }\n\n    @Override\n    protected void onBackPressed() {\n        closeDialogAndSaveAnswers();\n    }\n\n    protected void closeDialogAndSaveAnswers() {\n        viewModel.getSelectListAdapter().getFilter().filter(\"\");\n        if (viewModel.getSelectListAdapter().hasAnswerChanged()) {\n            listener.updateSelectedItems(viewModel.getSelectListAdapter().getSelectedItems());\n        }\n        dismiss();\n    }\n\n    @Nullable\n    @Override\n    protected Toolbar getToolbar() {\n        return getView().findViewById(org.odk.collect.androidshared.R.id.toolbar);\n    }\n\n    private void initToolbar() {\n        getToolbar().setNavigationIcon(R.drawable.ic_arrow_back);\n\n        if (viewModel.isAutoComplete()) {\n            initSearchBar();\n        }\n    }\n\n    private void initSearchBar() {\n        getToolbar().inflateMenu(R.menu.select_minimal_dialog_menu);\n\n        SearchView searchView = (SearchView) getToolbar().getMenu().findItem(R.id.menu_filter).getActionView();\n        searchView.setQueryHint(getResources().getString(org.odk.collect.strings.R.string.search));\n        searchView.setMaxWidth(Integer.MAX_VALUE);\n        searchView.onActionViewExpanded();\n        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {\n            @Override\n            public boolean onQueryTextSubmit(String query) {\n                searchView.clearFocus();\n                return false;\n            }\n\n            @Override\n            public boolean onQueryTextChange(String newText) {\n                viewModel.getSelectListAdapter().getFilter().filter(newText);\n                return false;\n            }\n        });\n    }\n\n    private void initRecyclerView() {\n        viewModel.getSelectListAdapter().setContext(getActivity());\n        AudioPlayer audioPlayer = audioPlayerFactory.create(requireActivity(), getViewLifecycleOwner());\n        viewModel.getSelectListAdapter().setAudioPlayer(audioPlayer);\n        binding.choicesRecyclerView.initRecyclerView(viewModel.getSelectListAdapter(), viewModel.isFlex());\n    }\n\n    public void setListener(SelectMinimalDialogListener listener) {\n        this.listener = listener;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/dialogs/SelectMultiMinimalDialog.java",
    "content": "package org.odk.collect.android.fragments.dialogs;\n\nimport android.content.Context;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.adapters.SelectMultipleListAdapter;\nimport org.odk.collect.android.utilities.MediaUtils;\n\nimport java.util.List;\n\npublic class SelectMultiMinimalDialog extends SelectMinimalDialog {\n    public SelectMultiMinimalDialog() {\n    }\n\n    public SelectMultiMinimalDialog(List<Selection> selectedItems, boolean isFlex, boolean isAutoComplete, Context context,\n                                    List<SelectChoice> items, FormEntryPrompt prompt, ReferenceManager referenceManager,\n                                    int playColor, int numColumns, boolean noButtonsMode, MediaUtils mediaUtils) {\n        super(isFlex, isAutoComplete);\n        adapter = new SelectMultipleListAdapter(selectedItems, null, context, items, prompt,\n                referenceManager, null, playColor, numColumns, noButtonsMode, mediaUtils);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/dialogs/SelectOneMinimalDialog.java",
    "content": "package org.odk.collect.android.fragments.dialogs;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.View;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.adapters.SelectOneListAdapter;\nimport org.odk.collect.android.listeners.SelectItemClickListener;\nimport org.odk.collect.android.utilities.MediaUtils;\n\nimport java.util.List;\n\npublic class SelectOneMinimalDialog extends SelectMinimalDialog implements SelectItemClickListener {\n    public SelectOneMinimalDialog() {\n    }\n\n    public SelectOneMinimalDialog(String selectedItem, boolean isFlex, boolean isAutoComplete, Context context,\n                                  List<SelectChoice> items, FormEntryPrompt prompt, ReferenceManager referenceManager,\n                                  int playColor, int numColumns, boolean noButtonsMode, MediaUtils mediaUtils) {\n        super(isFlex, isAutoComplete);\n        adapter = new SelectOneListAdapter(selectedItem, this, context, items, prompt,\n                referenceManager, null, playColor, numColumns, noButtonsMode, mediaUtils);\n    }\n\n    @Override\n    public void onViewCreated(@NotNull View view, Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        // updates needed to handle recreation (screen rotation for example)\n        ((SelectOneListAdapter) viewModel.getSelectListAdapter()).setSelectItemClickListener(this);\n    }\n\n    @Override\n    public void onItemClicked() {\n        closeDialogAndSaveAnswers();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/dialogs/SimpleDialog.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.fragments.dialogs;\n\nimport android.app.Dialog;\nimport android.os.Bundle;\n\nimport androidx.fragment.app.DialogFragment;\nimport androidx.fragment.app.FragmentManager;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport timber.log.Timber;\n\n/**\n * This class might be used as an universal simple dialog. You can use it if you just need to\n * display it and you don't need any callback.\n */\npublic class SimpleDialog extends DialogFragment {\n\n    public static final String COLLECT_DIALOG_TAG = \"collectDialogTag\";\n\n    private static final String DIALOG_TITLE = \"dialogTitle\";\n    private static final String ICON_ID = \"iconId\";\n    private static final String MESSAGE = \"message\";\n    private static final String BUTTON_TITLE = \"buttonTitle\";\n    private static final String FINISH_ACTIVITY = \"finishActivity\";\n\n    public static SimpleDialog newInstance(String dialogTitle, int iconId, String message, String buttonTitle, boolean finishActivity) {\n        Bundle bundle = new Bundle();\n        bundle.putString(DIALOG_TITLE, dialogTitle);\n        bundle.putInt(ICON_ID, iconId);\n        bundle.putString(MESSAGE, message);\n        bundle.putString(BUTTON_TITLE, buttonTitle);\n        bundle.putBoolean(FINISH_ACTIVITY, finishActivity);\n\n        SimpleDialog dialogFragment = new SimpleDialog();\n        dialogFragment.setArguments(bundle);\n        return dialogFragment;\n    }\n\n    /*\n    We keep this just in case to avoid problems if someone tries to show a dialog after\n    the activity’s state have been saved. Basically it shouldn't take place since we should control\n    the activity state if we want to show a dialog (especially after long tasks).\n     */\n    @Override\n    public void show(FragmentManager manager, String tag) {\n        try {\n            manager\n                    .beginTransaction()\n                    .add(this, tag)\n                    .commit();\n        } catch (IllegalStateException e) {\n            Timber.w(e);\n        }\n    }\n\n    @Override\n    public Dialog onCreateDialog(Bundle savedInstanceState) {\n        setCancelable(false);\n\n        return new MaterialAlertDialogBuilder(getActivity())\n                .setTitle(getArguments().getString(DIALOG_TITLE))\n                .setIcon(getArguments().getInt(ICON_ID))\n                .setMessage(getArguments().getString(MESSAGE))\n                .setPositiveButton(getArguments().getString(BUTTON_TITLE), (dialog, id) -> {\n                    if (getArguments().getBoolean(FINISH_ACTIVITY)) {\n                        getActivity().finish();\n                    }\n                })\n                .create();\n    }\n}"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/viewmodels/RankingViewModel.java",
    "content": "package org.odk.collect.android.fragments.viewmodels;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.form.api.FormEntryPrompt;\n\nimport java.util.List;\n\npublic class RankingViewModel extends ViewModel {\n\n    private final List<SelectChoice> items;\n    private final FormEntryPrompt formEntryPrompt;\n\n    private RankingViewModel(List<SelectChoice> items, FormEntryPrompt formEntryPrompt) {\n        this.items = items;\n        this.formEntryPrompt = formEntryPrompt;\n    }\n\n    public List<SelectChoice> getItems() {\n        return items;\n    }\n\n    public FormEntryPrompt getFormEntryPrompt() {\n        return formEntryPrompt;\n    }\n\n    public static class Factory implements ViewModelProvider.Factory {\n        private final List<SelectChoice> items;\n        private final FormEntryPrompt formEntryPrompt;\n\n        public Factory(List<SelectChoice> items, FormEntryPrompt formEntryPrompt) {\n            this.items = items;\n            this.formEntryPrompt = formEntryPrompt;\n        }\n\n        @NonNull\n        @Override\n        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {\n            return (T) new RankingViewModel(items, formEntryPrompt);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/fragments/viewmodels/SelectMinimalViewModel.java",
    "content": "package org.odk.collect.android.fragments.viewmodels;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.odk.collect.android.adapters.AbstractSelectListAdapter;\n\npublic class SelectMinimalViewModel extends ViewModel {\n    private final AbstractSelectListAdapter selectListAdapter;\n    private final boolean isFlex;\n    private final boolean isAutoComplete;\n\n    private SelectMinimalViewModel(AbstractSelectListAdapter selectListAdapter, boolean isFlex, boolean isAutoComplete) {\n        this.selectListAdapter = selectListAdapter;\n        this.isFlex = isFlex;\n        this.isAutoComplete = isAutoComplete;\n    }\n\n    public AbstractSelectListAdapter getSelectListAdapter() {\n        return selectListAdapter;\n    }\n\n    public boolean isFlex() {\n        return isFlex;\n    }\n\n    public boolean isAutoComplete() {\n        return isAutoComplete;\n    }\n\n    public static class Factory implements ViewModelProvider.Factory {\n        private final AbstractSelectListAdapter selectListAdapter;\n        private final boolean isFlex;\n        private final boolean isAutoComplete;\n\n        public Factory(AbstractSelectListAdapter selectListAdapter, boolean isFlex, boolean isAutoComplete) {\n            this.selectListAdapter = selectListAdapter;\n            this.isFlex = isFlex;\n            this.isAutoComplete = isAutoComplete;\n        }\n\n        @NonNull\n        @Override\n        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {\n            return (T) new SelectMinimalViewModel(selectListAdapter, isFlex, isAutoComplete);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/geo/MapConfiguratorProvider.java",
    "content": "package org.odk.collect.android.geo;\n\nimport static org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_CARTO;\nimport static org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_GOOGLE;\nimport static org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_MAPBOX;\nimport static org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_OSM;\nimport static org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_USGS;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_BASEMAP_SOURCE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_CARTO_MAP_STYLE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_GOOGLE_MAP_STYLE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_USGS_MAP_STYLE;\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.content.Context;\n\nimport androidx.annotation.NonNull;\n\nimport com.google.android.gms.maps.GoogleMap;\n\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.application.MapboxClassInstanceCreator;\nimport org.odk.collect.googlemaps.GoogleMapConfigurator;\nimport org.odk.collect.googlemaps.GoogleMapConfigurator.GoogleMapTypeOption;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.maps.MapConfigurator;\nimport org.odk.collect.osmdroid.OsmDroidMapConfigurator;\nimport org.odk.collect.osmdroid.OsmDroidMapConfigurator.WmsOption;\nimport org.odk.collect.osmdroid.WebMapService;\n\nimport java.util.ArrayList;\n\npublic class MapConfiguratorProvider {\n\n    private static SourceOption[] sourceOptions;\n    private static final String USGS_URL_BASE =\n        \"https://basemap.nationalmap.gov/arcgis/rest/services\";\n    private static final String OSM_COPYRIGHT = \"© OpenStreetMap contributors\";\n    private static final String CARTO_COPYRIGHT = \"© CARTO\";\n    private static final String CARTO_ATTRIBUTION = OSM_COPYRIGHT + \", \" + CARTO_COPYRIGHT;\n    private static final String USGS_ATTRIBUTION = \"Map services and data available from U.S. Geological Survey,\\nNational Geospatial Program.\";\n\n    private MapConfiguratorProvider() {\n\n    }\n\n    /**\n     * In the preference UI, the available basemaps are organized into \"sources\"\n     * to make them easier to find.  This defines the basemap sources and the\n     * basemap options available under each one, in their order of appearance.\n     */\n    public static void initOptions(Context context) {\n        if (sourceOptions != null) {\n            return;\n        }\n\n        ArrayList<SourceOption> sourceOptions = new ArrayList<>();\n\n        GoogleMapConfigurator googleMapsConfigurator = new GoogleMapConfigurator(\n                KEY_GOOGLE_MAP_STYLE, org.odk.collect.strings.R.string.basemap_source_google,\n                new GoogleMapTypeOption(GoogleMap.MAP_TYPE_NORMAL, org.odk.collect.strings.R.string.streets),\n                new GoogleMapTypeOption(GoogleMap.MAP_TYPE_TERRAIN, org.odk.collect.strings.R.string.terrain),\n                new GoogleMapTypeOption(GoogleMap.MAP_TYPE_HYBRID, org.odk.collect.strings.R.string.hybrid),\n                new GoogleMapTypeOption(GoogleMap.MAP_TYPE_SATELLITE, org.odk.collect.strings.R.string.satellite)\n        );\n\n        if (googleMapsConfigurator.isAvailable(context)) {\n            sourceOptions.add(new SourceOption(BASEMAP_SOURCE_GOOGLE, org.odk.collect.strings.R.string.basemap_source_google,\n                    googleMapsConfigurator\n            ));\n        }\n\n        if (isMapboxSupported()) {\n            sourceOptions.add(new SourceOption(BASEMAP_SOURCE_MAPBOX, org.odk.collect.strings.R.string.basemap_source_mapbox,\n                    MapboxClassInstanceCreator.createMapboxMapConfigurator()\n            ));\n        }\n\n        sourceOptions.add(new SourceOption(BASEMAP_SOURCE_OSM, org.odk.collect.strings.R.string.basemap_source_osm,\n                new OsmDroidMapConfigurator(\n                        new WebMapService(\n                                \"Mapnik\", 0, 19, 256, OSM_COPYRIGHT,\n                                \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\"\n                        )\n                )\n        ));\n        sourceOptions.add(new SourceOption(BASEMAP_SOURCE_USGS, org.odk.collect.strings.R.string.basemap_source_usgs,\n                new OsmDroidMapConfigurator(\n                        KEY_USGS_MAP_STYLE, org.odk.collect.strings.R.string.basemap_source_usgs,\n                        new WmsOption(\"topographic\", org.odk.collect.strings.R.string.topographic, new WebMapService(\n                                getLocalizedString(getApplication(), org.odk.collect.strings.R.string.openmap_usgs_topo), 0, 18, 256, USGS_ATTRIBUTION,\n                                USGS_URL_BASE + \"/USGSTopo/MapServer/tile/{z}/{y}/{x}\"\n                        )),\n                        new WmsOption(\"hybrid\", org.odk.collect.strings.R.string.hybrid, new WebMapService(\n                                getLocalizedString(getApplication(), org.odk.collect.strings.R.string.openmap_usgs_sat), 0, 18, 256, USGS_ATTRIBUTION,\n                                USGS_URL_BASE + \"/USGSImageryTopo/MapServer/tile/{z}/{y}/{x}\"\n                        )),\n                        new WmsOption(\"satellite\", org.odk.collect.strings.R.string.satellite, new WebMapService(\n                                getLocalizedString(getApplication(), org.odk.collect.strings.R.string.openmap_usgs_img), 0, 18, 256, USGS_ATTRIBUTION,\n                                USGS_URL_BASE + \"/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}\"\n                        ))\n                )\n        ));\n        sourceOptions.add(new SourceOption(BASEMAP_SOURCE_CARTO, org.odk.collect.strings.R.string.basemap_source_carto,\n                new OsmDroidMapConfigurator(\n                        KEY_CARTO_MAP_STYLE, org.odk.collect.strings.R.string.basemap_source_carto,\n                        new WmsOption(\"positron\", org.odk.collect.strings.R.string.carto_map_style_positron, new WebMapService(\n                                getLocalizedString(getApplication(), org.odk.collect.strings.R.string.openmap_cartodb_positron), 0, 18, 256, CARTO_ATTRIBUTION,\n                                \"http://1.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png\"\n                        )),\n                        new WmsOption(\"dark_matter\", org.odk.collect.strings.R.string.carto_map_style_dark_matter, new WebMapService(\n                                getLocalizedString(getApplication(), org.odk.collect.strings.R.string.openmap_cartodb_darkmatter), 0, 18, 256, CARTO_ATTRIBUTION,\n                                \"http://1.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png\"\n                        ))\n                )\n        ));\n\n        MapConfiguratorProvider.sourceOptions = sourceOptions.toArray(new SourceOption[]{});\n    }\n\n    /** Gets the currently selected MapConfigurator. */\n    public static @NonNull\n    MapConfigurator getConfigurator() {\n        return getOption(null).cftor;\n    }\n\n    /**\n     * Gets the MapConfigurator for the SourceOption with the given id, or the\n     * currently selected MapConfigurator if id is null.\n     */\n    public static @NonNull MapConfigurator getConfigurator(String id) {\n        return getOption(id).cftor;\n    }\n\n    /** Gets a list of the IDs of the basemap sources, in order. */\n    public static String[] getIds() {\n        String[] ids = new String[sourceOptions.length];\n        for (int i = 0; i < ids.length; i++) {\n            ids[i] = sourceOptions[i].id;\n        }\n        return ids;\n    }\n\n    /** Gets a list of the label string IDs of the basemap sources, in order. */\n    public static int[] getLabelIds() {\n        int[] labelIds = new int[sourceOptions.length];\n        for (int i = 0; i < labelIds.length; i++) {\n            labelIds[i] = sourceOptions[i].labelId;\n        }\n        return labelIds;\n    }\n\n    private static boolean isMapboxSupported() {\n        return MapboxClassInstanceCreator.isMapboxAvailable();\n    }\n\n    /**\n     * Gets the SourceOption with the given id, or the currently selected option\n     * if id is null, or the first option if the id is unknown.  Never null.\n     */\n    private static @NonNull SourceOption getOption(String id) {\n        if (id == null) {\n            id = DaggerUtils.getComponent(getApplication()).settingsProvider().getUnprotectedSettings().getString(KEY_BASEMAP_SOURCE);\n        }\n        for (SourceOption option : sourceOptions) {\n            if (option.id.equals(id)) {\n                return option;\n            }\n        }\n\n        return sourceOptions[0];\n    }\n\n    private static Collect getApplication() {\n        return Collect.getInstance();\n    }\n\n    private static class SourceOption {\n        private final String id;  // preference value to store\n        private final int labelId;  // string resource ID\n        private final MapConfigurator cftor;\n\n        private SourceOption(String id, int labelId, MapConfigurator cftor) {\n            this.id = id;\n            this.labelId = labelId;\n            this.cftor = cftor;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/geo/MapFragmentFactoryImpl.kt",
    "content": "package org.odk.collect.android.geo\n\nimport org.odk.collect.android.application.MapboxClassInstanceCreator\nimport org.odk.collect.googlemaps.GoogleMapFragment\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.osmdroid.OsmDroidMapFragment\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_CARTO\nimport org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_MAPBOX\nimport org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_OSM\nimport org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_USGS\nimport org.odk.collect.settings.keys.ProjectKeys.KEY_BASEMAP_SOURCE\n\nclass MapFragmentFactoryImpl(private val settingsProvider: SettingsProvider) : MapFragmentFactory {\n\n    override fun createMapFragment(): MapFragment {\n        val settings = settingsProvider.getUnprotectedSettings()\n\n        return when {\n            isBasemapOSM(settings.getString(KEY_BASEMAP_SOURCE)) -> OsmDroidMapFragment()\n            settings.getString(KEY_BASEMAP_SOURCE) == BASEMAP_SOURCE_MAPBOX -> MapboxClassInstanceCreator.createMapboxMapFragment()\n            else -> GoogleMapFragment()\n        }\n    }\n\n    private fun isBasemapOSM(basemap: String?): Boolean {\n        return basemap == BASEMAP_SOURCE_OSM ||\n            basemap == BASEMAP_SOURCE_USGS ||\n            basemap == BASEMAP_SOURCE_CARTO\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/DaggerUtils.kt",
    "content": "/*\n * Copyright (C) 2018 Callum Stott\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.injection\n\nimport android.content.Context\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.injection.config.AppDependencyComponent\n\nobject DaggerUtils {\n\n    @JvmStatic\n    fun getComponent(context: Context): AppDependencyComponent {\n        val component = (context.applicationContext as Collect).component\n        if (component != null) {\n            return component\n        } else {\n            throw IllegalStateException(\"Collect.applicationComponent is null!\")\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.kt",
    "content": "package org.odk.collect.android.injection.config\n\nimport android.app.Application\nimport dagger.BindsInstance\nimport dagger.Component\nimport org.javarosa.core.reference.ReferenceManager\nimport org.odk.collect.android.activities.AboutActivity\nimport org.odk.collect.android.activities.AppListActivity\nimport org.odk.collect.android.activities.DeleteFormsActivity\nimport org.odk.collect.android.activities.FirstLaunchActivity\nimport org.odk.collect.android.activities.FormDownloadListActivity\nimport org.odk.collect.android.activities.FormFillingActivity\nimport org.odk.collect.android.activities.FormMapActivity\nimport org.odk.collect.android.activities.InstanceChooserList\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.application.initialization.ApplicationInitializer\nimport org.odk.collect.android.application.initialization.ExistingProjectMigrator\nimport org.odk.collect.android.audio.AudioRecordingControllerFragment\nimport org.odk.collect.android.audio.AudioRecordingErrorDialogFragment\nimport org.odk.collect.android.backgroundwork.AutoUpdateTaskSpec\nimport org.odk.collect.android.backgroundwork.SendFormsTaskSpec\nimport org.odk.collect.android.backgroundwork.SyncFormsTaskSpec\nimport org.odk.collect.android.configure.qr.QRCodeScannerFragment\nimport org.odk.collect.android.configure.qr.QRCodeTabsActivity\nimport org.odk.collect.android.configure.qr.ShowQRCodeFragment\nimport org.odk.collect.android.entities.EntitiesRepositoryProvider\nimport org.odk.collect.android.external.AndroidShortcutsActivity\nimport org.odk.collect.android.external.FormUriActivity\nimport org.odk.collect.android.external.FormsProvider\nimport org.odk.collect.android.external.InstanceProvider\nimport org.odk.collect.android.formentry.BackgroundAudioPermissionDialogFragment\nimport org.odk.collect.android.formentry.ODKView\nimport org.odk.collect.android.formentry.repeats.DeleteRepeatDialogFragment\nimport org.odk.collect.android.formentry.saving.SaveAnswerFileErrorDialogFragment\nimport org.odk.collect.android.formentry.saving.SaveFormProgressDialogFragment\nimport org.odk.collect.android.formhierarchy.FormHierarchyFragmentHostActivity\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListActivity\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.formmanagement.OpenRosaClientProvider\nimport org.odk.collect.android.fragments.BarCodeScannerFragment\nimport org.odk.collect.android.fragments.dialogs.FormsDownloadResultDialog\nimport org.odk.collect.android.fragments.dialogs.SelectMinimalDialog\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.instancemanagement.send.InstanceUploaderActivity\nimport org.odk.collect.android.instancemanagement.send.InstanceUploaderListActivity\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.android.preferences.dialogs.AdminPasswordDialogFragment\nimport org.odk.collect.android.preferences.dialogs.ChangeAdminPasswordDialog\nimport org.odk.collect.android.preferences.dialogs.ResetDialogPreferenceFragmentCompat\nimport org.odk.collect.android.preferences.dialogs.ServerAuthDialogFragment\nimport org.odk.collect.android.preferences.screens.BasePreferencesFragment\nimport org.odk.collect.android.preferences.screens.BaseProjectPreferencesFragment\nimport org.odk.collect.android.preferences.screens.ExperimentalPreferencesFragment\nimport org.odk.collect.android.preferences.screens.FormManagementPreferencesFragment\nimport org.odk.collect.android.preferences.screens.FormMetadataPreferencesFragment\nimport org.odk.collect.android.preferences.screens.IdentityPreferencesFragment\nimport org.odk.collect.android.preferences.screens.MapsPreferencesFragment\nimport org.odk.collect.android.preferences.screens.ProjectDisplayPreferencesFragment\nimport org.odk.collect.android.preferences.screens.ProjectPreferencesActivity\nimport org.odk.collect.android.preferences.screens.ProjectPreferencesFragment\nimport org.odk.collect.android.preferences.screens.ServerPreferencesFragment\nimport org.odk.collect.android.preferences.screens.UserInterfacePreferencesFragment\nimport org.odk.collect.android.projects.ManualProjectCreatorDialog\nimport org.odk.collect.android.projects.ProjectResetter\nimport org.odk.collect.android.projects.ProjectSettingsDialog\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.projects.QrCodeProjectCreatorDialog\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.tasks.DownloadFormListTask\nimport org.odk.collect.android.tasks.InstanceUploaderTask\nimport org.odk.collect.android.tasks.MediaLoadingTask\nimport org.odk.collect.android.utilities.AuthDialogUtility\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.utilities.SavepointsRepositoryProvider\nimport org.odk.collect.android.utilities.ThemeUtils\nimport org.odk.collect.android.widgets.QuestionWidget\nimport org.odk.collect.android.widgets.items.SelectOneFromMapDialogFragment\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.network.NetworkStateProvider\nimport org.odk.collect.draw.DrawActivity\nimport org.odk.collect.googlemaps.GoogleMapFragment\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.permissions.PermissionsProvider\nimport org.odk.collect.projects.ProjectCreator\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.webpage.WebPageService\nimport javax.inject.Singleton\n\n/**\n * Dagger component for the application. Should include\n * application level Dagger Modules and be built with Application\n * object.\n *\n * Add an `inject(MyClass myClass)` method here for objects you want\n * to inject into so Dagger knows to wire it up.\n *\n * Annotated with @Singleton so modules can include @Singletons that will\n * be retained at an application level (as this an instance of this components\n * is owned by the Application object).\n *\n * If you need to call a provider directly from the component (in a test\n * for example) you can add a method with the type you are looking to fetch\n * (`MyType myType()`) to this interface.\n *\n * To read more about Dagger visit: https://google.github.io/dagger/users-guide\n */\n@Singleton\n@Component(modules = [AppDependencyModule::class])\ninterface AppDependencyComponent {\n    @Component.Builder\n    interface Builder {\n        @BindsInstance\n        fun application(application: Application): Builder\n\n        fun appDependencyModule(testDependencyModule: AppDependencyModule): Builder\n\n        fun build(): AppDependencyComponent\n    }\n\n    fun inject(collect: Collect)\n\n    fun inject(aboutActivity: AboutActivity)\n\n    fun inject(formFillingActivity: FormFillingActivity)\n\n    fun inject(uploader: InstanceUploaderTask)\n\n    fun inject(serverPreferencesFragment: ServerPreferencesFragment)\n\n    fun inject(projectDisplayPreferencesFragment: ProjectDisplayPreferencesFragment)\n\n    fun inject(authDialogUtility: AuthDialogUtility)\n\n    fun inject(formDownloadListActivity: FormDownloadListActivity)\n\n    fun inject(activity: InstanceUploaderListActivity)\n\n    @Deprecated(\"should use {@link QuestionWidget.Dependencies} instead\")\n    fun inject(questionWidget: QuestionWidget)\n\n    fun inject(odkView: ODKView)\n\n    fun inject(formMetadataPreferencesFragment: FormMetadataPreferencesFragment)\n\n    fun inject(formMapActivity: FormMapActivity)\n\n    fun inject(mapFragment: GoogleMapFragment)\n\n    fun inject(mainMenuActivity: MainMenuActivity)\n\n    fun inject(qrCodeTabsActivity: QRCodeTabsActivity)\n\n    fun inject(showQRCodeFragment: ShowQRCodeFragment)\n\n    fun inject(sendFormsTaskSpec: SendFormsTaskSpec)\n\n    fun inject(adminPasswordDialogFragment: AdminPasswordDialogFragment)\n\n    fun inject(formManagementPreferencesFragment: FormManagementPreferencesFragment)\n\n    fun inject(identityPreferencesFragment: IdentityPreferencesFragment)\n\n    fun inject(userInterfacePreferencesFragment: UserInterfacePreferencesFragment)\n\n    fun inject(saveFormProgressDialogFragment: SaveFormProgressDialogFragment)\n\n    fun inject(barCodeScannerFragment: BarCodeScannerFragment)\n\n    fun inject(qrCodeScannerFragment: QRCodeScannerFragment)\n\n    fun inject(projectPreferencesActivity: ProjectPreferencesActivity)\n\n    fun inject(resetDialogPreferenceFragmentCompat: ResetDialogPreferenceFragmentCompat)\n\n    fun inject(syncWork: SyncFormsTaskSpec)\n\n    fun inject(experimentalPreferencesFragment: ExperimentalPreferencesFragment)\n\n    fun inject(autoUpdateTaskSpec: AutoUpdateTaskSpec)\n\n    fun inject(serverAuthDialogFragment: ServerAuthDialogFragment)\n\n    fun inject(basePreferencesFragment: BasePreferencesFragment)\n\n    fun inject(instanceUploaderActivity: InstanceUploaderActivity)\n\n    fun inject(projectPreferencesFragment: ProjectPreferencesFragment)\n\n    fun inject(deleteFormsActivity: DeleteFormsActivity)\n\n    fun inject(selectMinimalDialog: SelectMinimalDialog)\n\n    fun inject(audioRecordingControllerFragment: AudioRecordingControllerFragment)\n\n    fun inject(saveAnswerFileErrorDialogFragment: SaveAnswerFileErrorDialogFragment)\n\n    fun inject(audioRecordingErrorDialogFragment: AudioRecordingErrorDialogFragment)\n\n    fun inject(instanceChooserList: InstanceChooserList)\n\n    fun inject(formsProvider: FormsProvider)\n\n    fun inject(instanceProvider: InstanceProvider)\n\n    fun inject(backgroundAudioPermissionDialogFragment: BackgroundAudioPermissionDialogFragment)\n\n    fun inject(changeAdminPasswordDialog: ChangeAdminPasswordDialog)\n\n    fun inject(mediaLoadingTask: MediaLoadingTask)\n\n    fun inject(themeUtils: ThemeUtils)\n\n    fun inject(baseProjectPreferencesFragment: BaseProjectPreferencesFragment)\n\n    fun inject(androidShortcutsActivity: AndroidShortcutsActivity)\n\n    fun inject(projectSettingsDialog: ProjectSettingsDialog)\n\n    fun inject(manualProjectCreatorDialog: ManualProjectCreatorDialog)\n\n    fun inject(qrCodeProjectCreatorDialog: QrCodeProjectCreatorDialog)\n\n    fun inject(firstLaunchActivity: FirstLaunchActivity)\n\n    fun inject(formUriActivity: FormUriActivity)\n\n    fun inject(mapsPreferencesFragment: MapsPreferencesFragment)\n\n    fun inject(formsDownloadResultDialog: FormsDownloadResultDialog)\n\n    fun inject(selectOneFromMapDialogFragment: SelectOneFromMapDialogFragment)\n\n    fun inject(drawActivity: DrawActivity)\n\n    fun inject(blankFormListActivity: BlankFormListActivity)\n\n    fun inject(deleteRepeatDialogFragment: DeleteRepeatDialogFragment)\n\n    fun inject(appListActivity: AppListActivity)\n\n    fun inject(downloadFormListTask: DownloadFormListTask)\n\n    fun inject(formHierarchyFragmentHostActivity: FormHierarchyFragmentHostActivity)\n\n    fun referenceManager(): ReferenceManager\n\n    fun settingsProvider(): SettingsProvider\n\n    fun applicationInitializer(): ApplicationInitializer\n\n    fun settingsImporter(): ODKAppSettingsImporter\n\n    fun projectsRepository(): ProjectsRepository\n\n    fun projectCreator(): ProjectCreator\n\n    fun currentProjectProvider(): ProjectsDataService\n\n    fun storagePathProvider(): StoragePathProvider\n\n    fun formsRepositoryProvider(): FormsRepositoryProvider\n\n    fun instancesRepositoryProvider(): InstancesRepositoryProvider\n\n    fun savepointsRepositoryProvider(): SavepointsRepositoryProvider\n\n    fun formSourceProvider(): OpenRosaClientProvider\n\n    fun existingProjectMigrator(): ExistingProjectMigrator\n\n    fun projectResetter(): ProjectResetter\n\n    fun mapFragmentFactory(): MapFragmentFactory\n\n    fun scheduler(): Scheduler\n\n    fun locationClient(): LocationClient\n\n    fun permissionsProvider(): PermissionsProvider\n\n    fun permissionsChecker(): PermissionsChecker\n\n    fun referenceLayerRepository(): ReferenceLayerRepository\n\n    fun networkStateProvider(): NetworkStateProvider\n\n    fun entitiesRepositoryProvider(): EntitiesRepositoryProvider\n\n    fun formsDataService(): FormsDataService\n\n    fun instancesDataService(): InstancesDataService\n\n    fun projectDependencyModuleFactory(): ProjectDependencyModuleFactory\n\n    fun webPageService(): WebPageService\n\n    fun uniqueIdGenerator(): UniqueIdGenerator\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java",
    "content": "package org.odk.collect.android.injection.config;\n\nimport static androidx.core.content.FileProvider.getUriForFile;\nimport static org.odk.collect.androidshared.data.AppStateKt.getState;\nimport static org.odk.collect.settings.keys.MetaKeys.KEY_INSTALL_ID;\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.singletonList;\n\nimport android.app.Application;\nimport android.content.Context;\nimport android.content.RestrictionsManager;\nimport android.webkit.MimeTypeMap;\n\nimport androidx.work.WorkManager;\n\nimport com.google.gson.Gson;\n\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport org.odk.collect.analytics.Analytics;\nimport org.odk.collect.analytics.BlockableFirebaseAnalytics;\nimport org.odk.collect.analytics.NoopAnalytics;\nimport org.odk.collect.android.BuildConfig;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.application.CollectSettingsChangeHandler;\nimport org.odk.collect.android.application.MapboxClassInstanceCreator;\nimport org.odk.collect.android.application.initialization.AnalyticsInitializer;\nimport org.odk.collect.android.application.initialization.ApplicationInitializer;\nimport org.odk.collect.android.application.initialization.CachedFormsCleaner;\nimport org.odk.collect.android.application.initialization.ExistingProjectMigrator;\nimport org.odk.collect.android.application.initialization.ExistingSettingsMigrator;\nimport org.odk.collect.android.application.initialization.GoogleDriveProjectsDeleter;\nimport org.odk.collect.android.application.initialization.MapsInitializer;\nimport org.odk.collect.android.application.initialization.SavepointsImporter;\nimport org.odk.collect.android.application.initialization.ScheduledWorkUpgrade;\nimport org.odk.collect.android.application.initialization.upgrade.UpgradeInitializer;\nimport org.odk.collect.android.backgroundwork.FormUpdateAndInstanceSubmitScheduler;\nimport org.odk.collect.android.backgroundwork.FormUpdateScheduler;\nimport org.odk.collect.android.backgroundwork.InstanceSubmitScheduler;\nimport org.odk.collect.android.configure.qr.AppConfigurationGenerator;\nimport org.odk.collect.android.configure.qr.CachingQRCodeGenerator;\nimport org.odk.collect.android.configure.qr.QRCodeGenerator;\nimport org.odk.collect.android.configure.qr.SettingsBarcodeScannerViewFactory;\nimport org.odk.collect.android.database.itemsets.DatabaseFastExternalItemsetsRepository;\nimport org.odk.collect.android.entities.EntitiesRepositoryProvider;\nimport org.odk.collect.android.external.InstancesContract;\nimport org.odk.collect.android.formentry.AppStateFormSessionRepository;\nimport org.odk.collect.android.formentry.FormSessionRepository;\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListViewModel;\nimport org.odk.collect.android.formmanagement.CollectFormEntryControllerFactory;\nimport org.odk.collect.android.formmanagement.FormsDataService;\nimport org.odk.collect.android.formmanagement.OpenRosaClientProvider;\nimport org.odk.collect.android.geo.MapConfiguratorProvider;\nimport org.odk.collect.android.geo.MapFragmentFactoryImpl;\nimport org.odk.collect.android.instancemanagement.InstancesDataService;\nimport org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider;\nimport org.odk.collect.android.instancemanagement.send.ReadyToSendViewModel;\nimport org.odk.collect.android.itemsets.FastExternalItemsetsRepository;\nimport org.odk.collect.android.mainmenu.MainMenuViewModelFactory;\nimport org.odk.collect.android.notifications.NotificationManagerNotifier;\nimport org.odk.collect.android.notifications.Notifier;\nimport org.odk.collect.android.preferences.Defaults;\nimport org.odk.collect.android.preferences.PreferenceVisibilityHandler;\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel;\nimport org.odk.collect.android.preferences.source.SharedPreferencesSettingsProvider;\nimport org.odk.collect.android.projects.ProjectCreatorImpl;\nimport org.odk.collect.android.projects.ProjectDeleter;\nimport org.odk.collect.android.projects.ProjectResetter;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.android.projects.SettingsConnectionMatcherImpl;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.tasks.FormLoaderTask;\nimport org.odk.collect.android.utilities.AdminPasswordProvider;\nimport org.odk.collect.android.utilities.AndroidUserAgent;\nimport org.odk.collect.android.utilities.ChangeLockProvider;\nimport org.odk.collect.android.utilities.ContentUriProvider;\nimport org.odk.collect.android.utilities.ExternalAppIntentProvider;\nimport org.odk.collect.android.utilities.FileProvider;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.android.utilities.ImageCompressionController;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.android.utilities.SavepointsRepositoryProvider;\nimport org.odk.collect.android.utilities.SoftKeyboardController;\nimport org.odk.collect.android.utilities.WebCredentialsUtils;\nimport org.odk.collect.android.version.VersionInformation;\nimport org.odk.collect.android.widgets.utilities.ViewModelAudioPlayerFactory;\nimport org.odk.collect.androidshared.bitmap.ImageCompressor;\nimport org.odk.collect.androidshared.system.BroadcastReceiverRegister;\nimport org.odk.collect.androidshared.system.BroadcastReceiverRegisterImpl;\nimport org.odk.collect.androidshared.system.IntentLauncher;\nimport org.odk.collect.androidshared.system.IntentLauncherImpl;\nimport org.odk.collect.androidshared.utils.ScreenUtils;\nimport org.odk.collect.androidshared.utils.SettingsUniqueIdGenerator;\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator;\nimport org.odk.collect.async.coroutines.CoroutineTaskRunner;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.async.SchedulerBuilder;\nimport org.odk.collect.async.services.ForegroundServiceTaskSpecRunner;\nimport org.odk.collect.async.workmanager.WorkManagerTaskSpecScheduler;\nimport org.odk.collect.async.network.ConnectivityProvider;\nimport org.odk.collect.async.network.NetworkStateProvider;\nimport org.odk.collect.audioclips.AudioPlayerFactory;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.recording.AudioRecorderFactory;\nimport org.odk.collect.entities.storage.EntitiesRepository;\nimport org.odk.collect.imageloader.GlideImageLoader;\nimport org.odk.collect.imageloader.ImageLoader;\nimport org.odk.collect.location.GoogleFusedLocationClient;\nimport org.odk.collect.location.LocationClient;\nimport org.odk.collect.location.LocationClientProvider;\nimport org.odk.collect.maps.MapFragmentFactory;\nimport org.odk.collect.maps.layers.DirectoryReferenceLayerRepository;\nimport org.odk.collect.maps.layers.ReferenceLayerRepository;\nimport org.odk.collect.metadata.InstallIDProvider;\nimport org.odk.collect.metadata.PropertyManager;\nimport org.odk.collect.metadata.SettingsInstallIDProvider;\nimport org.odk.collect.mobiledevicemanagement.MDMConfigHandler;\nimport org.odk.collect.mobiledevicemanagement.MDMConfigHandlerImpl;\nimport org.odk.collect.mobiledevicemanagement.MDMConfigObserver;\nimport org.odk.collect.openrosa.http.CollectThenSystemContentTypeMapper;\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface;\nimport org.odk.collect.openrosa.http.okhttp.OkHttpConnection;\nimport org.odk.collect.permissions.ContextCompatPermissionChecker;\nimport org.odk.collect.permissions.PermissionsChecker;\nimport org.odk.collect.permissions.PermissionsProvider;\nimport org.odk.collect.projects.ProjectCreator;\nimport org.odk.collect.projects.ProjectsRepository;\nimport org.odk.collect.projects.SettingsConnectionMatcher;\nimport org.odk.collect.projects.SharedPreferencesProjectsRepository;\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer;\nimport org.odk.collect.qrcode.zxing.QRCodeCreatorImpl;\nimport org.odk.collect.qrcode.zxing.QRCodeDecoder;\nimport org.odk.collect.qrcode.zxing.QRCodeDecoderImpl;\nimport org.odk.collect.settings.ODKAppSettingsImporter;\nimport org.odk.collect.settings.ODKAppSettingsMigrator;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.importing.ProjectDetailsCreatorImpl;\nimport org.odk.collect.settings.importing.SettingsChangeHandler;\nimport org.odk.collect.settings.keys.AppConfigurationKeys;\nimport org.odk.collect.settings.keys.MetaKeys;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.shared.strings.UUIDGenerator;\nimport org.odk.collect.utilities.UserAgentProvider;\nimport org.odk.collect.webpage.CustomTabsWebPageService;\nimport org.odk.collect.webpage.WebPageService;\n\nimport java.io.File;\n\nimport javax.inject.Named;\nimport javax.inject.Singleton;\n\nimport dagger.Module;\nimport dagger.Provides;\nimport kotlin.Unit;\nimport kotlin.jvm.functions.Function0;\n\n/**\n * Add dependency providers here (annotated with @Provides)\n * for objects you need to inject\n */\n@Module\npublic class AppDependencyModule {\n\n    @Provides\n    Context context(Application application) {\n        return application;\n    }\n\n    @Provides\n    MimeTypeMap provideMimeTypeMap() {\n        return MimeTypeMap.getSingleton();\n    }\n\n    @Provides\n    @Singleton\n    UserAgentProvider providesUserAgent() {\n        return new AndroidUserAgent();\n    }\n\n    @Provides\n    @Singleton\n    public OpenRosaHttpInterface provideHttpInterface(MimeTypeMap mimeTypeMap, UserAgentProvider userAgentProvider, Application application, VersionInformation versionInformation) {\n        String cacheDir = application.getCacheDir().getAbsolutePath();\n        return new OkHttpConnection(\n                cacheDir,\n                new CollectThenSystemContentTypeMapper(mimeTypeMap),\n                userAgentProvider.getUserAgent()\n        );\n    }\n\n    @Provides\n    WebCredentialsUtils provideWebCredentials(SettingsProvider settingsProvider) {\n        return new WebCredentialsUtils(settingsProvider.getUnprotectedSettings());\n    }\n\n    @Provides\n    @Singleton\n    public Analytics providesAnalytics(Application application) {\n        try {\n            return new BlockableFirebaseAnalytics(application);\n        } catch (IllegalStateException e) {\n            // Couldn't setup Firebase so use no-op instance\n            return new NoopAnalytics();\n        }\n    }\n\n    @Provides\n    public PermissionsProvider providesPermissionsProvider(PermissionsChecker permissionsChecker) {\n        return new PermissionsProvider(permissionsChecker);\n    }\n\n    @Provides\n    public ReferenceManager providesReferenceManager() {\n        return ReferenceManager.instance();\n    }\n\n    @Provides\n    @Singleton\n    public SettingsProvider providesSettingsProvider(Context context) {\n        return new SharedPreferencesSettingsProvider(context);\n    }\n\n\n    @Provides\n    public InstallIDProvider providesInstallIDProvider(SettingsProvider settingsProvider) {\n        return new SettingsInstallIDProvider(settingsProvider.getMetaSettings(), KEY_INSTALL_ID);\n    }\n\n    @Provides\n    public StoragePathProvider providesStoragePathProvider(Context context, ProjectsDataService projectsDataService, ProjectsRepository projectsRepository) {\n        File externalFilesDir = context.getExternalFilesDir(null);\n\n        if (externalFilesDir != null) {\n            return new StoragePathProvider(projectsDataService, projectsRepository, externalFilesDir.getAbsolutePath());\n        } else {\n            throw new IllegalStateException(\"Storage is not available!\");\n        }\n    }\n\n    @Provides\n    public AdminPasswordProvider providesAdminPasswordProvider(SettingsProvider settingsProvider) {\n        return new AdminPasswordProvider(settingsProvider.getProtectedSettings());\n    }\n\n    @Provides\n    public FormUpdateScheduler providesFormUpdateManger(Scheduler scheduler, SettingsProvider settingsProvider, Application application) {\n        return new FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application);\n    }\n\n    @Provides\n    public InstanceSubmitScheduler providesFormSubmitManager(Scheduler scheduler, SettingsProvider settingsProvider, Application application) {\n        return new FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application);\n    }\n\n    @Provides\n    public NetworkStateProvider providesNetworkStateProvider(Context context) {\n        return new ConnectivityProvider(context);\n    }\n\n    @Provides\n    public QRCodeGenerator providesQRCodeGenerator() {\n        return new CachingQRCodeGenerator(new QRCodeCreatorImpl());\n    }\n\n    @Provides\n    public VersionInformation providesVersionInformation() {\n        return new VersionInformation(() -> BuildConfig.VERSION_NAME);\n    }\n\n    @Provides\n    public FileProvider providesFileProvider(Context context) {\n        return filePath -> getUriForFile(context, BuildConfig.APPLICATION_ID + \".provider\", new File(filePath));\n    }\n\n    @Provides\n    public WorkManager providesWorkManager(Context context) {\n        return WorkManager.getInstance(context);\n    }\n\n    @Provides\n    public Scheduler providesScheduler(WorkManager workManager, Application application) {\n        return SchedulerBuilder.build(\n                new CoroutineTaskRunner(),\n                new ForegroundServiceTaskSpecRunner(application),\n                new WorkManagerTaskSpecScheduler(workManager)\n        );\n    }\n\n    @Provides\n    public ODKAppSettingsMigrator providesPreferenceMigrator(SettingsProvider settingsProvider) {\n        return new ODKAppSettingsMigrator(settingsProvider.getMetaSettings());\n    }\n\n    @Provides\n    @Singleton\n    public PropertyManager providesPropertyManager(InstallIDProvider installIDProvider, SettingsProvider settingsProvider) {\n        return new PropertyManager(installIDProvider, settingsProvider);\n    }\n\n    @Provides\n    public SettingsChangeHandler providesSettingsChangeHandler(PropertyManager propertyManager, FormUpdateScheduler formUpdateScheduler, FormsDataService formsDataService) {\n        return new CollectSettingsChangeHandler(propertyManager, formUpdateScheduler, formsDataService);\n    }\n\n    @Provides\n    public ODKAppSettingsImporter providesODKAppSettingsImporter(Context context, ProjectsRepository projectsRepository, SettingsProvider settingsProvider, SettingsChangeHandler settingsChangeHandler) {\n        JSONObject deviceUnsupportedSettings = new JSONObject();\n        if (!MapboxClassInstanceCreator.isMapboxAvailable()) {\n            try {\n                deviceUnsupportedSettings.put(\n                        AppConfigurationKeys.GENERAL,\n                        new JSONObject().put(ProjectKeys.KEY_BASEMAP_SOURCE, new JSONArray(singletonList(ProjectKeys.BASEMAP_SOURCE_MAPBOX)))\n                );\n            } catch (Throwable ignored) {\n                // ignore\n            }\n        }\n\n        return new ODKAppSettingsImporter(\n                projectsRepository,\n                settingsProvider,\n                Defaults.getUnprotected(),\n                Defaults.getProtected(),\n                asList(context.getResources().getStringArray(R.array.project_colors)),\n                settingsChangeHandler,\n                deviceUnsupportedSettings\n        );\n    }\n\n    @Provides\n    public QRCodeDecoder providesQRCodeDecoder() {\n        return new QRCodeDecoderImpl();\n    }\n\n    @Provides\n    public Notifier providesNotifier(Application application, SettingsProvider settingsProvider, ProjectsRepository projectsRepository, UniqueIdGenerator uniqueIdGenerator) {\n        return new NotificationManagerNotifier(application, settingsProvider, projectsRepository, uniqueIdGenerator);\n    }\n\n    @Provides\n    @Singleton\n    public ChangeLockProvider providesChangeLockProvider() {\n        return new ChangeLockProvider();\n    }\n\n    @Provides\n    ScreenUtils providesScreenUtils(Context context) {\n        return new ScreenUtils(context);\n    }\n\n    @Provides\n    public AudioRecorder providesAudioRecorder(Application application) {\n        return new AudioRecorderFactory(application).create();\n    }\n\n    @Provides\n    public EntitiesRepositoryProvider provideEntitiesRepositoryProvider(Context context, StoragePathProvider storagePathProvider) {\n        return new EntitiesRepositoryProvider(context, storagePathProvider);\n    }\n\n    @Provides\n    public SoftKeyboardController provideSoftKeyboardController() {\n        return SoftKeyboardController.INSTANCE;\n    }\n\n    @Provides\n    public AppConfigurationGenerator providesJsonPreferencesGenerator(SettingsProvider settingsProvider, ProjectsDataService projectsDataService) {\n        return new AppConfigurationGenerator(settingsProvider, projectsDataService);\n    }\n\n    @Provides\n    @Singleton\n    public PermissionsChecker providesPermissionsChecker(Context context) {\n        return new ContextCompatPermissionChecker(context);\n    }\n\n    @Provides\n    @Singleton\n    public ExternalAppIntentProvider providesExternalAppIntentProvider() {\n        return new ExternalAppIntentProvider();\n    }\n\n    @Provides\n    public FormSessionRepository providesFormSessionStore(Application application) {\n        return new AppStateFormSessionRepository(application);\n    }\n\n    @Provides\n    public WebPageService providesWebPageService() {\n        return CustomTabsWebPageService.INSTANCE;\n    }\n\n    @Provides\n    @Singleton\n    public ProjectsRepository providesProjectsRepository(UUIDGenerator uuidGenerator, Gson gson, SettingsProvider settingsProvider) {\n        return new SharedPreferencesProjectsRepository(uuidGenerator, gson, settingsProvider.getMetaSettings(), MetaKeys.KEY_PROJECTS);\n    }\n\n    @Provides\n    public ProjectCreator providesProjectCreator(ProjectsRepository projectsRepository, ProjectsDataService projectsDataService,\n                                                 ODKAppSettingsImporter settingsImporter, SettingsProvider settingsProvider) {\n        return new ProjectCreatorImpl(projectsRepository, projectsDataService, settingsImporter, settingsProvider);\n    }\n\n    @Provides\n    public Gson providesGson() {\n        return new Gson();\n    }\n\n    @Provides\n    @Singleton\n    public UUIDGenerator providesUUIDGenerator() {\n        return new UUIDGenerator();\n    }\n\n    @Provides\n    public InstancesDataService providesInstancesDataService(Application application, ProjectsDataService projectsDataService, InstanceSubmitScheduler instanceSubmitScheduler, ProjectDependencyModuleFactory projectsDependencyProviderFactory, Notifier notifier, PropertyManager propertyManager, OpenRosaHttpInterface httpInterface) {\n        Function0<Unit> onUpdate = () -> {\n            application.getContentResolver().notifyChange(\n                    InstancesContract.getUri(projectsDataService.requireCurrentProject().getUuid()),\n                    null\n            );\n\n            return null;\n        };\n\n        return new InstancesDataService(getState(application), instanceSubmitScheduler, projectsDependencyProviderFactory, notifier, propertyManager, httpInterface, onUpdate);\n    }\n\n    @Provides\n    public FastExternalItemsetsRepository providesItemsetsRepository() {\n        return new DatabaseFastExternalItemsetsRepository();\n    }\n\n    @Provides\n    public ProjectsDataService providesCurrentProjectProvider(Application application, SettingsProvider settingsProvider, ProjectsRepository projectsRepository, AnalyticsInitializer analyticsInitializer, Context context, MapsInitializer mapsInitializer) {\n        return new ProjectsDataService(getState(application), settingsProvider, projectsRepository, analyticsInitializer, mapsInitializer);\n    }\n\n    @Provides\n    public FormsRepositoryProvider providesFormsRepositoryProvider(Application application) {\n        return new FormsRepositoryProvider(application);\n    }\n\n    @Provides\n    public InstancesRepositoryProvider providesInstancesRepositoryProvider(Context context, StoragePathProvider storagePathProvider) {\n        return new InstancesRepositoryProvider(context, storagePathProvider);\n    }\n\n    @Provides\n    public SavepointsRepositoryProvider providesSavepointsRepositoryProvider(Context context, StoragePathProvider storagePathProvider) {\n        return new SavepointsRepositoryProvider(context, storagePathProvider);\n    }\n\n    @Provides\n    public ProjectPreferencesViewModel.Factory providesProjectPreferencesViewModel(AdminPasswordProvider adminPasswordProvider) {\n        return new ProjectPreferencesViewModel.Factory(adminPasswordProvider);\n    }\n\n    @Provides\n    public ReadyToSendViewModel.Factory providesReadyToSendViewModel(InstancesRepositoryProvider instancesRepositoryProvider, Scheduler scheduler) {\n        return new ReadyToSendViewModel.Factory(instancesRepositoryProvider.create(), scheduler, System::currentTimeMillis);\n    }\n\n    @Provides\n    public MainMenuViewModelFactory providesMainMenuViewModelFactory(VersionInformation versionInformation, Application application,\n                                                                     SettingsProvider settingsProvider, InstancesDataService instancesDataService,\n                                                                     Scheduler scheduler, ProjectsDataService projectsDataService,\n                                                                     AnalyticsInitializer analyticsInitializer, PermissionsChecker permissionChecker,\n                                                                     FormsRepositoryProvider formsRepositoryProvider, InstancesRepositoryProvider instancesRepositoryProvider,\n                                                                     AutoSendSettingsProvider autoSendSettingsProvider) {\n        return new MainMenuViewModelFactory(versionInformation, application, settingsProvider, instancesDataService, scheduler, projectsDataService, permissionChecker, formsRepositoryProvider, instancesRepositoryProvider, autoSendSettingsProvider);\n    }\n\n    @Provides\n    public AnalyticsInitializer providesAnalyticsInitializer(Analytics analytics, VersionInformation versionInformation, SettingsProvider settingsProvider) {\n        return new AnalyticsInitializer(analytics, versionInformation, settingsProvider);\n    }\n\n    @Provides\n    public OpenRosaClientProvider providesFormSourceProvider(SettingsProvider settingsProvider, OpenRosaHttpInterface openRosaHttpInterface) {\n        return new OpenRosaClientProvider(settingsProvider::getUnprotectedSettings, openRosaHttpInterface);\n    }\n\n    @Provides\n    public FormsDataService providesFormsUpdater(Application application, Notifier notifier, ProjectDependencyModuleFactory projectDependencyModuleFactory) {\n        return new FormsDataService(getState(application), notifier, projectDependencyModuleFactory, System::currentTimeMillis);\n    }\n\n    @Provides\n    public AutoSendSettingsProvider providesAutoSendSettingsProvider(Application application, SettingsProvider settingsProvider, NetworkStateProvider networkStateProvider) {\n        return new AutoSendSettingsProvider(application, networkStateProvider, settingsProvider);\n    }\n\n    @Provides\n    public ExistingProjectMigrator providesExistingProjectMigrator(Context context, StoragePathProvider storagePathProvider, ProjectsRepository projectsRepository, SettingsProvider settingsProvider, ProjectsDataService projectsDataService) {\n        return new ExistingProjectMigrator(context, storagePathProvider, projectsRepository, settingsProvider, projectsDataService, new ProjectDetailsCreatorImpl(asList(context.getResources().getStringArray(R.array.project_colors)), Defaults.getUnprotected()));\n    }\n\n    @Provides\n    public ScheduledWorkUpgrade providesFormUpdatesUpgrader(Scheduler scheduler, ProjectsRepository projectsRepository, FormUpdateScheduler formUpdateScheduler, InstanceSubmitScheduler instanceSubmitScheduler) {\n        return new ScheduledWorkUpgrade(scheduler, projectsRepository, formUpdateScheduler, instanceSubmitScheduler);\n    }\n\n    @Provides\n    public ExistingSettingsMigrator providesExistingSettingsMigrator(ProjectsRepository projectsRepository, SettingsProvider settingsProvider, ODKAppSettingsMigrator settingsMigrator) {\n        return new ExistingSettingsMigrator(projectsRepository, settingsProvider, settingsMigrator);\n    }\n\n    @Provides\n    public GoogleDriveProjectsDeleter providesGoogleDriveProjectsDeleter(ProjectsRepository projectsRepository, SettingsProvider settingsProvider, ProjectDeleter projectDeleter) {\n        return new GoogleDriveProjectsDeleter(projectsRepository, settingsProvider, projectDeleter);\n    }\n\n    @Provides\n    public UpgradeInitializer providesUpgradeInitializer(Context context, SettingsProvider settingsProvider, ExistingProjectMigrator existingProjectMigrator, ExistingSettingsMigrator existingSettingsMigrator, ScheduledWorkUpgrade scheduledWorkUpgrade, GoogleDriveProjectsDeleter googleDriveProjectsDeleter, ProjectsRepository projectsRepository, ProjectDependencyModuleFactory projectDependencyModuleFactory) {\n        return new UpgradeInitializer(\n                context,\n                settingsProvider,\n                existingProjectMigrator,\n                existingSettingsMigrator,\n                scheduledWorkUpgrade,\n                googleDriveProjectsDeleter,\n                new SavepointsImporter(projectsRepository, projectDependencyModuleFactory),\n                new CachedFormsCleaner(projectsRepository, projectDependencyModuleFactory)\n        );\n    }\n\n    @Provides\n    public ApplicationInitializer providesApplicationInitializer(Application context, PropertyManager propertyManager, Analytics analytics, UpgradeInitializer upgradeInitializer, AnalyticsInitializer analyticsInitializer, ProjectsRepository projectsRepository, SettingsProvider settingsProvider, MapsInitializer mapsInitializer, EntitiesRepositoryProvider entitiesRepositoryProvider, ProjectsDataService projectsDataService, Scheduler scheduler, InstancesRepositoryProvider instancesRepositoryProvider, FormsRepositoryProvider formsRepositoryProvider) {\n        return new ApplicationInitializer(context, propertyManager, analytics, upgradeInitializer, analyticsInitializer, mapsInitializer, projectsRepository, settingsProvider, entitiesRepositoryProvider, projectsDataService, scheduler, instancesRepositoryProvider, formsRepositoryProvider);\n    }\n\n    @Provides\n    public ProjectDeleter providesProjectDeleter(ProjectsRepository projectsRepository, ProjectsDataService projectsDataService, FormUpdateScheduler formUpdateScheduler, InstanceSubmitScheduler instanceSubmitScheduler, StoragePathProvider storagePathProvider, SettingsProvider settingsProvider) {\n        return new ProjectDeleter(projectsRepository, projectsDataService, formUpdateScheduler, instanceSubmitScheduler, storagePathProvider, settingsProvider);\n    }\n\n    @Provides\n    public ProjectResetter providesProjectResetter(StoragePathProvider storagePathProvider, PropertyManager propertyManager, SettingsProvider settingsProvider, FormsRepositoryProvider formsRepositoryProvider, SavepointsRepositoryProvider savepointsRepositoryProvider, InstancesDataService instancesDataService, ProjectsDataService projectsDataService) {\n        return new ProjectResetter(storagePathProvider, propertyManager, settingsProvider, formsRepositoryProvider, savepointsRepositoryProvider, instancesDataService, projectsDataService.requireCurrentProject().getUuid());\n    }\n\n    @Provides\n    public PreferenceVisibilityHandler providesDisabledPreferencesRemover(SettingsProvider settingsProvider, VersionInformation versionInformation) {\n        return new PreferenceVisibilityHandler(settingsProvider, versionInformation);\n    }\n\n    @Provides\n    public ReferenceLayerRepository providesReferenceLayerRepository(StoragePathProvider storagePathProvider, SettingsProvider settingsProvider) {\n        return new DirectoryReferenceLayerRepository(\n                storagePathProvider.getOdkDirPath(StorageSubdirectory.SHARED_LAYERS),\n                storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS),\n                () -> MapConfiguratorProvider.getConfigurator(\n                        settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE)\n                )\n        );\n    }\n\n    @Provides\n    public IntentLauncher providesIntentLauncher() {\n        return IntentLauncherImpl.INSTANCE;\n    }\n\n    @Provides\n    public LocationClient providesLocationClient(Application application) {\n        return LocationClientProvider.getClient(application);\n    }\n\n    @Provides\n    @Named(\"fused\")\n    public LocationClient providesFusedLocationClient(Application application) {\n        return new GoogleFusedLocationClient(application);\n    }\n\n    @Provides\n    public MediaUtils providesMediaUtils(IntentLauncher intentLauncher) {\n        return new MediaUtils(intentLauncher, new ContentUriProvider());\n    }\n\n    @Provides\n    public MapFragmentFactory providesMapFragmentFactory(SettingsProvider settingsProvider) {\n        return new MapFragmentFactoryImpl(settingsProvider);\n    }\n\n    @Provides\n    public ImageLoader providesImageLoader() {\n        return new GlideImageLoader();\n    }\n\n    @Provides\n    public BlankFormListViewModel.Factory providesBlankFormListViewModel(InstancesRepositoryProvider instancesRepositoryProvider, Application application, FormsDataService formsDataService, Scheduler scheduler, SettingsProvider settingsProvider, ProjectsDataService projectsDataService, UniqueIdGenerator uniqueIdGenerator) {\n        return new BlankFormListViewModel.Factory(instancesRepositoryProvider.create(), application, formsDataService, scheduler, settingsProvider.getUnprotectedSettings(), projectsDataService.requireCurrentProject().getUuid(), uniqueIdGenerator);\n    }\n\n    @Provides\n    @Singleton\n    public ImageCompressionController providesImageCompressorManager() {\n        return new ImageCompressionController(ImageCompressor.INSTANCE);\n    }\n\n    @Provides\n    public FormLoaderTask.FormEntryControllerFactory formEntryControllerFactory(ProjectsDataService projectsDataService, EntitiesRepositoryProvider entitiesRepositoryProvider, SettingsProvider settingsProvider) {\n        String projectId = projectsDataService.requireCurrentProject().getUuid();\n        EntitiesRepository entitiesRepository = entitiesRepositoryProvider.create(projectId);\n        return new CollectFormEntryControllerFactory(entitiesRepository, settingsProvider.getUnprotectedSettings(projectId));\n    }\n\n    @Provides\n    public BroadcastReceiverRegister providesBroadcastReceiverRegister(Context context) {\n        return new BroadcastReceiverRegisterImpl(context);\n    }\n\n    @Provides\n    public RestrictionsManager providesRestrictionsManager(Context context) {\n        return (RestrictionsManager) context.getSystemService(Context.RESTRICTIONS_SERVICE);\n    }\n\n    @Provides\n    public MDMConfigObserver providesMDMConfigObserver(\n            Scheduler scheduler,\n            SettingsProvider settingsProvider,\n            ProjectsRepository projectsRepository,\n            ProjectCreator projectCreator,\n            ODKAppSettingsImporter settingsImporter,\n            BroadcastReceiverRegister broadcastReceiverRegister,\n            RestrictionsManager restrictionsManager\n    ) {\n        SettingsConnectionMatcher settingsConnectionMatcher = new SettingsConnectionMatcherImpl(\n                projectsRepository,\n                settingsProvider\n        );\n\n        MDMConfigHandler mdmConfigHandler = new MDMConfigHandlerImpl(\n                settingsProvider,\n                projectsRepository,\n                projectCreator,\n                settingsImporter,\n                settingsConnectionMatcher\n        );\n\n        return new MDMConfigObserver(\n                scheduler,\n                mdmConfigHandler,\n                broadcastReceiverRegister,\n                restrictionsManager\n        );\n    }\n\n    @Provides\n    public BarcodeScannerViewContainer.Factory providesBarcodeScannerViewFactory(SettingsProvider settingsProvider) {\n        return new SettingsBarcodeScannerViewFactory(settingsProvider.getUnprotectedSettings());\n    }\n\n    @Provides\n    public AudioPlayerFactory providesAudioPlayerFactory(Scheduler scheduler) {\n        return new ViewModelAudioPlayerFactory(scheduler);\n    }\n\n    @Provides\n    @Singleton\n    public UniqueIdGenerator providesUniqueIdGenerator(SettingsProvider settingsProvider) {\n        return new SettingsUniqueIdGenerator(settingsProvider.getMetaSettings());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/config/CollectDrawDependencyModule.kt",
    "content": "package org.odk.collect.android.injection.config\n\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.draw.DrawDependencyModule\nimport org.odk.collect.settings.SettingsProvider\n\nclass CollectDrawDependencyModule(\n    private val applicationComponent: AppDependencyComponent\n) : DrawDependencyModule() {\n    override fun providesScheduler(): Scheduler {\n        return applicationComponent.scheduler()\n    }\n\n    override fun providesSettingsProvider(): SettingsProvider {\n        return applicationComponent.settingsProvider()\n    }\n\n    override fun providesImagePath(): String {\n        return applicationComponent.storagePathProvider().getTmpImageFilePath()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/config/CollectEntitiesDependencyModule.kt",
    "content": "package org.odk.collect.android.injection.config\n\nimport org.odk.collect.android.application.CollectComposeThemeProvider\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.entities.EntitiesDependencyModule\nimport org.odk.collect.entities.storage.EntitiesRepository\n\nclass CollectEntitiesDependencyModule(private val appDependencyComponent: AppDependencyComponent) : EntitiesDependencyModule() {\n    override fun providesEntitiesRepository(): EntitiesRepository {\n        val projectId: String =\n            appDependencyComponent.currentProjectProvider().requireCurrentProject().uuid\n        return appDependencyComponent.entitiesRepositoryProvider().create(projectId)\n    }\n\n    override fun providesScheduler(): Scheduler {\n        return appDependencyComponent.scheduler()\n    }\n\n    override fun providesComposeThemeProvider(): ComposeThemeProvider {\n        return object : CollectComposeThemeProvider {}\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGeoDependencyModule.kt",
    "content": "package org.odk.collect.android.injection.config\n\nimport android.app.Application\nimport android.content.Context\nimport android.location.LocationManager\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.geo.GeoDependencyModule\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.location.satellites.GpsStatusSatelliteInfoClient\nimport org.odk.collect.location.satellites.SatelliteInfoClient\nimport org.odk.collect.location.tracker.ForegroundServiceLocationTracker\nimport org.odk.collect.location.tracker.LocationTracker\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.webpage.WebPageService\n\nclass CollectGeoDependencyModule(\n    private val appDependencyComponent: AppDependencyComponent\n) : GeoDependencyModule() {\n\n    override fun providesMapFragmentFactory(): MapFragmentFactory {\n        return appDependencyComponent.mapFragmentFactory()\n    }\n\n    override fun providesLocationTracker(application: Application): LocationTracker {\n        return ForegroundServiceLocationTracker(application)\n    }\n\n    override fun providesLocationClient(): LocationClient {\n        return appDependencyComponent.locationClient()\n    }\n\n    override fun providesScheduler(): Scheduler {\n        return appDependencyComponent.scheduler()\n    }\n\n    override fun providesSatelliteInfoClient(context: Context): SatelliteInfoClient {\n        val locationManager =\n            context.getSystemService(Context.LOCATION_SERVICE) as LocationManager\n        return GpsStatusSatelliteInfoClient(locationManager)\n    }\n\n    override fun providesPermissionChecker(context: Context): PermissionsChecker {\n        return appDependencyComponent.permissionsChecker()\n    }\n\n    override fun providesReferenceLayerRepository(): ReferenceLayerRepository {\n        return appDependencyComponent.referenceLayerRepository()\n    }\n\n    override fun providesSettingsProvider(): SettingsProvider {\n        return appDependencyComponent.settingsProvider()\n    }\n\n    override fun providesWebPageService(): WebPageService {\n        return appDependencyComponent.webPageService()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/config/CollectGoogleMapsDependencyModule.kt",
    "content": "package org.odk.collect.android.injection.config\n\nimport org.odk.collect.googlemaps.GoogleMapsDependencyModule\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.settings.SettingsProvider\n\nclass CollectGoogleMapsDependencyModule(\n    private val appDependencyComponent: AppDependencyComponent\n) : GoogleMapsDependencyModule() {\n    override fun providesReferenceLayerRepository(): ReferenceLayerRepository {\n        return appDependencyComponent.referenceLayerRepository()\n    }\n\n    override fun providesLocationClient(): LocationClient {\n        return appDependencyComponent.locationClient()\n    }\n\n    override fun providesSettingsProvider(): SettingsProvider {\n        return appDependencyComponent.settingsProvider()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/config/CollectOsmDroidDependencyModule.kt",
    "content": "package org.odk.collect.android.injection.config\n\nimport org.odk.collect.android.geo.MapConfiguratorProvider\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.maps.MapConfigurator\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.osmdroid.OsmDroidDependencyModule\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\n\nclass CollectOsmDroidDependencyModule(\n    private val appDependencyComponent: AppDependencyComponent\n) : OsmDroidDependencyModule() {\n    override fun providesReferenceLayerRepository(): ReferenceLayerRepository {\n        return appDependencyComponent.referenceLayerRepository()\n    }\n\n    override fun providesLocationClient(): LocationClient {\n        return appDependencyComponent.locationClient()\n    }\n\n    override fun providesMapConfigurator(): MapConfigurator {\n        return MapConfiguratorProvider.getConfigurator(\n            appDependencyComponent.settingsProvider().getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE)\n        )\n    }\n\n    override fun providesSettingsProvider(): SettingsProvider {\n        return appDependencyComponent.settingsProvider()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/config/CollectProjectsDependencyModule.kt",
    "content": "package org.odk.collect.android.injection.config\n\nimport org.odk.collect.projects.ProjectsDependencyModule\nimport org.odk.collect.projects.ProjectsRepository\n\nclass CollectProjectsDependencyModule(\n    private val appDependencyComponent: AppDependencyComponent\n) : ProjectsDependencyModule() {\n    override fun providesProjectsRepository(): ProjectsRepository {\n        return appDependencyComponent.projectsRepository()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/config/CollectSelfieCameraDependencyModule.kt",
    "content": "package org.odk.collect.android.injection.config\n\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.selfiecamera.SelfieCameraDependencyModule\n\nclass CollectSelfieCameraDependencyModule(\n    private val appDependencyComponent: AppDependencyComponent\n) : SelfieCameraDependencyModule() {\n    override fun providesPermissionChecker(): PermissionsChecker {\n        return appDependencyComponent.permissionsChecker()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/injection/config/ProjectDependencyModuleFactory.kt",
    "content": "package org.odk.collect.android.injection.config\n\nimport org.odk.collect.android.entities.EntitiesRepositoryProvider\nimport org.odk.collect.android.formmanagement.OpenRosaClientProvider\nimport org.odk.collect.android.projects.FileDebugLogger\nimport org.odk.collect.android.projects.ProjectDependencyModule\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.storage.StoragePaths\nimport org.odk.collect.android.utilities.ChangeLockProvider\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.utilities.SavepointsRepositoryProvider\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.shared.DebugLogger\nimport java.io.File\nimport javax.inject.Inject\n\nclass ProjectDependencyModuleFactory @Inject constructor(\n    private val settingsProvider: SettingsProvider,\n    private val formsRepositoryProvider: FormsRepositoryProvider,\n    private val instancesRepositoryProvider: InstancesRepositoryProvider,\n    private val storagePathProvider: StoragePathProvider,\n    private val changeLockProvider: ChangeLockProvider,\n    private val openRosaClientProvider: OpenRosaClientProvider,\n    private val savepointsRepositoryProvider: SavepointsRepositoryProvider,\n    private val entitiesRepositoryProvider: EntitiesRepositoryProvider,\n) : ProjectDependencyFactory<ProjectDependencyModule> {\n    override fun create(projectId: String): ProjectDependencyModule {\n        return ProjectDependencyModule(\n            projectId,\n            settingsProvider::getUnprotectedSettings,\n            formsRepositoryProvider,\n            instancesRepositoryProvider,\n            storagePathProvider,\n            changeLockProvider,\n            { openRosaClientProvider.create(projectId) },\n            savepointsRepositoryProvider,\n            entitiesRepositoryProvider,\n            { openRosaClientProvider.create(projectId) },\n            DebugLoggerFactory(storagePathProvider)\n        )\n    }\n}\n\nprivate class DebugLoggerFactory(private val storagePathProvider: ProjectDependencyFactory<StoragePaths>) :\n    ProjectDependencyFactory<DebugLogger> {\n    override fun create(projectId: String): DebugLogger {\n        return FileDebugLogger(File(storagePathProvider.create(projectId).rootDir, \"debug.log\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport android.content.Context\nimport android.view.View\nimport org.odk.collect.androidshared.ui.SnackbarUtils.SnackbarDetails\nimport org.odk.collect.androidshared.ui.SnackbarUtils.SnackbarPresenterObserver\nimport org.odk.collect.strings.R\n\nclass FinalizeAllSnackbarPresenter(parentView: View, private val context: Context) :\n    SnackbarPresenterObserver<FinalizeAllResult>(parentView) {\n\n    override fun getSnackbarDetails(value: FinalizeAllResult): SnackbarDetails {\n        return if (value.unsupportedInstances) {\n            SnackbarDetails(\n                context.getString(\n                    R.string.bulk_finalize_unsupported,\n                    value.successCount\n                )\n            )\n        } else if (value.failureCount == 0) {\n            SnackbarDetails(\n                context.resources.getQuantityString(\n                    R.plurals.bulk_finalize_success,\n                    value.successCount,\n                    value.successCount\n                )\n            )\n        } else if (value.successCount == 0) {\n            SnackbarDetails(\n                context.resources.getQuantityString(\n                    R.plurals.bulk_finalize_failure,\n                    value.failureCount,\n                    value.failureCount\n                )\n            )\n        } else {\n            SnackbarDetails(\n                context.getString(\n                    R.string.bulk_finalize_partial_success,\n                    value.successCount,\n                    value.failureCount\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceDeleter.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.instances.InstancesRepository\n\nclass InstanceDeleter(\n    private val instancesRepository: InstancesRepository,\n    private val formsRepository: FormsRepository\n) {\n    fun delete(ids: Array<Long>) {\n        ids.forEach {\n            delete(it)\n        }\n    }\n\n    fun delete(id: Long?) {\n        instancesRepository[id]?.let { instance ->\n            if (instance.status == Instance.STATUS_SUBMITTED) {\n                instancesRepository.deleteWithLogging(id)\n            } else {\n                instancesRepository.delete(id)\n            }\n            val form =\n                formsRepository.getLatestByFormIdAndVersion(instance.formId, instance.formVersion)\n            if (form != null && form.isDeleted) {\n                val otherInstances = instancesRepository.getAllNotDeletedByFormIdAndVersion(\n                    form.formId,\n                    form.version\n                )\n                if (otherInstances.isEmpty()) {\n                    formsRepository.delete(form.dbId)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceDiskSynchronizer.java",
    "content": "/*\n * Copyright (C) 2017 Nyoman Ribeka\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.instancemanagement;\n\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.net.Uri;\n\nimport org.apache.commons.io.FileUtils;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.exception.EncryptionException;\nimport org.odk.collect.android.external.InstancesContract;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.injection.config.AppDependencyComponent;\nimport org.odk.collect.android.javarosawrapper.InstanceMetadata;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.tasks.SaveFormToDisk;\nimport org.odk.collect.android.utilities.EncryptionUtils;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\n\nimport timber.log.Timber;\n\npublic class InstanceDiskSynchronizer {\n\n    private static int counter;\n\n    private String currentStatus = \"\";\n    private final ProjectsDataService projectsDataService;\n    private final SettingsProvider settingsProvider;\n    private final StoragePathProvider storagePathProvider = new StoragePathProvider();\n    private final InstancesRepository instancesRepository;\n\n    public String getStatusMessage() {\n        return currentStatus;\n    }\n\n    public InstanceDiskSynchronizer(SettingsProvider settingsProvider) {\n        this.settingsProvider = settingsProvider;\n        instancesRepository = new InstancesRepositoryProvider(Collect.getInstance()).create();\n        AppDependencyComponent component = DaggerUtils.getComponent(Collect.getInstance());\n        projectsDataService = component.currentProjectProvider();\n    }\n\n    public String doInBackground() {\n        int currentInstance = ++counter;\n        Timber.i(\"[%d] doInBackground begins!\", currentInstance);\n        try {\n            List<String> instancePaths = new LinkedList<>();\n            File instancesPath = new File(storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES));\n            if (instancesPath.exists() && instancesPath.isDirectory()) {\n                File[] instanceFolders = instancesPath.listFiles();\n                if (instanceFolders == null || instanceFolders.length == 0) {\n                    Timber.i(\"[%d] Empty instance folder. Stopping scan process.\", currentInstance);\n                    Timber.d(\"Instance scan completed\");\n                    return currentStatus;\n                }\n\n                // Build the list of potential path that we need to add to the content provider\n                for (File instanceDir : instanceFolders) {\n                    File instanceFile = new File(instanceDir, instanceDir.getName() + \".xml\");\n                    if (!instanceFile.exists()) {\n                        // Look for submission file that might have been manually copied from e.g. Briefcase\n                        File submissionFile = new File(instanceDir, \"submission.xml\");\n                        if (submissionFile.exists()) {\n                            submissionFile.renameTo(instanceFile);\n                        }\n                    }\n                    if (instanceFile.exists() && instanceFile.canRead()) {\n                        instancePaths.add(instanceFile.getAbsolutePath());\n                    } else {\n                        Timber.i(\"[%d] Ignoring: %s\", currentInstance, instanceDir.getAbsolutePath());\n                    }\n                }\n\n                final boolean instanceSyncFlag = settingsProvider.getUnprotectedSettings().getBoolean(ProjectKeys.KEY_INSTANCE_SYNC);\n\n                int counter = 0;\n                for (String instancePath : instancePaths) {\n                    if (instancesRepository.getOneByPath(instancePath) != null) {\n                        continue; // Skip instances that are already stored in repo\n                    }\n\n                    String instanceFormId = getFormIdFromInstance(instancePath);\n                    // only process if we can find the id from the instance file\n                    if (instanceFormId != null) {\n                        try {\n                            // TODO: optimize this by caching the previously found form definition\n                            // TODO: optimize this by caching unavailable form definition to skip\n                            List<Form> forms = new FormsRepositoryProvider(Collect.getInstance()).create().getAllByFormId(instanceFormId);\n\n                            if (!forms.isEmpty()) {\n                                Form form = forms.get(0);\n                                String jrFormId = form.getFormId();\n                                String jrVersion = form.getVersion();\n                                String formName = form.getDisplayName();\n                                String submissionUri = form.getSubmissionUri();\n\n                                Instance instance = instancesRepository.save(new Instance.Builder()\n                                        .instanceFilePath(instancePath)\n                                        .submissionUri(submissionUri)\n                                        .displayName(formName)\n                                        .formId(jrFormId)\n                                        .formVersion(jrVersion)\n                                        .status(instanceSyncFlag ? Instance.STATUS_COMPLETE : Instance.STATUS_INCOMPLETE)\n                                        .canEditWhenComplete(true)\n                                        .build()\n                                );\n                                counter++;\n\n                                encryptInstanceIfNeeded(form, instance);\n                            }\n                        } catch (IOException | EncryptionException e) {\n                            Timber.w(e);\n                        }\n                    }\n                }\n                if (counter > 0) {\n                    currentStatus += getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.instance_scan_count, counter);\n                }\n            }\n        } finally {\n            Timber.i(\"[%d] doInBackground ends!\", currentInstance);\n        }\n        return currentStatus;\n    }\n\n    private String getFormIdFromInstance(final String instancePath) {\n        String instanceFormId = null;\n        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n        try {\n            DocumentBuilder builder = factory.newDocumentBuilder();\n            Document document = builder.parse(new File(instancePath));\n            Element element = document.getDocumentElement();\n            instanceFormId = element.getAttribute(\"id\");\n        } catch (Exception | Error e) {\n            Timber.w(\"Unable to read form id from %s\", instancePath);\n        }\n        return instanceFormId;\n    }\n\n    private String getInstanceIdFromInstance(final String instancePath) {\n        String instanceId = null;\n        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n        try {\n            DocumentBuilder builder = factory.newDocumentBuilder();\n            Document document = builder.parse(new File(instancePath));\n            Element element = document.getDocumentElement();\n            instanceId = element.getAttribute(\"instanceID\");\n        } catch (Exception | Error e) {\n            Timber.w(\"Unable to read form instanceID from %s\", instancePath);\n        }\n        return instanceId;\n    }\n\n    private void encryptInstanceIfNeeded(Form form, Instance instance) throws EncryptionException, IOException {\n        if (instance != null) {\n            if (shouldInstanceBeEncrypted(form)) {\n                encryptInstance(instance);\n            }\n        }\n    }\n\n    private void encryptInstance(Instance instance) throws EncryptionException, IOException {\n        String instancePath = instance.getInstanceFilePath();\n        File instanceXml = new File(instancePath);\n        if (!new File(instanceXml.getParentFile(), \"submission.xml.enc\").exists()) {\n            Uri uri = InstancesContract.getUri(projectsDataService.requireCurrentProject().getUuid(), instance.getDbId());\n            InstanceMetadata instanceMetadata = new InstanceMetadata(getInstanceIdFromInstance(instancePath), null, null);\n            EncryptionUtils.EncryptedFormInformation formInfo = EncryptionUtils.getEncryptedFormInformation(uri, instanceMetadata);\n\n            if (formInfo != null) {\n                File submissionXml = new File(instanceXml.getParentFile(), \"submission.xml\");\n                FileUtils.copyFile(instanceXml, submissionXml);\n\n                EncryptionUtils.generateEncryptedSubmission(instanceXml, submissionXml, formInfo);\n\n                instancesRepository.save(new Instance.Builder(instance)\n                        .canEditWhenComplete(false)\n                        .geometryType(null)\n                        .geometry(null)\n                        .build()\n                );\n\n                SaveFormToDisk.manageFilesAfterSavingEncryptedForm(instanceXml, submissionXml);\n                if (!EncryptionUtils.deletePlaintextFiles(instanceXml, null)) {\n                    Timber.e(new Error(\"Error deleting plaintext files for \" + instanceXml.getAbsolutePath()));\n                }\n            }\n        }\n    }\n\n    private boolean shouldInstanceBeEncrypted(Form form) {\n        return form.getBASE64RSAPublicKey() != null;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceExt.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport android.content.Context\nimport android.content.res.Resources\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.strings.R\nimport timber.log.Timber\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\nfun Instance.canBeEdited(settingsProvider: SettingsProvider): Boolean {\n    return isDraft() &&\n        settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_EDIT_SAVED)\n}\n\nfun Instance.getStatusDescription(resources: Resources): String {\n    return getStatusDescription(resources, status, Date(lastStatusChangeDate))\n}\n\nfun getStatusDescription(context: Context, state: String?, date: Date): String {\n    return getStatusDescription(context.resources, state, date)\n}\n\nfun Instance.isDraft(): Boolean {\n    return draftStatuses.contains(status)\n}\n\nfun Instance.isEdit(): Boolean {\n    return editOf != null\n}\n\nfun Instance.isDeletable(): Boolean {\n    return canDeleteBeforeSend() || status != Instance.STATUS_COMPLETE && status != Instance.STATUS_SUBMISSION_FAILED\n}\n\nfun Instance.showAsEditable(settingsProvider: SettingsProvider): Boolean {\n    return isDraft() && settingsProvider.getProtectedSettings()\n        .getBoolean(ProtectedProjectKeys.KEY_EDIT_SAVED)\n}\n\nfun Instance.userVisibleInstanceName(\n    resources: Resources = Collect.getInstance().resources\n): String {\n    return if (isEdit()) {\n        resources.getString(R.string.user_visible_instance_name, displayName, editNumber)\n    } else {\n        displayName\n    }\n}\n\nprivate fun getStatusDescription(resources: Resources, state: String?, date: Date): String {\n    return try {\n        if (Instance.STATUS_INCOMPLETE.equals(state, ignoreCase = true) ||\n            Instance.STATUS_INVALID.equals(state, ignoreCase = true) ||\n            Instance.STATUS_VALID.equals(state, ignoreCase = true) ||\n            Instance.STATUS_NEW_EDIT.equals(state, ignoreCase = true)\n        ) {\n            SimpleDateFormat(\n                resources.getString(R.string.saved_on_date_at_time),\n                Locale.getDefault()\n            ).format(date)\n        } else if (Instance.STATUS_COMPLETE.equals(state, ignoreCase = true)) {\n            SimpleDateFormat(\n                resources.getString(R.string.finalized_on_date_at_time),\n                Locale.getDefault()\n            ).format(date)\n        } else if (Instance.STATUS_SUBMITTED.equals(state, ignoreCase = true)) {\n            SimpleDateFormat(\n                resources.getString(R.string.sent_on_date_at_time),\n                Locale.getDefault()\n            ).format(date)\n        } else if (Instance.STATUS_SUBMISSION_FAILED.equals(state, ignoreCase = true)) {\n            SimpleDateFormat(\n                resources.getString(R.string.sending_failed_on_date_at_time),\n                Locale.getDefault()\n            ).format(date)\n        } else {\n            SimpleDateFormat(\n                resources.getString(R.string.added_on_date_at_time),\n                Locale.getDefault()\n            ).format(date)\n        }\n    } catch (e: IllegalArgumentException) {\n        Timber.e(e, \"Current locale: %s\", Locale.getDefault())\n        \"\"\n    }\n}\n\nfun Instance.getIcon(): Int {\n    return getInstanceIcon(this.status)\n}\n\nfun getInstanceIcon(status: String): Int {\n    return when (status) {\n        Instance.STATUS_INCOMPLETE, Instance.STATUS_INVALID, Instance.STATUS_VALID, Instance.STATUS_NEW_EDIT -> org.odk.collect.android.R.drawable.ic_form_state_saved\n        Instance.STATUS_COMPLETE -> org.odk.collect.android.R.drawable.ic_form_state_finalized\n        Instance.STATUS_SUBMITTED -> org.odk.collect.android.R.drawable.ic_form_state_submitted\n        Instance.STATUS_SUBMISSION_FAILED -> org.odk.collect.android.R.drawable.ic_form_state_submission_failed\n        else -> throw IllegalArgumentException()\n    }\n}\n\nprivate val draftStatuses = arrayOf(\n    Instance.STATUS_INCOMPLETE,\n    Instance.STATUS_INVALID,\n    Instance.STATUS_VALID,\n    Instance.STATUS_NEW_EDIT\n)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceListItemView.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport android.content.Context\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.TextView\nimport org.odk.collect.android.R\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.material.ErrorsPill\nimport org.odk.collect.strings.R.string\nimport timber.log.Timber\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\nobject InstanceListItemView {\n\n    private const val DISABLED_ALPHA = 0.38f\n\n    @JvmStatic\n    @Deprecated(\"This should eventually be replaced by a ViewHolder or View implementation\")\n    fun setInstance(view: View, instance: Instance, shouldCheckDisabled: Boolean) {\n        val context = view.context\n\n        val imageView = view.findViewById<ImageView>(R.id.image)\n        setImageFromStatus(imageView, instance)\n        view.findViewById<TextView>(R.id.form_title).text = instance.userVisibleInstanceName(context.resources)\n        setUpSubtext(view, instance, context)\n\n        val pill = view.findViewById<ErrorsPill>(R.id.chip)\n        if (pill != null) {\n            when (instance.status) {\n                Instance.STATUS_INVALID, Instance.STATUS_INCOMPLETE -> pill.errors = true\n                Instance.STATUS_VALID, Instance.STATUS_NEW_EDIT -> pill.errors = false\n                else -> pill.visibility = View.GONE\n            }\n        }\n\n        if (shouldCheckDisabled) {\n            var formExists = false\n            var isFormEncrypted = false\n            val formId = instance.formId\n            val formVersion = instance.formVersion\n            val form = FormsRepositoryProvider(context.applicationContext).create()\n                .getLatestByFormIdAndVersion(formId, formVersion)\n\n            if (form != null) {\n                val base64RSAPublicKey = form.basE64RSAPublicKey\n                formExists = true\n                isFormEncrypted = base64RSAPublicKey != null\n            }\n\n            val date = instance.deletedDate\n            if (date != null || !formExists || isFormEncrypted) {\n                val disabledMessage = if (date != null) {\n                    try {\n                        val deletedTime: String = context.getString(string.deleted_on_date_at_time)\n                        SimpleDateFormat(deletedTime, Locale.getDefault()).format(Date(date))\n                    } catch (e: IllegalArgumentException) {\n                        Timber.e(e)\n                        context.getString(string.submission_deleted)\n                    }\n                } else if (!formExists) {\n                    context.getString(string.deleted_form)\n                } else {\n                    context.getString(string.encrypted_form)\n                }\n\n                setDisabled(view, disabledMessage)\n            } else {\n                setEnabled(view)\n            }\n        }\n    }\n\n    private fun setEnabled(view: View) {\n        val formTitle = view.findViewById<TextView>(R.id.form_title)\n        val formSubtitle = view.findViewById<TextView>(R.id.form_subtitle)\n        val disabledCause = view.findViewById<TextView>(R.id.form_subtitle2)\n        val imageView = view.findViewById<ImageView>(R.id.image)\n        view.isEnabled = true\n        disabledCause.visibility = View.GONE\n        formTitle.alpha = 1f\n        formSubtitle.alpha = 1f\n        disabledCause.alpha = 1f\n        imageView.alpha = 1f\n    }\n\n    private fun setDisabled(view: View, disabledMessage: String) {\n        val formTitle = view.findViewById<TextView>(R.id.form_title)\n        val formSubtitle = view.findViewById<TextView>(R.id.form_subtitle)\n        val disabledCause = view.findViewById<TextView>(R.id.form_subtitle2)\n        val imageView = view.findViewById<ImageView>(R.id.image)\n        view.isEnabled = false\n        disabledCause.visibility = View.VISIBLE\n        disabledCause.text = disabledMessage\n\n        // Material design \"disabled\" opacity is 38%.\n        formTitle.alpha = DISABLED_ALPHA\n        formSubtitle.alpha = DISABLED_ALPHA\n        disabledCause.alpha = DISABLED_ALPHA\n        imageView.alpha = DISABLED_ALPHA\n    }\n\n    private fun setUpSubtext(view: View, instance: Instance, context: Context) {\n        val subtext = instance.getStatusDescription(context.resources)\n        val formSubtitle = view.findViewById<TextView>(R.id.form_subtitle)\n        formSubtitle.text = subtext\n    }\n\n    private fun setImageFromStatus(imageView: ImageView, instance: Instance) {\n        val imageResourceId = instance.getIcon()\n        imageView.setImageResource(imageResourceId)\n        imageView.tag = imageResourceId\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstanceSubmitter.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.upload.FormUploadException\nimport org.odk.collect.android.upload.InstanceServerUploader\nimport org.odk.collect.android.upload.InstanceUploader\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstanceAutoDeleteChecker\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.utilities.WebCredentialsUtils\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.metadata.PropertyManager\nimport org.odk.collect.metadata.PropertyManager.Companion.PROPMGR_DEVICE_ID\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\nimport timber.log.Timber\n\nclass InstanceSubmitter(\n    private val formsRepository: FormsRepository,\n    private val generalSettings: Settings,\n    private val propertyManager: PropertyManager,\n    private val httpInterface: OpenRosaHttpInterface,\n    private val instancesRepository: InstancesRepository\n) {\n\n    fun submitInstances(toUpload: List<Instance>): Map<Instance, FormUploadException?> {\n        val result = mutableMapOf<Instance, FormUploadException?>()\n        val deviceId = propertyManager.getSingularProperty(PROPMGR_DEVICE_ID)\n\n        val uploader = setUpODKUploader()\n\n        for (instance in toUpload.sortedBy { it.finalizationDate }) {\n            try {\n                val destinationUrl = uploader.getUrlToSubmitTo(instance, deviceId, null, null)\n                uploader.uploadOneSubmission(instance, destinationUrl)\n                result[instance] = null\n\n                deleteInstance(instance)\n                logUploadedForm(instance)\n            } catch (e: FormUploadException) {\n                Timber.d(e)\n                result[instance] = e\n            }\n        }\n        return result\n    }\n\n    private fun setUpODKUploader(): InstanceUploader {\n        return InstanceServerUploader(\n            httpInterface,\n            WebCredentialsUtils(generalSettings),\n            generalSettings,\n            instancesRepository\n        )\n    }\n\n    private fun deleteInstance(instance: Instance) {\n        // If the submission was successful, delete the instance if either the app-level\n        // delete preference is set or the form definition requests auto-deletion.\n        // TODO: this could take some time so might be better to do in a separate process,\n        // perhaps another worker. It also feels like this could fail and if so should be\n        // communicated to the user. Maybe successful delete should also be communicated?\n        if (InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, generalSettings.getBoolean(ProjectKeys.KEY_DELETE_AFTER_SEND), instance)) {\n            InstanceDeleter(\n                InstancesRepositoryProvider(Collect.getInstance()).create(),\n                FormsRepositoryProvider(Collect.getInstance()).create()\n            ).delete(instance.dbId)\n        }\n    }\n\n    private fun logUploadedForm(instance: Instance) {\n        val value = Collect.getFormIdentifierHash(instance.formId, instance.formVersion)\n\n        Analytics.log(AnalyticsEvents.SUBMISSION, \"HTTP auto\", value)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstancesDataService.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport kotlinx.coroutines.flow.StateFlow\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.backgroundwork.InstanceSubmitScheduler\nimport org.odk.collect.android.formentry.FormEntryUseCases\nimport org.odk.collect.android.formmanagement.CollectFormEntryControllerFactory\nimport org.odk.collect.android.instancemanagement.autosend.FormAutoSendMode\nimport org.odk.collect.android.instancemanagement.autosend.InstanceAutoSendFetcher\nimport org.odk.collect.android.instancemanagement.autosend.getAutoSendMode\nimport org.odk.collect.android.notifications.Notifier\nimport org.odk.collect.android.projects.ProjectDependencyModule\nimport org.odk.collect.android.state.DataKeys\nimport org.odk.collect.android.utilities.ExternalizableFormDefCache\nimport org.odk.collect.android.utilities.FormsUploadResultInterpreter\nimport org.odk.collect.androidshared.data.AppState\nimport org.odk.collect.androidshared.data.DataService\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.metadata.PropertyManager\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport java.io.File\n\nclass InstancesDataService(\n    appState: AppState,\n    private val instanceSubmitScheduler: InstanceSubmitScheduler,\n    private val projectDependencyModuleFactory: ProjectDependencyFactory<ProjectDependencyModule>,\n    private val notifier: Notifier,\n    private val propertyManager: PropertyManager,\n    private val httpInterface: OpenRosaHttpInterface,\n    onUpdate: () -> Unit\n) : DataService(appState, onUpdate) {\n\n    private val editableCount by qualifiedData(DataKeys.INSTANCES_EDITABLE_COUNT, 0) { projectId ->\n        val projectDependencyModule = projectDependencyModuleFactory.create(projectId)\n        val instancesRepository = projectDependencyModule.instancesRepository\n        instancesRepository.getCountByStatus(\n            Instance.STATUS_INCOMPLETE,\n            Instance.STATUS_INVALID,\n            Instance.STATUS_VALID,\n            Instance.STATUS_NEW_EDIT\n        )\n    }\n\n    private val sendableCount by qualifiedData(DataKeys.INSTANCES_SENDABLE_COUNT, 0) { projectId ->\n        val projectDependencyModule = projectDependencyModuleFactory.create(projectId)\n        val instancesRepository = projectDependencyModule.instancesRepository\n        instancesRepository.getCountByStatus(\n            Instance.STATUS_COMPLETE,\n            Instance.STATUS_SUBMISSION_FAILED\n        )\n    }\n\n    private val sentCount by qualifiedData(DataKeys.INSTANCES_SENT_COUNT, 0) { projectId ->\n        val projectDependencyModule = projectDependencyModuleFactory.create(projectId)\n        val instancesRepository = projectDependencyModule.instancesRepository\n        instancesRepository.getCountByStatus(\n            Instance.STATUS_SUBMITTED,\n            Instance.STATUS_SUBMISSION_FAILED\n        )\n    }\n\n    private val successfullySentCount by qualifiedData(DataKeys.INSTANCES_SUCCESSFULLY_SENT_COUNT, 0) { projectId ->\n        val projectDependencyModule = projectDependencyModuleFactory.create(projectId)\n        val instancesRepository = projectDependencyModule.instancesRepository\n        instancesRepository.getCountByStatus(Instance.STATUS_SUBMITTED)\n    }\n\n    private val instances by qualifiedData(DataKeys.INSTANCES, emptyList()) { projectId ->\n        val projectDependencyModule = projectDependencyModuleFactory.create(projectId)\n        val instancesRepository = projectDependencyModule.instancesRepository\n        instancesRepository.all\n    }\n\n    fun getEditableCount(projectId: String): StateFlow<Int> = editableCount.flow(projectId)\n    fun getSendableCount(projectId: String): StateFlow<Int> = sendableCount.flow(projectId)\n    fun getSentCount(projectId: String): StateFlow<Int> = sentCount.flow(projectId)\n    fun getSuccessfullySentCount(projectId: String): StateFlow<Int> = successfullySentCount.flow(projectId)\n\n    fun getInstances(projectId: String): StateFlow<List<Instance>> {\n        return instances.flow(projectId)\n    }\n\n    fun finalizeAllDrafts(projectId: String): FinalizeAllResult {\n        val projectDependencyModule = projectDependencyModuleFactory.create(projectId)\n        val instancesRepository = projectDependencyModule.instancesRepository\n        val formsRepository = projectDependencyModule.formsRepository\n        val savepointsRepository = projectDependencyModule.savepointsRepository\n        val entitiesRepository = projectDependencyModule.entitiesRepository\n        val debugLogger = projectDependencyModule.debugLogger\n\n        val projectRootDir = File(projectDependencyModule.rootDir)\n\n        val instances = instancesRepository.getAllByStatus(\n            Instance.STATUS_INCOMPLETE,\n            Instance.STATUS_INVALID,\n            Instance.STATUS_VALID,\n            Instance.STATUS_NEW_EDIT\n        )\n\n        val result = instances.fold(FinalizeAllResult(0, 0, false)) { result, instance ->\n            val formDefAndForm = FormEntryUseCases.loadFormDef(\n                instance,\n                formsRepository,\n                projectRootDir,\n                ExternalizableFormDefCache()\n            )\n\n            if (formDefAndForm == null) {\n                result.copy(failureCount = result.failureCount + 1)\n            } else {\n                val (formDef, form) = formDefAndForm\n\n                val formMediaDir = File(form.formMediaPath)\n                val formEntryController =\n                    CollectFormEntryControllerFactory(\n                        entitiesRepository,\n                        projectDependencyModule.generalSettings\n                    ).create(formDef, formMediaDir, instance)\n                val formController =\n                    FormEntryUseCases.loadDraft(form, instance, formEntryController)\n                if (formController == null) {\n                    result.copy(failureCount = result.failureCount + 1)\n                } else {\n                    val savePoint = savepointsRepository.get(form.dbId, instance.dbId)\n                    val needsEncrypted = form.basE64RSAPublicKey != null\n                    val newResult = if (savePoint != null) {\n                        Analytics.log(AnalyticsEvents.BULK_FINALIZE_SAVE_POINT)\n                        result.copy(\n                            failureCount = result.failureCount + 1,\n                            unsupportedInstances = true\n                        )\n                    } else if (needsEncrypted) {\n                        Analytics.log(AnalyticsEvents.BULK_FINALIZE_ENCRYPTED_FORM)\n                        result.copy(\n                            failureCount = result.failureCount + 1,\n                            unsupportedInstances = true\n                        )\n                    } else {\n                        val finalizedInstance = FormEntryUseCases.finalizeDraft(\n                            formController,\n                            instancesRepository,\n                            entitiesRepository,\n                            debugLogger\n                        )\n\n                        if (finalizedInstance == null) {\n                            result.copy(failureCount = result.failureCount + 1)\n                        } else {\n                            instanceFinalized(projectId, form)\n                            result\n                        }\n                    }\n\n                    Collect.getInstance().externalDataManager?.close()\n                    newResult\n                }\n            }\n        }\n\n        update(projectId)\n\n        return result.copy(successCount = instances.size - result.failureCount)\n    }\n\n    fun deleteInstances(projectId: String, instanceIds: LongArray): Boolean {\n        val projectDependencyModule = projectDependencyModuleFactory.create(projectId)\n        val instancesRepository = projectDependencyModule.instancesRepository\n        val formsRepository = projectDependencyModule.formsRepository\n\n        return projectDependencyModule.instancesLock.withLock { acquiredLock: Boolean ->\n            if (acquiredLock) {\n                instanceIds.forEach { instanceId ->\n                    InstanceDeleter(\n                        instancesRepository,\n                        formsRepository\n                    ).delete(\n                        instanceId\n                    )\n                }\n\n                update(projectId)\n                true\n            } else {\n                false\n            }\n        }\n    }\n\n    fun reset(projectId: String): Boolean {\n        val projectDependencyModule =\n            projectDependencyModuleFactory.create(projectId)\n        val instancesRepository = projectDependencyModule.instancesRepository\n\n        return projectDependencyModule.instancesLock.withLock { acquiredLock: Boolean ->\n            if (acquiredLock) {\n                val grouped = instancesRepository.all.groupBy { it.editOf ?: it.dbId }\n                grouped.values.forEach { group ->\n                    group.sortedByDescending { it.editNumber }\n                        .forEach {\n                            if (it.isDeletable()) {\n                                instancesRepository.delete(it.dbId)\n                            }\n                        }\n                }\n                update(projectId)\n                true\n            } else {\n                false\n            }\n        }\n    }\n\n    fun sendInstances(projectId: String, formAutoSend: Boolean = false): Boolean {\n        val projectDependencyModule =\n            projectDependencyModuleFactory.create(projectId)\n\n        val instanceSubmitter = InstanceSubmitter(\n            projectDependencyModule.formsRepository,\n            projectDependencyModule.generalSettings,\n            propertyManager,\n            httpInterface,\n            projectDependencyModule.instancesRepository\n        )\n\n        return projectDependencyModule.instancesLock.withLock { acquiredLock: Boolean ->\n            if (acquiredLock) {\n                val toUpload = InstanceAutoSendFetcher.getInstancesToAutoSend(\n                    projectDependencyModule.instancesRepository,\n                    projectDependencyModule.formsRepository,\n                    formAutoSend\n                )\n\n                if (toUpload.isNotEmpty()) {\n                    val results = instanceSubmitter.submitInstances(toUpload)\n                    notifier.onSubmission(results, projectDependencyModule.projectId)\n                    update(projectId)\n\n                    FormsUploadResultInterpreter.allFormsUploadedSuccessfully(results)\n                } else {\n                    true\n                }\n            } else {\n                false\n            }\n        }\n    }\n\n    fun instanceFinalized(projectId: String, form: Form) {\n        if (form.getAutoSendMode() == FormAutoSendMode.FORCED) {\n            instanceSubmitScheduler.scheduleFormAutoSend(projectId)\n        } else {\n            instanceSubmitScheduler.scheduleAutoSend(projectId)\n        }\n    }\n\n    fun editInstance(instanceFilePath: String, projectId: String): InstanceEditResult {\n        val projectDependencyModule = projectDependencyModuleFactory.create(projectId)\n\n        return LocalInstancesUseCases.editInstance(\n            instanceFilePath,\n            projectDependencyModule.instancesDir,\n            projectDependencyModule.instancesRepository,\n            projectDependencyModule.formsRepository\n        )\n    }\n}\n\ndata class FinalizeAllResult(\n    val successCount: Int,\n    val failureCount: Int,\n    val unsupportedInstances: Boolean\n)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/LocalInstancesUseCases.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.android.utilities.FormNameUtils\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.instances.InstancesRepository\nimport timber.log.Timber\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\nimport java.util.TimeZone\n\nobject LocalInstancesUseCases {\n    @JvmOverloads\n    @JvmStatic\n    fun createInstanceFile(\n        formName: String,\n        instancesDir: String,\n        timezone: TimeZone = TimeZone.getDefault(),\n        clock: () -> Long = { System.currentTimeMillis() }\n    ): File? {\n        val sanitizedFormName = FormNameUtils.formatFilenameFromFormName(formName)\n\n        val timestamp = SimpleDateFormat(\"yyyy-MM-dd_HH-mm-ss\", Locale.ENGLISH).also {\n            it.timeZone = timezone\n        }.format(Date(clock()))\n\n        val instanceDir = instancesDir + File.separator + sanitizedFormName + \"_\" + timestamp\n\n        if (FileUtils.createFolder(instanceDir)) {\n            return File(instanceDir + File.separator + sanitizedFormName + \"_\" + timestamp + \".xml\")\n        } else {\n            Timber.e(Error(\"Error creating form instance file\"))\n            return null\n        }\n    }\n\n    fun editInstance(\n        instanceFilePath: String,\n        instancesDir: String,\n        instancesRepository: InstancesRepository,\n        formsRepository: FormsRepository,\n        clock: () -> Long = { System.currentTimeMillis() }\n    ): InstanceEditResult {\n        val sourceInstance = instancesRepository.getOneByPath(instanceFilePath)!!\n\n        val latestEditInstance = findLatestEditIfExists(sourceInstance, instancesRepository)\n        if (latestEditInstance != null) {\n            return InstanceEditResult.EditBlockedByNewerExistingEdit(latestEditInstance)\n        }\n\n        val formHash = Analytics.getParamValue(\"form\")\n        val actionValue = if (sourceInstance.status == Instance.STATUS_COMPLETE) {\n            \"finalized $formHash\"\n        } else {\n            \"sent $formHash\"\n        }\n        Analytics.log(AnalyticsEvents.EDIT_FINALIZED_OR_SENT_FORM, \"action\", actionValue)\n\n        val targetInstance = cloneInstance(sourceInstance, instanceFilePath, instancesDir, instancesRepository, formsRepository, clock)\n\n        return InstanceEditResult.EditCompleted(targetInstance)\n    }\n\n    private fun findLatestEditIfExists(\n        instance: Instance,\n        instancesRepository: InstancesRepository\n    ): Instance? {\n        val editGroupId = if (instance.isEdit()) {\n            instance.editOf\n        } else {\n            instance.dbId\n        }\n\n        return instancesRepository\n            .all\n            .filter { it.editOf == editGroupId }\n            .maxByOrNull { it.editNumber!! }\n            ?.takeIf { it.dbId != instance.dbId }\n    }\n\n    private fun cloneInstance(\n        sourceInstance: Instance,\n        instanceFilePath: String,\n        instancesDir: String,\n        instancesRepository: InstancesRepository,\n        formsRepository: FormsRepository,\n        clock: () -> Long = { System.currentTimeMillis() }\n    ): Instance {\n        val formName = formsRepository.getAllByFormIdAndVersion(\n            sourceInstance.formId,\n            sourceInstance.formVersion\n        ).first().displayName\n        val targetInstanceFile = copyInstanceDir(File(instanceFilePath), instancesDir, formName, clock)\n\n        return instancesRepository.save(\n            Instance.Builder(sourceInstance)\n                .dbId(null)\n                .status(Instance.STATUS_NEW_EDIT)\n                .instanceFilePath(targetInstanceFile.absolutePath)\n                .editOf(sourceInstance.editOf ?: sourceInstance.dbId)\n                .editNumber((sourceInstance.editNumber ?: 0) + 1)\n                .build()\n        )\n    }\n\n    private fun copyInstanceDir(\n        sourceInstanceFile: File,\n        instancesDir: String,\n        formName: String,\n        clock: () -> Long = { System.currentTimeMillis() }\n    ): File {\n        val sourceInstanceDir = sourceInstanceFile.parentFile!!\n        val targetInstanceFile = createInstanceFile(formName, instancesDir, clock = clock)!!\n        val targetInstanceDir = targetInstanceFile.parentFile!!\n\n        sourceInstanceDir.copyRecursively(targetInstanceDir, true)\n        File(targetInstanceDir, sourceInstanceFile.name).renameTo(targetInstanceFile)\n\n        return targetInstanceFile\n    }\n}\n\nsealed class InstanceEditResult(val instance: Instance) {\n    data class EditCompleted(val resultInstance: Instance) : InstanceEditResult(resultInstance)\n    data class EditBlockedByNewerExistingEdit(val resultInstance: Instance) : InstanceEditResult(resultInstance)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProvider.kt",
    "content": "package org.odk.collect.android.instancemanagement.autosend\n\nimport android.app.Application\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.network.NetworkStateProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.enums.AutoSend\nimport org.odk.collect.settings.enums.StringIdEnumUtils.getAutoSend\n\nclass AutoSendSettingsProvider(\n    private val application: Application,\n    private val networkStateProvider: NetworkStateProvider,\n    private val settingsProvider: SettingsProvider\n) {\n\n    fun isAutoSendEnabledInSettings(projectId: String? = null): Boolean {\n        val currentNetworkType = networkStateProvider.currentNetwork ?: return false\n\n        val autosend = settingsProvider.getUnprotectedSettings(projectId).getAutoSend(application)\n        var sendwifi = autosend == AutoSend.WIFI_ONLY\n        var sendnetwork = autosend == AutoSend.CELLULAR_ONLY\n\n        if (autosend == AutoSend.WIFI_AND_CELLULAR) {\n            sendwifi = true\n            sendnetwork = true\n        }\n\n        return currentNetworkType == Scheduler.NetworkType.WIFI &&\n            sendwifi || currentNetworkType == Scheduler.NetworkType.CELLULAR && sendnetwork\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/FormExt.kt",
    "content": "package org.odk.collect.android.instancemanagement.autosend\n\nimport org.odk.collect.forms.Form\n\nfun Form.shouldFormBeSentAutomatically(isAutoSendEnabledInSettings: Boolean): Boolean {\n    return if (isAutoSendEnabledInSettings) {\n        getAutoSendMode() != FormAutoSendMode.OPT_OUT\n    } else {\n        getAutoSendMode() == FormAutoSendMode.FORCED\n    }\n}\n\nfun Form.getAutoSendMode(): FormAutoSendMode {\n    return if (autoSend?.trim()?.lowercase() == \"false\") {\n        FormAutoSendMode.OPT_OUT\n    } else if (autoSend?.trim()?.lowercase() == \"true\") {\n        FormAutoSendMode.FORCED\n    } else {\n        FormAutoSendMode.NEUTRAL\n    }\n}\n\nfun Form.getLastUpdated(): Long {\n    return lastDetectedAttachmentsUpdateDate ?: date\n}\n\nenum class FormAutoSendMode {\n    OPT_OUT,\n    FORCED,\n    NEUTRAL\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/InstanceAutoSendFetcher.kt",
    "content": "package org.odk.collect.android.instancemanagement.autosend\n\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.instances.InstancesRepository\n\nobject InstanceAutoSendFetcher {\n\n    fun getInstancesToAutoSend(\n        instancesRepository: InstancesRepository,\n        formsRepository: FormsRepository,\n        forcedOnly: Boolean = false\n    ): List<Instance> {\n        val allFinalizedForms = instancesRepository.getAllByStatus(\n            Instance.STATUS_COMPLETE,\n            Instance.STATUS_SUBMISSION_FAILED\n        )\n\n        val filter: (Form) -> Boolean = if (forcedOnly) {\n            { form -> form.getAutoSendMode() == FormAutoSendMode.FORCED }\n        } else {\n            { form -> form.getAutoSendMode() == FormAutoSendMode.NEUTRAL }\n        }\n\n        return allFinalizedForms.filter {\n            formsRepository.getLatestByFormIdAndVersion(it.formId, it.formVersion)\n                ?.let { form -> filter(form) } ?: false\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderActivity.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.instancemanagement.send;\n\nimport static java.util.Arrays.stream;\n\nimport android.app.Dialog;\nimport android.app.ProgressDialog;\nimport android.content.DialogInterface;\nimport android.net.Uri;\nimport android.os.Bundle;\n\nimport org.odk.collect.android.activities.FormFillingActivity;\nimport org.odk.collect.android.fragments.dialogs.SimpleDialog;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.listeners.InstanceUploaderListener;\nimport org.odk.collect.android.tasks.InstanceUploaderTask;\nimport org.odk.collect.android.utilities.ApplicationConstants;\nimport org.odk.collect.android.utilities.ArrayUtils;\nimport org.odk.collect.android.utilities.AuthDialogUtility;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.android.utilities.InstanceUploaderUtils;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\nimport org.odk.collect.android.views.DayNightProgressDialog;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.openrosa.http.OpenRosaConstants;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.strings.localization.LocalizedActivity;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Set;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\n/**\n * Activity to upload completed forms.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n */\npublic class InstanceUploaderActivity extends LocalizedActivity implements InstanceUploaderListener,\n        AuthDialogUtility.AuthDialogUtilityResultListener {\n    private static final int PROGRESS_DIALOG = 1;\n    private static final int AUTH_DIALOG = 2;\n\n    private static final String AUTH_URI = \"auth\";\n    private static final String ALERT_MSG = \"alertmsg\";\n    private static final String TO_SEND = \"tosend\";\n\n    private ProgressDialog progressDialog;\n\n    private String alertMsg;\n\n    private InstanceUploaderTask instanceUploaderTask;\n\n    // maintain a list of what we've yet to send, in case we're interrupted by auth requests\n    private Long[] instancesToSend;\n\n    // URL specified when authentication is requested or specified from intent extra as override\n    private String url;\n\n    // Set from intent extras\n    private String username;\n    private String password;\n    private Boolean deleteInstanceAfterUpload;\n\n    private boolean isInstanceStateSaved;\n\n    @Inject\n    InstancesRepositoryProvider instancesRepositoryProvider;\n    private InstancesRepository instancesRepository;\n\n    @Inject\n    FormsRepositoryProvider formsRepositoryProvider;\n    private FormsRepository formsRepository;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        DaggerUtils.getComponent(this).inject(this);\n        instancesRepository = instancesRepositoryProvider.create();\n        formsRepository = formsRepositoryProvider.create();\n\n        init(savedInstanceState);\n    }\n\n    private void init(Bundle savedInstanceState) {\n        alertMsg = getString(org.odk.collect.strings.R.string.please_wait);\n\n        setTitle(getString(org.odk.collect.strings.R.string.send_data));\n\n        // Get simple saved state\n        if (savedInstanceState != null) {\n            if (savedInstanceState.containsKey(ALERT_MSG)) {\n                alertMsg = savedInstanceState.getString(ALERT_MSG);\n            }\n\n            url = savedInstanceState.getString(AUTH_URI);\n        }\n\n        Bundle dataBundle;\n\n        // If we are resuming, use the TO_SEND list of not-yet-sent submissions\n        // Otherwise, construct the list from the incoming intent value\n        long[] selectedInstanceIDs;\n        if (savedInstanceState != null && savedInstanceState.containsKey(TO_SEND)) {\n            selectedInstanceIDs = savedInstanceState.getLongArray(TO_SEND);\n            dataBundle = savedInstanceState;\n        } else {\n            selectedInstanceIDs = getIntent().getLongArrayExtra(FormFillingActivity.KEY_INSTANCES);\n            dataBundle = getIntent().getExtras();\n\n            boolean missingInstances = stream(selectedInstanceIDs).anyMatch(id -> instancesRepository.get(id) == null);\n            if (missingInstances) {\n                selectedInstanceIDs = new long[]{};\n            }\n        }\n\n        // An external application can temporarily override destination URL, username, password\n        // and whether instances should be deleted after submission by specifying intent extras.\n        if (dataBundle != null && dataBundle.containsKey(ApplicationConstants.BundleKeys.URL)) {\n            // TODO: I think this means redirection from a URL set through an extra is not supported\n            url = dataBundle.getString(ApplicationConstants.BundleKeys.URL);\n\n            // Remove trailing slashes (only necessary for the intent case but doesn't hurt on resume)\n            while (url != null && url.endsWith(\"/\")) {\n                url = url.substring(0, url.length() - 1);\n            }\n\n            if (dataBundle.containsKey(ApplicationConstants.BundleKeys.USERNAME)\n                    && dataBundle.containsKey(ApplicationConstants.BundleKeys.PASSWORD)) {\n                username = dataBundle.getString(ApplicationConstants.BundleKeys.USERNAME);\n                password = dataBundle.getString(ApplicationConstants.BundleKeys.PASSWORD);\n            }\n\n            if (dataBundle.containsKey(ApplicationConstants.BundleKeys.DELETE_INSTANCE_AFTER_SUBMISSION)) {\n                deleteInstanceAfterUpload = dataBundle.getBoolean(ApplicationConstants.BundleKeys.DELETE_INSTANCE_AFTER_SUBMISSION);\n            }\n        }\n\n        instancesToSend = ArrayUtils.toObject(selectedInstanceIDs);\n\n        if (instancesToSend.length == 0) {\n            Timber.e(new Error(\"onCreate: No instances to upload!\"));\n            // drop through -- everything will process through OK\n        } else {\n            Timber.i(\"onCreate: Beginning upload of %d instances!\", instancesToSend.length);\n        }\n\n        // Get the task if there was a configuration change but the app did not go out of memory.\n        // If the app went out of memory, the task is null but the simple state was saved so\n        // the task status is reconstructed from that state.\n        instanceUploaderTask = (InstanceUploaderTask) getLastCustomNonConfigurationInstance();\n\n        if (instanceUploaderTask == null) {\n            // set up dialog and upload task\n            showDialog(PROGRESS_DIALOG);\n            instanceUploaderTask = new InstanceUploaderTask();\n\n            if (url != null) {\n                instanceUploaderTask.setCompleteDestinationUrl(url + OpenRosaConstants.SUBMISSION, getReferrerUri(), true);\n\n                if (deleteInstanceAfterUpload != null) {\n                    instanceUploaderTask.setDeleteInstanceAfterSubmission(deleteInstanceAfterUpload);\n                }\n\n                String host = Uri.parse(url).getHost();\n                if (host != null) {\n                    // We do not need to clear the cookies since they are cleared before any request is made and the Credentials provider is used\n                    if (password != null && username != null) {\n                        instanceUploaderTask.setCustomUsername(username);\n                        instanceUploaderTask.setCustomPassword(password);\n                    }\n                }\n            }\n\n            // register this activity with the new uploader task\n            instanceUploaderTask.setUploaderListener(this);\n            instanceUploaderTask.setRepositories(instancesRepository, formsRepository, settingsProvider);\n            instanceUploaderTask.execute(instancesToSend);\n        }\n    }\n\n    @Override\n    protected void onResume() {\n        if (instancesToSend != null) {\n            Timber.i(\"onResume: Resuming upload of %d instances!\", instancesToSend.length);\n        }\n        if (instanceUploaderTask != null) {\n            instanceUploaderTask.setUploaderListener(this);\n        }\n        super.onResume();\n    }\n\n    @Override\n    protected void onPostResume() {\n        super.onPostResume();\n        isInstanceStateSaved = false;\n    }\n\n    @Override\n    protected void onSaveInstanceState(Bundle outState) {\n        isInstanceStateSaved = true;\n        super.onSaveInstanceState(outState);\n        outState.putString(ALERT_MSG, alertMsg);\n        outState.putString(AUTH_URI, url);\n        outState.putLongArray(TO_SEND, ArrayUtils.toPrimitive(instancesToSend));\n\n        if (url != null) {\n            outState.putString(ApplicationConstants.BundleKeys.URL, url);\n\n            if (username != null && password != null) {\n                outState.putString(ApplicationConstants.BundleKeys.USERNAME, username);\n                outState.putString(ApplicationConstants.BundleKeys.PASSWORD, password);\n            }\n        }\n    }\n\n    @Override\n    public Object onRetainCustomNonConfigurationInstance() {\n        return instanceUploaderTask;\n    }\n\n    @Override\n    protected void onDestroy() {\n        if (instanceUploaderTask != null) {\n            instanceUploaderTask.setUploaderListener(null);\n        }\n        super.onDestroy();\n    }\n\n    @Override\n    public void uploadingComplete(HashMap<String, String> result) {\n        Timber.i(\"uploadingComplete: Processing results (%d) from upload of %d instances!\",\n                result.size(), instancesToSend.length);\n\n        try {\n            dismissDialog(PROGRESS_DIALOG);\n        } catch (Exception e) {\n            // tried to close a dialog not open. don't care.\n        }\n\n        // If the activity is paused or in the process of pausing, don't show the dialog\n        if (!isInstanceStateSaved) {\n            createUploadInstancesResultDialog(InstanceUploaderUtils.getUploadResultMessage(instancesRepository, this, result));\n        } else {\n            // Clean up\n            finish();\n        }\n    }\n\n    @Override\n    public void progressUpdate(int progress, int total) {\n        alertMsg = getString(org.odk.collect.strings.R.string.sending_items, String.valueOf(progress), String.valueOf(total));\n        progressDialog.setMessage(alertMsg);\n    }\n\n    @Override\n    protected Dialog onCreateDialog(int id) {\n        switch (id) {\n            case PROGRESS_DIALOG:\n                progressDialog = new DayNightProgressDialog(this);\n                DialogInterface.OnClickListener loadingButtonListener =\n                        new DialogInterface.OnClickListener() {\n                            @Override\n                            public void onClick(DialogInterface dialog, int which) {\n                                dialog.dismiss();\n                                instanceUploaderTask.cancel(true);\n                                instanceUploaderTask.setUploaderListener(null);\n                                finish();\n                            }\n                        };\n                progressDialog.setTitle(getString(org.odk.collect.strings.R.string.uploading_data));\n                progressDialog.setMessage(alertMsg);\n                progressDialog.setIndeterminate(true);\n                progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);\n                progressDialog.setCancelable(false);\n                progressDialog.setButton(getString(org.odk.collect.strings.R.string.cancel), loadingButtonListener);\n                return progressDialog;\n            case AUTH_DIALOG:\n                Timber.i(\"onCreateDialog(AUTH_DIALOG): for upload of %d instances!\",\n                        instancesToSend.length);\n\n                AuthDialogUtility authDialogUtility = new AuthDialogUtility();\n                if (username != null && password != null && url != null) {\n                    authDialogUtility.setCustomUsername(username);\n                    authDialogUtility.setCustomPassword(password);\n                }\n\n                return authDialogUtility.createDialog(this, this, this.url);\n        }\n\n        return null;\n    }\n\n    /**\n     * Prompts the user for credentials for the server at the given URL. Once credentials are\n     * provided, starts a new upload task with just the instances that were not yet reached.\n     *\n     * messagesByInstanceIdAttempted makes it possible to identify the instances that were part\n     * of the latest submission attempt. The database provides generic status which could have come\n     * from an unrelated submission attempt.\n     */\n    @Override\n    public void authRequest(Uri url, HashMap<String, String> messagesByInstanceIdAttempted) {\n        if (progressDialog.isShowing()) {\n            // should always be showing here\n            progressDialog.dismiss();\n        }\n\n        // Remove sent instances from instances to send\n        ArrayList<Long> workingSet = new ArrayList<>();\n        Collections.addAll(workingSet, instancesToSend);\n        if (messagesByInstanceIdAttempted != null) {\n            Set<String> uploadedInstances = messagesByInstanceIdAttempted.keySet();\n\n            for (String uploadedInstance : uploadedInstances) {\n                Long removeMe = Long.valueOf(uploadedInstance);\n                boolean removed = workingSet.remove(removeMe);\n                if (removed) {\n                    Timber.i(\"%d was already attempted, removing from queue before restarting task\",\n                            removeMe);\n                }\n            }\n        }\n\n        // and reconstruct the pending set of instances to send\n        Long[] updatedToSend = new Long[workingSet.size()];\n        for (int i = 0; i < workingSet.size(); ++i) {\n            updatedToSend[i] = workingSet.get(i);\n        }\n        instancesToSend = updatedToSend;\n\n        this.url = url.toString();\n\n        /** Once credentials are provided in the dialog, {@link #updatedCredentials()} is called */\n        showDialog(AUTH_DIALOG);\n    }\n\n    private void createUploadInstancesResultDialog(String message) {\n        String dialogTitle = getString(org.odk.collect.strings.R.string.upload_results);\n        String buttonTitle = getString(org.odk.collect.strings.R.string.ok);\n\n        SimpleDialog simpleDialog = SimpleDialog.newInstance(dialogTitle, 0, message, buttonTitle, true);\n        simpleDialog.show(getSupportFragmentManager(), SimpleDialog.COLLECT_DIALOG_TAG);\n    }\n\n    @Override\n    public void updatedCredentials() {\n        showDialog(PROGRESS_DIALOG);\n        instanceUploaderTask = new InstanceUploaderTask();\n\n        // register this activity with the new uploader task\n        instanceUploaderTask.setUploaderListener(this);\n        // In the case of credentials set via intent extras, the credentials are stored in the\n        // global WebCredentialsUtils but the task also needs to know what server to set to\n        // TODO: is this really needed here? When would the task not have gotten a server set in\n        // init already?\n        if (url != null) {\n            instanceUploaderTask.setCompleteDestinationUrl(url + OpenRosaConstants.SUBMISSION, getReferrerUri(), false);\n        }\n        instanceUploaderTask.setRepositories(instancesRepository, formsRepository, settingsProvider);\n        instanceUploaderTask.execute(instancesToSend);\n    }\n\n    @Override\n    public void cancelledUpdatingCredentials() {\n        finish();\n    }\n\n    private String getReferrerUri() {\n        Uri referrerUri = getReferrer();\n        if (referrerUri != null) {\n            return referrerUri.toString();\n        }\n        return \"unknown\";\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.instancemanagement.send;\n\nimport static org.odk.collect.android.activities.AppListActivity.LOADER_ID;\nimport static org.odk.collect.android.activities.AppListActivity.toggleButtonLabel;\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_ASC;\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_DATE_DESC;\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_ASC;\nimport static org.odk.collect.android.utilities.ApplicationConstants.SortingOrder.BY_NAME_DESC;\nimport static org.odk.collect.androidshared.ui.EdgeToEdge.setView;\nimport static org.odk.collect.lists.selects.MultiSelectViewModelKt.updateSelectAll;\n\nimport android.content.Intent;\nimport android.database.Cursor;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.View.OnLongClickListener;\nimport android.widget.AdapterView;\nimport android.widget.ListView;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.appcompat.widget.SearchView;\nimport androidx.core.content.ContextCompat;\nimport androidx.core.view.MenuItemCompat;\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.loader.app.LoaderManager;\nimport androidx.loader.content.Loader;\nimport androidx.work.WorkInfo;\nimport androidx.work.WorkManager;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.activities.FormFillingActivity;\nimport org.odk.collect.android.adapters.InstanceUploaderAdapter;\nimport org.odk.collect.android.backgroundwork.FormUpdateAndInstanceSubmitScheduler;\nimport org.odk.collect.android.backgroundwork.InstanceSubmitScheduler;\nimport org.odk.collect.android.dao.CursorLoaderFactory;\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns;\nimport org.odk.collect.android.databinding.InstanceUploaderListBinding;\nimport org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog;\nimport org.odk.collect.android.formlists.sorting.FormListSortingOption;\nimport org.odk.collect.android.formmanagement.FormFillingIntentFactory;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.mainmenu.MainMenuActivity;\nimport org.odk.collect.android.preferences.screens.ProjectPreferencesActivity;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.androidshared.ui.MenuExtKt;\nimport org.odk.collect.androidshared.ui.ObviousProgressBar;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard;\nimport org.odk.collect.async.network.NetworkStateProvider;\nimport org.odk.collect.lists.selects.MultiSelectViewModel;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.strings.localization.LocalizedActivity;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\n/**\n * Responsible for displaying all the valid forms in the forms directory. Stores\n * the path to selected form for use by {@link MainMenuActivity}.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n * @author Yaw Anokwa (yanokwa@gmail.com)\n */\n\npublic class InstanceUploaderListActivity extends LocalizedActivity implements\n        OnLongClickListener, AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor> {\n    private static final String SHOW_ALL_MODE = \"showAllMode\";\n\n    private static final String IS_SEARCH_BOX_SHOWN = \"isSearchBoxShown\";\n    private static final String SEARCH_TEXT = \"searchText\";\n\n    private static final int INSTANCE_UPLOADER = 0;\n\n    InstanceUploaderListBinding binding;\n\n    @Inject\n    ProjectsDataService projectsDataService;\n\n    private boolean showAllMode;\n\n    // Default to true so the send button is disabled until the worker status is updated by the\n    // observer\n    private boolean autoSendOngoing = true;\n\n    @Inject\n    NetworkStateProvider connectivityProvider;\n\n    @Inject\n    InstanceSubmitScheduler instanceSubmitScheduler;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    @Inject\n    ReadyToSendViewModel.Factory factory;\n\n    private ListView listView;\n    private InstanceUploaderAdapter listAdapter;\n    private Integer selectedSortingOrder;\n    private List<FormListSortingOption> sortingOptions;\n    private ObviousProgressBar progressBar;\n    private String filterText;\n\n    private MultiSelectViewModel<Object> multiSelectViewModel;\n    private ReadyToSendViewModel readyToSendViewModel;\n    private boolean allSelected;\n\n    private boolean isSearchBoxShown;\n\n    private SearchView searchView;\n    private String savedFilterText;\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        Timber.i(\"onCreate\");\n\n        if (savedInstanceState != null) {\n            isSearchBoxShown = savedInstanceState.getBoolean(IS_SEARCH_BOX_SHOWN);\n            savedFilterText = savedInstanceState.getString(SEARCH_TEXT);\n        }\n\n        DaggerUtils.getComponent(this).inject(this);\n\n        multiSelectViewModel = new ViewModelProvider(this).get(MultiSelectViewModel.class);\n        multiSelectViewModel.getSelected().observe(this, ids -> {\n            binding.uploadButton.setEnabled(!ids.isEmpty());\n            allSelected = updateSelectAll(binding.toggleButton, listAdapter.getCount(), ids.size());\n            listAdapter.setSelected(ids.stream().map(Long::valueOf).collect(Collectors.toSet()));\n        });\n        readyToSendViewModel = new ViewModelProvider(this, factory).get(ReadyToSendViewModel.class);\n        readyToSendViewModel.getData().observe(this, data -> binding.readyToSendBanner.setData(data));\n\n        // set title\n        setTitle(getString(org.odk.collect.strings.R.string.send_data));\n        binding = InstanceUploaderListBinding.inflate(LayoutInflater.from(this));\n\n        setView(this, binding.getRoot(), false);\n        listView = findViewById(R.id.scrollable_container);\n        listView.setOnItemClickListener((AdapterView.OnItemClickListener) this);\n        listView.setEmptyView(findViewById(android.R.id.empty));\n        progressBar = findViewById(org.odk.collect.androidshared.R.id.progressBar);\n\n        // Use the nicer-looking drawable with Material Design insets.\n        listView.setDivider(ContextCompat.getDrawable(this, org.odk.collect.androidshared.R.drawable.list_item_divider));\n        listView.setDividerHeight(1);\n\n        setSupportActionBar(findViewById(org.odk.collect.androidshared.R.id.toolbar));\n\n        init();\n\n        binding.uploadButton.setOnClickListener(v -> onUploadButtonsClicked());\n        if (savedInstanceState != null) {\n            showAllMode = savedInstanceState.getBoolean(SHOW_ALL_MODE);\n        }\n    }\n\n    public void onUploadButtonsClicked() {\n        if (!connectivityProvider.isDeviceOnline()) {\n            ToastUtils.showShortToast(org.odk.collect.strings.R.string.no_connection);\n            return;\n        }\n\n        if (autoSendOngoing) {\n            ToastUtils.showShortToast(org.odk.collect.strings.R.string.send_in_progress);\n            return;\n        }\n\n        Set<Long> selectedItems = multiSelectViewModel.getSelected().getValue().stream().map(Long::valueOf).collect(Collectors.toSet());\n        if (!selectedItems.isEmpty()) {\n            binding.uploadButton.setEnabled(false);\n\n            uploadSelectedFiles(selectedItems.stream().mapToLong(Long::longValue).toArray());\n            multiSelectViewModel.unselectAll();\n        } else {\n            // no items selected\n            ToastUtils.showLongToast(org.odk.collect.strings.R.string.noselect_error);\n        }\n    }\n\n    void init() {\n        binding.uploadButton.setText(org.odk.collect.strings.R.string.send_selected_data);\n\n        binding.toggleButton.setLongClickable(true);\n        binding.toggleButton.setOnClickListener(v -> {\n            if (!allSelected) {\n                for (int i = 0; i < listView.getCount(); i++) {\n                    multiSelectViewModel.select(String.valueOf(listView.getItemIdAtPosition(i)));\n                }\n            } else {\n                multiSelectViewModel.unselectAll();\n            }\n        });\n        binding.toggleButton.setOnLongClickListener(this);\n\n        setupAdapter();\n\n        sortingOptions = Arrays.asList(\n                new FormListSortingOption(\n                        R.drawable.ic_sort_by_alpha,\n                        org.odk.collect.strings.R.string.sort_by_name_asc\n                ),\n                new FormListSortingOption(\n                        R.drawable.ic_sort_by_alpha,\n                        org.odk.collect.strings.R.string.sort_by_name_desc\n                ),\n                new FormListSortingOption(\n                        R.drawable.ic_access_time,\n                        org.odk.collect.strings.R.string.sort_by_date_desc\n                ),\n                new FormListSortingOption(\n                        R.drawable.ic_access_time,\n                        org.odk.collect.strings.R.string.sort_by_date_asc\n                )\n        );\n\n        getSupportLoaderManager().initLoader(LOADER_ID, null, this);\n\n        // Start observer that sets autoSendOngoing field based on AutoSendWorker status\n        updateAutoSendStatus();\n    }\n\n    /**\n     * Updates whether an auto-send job is ongoing.\n     */\n    private void updateAutoSendStatus() {\n        // This shouldn't use WorkManager directly but it's likely this code will be removed when\n        // we eventually move sending forms to a Foreground Service (rather than a blocking AsyncTask)\n        String tag = ((FormUpdateAndInstanceSubmitScheduler) instanceSubmitScheduler).getAutoSendTag(projectsDataService.requireCurrentProject().getUuid());\n        LiveData<List<WorkInfo>> statuses = WorkManager.getInstance().getWorkInfosForUniqueWorkLiveData(tag);\n        statuses.observe(this, workStatuses -> {\n            if (workStatuses != null) {\n                for (WorkInfo status : workStatuses) {\n                    if (status.getState().equals(WorkInfo.State.RUNNING)) {\n                        autoSendOngoing = true;\n                        return;\n                    }\n                }\n                autoSendOngoing = false;\n            }\n        });\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        restoreSelectedSortingOrder();\n        binding.uploadButton.setText(org.odk.collect.strings.R.string.send_selected_data);\n    }\n\n    private void uploadSelectedFiles(long[] instanceIds) {\n        Intent i = new Intent(this, InstanceUploaderActivity.class);\n        i.putExtra(FormFillingActivity.KEY_INSTANCES, instanceIds);\n        startActivityForResult(i, INSTANCE_UPLOADER);\n    }\n\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.instance_uploader_menu, menu);\n\n        getMenuInflater().inflate(R.menu.blank_form_list_menu, menu);\n        MenuExtKt.enableIconsVisibility(menu);\n\n        final MenuItem sortItem = menu.findItem(R.id.menu_sort);\n        final MenuItem searchItem = menu.findItem(R.id.menu_filter);\n        searchView = (SearchView) MenuItemCompat.getActionView(searchItem);\n        searchView.setQueryHint(getResources().getString(org.odk.collect.strings.R.string.search));\n        searchView.setMaxWidth(Integer.MAX_VALUE);\n        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {\n            @Override\n            public boolean onQueryTextSubmit(String query) {\n                filterText = query;\n                updateAdapter();\n                searchView.clearFocus();\n                return false;\n            }\n\n            @Override\n            public boolean onQueryTextChange(String newText) {\n                filterText = newText;\n                updateAdapter();\n                return false;\n            }\n        });\n\n        MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {\n            @Override\n            public boolean onMenuItemActionExpand(MenuItem item) {\n                sortItem.setVisible(false);\n                return true;\n            }\n\n            @Override\n            public boolean onMenuItemActionCollapse(MenuItem item) {\n                sortItem.setVisible(true);\n                return true;\n            }\n        });\n\n        if (isSearchBoxShown) {\n            searchItem.expandActionView();\n            searchView.setQuery(savedFilterText, false);\n        }\n\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        if (!MultiClickGuard.allowClick(getClass().getName())) {\n            return true;\n        }\n\n        if (item.getItemId() == R.id.menu_preferences) {\n            createPreferencesMenu();\n            return true;\n        } else if (item.getItemId() == R.id.menu_change_view) {\n            showSentAndUnsentChoices();\n            return true;\n        }\n\n        if (item.getItemId() == R.id.menu_sort) {\n            new FormListSortingBottomSheetDialog(\n                    this,\n                    sortingOptions,\n                    selectedSortingOrder,\n                    selectedOption -> {\n                        saveSelectedSortingOrder(selectedOption);\n                        updateAdapter();\n                    }\n            ).show();\n            return true;\n        }\n\n        return super.onOptionsItemSelected(item);\n    }\n\n    private void createPreferencesMenu() {\n        Intent i = new Intent(this, ProjectPreferencesActivity.class);\n        startActivity(i);\n    }\n\n    @Override\n    public void onItemClick(AdapterView<?> parent, View view, int position, long rowId) {\n        Cursor c = (Cursor) listView.getAdapter().getItem(position);\n        boolean encryptedForm = !Boolean.parseBoolean(c.getString(c.getColumnIndex(DatabaseInstanceColumns.CAN_EDIT_WHEN_COMPLETE)));\n        if (encryptedForm) {\n            ToastUtils.showLongToast(org.odk.collect.strings.R.string.encrypted_form);\n        } else {\n            long instanceId = c.getLong(c.getColumnIndex(DatabaseInstanceColumns._ID));\n            Intent intent = FormFillingIntentFactory.editDraftFormIntent(this, projectsDataService.requireCurrentProject().getUuid(), instanceId);\n            startActivity(intent);\n            finish();\n        }\n    }\n\n    @Override\n    protected void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n\n        if (searchView != null) {\n            outState.putBoolean(IS_SEARCH_BOX_SHOWN, !searchView.isIconified());\n            outState.putString(SEARCH_TEXT, String.valueOf(searchView.getQuery()));\n        } else {\n            Timber.e(new Error(\"Unexpected null search view (issue #1412)\"));\n        }\n\n        outState.putBoolean(SHOW_ALL_MODE, showAllMode);\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {\n        if (resultCode == RESULT_CANCELED) {\n            multiSelectViewModel.unselectAll();\n            return;\n        }\n\n        switch (requestCode) {\n            // returns with a form path, start entry\n            case INSTANCE_UPLOADER:\n                if (intent.getBooleanExtra(FormFillingActivity.KEY_SUCCESS, false)) {\n                    listView.clearChoices();\n                    if (listAdapter.isEmpty()) {\n                        finish();\n                    }\n                }\n                break;\n        }\n        super.onActivityResult(requestCode, resultCode, intent);\n    }\n\n    private void setupAdapter() {\n        listAdapter = new InstanceUploaderAdapter(this, null, dbId -> {\n            multiSelectViewModel.toggle(dbId.toString());\n        });\n\n        listView.setAdapter(listAdapter);\n    }\n\n    private String getSortingOrderKey() {\n        return ProjectKeys.KEY_SAVED_FORM_SORT_ORDER;\n    }\n\n    private void updateAdapter() {\n        getSupportLoaderManager().restartLoader(LOADER_ID, null, this);\n    }\n\n    @NonNull\n    @Override\n    public Loader<Cursor> onCreateLoader(int id, Bundle args) {\n        showProgressBar();\n        if (showAllMode) {\n            return new CursorLoaderFactory(projectsDataService).createCompletedUndeletedInstancesCursorLoader(getFilterText(), getSortingOrder());\n        } else {\n            return new CursorLoaderFactory(projectsDataService).createFinalizedInstancesCursorLoader(getFilterText(), getSortingOrder());\n        }\n    }\n\n    @Override\n    public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {\n        hideProgressBarAndAllow();\n        listAdapter.changeCursor(cursor);\n        toggleButtonLabel(findViewById(R.id.toggle_button), listView);\n\n        if (listAdapter.isEmpty()) {\n            findViewById(R.id.buttonholder).setVisibility(View.GONE);\n        } else {\n            findViewById(R.id.buttonholder).setVisibility(View.VISIBLE);\n        }\n        readyToSendViewModel.update();\n    }\n\n    @Override\n    public void onLoaderReset(@NonNull Loader<Cursor> loader) {\n        listAdapter.swapCursor(null);\n    }\n\n    @Override\n    public boolean onLongClick(View v) {\n        return showSentAndUnsentChoices();\n    }\n\n    /*\n     * Create a dialog with options to save and exit, save, or quit without\n     * saving\n     */\n    private boolean showSentAndUnsentChoices() {\n        String[] items = {getString(org.odk.collect.strings.R.string.show_unsent_forms),\n                getString(org.odk.collect.strings.R.string.show_sent_and_unsent_forms)};\n\n        AlertDialog alertDialog = new MaterialAlertDialogBuilder(this)\n                .setTitle(getString(org.odk.collect.strings.R.string.change_view))\n                .setNeutralButton(getString(org.odk.collect.strings.R.string.cancel), (dialog, id) -> {\n                    dialog.cancel();\n                })\n                .setItems(items, (dialog, which) -> {\n                    switch (which) {\n                        case 0: // show unsent\n                            showAllMode = false;\n                            updateAdapter();\n                            break;\n\n                        case 1: // show all\n                            showAllMode = true;\n                            updateAdapter();\n                            break;\n\n                        case 2:// do nothing\n                            break;\n                    }\n                }).create();\n        alertDialog.show();\n        return true;\n    }\n\n    private String getSortingOrder() {\n        String sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + \" COLLATE NOCASE ASC, \" + DatabaseInstanceColumns.STATUS + \" DESC\";\n        switch (getSelectedSortingOrder()) {\n            case BY_NAME_ASC:\n                sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + \" COLLATE NOCASE ASC, \" + DatabaseInstanceColumns.STATUS + \" DESC\";\n                break;\n            case BY_NAME_DESC:\n                sortingOrder = DatabaseInstanceColumns.DISPLAY_NAME + \" COLLATE NOCASE DESC, \" + DatabaseInstanceColumns.STATUS + \" DESC\";\n                break;\n            case BY_DATE_ASC:\n                sortingOrder = DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE + \" ASC\";\n                break;\n            case BY_DATE_DESC:\n                sortingOrder = DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE + \" DESC\";\n                break;\n        }\n        return sortingOrder;\n    }\n\n    private int getSelectedSortingOrder() {\n        if (selectedSortingOrder == null) {\n            restoreSelectedSortingOrder();\n        }\n        return selectedSortingOrder;\n    }\n\n    private void restoreSelectedSortingOrder() {\n        selectedSortingOrder = settingsProvider.getUnprotectedSettings().getInt(getSortingOrderKey());\n    }\n\n    private void showProgressBar() {\n        progressBar.show();\n    }\n\n    private void hideProgressBarAndAllow() {\n        hideProgressBar();\n    }\n\n    private void hideProgressBar() {\n        progressBar.hide();\n    }\n\n    private CharSequence getFilterText() {\n        return filterText != null ? filterText : \"\";\n    }\n\n    private void saveSelectedSortingOrder(int selectedStringOrder) {\n        selectedSortingOrder = selectedStringOrder;\n        settingsProvider.getUnprotectedSettings().save(getSortingOrderKey(), selectedStringOrder);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBanner.kt",
    "content": "package org.odk.collect.android.instancemanagement.send\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport org.odk.collect.android.databinding.ReadyToSendBannerBinding\nimport org.odk.collect.shared.TimeInMs\nimport org.odk.collect.strings.R\n\nclass ReadyToSendBanner @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyle: Int = 0\n) : FrameLayout(context, attrs, defStyle) {\n\n    private val binding = ReadyToSendBannerBinding.inflate(LayoutInflater.from(context), this, true)\n\n    fun setData(data: ReadyToSendViewModel.Data) {\n        if (data.numberOfSentInstances > 0 && data.numberOfInstancesReadyToSend > 0) {\n            if (data.lastInstanceSentTimeMillis >= TimeInMs.ONE_DAY) {\n                val days: Int = (data.lastInstanceSentTimeMillis / TimeInMs.ONE_DAY).toInt()\n                binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_days_ago, days, days)\n            } else if (data.lastInstanceSentTimeMillis >= TimeInMs.ONE_HOUR) {\n                val hours: Int = (data.lastInstanceSentTimeMillis / TimeInMs.ONE_HOUR).toInt()\n                binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_hours_ago, hours, hours)\n            } else if (data.lastInstanceSentTimeMillis >= TimeInMs.ONE_MINUTE) {\n                val minutes: Int = (data.lastInstanceSentTimeMillis / TimeInMs.ONE_MINUTE).toInt()\n                binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_minutes_ago, minutes, minutes)\n            } else {\n                val seconds: Int = (data.lastInstanceSentTimeMillis / TimeInMs.ONE_SECOND).toInt()\n                binding.title.text = context.resources.getQuantityString(R.plurals.last_form_sent_seconds_ago, seconds, seconds)\n            }\n\n            binding.subtext.text = context.resources.getQuantityString(R.plurals.forms_ready_to_send, data.numberOfInstancesReadyToSend, data.numberOfInstancesReadyToSend)\n            binding.banner.visibility = VISIBLE\n        } else {\n            binding.banner.visibility = GONE\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModel.kt",
    "content": "package org.odk.collect.android.instancemanagement.send\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.instances.InstancesRepository\nimport java.util.function.Supplier\n\nclass ReadyToSendViewModel(\n    private val instancesRepository: InstancesRepository,\n    private val scheduler: Scheduler,\n    private val clock: Supplier<Long>\n) : ViewModel() {\n    private val _data = MutableLiveData<Data>()\n    val data: LiveData<Data> = _data\n\n    fun update() {\n        scheduler.immediate(\n            background = {\n                val sentInstances = instancesRepository.getAllByStatus(Instance.STATUS_SUBMITTED)\n                val numberOfSentInstances = sentInstances.size\n                val numberOfInstancesReadyToSend = instancesRepository.getCountByStatus(\n                    Instance.STATUS_COMPLETE,\n                    Instance.STATUS_SUBMISSION_FAILED\n                )\n                val lastInstanceSentTimeMillis = if (sentInstances.isNotEmpty()) {\n                    val lastSentInstance = sentInstances.maxBy { instance -> instance.lastStatusChangeDate }\n                    clock.get() - lastSentInstance.lastStatusChangeDate\n                } else {\n                    0\n                }\n                Data(numberOfInstancesReadyToSend, numberOfSentInstances, lastInstanceSentTimeMillis)\n            },\n            foreground = {\n                _data.value = it\n            }\n        )\n    }\n\n    open class Factory(\n        private val instancesRepository: InstancesRepository,\n        private val scheduler: Scheduler,\n        private val clock: Supplier<Long>\n    ) : ViewModelProvider.Factory {\n        override fun <T : ViewModel> create(modelClass: Class<T>): T {\n            return ReadyToSendViewModel(instancesRepository, scheduler, clock) as T\n        }\n    }\n\n    data class Data(\n        val numberOfInstancesReadyToSend: Int,\n        val numberOfSentInstances: Int,\n        val lastInstanceSentTimeMillis: Long\n    )\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/itemsets/FastExternalItemsetsRepository.kt",
    "content": "package org.odk.collect.android.itemsets\n\ninterface FastExternalItemsetsRepository {\n\n    fun deleteAllByCsvPath(path: String)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt",
    "content": "package org.odk.collect.android.javarosawrapper\n\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.core.services.transport.payload.ByteArrayPayload\nimport org.javarosa.form.api.FormEntryCaption\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.exception.JavaRosaException\nimport org.odk.collect.android.formentry.audit.AuditEventLogger\nimport org.odk.collect.entities.javarosa.finalization.EntitiesExtra\nimport java.io.File\nimport java.io.IOException\n\ninterface FormController {\n    fun getFormDef(): FormDef?\n\n    fun getMediaFolder(): File?\n\n    fun getInstanceFile(): File?\n\n    fun setInstanceFile(instanceFile: File?)\n\n    fun getAbsoluteInstancePath(): String?\n\n    fun getLastSavedPath(): String?\n\n    fun getIndexWaitingForData(): FormIndex?\n\n    fun setIndexWaitingForData(index: FormIndex?)\n\n    fun getAuditEventLogger(): AuditEventLogger?\n\n    fun isEditing(): Boolean\n\n    /**\n     * For logging purposes...\n     *\n     * @return xpath value for this index\n     */\n    fun getXPath(index: FormIndex?): String?\n\n    fun getIndexFromXPath(xpath: String): FormIndex?\n\n    /**\n     * returns the event for the current FormIndex.\n     */\n    fun getEvent(): Int\n\n    /**\n     * returns the event for the given FormIndex.\n     */\n    fun getEvent(index: FormIndex?): Int\n\n    /**\n     * @return current FormIndex.\n     */\n    fun getFormIndex(): FormIndex\n\n    /**\n     * @return the currently selected language.\n     */\n    fun getLanguage(): String?\n\n    /**\n     * Return the langauges supported by the currently loaded form.\n     *\n     * @return Array of Strings containing the languages embedded in the XForm.\n     */\n    fun getLanguages(): Array<String>?\n\n    /**\n     * Sets the current language.\n     */\n    fun setLanguage(language: String?)\n\n    /**\n     * @return A String containing the title of the current form.\n     */\n    fun getFormTitle(): String?\n\n    /**\n     * Return the caption for the current FormIndex. This is usually used for a repeat prompt.\n     */\n    fun getCaptionPrompt(): FormEntryCaption?\n\n    /**\n     * Returns a caption prompt for the given index. This is used to create a multi-question per\n     * screen view.\n     */\n    fun getCaptionPrompt(index: FormIndex?): FormEntryCaption?\n\n    /**\n     * This fires off the jr:preload actions and events to save values like the\n     * end time of a form.\n     */\n    fun finalizeForm()\n\n    /**\n     * Returns true if the question at the given FormIndex uses the search() appearance/function\n     * of \"fast itemset\" feature.\n     * <p>\n     * Precondition: there is a question at the given FormIndex.\n     */\n    fun usesDatabaseExternalDataFeature(index: FormIndex): Boolean\n\n    /**\n     * Tests if the current FormIndex is located inside a group that is marked as a \"field-list\"\n     *\n     * @return true if index is in a \"field-list\". False otherwise.\n     */\n    fun indexIsInFieldList(): Boolean\n\n    /**\n     * Tests if the FormIndex 'index' is located inside a group that is marked as a \"field-list\"\n     *\n     * @return true if index is in a \"field-list\". False otherwise.\n     */\n    fun indexIsInFieldList(index: FormIndex?): Boolean\n\n    fun currentPromptIsQuestion(): Boolean\n\n    fun isCurrentQuestionFirstInForm(): Boolean\n\n    /**\n     * Attempts to save answer into the given FormIndex into the data model.\n     */\n    @Throws(JavaRosaException::class)\n    fun answerQuestion(index: FormIndex?, data: IAnswerData?): Int\n\n    /**\n     * Validates a single answer against its constraint.\n     *\n     * @return SuccessValidationResult if the answer is valid, or an error result describing\n     * the violated constraint.\n     */\n    fun validateAnswerConstraint(index: FormIndex, answer: IAnswerData?): ValidationResult\n\n    /**\n     * Goes through the entire form to make sure all entered answers comply with their constraints.\n     * Constraints are ignored on 'jump to', so answers can be outside of constraints. We don't\n     * allow saving to disk, though, until all answers conform to their constraints/requirements.\n     *\n     * @return ANSWER_OK and leave index unchanged or change index to bad value and return error\n     * type.\n     */\n    @Throws(JavaRosaException::class)\n    fun validateAnswers(moveToInvalidIndex: Boolean): ValidationResult\n\n    /**\n     * saveAnswer attempts to save the current answer into the data model without doing any\n     * constraint checking. Only use this if you know what you're doing. For normal form filling\n     * you\n     * should always use answerQuestion or answerCurrentQuestion.\n     *\n     * @return true if saved successfully, false otherwise.\n     */\n    @Throws(JavaRosaException::class)\n    fun saveAnswer(index: FormIndex?, data: IAnswerData?): Boolean\n\n    /**\n     * Navigates forward in the form.\n     *\n     * @return the next event that should be handled by a view.\n     */\n    fun stepToNextEvent(stepIntoGroup: Boolean): Int\n\n    /**\n     * Navigates backward in the form.\n     *\n     * @return the event that should be handled by a view.\n     */\n    fun stepToPreviousEvent(): Int\n\n    /**\n     * Jumps to a given FormIndex.\n     *\n     * @return EVENT for the specified Index.\n     */\n    fun jumpToIndex(index: FormIndex?): Int\n\n    /**\n     * If using a view like HierarchyView that doesn't support multi-question per screen, step over\n     * the group represented by the FormIndex.\n     */\n    fun stepOverGroup(): Int\n\n    /**\n     * Move the current form index to the index of the previous question in the form.\n     * Step backward out of repeats and groups as needed. If the resulting question\n     * is itself within a field-list, move upward to the group or repeat defining that\n     * field-list.\n     */\n    @Throws(JavaRosaException::class)\n    fun stepToPreviousScreenEvent(): Int\n\n    /**\n     * Move the current form index to the index of the next question in the form.\n     * Stop if we should ask to create a new repeat group or if we reach the end of the form.\n     * If we enter a group or repeat, return that if it is a field-list definition.\n     * Otherwise, descend into the group or repeat searching for the first question.\n     */\n    @Throws(JavaRosaException::class)\n    fun stepToNextScreenEvent(): Int\n\n    /**\n     * Move the current form index to the index of the first displayable group\n     * (that is, a repeatable group or a visible group),\n     * or to the start of the form.\n     */\n    fun stepToOuterScreenEvent(): Int\n\n    /**\n     * Jumps to the next prompt for a repeated instance of the group referenced by the current FormIndex.\n     */\n    fun jumpToNewRepeatPrompt()\n\n    /**\n     * Returns true if the index is either a repeatable group or a visible group.\n     */\n    fun isDisplayableGroup(index: FormIndex?): Boolean\n\n    @Throws(JavaRosaException::class)\n    fun saveOneScreenAnswer(index: FormIndex?, data: IAnswerData?, evaluateConstraints: Boolean): ValidationResult\n\n    /**\n     * @return FailedConstraint of first failed constraint or null if all questions were saved.\n     */\n    @Throws(JavaRosaException::class)\n    fun saveAllScreenAnswers(answers: HashMap<FormIndex, IAnswerData>?, evaluateConstraints: Boolean): ValidationResult\n\n    /**\n     * Creates a new repeated instance of the group referenced by the current FormIndex.\n     */\n    fun newRepeat()\n\n    /**\n     * If the current FormIndex is within a repeated group, will find the innermost repeat, delete\n     * it, and jump the FormEntryController to the previous valid index. That is, if you have\n     * group1\n     * (2) > group2 (3) and you call deleteRepeat, it will delete the 3rd instance of group2.\n     */\n    fun deleteRepeat()\n\n    fun getQuestionPrompt(): FormEntryPrompt?\n\n    fun getQuestionPrompt(index: FormIndex?): FormEntryPrompt?\n\n    /**\n     * Returns an array of question prompts corresponding to the current {@link FormIndex}. These\n     * are the prompts that should be displayed to the user and don't include any non-relevant\n     * questions.\n     * <p>\n     * The array has a single element if there is a question at this {@link FormIndex} or multiple\n     * elements if there is a group.\n     *\n     * @throws RepeatsInFieldListException if there is a group at this {@link FormIndex} and it contains\n     *                          elements that are not questions or regular (non-repeat) groups.\n     */\n    @Throws(RepeatsInFieldListException::class)\n    fun getQuestionPrompts(index: FormIndex): Array<FormEntryPrompt>\n\n    fun getQuestionPromptConstraintText(index: FormIndex?): String?\n\n    fun getQuestionPromptRequiredText(index: FormIndex?): String?\n\n    fun currentCaptionPromptIsQuestion(): Boolean\n\n    /**\n     * @return true if a group contains at least one relevant question, otherwise false\n     */\n    fun isGroupRelevant(): Boolean\n\n    fun getGroupsForIndex(formIndex: FormIndex?): Array<FormEntryCaption>\n\n    /**\n     * This is used to enable/disable the \"Delete Repeat\" menu option.\n     */\n    fun indexContainsRepeatableGroup(): Boolean\n\n    fun indexContainsRepeatableGroup(formIndex: FormIndex?): Boolean\n\n    /**\n     * The count of the closest group that repeats or -1.\n     */\n    fun getLastRepeatedGroupRepeatCount(): Int\n\n    /**\n     * The name of the closest group that repeats or null.\n     */\n    fun getLastRepeatedGroupName(): String?\n\n    /**\n     * The text of closest group the prompt belongs to.\n     */\n    fun getLastGroupText(): String?\n\n    /**\n     * Once a submission is marked as complete, it is saved in the\n     * submission format, which might be a fragment of the original\n     * form.\n     *\n     * @return true if the submission is the entire form.  If it is,\n     * then the submission can be re-opened for editing\n     * after it was marked-as-complete (provided it has\n     * not been encrypted).\n     */\n    fun isSubmissionEntireForm(): Boolean\n\n    /**\n     * Constructs the XML payload for a filled-in form instance. This payload\n     * enables a filled-in form to be re-opened and edited.\n     */\n    @Throws(IOException::class)\n    fun getFilledInFormXml(): ByteArrayPayload\n\n    /**\n     * Extract the portion of the form that should be uploaded to the server.\n     */\n    @Throws(IOException::class)\n    fun getSubmissionXml(): ByteArrayPayload?\n\n    /**\n     * Get the OpenRosa required metadata of the portion of the form beng submitted\n     */\n    fun getSubmissionMetadata(): InstanceMetadata?\n\n    /**\n     * Returns true if the current form definition audits user location in the background.\n     */\n    fun currentFormAuditsLocation(): Boolean\n\n    /**\n     * Returns true if the current form definition collects user location in the background either\n     * because of the audit configuration or because it contains odk:setgeopoint actions.\n     */\n    fun currentFormCollectsBackgroundLocation(): Boolean\n\n    fun getAnswer(treeReference: TreeReference?): IAnswerData?\n\n    fun getEntities(): EntitiesExtra?\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormControllerExt.kt",
    "content": "package org.odk.collect.android.javarosawrapper\n\nimport org.javarosa.form.api.FormEntryCaption\nimport org.javarosa.form.api.FormEntryPrompt\n\nobject FormControllerExt {\n\n    @JvmStatic\n    @Throws(RepeatsInFieldListException::class)\n    fun FormController.getQuestionPrompts(): Array<FormEntryPrompt> {\n        return getQuestionPrompts(getFormIndex())\n    }\n\n    @JvmStatic\n    fun FormController.getGroupsForCurrentIndex(): Array<FormEntryCaption> {\n        return getGroupsForIndex(getFormIndex())\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormDesignException.kt",
    "content": "/*\n * Copyright (C) 2020 ODK\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.javarosawrapper\n\nimport java.lang.Exception\n\n/**\n * Thrown when there is an issue in form design. This type of error requires the form designer to\n * modify the form.\n */\nopen class FormDesignException(message: String) : Exception(message)\n\nclass RepeatsInFieldListException(message: String) : FormDesignException(message)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormIndexUtils.java",
    "content": "package org.odk.collect.android.javarosawrapper;\n\nimport androidx.annotation.Nullable;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.GroupDef;\nimport org.javarosa.core.model.IFormElement;\n\npublic final class FormIndexUtils {\n\n    private FormIndexUtils() {\n\n    }\n\n    /**\n     * Used to find one level up from the formIndex. That is, if you're at 5_0, 1 (the second question\n     * in a repeating group), this method will return a FormIndex of 5_0 (the start of the repeating\n     * group). If you're at index 16 or 5_0, this will return null\n     */\n    @Nullable\n    public static FormIndex getPreviousLevel(FormIndex index) {\n        if (index.isTerminal()) {\n            return null;\n        } else {\n            return new FormIndex(getPreviousLevel(index.getNextLevel()), index);\n        }\n    }\n\n    @Nullable\n    public static FormIndex getRepeatGroupIndex(FormIndex index, FormDef formDef) {\n        IFormElement element = formDef.getChild(index);\n        if (element instanceof GroupDef && ((GroupDef) element).getRepeat()) {\n            return index;\n        } else {\n            FormIndex previousLevel = getPreviousLevel(index);\n\n            if (previousLevel != null) {\n                return getRepeatGroupIndex(previousLevel, formDef);\n            } else {\n                return null;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/javarosawrapper/InstanceMetadata.java",
    "content": "package org.odk.collect.android.javarosawrapper;\n\nimport org.odk.collect.android.formentry.audit.AuditConfig;\nimport org.odk.collect.android.utilities.FormNameUtils;\n\n/**\n * OpenRosa metadata of a form instance.\n * <p>\n * Contains the values for the required metadata\n * fields and nothing else.\n *\n * @author mitchellsundt@gmail.com\n */\npublic class InstanceMetadata {\n    public final String instanceId;\n    public final String instanceName;\n    public final AuditConfig auditConfig;\n\n    public InstanceMetadata(String instanceId, String instanceName, AuditConfig auditConfig) {\n        this.instanceId = instanceId;\n        this.instanceName = FormNameUtils.normalizeFormName(instanceName, false);\n        this.auditConfig = auditConfig;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java",
    "content": "/*\n * Copyright (C) 2009 JavaRosa\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.javarosawrapper;\n\nimport static org.odk.collect.android.javarosawrapper.FormIndexUtils.getPreviousLevel;\nimport static org.odk.collect.android.javarosawrapper.FormIndexUtils.getRepeatGroupIndex;\nimport static org.odk.collect.android.utilities.ApplicationConstants.Namespaces.XML_OPENDATAKIT_NAMESPACE;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.GroupDef;\nimport org.javarosa.core.model.IDataReference;\nimport org.javarosa.core.model.IFormElement;\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.SubmissionProfile;\nimport org.javarosa.core.model.ValidateOutcome;\nimport org.javarosa.core.model.actions.setgeopoint.SetGeopointActionHandler;\nimport org.javarosa.core.model.condition.EvaluationContext;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.model.instance.FormInstance;\nimport org.javarosa.core.model.instance.TreeElement;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.javarosa.core.services.transport.payload.ByteArrayPayload;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.javarosa.form.api.FormEntryController;\nimport org.javarosa.form.api.FormEntryModel;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.javarosa.model.xform.XFormSerializingVisitor;\nimport org.javarosa.model.xform.XPathReference;\nimport org.javarosa.xform.parse.XFormParser;\nimport org.javarosa.xpath.XPathParseTool;\nimport org.javarosa.xpath.expr.XPathExpression;\nimport org.odk.collect.android.dynamicpreload.ExternalDataUtil;\nimport org.odk.collect.android.exception.JavaRosaException;\nimport org.odk.collect.android.formentry.audit.AsyncTaskAuditEventWriter;\nimport org.odk.collect.android.formentry.audit.AuditConfig;\nimport org.odk.collect.android.formentry.audit.AuditEventLogger;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.entities.javarosa.finalization.EntitiesExtra;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\n\nimport timber.log.Timber;\n\n/**\n * This class is a wrapper for Javarosa's FormEntryController. In theory, if you wanted to replace\n * javarosa as the form engine, you should only need to replace the methods in this file. Also, we\n * haven't wrapped every method provided by FormEntryController, only the ones we've needed so far.\n * Feel free to add more as necessary.\n *\n * @author carlhartung\n */\npublic class JavaRosaFormController implements FormController {\n\n    public static final boolean STEP_INTO_GROUP = true;\n    public static final boolean STEP_OVER_GROUP = false;\n\n    /**\n     * OpenRosa metadata tag names.\n     */\n    public static final String INSTANCE_ID = \"instanceID\";\n    private static final String INSTANCE_NAME = \"instanceName\";\n\n    /*\n     * Non OpenRosa metadata tag names\n     */\n    private static final String AUDIT = \"audit\";\n    public static final String AUDIT_FILE_NAME = \"audit.csv\";\n    private final boolean isEditing;\n\n    /*\n     * Store the auditEventLogger object with the form controller state\n     */\n    private AuditEventLogger auditEventLogger;\n\n    private final File mediaFolder;\n    @Nullable\n    private File instanceFile;\n    private final FormEntryController formEntryController;\n    private FormIndex indexWaitingForData;\n\n    public JavaRosaFormController(File mediaFolder, FormEntryController fec, File instanceFile) {\n        this.mediaFolder = mediaFolder;\n        formEntryController = fec;\n        this.instanceFile = instanceFile;\n        isEditing = instanceFile != null;\n    }\n\n    @Override\n    public boolean isEditing() {\n        return isEditing;\n    }\n\n    public FormDef getFormDef() {\n        return formEntryController.getModel().getForm();\n    }\n\n    public File getMediaFolder() {\n        return mediaFolder;\n    }\n\n    @Nullable\n    public File getInstanceFile() {\n        return instanceFile;\n    }\n\n    public void setInstanceFile(File instanceFile) {\n        this.instanceFile = instanceFile;\n    }\n\n    @Nullable\n    public String getAbsoluteInstancePath() {\n        return instanceFile != null ? instanceFile.getAbsolutePath() : null;\n    }\n\n    @Nullable\n    public String getLastSavedPath() {\n        return mediaFolder != null ? FileUtils.getLastSavedPath(mediaFolder) : null;\n    }\n\n    public void setIndexWaitingForData(FormIndex index) {\n        indexWaitingForData = index;\n    }\n\n    public FormIndex getIndexWaitingForData() {\n        return indexWaitingForData;\n    }\n\n    public AuditEventLogger getAuditEventLogger() {\n        if (auditEventLogger == null) {\n            AuditConfig auditConfig = getSubmissionMetadata().auditConfig;\n\n            if (auditConfig != null) {\n                auditEventLogger = new AuditEventLogger(auditConfig, new AsyncTaskAuditEventWriter(new File(instanceFile.getParentFile().getPath() + File.separator + AUDIT_FILE_NAME), auditConfig.isLocationEnabled(), auditConfig.isTrackingChangesEnabled(), auditConfig.isIdentifyUserEnabled(), auditConfig.isTrackChangesReasonEnabled()), this);\n            } else {\n                auditEventLogger = new AuditEventLogger(null, null, this);\n            }\n        }\n\n        return auditEventLogger;\n    }\n\n    public String getXPath(FormIndex index) {\n        String value;\n        switch (getEvent(index)) {\n            case FormEntryController.EVENT_BEGINNING_OF_FORM:\n                value = \"beginningOfForm\";\n                break;\n            case FormEntryController.EVENT_END_OF_FORM:\n                value = \"endOfForm\";\n                break;\n            case FormEntryController.EVENT_GROUP:\n                value = \"group.\" + index.getReference().toString();\n                break;\n            case FormEntryController.EVENT_QUESTION:\n                value = \"question.\" + index.getReference().toString();\n                break;\n            case FormEntryController.EVENT_PROMPT_NEW_REPEAT:\n                value = \"promptNewRepeat.\" + index.getReference().toString();\n                break;\n            case FormEntryController.EVENT_REPEAT:\n                value = \"repeat.\" + index.getReference().toString();\n                break;\n            case FormEntryController.EVENT_REPEAT_JUNCTURE:\n                value = \"repeatJuncture.\" + index.getReference().toString();\n                break;\n            default:\n                value = \"unexpected\";\n                break;\n        }\n        return value;\n    }\n\n    @Nullable\n    public FormIndex getIndexFromXPath(String xpath) {\n        switch (xpath) {\n            case \"beginningOfForm\":\n                return FormIndex.createBeginningOfFormIndex();\n            case \"endOfForm\":\n                return FormIndex.createEndOfFormIndex();\n            case \"unexpected\":\n                Timber.e(new Error(\"Unexpected string from XPath\"));\n                return null;\n            default:\n                FormIndex returned = null;\n                FormIndex saved = getFormIndex();\n                // the only way I know how to do this is to step through the entire form\n                // until the XPath of a form entry matches that of the supplied XPath\n                try {\n                    jumpToIndex(FormIndex.createBeginningOfFormIndex());\n                    int event = stepToNextEvent(true);\n                    while (event != FormEntryController.EVENT_END_OF_FORM) {\n                        String candidateXPath = getXPath(getFormIndex());\n                        if (candidateXPath.equals(xpath)) {\n                            returned = getFormIndex();\n                            break;\n                        }\n                        event = stepToNextEvent(true);\n                    }\n                } finally {\n                    jumpToIndex(saved);\n                }\n                return returned;\n        }\n    }\n\n    public int getEvent() {\n        return formEntryController.getModel().getEvent();\n    }\n\n    public int getEvent(FormIndex index) {\n        return formEntryController.getModel().getEvent(index);\n    }\n\n    public FormIndex getFormIndex() {\n        return formEntryController.getModel().getFormIndex();\n    }\n\n    public String[] getLanguages() {\n        return formEntryController.getModel().getLanguages();\n    }\n\n    public String getFormTitle() {\n        return formEntryController.getModel().getFormTitle();\n    }\n\n    public String getLanguage() {\n        return formEntryController.getModel().getLanguage();\n    }\n\n    private String getBindAttribute(FormIndex idx, String attributeNamespace, String attributeName) {\n        return formEntryController.getModel().getForm().getMainInstance().resolveReference(\n                idx.getReference()).getBindAttributeValue(attributeNamespace, attributeName);\n    }\n\n    /**\n     * @return an array of FormEntryCaptions for the current FormIndex. This is how we get group\n     * information Group 1 > Group 2> etc... The element at [size-1] is the current question\n     * text, with group names decreasing in hierarchy until array element at [0] is the root\n     */\n    private FormEntryCaption[] getCaptionHierarchy() {\n        return formEntryController.getModel().getCaptionHierarchy();\n    }\n\n    /**\n     * @return an array of FormEntryCaptions for the supplied FormIndex. This is how we get group\n     * information Group 1 > Group 2> etc... The element at [size-1] is the current question\n     * text, with group names decreasing in hierarchy until array element at [0] is the root\n     */\n    private FormEntryCaption[] getCaptionHierarchy(FormIndex index) {\n        return formEntryController.getModel().getCaptionHierarchy(index);\n    }\n\n    public FormEntryCaption getCaptionPrompt(FormIndex index) {\n        return formEntryController.getModel().getCaptionPrompt(index);\n    }\n\n    public FormEntryCaption getCaptionPrompt() {\n        return formEntryController.getModel().getCaptionPrompt();\n    }\n\n    public void finalizeForm() {\n        formEntryController.finalizeFormEntry();\n    }\n\n    /**\n     * TODO: We need a good description of what this does, exactly, and why.\n     */\n    private FormInstance getInstance() {\n        return formEntryController.getModel().getForm().getInstance();\n    }\n\n    /**\n     * A convenience method for determining if the current FormIndex is in a group that is/should\n     * be\n     * displayed as a multi-question view. This is useful for returning from the formhierarchy view\n     * to a selected index.\n     */\n    private boolean groupIsFieldList(FormIndex index) {\n        // if this isn't a group, return right away\n        IFormElement element = formEntryController.getModel().getForm().getChild(index);\n        if (!(element instanceof GroupDef) || element.getAppearanceAttr() == null) {\n            return false;\n        }\n\n        return element.getAppearanceAttr().toLowerCase(Locale.ENGLISH).contains(Appearances.FIELD_LIST);\n    }\n\n    private boolean repeatIsFieldList(FormIndex index) {\n        return groupIsFieldList(index);\n    }\n\n    /**\n     * Returns the `appearance` attribute of the current index, if any.\n     */\n    private String getAppearanceAttr(@NonNull FormIndex index) {\n        // FormDef can't have an appearance, it would throw an exception.\n        if (index.isBeginningOfFormIndex()) {\n            return null;\n        }\n\n        IFormElement element = formEntryController.getModel().getForm().getChild(index);\n        return element.getAppearanceAttr();\n    }\n\n    public boolean usesDatabaseExternalDataFeature(@NonNull FormIndex index) {\n        String queryAttribute = getFormDef().getChild(index).getAdditionalAttribute(null, \"query\");\n        String appearanceAttribute = getAppearanceAttr(index);\n\n        return appearanceAttribute != null && ExternalDataUtil.SEARCH_FUNCTION_REGEX.matcher(appearanceAttribute).find()\n                || queryAttribute != null && queryAttribute.length() > 0;\n    }\n\n    public boolean indexIsInFieldList(FormIndex index) {\n        int event = getEvent(index);\n        if (event == FormEntryController.EVENT_QUESTION) {\n            // caption[0..len-1]\n            // caption[len-1] == the question itself\n            // caption[len-2] == the first group it is contained in.\n            FormEntryCaption[] captions = getCaptionHierarchy(index);\n            if (captions.length < 2) {\n                // no group\n                return false;\n            }\n            // If at least one of the groups you are inside is a field list, your index is in a field list\n            for (FormEntryCaption caption : captions) {\n                if (groupIsFieldList(caption.getIndex())) {\n                    return true;\n                }\n            }\n            return false;\n        } else if (event == FormEntryController.EVENT_GROUP) {\n            return groupIsFieldList(index);\n        } else if (event == FormEntryController.EVENT_REPEAT) {\n            return repeatIsFieldList(index);\n        } else {\n            return false;\n        }\n\n    }\n\n    public boolean indexIsInFieldList() {\n        return indexIsInFieldList(getFormIndex());\n    }\n\n    public boolean currentPromptIsQuestion() {\n        return getEvent() == FormEntryController.EVENT_QUESTION\n                || ((getEvent() == FormEntryController.EVENT_GROUP\n                || getEvent() == FormEntryController.EVENT_REPEAT)\n                && indexIsInFieldList());\n    }\n\n    public boolean isCurrentQuestionFirstInForm() {\n        boolean isFirstQuestion = true;\n        FormIndex originalFormIndex = getFormIndex();\n        try {\n            isFirstQuestion = stepToPreviousScreenEvent() == FormEntryController.EVENT_BEGINNING_OF_FORM\n                    && stepToNextScreenEvent() != FormEntryController.EVENT_PROMPT_NEW_REPEAT;\n        } catch (JavaRosaException e) {\n            Timber.d(e);\n        }\n        jumpToIndex(originalFormIndex);\n        return isFirstQuestion;\n    }\n\n    public int answerQuestion(FormIndex index, IAnswerData data) throws JavaRosaException {\n        try {\n            return formEntryController.answerQuestion(index, data, true);\n        } catch (Exception e) {\n            throw new JavaRosaException(e);\n        }\n    }\n\n    public ValidationResult validateAnswerConstraint(FormIndex index, IAnswerData answer) {\n        boolean isAnswerValid = getFormDef().evaluateConstraint(index.getReference(), answer);\n        if (isAnswerValid) {\n            return SuccessValidationResult.INSTANCE;\n        } else {\n            return getFailedValidationResult(index, FormEntryController.ANSWER_CONSTRAINT_VIOLATED);\n        }\n    }\n\n    public ValidationResult validateAnswers(boolean moveToInvalidIndex) throws JavaRosaException {\n        try {\n            ValidateOutcome validateOutcome = getFormDef().validate();\n            if (validateOutcome != null) {\n                if (moveToInvalidIndex) {\n                    this.jumpToIndex(validateOutcome.failedPrompt);\n                    if (indexIsInFieldList()) {\n                        stepToPreviousScreenEvent();\n                    }\n                }\n                return getFailedValidationResult(validateOutcome.failedPrompt, validateOutcome.outcome);\n            }\n            return SuccessValidationResult.INSTANCE;\n        } catch (RuntimeException e) {\n            throw new JavaRosaException(e);\n        }\n    }\n\n    private ValidationResult getFailedValidationResult(FormIndex index, int status) {\n        ValidationResult validationResult = null;\n\n        String errorMessage;\n        if (status == FormEntryController.ANSWER_CONSTRAINT_VIOLATED) {\n            errorMessage = getQuestionPromptConstraintText(index);\n            if (errorMessage == null) {\n                errorMessage = getQuestionPrompt(index).getSpecialFormQuestionText(\"constraintMsg\");\n            }\n            validationResult = new FailedValidationResult(index, status, errorMessage, org.odk.collect.strings.R.string.invalid_answer_error);\n        } else if (status == FormEntryController.ANSWER_REQUIRED_BUT_EMPTY) {\n            errorMessage = getQuestionPromptRequiredText(index);\n            if (errorMessage == null) {\n                errorMessage = getQuestionPrompt(index).getSpecialFormQuestionText(\"requiredMsg\");\n            }\n            validationResult = new FailedValidationResult(index, status, errorMessage, org.odk.collect.strings.R.string.required_answer_error);\n        }\n        return validationResult;\n    }\n\n    public boolean saveAnswer(FormIndex index, IAnswerData data) throws JavaRosaException {\n        try {\n            return formEntryController.saveAnswer(index, data, true);\n        } catch (Exception e) {\n            String dataType = data != null ? data.getClass().toString() : null;\n            String ref = index != null ? index.getReference().toString() : null;\n            Timber.w(\"Error saving answer of type %s with ref %s for index %s\",\n                    dataType, ref, index);\n\n            throw new JavaRosaException(e);\n        }\n    }\n\n    public int stepToNextEvent(boolean stepIntoGroup) {\n        if ((getEvent() == FormEntryController.EVENT_GROUP\n                || getEvent() == FormEntryController.EVENT_REPEAT)\n                && indexIsInFieldList() && !isGroupEmpty() && !stepIntoGroup) {\n            return stepOverGroup();\n        } else {\n            return formEntryController.stepToNextEvent();\n        }\n    }\n\n    public int stepOverGroup() {\n        GroupDef gd =\n                (GroupDef) formEntryController.getModel().getForm()\n                        .getChild(getFormIndex());\n        List<FormIndex> indices = getIndicesForGroup(gd);\n\n        // jump to the end of the group\n        formEntryController.jumpToIndex(indices.get(indices.size() - 1));\n        return stepToNextEvent(STEP_OVER_GROUP);\n    }\n\n    public int stepToPreviousScreenEvent() throws JavaRosaException {\n        try {\n            if (getEvent() != FormEntryController.EVENT_BEGINNING_OF_FORM) {\n                int event = stepToPreviousEvent();\n\n                while (event == FormEntryController.EVENT_REPEAT_JUNCTURE\n                        || event == FormEntryController.EVENT_PROMPT_NEW_REPEAT\n                        || (event == FormEntryController.EVENT_QUESTION && indexIsInFieldList())\n                        || ((event == FormEntryController.EVENT_GROUP\n                        || event == FormEntryController.EVENT_REPEAT)\n                        && !indexIsInFieldList())) {\n                    event = stepToPreviousEvent();\n                }\n\n                // Handle nested field-list group\n                if (getEvent() == FormEntryController.EVENT_GROUP) {\n                    FormIndex currentIndex = getFormIndex();\n\n                    if (groupIsFieldList(currentIndex)) {\n                        // jump to outermost containing field-list\n                        FormEntryCaption[] fclist = this.getCaptionHierarchy(currentIndex);\n                        for (FormEntryCaption caption : fclist) {\n                            if (groupIsFieldList(caption.getIndex())) {\n                                formEntryController.jumpToIndex(caption.getIndex());\n                                break;\n                            }\n                        }\n                    }\n                }\n            }\n            return getEvent();\n        } catch (RuntimeException e) {\n            throw new JavaRosaException(e);\n        }\n    }\n\n    public int stepToNextScreenEvent() throws JavaRosaException {\n        try {\n            if (getEvent() != FormEntryController.EVENT_END_OF_FORM) {\n                int event;\n                group_skip:\n                do {\n                    event = stepToNextEvent(STEP_OVER_GROUP);\n                    switch (event) {\n                        case FormEntryController.EVENT_QUESTION:\n                        case FormEntryController.EVENT_END_OF_FORM:\n                        case FormEntryController.EVENT_PROMPT_NEW_REPEAT:\n                            break group_skip;\n                        case FormEntryController.EVENT_GROUP:\n                        case FormEntryController.EVENT_REPEAT:\n                            try {\n                                if (indexIsInFieldList() && FormControllerExt.getQuestionPrompts(this).length != 0) {\n                                    break group_skip;\n                                }\n                            } catch (RepeatsInFieldListException e) {\n                                break group_skip;\n                            }\n                            // otherwise it's not a field-list group, so just skip it\n                            break;\n                        case FormEntryController.EVENT_REPEAT_JUNCTURE:\n                            Timber.i(\"repeat juncture: %s\", getFormIndex().getReference().toString());\n                            // skip repeat junctures until we implement them\n                            break;\n                        default:\n                            Timber.w(\"JavaRosa added a new EVENT type and didn't tell us... shame on them.\");\n                            break;\n                    }\n                } while (event != FormEntryController.EVENT_END_OF_FORM);\n            }\n            return getEvent();\n        } catch (RuntimeException e) {\n            throw new JavaRosaException(e);\n        }\n    }\n\n    /**\n     * Move the current form index to the next event of the given type\n     * (or the end if none is found).\n     */\n    private int stepToNextEventType(int eventType) {\n        int event = getEvent();\n        do {\n            if (event == FormEntryController.EVENT_END_OF_FORM) {\n                break;\n            }\n            event = stepToNextEvent(STEP_OVER_GROUP);\n        } while (event != eventType);\n\n        return event;\n    }\n\n    public int stepToOuterScreenEvent() {\n        FormIndex index = getFormIndex();\n\n        // Step out once to begin with if we're coming from a question.\n        if (getEvent() == FormEntryController.EVENT_QUESTION) {\n            index = getPreviousLevel(index);\n        }\n\n        // Save where we started from.\n        FormIndex startIndex = index;\n\n        // Step out once more no matter what.\n        index = getPreviousLevel(index);\n\n        // Step out of any group indexes that are present, unless they're visible.\n        while (index != null\n                && getEvent(index) == FormEntryController.EVENT_GROUP\n                && !isDisplayableGroup(index)) {\n            index = getPreviousLevel(index);\n        }\n\n        if (index == null) {\n            jumpToIndex(FormIndex.createBeginningOfFormIndex());\n        } else {\n            if (isDisplayableGroup(startIndex)) {\n                // We were at a displayable group, so stepping back brought us to the previous level\n                jumpToIndex(index);\n            } else {\n                // We were at a question, so stepping back brought us to either:\n                // The beginning, or the start of a displayable group. So we need to step\n                // out again to go past the group.\n                index = getPreviousLevel(index);\n                if (index == null) {\n                    jumpToIndex(FormIndex.createBeginningOfFormIndex());\n                } else {\n                    jumpToIndex(index);\n                }\n            }\n        }\n        return getEvent();\n    }\n\n    public boolean isDisplayableGroup(FormIndex index) {\n        int event = getEvent(index);\n        return event == FormEntryController.EVENT_REPEAT\n                || event == FormEntryController.EVENT_PROMPT_NEW_REPEAT\n                || (event == FormEntryController.EVENT_GROUP\n                && isPresentationGroup(index) && isLogicalGroup(index));\n    }\n\n    /**\n     * Returns true if the group has a displayable label,\n     * i.e. it's a \"presentation group\".\n     */\n    private boolean isPresentationGroup(FormIndex groupIndex) {\n        String label = getCaptionPrompt(groupIndex).getShortText();\n        return label != null;\n    }\n\n    /**\n     * Returns true if the group has an XML `ref` attribute,\n     * i.e. it's a \"logical group\".\n     * <p>\n     * TODO: Improve this nasty way to recreate what XFormParser#parseGroup does for nodes without a `ref`.\n     */\n    private boolean isLogicalGroup(FormIndex groupIndex) {\n        TreeReference groupRef = groupIndex.getReference();\n        TreeReference parentRef = groupRef.getParentRef();\n        IDataReference absRef = FormDef.getAbsRef(new XPathReference(groupRef), parentRef);\n        IDataReference bindRef = getCaptionPrompt(groupIndex).getFormElement().getBind();\n        // If the group's bind is equal to what it would have been set to during parsing, it must not have a ref.\n        return !absRef.equals(bindRef);\n    }\n\n    public ValidationResult saveAllScreenAnswers(HashMap<FormIndex, IAnswerData> answers, boolean evaluateConstraints) throws JavaRosaException {\n        if (currentPromptIsQuestion()) {\n            for (FormIndex index : answers.keySet()) {\n                ValidationResult validationResult = saveOneScreenAnswer(\n                        index,\n                        answers.get(index),\n                        evaluateConstraints\n                );\n                if (validationResult instanceof FailedValidationResult) {\n                    return validationResult;\n                }\n            }\n        }\n\n        return SuccessValidationResult.INSTANCE;\n    }\n\n    public ValidationResult saveOneScreenAnswer(FormIndex index, IAnswerData answer, boolean evaluateConstraints) throws JavaRosaException {\n        // Within a group, you can only save for question events\n        if (getEvent(index) == FormEntryController.EVENT_QUESTION) {\n            saveAnswer(index, answer);\n            if (evaluateConstraints) {\n                int saveStatus = answerQuestion(index, answer);\n                if (saveStatus != FormEntryController.ANSWER_OK) {\n                    return getFailedValidationResult(index, saveStatus);\n                }\n            }\n        } else {\n            Timber.w(\"Attempted to save an index referencing something other than a question: %s\",\n                    index.getReference().toString());\n        }\n        return SuccessValidationResult.INSTANCE;\n    }\n\n    public int stepToPreviousEvent() {\n        /*\n         * Right now this will always skip to the beginning of a group if that group is represented\n         * as a 'field-list'. Should a need ever arise to step backwards by only one step in a\n         * 'field-list', this method will have to be updated.\n         */\n\n        formEntryController.stepToPreviousEvent();\n\n        // If after we've stepped, we're in a field-list, jump back to the beginning of the group\n        //\n\n        if (indexIsInFieldList()\n                && getEvent() == FormEntryController.EVENT_QUESTION) {\n            // caption[0..len-1]\n            // caption[len-1] == the question itself\n            // caption[len-2] == the first group it is contained in.\n            FormEntryCaption[] captions = getCaptionHierarchy();\n            FormEntryCaption grp = captions[captions.length - 2];\n            int event = formEntryController.jumpToIndex(grp.getIndex());\n            // and test if this group or at least one of its children is relevant...\n            FormIndex idx = grp.getIndex();\n            if (!formEntryController.getModel().isIndexRelevant(idx)) {\n                return stepToPreviousEvent();\n            }\n            idx = formEntryController.getModel().incrementIndex(idx, true);\n            while (FormIndex.isSubElement(grp.getIndex(), idx)) {\n                if (formEntryController.getModel().isIndexRelevant(idx)) {\n                    return event;\n                }\n                idx = formEntryController.getModel().incrementIndex(idx, true);\n            }\n            return stepToPreviousEvent();\n        } else if (indexIsInFieldList() && getEvent() == FormEntryController.EVENT_GROUP) {\n            FormIndex grpidx = formEntryController.getModel().getFormIndex();\n            int event = formEntryController.getModel().getEvent();\n            // and test if this group or at least one of its children is relevant...\n            if (!formEntryController.getModel().isIndexRelevant(grpidx)) {\n                return stepToPreviousEvent(); // shouldn't happen?\n            }\n            FormIndex idx = formEntryController.getModel().incrementIndex(grpidx, true);\n            while (FormIndex.isSubElement(grpidx, idx)) {\n                if (formEntryController.getModel().isIndexRelevant(idx)) {\n                    return event;\n                }\n                idx = formEntryController.getModel().incrementIndex(idx, true);\n            }\n            return stepToPreviousEvent();\n        }\n\n        return getEvent();\n\n    }\n\n    public int jumpToIndex(FormIndex index) {\n        return formEntryController.jumpToIndex(index);\n    }\n\n    public void jumpToNewRepeatPrompt() {\n        FormIndex repeatGroupIndex = getRepeatGroupIndex(getFormIndex(), getFormDef());\n        Integer depth = repeatGroupIndex.getDepth();\n        Integer promptDepth = null;\n\n        while (!depth.equals(promptDepth)) {\n            stepToNextEventType(FormEntryController.EVENT_PROMPT_NEW_REPEAT);\n            promptDepth = getFormIndex().getDepth();\n        }\n    }\n\n    public void newRepeat() {\n        formEntryController.newRepeat();\n    }\n\n    public void deleteRepeat() {\n        FormIndex fi = formEntryController.deleteRepeat();\n        formEntryController.jumpToIndex(fi);\n    }\n\n    public void setLanguage(String language) {\n        formEntryController.setLanguage(language);\n    }\n\n    @NonNull\n    public FormEntryPrompt[] getQuestionPrompts(@NonNull FormIndex index) throws RepeatsInFieldListException {\n        // For questions, there is only one.\n        // For groups, there could be many, but we set that below\n        FormEntryPrompt[] questions = new FormEntryPrompt[0];\n\n        IFormElement element = formEntryController.getModel().getForm().getChild(index);\n        if (element instanceof GroupDef) {\n            GroupDef gd = (GroupDef) element;\n            // we only display relevant questions\n            List<FormEntryPrompt> questionList = new ArrayList<>();\n            for (FormIndex indexInGroup : getIndicesForGroup(gd)) {\n                if (getEvent(indexInGroup) != FormEntryController.EVENT_QUESTION) {\n                    throw new RepeatsInFieldListException(\"Repeats in 'field-list' groups \" +\n                            \"are not supported. Please update the form design to remove the \" +\n                            \"following repeat from a field list: \" + indexInGroup.getReference().toString(false));\n                }\n\n                // we only display relevant questions\n                if (formEntryController.getModel().isIndexRelevant(indexInGroup)) {\n                    questionList.add(getQuestionPrompt(indexInGroup));\n                }\n                questions = new FormEntryPrompt[questionList.size()];\n                questionList.toArray(questions);\n            }\n        } else {\n            // We have a question, so just get the one prompt\n            questions = new FormEntryPrompt[1];\n            questions[0] = getQuestionPrompt();\n        }\n\n        return questions;\n    }\n\n    private boolean isGroupEmpty() {\n        GroupDef group = (GroupDef) formEntryController.getModel().getForm().getChild(getFormIndex());\n        return getIndicesForGroup(group).isEmpty();\n    }\n\n    /**\n     * Recursively gets all indices contained in this group and its children\n     */\n    private List<FormIndex> getIndicesForGroup(GroupDef gd) {\n        return getIndicesForGroup(gd,\n                formEntryController.getModel().incrementIndex(getFormIndex(), true), false);\n    }\n\n    private List<FormIndex> getIndicesForGroup(GroupDef gd, FormIndex currentChildIndex, boolean jumpIntoRepeatGroups) {\n        List<FormIndex> indices = new ArrayList<>();\n        for (int i = 0; i < gd.getChildren().size(); i++) {\n            final FormEntryModel formEntryModel = formEntryController.getModel();\n            if (getEvent(currentChildIndex) == FormEntryController.EVENT_GROUP\n                    || (jumpIntoRepeatGroups && getEvent(currentChildIndex) == FormEntryController.EVENT_REPEAT)) {\n                IFormElement nestedElement = formEntryModel.getForm().getChild(currentChildIndex);\n                if (nestedElement instanceof GroupDef) {\n                    indices.addAll(getIndicesForGroup((GroupDef) nestedElement,\n                            formEntryModel.incrementIndex(currentChildIndex, true), jumpIntoRepeatGroups));\n                    currentChildIndex = formEntryModel.incrementIndex(currentChildIndex, false);\n                }\n            } else if (!jumpIntoRepeatGroups || getEvent(currentChildIndex) != FormEntryController.EVENT_PROMPT_NEW_REPEAT) {\n                indices.add(currentChildIndex);\n                currentChildIndex = formEntryModel.incrementIndex(currentChildIndex, false);\n            }\n        }\n        return indices;\n    }\n\n    public boolean isGroupRelevant() {\n        GroupDef groupDef = (GroupDef) getCaptionPrompt().getFormElement();\n        FormIndex currentChildIndex = formEntryController.getModel().incrementIndex(getFormIndex(), true);\n        for (FormIndex index : getIndicesForGroup(groupDef, currentChildIndex, true)) {\n            if (formEntryController.getModel().isIndexRelevant(index)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public FormEntryPrompt getQuestionPrompt(FormIndex index) {\n        return formEntryController.getModel().getQuestionPrompt(index);\n    }\n\n    public FormEntryPrompt getQuestionPrompt() {\n        return formEntryController.getModel().getQuestionPrompt();\n    }\n\n    public String getQuestionPromptConstraintText(FormIndex index) {\n        return formEntryController.getModel().getQuestionPrompt(index).getConstraintText();\n    }\n\n    public boolean currentCaptionPromptIsQuestion() {\n        return getCaptionPrompt().getFormElement() instanceof QuestionDef;\n    }\n\n    public String getQuestionPromptRequiredText(FormIndex index) {\n        // look for the text under the requiredMsg bind attribute\n        String constraintText = getBindAttribute(index, XFormParser.NAMESPACE_JAVAROSA,\n                \"requiredMsg\");\n        if (constraintText != null) {\n            XPathExpression xpathRequiredMsg;\n            try {\n                xpathRequiredMsg = XPathParseTool.parseXPath(\"string(\" + constraintText + \")\");\n            } catch (Exception e) {\n                // Expected in probably most cases.\n                // This is a string literal, so no need to evaluate anything.\n                return constraintText;\n            }\n\n            if (xpathRequiredMsg != null) {\n                try {\n                    FormDef form = formEntryController.getModel().getForm();\n                    TreeElement treeElement = form.getMainInstance().resolveReference(\n                            index.getReference());\n                    EvaluationContext ec = new EvaluationContext(form.getEvaluationContext(),\n                            treeElement.getRef());\n                    Object value = xpathRequiredMsg.eval(form.getMainInstance(), ec);\n                    if (!value.equals(\"\")) {\n                        return (String) value;\n                    }\n                    return null;\n                } catch (Exception e) {\n                    Timber.e(e, \"Error evaluating a valid-looking required xpath \");\n                    return constraintText;\n                }\n            } else {\n                return constraintText;\n            }\n        }\n        return null;\n    }\n\n    public FormEntryCaption[] getGroupsForIndex(FormIndex index) {\n        int eventAtIndex = getEvent(index);\n\n        // return an empty array if you ask for something impossible\n        if (index == null || !(eventAtIndex == FormEntryController.EVENT_QUESTION\n                || eventAtIndex == FormEntryController.EVENT_PROMPT_NEW_REPEAT\n                || eventAtIndex == FormEntryController.EVENT_GROUP\n                || eventAtIndex == FormEntryController.EVENT_REPEAT)) {\n            return new FormEntryCaption[0];\n        }\n\n        // the first caption is the question, so we skip it if it's an EVENT_QUESTION\n        // otherwise, the first caption is a group so we start at index 0\n        int lastquestion = 1;\n        if (eventAtIndex == FormEntryController.EVENT_PROMPT_NEW_REPEAT\n                || eventAtIndex == FormEntryController.EVENT_GROUP\n                || eventAtIndex == FormEntryController.EVENT_REPEAT) {\n            lastquestion = 0;\n        }\n\n        FormEntryCaption[] v = getCaptionHierarchy(index);\n        FormEntryCaption[] groups = new FormEntryCaption[v.length - lastquestion];\n        System.arraycopy(v, 0, groups, 0, v.length - lastquestion);\n        return groups;\n    }\n\n    public boolean indexContainsRepeatableGroup() {\n        return indexContainsRepeatableGroup(getFormIndex());\n    }\n\n    public boolean indexContainsRepeatableGroup(FormIndex formIndex) {\n        FormEntryCaption[] groups = getCaptionHierarchy(formIndex);\n        if (groups.length == 0) {\n            return false;\n        }\n        for (int i = 0; i < groups.length; i++) {\n            if (groups[i].repeats()) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public int getLastRepeatedGroupRepeatCount() {\n        FormEntryCaption[] groups = getCaptionHierarchy();\n        if (groups.length > 0) {\n            for (int i = groups.length - 1; i > -1; i--) {\n                if (groups[i].repeats()) {\n                    return groups[i].getMultiplicity();\n\n                }\n            }\n        }\n        return -1;\n    }\n\n    public String getLastRepeatedGroupName() {\n        FormEntryCaption[] groups = getCaptionHierarchy();\n        // no change\n        if (groups.length > 0) {\n            for (int i = groups.length - 1; i > -1; i--) {\n                if (groups[i].repeats()) {\n                    return groups[i].getLongText();\n                }\n            }\n        }\n        return null;\n    }\n\n    /**\n     * The closest group the prompt belongs to.\n     *\n     * @return FormEntryCaption\n     */\n    private FormEntryCaption getLastGroup() {\n        FormEntryCaption[] groups = getCaptionHierarchy();\n        if (groups == null || groups.length == 0) {\n            return null;\n        } else {\n            return groups[groups.length - 1];\n        }\n    }\n\n    public String getLastGroupText() {\n        if (getLastGroup() != null) {\n            return getLastGroup().getLongText();\n        }\n        return null;\n    }\n\n    /**\n     * Find the portion of the form that is to be submitted\n     */\n    private IDataReference getSubmissionDataReference() {\n        FormDef formDef = formEntryController.getModel().getForm();\n        // Determine the information about the submission...\n        SubmissionProfile p = formDef.getSubmissionProfile();\n        if (p == null || p.getRef() == null) {\n            return new XPathReference(\"/\");\n        } else {\n            return p.getRef();\n        }\n    }\n\n    public boolean isSubmissionEntireForm() {\n        IDataReference sub = getSubmissionDataReference();\n        return getInstance().resolveReference(sub) == null;\n    }\n\n    public ByteArrayPayload getFilledInFormXml() throws IOException {\n        // assume no binary data inside the model.\n        FormInstance datamodel = getInstance();\n        XFormSerializingVisitor serializer = new XFormSerializingVisitor();\n\n        return (ByteArrayPayload) serializer.createSerializedPayload(datamodel);\n    }\n\n    public ByteArrayPayload getSubmissionXml() throws IOException {\n        FormInstance instance = getInstance();\n        XFormSerializingVisitor serializer = new XFormSerializingVisitor();\n        return (ByteArrayPayload) serializer.createSerializedPayload(instance,\n                getSubmissionDataReference());\n    }\n\n    /**\n     * Traverse the submission looking for the first matching tag in depth-first order.\n     */\n    private TreeElement findDepthFirst(TreeElement parent, String name) {\n        int len = parent.getNumChildren();\n        for (int i = 0; i < len; ++i) {\n            TreeElement e = parent.getChildAt(i);\n            if (name.equals(e.getName())) {\n                return e;\n            } else if (e.getNumChildren() != 0) {\n                TreeElement v = findDepthFirst(e, name);\n                if (v != null) {\n                    return v;\n                }\n            }\n        }\n        return null;\n    }\n\n    public InstanceMetadata getSubmissionMetadata() {\n        FormDef formDef = formEntryController.getModel().getForm();\n        TreeElement rootElement = formDef.getInstance().getRoot();\n\n        TreeElement trueSubmissionElement;\n        // Determine the information about the submission...\n        SubmissionProfile p = formDef.getSubmissionProfile();\n        if (p == null || p.getRef() == null) {\n            trueSubmissionElement = rootElement;\n        } else {\n            IDataReference ref = p.getRef();\n            trueSubmissionElement = formDef.getInstance().resolveReference(ref);\n            // resolveReference returns null if the reference is to the root element...\n            if (trueSubmissionElement == null) {\n                trueSubmissionElement = rootElement;\n            }\n        }\n\n        // and find the depth-first meta block in this...\n        TreeElement e = findDepthFirst(trueSubmissionElement, \"meta\");\n\n        String instanceId = null;\n        String instanceName = null;\n        AuditConfig auditConfig = null;\n\n        if (e != null) {\n            List<TreeElement> v;\n\n            // instance id...\n            v = e.getChildrenWithName(INSTANCE_ID);\n            if (v.size() == 1) {\n                IAnswerData sa = v.get(0).getValue();\n                if (sa != null) {\n                    instanceId = sa.getDisplayText();\n                }\n            }\n\n            // instance name...\n            v = e.getChildrenWithName(INSTANCE_NAME);\n            if (v.size() == 1) {\n                IAnswerData sa = v.get(0).getValue();\n                if (sa != null) {\n                    instanceName = sa.getDisplayText();\n                }\n            }\n\n            // timing element...\n            v = e.getChildrenWithName(AUDIT);\n            if (v.size() == 1) {\n\n                TreeElement auditElement = v.get(0);\n\n                String locationPriority = auditElement.getBindAttributeValue(XML_OPENDATAKIT_NAMESPACE, \"location-priority\");\n                String locationMinInterval = auditElement.getBindAttributeValue(XML_OPENDATAKIT_NAMESPACE, \"location-min-interval\");\n                String locationMaxAge = auditElement.getBindAttributeValue(XML_OPENDATAKIT_NAMESPACE, \"location-max-age\");\n                boolean isTrackingChangesEnabled = Boolean.parseBoolean(auditElement.getBindAttributeValue(XML_OPENDATAKIT_NAMESPACE, \"track-changes\"));\n                boolean isIdentifyUserEnabled = Boolean.parseBoolean(auditElement.getBindAttributeValue(XML_OPENDATAKIT_NAMESPACE, \"identify-user\"));\n                String trackChangesReason = auditElement.getBindAttributeValue(XML_OPENDATAKIT_NAMESPACE, \"track-changes-reasons\");\n\n                auditConfig = new AuditConfig.Builder()\n                        .setMode(locationPriority)\n                        .setLocationMinInterval(locationMinInterval)\n                        .setLocationMaxAge(locationMaxAge)\n                        .setIsTrackingChangesEnabled(isTrackingChangesEnabled)\n                        .setIsIdentifyUserEnabled(isIdentifyUserEnabled)\n                        .setIsTrackChangesReasonEnabled(trackChangesReason != null && trackChangesReason.equals(\"on-form-edit\"))\n                        .createAuditConfig();\n\n                IAnswerData answerData = new StringData();\n                answerData.setValue(AUDIT_FILE_NAME);\n                auditElement.setValue(answerData);\n            }\n        }\n\n        return new InstanceMetadata(instanceId, instanceName, auditConfig);\n    }\n\n    public boolean currentFormAuditsLocation() {\n        AuditConfig auditConfig = getSubmissionMetadata().auditConfig;\n\n        return auditConfig != null && auditConfig.isLocationEnabled();\n    }\n\n    public boolean currentFormCollectsBackgroundLocation() {\n        return currentFormAuditsLocation() || getFormDef().hasAction(SetGeopointActionHandler.ELEMENT_NAME);\n    }\n\n    public IAnswerData getAnswer(TreeReference treeReference) {\n        return getFormDef().getMainInstance().resolveReference(treeReference).getValue();\n    }\n\n    public EntitiesExtra getEntities() {\n        return formEntryController.getModel().getExtras().get(EntitiesExtra.class);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/javarosawrapper/ValidationResult.kt",
    "content": "package org.odk.collect.android.javarosawrapper\n\nimport androidx.annotation.StringRes\nimport org.javarosa.core.model.FormIndex\n\nsealed class ValidationResult\n\nobject SuccessValidationResult : ValidationResult()\n\ndata class FailedValidationResult(\n    val index: FormIndex,\n    val status: Int,\n    val customErrorMessage: String?,\n    @StringRes val defaultErrorMessage: Int\n) : ValidationResult()\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/listeners/AdvanceToNextListener.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.listeners;\n\npublic interface AdvanceToNextListener {\n\n    void advance(); //Move on to the next question\n}"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/listeners/DeleteInstancesListener.java",
    "content": "/*\n * Copyright (C) 2012 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.listeners;\n\n/**\n * Callback interface invoked upon the completion of a DeleteInstancesTask\n *\n * @author norman86@gmail.com\n * @author mitchellsundt@gmail.com\n */\npublic interface DeleteInstancesListener {\n    void deleteComplete(int deletedInstances);\n\n    void progressUpdate(int progress, int total);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/listeners/DownloadFormsTaskListener.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.listeners;\n\nimport org.odk.collect.android.formmanagement.download.FormDownloadException;\nimport org.odk.collect.android.formmanagement.ServerFormDetails;\n\nimport java.util.Map;\n\n/**\n * @author Carl Hartung (carlhartung@gmail.com)\n */\npublic interface DownloadFormsTaskListener {\n    void formsDownloadingComplete(Map<ServerFormDetails, FormDownloadException> result);\n\n    void progressUpdate(String currentFile, int progress, int total);\n\n    void formsDownloadingCancelled();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/listeners/FormListDownloaderListener.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.listeners;\n\nimport org.odk.collect.android.formmanagement.ServerFormDetails;\nimport org.odk.collect.forms.FormSourceException;\n\nimport java.util.HashMap;\n\n/**\n * @author Carl Hartung (carlhartung@gmail.com)\n */\npublic interface FormListDownloaderListener {\n    void formListDownloadingComplete(HashMap<String, ServerFormDetails> formList, FormSourceException exception);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/listeners/FormLoaderListener.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.listeners;\n\nimport org.javarosa.core.model.FormDef;\nimport org.odk.collect.android.tasks.FormLoaderTask;\nimport org.odk.collect.android.tasks.ProgressNotifier;\n\n/**\n * @author Carl Hartung (carlhartung@gmail.com)\n */\npublic interface FormLoaderListener extends ProgressNotifier {\n    void loadingComplete(FormLoaderTask task, FormDef fd, String warningMsg);\n\n    void loadingError(String errorMsg);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/listeners/InstanceUploaderListener.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.listeners;\n\nimport android.net.Uri;\n\nimport java.util.HashMap;\n\n/**\n * @author Carl Hartung (carlhartung@gmail.com)\n */\npublic interface InstanceUploaderListener {\n    void uploadingComplete(HashMap<String, String> result);\n\n    void progressUpdate(int progress, int total);\n\n    void authRequest(Uri url, HashMap<String, String> doneSoFar);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/listeners/Result.java",
    "content": "package org.odk.collect.android.listeners;\n\n/**\n * Generic Result interface that returns a result of type T\n * from another thread or function.\n *\n */\npublic interface Result<T> {\n    void onComplete(T result);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/listeners/SelectItemClickListener.java",
    "content": "package org.odk.collect.android.listeners;\n\npublic interface SelectItemClickListener {\n    void onItemClicked();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/listeners/ThousandsSeparatorTextWatcher.java",
    "content": "package org.odk.collect.android.listeners;\n\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.widget.EditText;\n\nimport java.text.DecimalFormat;\nimport java.util.regex.Pattern;\n\nimport timber.log.Timber;\n\n/**\n * Created by srv_twry on 4/12/17.\n * Source: https://stackoverflow.com/a/34265406/137744\n * The custom TextWatcher that automatically adds thousand separators in EditText.\n */\n\npublic class ThousandsSeparatorTextWatcher implements TextWatcher {\n    private final EditText editText;\n    private static String thousandSeparator;\n    private int cursorPosition;\n\n    public ThousandsSeparatorTextWatcher(EditText editText) {\n        this.editText = editText;\n        DecimalFormat df = new DecimalFormat();\n        df.setDecimalSeparatorAlwaysShown(true);\n        thousandSeparator = Character.toString(df.getDecimalFormatSymbols().getGroupingSeparator());\n\n        // The decimal marker is always \".\" (see DecimalWidget) so avoid it as thousands separator\n        if (thousandSeparator.equals(\".\")) {\n            thousandSeparator = \" \";\n        }\n    }\n\n    @Override\n    public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {\n        cursorPosition = editText.getText().toString().length() - editText.getSelectionStart();\n    }\n\n    @Override\n    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}\n\n    @Override\n    public void afterTextChanged(Editable s) {\n        try {\n            editText.removeTextChangedListener(this);\n            String value = editText.getText().toString();\n\n            if (!value.equals(\"\")) {\n                String str = editText.getText().toString().replaceAll(Pattern.quote(thousandSeparator), \"\");\n                if (!value.equals(\"\")) {\n                    editText.setText(getDecimalFormattedString(str));\n                }\n                editText.setSelection(editText.getText().toString().length());\n            }\n\n            //setting the cursor back to where it was\n            int selectionIndex = editText.getText().toString().length() - cursorPosition;\n            editText.setSelection(Math.max(selectionIndex, 0));\n            editText.addTextChangedListener(this);\n        } catch (Exception ex) {\n            Timber.e(ex);\n            editText.addTextChangedListener(this);\n        }\n    }\n\n    private static String getDecimalFormattedString(String value) {\n        // Always use a period because keyboard isn't localized. See DecimalWidget.\n        String decimalMarker = \".\";\n\n        String[] splitValue = value.split(Pattern.quote(decimalMarker));\n        String beforeDecimal = value;\n        String afterDecimal = null;\n        String finalResult = \"\";\n\n        if (splitValue.length == 2) {\n            beforeDecimal = splitValue[0];\n            afterDecimal = splitValue[1];\n        }\n\n        int count = 0;\n        for (int i = beforeDecimal.length() - 1; i >= 0; i--) {\n            finalResult = beforeDecimal.charAt(i) + finalResult;\n            count++;\n            if (count == 3 && i > 0) {\n                finalResult = thousandSeparator + finalResult;\n                count = 0;\n            }\n        }\n\n        if (afterDecimal != null) {\n            finalResult = finalResult + decimalMarker + afterDecimal;\n        }\n\n        return finalResult;\n    }\n\n    /*\n    * Returns the string after removing all the thousands separators.\n    * */\n    public static String getOriginalString(String string) {\n        return string.replace(thousandSeparator, \"\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/listeners/WidgetValueChangedListener.java",
    "content": "package org.odk.collect.android.listeners;\n\nimport org.odk.collect.android.widgets.QuestionWidget;\n\npublic interface WidgetValueChangedListener {\n    void widgetValueChanged(QuestionWidget changedWidget);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/location/client/MaxAccuracyWithinTimeoutLocationClientWrapper.java",
    "content": "package org.odk.collect.android.location.client;\n\nimport android.location.Location;\nimport android.os.Handler;\nimport android.os.Looper;\n\nimport androidx.annotation.Nullable;\n\nimport com.google.android.gms.location.LocationListener;\n\nimport org.odk.collect.location.LocationClient;\n\nimport timber.log.Timber;\n\n/**\n * Provides location updates for a set timeout period. Once a request is initiated, updates are only\n * provided if the new reading has higher accuracy.\n *\n * New requests reset the timeout and the highest accuracy.\n */\npublic class MaxAccuracyWithinTimeoutLocationClientWrapper implements LocationClient.LocationClientListener, LocationListener {\n    private final LocationClient locationClient;\n\n    private final LocationListener listener;\n\n    /** The highest accuracy reading seen since the current request was started. Null if no updates\n     * have been received since the current request was made. **/\n    @Nullable\n    private Location highestAccuracyReading;\n    private final Handler timerHandler;\n\n    private static final LocationClient.Priority DEFAULT_PRIORITY = LocationClient.Priority.PRIORITY_HIGH_ACCURACY;\n\n    public MaxAccuracyWithinTimeoutLocationClientWrapper(LocationClient locationClient, LocationListener listener) {\n        this.locationClient = locationClient;\n        this.locationClient.setPriority(DEFAULT_PRIORITY);\n\n        this.listener = listener;\n        this.timerHandler = new Handler(Looper.getMainLooper());\n    }\n\n    /**\n     * Requests that location updates be provided to the listener for {@code timeoutSeconds}.\n     */\n    public void requestLocationUpdates(long timeoutSeconds) {\n        new Handler(Looper.getMainLooper()).post(() -> {\n            locationClient.start(this);\n        });\n\n        // If updates are requested more than once, reset the highest accuracy\n        highestAccuracyReading = null;\n\n        timerHandler.removeCallbacksAndMessages(null);\n        timerHandler.postDelayed(() -> {\n            locationClient.stop();\n            Timber.i(\"MaxAccuracyWithinTimeoutLocationClient: stopping location updates\");\n        }, timeoutSeconds * 1000);\n    }\n\n    //region LocationClientListener\n    @Override\n    public void onClientStart() {\n        Timber.i(\"MaxAccuracyWithinTimeoutLocationClient: starting location updates\");\n        try {\n            locationClient.requestLocationUpdates(this);\n        } catch (SecurityException e) {\n            // Device-level location permissions have not been granted. The user will be prompted to\n            // provide permissions. It will be too late for this triggered action but will work for\n            // future ones.\n        }\n    }\n\n    @Override\n    public void onClientStartFailure() {\n\n    }\n\n    @Override\n    public void onClientStop() {\n\n    }\n    //endregion\n\n    //region LocationListener\n\n    /**\n     * Updates the listener if {@code location} meets any of these criteria:\n     * - it is the first location update\n     * - it is more accurate than the most accurate one seen yet\n     * - it is the first location update with accuracy\n     *\n     * Otherwise, the new location reading is discarded.\n     */\n    @Override\n    public void onLocationChanged(Location location) {\n        Timber.i(\"MaxAccuracyWithinTimeoutLocationClient: got location %s\", location);\n        if (highestAccuracyReading == null || !highestAccuracyReading.hasAccuracy()\n                || location.hasAccuracy() && highestAccuracyReading.hasAccuracy() && location.getAccuracy() < highestAccuracyReading.getAccuracy()) {\n            highestAccuracyReading = location;\n            listener.onLocationChanged(location);\n            Timber.i(\"MaxAccuracyWithinTimeoutLocationClient: passed on location %s\", location);\n        }\n    }\n    //endregion\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/logic/FileReference.java",
    "content": "/**\n *\n */\n\npackage org.odk.collect.android.logic;\n\nimport org.javarosa.core.reference.Reference;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\n/**\n * @author ctsims\n */\npublic class FileReference implements Reference {\n    String localPart;\n    String referencePart;\n\n    public FileReference(String localPart, String referencePart) {\n        this.localPart = localPart;\n        this.referencePart = referencePart;\n    }\n\n    private String getInternalURI() {\n        return \"/\" + localPart + referencePart;\n    }\n    \n    @Override\n    public boolean doesBinaryExist() {\n        return new File(getInternalURI()).exists();\n    }\n\n    @Override\n    public InputStream getStream() throws IOException {\n        return new FileInputStream(getInternalURI());\n    }\n\n    @Override\n    public String getURI() {\n        return \"jr://file\" + referencePart;\n    }\n\n    @Override\n    public boolean isReadOnly() {\n        return false;\n    }\n\n    @Override\n    public OutputStream getOutputStream() throws IOException {\n        return new FileOutputStream(getInternalURI());\n    }\n\n    @Override\n    public void remove() {\n        // TODO bad practice to ignore return values\n        new File(getInternalURI()).delete();\n    }\n\n    @Override\n    public String getLocalURI() {\n        return getInternalURI();\n    }\n\n    @Override\n    public Reference[] probeAlternativeReferences() {\n        //We can't poll the JAR for resources, unfortunately. It's possible\n        //we could try to figure out something about the file and poll alternatives\n        //based on type (PNG-> JPG, etc)\n        return new Reference[0];\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/logic/FileReferenceFactory.java",
    "content": "/**\n *\n */\n\npackage org.odk.collect.android.logic;\n\nimport org.javarosa.core.reference.PrefixedRootFactory;\nimport org.javarosa.core.reference.Reference;\n\n/**\n * @author ctsims\n */\npublic class FileReferenceFactory extends PrefixedRootFactory {\n\n    String localRoot;\n\n    public FileReferenceFactory(String localRoot) {\n        super(new String[]{\n                \"file\"\n        });\n        this.localRoot = localRoot;\n    }\n\n    @Override\n    protected Reference factory(String terminal, String uri) {\n        return new FileReference(localRoot, terminal);\n    }\n\n    @Override\n    public String toString() {\n        return \"FileReferenceFactory{\" +\n                \"localRoot='\" + localRoot + '\\'' +\n                \"} \" + super.toString();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/logic/ImmutableDisplayableQuestion.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.logic;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.form.api.FormEntryPrompt;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Represents all the user-visible aspects of a question at a particular point in time. This\n * information is generally accessed through {@link FormEntryPrompt}s which change as the form is\n * being filled and {@link org.javarosa.core.model.condition.Triggerable}s are evaluated.\n * {@link ImmutableDisplayableQuestion} objects are used to make snapshots of what the user sees.\n *\n * Inspired by https://github.com/dimagi/commcare-android/blob/fd63a5b2471a1e33ac6bf1ce77ac82daf558b08a/app/src/org/commcare/activities/components/FormRelevancyUpdating.java#L48\n */\npublic class ImmutableDisplayableQuestion {\n    /**\n     * Where in the form this question is located.\n     */\n    private final FormIndex index;\n\n    /**\n     * The main text of the question. This will include a prefixed asterisk if the question is\n     * required.\n     */\n    private final String questionText;\n\n    /**\n     * The hint text of the question.\n     */\n    private final String helpText;\n\n    /**\n     * The guidance hint text of the question.\n     */\n    private final String guidanceText;\n\n    /**\n     * The answer text of the question.\n     */\n    private final String answerText;\n\n    /**\n     * Whether the question is read-only.\n     */\n    private final boolean isReadOnly;\n\n    /**\n     * Whether the question is required.\n     */\n    private final boolean isRequired;\n\n    /**\n     * The choices displayed to a user if this question is of a type that has choices.\n     */\n    private List<SelectChoice> selectChoices;\n\n    /**\n     * Saves all the user-visible aspects of the given {@link FormEntryPrompt}.\n     */\n    public ImmutableDisplayableQuestion(FormEntryPrompt question) {\n        index = question.getIndex();\n        questionText = question.getQuestionText();\n        helpText = question.getHelpText();\n        guidanceText = question.getSpecialFormQuestionText(question.getQuestion().getHelpTextID(), \"guidance\");\n        answerText = question.getAnswerText();\n        isReadOnly = question.isReadOnly();\n        isRequired = question.isRequired();\n\n        List<SelectChoice> choices = question.getSelectChoices();\n        if (choices != null) {\n            selectChoices = new ArrayList<>();\n            selectChoices.addAll(choices);\n        }\n    }\n\n    public FormIndex getFormIndex() {\n        return index;\n    }\n\n    public Object getAnswerText() {\n        return answerText;\n    }\n\n    /**\n     * Returns {@code true} if the provided {@link FormEntryPrompt} has the same user-visible\n     * aspects, {@code false} otherwise.\n     */\n    public boolean sameAs(FormEntryPrompt question) {\n        return question != null\n                && question.getIndex().equals(index)\n                && (question.getQuestionText() == null ? questionText == null : question.getQuestionText().equals(questionText))\n                && (question.getHelpText() == null ? helpText == null : question.getHelpText().equals(helpText))\n                && (getGuidanceHintText(question) == null ? guidanceText == null : getGuidanceHintText(question).equals(guidanceText))\n                && (question.getAnswerText() == null ? answerText == null : question.getAnswerText().equals(answerText))\n                && (question.isReadOnly() == isReadOnly)\n                && selectChoiceListsEqual(question.getSelectChoices(), selectChoices)\n                && question.isRequired() == isRequired;\n    }\n\n    private static boolean selectChoiceListsEqual(List<SelectChoice> selectChoiceList1, List<SelectChoice> selectChoiceList2) {\n        if (selectChoiceList1 == null) {\n            return selectChoiceList2 == null;\n        }\n\n        if (selectChoiceList1.size() != selectChoiceList2.size()) {\n            return false;\n        }\n\n        for (int i = 0; i < selectChoiceList1.size(); i++) {\n            if (!selectChoicesEqual(selectChoiceList1.get(i), selectChoiceList2.get(i))) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * TODO: move to JavaRosa\n     */\n    private static String getGuidanceHintText(FormEntryPrompt question) {\n        return question.getSpecialFormQuestionText(question.getQuestion().getHelpTextID(), \"guidance\");\n    }\n\n    /**\n     * Returns true if the two SelectChoice objects currently represent the same choice in the same\n     * position.\n     *\n     * Note: this is here rather than as a .equals for SelectChoice in JavaRosa because SelectChoice\n     * is mutable and keeps track of both the choice and its current position. Clients may want to\n     * define equality differently for different usages.\n     */\n    private static boolean selectChoicesEqual(SelectChoice selectChoice1, SelectChoice selectChoice2) {\n        if (selectChoice1 == null) {\n            return selectChoice2 == null;\n        }\n\n        if (selectChoice1.getLabelInnerText() == null) {\n            if (selectChoice2.getLabelInnerText() != null) {\n                return false;\n            }\n        } else if (!selectChoice1.getLabelInnerText().equals(selectChoice2.getLabelInnerText())) {\n            return false;\n        }\n\n        if (selectChoice1.getTextID() == null) {\n            if (selectChoice2.getTextID() != null) {\n                return false;\n            }\n        } else if (!selectChoice1.getTextID().equals(selectChoice2.getTextID())) {\n            return false;\n        }\n\n        if (selectChoice1.getValue() == null) {\n            if (selectChoice2.getValue() != null) {\n                return false;\n            }\n        } else if (!selectChoice1.getValue().equals(selectChoice2.getValue())) {\n            return false;\n        }\n\n        return selectChoice1.getIndex() == selectChoice2.getIndex()\n                && selectChoice1.isLocalizable() == selectChoice2.isLocalizable()\n                && selectChoice1.copyNode == selectChoice2.copyNode;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/logic/actions/setgeopoint/CollectSetGeopointAction.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.logic.actions.setgeopoint;\n\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_BACKGROUND_LOCATION;\n\nimport android.location.Location;\n\nimport com.google.android.gms.location.LocationListener;\n\nimport org.javarosa.core.model.actions.setgeopoint.SetGeopointAction;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.location.client.MaxAccuracyWithinTimeoutLocationClientWrapper;\nimport org.odk.collect.androidshared.system.PlayServicesChecker;\nimport org.odk.collect.geo.GeoUtils;\nimport org.odk.collect.location.GoogleFusedLocationClient;\n\nimport timber.log.Timber;\n\n/**\n * An Android-specific implementation of {@link SetGeopointAction}.\n *\n * When the action is triggered, the first location available is saved and the highest-accuracy\n * reading within {@link #SECONDS_TO_CONSIDER_UPDATES} seconds will replace it. If no location is\n * available within {@link #SECONDS_TO_CONSIDER_UPDATES} seconds, no location is written.\n *\n * It is possible for the same action to be triggered multiple times within\n * {@link #SECONDS_TO_CONSIDER_UPDATES} seconds if the action is triggered by a value changed\n * event, for example. In that case, the highest accuracy reading resets each time the action is\n * triggered.\n *\n * In a repeat, the target node for subsequent times the action is triggered could be different than\n * the target node the first time it was triggered. In that case, only the current target node is\n * updated.\n */\npublic class CollectSetGeopointAction extends SetGeopointAction implements LocationListener {\n    private static final int SECONDS_TO_CONSIDER_UPDATES = 20;\n\n    private MaxAccuracyWithinTimeoutLocationClientWrapper maxAccuracyLocationClient;\n\n    public CollectSetGeopointAction() {\n        // For serialization\n    }\n\n    // Needed to set the action name.\n    CollectSetGeopointAction(TreeReference targetReference) {\n        super(targetReference);\n    }\n\n    @Override\n    public void requestLocationUpdates() {\n        // Do initialization on first location request so the client doesn't need to be serialized\n        if (maxAccuracyLocationClient == null) {\n            maxAccuracyLocationClient = new MaxAccuracyWithinTimeoutLocationClientWrapper(new GoogleFusedLocationClient(Collect.getInstance()), this);\n        }\n\n        // Only start acquiring location if the Collect preference allows it and Google Play\n        // Services are available. If it's not allowed, leave the target field blank.\n        if (isBackgroundLocationEnabled()\n            && new PlayServicesChecker().isGooglePlayServicesAvailable(Collect.getInstance().getApplicationContext())) {\n            maxAccuracyLocationClient.requestLocationUpdates(SECONDS_TO_CONSIDER_UPDATES);\n        }\n    }\n\n    /**\n     * When the location updates, if location updates are allowed, no location has been received yet\n     * or the new location has a higher accuracy than previous locations, save the location in the\n     * model, matching the target node's type.\n     *\n     * If the background location preference is toggled to disabled while location is being acquired,\n     * the node receiving the location will be cleared.\n     */\n    @Override\n    public void onLocationChanged(Location location) {\n        if (isBackgroundLocationEnabled()) {\n            Timber.i(\"Setgeopoint action for \" + getContextualizedTargetReference() + \": location update\");\n\n            String formattedLocation = GeoUtils.formatLocationResultString(location);\n            saveLocationValue(formattedLocation);\n        } else {\n            saveLocationValue(\"\");\n        }\n    }\n\n    private boolean isBackgroundLocationEnabled() {\n        return DaggerUtils\n                .getComponent(Collect.getInstance())\n                .settingsProvider()\n                .getUnprotectedSettings()\n                .getBoolean(KEY_BACKGROUND_LOCATION);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/logic/actions/setgeopoint/CollectSetGeopointActionHandler.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.logic.actions.setgeopoint;\n\nimport org.javarosa.core.model.actions.setgeopoint.SetGeopointAction;\nimport org.javarosa.core.model.actions.setgeopoint.SetGeopointActionHandler;\n\n/**\n * Handler for Collect's implementation of the odk:setgeopoint action.\n */\npublic class CollectSetGeopointActionHandler extends SetGeopointActionHandler {\n    public SetGeopointAction getSetGeopointAction() {\n        return new CollectSetGeopointAction(null);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/mainmenu/CurrentProjectViewModel.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.asLiveData\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.projects.Project\n\nclass CurrentProjectViewModel(\n    private val projectsDataService: ProjectsDataService\n) : ViewModel() {\n\n    init {\n        projectsDataService.update()\n    }\n\n    val currentProject = projectsDataService.getCurrentProject().asLiveData()\n\n    fun setCurrentProject(project: Project.Saved) {\n        Analytics.log(AnalyticsEvents.SWITCH_PROJECT)\n        projectsDataService.setCurrentProject(project.uuid)\n    }\n\n    fun hasCurrentProject(): Boolean {\n        return projectsDataService.getCurrentProject().value != null\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.os.Build\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM\nimport androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode\nimport androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.android.R\nimport org.odk.collect.android.activities.ActivityUtils\nimport org.odk.collect.android.activities.CrashHandlerActivity\nimport org.odk.collect.android.activities.FirstLaunchActivity\nimport org.odk.collect.android.application.CollectComposeThemeProvider\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.projects.ProjectSettingsDialog\nimport org.odk.collect.android.utilities.ThemeUtils\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.crashhandler.CrashHandler\nimport org.odk.collect.mobiledevicemanagement.MDMConfigObserver\nimport org.odk.collect.permissions.PermissionsProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport org.odk.collect.webpage.WebPageService\nimport javax.inject.Inject\n\nclass MainMenuActivity : LocalizedActivity(), CollectComposeThemeProvider {\n\n    @Inject\n    lateinit var viewModelFactory: MainMenuViewModelFactory\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    @Inject\n    lateinit var permissionsProvider: PermissionsProvider\n\n    @Inject\n    lateinit var mdmConfigObserver: MDMConfigObserver\n\n    @Inject\n    lateinit var webPageService: WebPageService\n\n    private lateinit var currentProjectViewModel: CurrentProjectViewModel\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        initSplashScreen()\n\n        /*\n        Don't reopen if the app is already open - allows entry points like notifications to use\n        this Activity as a target to reopen the app without interrupting an ongoing session\n         */\n        if (!isTaskRoot) {\n            super.onCreate(null)\n            finish()\n            return\n        }\n\n        CrashHandler.getInstance(this)?.also {\n            if (it.hasCrashed(this)) {\n                super.onCreate(null)\n                ActivityUtils.startActivityAndCloseAllOthers(this, CrashHandlerActivity::class.java)\n                return\n            }\n        }\n\n        DaggerUtils.getComponent(this).inject(this)\n\n        val viewModelProvider = ViewModelProvider(this, viewModelFactory)\n        currentProjectViewModel = viewModelProvider[CurrentProjectViewModel::class.java]\n\n        setDefaultNightMode(MODE_NIGHT_FOLLOW_SYSTEM)\n\n        if (!currentProjectViewModel.hasCurrentProject()) {\n            super.onCreate(null)\n            ActivityUtils.startActivityAndCloseAllOthers(this, FirstLaunchActivity::class.java)\n            return\n        } else {\n            this.supportFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n                .forClass(PermissionsDialogFragment::class) {\n                    PermissionsDialogFragment(\n                        permissionsProvider,\n                        viewModelProvider[RequestPermissionsViewModel::class.java]\n                    )\n                }\n                .forClass(ProjectSettingsDialog::class) {\n                    ProjectSettingsDialog(viewModelFactory)\n                }\n                .forClass(MainMenuFragment::class) {\n                    MainMenuFragment(viewModelFactory, webPageService)\n                }\n                .build()\n\n            super.onCreate(savedInstanceState)\n            setView(R.layout.main_menu_activity, false)\n            lifecycle.addObserver(mdmConfigObserver)\n        }\n    }\n\n    private fun initSplashScreen() {\n        /*\n        We don't need the `installSplashScreen` call on Android 12+ (the system handles the\n        splash screen for us) and it causes problems if we later switch between dark/light themes\n        with the ThemeUtils#setDarkModeForCurrentProject call.\n         */\n        if (Build.VERSION.SDK_INT < 31) {\n            installSplashScreen()\n        } else {\n            setTheme(R.style.Theme_Collect)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuButton.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.content.Context\nimport android.graphics.Typeface\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport androidx.core.content.withStyledAttributes\nimport com.google.android.material.badge.BadgeDrawable\nimport com.google.android.material.badge.BadgeUtils\nimport com.google.android.material.badge.ExperimentalBadgeUtils\nimport org.odk.collect.android.R\nimport org.odk.collect.android.databinding.MainMenuButtonBinding\nimport org.odk.collect.androidshared.system.ContextExt.getThemeAttributeValue\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\nclass MainMenuButton(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {\n\n    constructor(context: Context) : this(context, null)\n\n    private val binding = MainMenuButtonBinding.inflate(LayoutInflater.from(context), this, true)\n    private val badge: BadgeDrawable\n    private var highlightable: Boolean = false\n\n    init {\n        context.withStyledAttributes(attrs, R.styleable.MainMenuButton) {\n            val buttonIcon = this.getResourceId(R.styleable.MainMenuButton_icon, 0)\n            val buttonName = this.getString(R.styleable.MainMenuButton_name)\n            highlightable = this.getBoolean(R.styleable.MainMenuButton_highlightable, false)\n\n            binding.icon.setImageResource(buttonIcon)\n            binding.name.text = buttonName\n        }\n\n        badge = BadgeDrawable.create(context).apply {\n            backgroundColor = getThemeAttributeValue(context, androidx.appcompat.R.attr.colorPrimary)\n            badgeGravity = BadgeDrawable.BOTTOM_END\n        }\n    }\n\n    val text: String\n        get() = binding.name.text.toString()\n\n    override fun performClick(): Boolean {\n        return MultiClickGuard.allowClick(context.getString(R.string.main_menu_screen)) && super.performClick()\n    }\n\n    fun setNumberOfForms(number: Int) {\n        binding.number.text = if (number < 1) {\n            \"\"\n        } else {\n            number.toString()\n        }\n\n        @ExperimentalBadgeUtils\n        if (highlightable) {\n            if (number > 0) {\n                binding.icon.viewTreeObserver.addOnGlobalLayoutListener {\n                    BadgeUtils.attachBadgeDrawable(badge, binding.icon)\n                }\n                binding.name.setTypeface(binding.name.typeface, Typeface.BOLD)\n                binding.number.setTypeface(binding.name.typeface, Typeface.BOLD)\n            } else {\n                binding.icon.viewTreeObserver.addOnGlobalLayoutListener {\n                    BadgeUtils.detachBadgeDrawable(badge, binding.icon)\n                }\n                binding.name.typeface = Typeface.DEFAULT\n                binding.number.typeface = Typeface.DEFAULT\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuFragment.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.Menu\nimport android.view.MenuInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.net.toUri\nimport androidx.fragment.app.Fragment\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.android.activities.DeleteFormsActivity\nimport org.odk.collect.android.activities.FormDownloadListActivity\nimport org.odk.collect.android.activities.InstanceChooserList\nimport org.odk.collect.android.application.MapboxClassInstanceCreator\nimport org.odk.collect.android.databinding.MainMenuBinding\nimport org.odk.collect.android.formentry.FormOpeningMode\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListActivity\nimport org.odk.collect.android.formmanagement.FormFillingIntentFactory\nimport org.odk.collect.android.instancemanagement.send.InstanceUploaderListActivity\nimport org.odk.collect.android.projects.ProjectIconView\nimport org.odk.collect.android.projects.ProjectSettingsDialog\nimport org.odk.collect.android.utilities.ActionRegister\nimport org.odk.collect.androidshared.data.consume\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.SnackbarUtils\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.webpage.WebPageService\n\nclass MainMenuFragment(\n    private val viewModelFactory: ViewModelProvider.Factory,\n    private val webPageService: WebPageService\n) : Fragment() {\n\n    private lateinit var mainMenuViewModel: MainMenuViewModel\n    private lateinit var currentProjectViewModel: CurrentProjectViewModel\n    private lateinit var permissionsViewModel: RequestPermissionsViewModel\n\n    private val formEntryFlowLauncher =\n        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->\n            val uri = result.data?.data\n            mainMenuViewModel.setSavedForm(uri)\n        }\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        val viewModelProvider = ViewModelProvider(requireActivity(), viewModelFactory)\n        mainMenuViewModel = viewModelProvider[MainMenuViewModel::class.java]\n        currentProjectViewModel = viewModelProvider[CurrentProjectViewModel::class.java]\n        permissionsViewModel = viewModelProvider[RequestPermissionsViewModel::class.java]\n\n        setHasOptionsMenu(true)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return MainMenuBinding.inflate(inflater, container, false).root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        currentProjectViewModel.currentProject.observe(viewLifecycleOwner) { project ->\n            if (project != null) {\n                requireActivity().invalidateOptionsMenu()\n                requireActivity().title = project.name\n            }\n        }\n\n        val binding = MainMenuBinding.bind(view)\n        initToolbar(binding)\n        initMapbox()\n        initButtons(binding)\n        initAppName(binding)\n\n        if (permissionsViewModel.shouldAskForPermissions()) {\n            DialogFragmentUtils.showIfNotShowing(\n                PermissionsDialogFragment::class.java,\n                this.parentFragmentManager\n            )\n        }\n\n        mainMenuViewModel.savedForm.consume(viewLifecycleOwner) { value ->\n            SnackbarUtils.showSnackbar(\n                requireView(),\n                getString(value.message),\n                SnackbarUtils.DURATION_LONG,\n                action = value.action?.let { action ->\n                    SnackbarUtils.Action(getString(action)) {\n                        formEntryFlowLauncher.launch(\n                            FormFillingIntentFactory.editDraftFormIntent(\n                                requireContext(),\n                                value.uri\n                            )\n                        )\n                    }\n                },\n                displayDismissButton = true\n            )\n        }\n\n        currentProjectViewModel.currentProject.observe(viewLifecycleOwner) {\n            if (it?.isOldGoogleDriveProject == true) {\n                binding.googleDriveDeprecationBanner.root.visibility = View.VISIBLE\n                binding.googleDriveDeprecationBanner.learnMoreButton.setOnClickListener {\n                    webPageService.openWebPage(\n                        requireActivity(),\n                        \"https://forum.getodk.org/t/40097\".toUri()\n                    )\n                }\n            } else {\n                binding.googleDriveDeprecationBanner.root.visibility = View.GONE\n            }\n        }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        mainMenuViewModel.refreshInstances()\n\n        val binding = MainMenuBinding.bind(requireView())\n        setButtonsVisibility(binding)\n    }\n\n    override fun onPrepareOptionsMenu(menu: Menu) {\n        val projectsMenuItem = menu.findItem(org.odk.collect.android.R.id.projects)\n        (projectsMenuItem.actionView as ProjectIconView).apply {\n            project = currentProjectViewModel.currentProject.value\n            setOnClickListener { onOptionsItemSelected(projectsMenuItem) }\n            contentDescription = getString(string.projects)\n        }\n    }\n\n    override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) {\n        menuInflater.inflate(org.odk.collect.android.R.menu.main_menu, menu)\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        if (!MultiClickGuard.allowClick(javaClass.name)) {\n            return true\n        }\n        if (item.itemId == org.odk.collect.android.R.id.projects) {\n            DialogFragmentUtils.showIfNotShowing(\n                ProjectSettingsDialog::class.java,\n                parentFragmentManager\n            )\n            return true\n        }\n\n        return super.onOptionsItemSelected(item)\n    }\n\n    private fun initToolbar(binding: MainMenuBinding) {\n        val toolbar = binding.root.findViewById<Toolbar>(org.odk.collect.androidshared.R.id.toolbar)\n        (requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)\n    }\n\n    private fun initMapbox() {\n        if (MapboxClassInstanceCreator.isMapboxAvailable()) {\n            childFragmentManager\n                .beginTransaction()\n                .add(\n                    org.odk.collect.android.R.id.map_box_initialization_fragment,\n                    MapboxClassInstanceCreator.createMapBoxInitializationFragment()!!\n                )\n                .commit()\n        }\n    }\n\n    private fun initButtons(binding: MainMenuBinding) {\n        binding.enterData.setOnClickListener {\n            ActionRegister.actionDetected()\n\n            formEntryFlowLauncher.launch(\n                Intent(requireActivity(), BlankFormListActivity::class.java)\n            )\n        }\n\n        binding.reviewData.setOnClickListener {\n            formEntryFlowLauncher.launch(\n                Intent(requireActivity(), InstanceChooserList::class.java).apply {\n                    putExtra(\n                        FormOpeningMode.FORM_MODE_KEY,\n                        FormOpeningMode.EDIT_SAVED\n                    )\n                }\n            )\n        }\n\n        binding.sendData.setOnClickListener {\n            formEntryFlowLauncher.launch(\n                Intent(\n                    requireActivity(),\n                    InstanceUploaderListActivity::class.java\n                )\n            )\n        }\n\n        binding.viewSentForms.setOnClickListener {\n            startActivity(\n                Intent(requireActivity(), InstanceChooserList::class.java).apply {\n                    putExtra(\n                        FormOpeningMode.FORM_MODE_KEY,\n                        FormOpeningMode.VIEW_SENT\n                    )\n                }\n            )\n        }\n\n        binding.getForms.setOnClickListener {\n            val intent = Intent(requireContext(), FormDownloadListActivity::class.java)\n            startActivity(intent)\n        }\n\n        binding.manageForms.setOnClickListener {\n            startActivity(Intent(requireContext(), DeleteFormsActivity::class.java))\n        }\n\n        mainMenuViewModel.sendableInstancesCount.observe(viewLifecycleOwner) { finalized: Int ->\n            binding.sendData.setNumberOfForms(finalized)\n        }\n        mainMenuViewModel.editableInstancesCount.observe(viewLifecycleOwner) { unsent: Int ->\n            binding.reviewData.setNumberOfForms(unsent)\n        }\n        mainMenuViewModel.sentInstancesCount.observe(viewLifecycleOwner) { sent: Int ->\n            binding.viewSentForms.setNumberOfForms(sent)\n        }\n    }\n\n    private fun initAppName(binding: MainMenuBinding) {\n        binding.appName.text = String.format(\n            \"%s %s\",\n            getString(string.collect_app_name),\n            mainMenuViewModel.version\n        )\n\n        val versionSHA = mainMenuViewModel.versionCommitDescription\n        if (versionSHA != null) {\n            binding.versionSha.text = versionSHA\n        } else {\n            binding.versionSha.visibility = View.GONE\n        }\n    }\n\n    private fun setButtonsVisibility(binding: MainMenuBinding) {\n        binding.reviewData.visibility =\n            if (mainMenuViewModel.shouldEditSavedFormButtonBeVisible()) View.VISIBLE else View.GONE\n        binding.sendData.visibility =\n            if (mainMenuViewModel.shouldSendFinalizedFormButtonBeVisible()) View.VISIBLE else View.GONE\n        binding.viewSentForms.visibility =\n            if (mainMenuViewModel.shouldViewSentFormButtonBeVisible()) View.VISIBLE else View.GONE\n        binding.getForms.visibility =\n            if (mainMenuViewModel.shouldGetBlankFormButtonBeVisible()) View.VISIBLE else View.GONE\n        binding.manageForms.visibility =\n            if (mainMenuViewModel.shouldDeleteSavedFormButtonBeVisible()) View.VISIBLE else View.GONE\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuViewModel.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.app.Application\nimport android.net.Uri\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.asLiveData\nimport androidx.lifecycle.map\nimport androidx.lifecycle.switchMap\nimport org.odk.collect.android.instancemanagement.InstanceDiskSynchronizer\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider\nimport org.odk.collect.android.instancemanagement.autosend.shouldFormBeSentAutomatically\nimport org.odk.collect.android.instancemanagement.canBeEdited\nimport org.odk.collect.android.instancemanagement.isDraft\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.utilities.ContentUriHelper\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.enums.FormUpdateMode\nimport org.odk.collect.settings.enums.StringIdEnumUtils.getFormUpdateMode\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\n\nclass MainMenuViewModel(\n    private val application: Application,\n    private val versionInformation: VersionInformation,\n    private val settingsProvider: SettingsProvider,\n    private val instancesDataService: InstancesDataService,\n    private val scheduler: Scheduler,\n    private val formsRepositoryProvider: FormsRepositoryProvider,\n    private val instancesRepositoryProvider: InstancesRepositoryProvider,\n    private val autoSendSettingsProvider: AutoSendSettingsProvider,\n    private val projectsDataService: ProjectsDataService\n) : ViewModel() {\n\n    val version: String\n        get() = versionInformation.versionToDisplay\n\n    val versionCommitDescription: String?\n        get() {\n            var commitDescription = \"\"\n            if (versionInformation.commitCount != null) {\n                commitDescription =\n                    appendToCommitDescription(\n                        commitDescription,\n                        versionInformation.commitCount.toString()\n                    )\n            }\n            if (versionInformation.commitSHA != null) {\n                commitDescription =\n                    appendToCommitDescription(commitDescription, versionInformation.commitSHA!!)\n            }\n            if (versionInformation.isDirty) {\n                commitDescription = appendToCommitDescription(commitDescription, \"dirty\")\n            }\n            return if (commitDescription.isNotEmpty()) {\n                commitDescription\n            } else {\n                null\n            }\n        }\n\n    private val _savedForm = MutableLiveData<SavedForm>()\n    val savedForm: LiveData<Consumable<SavedForm>> = _savedForm.map { Consumable(it) }\n\n    fun shouldEditSavedFormButtonBeVisible(): Boolean {\n        return settingsProvider.getProtectedSettings()\n            .getBoolean(ProtectedProjectKeys.KEY_EDIT_SAVED)\n    }\n\n    fun shouldSendFinalizedFormButtonBeVisible(): Boolean {\n        return settingsProvider.getProtectedSettings()\n            .getBoolean(ProtectedProjectKeys.KEY_SEND_FINALIZED)\n    }\n\n    fun shouldViewSentFormButtonBeVisible(): Boolean {\n        return settingsProvider.getProtectedSettings()\n            .getBoolean(ProtectedProjectKeys.KEY_VIEW_SENT)\n    }\n\n    fun shouldGetBlankFormButtonBeVisible(): Boolean {\n        val buttonEnabled =\n            settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_GET_BLANK)\n        return !isMatchExactlyEnabled() && buttonEnabled\n    }\n\n    fun shouldDeleteSavedFormButtonBeVisible(): Boolean {\n        return settingsProvider.getProtectedSettings()\n            .getBoolean(ProtectedProjectKeys.KEY_DELETE_SAVED)\n    }\n\n    private fun isMatchExactlyEnabled(): Boolean {\n        return settingsProvider.getUnprotectedSettings()\n            .getFormUpdateMode(application) == FormUpdateMode.MATCH_EXACTLY\n    }\n\n    private fun appendToCommitDescription(commitDescription: String, part: String): String {\n        return if (commitDescription.isEmpty()) {\n            part\n        } else {\n            \"$commitDescription-$part\"\n        }\n    }\n\n    fun refreshInstances() {\n        scheduler.immediate<Any?>({\n            InstanceDiskSynchronizer(settingsProvider).doInBackground()\n            instancesDataService.update(projectsDataService.requireCurrentProject().uuid)\n            null\n        }) { }\n    }\n\n    private val currentProject = projectsDataService.getCurrentProject().asLiveData()\n    val editableInstancesCount: LiveData<Int> = currentProject.switchMap {\n        instancesDataService.getEditableCount(it!!.uuid).asLiveData()\n    }\n\n    val sendableInstancesCount: LiveData<Int> = currentProject.switchMap {\n        instancesDataService.getSendableCount(it!!.uuid).asLiveData()\n    }\n\n    val sentInstancesCount: LiveData<Int> = currentProject.switchMap {\n        instancesDataService.getSentCount(it!!.uuid).asLiveData()\n    }\n\n    fun setSavedForm(uri: Uri?) {\n        if (uri == null) {\n            return\n        }\n\n        scheduler.immediate {\n            val details = getFormSavedSnackbarDetails(uri)\n            if (details != null) {\n                _savedForm.postValue(SavedForm(uri, details.first, details.second))\n            }\n        }\n    }\n\n    private fun getFormSavedSnackbarDetails(uri: Uri): Pair<Int, Int?>? {\n        val instance = instancesRepositoryProvider.create().get(ContentUriHelper.getIdFromUri(uri))\n        return if (instance != null) {\n            val message = if (instance.isDraft()) {\n                org.odk.collect.strings.R.string.form_saved_as_draft\n            } else if (instance.status == Instance.STATUS_COMPLETE || instance.status == Instance.STATUS_SUBMISSION_FAILED) {\n                val form = formsRepositoryProvider.create()\n                    .getAllByFormIdAndVersion(instance.formId, instance.formVersion).first()\n                if (form.shouldFormBeSentAutomatically(autoSendSettingsProvider.isAutoSendEnabledInSettings())) {\n                    org.odk.collect.strings.R.string.form_sending\n                } else {\n                    org.odk.collect.strings.R.string.form_saved\n                }\n            } else {\n                return null\n            }\n\n            val action = if (instance.canBeEdited(settingsProvider)) {\n                org.odk.collect.strings.R.string.edit_form\n            } else {\n                if (instance.isDraft() || instance.canEditWhenComplete()) {\n                    org.odk.collect.strings.R.string.view_form\n                } else {\n                    null\n                }\n            }\n\n            return Pair(message, action)\n        } else {\n            null\n        }\n    }\n\n    data class SavedForm(val uri: Uri, val message: Int, val action: Int?)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuViewModelFactory.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.app.Application\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.settings.SettingsProvider\n\nopen class MainMenuViewModelFactory(\n    private val versionInformation: VersionInformation,\n    private val application: Application,\n    private val settingsProvider: SettingsProvider,\n    private val instancesDataService: InstancesDataService,\n    private val scheduler: Scheduler,\n    private val projectsDataService: ProjectsDataService,\n    private val permissionChecker: PermissionsChecker,\n    private val formsRepositoryProvider: FormsRepositoryProvider,\n    private val instancesRepositoryProvider: InstancesRepositoryProvider,\n    private val autoSendSettingsProvider: AutoSendSettingsProvider\n) : ViewModelProvider.Factory {\n    override fun <T : ViewModel> create(modelClass: Class<T>): T {\n        return when (modelClass) {\n            MainMenuViewModel::class.java -> MainMenuViewModel(\n                application,\n                versionInformation,\n                settingsProvider,\n                instancesDataService,\n                scheduler,\n                formsRepositoryProvider,\n                instancesRepositoryProvider,\n                autoSendSettingsProvider,\n                projectsDataService\n            )\n\n            CurrentProjectViewModel::class.java -> CurrentProjectViewModel(\n                projectsDataService\n            )\n\n            RequestPermissionsViewModel::class.java -> RequestPermissionsViewModel(\n                settingsProvider,\n                permissionChecker\n            )\n\n            else -> throw IllegalArgumentException()\n        } as T\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/mainmenu/PermissionsDialogFragment.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.app.Dialog\nimport android.os.Build\nimport android.os.Bundle\nimport androidx.annotation.RequiresApi\nimport androidx.fragment.app.DialogFragment\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.android.R\nimport org.odk.collect.permissions.PermissionListener\nimport org.odk.collect.permissions.PermissionsProvider\n\nclass PermissionsDialogFragment(\n    private val permissionsProvider: PermissionsProvider,\n    private val requestPermissionsViewModel: RequestPermissionsViewModel\n) : DialogFragment() {\n\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        isCancelable = false\n\n        return MaterialAlertDialogBuilder(requireContext())\n            .setTitle(org.odk.collect.strings.R.string.permission_dialog_title)\n            .setView(R.layout.permissions_dialog_layout)\n            .setPositiveButton(org.odk.collect.strings.R.string.ok) { _, _ ->\n                requestPermissionsViewModel.permissionsRequested()\n                permissionsProvider.requestPermissions(\n                    requireActivity(),\n                    object : PermissionListener {\n                        override fun granted() {}\n                    },\n                    *requestPermissionsViewModel.permissions\n                )\n            }\n            .create()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/mainmenu/RequestPermissionsViewModel.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.Manifest\nimport android.os.Build\nimport androidx.annotation.RequiresApi\nimport androidx.lifecycle.ViewModel\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.MetaKeys\n\nclass RequestPermissionsViewModel(\n    private val settingsProvider: SettingsProvider,\n    private val permissionChecker: PermissionsChecker\n) : ViewModel() {\n\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    val permissions = arrayOf(Manifest.permission.POST_NOTIFICATIONS)\n\n    fun shouldAskForPermissions(): Boolean {\n        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {\n            false\n        } else {\n            val permissionsAlreadyRequested =\n                settingsProvider.getMetaSettings().getBoolean(MetaKeys.PERMISSIONS_REQUESTED)\n            val permissionsGranted =\n                permissionChecker.isPermissionGranted(*permissions)\n\n            !(permissionsAlreadyRequested || permissionsGranted)\n        }\n    }\n\n    fun permissionsRequested() {\n        settingsProvider.getMetaSettings().save(MetaKeys.PERMISSIONS_REQUESTED, true)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/mainmenu/StartNewFormButton.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.widget.FrameLayout\nimport android.widget.TextView\nimport org.odk.collect.android.R\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\nclass StartNewFormButton(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {\n\n    constructor(context: Context) : this(context, null)\n\n    init {\n        inflate(context, R.layout.start_new_from_button, this)\n    }\n\n    val text: String\n        get() = findViewById<TextView>(R.id.name).text.toString()\n\n    override fun performClick(): Boolean {\n        return MultiClickGuard.allowClick(context.getString(R.string.main_menu_screen)) && super.performClick()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/notifications/NotificationManagerNotifier.kt",
    "content": "package org.odk.collect.android.notifications\n\nimport android.app.Application\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.content.Context\nimport org.odk.collect.android.formmanagement.ServerFormDetails\nimport org.odk.collect.android.formmanagement.download.FormDownloadException\nimport org.odk.collect.android.notifications.builders.FormUpdatesAvailableNotificationBuilder\nimport org.odk.collect.android.notifications.builders.FormUpdatesDownloadedNotificationBuilder\nimport org.odk.collect.android.notifications.builders.FormsSubmissionNotificationBuilder\nimport org.odk.collect.android.notifications.builders.FormsSyncFailedNotificationBuilder\nimport org.odk.collect.android.notifications.builders.FormsSyncStoppedNotificationBuilder\nimport org.odk.collect.android.upload.FormUploadException\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.MetaKeys\nimport org.odk.collect.strings.localization.getLocalizedString\n\nclass NotificationManagerNotifier(\n    private val application: Application,\n    private val settingsProvider: SettingsProvider,\n    private val projectsRepository: ProjectsRepository,\n    private val uniqueIdGenerator: UniqueIdGenerator\n) : Notifier {\n    private val notificationManager: NotificationManager =\n        application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n\n    override fun onUpdatesAvailable(updates: List<ServerFormDetails>, projectId: String) {\n        val notificationId = uniqueIdGenerator.getInt(FORM_UPDATE_NOTIFICATION_IDENTIFIER)\n\n        val metaPrefs = settingsProvider.getMetaSettings()\n        val updateId = updates.mapTo(HashSet()) { (_, _, formId, _, hash, manifest, _) ->\n            formId + hash + manifest?.hash\n        }\n        if (metaPrefs.getStringSet(MetaKeys.LAST_UPDATED_NOTIFICATION) != updateId) {\n            notificationManager.notify(\n                notificationId,\n                FormUpdatesAvailableNotificationBuilder.build(\n                    application,\n                    getProjectName(projectId),\n                    notificationId\n                )\n            )\n            metaPrefs.save(MetaKeys.LAST_UPDATED_NOTIFICATION, updateId)\n        }\n    }\n\n    override fun onUpdatesDownloaded(result: Map<ServerFormDetails, FormDownloadException?>, projectId: String) {\n        val notificationId = uniqueIdGenerator.getInt(FORM_UPDATE_NOTIFICATION_IDENTIFIER)\n\n        notificationManager.notify(\n            notificationId,\n            FormUpdatesDownloadedNotificationBuilder.build(\n                application,\n                result,\n                getProjectName(projectId),\n                notificationId\n            )\n        )\n    }\n\n    override fun onSync(exception: FormSourceException?, projectId: String) {\n        val notificationId = uniqueIdGenerator.getInt(FORM_SYNC_ERROR_NOTIFICATION_IDENTIFIER)\n\n        if (exception == null) {\n            notificationManager.cancel(notificationId)\n        } else {\n            notificationManager.notify(\n                notificationId,\n                FormsSyncFailedNotificationBuilder.build(\n                    application,\n                    exception,\n                    getProjectName(projectId),\n                    notificationId\n                )\n            )\n        }\n    }\n\n    override fun onSyncStopped(projectId: String) {\n        val notificationId = uniqueIdGenerator.getInt(FORM_SYNC_ERROR_NOTIFICATION_IDENTIFIER)\n\n        notificationManager.notify(\n            notificationId,\n            FormsSyncStoppedNotificationBuilder.build(\n                application,\n                getProjectName(projectId),\n                notificationId\n            )\n        )\n    }\n\n    override fun onSubmission(result: Map<Instance, FormUploadException?>, projectId: String) {\n        val notificationId = uniqueIdGenerator.getInt(AUTO_SEND_RESULT_NOTIFICATION_IDENTIFIER)\n\n        notificationManager.notify(\n            notificationId,\n            FormsSubmissionNotificationBuilder.build(\n                application,\n                result,\n                getProjectName(projectId),\n                notificationId\n            )\n        )\n    }\n\n    companion object {\n        const val COLLECT_NOTIFICATION_CHANNEL = \"collect_notification_channel\"\n        private val FORM_UPDATE_NOTIFICATION_IDENTIFIER = \"form_update\"\n        private val FORM_SYNC_ERROR_NOTIFICATION_IDENTIFIER = \"form_sync_error\"\n        private val AUTO_SEND_RESULT_NOTIFICATION_IDENTIFIER = \"auto_send_result\"\n    }\n\n    private fun getProjectName(projectId: String) = projectsRepository.get(projectId)?.name ?: \"\"\n\n    init {\n        notificationManager.createNotificationChannel(\n            NotificationChannel(\n                COLLECT_NOTIFICATION_CHANNEL,\n                application.getLocalizedString(org.odk.collect.strings.R.string.notification_channel_name),\n                NotificationManager.IMPORTANCE_DEFAULT\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/notifications/NotificationUtils.kt",
    "content": "package org.odk.collect.android.notifications\n\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.errors.ErrorActivity\nimport org.odk.collect.errors.ErrorItem\nimport java.io.Serializable\n\nobject NotificationUtils {\n\n    /**\n     * Creates a [PendingIntent] that will start the [MainMenuActivity]. [MainMenuActivity]\n     * finishes automatically if it's not started as the root of a task, so that means the\n     * [Intent] will either land the user where they were last or reopen the app.\n     */\n    fun createOpenAppContentIntent(context: Context, notificationId: Int): PendingIntent {\n        val intent = context\n            .packageManager\n            .getLaunchIntentForPackage(BuildConfig.APPLICATION_ID)\n\n        return PendingIntent.getActivity(\n            context,\n            notificationId,\n            intent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n\n    fun createOpenErrorsActionIntent(\n        context: Context,\n        errorItems: List<ErrorItem>,\n        notificationId: Int\n    ): PendingIntent {\n        val showDetailsIntent = Intent(context, ErrorActivity::class.java).apply {\n            putExtra(ErrorActivity.EXTRA_ERRORS, errorItems as Serializable)\n            putExtra(ErrorActivity.EXTRA_NOTIFICATION_ID, notificationId)\n        }\n\n        return PendingIntent.getActivity(\n            context,\n            notificationId,\n            showDetailsIntent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/notifications/Notifier.kt",
    "content": "package org.odk.collect.android.notifications\n\nimport org.odk.collect.android.formmanagement.ServerFormDetails\nimport org.odk.collect.android.formmanagement.download.FormDownloadException\nimport org.odk.collect.android.upload.FormUploadException\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.forms.instances.Instance\n\ninterface Notifier {\n    fun onUpdatesAvailable(updates: List<ServerFormDetails>, projectId: String)\n    fun onUpdatesDownloaded(result: Map<ServerFormDetails, FormDownloadException?>, projectId: String)\n    fun onSync(exception: FormSourceException?, projectId: String)\n    fun onSyncStopped(projectId: String)\n    fun onSubmission(result: Map<Instance, FormUploadException?>, projectId: String)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormUpdatesAvailableNotificationBuilder.kt",
    "content": "package org.odk.collect.android.notifications.builders\n\nimport android.app.Application\nimport android.app.Notification\nimport androidx.core.app.NotificationCompat\nimport org.odk.collect.android.notifications.NotificationManagerNotifier\nimport org.odk.collect.android.notifications.NotificationUtils\nimport org.odk.collect.strings.localization.getLocalizedString\n\nobject FormUpdatesAvailableNotificationBuilder {\n\n    @JvmStatic\n    fun build(application: Application, projectName: String, notificationId: Int): Notification {\n        val contentIntent = NotificationUtils.createOpenAppContentIntent(\n            application,\n            notificationId\n        )\n\n        return NotificationCompat.Builder(\n            application,\n            NotificationManagerNotifier.COLLECT_NOTIFICATION_CHANNEL\n        ).apply {\n            setContentIntent(contentIntent)\n            setContentTitle(application.getLocalizedString(org.odk.collect.strings.R.string.form_updates_available))\n            setContentText(null)\n            setSubText(projectName)\n            setSmallIcon(org.odk.collect.icons.R.drawable.ic_notification_small)\n            setAutoCancel(true)\n        }.build()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormUpdatesDownloadedNotificationBuilder.kt",
    "content": "package org.odk.collect.android.notifications.builders\n\nimport android.app.Application\nimport android.app.Notification\nimport androidx.core.app.NotificationCompat\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formmanagement.ServerFormDetails\nimport org.odk.collect.android.formmanagement.download.FormDownloadException\nimport org.odk.collect.android.notifications.NotificationManagerNotifier\nimport org.odk.collect.android.notifications.NotificationUtils\nimport org.odk.collect.android.utilities.FormsDownloadResultInterpreter\nimport org.odk.collect.strings.localization.getLocalizedString\n\nobject FormUpdatesDownloadedNotificationBuilder {\n\n    fun build(application: Application, result: Map<ServerFormDetails, FormDownloadException?>, projectName: String, notificationId: Int): Notification {\n        val allFormsDownloadedSuccessfully = FormsDownloadResultInterpreter.allFormsDownloadedSuccessfully(result)\n\n        val contentIntent = NotificationUtils.createOpenAppContentIntent(\n            application,\n            notificationId\n        )\n\n        val errorItems = FormsDownloadResultInterpreter.getFailures(result, application)\n        val showDetailsIntent =\n            NotificationUtils.createOpenErrorsActionIntent(application, errorItems, notificationId)\n\n        val title =\n            if (allFormsDownloadedSuccessfully) {\n                application.getLocalizedString(org.odk.collect.strings.R.string.forms_download_succeeded)\n            } else {\n                application.getLocalizedString(org.odk.collect.strings.R.string.forms_download_failed)\n            }\n\n        val message =\n            if (allFormsDownloadedSuccessfully) {\n                application.getLocalizedString(org.odk.collect.strings.R.string.all_downloads_succeeded)\n            } else {\n                application.getLocalizedString(\n                    org.odk.collect.strings.R.string.some_downloads_failed,\n                    FormsDownloadResultInterpreter.getNumberOfFailures(result),\n                    result.size\n                )\n            }\n\n        return NotificationCompat.Builder(\n            application,\n            NotificationManagerNotifier.COLLECT_NOTIFICATION_CHANNEL\n        ).apply {\n            setContentIntent(contentIntent)\n            setContentTitle(title)\n            setContentText(message)\n            setSubText(projectName)\n            setSmallIcon(org.odk.collect.icons.R.drawable.ic_notification_small)\n            setAutoCancel(true)\n\n            if (!allFormsDownloadedSuccessfully) {\n                addAction(\n                    R.drawable.ic_outline_info_small,\n                    application.getLocalizedString(org.odk.collect.strings.R.string.show_details),\n                    showDetailsIntent\n                )\n            }\n        }.build()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSubmissionNotificationBuilder.kt",
    "content": "package org.odk.collect.android.notifications.builders\n\nimport android.app.Application\nimport android.app.Notification\nimport androidx.core.app.NotificationCompat\nimport org.odk.collect.android.R\nimport org.odk.collect.android.notifications.NotificationManagerNotifier\nimport org.odk.collect.android.notifications.NotificationUtils\nimport org.odk.collect.android.upload.FormUploadException\nimport org.odk.collect.android.utilities.FormsUploadResultInterpreter\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.strings.localization.getLocalizedString\n\nobject FormsSubmissionNotificationBuilder {\n\n    fun build(\n        application: Application,\n        result: Map<Instance, FormUploadException?>,\n        projectName: String,\n        notificationId: Int\n    ): Notification {\n        val allFormsUploadedSuccessfully = FormsUploadResultInterpreter.allFormsUploadedSuccessfully(result)\n\n        return NotificationCompat.Builder(\n            application,\n            NotificationManagerNotifier.COLLECT_NOTIFICATION_CHANNEL\n        ).apply {\n            setContentIntent(\n                NotificationUtils.createOpenAppContentIntent(\n                    application,\n                    notificationId\n                )\n            )\n            setContentTitle(getTitle(application, allFormsUploadedSuccessfully))\n            setContentText(getMessage(application, allFormsUploadedSuccessfully, result))\n            setSubText(projectName)\n            setSmallIcon(org.odk.collect.icons.R.drawable.ic_notification_small)\n            setAutoCancel(true)\n\n            if (!allFormsUploadedSuccessfully) {\n                val errorItems = FormsUploadResultInterpreter.getFailures(result, application)\n\n                addAction(\n                    R.drawable.ic_outline_info_small,\n                    application.getLocalizedString(org.odk.collect.strings.R.string.show_details),\n                    NotificationUtils.createOpenErrorsActionIntent(application, errorItems, notificationId)\n                )\n            }\n        }.build()\n    }\n\n    private fun getTitle(application: Application, allFormsUploadedSuccessfully: Boolean): String {\n        return if (allFormsUploadedSuccessfully) {\n            application.getLocalizedString(org.odk.collect.strings.R.string.forms_upload_succeeded)\n        } else {\n            application.getLocalizedString(org.odk.collect.strings.R.string.forms_upload_failed)\n        }\n    }\n\n    private fun getMessage(application: Application, allFormsUploadedSuccessfully: Boolean, result: Map<Instance, FormUploadException?>): String {\n        return if (allFormsUploadedSuccessfully) {\n            application.getLocalizedString(org.odk.collect.strings.R.string.all_uploads_succeeded)\n        } else {\n            application.getLocalizedString(\n                org.odk.collect.strings.R.string.some_uploads_failed,\n                FormsUploadResultInterpreter.getNumberOfFailures(result),\n                result.size\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSyncFailedNotificationBuilder.kt",
    "content": "package org.odk.collect.android.notifications.builders\n\nimport android.app.Application\nimport android.app.Notification\nimport android.app.PendingIntent\nimport androidx.core.app.NotificationCompat\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formmanagement.FormSourceExceptionMapper\nimport org.odk.collect.android.notifications.NotificationManagerNotifier\nimport org.odk.collect.android.notifications.NotificationUtils\nimport org.odk.collect.android.notifications.NotificationUtils.createOpenErrorsActionIntent\nimport org.odk.collect.errors.ErrorItem\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.strings.localization.getLocalizedString\n\nobject FormsSyncFailedNotificationBuilder {\n\n    fun build(application: Application, exception: FormSourceException, projectName: String, notificationId: Int): Notification {\n        val contentIntent = NotificationUtils.createOpenAppContentIntent(application, notificationId)\n\n        return NotificationCompat.Builder(\n            application,\n            NotificationManagerNotifier.COLLECT_NOTIFICATION_CHANNEL\n        ).apply {\n            setContentIntent(contentIntent)\n            setContentTitle(application.getLocalizedString(org.odk.collect.strings.R.string.form_update_error))\n            setSubText(projectName)\n            setSmallIcon(org.odk.collect.icons.R.drawable.ic_notification_small)\n            setAutoCancel(true)\n            addAction(\n                R.drawable.ic_outline_info_small,\n                application.getLocalizedString(org.odk.collect.strings.R.string.show_details),\n                getShowDetailsPendingIntent(application, projectName, exception, notificationId)\n            )\n        }.build()\n    }\n\n    private fun getShowDetailsPendingIntent(\n        application: Application,\n        projectName: String,\n        exception: FormSourceException,\n        notificationId: Int\n    ): PendingIntent {\n        val errorItem = ErrorItem(\n            application.getLocalizedString(org.odk.collect.strings.R.string.form_update_error),\n            projectName,\n            FormSourceExceptionMapper(application).getMessage(exception)\n        )\n\n        return createOpenErrorsActionIntent(application, listOf(errorItem), notificationId)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/notifications/builders/FormsSyncStoppedNotificationBuilder.kt",
    "content": "package org.odk.collect.android.notifications.builders\n\nimport android.app.Application\nimport android.app.Notification\nimport android.app.PendingIntent\nimport android.content.Intent\nimport androidx.core.app.NotificationCompat\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListActivity\nimport org.odk.collect.android.notifications.NotificationManagerNotifier\nimport org.odk.collect.strings.localization.getLocalizedString\n\nobject FormsSyncStoppedNotificationBuilder {\n\n    fun build(application: Application, projectName: String, notificationId: Int): Notification {\n        val contentIntent = PendingIntent.getActivity(\n            application,\n            notificationId,\n            Intent(application, BlankFormListActivity::class.java),\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n\n        return NotificationCompat.Builder(\n            application,\n            NotificationManagerNotifier.COLLECT_NOTIFICATION_CHANNEL\n        ).apply {\n            setContentIntent(contentIntent)\n            setContentTitle(application.getLocalizedString(org.odk.collect.strings.R.string.form_update_error))\n            setContentText(application.getLocalizedString(org.odk.collect.strings.R.string.form_update_error_massage))\n            setSubText(projectName)\n            setSmallIcon(org.odk.collect.icons.R.drawable.ic_notification_small)\n            setAutoCancel(true)\n        }.build()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/Defaults.kt",
    "content": "package org.odk.collect.android.preferences\n\nimport com.google.android.gms.maps.GoogleMap\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\n\nobject Defaults {\n\n    @JvmStatic\n    val unprotected: HashMap<String, Any>\n        get() {\n            val hashMap = HashMap<String, Any>()\n            // odk_server_preferences.xml\n            hashMap[ProjectKeys.KEY_SERVER_URL] = \"https://demo.getodk.org\"\n            hashMap[ProjectKeys.KEY_USERNAME] = \"\"\n            hashMap[ProjectKeys.KEY_PASSWORD] = \"\"\n            // form_management_preferences.xml\n            hashMap[ProjectKeys.KEY_AUTOSEND] = \"off\"\n            hashMap[ProjectKeys.KEY_GUIDANCE_HINT] = \"yes_collapsed\"\n            hashMap[ProjectKeys.KEY_DELETE_AFTER_SEND] = false\n            hashMap[ProjectKeys.KEY_CONSTRAINT_BEHAVIOR] = ProjectKeys.CONSTRAINT_BEHAVIOR_ON_SWIPE\n            hashMap[ProjectKeys.KEY_HIGH_RESOLUTION] = true\n            hashMap[ProjectKeys.KEY_IMAGE_SIZE] = \"original_image_size\"\n            hashMap[ProjectKeys.KEY_INSTANCE_SYNC] = true\n            hashMap[ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK] = \"every_fifteen_minutes\"\n            hashMap[ProjectKeys.KEY_AUTOMATIC_UPDATE] = false\n            hashMap[ProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS] = true\n            hashMap[ProjectKeys.KEY_BACKGROUND_LOCATION] = true\n            hashMap[ProjectKeys.KEY_BACKGROUND_RECORDING] = true\n            hashMap[ProjectKeys.KEY_FORM_UPDATE_MODE] = \"manual\"\n            // form_metadata_preferences.xml\n            hashMap[ProjectKeys.KEY_METADATA_USERNAME] = \"\"\n            hashMap[ProjectKeys.KEY_METADATA_PHONENUMBER] = \"\"\n            hashMap[ProjectKeys.KEY_METADATA_EMAIL] = \"\"\n            // identity_preferences.xml\n            hashMap[ProjectKeys.KEY_ANALYTICS] = true\n            // server_preferences.xml\n            hashMap[ProjectKeys.KEY_PROTOCOL] = ProjectKeys.PROTOCOL_SERVER\n            // user_interface_preferences.xml\n            hashMap[ProjectKeys.KEY_APP_LANGUAGE] = \"\"\n            hashMap[ProjectKeys.KEY_FONT_SIZE] = QuestionFontSizeUtils.DEFAULT_FONT_SIZE.toString()\n            hashMap[ProjectKeys.KEY_NAVIGATION] = ProjectKeys.NAVIGATION_BOTH\n            hashMap[ProjectKeys.KEY_EXTERNAL_APP_RECORDING] = false\n            // map_preferences.xml\n            hashMap[ProjectKeys.KEY_BASEMAP_SOURCE] = ProjectKeys.BASEMAP_SOURCE_GOOGLE\n            hashMap[ProjectKeys.KEY_CARTO_MAP_STYLE] = \"positron\"\n            hashMap[ProjectKeys.KEY_USGS_MAP_STYLE] = \"topographic\"\n            hashMap[ProjectKeys.KEY_GOOGLE_MAP_STYLE] = GoogleMap.MAP_TYPE_NORMAL.toString()\n            hashMap[ProjectKeys.KEY_MAPBOX_MAP_STYLE] = \"mapbox://styles/mapbox/streets-v11\"\n            // experimental_preferences.xml\n            hashMap[ProjectKeys.KEY_DEBUG_FILTERS] = BuildConfig.BUILD_TYPE == \"selfSignedRelease\"\n            hashMap[ProjectKeys.KEY_ZXING_SCANNING] = false\n            return hashMap\n        }\n\n    @JvmStatic\n    val protected: Map<String, Any>\n        get() {\n            val defaults: MutableMap<String, Any> = HashMap()\n            for (key in ProtectedProjectKeys.allKeys()) {\n                if (key == ProtectedProjectKeys.KEY_ADMIN_PW) {\n                    defaults[key] = \"\"\n                } else {\n                    defaults[key] = true\n                }\n            }\n\n            return defaults\n        }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/PreferenceVisibilityHandler.kt",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.preferences\n\nimport androidx.preference.PreferenceGroup\nimport androidx.preference.PreferenceScreen\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\n\nclass PreferenceVisibilityHandler(\n    private val settingsProvider: SettingsProvider,\n    private val versionInformation: VersionInformation\n) {\n\n    fun updatePreferencesVisibility(preferenceScreen: PreferenceScreen, state: ProjectPreferencesViewModel.State) {\n        updatePreferences(preferenceScreen, state)\n        updateCategories(preferenceScreen)\n    }\n\n    // Hides preferences that are excluded by the protected settings\n    private fun updatePreferences(preferenceGroup: PreferenceGroup, state: ProjectPreferencesViewModel.State) {\n        for (i in 0 until preferenceGroup.preferenceCount) {\n            val preference = preferenceGroup.getPreference(i)\n            if (preference is PreferenceGroup) {\n                updatePreferences(preference, state)\n            }\n            when (preference.key) {\n                \"protocol\" -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_CHANGE_SERVER)\n                \"project_display\" -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_CHANGE_PROJECT_DISPLAY)\n                \"user_interface\" -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || hasAtLeastOnePreferenceEnabled(\n                    listOf(\n                        ProtectedProjectKeys.KEY_APP_LANGUAGE,\n                        ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE,\n                        ProtectedProjectKeys.KEY_NAVIGATION\n                    )\n                )\n                \"maps\" -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_MAPS)\n                \"form_management\" -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || hasAtLeastOnePreferenceEnabled(\n                    listOf(\n                        ProtectedProjectKeys.KEY_FORM_UPDATE_MODE,\n                        ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK,\n                        ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE,\n                        ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS,\n                        ProtectedProjectKeys.KEY_AUTOSEND,\n                        ProtectedProjectKeys.KEY_DELETE_AFTER_SEND,\n                        ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR,\n                        ProtectedProjectKeys.KEY_HIGH_RESOLUTION,\n                        ProtectedProjectKeys.KEY_IMAGE_SIZE,\n                        ProtectedProjectKeys.KEY_GUIDANCE_HINT,\n                        ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING,\n                        ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC\n                    )\n                )\n                \"user_and_device_identity\" -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || hasAtLeastOnePreferenceEnabled(\n                    listOf(\n                        ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA,\n                        ProtectedProjectKeys.KEY_ANALYTICS\n                    )\n                )\n                ProjectKeys.KEY_APP_LANGUAGE -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_APP_LANGUAGE)\n                ProjectKeys.KEY_FONT_SIZE -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE)\n                ProjectKeys.KEY_NAVIGATION -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_NAVIGATION)\n\n                ProjectKeys.KEY_FORM_UPDATE_MODE -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE)\n                ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)\n                ProjectKeys.KEY_AUTOMATIC_UPDATE -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE)\n                ProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS)\n                ProjectKeys.KEY_AUTOSEND -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_AUTOSEND)\n                ProjectKeys.KEY_DELETE_AFTER_SEND -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND)\n                ProjectKeys.KEY_CONSTRAINT_BEHAVIOR -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR)\n                ProjectKeys.KEY_HIGH_RESOLUTION -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_HIGH_RESOLUTION)\n                ProjectKeys.KEY_IMAGE_SIZE -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_IMAGE_SIZE)\n                ProjectKeys.KEY_GUIDANCE_HINT -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_GUIDANCE_HINT)\n                ProjectKeys.KEY_EXTERNAL_APP_RECORDING -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING)\n                ProjectKeys.KEY_INSTANCE_SYNC -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC)\n\n                ProjectKeys.KEY_FORM_METADATA -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA)\n                ProjectKeys.KEY_ANALYTICS -> preference.isVisible = state == ProjectPreferencesViewModel.State.UNLOCKED || isOptionEnabled(ProtectedProjectKeys.KEY_ANALYTICS)\n\n                \"experimental\" -> preference.isVisible = !versionInformation.isRelease || BuildConfig.DEBUG\n\n                \"admin_password\" -> preference.isVisible = state != ProjectPreferencesViewModel.State.LOCKED\n                \"project_management\" -> preference.isVisible = state != ProjectPreferencesViewModel.State.LOCKED\n                \"access_control\" -> preference.isVisible = state != ProjectPreferencesViewModel.State.LOCKED\n                \"unlock_protected_settings\" -> preference.isVisible = state == ProjectPreferencesViewModel.State.LOCKED\n            }\n        }\n    }\n\n    // Hides empty categories - this won't work with nested categories but we don't use them in our\n    // settings and we rather shouldn't do that in the future since it would make them very complex\n    private fun updateCategories(preferenceScreen: PreferenceScreen) {\n        for (i in 0 until preferenceScreen.preferenceCount) {\n            val preference = preferenceScreen.getPreference(i)\n            if (preference is PreferenceGroup) {\n                preference.isVisible = hasCategoryAnyVisiblePreferences(preference)\n            }\n        }\n    }\n\n    private fun hasCategoryAnyVisiblePreferences(preferenceGroup: PreferenceGroup): Boolean {\n        for (i in 0 until preferenceGroup.preferenceCount) {\n            val preference = preferenceGroup.getPreference(i)\n            if (preference.isVisible) {\n                return true\n            }\n        }\n        return false\n    }\n\n    private fun hasAtLeastOnePreferenceEnabled(keys: Collection<String>): Boolean {\n        for (key in keys) {\n            val value = settingsProvider.getProtectedSettings().getBoolean(key)\n            if (value) {\n                return true\n            }\n        }\n        return false\n    }\n\n    private fun isOptionEnabled(key: String) = settingsProvider.getProtectedSettings().getBoolean(key)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/ProjectPreferencesViewModel.kt",
    "content": "package org.odk.collect.android.preferences\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.android.utilities.AdminPasswordProvider\nimport org.odk.collect.androidshared.data.Consumable\n\nclass ProjectPreferencesViewModel(adminPasswordProvider: AdminPasswordProvider) : ViewModel() {\n    enum class State {\n        LOCKED, // Admin password is set but not entered\n        UNLOCKED, // Admin password is set and entered\n        NOT_PROTECTED // Admin password is not set\n    }\n\n    private var _state = MutableLiveData<Consumable<State>>()\n    var state: LiveData<Consumable<State>> = _state\n\n    init {\n        _state.value = if (adminPasswordProvider.isAdminPasswordSet) {\n            Consumable(State.LOCKED)\n        } else {\n            Consumable(State.NOT_PROTECTED)\n        }\n    }\n\n    fun isStateLocked() = state.value == Consumable(State.LOCKED)\n\n    fun isStateUnlocked() = state.value == Consumable(State.UNLOCKED)\n\n    fun isStateNotProtected() = state.value == Consumable(State.NOT_PROTECTED)\n\n    fun setStateLocked() {\n        _state.value = Consumable(State.LOCKED)\n    }\n\n    fun setStateUnlocked() {\n        _state.value = Consumable(State.UNLOCKED)\n    }\n\n    fun setStateNotProtected() {\n        _state.value = Consumable(State.NOT_PROTECTED)\n    }\n\n    open class Factory(private val adminPasswordProvider: AdminPasswordProvider) : ViewModelProvider.Factory {\n        override fun <T : ViewModel> create(modelClass: Class<T>): T {\n            return ProjectPreferencesViewModel(adminPasswordProvider) as T\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/ServerPreferencesAdder.java",
    "content": "/*\n * Copyright (C) 2018 Callum Stott\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.preferences;\n\nimport android.widget.Toast;\n\nimport androidx.preference.PreferenceFragmentCompat;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.preferences.screens.ServerPreferencesFragment;\n\n/**\n * Extracted use case class to isolate and allow testing of functionality (in this\n * case error handling). Originally contained in {@link ServerPreferencesFragment}.\n **/\npublic class ServerPreferencesAdder {\n\n    private final PreferenceFragmentCompat fragment;\n\n    public ServerPreferencesAdder(PreferenceFragmentCompat fragment) {\n        this.fragment = fragment;\n    }\n\n    public boolean add() {\n        try {\n            fragment.addPreferencesFromResource(R.xml.odk_server_preferences);\n            return true;\n        } catch (ClassCastException e) {\n            Toast.makeText(fragment.getActivity(), org.odk.collect.strings.R.string.corrupt_imported_preferences_error, Toast.LENGTH_LONG).show();\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/SettingsExt.kt",
    "content": "package org.odk.collect.android.preferences\n\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.shared.settings.Settings\n\nobject SettingsExt {\n    fun Settings.getExperimentalOptIn(key: String): Boolean {\n        val versionInformation = VersionInformation { BuildConfig.VERSION_NAME }\n\n        return if (!versionInformation.isRelease) {\n            this.getBoolean(key)\n        } else {\n            false\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/dialogs/AdminPasswordDialogFragment.kt",
    "content": "package org.odk.collect.android.preferences.dialogs\n\nimport android.app.Dialog\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport android.text.InputType\nimport android.view.LayoutInflater\nimport android.widget.CompoundButton\nimport androidx.fragment.app.DialogFragment\nimport androidx.lifecycle.ViewModelProvider\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.android.databinding.AdminPasswordDialogLayoutBinding\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel\nimport org.odk.collect.android.utilities.AdminPasswordProvider\nimport org.odk.collect.android.utilities.SoftKeyboardController\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport javax.inject.Inject\n\nclass AdminPasswordDialogFragment : DialogFragment() {\n    @Inject\n    lateinit var factory: ProjectPreferencesViewModel.Factory\n\n    @Inject\n    lateinit var adminPasswordProvider: AdminPasswordProvider\n\n    @Inject\n    lateinit var softKeyboardController: SoftKeyboardController\n\n    lateinit var binding: AdminPasswordDialogLayoutBinding\n\n    lateinit var projectPreferencesViewModel: ProjectPreferencesViewModel\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n        projectPreferencesViewModel = ViewModelProvider(requireActivity(), factory)[ProjectPreferencesViewModel::class.java]\n    }\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        binding = AdminPasswordDialogLayoutBinding.inflate(LayoutInflater.from(context))\n\n        binding.editText.post {\n            softKeyboardController.showSoftKeyboard(binding.editText)\n        }\n\n        binding.checkBox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->\n            if (isChecked) {\n                binding.editText.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD\n            } else {\n                binding.editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD\n            }\n        }\n\n        return MaterialAlertDialogBuilder(requireContext())\n            .setView(binding.root)\n            .setTitle(getString(org.odk.collect.strings.R.string.enter_admin_password))\n            .setPositiveButton(getString(org.odk.collect.strings.R.string.ok)) { _: DialogInterface?, _: Int ->\n                if (adminPasswordProvider.adminPassword == binding.editText.text.toString()) {\n                    projectPreferencesViewModel.setStateUnlocked()\n                } else {\n                    ToastUtils.showShortToast(\n                        org.odk.collect.strings.R.string.admin_password_incorrect\n                    )\n                }\n                dismiss()\n            }\n            .setNegativeButton(getString(org.odk.collect.strings.R.string.cancel)) { _: DialogInterface?, _: Int -> dismiss() }\n            .create()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/dialogs/ChangeAdminPasswordDialog.kt",
    "content": "package org.odk.collect.android.preferences.dialogs\n\nimport android.app.Dialog\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport android.text.InputType\nimport android.view.LayoutInflater\nimport android.widget.CompoundButton\nimport androidx.fragment.app.DialogFragment\nimport androidx.lifecycle.ViewModelProvider\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.android.databinding.PasswordDialogLayoutBinding\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel\nimport org.odk.collect.android.utilities.SoftKeyboardController\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport javax.inject.Inject\n\nclass ChangeAdminPasswordDialog : DialogFragment() {\n    @Inject\n    lateinit var factory: ProjectPreferencesViewModel.Factory\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    @Inject\n    lateinit var softKeyboardController: SoftKeyboardController\n\n    lateinit var binding: PasswordDialogLayoutBinding\n\n    lateinit var projectPreferencesViewModel: ProjectPreferencesViewModel\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n        projectPreferencesViewModel = ViewModelProvider(requireActivity(), factory)[ProjectPreferencesViewModel::class.java]\n    }\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        binding = PasswordDialogLayoutBinding.inflate(LayoutInflater.from(context))\n\n        binding.pwdField.post {\n            softKeyboardController.showSoftKeyboard(binding.pwdField)\n        }\n        binding.checkBox2.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->\n            if (isChecked) {\n                binding.pwdField.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD\n            } else {\n                binding.pwdField.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD\n            }\n        }\n\n        return MaterialAlertDialogBuilder(requireContext())\n            .setView(binding.root)\n            .setTitle(org.odk.collect.strings.R.string.change_admin_password)\n            .setView(binding.root)\n            .setPositiveButton(getString(org.odk.collect.strings.R.string.ok)) { _: DialogInterface?, _: Int ->\n                val password = binding.pwdField.text.toString()\n\n                settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_ADMIN_PW, password)\n\n                if (password.isEmpty()) {\n                    projectPreferencesViewModel.setStateNotProtected()\n                    ToastUtils.showShortToast(\n                        org.odk.collect.strings.R.string.admin_password_disabled\n                    )\n                } else {\n                    projectPreferencesViewModel.setStateUnlocked()\n                    ToastUtils.showShortToast(\n                        org.odk.collect.strings.R.string.admin_password_changed\n                    )\n                }\n                dismiss()\n            }\n            .setNegativeButton(getString(org.odk.collect.strings.R.string.cancel)) { _: DialogInterface?, _: Int -> dismiss() }\n            .setCancelable(false)\n            .create()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/dialogs/DeleteProjectDialog.kt",
    "content": "package org.odk.collect.android.preferences.dialogs\n\nimport android.app.Dialog\nimport android.os.Bundle\nimport androidx.core.text.HtmlCompat\nimport androidx.core.view.isVisible\nimport androidx.core.widget.doAfterTextChanged\nimport androidx.fragment.app.DialogFragment\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.activities.ActivityUtils\nimport org.odk.collect.android.activities.FirstLaunchActivity\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.databinding.DeleteProjectDialogLayoutBinding\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.android.projects.DeleteProjectResult\nimport org.odk.collect.android.projects.ProjectDeleter\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.androidshared.async.TrackableWorker\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.async.Scheduler\n\nclass DeleteProjectDialog(\n    private val projectDeleter: ProjectDeleter,\n    private val projectsDataService: ProjectsDataService,\n    private val formsDataService: FormsDataService,\n    private val instancesDataService: InstancesDataService,\n    private val scheduler: Scheduler\n) : DialogFragment() {\n    private val viewModel: DeleteProjectViewModel by viewModels {\n        object : ViewModelProvider.Factory {\n            override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                return DeleteProjectViewModel(\n                    projectsDataService,\n                    projectDeleter,\n                    formsDataService,\n                    instancesDataService,\n                    scheduler\n                ) as T\n            }\n        }\n    }\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        val binding = DeleteProjectDialogLayoutBinding.inflate(layoutInflater).apply {\n            cancelButton.setOnClickListener { dismiss() }\n            deleteButton.setOnClickListener { viewModel.deleteProject() }\n\n            confirmationFieldInput.doAfterTextChanged { text ->\n                val deleteTrigger = getString(org.odk.collect.strings.R.string.delete_trigger)\n                deleteButton.isEnabled = deleteTrigger.equals(text.toString().trim(), true)\n            }\n        }\n\n        viewModel.isWorking.observe(this) { isWorking ->\n            binding.progressBar.isVisible = isWorking\n            binding.title.isVisible = !isWorking\n            binding.message.isVisible = !isWorking\n            binding.confirmationField.isVisible = !isWorking\n            binding.deleteButton.isClickable = !isWorking\n        }\n\n        viewModel.projectData.observe(this) { projectData ->\n            binding.title.text = getString(\n                org.odk.collect.strings.R.string.delete_project_dialog_title,\n                projectData.projectName\n            )\n            val message = createDeleteMessage(\n                projectData.numberOfForms,\n                projectData.numberOfSentForms,\n                projectData.numberOfUnsentForms,\n                projectData.numberOfDraftForms\n            )\n            binding.message.text = HtmlCompat.fromHtml(message, HtmlCompat.FROM_HTML_MODE_LEGACY)\n        }\n\n        viewModel.deleteProjectResult.observe(this) { result ->\n            dismiss()\n            when (result) {\n                is DeleteProjectResult.DeletedSuccessfullyCurrentProject -> {\n                    val newCurrentProject = result.newCurrentProject\n                    ActivityUtils.startActivityAndCloseAllOthers(\n                        requireActivity(),\n                        MainMenuActivity::class.java\n                    )\n                    ToastUtils.showLongToast(\n                        getString(\n                            org.odk.collect.strings.R.string.switched_project,\n                            newCurrentProject.name\n                        )\n                    )\n                }\n\n                is DeleteProjectResult.DeletedSuccessfullyLastProject -> {\n                    ActivityUtils.startActivityAndCloseAllOthers(\n                        requireActivity(),\n                        FirstLaunchActivity::class.java\n                    )\n                }\n\n                is DeleteProjectResult.DeletedSuccessfullyInactiveProject -> {\n                    // not possible here\n                }\n            }\n        }\n\n        return MaterialAlertDialogBuilder(requireContext())\n            .setView(binding.root)\n            .create()\n    }\n\n    private fun createDeleteMessage(\n        formDefinitionsCount: Int,\n        sentCount: Int,\n        unsentCount: Int,\n        draftsCount: Int\n    ): String {\n        val message = getString(org.odk.collect.strings.R.string.delete_project_message)\n        val formDefinitions =\n            getString(org.odk.collect.strings.R.string.form_definitions_count, formDefinitionsCount)\n        val sent = getString(org.odk.collect.strings.R.string.sent_count, sentCount)\n        val drafts = getString(org.odk.collect.strings.R.string.drafts_count, draftsCount)\n        val unsent = if (unsentCount > 0) {\n            \"${getString(org.odk.collect.strings.R.string.unsent_count, unsentCount)} ⚠\\uFE0F\"\n        } else {\n            getString(org.odk.collect.strings.R.string.unsent_count, unsentCount)\n        }\n\n        val instructions = getString(\n            org.odk.collect.strings.R.string.delete_project_instructions,\n            \"<b>${getString(org.odk.collect.strings.R.string.delete_trigger)}</b>\"\n        )\n\n        return \"\"\"\n        $message:<br/>\n            <br/>\n        • $formDefinitions<br/>\n        • $sent<br/>\n        • $unsent<br/>\n        • $drafts<br/>\n        <br/>\n        $instructions\n        \"\"\"\n    }\n\n    class DeleteProjectViewModel(\n        projectsDataService: ProjectsDataService,\n        private val projectDeleter: ProjectDeleter,\n        private val formsDataService: FormsDataService,\n        private val instancesDataService: InstancesDataService,\n        scheduler: Scheduler\n    ) : ViewModel() {\n        private val trackableWorker = TrackableWorker(scheduler)\n\n        val isWorking = trackableWorker.isWorking\n\n        private val _projectData = MutableLiveData<ProjectData>()\n        val projectData: LiveData<ProjectData> = _projectData\n\n        private val _deleteProjectResult = MutableLiveData<DeleteProjectResult>()\n        val deleteProjectResult: LiveData<DeleteProjectResult> = _deleteProjectResult\n\n        private val project = projectsDataService.getCurrentProject().value!!\n\n        init {\n            trackableWorker.immediate {\n                formsDataService.update(project.uuid)\n                instancesDataService.update(project.uuid)\n\n                val numberOfForms = formsDataService.getFormsCount(project.uuid).value\n                val numberOfSentForms =\n                    instancesDataService.getSuccessfullySentCount(project.uuid).value\n                val numberOfUnsentForms = instancesDataService.getSendableCount(project.uuid).value\n                val numberOfDraftForms = instancesDataService.getEditableCount(project.uuid).value\n                _projectData.postValue(\n                    ProjectData(\n                        project.name,\n                        numberOfForms,\n                        numberOfSentForms,\n                        numberOfUnsentForms,\n                        numberOfDraftForms\n                    )\n                )\n            }\n        }\n\n        fun deleteProject() {\n            Analytics.log(AnalyticsEvents.DELETE_PROJECT)\n            trackableWorker.immediate {\n                val result = projectDeleter.deleteProject(project.uuid)\n                _deleteProjectResult.postValue(result)\n            }\n        }\n\n        data class ProjectData(\n            val projectName: String,\n            val numberOfForms: Int,\n            val numberOfSentForms: Int,\n            val numberOfUnsentForms: Int,\n            val numberOfDraftForms: Int\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/dialogs/ResetDialogPreference.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.preferences.dialogs;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.preference.DialogPreference;\n\nimport org.odk.collect.android.R;\n\npublic class ResetDialogPreference extends DialogPreference {\n\n    public ResetDialogPreference(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    @Override\n    public int getDialogLayoutResource() {\n        return R.layout.reset_dialog_layout;\n    }\n}"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/dialogs/ResetDialogPreferenceFragmentCompat.java",
    "content": "package org.odk.collect.android.preferences.dialogs;\n\nimport static org.odk.collect.android.fragments.dialogs.ResetSettingsResultDialog.RESET_SETTINGS_RESULT_DIALOG_TAG;\nimport static org.odk.collect.android.projects.ProjectResetter.ResetAction.RESET_PREFERENCES;\n\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.os.AsyncTask;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.CompoundButton;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.appcompat.widget.AppCompatCheckBox;\nimport androidx.preference.PreferenceDialogFragmentCompat;\n\nimport org.odk.collect.analytics.Analytics;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.analytics.AnalyticsEvents;\nimport org.odk.collect.android.fragments.dialogs.ResetSettingsResultDialog;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.preferences.screens.ProjectPreferencesActivity;\nimport org.odk.collect.android.projects.ProjectResetter;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\npublic class ResetDialogPreferenceFragmentCompat extends PreferenceDialogFragmentCompat implements CompoundButton.OnCheckedChangeListener {\n\n    @Inject\n    ProjectResetter projectResetter;\n\n    private AppCompatCheckBox preferences;\n    private AppCompatCheckBox instances;\n    private AppCompatCheckBox forms;\n    private AppCompatCheckBox layers;\n    private AppCompatCheckBox cache;\n\n    private Context context;\n\n    public static ResetDialogPreferenceFragmentCompat newInstance(String key) {\n        ResetDialogPreferenceFragmentCompat fragment = new ResetDialogPreferenceFragmentCompat();\n        Bundle bundle = new Bundle(1);\n        bundle.putString(PreferenceDialogFragmentCompat.ARG_KEY, key);\n        fragment.setArguments(bundle);\n        return fragment;\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        this.context = context;\n        super.onAttach(context);\n\n        DaggerUtils.getComponent(context).inject(this);\n    }\n\n    @Override\n    public void onBindDialogView(View view) {\n        preferences = view.findViewById(R.id.preferences);\n        instances = view.findViewById(R.id.instances);\n        forms = view.findViewById(R.id.forms);\n        layers = view.findViewById(R.id.layers);\n        cache = view.findViewById(R.id.cache);\n        preferences.setOnCheckedChangeListener(this);\n        instances.setOnCheckedChangeListener(this);\n        forms.setOnCheckedChangeListener(this);\n        layers.setOnCheckedChangeListener(this);\n        cache.setOnCheckedChangeListener(this);\n        super.onBindDialogView(view);\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        adjustResetButtonAccessibility();\n    }\n\n    @Override\n    public void onDetach() {\n        preferences = null;\n        instances = null;\n        forms = null;\n        layers = null;\n        cache = null;\n        super.onDetach();\n    }\n\n    @Override\n    public void onClick(DialogInterface dialog, int which) {\n        if (which == DialogInterface.BUTTON_POSITIVE) {\n            resetSelected();\n        }\n    }\n\n    @Override\n    public void onDialogClosed(boolean positiveResult) {\n    }\n\n    private void resetSelected() {\n        Analytics.log(AnalyticsEvents.RESET_PROJECT);\n\n        final List<Integer> resetActions = new ArrayList<>();\n\n        if (preferences.isChecked()) {\n            resetActions.add(RESET_PREFERENCES);\n        }\n        if (instances.isChecked()) {\n            resetActions.add(ProjectResetter.ResetAction.RESET_INSTANCES);\n        }\n        if (forms.isChecked()) {\n            resetActions.add(ProjectResetter.ResetAction.RESET_FORMS);\n        }\n        if (layers.isChecked()) {\n            resetActions.add(ProjectResetter.ResetAction.RESET_LAYERS);\n        }\n        if (cache.isChecked()) {\n            resetActions.add(ProjectResetter.ResetAction.RESET_CACHE);\n        }\n\n        if (!resetActions.isEmpty()) {\n            new AsyncTask<Void, Void, List<Integer>>() {\n                @Override\n                protected void onPreExecute() {\n                    DialogFragmentUtils.showIfNotShowing(ResetProgressDialog.class, ((ProjectPreferencesActivity) context).getSupportFragmentManager());\n                }\n\n                @Override\n                protected List<Integer> doInBackground(Void... voids) {\n                    return projectResetter.reset(resetActions);\n                }\n\n                @Override\n                protected void onPostExecute(List<Integer> failedResetActions) {\n                    DialogFragmentUtils.dismissDialog(ResetProgressDialog.class, ((ProjectPreferencesActivity) context).getSupportFragmentManager());\n                    handleResult(resetActions, failedResetActions);\n                }\n            }.execute();\n        }\n    }\n\n    private void handleResult(final List<Integer> resetActions, List<Integer> failedResetActions) {\n        final StringBuilder resultMessage = new StringBuilder();\n        for (int action : resetActions) {\n            switch (action) {\n                case RESET_PREFERENCES:\n                    if (failedResetActions.contains(action)) {\n                        resultMessage.append(String.format(context.getString(org.odk.collect.strings.R.string.reset_settings_result),\n                                context.getString(org.odk.collect.strings.R.string.error_occured)));\n                    } else {\n                        resultMessage.append(String.format(context.getString(org.odk.collect.strings.R.string.reset_settings_result),\n                                context.getString(org.odk.collect.strings.R.string.success)));\n                    }\n                    break;\n                case ProjectResetter.ResetAction.RESET_INSTANCES:\n                    if (failedResetActions.contains(action)) {\n                        resultMessage.append(String.format(context.getString(org.odk.collect.strings.R.string.reset_saved_forms_result),\n                                context.getString(org.odk.collect.strings.R.string.error_occured)));\n                    } else {\n                        resultMessage.append(String.format(context.getString(org.odk.collect.strings.R.string.reset_saved_forms_result),\n                                context.getString(org.odk.collect.strings.R.string.success)));\n                    }\n                    break;\n                case ProjectResetter.ResetAction.RESET_FORMS:\n                    if (failedResetActions.contains(action)) {\n                        resultMessage.append(String.format(context.getString(org.odk.collect.strings.R.string.reset_blank_forms_result),\n                                context.getString(org.odk.collect.strings.R.string.error_occured)));\n                    } else {\n                        resultMessage.append(String.format(context.getString(org.odk.collect.strings.R.string.reset_blank_forms_result),\n                                context.getString(org.odk.collect.strings.R.string.success)));\n                    }\n                    break;\n                case ProjectResetter.ResetAction.RESET_CACHE:\n                    if (failedResetActions.contains(action)) {\n                        resultMessage.append(String.format(context.getString(org.odk.collect.strings.R.string.reset_cache_result),\n                                context.getString(org.odk.collect.strings.R.string.error_occured)));\n                    } else {\n                        resultMessage.append(String.format(context.getString(org.odk.collect.strings.R.string.reset_cache_result),\n                                context.getString(org.odk.collect.strings.R.string.success)));\n                    }\n                    break;\n                case ProjectResetter.ResetAction.RESET_LAYERS:\n                    if (failedResetActions.contains(action)) {\n                        resultMessage.append(String.format(context.getString(org.odk.collect.strings.R.string.reset_layers_result),\n                                context.getString(org.odk.collect.strings.R.string.error_occured)));\n                    } else {\n                        resultMessage.append(String.format(context.getString(org.odk.collect.strings.R.string.reset_layers_result),\n                                context.getString(org.odk.collect.strings.R.string.success)));\n                    }\n                    break;\n            }\n            if (resetActions.indexOf(action) < resetActions.size() - 1) {\n                resultMessage.append(\"\\n\\n\");\n            }\n        }\n        if (!((ProjectPreferencesActivity) context).isInstanceStateSaved()) {\n            ((ProjectPreferencesActivity) context).runOnUiThread(() -> {\n                if (resetActions.contains(RESET_PREFERENCES)) {\n                    ((ProjectPreferencesActivity) context).recreate();\n                }\n                ResetSettingsResultDialog resetSettingsResultDialog = ResetSettingsResultDialog.newInstance(String.valueOf(resultMessage));\n                try {\n                    resetSettingsResultDialog.show(((ProjectPreferencesActivity) context).getSupportFragmentManager(), RESET_SETTINGS_RESULT_DIALOG_TAG);\n                } catch (ClassCastException e) {\n                    Timber.i(e);\n                }\n            });\n        }\n        context = null;\n    }\n\n    @Override\n    public void onCheckedChanged(CompoundButton compoundButton, boolean b) {\n        adjustResetButtonAccessibility();\n    }\n\n    public void adjustResetButtonAccessibility() {\n        if (preferences.isChecked() || instances.isChecked() || forms.isChecked()\n                || layers.isChecked() || cache.isChecked()) {\n            ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);\n        } else {\n            ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/dialogs/ResetProgressDialog.kt",
    "content": "package org.odk.collect.android.preferences.dialogs\n\nimport android.content.Context\nimport org.odk.collect.android.R\nimport org.odk.collect.material.MaterialProgressDialogFragment\nimport org.odk.collect.strings.localization.getLocalizedString\n\nclass ResetProgressDialog : MaterialProgressDialogFragment() {\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n\n        setTitle(context.getLocalizedString(org.odk.collect.strings.R.string.please_wait))\n        setMessage(context.getLocalizedString(org.odk.collect.strings.R.string.reset_in_progress))\n        isCancelable = false\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/dialogs/ServerAuthDialogFragment.java",
    "content": "package org.odk.collect.android.preferences.dialogs;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.odk.collect.android.databinding.ServerAuthDialogBinding;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.shared.settings.Settings;\n\nimport javax.inject.Inject;\n\npublic class ServerAuthDialogFragment extends DialogFragment {\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    private View dialogView;\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        DaggerUtils.getComponent(context).inject(this);\n    }\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        ServerAuthDialogBinding binding = ServerAuthDialogBinding.inflate(requireActivity().getLayoutInflater());\n        dialogView = binding.getRoot();\n\n        Settings generalSettings = settingsProvider.getUnprotectedSettings();\n        binding.usernameEdit.setText(generalSettings.getString(ProjectKeys.KEY_USERNAME));\n        binding.passwordEdit.setText(generalSettings.getString(ProjectKeys.KEY_PASSWORD));\n\n        return new MaterialAlertDialogBuilder(requireContext())\n                .setTitle(org.odk.collect.strings.R.string.server_requires_auth)\n                .setMessage(requireContext().getString(org.odk.collect.strings.R.string.server_auth_credentials, generalSettings.getString(ProjectKeys.KEY_SERVER_URL)))\n                .setView(dialogView)\n                .setPositiveButton(org.odk.collect.strings.R.string.ok, (dialogInterface, i) -> {\n                    generalSettings.save(ProjectKeys.KEY_USERNAME, binding.usernameEdit.getText().toString());\n                    generalSettings.save(ProjectKeys.KEY_PASSWORD, binding.passwordEdit.getText().toString());\n                })\n                .create();\n    }\n\n    public View getDialogView() {\n        return dialogView;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/filters/ControlCharacterFilter.java",
    "content": "package org.odk.collect.android.preferences.filters;\n\nimport android.text.InputFilter;\nimport android.text.Spanned;\n\n/**\n * Rejects edits that contain control characters, including linefeed and carriage return.\n */\npublic class ControlCharacterFilter implements InputFilter {\n    public CharSequence filter(CharSequence source, int start, int end,\n                               Spanned dest, int dstart, int dend) {\n        for (int i = start; i < end; i++) {\n            if (Character.getType(source.charAt(i)) == Character.CONTROL) {\n                return \"\";\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/AccessControlPreferencesFragment.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.os.Bundle\nimport androidx.preference.Preference\nimport org.odk.collect.android.R\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\nclass AccessControlPreferencesFragment :\n    BaseAdminPreferencesFragment(),\n    Preference.OnPreferenceClickListener {\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        super.onCreatePreferences(savedInstanceState, rootKey)\n        setPreferencesFromResource(R.xml.access_control_preferences, rootKey)\n\n        findPreference<Preference>(MAIN_MENU_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(USER_SETTINGS_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(FORM_ENTRY_KEY)!!.onPreferenceClickListener = this\n    }\n\n    override fun onPreferenceClick(preference: Preference): Boolean {\n        if (MultiClickGuard.allowClick(javaClass.name)) {\n            when (preference.key) {\n                MAIN_MENU_KEY -> displayPreferences(MainMenuAccessPreferencesFragment())\n                USER_SETTINGS_KEY -> displayPreferences(UserSettingsAccessPreferencesFragment())\n                FORM_ENTRY_KEY -> displayPreferences(FormEntryAccessPreferencesFragment())\n            }\n            return true\n        }\n        return false\n    }\n\n    companion object {\n        private const val MAIN_MENU_KEY = \"main_menu\"\n        private const val USER_SETTINGS_KEY = \"user_settings\"\n        private const val FORM_ENTRY_KEY = \"form_entry\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/BaseAdminPreferencesFragment.java",
    "content": "package org.odk.collect.android.preferences.screens;\n\nimport android.content.Context;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.preferences.source.SettingsStore;\nimport org.odk.collect.shared.settings.Settings;\n\npublic abstract class BaseAdminPreferencesFragment extends BasePreferencesFragment {\n\n    private String projectId;\n    private Settings adminSettings;\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        projectId = projectsDataService.requireCurrentProject().getUuid();\n        adminSettings = settingsProvider.getProtectedSettings(projectId);\n    }\n\n    @Override\n    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {\n        getPreferenceManager().setPreferenceDataStore(new SettingsStore(adminSettings));\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        adminSettings.registerOnSettingChangeListener(this);\n    }\n\n    @Override\n    public void onPause() {\n        super.onPause();\n        adminSettings.unregisterOnSettingChangeListener(this);\n    }\n\n    @Override\n    public void onSettingChanged(@NotNull String key) {\n        settingsChangeHandler.onSettingChanged(projectId, adminSettings.getAll().get(key), key);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/BasePreferencesFragment.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.View\nimport androidx.fragment.app.Fragment\nimport androidx.preference.DialogPreference\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceFragmentCompat\nimport org.odk.collect.android.R\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.androidshared.utils.AppBarUtils\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.importing.SettingsChangeHandler\nimport org.odk.collect.shared.settings.Settings.OnSettingChangeListener\nimport javax.inject.Inject\n\nabstract class BasePreferencesFragment : PreferenceFragmentCompat(), OnSettingChangeListener {\n    @Inject\n    lateinit var settingsChangeHandler: SettingsChangeHandler\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n    }\n\n    override fun onDisplayPreferenceDialog(preference: Preference) {\n        super.onDisplayPreferenceDialog(preference)\n\n        // If we don't do this there is extra padding on \"Cancel\" and \"OK\" on\n        // the preference dialogs. This appears to have something to with the `updateLocale`\n        // calls in `CollectAbstractActivity` and weirdly only happens for English.\n        val dialogPreference = preference as DialogPreference\n        dialogPreference.setNegativeButtonText(org.odk.collect.strings.R.string.cancel)\n        dialogPreference.setPositiveButtonText(org.odk.collect.strings.R.string.ok)\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        requireActivity().let {\n            AppBarUtils.setupAppBarLayout(it, preferenceScreen.title ?: \"\")\n        }\n\n        super.onViewCreated(view, savedInstanceState)\n    }\n\n    protected fun displayPreferences(fragment: Fragment?) {\n        if (fragment != null) {\n            requireActivity().supportFragmentManager\n                .beginTransaction()\n                .replace(R.id.preferences_fragment_container, fragment)\n                .addToBackStack(null)\n                .commit()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/BaseProjectPreferencesFragment.java",
    "content": "package org.odk.collect.android.preferences.screens;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.preferences.PreferenceVisibilityHandler;\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel;\nimport org.odk.collect.android.preferences.source.SettingsStore;\nimport org.odk.collect.android.utilities.AdminPasswordProvider;\nimport org.odk.collect.shared.settings.Settings;\n\nimport javax.inject.Inject;\n\npublic abstract class BaseProjectPreferencesFragment extends BasePreferencesFragment {\n\n    @Inject\n    AdminPasswordProvider adminPasswordProvider;\n\n    @Inject\n    ProjectPreferencesViewModel.Factory factory;\n\n    @Inject\n    PreferenceVisibilityHandler preferenceVisibilityHandler;\n\n    protected ProjectPreferencesViewModel projectPreferencesViewModel;\n    private Settings settings;\n    private String projectId;\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        DaggerUtils.getComponent(context).inject(this);\n        projectPreferencesViewModel = new ViewModelProvider(requireActivity(), factory).get(ProjectPreferencesViewModel.class);\n\n        projectId = projectsDataService.requireCurrentProject().getUuid();\n        settings = settingsProvider.getUnprotectedSettings(projectId);\n    }\n\n    @Override\n    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {\n        getPreferenceManager().setPreferenceDataStore(new SettingsStore(settings));\n    }\n\n    @Override\n    public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) {\n        preferenceVisibilityHandler.updatePreferencesVisibility(getPreferenceScreen(), projectPreferencesViewModel.getState().getValue().getValue());\n        super.onViewCreated(view, savedInstanceState);\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        settings.registerOnSettingChangeListener(this);\n    }\n\n    @Override\n    public void onPause() {\n        super.onPause();\n        settings.unregisterOnSettingChangeListener(this);\n    }\n\n    @Override\n    public void onSettingChanged(@NotNull String key) {\n        settingsChangeHandler.onSettingChanged(projectId, settings.getAll().get(key), key);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/DevToolsPreferencesFragment.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.os.Bundle\nimport androidx.preference.Preference\nimport org.odk.collect.android.R\n\nclass DevToolsPreferencesFragment : BaseProjectPreferencesFragment() {\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        super.onCreatePreferences(savedInstanceState, rootKey)\n        setPreferencesFromResource(R.xml.dev_tools_preferences, rootKey)\n\n        findPreference<Preference>(\"crash_app\")?.setOnPreferenceClickListener {\n            throw RuntimeException(\"Simulated crash\")\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/ExperimentalPreferencesFragment.java",
    "content": "package org.odk.collect.android.preferences.screens;\n\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.FragmentActivity;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.R;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.entities.browser.EntityBrowserActivity;\n\npublic class ExperimentalPreferencesFragment extends BaseProjectPreferencesFragment {\n\n    @Override\n    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {\n        super.onCreatePreferences(savedInstanceState, rootKey);\n        setPreferencesFromResource(R.xml.experimental_preferences, rootKey);\n\n        findPreference(\"entities\").setOnPreferenceClickListener(preference -> {\n            FragmentActivity activity = requireActivity();\n            activity.startActivity(new Intent(activity, EntityBrowserActivity.class));\n            return true;\n        });\n\n        findPreference(\"dev_tools\").setOnPreferenceClickListener(preference -> {\n            displayPreferences(new DevToolsPreferencesFragment());\n            return true;\n        });\n    }\n\n    @Override\n    public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n\n        if (getPreferenceScreen().getPreferenceCount() == 0) {\n            ToastUtils.showLongToast(\"No experimental settings at the moment!\");\n            getParentFragmentManager().popBackStack();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragment.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.os.Bundle\nimport androidx.preference.CheckBoxPreference\nimport androidx.preference.Preference\nimport org.odk.collect.android.R\nimport org.odk.collect.android.fragments.dialogs.MovingBackwardsDialog\nimport org.odk.collect.android.fragments.dialogs.SimpleDialog\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\n\nclass FormEntryAccessPreferencesFragment : BaseAdminPreferencesFragment() {\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        super.onCreatePreferences(savedInstanceState, rootKey)\n        addPreferencesFromResource(R.xml.form_entry_access_preferences)\n\n        findPreference(ProtectedProjectKeys.KEY_MOVING_BACKWARDS).onPreferenceChangeListener =\n            Preference.OnPreferenceChangeListener { preference: Preference, _: Any? ->\n                if ((preference as CheckBoxPreference).isChecked) {\n                    MovingBackwardsDialog().show(\n                        requireActivity().supportFragmentManager,\n                        MovingBackwardsDialog.MOVING_BACKWARDS_DIALOG_TAG\n                    )\n                } else {\n                    SimpleDialog.newInstance(\n                        requireActivity().getString(org.odk.collect.strings.R.string.moving_backwards_enabled_title),\n                        0,\n                        requireActivity().getString(org.odk.collect.strings.R.string.moving_backwards_enabled_message),\n                        requireActivity().getString(org.odk.collect.strings.R.string.ok),\n                        false\n                    ).show(\n                        requireActivity().supportFragmentManager,\n                        SimpleDialog.COLLECT_DIALOG_TAG\n                    )\n                    onMovingBackwardsEnabled()\n                }\n                true\n            }\n\n        findPreference(ProtectedProjectKeys.KEY_JUMP_TO).isEnabled =\n            settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM)\n        findPreference(ProtectedProjectKeys.KEY_SAVE_MID).isEnabled =\n            settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM)\n\n        findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled =\n            settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM) && findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isChecked\n        findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).onPreferenceChangeListener =\n            Preference.OnPreferenceChangeListener { _: Preference, newValue: Any? ->\n                findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = newValue as Boolean\n                true\n            }\n\n        findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isChecked\n        findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).onPreferenceChangeListener =\n            Preference.OnPreferenceChangeListener { _: Preference, newValue: Any? ->\n                findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = newValue as Boolean\n                true\n            }\n    }\n\n    fun preventOtherWaysOfEditingForm() {\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, false)\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, false)\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false)\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true)\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_JUMP_TO, false)\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR, ProjectKeys.CONSTRAINT_BEHAVIOR_ON_SWIPE)\n\n        findPreference(ProtectedProjectKeys.KEY_JUMP_TO).isEnabled = false\n        findPreference(ProtectedProjectKeys.KEY_SAVE_MID).isEnabled = false\n        findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = false\n        findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = false\n        findPreference(ProtectedProjectKeys.KEY_JUMP_TO).isChecked = false\n        findPreference(ProtectedProjectKeys.KEY_SAVE_MID).isChecked = false\n        findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isChecked = false\n        findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isChecked = true\n    }\n\n    private fun onMovingBackwardsEnabled() {\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, true)\n        findPreference(ProtectedProjectKeys.KEY_JUMP_TO).isEnabled = true\n        findPreference(ProtectedProjectKeys.KEY_SAVE_MID).isEnabled = true\n        findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = true\n        findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = true\n    }\n\n    private fun findPreference(key: String): CheckBoxPreference {\n        return findPreference<Preference>(key) as CheckBoxPreference\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormManagementPreferencesFragment.java",
    "content": "/*\n * Copyright (C) 2017 Shobhit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.preferences.screens;\n\nimport static org.odk.collect.android.preferences.utilities.PreferencesUtils.displayDisabled;\nimport static org.odk.collect.settings.enums.StringIdEnumUtils.getFormUpdateMode;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_AUTOMATIC_UPDATE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_AUTOSEND;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_CONSTRAINT_BEHAVIOR;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_FORM_UPDATE_MODE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_GUIDANCE_HINT;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_IMAGE_SIZE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK;\nimport static org.odk.collect.settings.keys.ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM;\n\nimport android.content.Context;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.preference.CheckBoxPreference;\nimport androidx.preference.ListPreference;\nimport androidx.preference.Preference;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.backgroundwork.FormUpdateScheduler;\nimport org.odk.collect.android.backgroundwork.InstanceSubmitScheduler;\nimport org.odk.collect.settings.enums.AutoSend;\nimport org.odk.collect.settings.enums.StringIdEnumUtils;\nimport org.odk.collect.shared.settings.Settings;\n\nimport javax.inject.Inject;\n\npublic class FormManagementPreferencesFragment extends BaseProjectPreferencesFragment {\n\n    @Inject\n    FormUpdateScheduler formUpdateScheduler;\n\n    @Inject\n    InstanceSubmitScheduler instanceSubmitScheduler;\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        Collect.getInstance().getComponent().inject(this);\n    }\n\n    @Override\n    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {\n        super.onCreatePreferences(savedInstanceState, rootKey);\n        setPreferencesFromResource(R.xml.form_management_preferences, rootKey);\n\n        initListPref(KEY_PERIODIC_FORM_UPDATES_CHECK);\n        initPref(KEY_AUTOMATIC_UPDATE);\n        initListPref(KEY_CONSTRAINT_BEHAVIOR);\n        initListPref(KEY_AUTOSEND);\n        initListPref(KEY_IMAGE_SIZE);\n        initGuidancePrefs();\n\n        updateDisabledPrefs();\n    }\n\n    @Override\n    public void onSettingChanged(@NotNull String key) {\n        super.onSettingChanged(key);\n\n        if (key.equals(KEY_FORM_UPDATE_MODE) || key.equals(KEY_PERIODIC_FORM_UPDATES_CHECK)) {\n            updateDisabledPrefs();\n        }\n\n        if (key.equals(KEY_AUTOSEND) && !StringIdEnumUtils.getAutoSend(settingsProvider.getUnprotectedSettings(), requireContext()).equals(AutoSend.OFF)) {\n            instanceSubmitScheduler.scheduleAutoSend(projectsDataService.requireCurrentProject().getUuid());\n        }\n    }\n\n    private void updateDisabledPrefs() {\n        Settings generalSettings = settingsProvider.getUnprotectedSettings();\n\n        // Might be null if disabled in Protected settings\n        @Nullable Preference updateFrequency = findPreference(KEY_PERIODIC_FORM_UPDATES_CHECK);\n        @Nullable CheckBoxPreference automaticDownload = findPreference(KEY_AUTOMATIC_UPDATE);\n\n        switch (getFormUpdateMode(generalSettings, requireContext())) {\n            case MANUAL:\n                if (automaticDownload != null) {\n                    displayDisabled(automaticDownload, false);\n                }\n                if (updateFrequency != null) {\n                    updateFrequency.setEnabled(false);\n                }\n                break;\n            case PREVIOUSLY_DOWNLOADED_ONLY:\n                if (automaticDownload != null) {\n                    automaticDownload.setEnabled(true);\n                    automaticDownload.setChecked(generalSettings.getBoolean(KEY_AUTOMATIC_UPDATE));\n                }\n                if (updateFrequency != null) {\n                    updateFrequency.setEnabled(true);\n                }\n                break;\n            case MATCH_EXACTLY:\n                if (automaticDownload != null) {\n                    displayDisabled(automaticDownload, true);\n                }\n                if (updateFrequency != null) {\n                    updateFrequency.setEnabled(true);\n                }\n                break;\n        }\n    }\n\n    private void initListPref(String key) {\n        final ListPreference pref = findPreference(key);\n\n        if (pref != null) {\n            pref.setSummary(pref.getEntry());\n            pref.setOnPreferenceChangeListener((preference, newValue) -> {\n                int index = ((ListPreference) preference).findIndexOfValue(newValue.toString());\n                CharSequence entry = ((ListPreference) preference).getEntries()[index];\n                preference.setSummary(entry);\n                return true;\n            });\n            if (key.equals(KEY_CONSTRAINT_BEHAVIOR)) {\n                pref.setEnabled(settingsProvider.getProtectedSettings().getBoolean(ALLOW_OTHER_WAYS_OF_EDITING_FORM));\n            }\n        }\n    }\n\n    private void initPref(String key) {\n        final Preference pref = findPreference(key);\n\n        if (pref != null) {\n            if (key.equals(KEY_AUTOMATIC_UPDATE)) {\n                String formUpdateCheckPeriod = settingsProvider.getUnprotectedSettings().getString(KEY_PERIODIC_FORM_UPDATES_CHECK);\n\n                // Only enable automatic form updates if periodic updates are set\n                pref.setEnabled(!formUpdateCheckPeriod.equals(getString(org.odk.collect.strings.R.string.never_value)));\n            }\n        }\n    }\n\n    private void initGuidancePrefs() {\n        final ListPreference guidance = findPreference(KEY_GUIDANCE_HINT);\n\n        if (guidance == null) {\n            return;\n        }\n\n        guidance.setSummary(guidance.getEntry());\n        guidance.setOnPreferenceChangeListener((preference, newValue) -> {\n            int index = ((ListPreference) preference).findIndexOfValue(newValue.toString());\n            String entry = (String) ((ListPreference) preference).getEntries()[index];\n            preference.setSummary(entry);\n            return true;\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormMetadataPreferencesFragment.java",
    "content": "package org.odk.collect.android.preferences.screens;\n\nimport static org.odk.collect.metadata.PropertyManager.PROPMGR_DEVICE_ID;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_METADATA_EMAIL;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_METADATA_PHONENUMBER;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.text.Editable;\nimport android.text.Selection;\nimport android.text.TextUtils;\nimport android.view.inputmethod.EditorInfo;\n\nimport androidx.annotation.Nullable;\nimport androidx.preference.EditTextPreference;\nimport androidx.preference.Preference;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.androidshared.utils.Validator;\nimport org.odk.collect.metadata.PropertyManager;\n\nimport javax.inject.Inject;\n\npublic class FormMetadataPreferencesFragment extends BaseProjectPreferencesFragment {\n    @Inject\n    PropertyManager propertyManager;\n\n    private Preference emailPreference;\n    private EditTextPreference phonePreference;\n    private Preference deviceIDPreference;\n\n    @Override\n    public void onAttach(@NotNull Context context) {\n        super.onAttach(context);\n        DaggerUtils.getComponent(context).inject(this);\n    }\n\n    @Override\n    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {\n        super.onCreatePreferences(savedInstanceState, rootKey);\n        setPreferencesFromResource(R.xml.form_metadata_preferences, rootKey);\n\n        emailPreference = findPreference(KEY_METADATA_EMAIL);\n        phonePreference = findPreference(KEY_METADATA_PHONENUMBER);\n        deviceIDPreference = findPreference(PROPMGR_DEVICE_ID);\n    }\n\n    @Override\n    public void onActivityCreated(@Nullable Bundle savedInstanceState) {\n        super.onActivityCreated(savedInstanceState);\n\n        setupPrefs();\n    }\n\n    private void setupPrefs() {\n        emailPreference.setOnPreferenceChangeListener((preference, newValue) -> {\n            String newValueString = newValue.toString();\n            if (!newValueString.isEmpty() && !Validator.isEmailAddressValid(newValueString)) {\n                ToastUtils.showLongToast(org.odk.collect.strings.R.string.invalid_email_address);\n                return false;\n            }\n\n            return true;\n        });\n\n        phonePreference.setOnBindEditTextListener(editText -> {\n            editText.setInputType(EditorInfo.TYPE_CLASS_PHONE);\n            Editable text = editText.getText();\n            Selection.setSelection(text, text.length());\n        });\n\n        deviceIDPreference.setSummaryProvider(new PropertyManagerPropertySummaryProvider(propertyManager, PROPMGR_DEVICE_ID));\n    }\n\n    private class PropertyManagerPropertySummaryProvider implements Preference.SummaryProvider<EditTextPreference> {\n\n        private final PropertyManager propertyManager;\n        private final String propertyKey;\n\n        PropertyManagerPropertySummaryProvider(PropertyManager propertyManager, String propertyName) {\n            this.propertyManager = propertyManager;\n            this.propertyKey = propertyName;\n        }\n\n        @Override\n        public CharSequence provideSummary(EditTextPreference preference) {\n            String value = propertyManager.reload().getSingularProperty(propertyKey);\n            if (!TextUtils.isEmpty(value)) {\n                return value;\n            } else {\n                return getString(org.odk.collect.strings.R.string.preference_not_available);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/IdentityPreferencesFragment.kt",
    "content": "/*\n * Copyright (C) 2017 Shobhit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.preferences.screens\n\nimport android.content.Context\nimport android.os.Bundle\nimport androidx.preference.CheckBoxPreference\nimport androidx.preference.Preference\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.R\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.preferences.utilities.PreferencesUtils\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\nimport org.odk.collect.settings.keys.ProjectKeys\nimport javax.inject.Inject\n\nclass IdentityPreferencesFragment : BaseProjectPreferencesFragment() {\n    @Inject\n    lateinit var analytics: Analytics\n\n    @Inject\n    lateinit var versionInformation: VersionInformation\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n    }\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        super.onCreatePreferences(savedInstanceState, rootKey)\n        setPreferencesFromResource(R.xml.identity_preferences, rootKey)\n        findPreference<Preference>(\"form_metadata\")!!.onPreferenceClickListener =\n            Preference.OnPreferenceClickListener listener@{\n                if (MultiClickGuard.allowClick(javaClass.name)) {\n                    displayPreferences(FormMetadataPreferencesFragment())\n                    return@listener true\n                }\n                false\n            }\n        initAnalyticsPref()\n    }\n\n    private fun initAnalyticsPref() {\n        val analyticsPreference = findPreference<Preference>(ProjectKeys.KEY_ANALYTICS) as CheckBoxPreference?\n        if (analyticsPreference != null) {\n            if (versionInformation.isBeta) {\n                PreferencesUtils.displayDisabled(analyticsPreference, true)\n                analyticsPreference.summary =\n                    analyticsPreference.summary.toString() + \" Usage data collection cannot be disabled in beta versions of Collect.\"\n            } else {\n                analyticsPreference.onPreferenceClickListener =\n                    Preference.OnPreferenceClickListener {\n                        analytics.setAnalyticsCollectionEnabled(analyticsPreference.isChecked)\n                        true\n                    }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/MainMenuAccessPreferencesFragment.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.os.Bundle\nimport androidx.preference.Preference\nimport org.odk.collect.android.R\nimport org.odk.collect.android.preferences.utilities.PreferencesUtils\nimport org.odk.collect.settings.enums.FormUpdateMode\nimport org.odk.collect.settings.enums.StringIdEnumUtils.getFormUpdateMode\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\n\nclass MainMenuAccessPreferencesFragment : BaseAdminPreferencesFragment() {\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        super.onCreatePreferences(savedInstanceState, rootKey)\n        setPreferencesFromResource(R.xml.main_menu_access_preferences, rootKey)\n\n        findPreference<Preference>(ProtectedProjectKeys.KEY_EDIT_SAVED)!!.isEnabled =\n            settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM)\n\n        val formUpdateMode = settingsProvider.getUnprotectedSettings().getFormUpdateMode(requireContext())\n        if (formUpdateMode == FormUpdateMode.MATCH_EXACTLY) {\n            PreferencesUtils.displayDisabled(findPreference(ProtectedProjectKeys.KEY_GET_BLANK), false)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt",
    "content": "/*\n * Copyright (C) 2017 Shobhit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.preferences.screens\n\nimport android.content.Context\nimport android.os.Bundle\nimport androidx.preference.ListPreference\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceCategory\nimport org.odk.collect.android.R\nimport org.odk.collect.android.geo.MapConfiguratorProvider\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.androidshared.ui.PrefUtils\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard.allowClick\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.maps.MapConfigurator\nimport org.odk.collect.maps.layers.OfflineMapLayersPickerBottomSheetDialogFragment\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProjectKeys.CATEGORY_BASEMAP\nimport org.odk.collect.settings.keys.ProjectKeys.KEY_BASEMAP_SOURCE\nimport org.odk.collect.strings.localization.getLocalizedString\nimport org.odk.collect.webpage.WebPageService\nimport javax.inject.Inject\n\nclass MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnPreferenceClickListener {\n\n    private lateinit var basemapSourcePref: ListPreference\n\n    @Inject\n    lateinit var referenceLayerRepository: ReferenceLayerRepository\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    @Inject\n    lateinit var webPageService: WebPageService\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        childFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(OfflineMapLayersPickerBottomSheetDialogFragment::class) {\n                OfflineMapLayersPickerBottomSheetDialogFragment(requireActivity().activityResultRegistry, referenceLayerRepository, scheduler, settingsProvider, webPageService)\n            }\n            .build()\n\n        super.onCreate(savedInstanceState)\n    }\n\n    override fun onSettingChanged(key: String) {\n        super.onSettingChanged(key)\n        if (key == ProjectKeys.KEY_REFERENCE_LAYER) {\n            findPreference<Preference>(ProjectKeys.KEY_REFERENCE_LAYER)!!.summary = getLayerName()\n        } else if (key == KEY_BASEMAP_SOURCE) {\n            val cftor = MapConfiguratorProvider.getConfigurator(settingsProvider.getUnprotectedSettings().getString(key))\n            if (!cftor.isAvailable(requireContext())) {\n                cftor.showUnavailableMessage(requireContext())\n            } else {\n                onBasemapSourceChanged(cftor)\n            }\n        }\n    }\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        super.onCreatePreferences(savedInstanceState, rootKey)\n        setPreferencesFromResource(R.xml.maps_preferences, rootKey)\n        initBasemapSourcePref()\n        initLayersPref()\n    }\n\n    override fun onPreferenceClick(preference: Preference): Boolean {\n        if (allowClick(javaClass.name)) {\n            when (preference.key) {\n                ProjectKeys.KEY_REFERENCE_LAYER -> {\n                    DialogFragmentUtils.showIfNotShowing(\n                        OfflineMapLayersPickerBottomSheetDialogFragment::class.java,\n                        childFragmentManager\n                    )\n                }\n            }\n            return true\n        }\n        return false\n    }\n\n    /**\n     * Creates the Basemap Source preference widget (but doesn't add it to\n     * the screen; onBasemapSourceChanged will do that part).\n     */\n    private fun initBasemapSourcePref() {\n        basemapSourcePref = PrefUtils.createListPref(\n            requireContext(),\n            KEY_BASEMAP_SOURCE,\n            getString(org.odk.collect.strings.R.string.basemap_source),\n            MapConfiguratorProvider.getLabelIds(),\n            MapConfiguratorProvider.getIds(),\n            settingsProvider.getUnprotectedSettings()\n        )\n\n        basemapSourcePref.setIconSpaceReserved(false)\n        onBasemapSourceChanged(MapConfiguratorProvider.getConfigurator())\n    }\n\n    private fun initLayersPref() {\n        findPreference<Preference>(ProjectKeys.KEY_REFERENCE_LAYER)?.apply {\n            onPreferenceClickListener = this@MapsPreferencesFragment\n            summary = getLayerName()\n        }\n    }\n\n    private fun getLayerName(): String {\n        val layerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER)\n        return if (layerId == null) {\n            requireContext().getLocalizedString(org.odk.collect.strings.R.string.none)\n        } else {\n            referenceLayerRepository.get(layerId)!!.name\n        }\n    }\n\n    /** Updates the rest of the preference UI when the Basemap Source is changed.  */\n    private fun onBasemapSourceChanged(cftor: MapConfigurator) {\n        // Set up the preferences in the \"Basemap\" section.\n        val baseCategory = findPreference<PreferenceCategory>(CATEGORY_BASEMAP)\n        baseCategory!!.removeAll()\n        baseCategory.addPreference(basemapSourcePref)\n        for (pref in cftor.createPrefs(requireContext(), settingsProvider.getUnprotectedSettings())) {\n            pref.isIconSpaceReserved = false\n            baseCategory.addPreference(pref)\n        }\n\n        // Clear the reference layer if it does not exist or it isn't supported by the new basemap.\n        val layerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER)\n        if (layerId != null) {\n            val layer = referenceLayerRepository.get(layerId)\n            if (layer == null) {\n                settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/ProjectDisplayPreferencesFragment.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.content.Context\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.text.Spannable\nimport android.text.SpannableString\nimport android.text.style.ForegroundColorSpan\nimport android.widget.EditText\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.preference.EditTextPreference\nimport androidx.preference.Preference\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.R\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.androidshared.ColorPickerDialog\nimport org.odk.collect.androidshared.ColorPickerViewModel\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.OneSignTextWatcher\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.shared.PathUtils\nimport timber.log.Timber\nimport java.io.File\nimport javax.inject.Inject\n\nclass ProjectDisplayPreferencesFragment :\n    BaseAdminPreferencesFragment(), Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {\n\n    @Inject\n    lateinit var projectsRepository: ProjectsRepository\n\n    @Inject\n    lateinit var storagePathProvider: StoragePathProvider\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n        val colorPickerViewModel = ViewModelProvider(requireActivity()).get(\n            ColorPickerViewModel::class.java\n        )\n        colorPickerViewModel.pickedColor.observe(\n            this,\n            { color: String ->\n                Analytics.log(AnalyticsEvents.CHANGE_PROJECT_COLOR)\n\n                val (uuid, name, icon) = projectsDataService.requireCurrentProject()\n                projectsRepository.save(Project.Saved(uuid, name, icon, color))\n                findPreference<Preference>(PROJECT_COLOR_KEY)!!.summaryProvider =\n                    ProjectDetailsSummaryProvider(\n                        PROJECT_COLOR_KEY,\n                        projectsDataService\n                    )\n            }\n        )\n    }\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        super.onCreatePreferences(savedInstanceState, rootKey)\n        setPreferencesFromResource(R.xml.project_display_preferences, rootKey)\n        findPreference<Preference>(PROJECT_COLOR_KEY)!!.onPreferenceClickListener = this\n\n        findPreference<Preference>(PROJECT_NAME_KEY)!!.summaryProvider =\n            ProjectDetailsSummaryProvider(\n                PROJECT_NAME_KEY,\n                projectsDataService\n            )\n        findPreference<Preference>(PROJECT_ICON_KEY)!!.summaryProvider =\n            ProjectDetailsSummaryProvider(\n                PROJECT_ICON_KEY,\n                projectsDataService\n            )\n        findPreference<Preference>(PROJECT_COLOR_KEY)!!.summaryProvider =\n            ProjectDetailsSummaryProvider(\n                PROJECT_COLOR_KEY,\n                projectsDataService\n            )\n        findPreference<Preference>(PROJECT_NAME_KEY)!!.onPreferenceChangeListener = this\n        findPreference<Preference>(PROJECT_ICON_KEY)!!.onPreferenceChangeListener = this\n        (findPreference<Preference>(PROJECT_NAME_KEY) as EditTextPreference).text =\n            projectsDataService.requireCurrentProject().name\n        (findPreference<Preference>(PROJECT_ICON_KEY) as EditTextPreference).text =\n            projectsDataService.requireCurrentProject().icon\n        (findPreference<Preference>(PROJECT_ICON_KEY) as EditTextPreference).setOnBindEditTextListener { editText: EditText ->\n            editText.addTextChangedListener(\n                OneSignTextWatcher(editText)\n            )\n        }\n    }\n\n    private class ProjectDetailsSummaryProvider(\n        private val key: String,\n        private val projectsDataService: ProjectsDataService\n    ) : Preference.SummaryProvider<Preference> {\n        override fun provideSummary(preference: Preference): CharSequence {\n            return when (key) {\n                PROJECT_NAME_KEY -> projectsDataService.requireCurrentProject().name\n                PROJECT_ICON_KEY -> projectsDataService.requireCurrentProject().icon\n                PROJECT_COLOR_KEY -> {\n                    val summary: Spannable = SpannableString(\"■\")\n                    summary.setSpan(\n                        ForegroundColorSpan(\n                            Color.parseColor(\n                                projectsDataService.requireCurrentProject().color\n                            )\n                        ),\n                        0,\n                        summary.length,\n                        0\n                    )\n                    summary\n                }\n                else -> \"\"\n            }\n        }\n    }\n\n    override fun onPreferenceClick(preference: Preference): Boolean {\n        if (MultiClickGuard.allowClick(javaClass.name)) {\n            when (preference.key) {\n                PROJECT_COLOR_KEY -> {\n                    val (_, _, icon, color) = projectsDataService.requireCurrentProject()\n                    val bundle = Bundle()\n                    bundle.putString(ColorPickerDialog.CURRENT_COLOR, color)\n                    bundle.putString(ColorPickerDialog.CURRENT_ICON, icon)\n                    DialogFragmentUtils.showIfNotShowing(\n                        ColorPickerDialog::class.java,\n                        bundle,\n                        requireActivity().supportFragmentManager\n                    )\n                }\n            }\n            return true\n        }\n        return false\n    }\n\n    override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {\n        val (uuid, name, icon, color) = projectsDataService.requireCurrentProject()\n        when (preference.key) {\n            PROJECT_NAME_KEY -> {\n                Analytics.log(AnalyticsEvents.CHANGE_PROJECT_NAME)\n\n                val sanitizedOldProjectName = PathUtils.getPathSafeFileName(name)\n                try {\n                    File(storagePathProvider.getProjectRootDirPath() + File.separator + sanitizedOldProjectName).delete()\n                } catch (e: Exception) {\n                    Timber.e(\n                        Error(\n                            FileUtils.getFilenameError(\n                                name\n                            )\n                        )\n                    )\n                }\n\n                val sanitizedNewProjectName = PathUtils.getPathSafeFileName(newValue.toString())\n                try {\n                    File(storagePathProvider.getProjectRootDirPath() + File.separator + sanitizedNewProjectName).createNewFile()\n                } catch (e: Exception) {\n                    Timber.e(\n                        Error(\n                            FileUtils.getFilenameError(\n                                newValue as String\n                            )\n                        )\n                    )\n                }\n\n                projectsRepository.save(\n                    Project.Saved(\n                        uuid,\n                        newValue.toString(),\n                        icon,\n                        color\n                    )\n                )\n            }\n            PROJECT_ICON_KEY -> {\n                Analytics.log(AnalyticsEvents.CHANGE_PROJECT_ICON)\n\n                projectsRepository.save(\n                    Project.Saved(\n                        uuid,\n                        name,\n                        newValue.toString(),\n                        color\n                    )\n                )\n            }\n        }\n        return true\n    }\n\n    companion object {\n        const val PROJECT_NAME_KEY = \"project_name\"\n        const val PROJECT_ICON_KEY = \"project_icon\"\n        const val PROJECT_COLOR_KEY = \"project_color\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/ProjectManagementPreferencesFragment.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.preference.Preference\nimport org.odk.collect.android.R\nimport org.odk.collect.android.configure.qr.QRCodeTabsActivity\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.preferences.dialogs.DeleteProjectDialog\nimport org.odk.collect.android.preferences.dialogs.ResetDialogPreference\nimport org.odk.collect.android.preferences.dialogs.ResetDialogPreferenceFragmentCompat\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\nclass ProjectManagementPreferencesFragment :\n    BaseAdminPreferencesFragment(),\n    Preference.OnPreferenceClickListener {\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n    }\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        super.onCreatePreferences(savedInstanceState, rootKey)\n        setPreferencesFromResource(R.xml.project_management_preferences, rootKey)\n\n        findPreference<Preference>(IMPORT_SETTINGS_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(DELETE_PROJECT_KEY)!!.onPreferenceClickListener = this\n    }\n\n    override fun onDisplayPreferenceDialog(preference: Preference) {\n        if (MultiClickGuard.allowClick(javaClass.name)) {\n            var resetDialogPreference: ResetDialogPreference? = null\n            if (preference is ResetDialogPreference) {\n                resetDialogPreference = preference\n            }\n            if (resetDialogPreference != null) {\n                val dialogFragment = ResetDialogPreferenceFragmentCompat.newInstance(preference.key)\n                dialogFragment.setTargetFragment(this, 0)\n                dialogFragment.show(parentFragmentManager, null)\n            } else {\n                super.onDisplayPreferenceDialog(preference)\n            }\n        }\n    }\n\n    override fun onPreferenceClick(preference: Preference): Boolean {\n        if (MultiClickGuard.allowClick(javaClass.name)) {\n            when (preference.key) {\n                IMPORT_SETTINGS_KEY -> {\n                    val pref = Intent(activity, QRCodeTabsActivity::class.java)\n                    startActivity(pref)\n                }\n                DELETE_PROJECT_KEY -> DialogFragmentUtils.showIfNotShowing(\n                    DeleteProjectDialog::class.java,\n                    childFragmentManager\n                )\n            }\n            return true\n        }\n        return false\n    }\n\n    companion object {\n        const val IMPORT_SETTINGS_KEY = \"import_settings\"\n        const val DELETE_PROJECT_KEY = \"delete_project\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/ProjectPreferencesActivity.kt",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.preferences.screens\n\nimport android.os.Bundle\nimport org.odk.collect.android.R\nimport org.odk.collect.android.activities.ActivityUtils\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.fragments.dialogs.MovingBackwardsDialog.MovingBackwardsDialogListener\nimport org.odk.collect.android.fragments.dialogs.ResetSettingsResultDialog.ResetSettingsResultDialogListener\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.android.preferences.dialogs.DeleteProjectDialog\nimport org.odk.collect.android.projects.ProjectDeleter\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.metadata.PropertyManager\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport javax.inject.Inject\n\nclass ProjectPreferencesActivity :\n    LocalizedActivity(),\n    ResetSettingsResultDialogListener,\n    MovingBackwardsDialogListener {\n\n    private var isInstanceStateSaved = false\n\n    @Inject\n    lateinit var propertyManager: PropertyManager\n\n    @Inject\n    lateinit var projectDeleter: ProjectDeleter\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    @Inject\n    lateinit var formsDataService: FormsDataService\n\n    @Inject\n    lateinit var instancesDataService: InstancesDataService\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        DaggerUtils.getComponent(this).inject(this)\n        supportFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(ProjectPreferencesFragment::class.java) {\n                ProjectPreferencesFragment(intent.getBooleanExtra(EXTRA_IN_FORM_ENTRY, false))\n            }\n            .forClass(DeleteProjectDialog::class) {\n                DeleteProjectDialog(\n                    projectDeleter,\n                    projectsDataService,\n                    formsDataService,\n                    instancesDataService,\n                    scheduler\n                )\n            }\n            .build()\n\n        super.onCreate(savedInstanceState)\n        setView(R.layout.activity_preferences_layout, false)\n    }\n\n    override fun onPause() {\n        super.onPause()\n        propertyManager.reload()\n    }\n\n    override fun onPostResume() {\n        super.onPostResume()\n        isInstanceStateSaved = false\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        isInstanceStateSaved = true\n        super.onSaveInstanceState(outState)\n    }\n\n    override fun onDialogClosed() {\n        ActivityUtils.startActivityAndCloseAllOthers(this, MainMenuActivity::class.java)\n    }\n\n    override fun preventOtherWaysOfEditingForm() {\n        val fragment = supportFragmentManager.findFragmentById(R.id.preferences_fragment_container) as FormEntryAccessPreferencesFragment\n        fragment.preventOtherWaysOfEditingForm()\n    }\n\n    fun isInstanceStateSaved() = isInstanceStateSaved\n\n    companion object {\n        const val EXTRA_IN_FORM_ENTRY = \"in_form_entry\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/ProjectPreferencesFragment.kt",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.preferences.screens\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.Menu\nimport android.view.MenuInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.os.bundleOf\nimport androidx.preference.Preference\nimport org.odk.collect.android.R\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel\nimport org.odk.collect.android.preferences.dialogs.AdminPasswordDialogFragment\nimport org.odk.collect.android.preferences.dialogs.ChangeAdminPasswordDialog\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\nclass ProjectPreferencesFragment(private val inFormEntry: Boolean) :\n    BaseProjectPreferencesFragment(),\n    Preference.OnPreferenceClickListener {\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n\n        projectPreferencesViewModel.state.observe(\n            this,\n            { state: Consumable<ProjectPreferencesViewModel.State> ->\n                if (!state.isConsumed()) {\n                    state.consume()\n                    preferenceVisibilityHandler.updatePreferencesVisibility(preferenceScreen, state.value)\n                    requireActivity().invalidateOptionsMenu()\n                }\n            }\n        )\n    }\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        super.onCreatePreferences(savedInstanceState, rootKey)\n        setPreferencesFromResource(R.xml.project_preferences, rootKey)\n\n        findPreference<Preference>(PROTOCOL_PREFERENCE_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(PROJECT_DISPLAY_PREFERENCE_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(USER_INTERFACE_PREFERENCE_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(MAPS_PREFERENCE_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(FORM_MANAGEMENT_PREFERENCE_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(USER_AND_DEVICE_IDENTITY_PREFERENCE_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(EXPERIMENTAL_PREFERENCE_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(UNLOCK_PROTECTED_SETTINGS_PREFERENCE_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(CHANGE_ADMIN_PASSWORD_PREFERENCE_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(ACCESS_CONTROL_PREFERENCE_KEY)!!.onPreferenceClickListener = this\n        findPreference<Preference>(PROJECT_MANAGEMENT_PREFERENCE_KEY)!!.also {\n            it.onPreferenceClickListener = this\n            if (inFormEntry) {\n                it.isEnabled = false\n                it.setSummary(org.odk.collect.strings.R.string.setting_not_available_during_form_entry)\n            }\n        }\n    }\n\n    override fun onPreferenceClick(preference: Preference): Boolean {\n        if (MultiClickGuard.allowClick(javaClass.name)) {\n            when (preference.key) {\n                PROTOCOL_PREFERENCE_KEY -> displayPreferences(ServerPreferencesFragment())\n                PROJECT_DISPLAY_PREFERENCE_KEY -> displayPreferences(ProjectDisplayPreferencesFragment())\n                USER_INTERFACE_PREFERENCE_KEY -> {\n                    val fragment = UserInterfacePreferencesFragment()\n                    fragment.arguments =\n                        bundleOf(UserInterfacePreferencesFragment.ARG_IN_FORM_ENTRY to inFormEntry)\n                    displayPreferences(fragment)\n                }\n                MAPS_PREFERENCE_KEY -> displayPreferences(MapsPreferencesFragment())\n                FORM_MANAGEMENT_PREFERENCE_KEY -> displayPreferences(FormManagementPreferencesFragment())\n                USER_AND_DEVICE_IDENTITY_PREFERENCE_KEY -> displayPreferences(IdentityPreferencesFragment())\n                EXPERIMENTAL_PREFERENCE_KEY -> displayPreferences(ExperimentalPreferencesFragment())\n                UNLOCK_PROTECTED_SETTINGS_PREFERENCE_KEY -> DialogFragmentUtils.showIfNotShowing(\n                    AdminPasswordDialogFragment::class.java,\n                    requireActivity().supportFragmentManager\n                )\n                CHANGE_ADMIN_PASSWORD_PREFERENCE_KEY -> DialogFragmentUtils.showIfNotShowing(\n                    ChangeAdminPasswordDialog::class.java,\n                    requireActivity().supportFragmentManager\n                )\n                PROJECT_MANAGEMENT_PREFERENCE_KEY -> displayPreferences(ProjectManagementPreferencesFragment())\n                ACCESS_CONTROL_PREFERENCE_KEY -> displayPreferences(AccessControlPreferencesFragment())\n            }\n            return true\n        }\n        return false\n    }\n\n    override fun onPrepareOptionsMenu(menu: Menu) {\n        when {\n            projectPreferencesViewModel.isStateLocked() -> {\n                menu.findItem(R.id.menu_locked).isVisible = true\n                menu.findItem(R.id.menu_unlocked).isVisible = false\n            }\n            projectPreferencesViewModel.isStateUnlocked() -> {\n                menu.findItem(R.id.menu_locked).isVisible = false\n                menu.findItem(R.id.menu_unlocked).isVisible = true\n            }\n            else -> {\n                menu.findItem(R.id.menu_locked).isVisible = false\n                menu.findItem(R.id.menu_unlocked).isVisible = false\n            }\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        setHasOptionsMenu(true) // Needs to be here rather than `onAttach` or crash can occur when switching system theme\n        return super.onCreateView(inflater, container, savedInstanceState)\n    }\n\n    override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) {\n        menuInflater.inflate(R.menu.project_preferences_menu, menu)\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        if (item.itemId == R.id.menu_locked) {\n            DialogFragmentUtils.showIfNotShowing(\n                AdminPasswordDialogFragment::class.java,\n                requireActivity().supportFragmentManager\n            )\n            return true\n        }\n        return false\n    }\n\n    companion object {\n        private const val PROTOCOL_PREFERENCE_KEY = \"protocol\"\n        private const val PROJECT_DISPLAY_PREFERENCE_KEY = \"project_display\"\n        private const val USER_INTERFACE_PREFERENCE_KEY = \"user_interface\"\n        private const val MAPS_PREFERENCE_KEY = \"maps\"\n        private const val FORM_MANAGEMENT_PREFERENCE_KEY = \"form_management\"\n        private const val USER_AND_DEVICE_IDENTITY_PREFERENCE_KEY = \"user_and_device_identity\"\n        private const val EXPERIMENTAL_PREFERENCE_KEY = \"experimental\"\n        private const val UNLOCK_PROTECTED_SETTINGS_PREFERENCE_KEY = \"unlock_protected_settings\"\n        private const val CHANGE_ADMIN_PASSWORD_PREFERENCE_KEY = \"admin_password\"\n        private const val PROJECT_MANAGEMENT_PREFERENCE_KEY = \"project_management\"\n        private const val ACCESS_CONTROL_PREFERENCE_KEY = \"access_control\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/ServerPreferencesFragment.java",
    "content": "/*\n * Copyright 2017 Shobhit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.preferences.screens;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.text.InputFilter;\n\nimport androidx.preference.EditTextPreference;\nimport androidx.preference.Preference;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.backgroundwork.FormUpdateScheduler;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.preferences.ServerPreferencesAdder;\nimport org.odk.collect.android.preferences.filters.ControlCharacterFilter;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.androidshared.utils.Validator;\nimport org.odk.collect.settings.keys.ProjectKeys;\n\nimport javax.inject.Inject;\n\npublic class ServerPreferencesFragment extends BaseProjectPreferencesFragment {\n    private EditTextPreference passwordPreference;\n\n    @Inject\n    FormUpdateScheduler formUpdateScheduler;\n\n    @Override\n    public void onAttach(@NotNull Context context) {\n        super.onAttach(context);\n        DaggerUtils.getComponent(context).inject(this);\n    }\n\n    @Override\n    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {\n        super.onCreatePreferences(savedInstanceState, rootKey);\n        setPreferencesFromResource(R.xml.server_preferences, rootKey);\n        addServerPreferences();\n    }\n\n    public void addServerPreferences() {\n        if (!new ServerPreferencesAdder(this).add()) {\n            return;\n        }\n        EditTextPreference serverUrlPreference = findPreference(ProjectKeys.KEY_SERVER_URL);\n        EditTextPreference usernamePreference = findPreference(ProjectKeys.KEY_USERNAME);\n        passwordPreference = findPreference(ProjectKeys.KEY_PASSWORD);\n\n        serverUrlPreference.setOnPreferenceChangeListener(createChangeListener());\n        serverUrlPreference.setSummary(serverUrlPreference.getText());\n\n        usernamePreference.setOnPreferenceChangeListener(createChangeListener());\n        usernamePreference.setSummary(usernamePreference.getText());\n\n        usernamePreference.setOnBindEditTextListener(editText -> {\n            editText.setFilters(new InputFilter[]{new ControlCharacterFilter()});\n        });\n\n        passwordPreference.setOnPreferenceChangeListener(createChangeListener());\n        maskPasswordSummary(passwordPreference.getText());\n\n        passwordPreference.setOnBindEditTextListener(editText -> {\n            editText.setFilters(new InputFilter[]{new ControlCharacterFilter()});\n        });\n    }\n\n    private Preference.OnPreferenceChangeListener createChangeListener() {\n        return (preference, newValue) -> {\n            switch (preference.getKey()) {\n                case ProjectKeys.KEY_SERVER_URL:\n                    String url = newValue.toString();\n\n                    if (Validator.isUrlValid(url)) {\n                        preference.setSummary(newValue.toString());\n                    } else {\n                        ToastUtils.showShortToast(org.odk.collect.strings.R.string.url_error);\n                        return false;\n                    }\n                    break;\n\n                case ProjectKeys.KEY_USERNAME:\n                    String username = newValue.toString();\n\n                    // do not allow leading and trailing whitespace\n                    if (!username.equals(username.trim())) {\n                        ToastUtils.showShortToast(org.odk.collect.strings.R.string.username_error_whitespace);\n                        return false;\n                    }\n\n                    preference.setSummary(username);\n                    return true;\n\n                case ProjectKeys.KEY_PASSWORD:\n                    String pw = newValue.toString();\n\n                    // do not allow leading and trailing whitespace\n                    if (!pw.equals(pw.trim())) {\n                        ToastUtils.showShortToast(org.odk.collect.strings.R.string.password_error_whitespace);\n                        return false;\n                    }\n\n                    maskPasswordSummary(pw);\n                    break;\n            }\n            return true;\n        };\n    }\n\n    private void maskPasswordSummary(String password) {\n        passwordPreference.setSummary(password != null && password.length() > 0\n                ? \"********\"\n                : \"\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/UserInterfacePreferencesFragment.java",
    "content": "/*\n * Copyright (C) 2017 Shobhit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.preferences.screens;\n\nimport static org.odk.collect.android.activities.ActivityUtils.startActivityAndCloseAllOthers;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_APP_LANGUAGE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_FONT_SIZE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_NAVIGATION;\n\nimport android.content.Context;\nimport android.os.Bundle;\n\nimport androidx.preference.ListPreference;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.mainmenu.MainMenuActivity;\nimport org.odk.collect.android.utilities.LocaleHelper;\nimport org.odk.collect.android.version.VersionInformation;\n\nimport java.util.ArrayList;\nimport java.util.TreeMap;\n\nimport javax.inject.Inject;\n\npublic class UserInterfacePreferencesFragment extends BaseProjectPreferencesFragment {\n\n    public static final String ARG_IN_FORM_ENTRY = \"in_form_entry\";\n\n    @Inject\n    VersionInformation versionInformation;\n\n    private boolean inFormEntry;\n\n    @Override\n    public void onAttach(Context context) {\n        super.onAttach(context);\n        DaggerUtils.getComponent(context).inject(this);\n\n        Bundle arguments = getArguments();\n        if (arguments != null) {\n            inFormEntry = arguments.getBoolean(ARG_IN_FORM_ENTRY);\n        }\n    }\n\n    @Override\n    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {\n        super.onCreatePreferences(savedInstanceState, rootKey);\n        setPreferencesFromResource(R.xml.user_interface_preferences, rootKey);\n\n        initNavigationPrefs();\n        initFontSizePref();\n        initLanguagePrefs();\n    }\n\n    private void initNavigationPrefs() {\n        final ListPreference pref = findPreference(KEY_NAVIGATION);\n\n        if (pref != null) {\n            pref.setSummary(pref.getEntry());\n            pref.setOnPreferenceChangeListener((preference, newValue) -> {\n                int index = ((ListPreference) preference).findIndexOfValue(newValue.toString());\n                String entry = (String) ((ListPreference) preference).getEntries()[index];\n                preference.setSummary(entry);\n                return true;\n            });\n        }\n    }\n\n    private void initFontSizePref() {\n        final ListPreference pref = findPreference(KEY_FONT_SIZE);\n\n        if (pref != null) {\n            pref.setSummary(pref.getEntry());\n            pref.setOnPreferenceChangeListener((preference, newValue) -> {\n                int index = ((ListPreference) preference).findIndexOfValue(newValue.toString());\n                CharSequence entry = ((ListPreference) preference).getEntries()[index];\n                preference.setSummary(entry);\n                return true;\n            });\n        }\n    }\n\n    private void initLanguagePrefs() {\n        final ListPreference pref = findPreference(KEY_APP_LANGUAGE);\n\n        if (pref != null) {\n            if (!inFormEntry) {\n                TreeMap<String, String> languageList = LocaleHelper.languageList();\n                ArrayList<String> entryValues = new ArrayList<>();\n                entryValues.add(0, \"\");\n                entryValues.addAll(languageList.values());\n                pref.setEntryValues(entryValues.toArray(new String[0]));\n                ArrayList<String> entries = new ArrayList<>();\n                entries.add(0, getActivity().getResources()\n                        .getString(org.odk.collect.strings.R.string.use_device_language));\n                entries.addAll(languageList.keySet());\n                pref.setEntries(entries.toArray(new String[0]));\n                if (pref.getValue() == null) {\n                    //set Default value to \"Use phone locale\"\n                    pref.setValueIndex(0);\n                }\n                pref.setSummary(pref.getEntry());\n                pref.setOnPreferenceChangeListener((preference, newValue) -> {\n                    int index = ((ListPreference) preference).findIndexOfValue(newValue.toString());\n                    String entry = (String) ((ListPreference) preference).getEntries()[index];\n                    preference.setSummary(entry);\n\n                    settingsProvider.getUnprotectedSettings().save(KEY_APP_LANGUAGE, newValue.toString());\n\n                    startActivityAndCloseAllOthers(getActivity(), MainMenuActivity.class);\n                    return true;\n                });\n            } else {\n                pref.setEnabled(false);\n                pref.setSummary(org.odk.collect.strings.R.string.setting_not_available_during_form_entry);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/screens/UserSettingsAccessPreferencesFragment.java",
    "content": "package org.odk.collect.android.preferences.screens;\n\nimport android.os.Bundle;\n\nimport org.odk.collect.android.R;\n\npublic class UserSettingsAccessPreferencesFragment extends BaseAdminPreferencesFragment {\n\n    @Override\n    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {\n        super.onCreatePreferences(savedInstanceState, rootKey);\n        setPreferencesFromResource(R.xml.user_settings_access_preferences, rootKey);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/source/SettingsStore.kt",
    "content": "package org.odk.collect.android.preferences.source\n\nimport androidx.preference.PreferenceDataStore\nimport org.odk.collect.shared.settings.Settings\n\nclass SettingsStore(private val settings: Settings) : PreferenceDataStore() {\n    override fun putString(key: String, value: String?) {\n        settings.save(key, value)\n    }\n\n    override fun putBoolean(key: String, value: Boolean) {\n        settings.save(key, value)\n    }\n\n    override fun putLong(key: String, value: Long) {\n        settings.save(key, value)\n    }\n\n    override fun putInt(key: String, value: Int) {\n        settings.save(key, value)\n    }\n\n    override fun putFloat(key: String, value: Float) {\n        settings.save(key, value)\n    }\n\n    override fun putStringSet(key: String, value: Set<String>?) {\n        settings.save(key, value)\n    }\n\n    override fun getString(key: String, defValue: String?): String? {\n        return settings.getString(key)\n    }\n\n    override fun getBoolean(key: String, defValue: Boolean): Boolean {\n        return settings.getBoolean(key)\n    }\n\n    override fun getLong(key: String, defValue: Long): Long {\n        return settings.getLong(key)\n    }\n\n    override fun getInt(key: String, defValue: Int): Int {\n        return settings.getInt(key)\n    }\n\n    override fun getFloat(key: String, defValue: Float): Float {\n        return settings.getFloat(key)\n    }\n\n    override fun getStringSet(key: String, defValue: Set<String>?): Set<String>? {\n        return settings.getStringSet(key)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/source/SharedPreferencesSettings.kt",
    "content": "package org.odk.collect.android.preferences.source\n\nimport android.content.SharedPreferences\nimport org.odk.collect.shared.settings.Settings\nimport java.lang.ref.WeakReference\nimport java.util.Collections\n\nclass SharedPreferencesSettings(\n    private val sharedPreferences: SharedPreferences,\n    private val settingKeysToDefaults: Map<String, Any> = emptyMap()\n) : Settings {\n\n    private val listeners =\n        mutableListOf<Pair<WeakReference<Settings.OnSettingChangeListener>, SharedPreferences.OnSharedPreferenceChangeListener>>()\n\n    override fun setDefaultForAllSettingsWithoutValues() {\n        for ((key, value) in settingKeysToDefaults) {\n            if (!sharedPreferences.contains(key)) {\n                save(key, value)\n            }\n        }\n    }\n\n    override fun save(key: String, value: Any?) {\n        saveAll(Collections.singletonMap(key, value))\n    }\n\n    override fun saveAll(settings: Map<String, Any?>) {\n        val editor = sharedPreferences.edit()\n        for ((key, value) in settings) {\n            when (value) {\n                null, is String -> editor.putString(key, value as String?)\n                is Boolean -> editor.putBoolean(key, value)\n                is Long -> editor.putLong(key, value)\n                is Int -> editor.putInt(key, value)\n                is Float -> editor.putFloat(key, value)\n                is Set<*> -> editor.putStringSet(key, value as Set<String?>)\n                else -> throw RuntimeException(\"Unhandled setting value type: $value\")\n            }\n        }\n        editor.apply()\n    }\n\n    override fun remove(key: String) {\n        sharedPreferences.edit().remove(key).apply()\n    }\n\n    override fun reset(key: String) {\n        save(key, settingKeysToDefaults[key])\n    }\n\n    override fun clear() {\n        sharedPreferences.edit().clear().apply()\n    }\n\n    override fun contains(key: String): Boolean {\n        return sharedPreferences.contains(key)\n    }\n\n    override fun getAll(): Map<String, *> {\n        return sharedPreferences.all\n    }\n\n    override fun getString(key: String): String? {\n        val defaultValue = (settingKeysToDefaults[key]) as String?\n        return sharedPreferences.getString(key, defaultValue)\n    }\n\n    override fun getBoolean(key: String): Boolean {\n        val defaultValue = (settingKeysToDefaults[key] ?: false) as Boolean\n        return sharedPreferences.getBoolean(key, defaultValue)\n    }\n\n    override fun getLong(key: String): Long {\n        val defaultValue = (settingKeysToDefaults[key] ?: 0L) as Long\n        return sharedPreferences.getLong(key, defaultValue)\n    }\n\n    override fun getInt(key: String): Int {\n        val defaultValue = (settingKeysToDefaults[key] ?: 0) as Int\n        return sharedPreferences.getInt(key, defaultValue)\n    }\n\n    override fun getFloat(key: String): Float {\n        val defaultValue = (settingKeysToDefaults[key] ?: 0f) as Float\n        return sharedPreferences.getFloat(key, defaultValue)\n    }\n\n    override fun getStringSet(key: String): Set<String>? {\n        val defaultValue = (settingKeysToDefaults[key] ?: emptySet<Any>()) as Set<String>\n        return sharedPreferences.getStringSet(key, defaultValue)\n    }\n\n    override fun registerOnSettingChangeListener(listener: Settings.OnSettingChangeListener) {\n        val sharedPreferencesListener =\n            SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? ->\n                key?.let {\n                    listener.onSettingChanged(it)\n                }\n            }\n\n        listeners.add(Pair(WeakReference(listener), sharedPreferencesListener))\n        sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)\n    }\n\n    override fun unregisterOnSettingChangeListener(listener: Settings.OnSettingChangeListener) {\n        val pair = listeners.find { pair -> listener == pair.first.get() }\n        listeners.remove(pair)\n\n        if (pair != null) {\n            sharedPreferences.unregisterOnSharedPreferenceChangeListener(pair.second)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/source/SharedPreferencesSettingsProvider.kt",
    "content": "package org.odk.collect.android.preferences.source\n\nimport android.content.Context\nimport org.odk.collect.android.preferences.Defaults\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.MetaKeys\nimport org.odk.collect.shared.settings.Settings\nimport javax.inject.Singleton\n\n@Singleton\nclass SharedPreferencesSettingsProvider(private val context: Context) : SettingsProvider {\n\n    private val settings = mutableMapOf<String, Settings>()\n\n    override fun getMetaSettings() = settings.getOrPut(META_SETTINGS_NAME) {\n        SharedPreferencesSettings(getSharedPrefs(META_SETTINGS_NAME))\n    }\n\n    override fun getUnprotectedSettings(projectId: String?): Settings {\n        val settingsId = getSettingsId(GENERAL_SETTINGS_NAME, projectId)\n\n        return settings.getOrPut(settingsId) {\n            SharedPreferencesSettings(getSharedPrefs(settingsId), Defaults.unprotected)\n        }\n    }\n\n    override fun getProtectedSettings(projectId: String?): Settings {\n        val settingsId = getSettingsId(ADMIN_SETTINGS_NAME, projectId)\n\n        return settings.getOrPut(settingsId) {\n            SharedPreferencesSettings(getSharedPrefs(settingsId), Defaults.protected)\n        }\n    }\n\n    override fun clearAll(projectIds: List<String>) {\n        projectIds.forEach {\n            getUnprotectedSettings(it).clear()\n            getProtectedSettings(it).clear()\n        }\n\n        getMetaSettings().clear()\n        settings.clear()\n    }\n\n    private fun getSharedPrefs(name: String) =\n        context.getSharedPreferences(name, Context.MODE_PRIVATE)\n\n    private fun getSettingsId(settingName: String, projectId: String?): String {\n        return if (projectId == null) {\n            settingName + (getMetaSettings().getString(MetaKeys.CURRENT_PROJECT_ID))\n        } else {\n            settingName + projectId\n        }\n    }\n\n    companion object {\n        private const val META_SETTINGS_NAME = \"meta\"\n        private const val GENERAL_SETTINGS_NAME = \"general_prefs\"\n        private const val ADMIN_SETTINGS_NAME = \"admin_prefs\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/preferences/utilities/PreferencesUtils.java",
    "content": "package org.odk.collect.android.preferences.utilities;\n\nimport androidx.preference.CheckBoxPreference;\nimport androidx.preference.Preference;\n\npublic final class PreferencesUtils {\n\n    private PreferencesUtils() {\n\n    }\n\n    public static void displayDisabled(CheckBoxPreference preference, boolean displayValue) {\n        preference.setPersistent(false);\n        preference.setEnabled(false);\n        preference.setChecked(displayValue);\n        preference.setPersistent(true);\n    }\n\n    public static void displayDisabled(Preference preference, String displayValue) {\n        preference.setEnabled(false);\n        preference.setSummaryProvider(pref -> displayValue);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/DuplicateProjectConfirmationDialog.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.app.Dialog\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport androidx.fragment.app.DialogFragment\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.R\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.projects.DuplicateProjectConfirmationKeys.MATCHING_PROJECT\nimport org.odk.collect.android.projects.DuplicateProjectConfirmationKeys.SETTINGS_JSON\n\nclass DuplicateProjectConfirmationDialog : DialogFragment() {\n    lateinit var listener: DuplicateProjectConfirmationListener\n\n    interface DuplicateProjectConfirmationListener {\n        fun createProject(settingsJson: String)\n        fun switchToProject(uuid: String)\n        fun cancel()\n    }\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        Analytics.log(AnalyticsEvents.DUPLICATE_PROJECT)\n        listener = parentFragment as DuplicateProjectConfirmationListener\n\n        val settingsJson = arguments?.getString(SETTINGS_JSON, \"\") ?: \"\"\n        val matchingProject = arguments?.getString(MATCHING_PROJECT, \"\") ?: \"\"\n\n        return MaterialAlertDialogBuilder(requireContext())\n            .setTitle(org.odk.collect.strings.R.string.duplicate_project)\n            .setMessage(org.odk.collect.strings.R.string.duplicate_project_details)\n            .setPositiveButton(org.odk.collect.strings.R.string.add_duplicate_project) { _, _ -> listener.createProject(settingsJson) }\n            .setNegativeButton(org.odk.collect.strings.R.string.switch_to_existing) { _, _ ->\n                run {\n                    listener.switchToProject(matchingProject)\n                    Analytics.log(AnalyticsEvents.DUPLICATE_PROJECT_SWITCH)\n                }\n            }\n            .create()\n    }\n\n    override fun onCancel(dialog: DialogInterface) {\n        super.onCancel(dialog)\n        listener.cancel()\n    }\n}\n\nobject DuplicateProjectConfirmationKeys {\n    const val SETTINGS_JSON = \"settingsJson\"\n    const val MATCHING_PROJECT = \"matchingProject\"\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/FileDebugLogger.kt",
    "content": "package org.odk.collect.android.projects\n\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.shared.DebugLogger\nimport java.io.File\nimport java.time.LocalDateTime\n\nclass FileDebugLogger(private val file: File) : DebugLogger {\n\n    override fun log(tag: String, message: String) {\n        if (enabled) {\n            val line = \"${LocalDateTime.now()} $tag \\\"$message\\\"\\n\"\n            file.appendText(line)\n        }\n    }\n\n    companion object {\n        private val enabled = BuildConfig.DEBUG\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/ManualProjectCreatorDialog.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.widget.doOnTextChanged\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.activities.ActivityUtils\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.configure.qr.AppConfigurationGenerator\nimport org.odk.collect.android.databinding.ManualProjectCreatorDialogLayoutBinding\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.android.projects.DuplicateProjectConfirmationKeys.MATCHING_PROJECT\nimport org.odk.collect.android.projects.DuplicateProjectConfirmationKeys.SETTINGS_JSON\nimport org.odk.collect.android.utilities.SoftKeyboardController\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.EdgeToEdge.applyBottomBarInsetMargins\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.androidshared.utils.Validator\nimport org.odk.collect.material.MaterialFullScreenDialogFragment\nimport org.odk.collect.projects.ProjectCreator\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.projects.SettingsConnectionMatcher\nimport org.odk.collect.settings.SettingsProvider\nimport javax.inject.Inject\n\nclass ManualProjectCreatorDialog :\n    MaterialFullScreenDialogFragment(),\n    DuplicateProjectConfirmationDialog.DuplicateProjectConfirmationListener {\n\n    @Inject\n    lateinit var projectCreator: ProjectCreator\n\n    @Inject\n    lateinit var appConfigurationGenerator: AppConfigurationGenerator\n\n    @Inject\n    lateinit var softKeyboardController: SoftKeyboardController\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    @Inject\n    lateinit var projectsRepository: ProjectsRepository\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    lateinit var settingsConnectionMatcher: SettingsConnectionMatcher\n\n    private lateinit var binding: ManualProjectCreatorDialogLayoutBinding\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n        settingsConnectionMatcher = SettingsConnectionMatcherImpl(projectsRepository, settingsProvider)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        binding = ManualProjectCreatorDialogLayoutBinding.inflate(inflater)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        setUpToolbar()\n\n        binding.urlInputText.doOnTextChanged { text, _, _, _ ->\n            binding.addButton.isEnabled = !text.isNullOrBlank()\n        }\n\n        binding.urlInputText.post {\n            softKeyboardController.showSoftKeyboard(binding.urlInputText)\n        }\n\n        binding.cancelButton.setOnClickListener {\n            dismiss()\n        }\n\n        binding.addButton.setOnClickListener {\n            handleAddingNewProject()\n        }\n\n        binding.bottomContainer.applyBottomBarInsetMargins()\n    }\n\n    override fun onCloseClicked() {\n    }\n\n    override fun onBackPressed() {\n        dismiss()\n    }\n\n    override fun getToolbar(): Toolbar {\n        return binding.toolbarLayout.toolbar\n    }\n\n    private fun setUpToolbar() {\n        getToolbar().setTitle(org.odk.collect.strings.R.string.add_project)\n    }\n\n    private fun handleAddingNewProject() {\n        if (!Validator.isUrlValid(binding.urlInputText.text?.trim().toString())) {\n            ToastUtils.showShortToast(org.odk.collect.strings.R.string.url_error)\n        } else {\n            val settingsJson = appConfigurationGenerator.getAppConfigurationAsJsonWithServerDetails(\n                binding.urlInputText.text?.trim().toString(),\n                binding.usernameInputText.text?.trim().toString(),\n                binding.passwordInputText.text?.trim().toString()\n            )\n\n            settingsConnectionMatcher.getProjectWithMatchingConnection(settingsJson)?.let { uuid ->\n                val confirmationArgs = Bundle()\n                confirmationArgs.putString(SETTINGS_JSON, settingsJson)\n                confirmationArgs.putString(MATCHING_PROJECT, uuid)\n                DialogFragmentUtils.showIfNotShowing(\n                    DuplicateProjectConfirmationDialog::class.java,\n                    confirmationArgs,\n                    childFragmentManager\n                )\n            } ?: run {\n                createProject(settingsJson)\n                Analytics.log(AnalyticsEvents.MANUAL_CREATE_PROJECT)\n            }\n        }\n    }\n\n    override fun createProject(settingsJson: String) {\n        projectCreator.createNewProject(settingsJson, true)\n        ActivityUtils.startActivityAndCloseAllOthers(activity, MainMenuActivity::class.java)\n        ToastUtils.showLongToast(\n            getString(org.odk.collect.strings.R.string.switched_project, projectsDataService.requireCurrentProject().name)\n        )\n    }\n\n    override fun switchToProject(uuid: String) {\n        projectsDataService.setCurrentProject(uuid)\n        ActivityUtils.startActivityAndCloseAllOthers(activity, MainMenuActivity::class.java)\n        ToastUtils.showLongToast(\n            getString(\n                org.odk.collect.strings.R.string.switched_project,\n                projectsDataService.requireCurrentProject().name\n            )\n        )\n    }\n\n    override fun cancel() {}\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/ProjectCreatorImpl.kt",
    "content": "package org.odk.collect.android.projects\n\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectConfigurationResult\nimport org.odk.collect.projects.ProjectCreator\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.odk.collect.settings.SettingsProvider\n\nclass ProjectCreatorImpl(\n    private val projectsRepository: ProjectsRepository,\n    private val projectsDataService: ProjectsDataService,\n    private val settingsImporter: ODKAppSettingsImporter,\n    private val settingsProvider: SettingsProvider\n) : ProjectCreator {\n\n    override fun createNewProject(settingsJson: String, switchToTheNewProject: Boolean): ProjectConfigurationResult {\n        val savedProject = projectsRepository.save(Project.New(\"\", \"\", \"\"))\n        val settingsImportingResult = settingsImporter.fromJSON(settingsJson, savedProject)\n\n        return if (settingsImportingResult == ProjectConfigurationResult.SUCCESS) {\n            if (switchToTheNewProject) {\n                projectsDataService.setCurrentProject(savedProject.uuid)\n            }\n            settingsImportingResult\n        } else {\n            settingsProvider.getUnprotectedSettings(savedProject.uuid).clear()\n            settingsProvider.getProtectedSettings(savedProject.uuid).clear()\n            projectsRepository.delete(savedProject.uuid)\n            settingsImportingResult\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/ProjectDeleter.kt",
    "content": "package org.odk.collect.android.projects\n\nimport org.odk.collect.android.backgroundwork.FormUpdateScheduler\nimport org.odk.collect.android.backgroundwork.InstanceSubmitScheduler\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.db.sqlite.DatabaseConnection\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport java.io.File\n\nclass ProjectDeleter(\n    private val projectsRepository: ProjectsRepository,\n    private val projectsDataService: ProjectsDataService,\n    private val formUpdateScheduler: FormUpdateScheduler,\n    private val instanceSubmitScheduler: InstanceSubmitScheduler,\n    private val storagePathProvider: StoragePathProvider,\n    private val settingsProvider: SettingsProvider\n) {\n    fun deleteProject(projectId: String): DeleteProjectResult {\n        return performProjectDeletion(projectId)\n    }\n\n    private fun performProjectDeletion(projectId: String): DeleteProjectResult {\n        formUpdateScheduler.cancelUpdates(projectId)\n        instanceSubmitScheduler.cancelSubmit(projectId)\n\n        settingsProvider.getUnprotectedSettings(projectId).clear()\n        settingsProvider.getProtectedSettings(projectId).clear()\n\n        projectsRepository.delete(projectId)\n\n        File(storagePathProvider.getProjectRootDirPath(projectId)).deleteRecursively()\n\n        DatabaseConnection.cleanUp()\n\n        return try {\n            projectsDataService.requireCurrentProject()\n            DeleteProjectResult.DeletedSuccessfullyInactiveProject\n        } catch (e: IllegalStateException) {\n            if (projectsRepository.getAll().isEmpty()) {\n                DeleteProjectResult.DeletedSuccessfullyLastProject\n            } else {\n                val newProject = projectsRepository.getAll()[0]\n                projectsDataService.setCurrentProject(newProject.uuid)\n                DeleteProjectResult.DeletedSuccessfullyCurrentProject(newProject)\n            }\n        }\n    }\n}\n\nsealed class DeleteProjectResult {\n    data object DeletedSuccessfullyLastProject : DeleteProjectResult()\n\n    data object DeletedSuccessfullyInactiveProject : DeleteProjectResult()\n\n    data class DeletedSuccessfullyCurrentProject(val newCurrentProject: Project.Saved) : DeleteProjectResult()\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/ProjectDependencyModule.kt",
    "content": "package org.odk.collect.android.projects\n\nimport org.odk.collect.android.storage.StoragePaths\nimport org.odk.collect.android.utilities.ChangeLocks\nimport org.odk.collect.entities.server.EntitySource\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.forms.FormSource\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.projects.projectDependency\nimport org.odk.collect.shared.DebugLogger\nimport org.odk.collect.shared.settings.Settings\n\n/**\n * Provides all the basic/building block dependencies needed when performing logic inside a\n * project.\n */\ndata class ProjectDependencyModule(\n    val projectId: String,\n    private val settingsFactory: ProjectDependencyFactory<Settings>,\n    private val formsRepositoryFactory: ProjectDependencyFactory<FormsRepository>,\n    private val instancesRepositoryProvider: ProjectDependencyFactory<InstancesRepository>,\n    private val storagePathsFactory: ProjectDependencyFactory<StoragePaths>,\n    private val changeLockFactory: ProjectDependencyFactory<ChangeLocks>,\n    private val formSourceFactory: ProjectDependencyFactory<FormSource>,\n    private val savepointsRepositoryFactory: ProjectDependencyFactory<SavepointsRepository>,\n    private val entitiesRepositoryFactory: ProjectDependencyFactory<EntitiesRepository>,\n    private val entitySourceFactory: ProjectDependencyFactory<EntitySource>,\n    private val debugLoggerFactory: ProjectDependencyFactory<DebugLogger>\n) {\n    val generalSettings by projectDependency(projectId, settingsFactory)\n    val formsRepository by projectDependency(projectId, formsRepositoryFactory)\n    val instancesRepository by projectDependency(projectId, instancesRepositoryProvider)\n    val formSource by projectDependency(projectId, formSourceFactory)\n    val formsLock by projectDependency(projectId, changeLockFactory) { it.formsLock }\n    val instancesLock by projectDependency(projectId, changeLockFactory) { it.instancesLock }\n    val formsDir by projectDependency(projectId, storagePathsFactory) { it.formsDir }\n    val cacheDir by projectDependency(projectId, storagePathsFactory) { it.cacheDir }\n    val entitiesRepository by projectDependency(projectId, entitiesRepositoryFactory)\n    val savepointsRepository by projectDependency(projectId, savepointsRepositoryFactory)\n    val rootDir by projectDependency(projectId, storagePathsFactory) { it.rootDir }\n    val instancesDir by projectDependency(projectId, storagePathsFactory) { it.instancesDir }\n    val entitySource by projectDependency(projectId, entitySourceFactory)\n    val debugLogger by projectDependency(projectId, debugLoggerFactory)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/ProjectIconView.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.content.Context\nimport android.graphics.Color\nimport android.graphics.drawable.GradientDrawable\nimport android.util.AttributeSet\nimport android.widget.FrameLayout\nimport android.widget.TextView\nimport org.odk.collect.android.R\nimport org.odk.collect.projects.Project\nimport kotlin.properties.Delegates\n\nclass ProjectIconView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {\n\n    constructor(context: Context) : this(context, null)\n\n    init {\n        inflate(context, R.layout.project_icon_view, this)\n    }\n\n    var project: Project? by Delegates.observable(null) { _, _, new ->\n        if (new != null) {\n            findViewById<TextView>(R.id.project_icon_text).apply {\n                try {\n                    (background as GradientDrawable).setColor(Color.parseColor(new.color))\n                } catch (e: Exception) {\n                    // ignore\n                }\n\n                text = new.icon\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/ProjectListItemView.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.widget.FrameLayout\nimport android.widget.TextView\nimport org.odk.collect.android.R\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\nimport java.net.URL\n\nclass ProjectListItemView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {\n\n    constructor(context: Context) : this(context, null)\n\n    init {\n        inflate(context, R.layout.project_list_item, this)\n    }\n\n    fun setupView(project: Project, generalSettings: Settings) {\n        findViewById<ProjectIconView>(R.id.project_icon).project = project\n        findViewById<TextView>(R.id.project_name).text = project.name\n        findViewById<TextView>(R.id.project_subtext).text = getSubtext(generalSettings)\n    }\n\n    private fun getSubtext(generalSettings: Settings): String {\n        val username = generalSettings.getString(ProjectKeys.KEY_USERNAME) ?: \"\"\n        val url = generalSettings.getString(ProjectKeys.KEY_SERVER_URL) ?: \"\"\n        val connectedTo = try {\n            URL(url).host\n        } catch (e: Exception) {\n            url\n        }\n\n        return if (username.isNotBlank()) {\n            \"$username / $connectedTo\"\n        } else {\n            connectedTo\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/ProjectResetter.kt",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.projects\n\nimport org.odk.collect.android.fastexternalitemset.ItemsetDbAdapter\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.storage.StoragePaths\nimport org.odk.collect.android.utilities.WebCredentialsUtils\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport org.odk.collect.metadata.PropertyManager\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.settings.SettingsProvider\nimport java.io.File\n\nclass ProjectResetter(\n    storagePathProvider: ProjectDependencyFactory<StoragePaths>,\n    private val propertyManager: PropertyManager,\n    private val settingsProvider: SettingsProvider,\n    private val formsRepositoryProvider: ProjectDependencyFactory<FormsRepository>,\n    private val savepointsRepositoryProvider: ProjectDependencyFactory<SavepointsRepository>,\n    private val instancesDataService: InstancesDataService,\n    private val projectId: String\n) {\n    private val storagePaths = storagePathProvider.create(projectId)\n\n    private var failedResetActions = mutableListOf<Int>()\n\n    fun reset(resetActions: List<Int>): List<Int> {\n        for (action in resetActions) {\n            when (action) {\n                ResetAction.RESET_PREFERENCES -> resetPreferences()\n                ResetAction.RESET_INSTANCES -> resetInstances()\n                ResetAction.RESET_FORMS -> resetForms()\n                ResetAction.RESET_LAYERS -> resetLayers()\n                ResetAction.RESET_CACHE -> resetCache()\n            }\n        }\n        return failedResetActions\n    }\n\n    private fun resetPreferences() {\n        WebCredentialsUtils.clearAllCredentials()\n\n        settingsProvider.getUnprotectedSettings().clear()\n        settingsProvider.getUnprotectedSettings().setDefaultForAllSettingsWithoutValues()\n        settingsProvider.getProtectedSettings().clear()\n        settingsProvider.getProtectedSettings().setDefaultForAllSettingsWithoutValues()\n\n        if (!deleteFolderContent(storagePaths.settingsDir)) {\n            failedResetActions.add(ResetAction.RESET_PREFERENCES)\n        }\n\n        propertyManager.reload()\n    }\n\n    private fun resetInstances() {\n        if (instancesDataService.reset(projectId)) {\n            savepointsRepositoryProvider.create(projectId).deleteAll()\n        } else {\n            failedResetActions.add(ResetAction.RESET_INSTANCES)\n        }\n    }\n\n    private fun resetForms() {\n        formsRepositoryProvider.create(projectId).deleteAll()\n\n        File(storagePaths.metaDir + File.separator + ItemsetDbAdapter.DATABASE_NAME).delete()\n\n        if (!deleteFolderContent(storagePaths.formsDir)) {\n            failedResetActions.add(ResetAction.RESET_FORMS)\n        }\n    }\n\n    private fun resetLayers() {\n        if (!deleteFolderContent(storagePaths.layersDir)) {\n            failedResetActions.add(ResetAction.RESET_LAYERS)\n        }\n    }\n\n    private fun resetCache() {\n        savepointsRepositoryProvider.create(projectId).deleteAll()\n        if (!deleteFolderContent(storagePaths.cacheDir)) {\n            failedResetActions.add(ResetAction.RESET_CACHE)\n        }\n    }\n\n    private fun deleteFolderContent(path: String): Boolean {\n        var result = true\n        val file = File(path)\n        if (file.exists()) {\n            val files = file.listFiles()\n            if (files != null) {\n                for (f in files) {\n                    result = f.deleteRecursively()\n                }\n            }\n        }\n        return result\n    }\n\n    object ResetAction {\n        const val RESET_PREFERENCES = 0\n        const val RESET_INSTANCES = 1\n        const val RESET_FORMS = 2\n        const val RESET_LAYERS = 3\n        const val RESET_CACHE = 4\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/ProjectSettingsDialog.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.app.Dialog\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.View.INVISIBLE\nimport android.view.View.VISIBLE\nimport androidx.fragment.app.DialogFragment\nimport androidx.lifecycle.ViewModelProvider\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.android.activities.AboutActivity\nimport org.odk.collect.android.activities.ActivityUtils\nimport org.odk.collect.android.databinding.ProjectSettingsDialogLayoutBinding\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.mainmenu.CurrentProjectViewModel\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.android.preferences.screens.ProjectPreferencesActivity\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport javax.inject.Inject\n\nclass ProjectSettingsDialog(private val viewModelFactory: ViewModelProvider.Factory) :\n    DialogFragment() {\n\n    @Inject\n    lateinit var projectsRepository: ProjectsRepository\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    lateinit var binding: ProjectSettingsDialogLayoutBinding\n\n    private lateinit var currentProjectViewModel: CurrentProjectViewModel\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n\n        currentProjectViewModel = ViewModelProvider(\n            requireActivity(),\n            viewModelFactory\n        )[CurrentProjectViewModel::class.java]\n    }\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        binding = ProjectSettingsDialogLayoutBinding.inflate(layoutInflater)\n\n        currentProjectViewModel.currentProject.observe(this) {\n            if (it != null) {\n                binding.currentProject.setupView(it, settingsProvider.getUnprotectedSettings())\n                binding.currentProject.contentDescription =\n                    getString(org.odk.collect.strings.R.string.using_project, it.name)\n\n                inflateListOfInActiveProjects(requireContext(), it)\n            }\n        }\n\n        binding.closeIcon.setOnClickListener {\n            dismiss()\n        }\n\n        binding.generalSettingsButton.setOnClickListener {\n            startActivity(Intent(requireContext(), ProjectPreferencesActivity::class.java))\n            dismiss()\n        }\n\n        binding.addProjectButton.setOnClickListener {\n            DialogFragmentUtils.showIfNotShowing(\n                QrCodeProjectCreatorDialog::class.java,\n                requireActivity().supportFragmentManager\n            )\n            dismiss()\n        }\n\n        binding.aboutButton.setOnClickListener {\n            startActivity(Intent(requireContext(), AboutActivity::class.java))\n            dismiss()\n        }\n\n        return MaterialAlertDialogBuilder(requireContext())\n            .setView(binding.root)\n            .create()\n    }\n\n    private fun inflateListOfInActiveProjects(context: Context, currentProject: Project.Saved) {\n        binding.projectList.removeAllViews()\n\n        if (projectsRepository.getAll().none { it.uuid != currentProject.uuid }) {\n            binding.topDivider.visibility = INVISIBLE\n        } else {\n            binding.topDivider.visibility = VISIBLE\n        }\n\n        projectsRepository.getAll().filter {\n            it.uuid != currentProject.uuid\n        }.forEach { project ->\n            val projectView = ProjectListItemView(context)\n\n            projectView.setOnClickListener {\n                switchProject(project)\n            }\n\n            projectView.setupView(project, settingsProvider.getUnprotectedSettings(project.uuid))\n            projectView.contentDescription =\n                getString(org.odk.collect.strings.R.string.switch_to_project, project.name)\n            binding.projectList.addView(projectView)\n        }\n    }\n\n    private fun switchProject(project: Project.Saved) {\n        currentProjectViewModel.setCurrentProject(project)\n\n        ActivityUtils.startActivityAndCloseAllOthers(\n            requireActivity(),\n            MainMenuActivity::class.java\n        )\n        ToastUtils.showLongToast(\n            getString(org.odk.collect.strings.R.string.switched_project, project.name)\n        )\n        dismiss()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/ProjectsDataService.kt",
    "content": "package org.odk.collect.android.projects\n\nimport kotlinx.coroutines.flow.StateFlow\nimport org.odk.collect.android.application.initialization.AnalyticsInitializer\nimport org.odk.collect.android.application.initialization.MapsInitializer\nimport org.odk.collect.android.state.DataKeys\nimport org.odk.collect.androidshared.data.AppState\nimport org.odk.collect.androidshared.data.DataService\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.MetaKeys\n\nclass ProjectsDataService(\n    appState: AppState,\n    private val settingsProvider: SettingsProvider,\n    private val projectsRepository: ProjectsRepository,\n    private val analyticsInitializer: AnalyticsInitializer,\n    private val mapsInitializer: MapsInitializer\n) : DataService(appState) {\n\n    private val currentProject by data(DataKeys.PROJECT, null) {\n        val currentProjectId = getCurrentProjectId()\n\n        if (currentProjectId != null) {\n            projectsRepository.get(currentProjectId)\n        } else {\n            val projects = projectsRepository.getAll()\n\n            if (projects.isNotEmpty()) {\n                projects[0]\n            } else {\n                null\n            }\n        }\n    }\n\n    fun getCurrentProject(): StateFlow<Project.Saved?> {\n        return currentProject.flow()\n    }\n\n    @Deprecated(\n        \"Most components should be passed project ID/project as a value\",\n        replaceWith = ReplaceWith(\"getCurrentProject().value!!\")\n    )\n    fun requireCurrentProject(): Project.Saved {\n        update()\n        val project = getCurrentProject().value\n\n        if (project != null) {\n            return project\n        } else {\n            throw IllegalStateException(\"No current project!\")\n        }\n    }\n\n    fun setCurrentProject(projectId: String) {\n        settingsProvider.getMetaSettings().save(MetaKeys.CURRENT_PROJECT_ID, projectId)\n\n        analyticsInitializer.initialize()\n        mapsInitializer.initialize()\n\n        update()\n    }\n\n    private fun getCurrentProjectId(): String? {\n        return settingsProvider.getMetaSettings().getString(MetaKeys.CURRENT_PROJECT_ID)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/QrCodeProjectCreatorDialog.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.activity.result.ActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.appcompat.widget.Toolbar\nimport com.google.zxing.client.android.BeepManager\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.R\nimport org.odk.collect.android.activities.ActivityUtils\nimport org.odk.collect.android.analytics.AnalyticsEvents\nimport org.odk.collect.android.databinding.QrCodeProjectCreatorDialogLayoutBinding\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.androidshared.ui.ToastUtils.showShortToast\nimport org.odk.collect.androidshared.ui.enableIconsVisibility\nimport org.odk.collect.androidshared.utils.CompressionUtils\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.material.MaterialFullScreenDialogFragment\nimport org.odk.collect.permissions.PermissionListener\nimport org.odk.collect.permissions.PermissionsProvider\nimport org.odk.collect.projects.ProjectConfigurationResult\nimport org.odk.collect.projects.ProjectCreator\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.projects.SettingsConnectionMatcher\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.qrcode.zxing.QRCodeDecoder\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.odk.collect.settings.SettingsProvider\nimport timber.log.Timber\nimport javax.inject.Inject\n\nclass QrCodeProjectCreatorDialog :\n    MaterialFullScreenDialogFragment(),\n    DuplicateProjectConfirmationDialog.DuplicateProjectConfirmationListener {\n\n    @Inject\n    lateinit var permissionsProvider: PermissionsProvider\n\n    @Inject\n    lateinit var projectCreator: ProjectCreator\n\n    @Inject\n    lateinit var projectsDataService: ProjectsDataService\n\n    @Inject\n    lateinit var projectsRepository: ProjectsRepository\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    lateinit var settingsConnectionMatcher: SettingsConnectionMatcher\n\n    private lateinit var beepManager: BeepManager\n    lateinit var binding: QrCodeProjectCreatorDialogLayoutBinding\n\n    @Inject\n    lateinit var qrCodeDecoder: QRCodeDecoder\n\n    @Inject\n    lateinit var settingsImporter: ODKAppSettingsImporter\n\n    @Inject\n    lateinit var intentLauncher: IntentLauncher\n\n    @Inject\n    lateinit var barcodeScannerViewFactory: BarcodeScannerViewContainer.Factory\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    private var savedInstanceState: Bundle? = null\n\n    private val imageQrCodeImportResultLauncher =\n        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->\n            val imageUri: Uri? = result.data?.data\n            if (imageUri != null) {\n                permissionsProvider.requestReadUriPermission(\n                    requireActivity(),\n                    imageUri,\n                    requireActivity().contentResolver,\n                    object : PermissionListener {\n                        override fun granted() {\n                            // Do not call from a fragment that does not exist anymore https://github.com/getodk/collect/issues/4741\n                            if (isAdded) {\n                                requireActivity().contentResolver.openInputStream(imageUri).use {\n                                    val settingsJson = try {\n                                        qrCodeDecoder.decode(it)\n                                    } catch (e: QRCodeDecoder.QRCodeInvalidException) {\n                                        showShortToast(\n                                            org.odk.collect.strings.R.string.invalid_qrcode\n                                        )\n                                        \"\"\n                                    } catch (e: QRCodeDecoder.QRCodeNotFoundException) {\n                                        showShortToast(\n                                            org.odk.collect.strings.R.string.qr_code_not_found\n                                        )\n                                        \"\"\n                                    }\n                                    createProjectOrError(settingsJson)\n                                }\n                            }\n                        }\n                    }\n                )\n            }\n        }\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n\n        settingsConnectionMatcher =\n            SettingsConnectionMatcherImpl(projectsRepository, settingsProvider)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        this.savedInstanceState = savedInstanceState\n\n        binding = QrCodeProjectCreatorDialogLayoutBinding.inflate(inflater)\n        binding.toolbarLayout.toolbar.setTitle(org.odk.collect.strings.R.string.add_project)\n\n        configureMenu()\n\n        binding.configureManuallyButton.setOnClickListener {\n            DialogFragmentUtils.showIfNotShowing(\n                ManualProjectCreatorDialog::class.java,\n                requireActivity().supportFragmentManager\n            )\n        }\n        binding.cancelButton.setOnClickListener {\n            dismiss()\n        }\n        beepManager = BeepManager(requireActivity())\n\n        binding.barcodeView.setup(\n            barcodeScannerViewFactory,\n            requireActivity(),\n            viewLifecycleOwner,\n            true\n        )\n\n        binding.barcodeView.barcodeScannerView.latestBarcode.observe(\n            viewLifecycleOwner\n        ) { result: String ->\n            try {\n                beepManager.playBeepSoundAndVibrate()\n            } catch (e: Exception) {\n                // ignore because beeping isn't essential and this can crash the whole app\n            }\n\n            val settingsJson = try {\n                CompressionUtils.decompress(result)\n            } catch (e: Exception) {\n                showShortToast(\n                    getString(org.odk.collect.strings.R.string.invalid_qrcode)\n                )\n                \"\"\n            }\n            createProjectOrError(settingsJson)\n        }\n\n        return binding.root\n    }\n\n    override fun onStart() {\n        super.onStart()\n\n        permissionsProvider.requestCameraPermission(\n            requireActivity(),\n            object : PermissionListener {\n                override fun granted() {\n                    // Do not call from a fragment that does not exist anymore https://github.com/getodk/collect/issues/4741\n                    if (isAdded) {\n                        binding.barcodeView.barcodeScannerView.start()\n                    }\n                }\n            }\n        )\n    }\n\n    private fun configureMenu() {\n        val toolbar = binding.toolbarLayout.toolbar\n        toolbar.inflateMenu(R.menu.qr_code_scan_menu)\n\n        val menu = toolbar.menu\n        menu.enableIconsVisibility()\n\n        menu.removeItem(R.id.menu_item_share)\n\n        toolbar.setOnMenuItemClickListener {\n            when (it.itemId) {\n                R.id.menu_item_scan_sd_card -> {\n                    val photoPickerIntent = Intent(Intent.ACTION_GET_CONTENT)\n                    photoPickerIntent.type = \"image/*\"\n                    intentLauncher.launchForResult(\n                        imageQrCodeImportResultLauncher,\n                        photoPickerIntent\n                    ) {\n                        showShortToast(\n                            getString(\n                                org.odk.collect.strings.R.string.activity_not_found,\n                                getString(org.odk.collect.strings.R.string.choose_image)\n                            )\n                        )\n                        Timber.w(\n                            getString(\n                                org.odk.collect.strings.R.string.activity_not_found,\n                                getString(org.odk.collect.strings.R.string.choose_image)\n                            )\n                        )\n                    }\n                }\n            }\n            false\n        }\n    }\n\n    override fun onCloseClicked() {\n    }\n\n    override fun onBackPressed() {\n        dismiss()\n    }\n\n    override fun getToolbar(): Toolbar? {\n        return binding.toolbarLayout.toolbar\n    }\n\n    private fun createProjectOrError(settingsJson: String) {\n        settingsConnectionMatcher.getProjectWithMatchingConnection(settingsJson)?.let { uuid ->\n            val confirmationArgs = Bundle()\n            confirmationArgs.putString(\n                DuplicateProjectConfirmationKeys.SETTINGS_JSON,\n                settingsJson\n            )\n            confirmationArgs.putString(DuplicateProjectConfirmationKeys.MATCHING_PROJECT, uuid)\n            DialogFragmentUtils.showIfNotShowing(\n                DuplicateProjectConfirmationDialog::class.java,\n                confirmationArgs,\n                childFragmentManager\n            )\n        } ?: run {\n            createProject(settingsJson)\n        }\n    }\n\n    override fun createProject(settingsJson: String) {\n        when (projectCreator.createNewProject(settingsJson, true)) {\n            ProjectConfigurationResult.SUCCESS -> {\n                Analytics.log(AnalyticsEvents.QR_CREATE_PROJECT)\n\n                ActivityUtils.startActivityAndCloseAllOthers(activity, MainMenuActivity::class.java)\n                ToastUtils.showLongToast(\n                    getString(\n                        org.odk.collect.strings.R.string.switched_project,\n                        projectsDataService.requireCurrentProject().name\n                    )\n                )\n            }\n\n            ProjectConfigurationResult.INVALID_SETTINGS -> {\n                ToastUtils.showLongToast(\n                    getString(\n                        org.odk.collect.strings.R.string.invalid_qrcode\n                    )\n                )\n\n                restartScanning()\n            }\n\n            ProjectConfigurationResult.GD_PROJECT -> {\n                ToastUtils.showLongToast(\n                    getString(\n                        org.odk.collect.strings.R.string.settings_with_gd_protocol\n                    )\n                )\n\n                restartScanning()\n            }\n        }\n    }\n\n    private fun restartScanning() {\n        scheduler.immediate(foreground = true, delay = 2000L) {\n            binding.barcodeView.barcodeScannerView.start()\n        }\n    }\n\n    override fun switchToProject(uuid: String) {\n        projectsDataService.setCurrentProject(uuid)\n        ActivityUtils.startActivityAndCloseAllOthers(activity, MainMenuActivity::class.java)\n        ToastUtils.showLongToast(\n            getString(\n                org.odk.collect.strings.R.string.switched_project,\n                projectsDataService.requireCurrentProject().name\n            )\n        )\n    }\n\n    override fun cancel() {\n        binding.barcodeView.barcodeScannerView.start()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/projects/SettingsConnectionMatcherImpl.kt",
    "content": "package org.odk.collect.android.projects\n\nimport org.json.JSONException\nimport org.json.JSONObject\nimport org.odk.collect.android.preferences.Defaults\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.projects.SettingsConnectionMatcher\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.AppConfigurationKeys\nimport org.odk.collect.settings.keys.ProjectKeys\n\nclass SettingsConnectionMatcherImpl(\n    private val projectsRepository: ProjectsRepository,\n    private val settingsProvider: SettingsProvider\n) : SettingsConnectionMatcher {\n\n    override fun getProjectWithMatchingConnection(settingsJson: String): String? {\n        try {\n            val jsonObject = JSONObject(settingsJson)\n            val jsonSettings = jsonObject.getJSONObject(AppConfigurationKeys.GENERAL)\n\n            val jsonUrl = try { jsonSettings.get(ProjectKeys.KEY_SERVER_URL) } catch (e: JSONException) { Defaults.unprotected[ProjectKeys.KEY_SERVER_URL]!! }\n            val jsonUsername = try { jsonSettings.get(ProjectKeys.KEY_USERNAME) } catch (e: JSONException) { \"\" }\n\n            projectsRepository.getAll().forEach {\n                val projectSettings = settingsProvider.getUnprotectedSettings(it.uuid)\n                val projectUrl = projectSettings.getString(ProjectKeys.KEY_SERVER_URL)\n                val projectUsername = projectSettings.getString(ProjectKeys.KEY_USERNAME)\n\n                if (jsonUrl.equals(projectUrl) && jsonUsername.equals(projectUsername)) {\n                    return it.uuid\n                }\n            }\n        } catch (e: JSONException) {\n            return null\n        }\n        return null\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/savepoints/SavepointTask.kt",
    "content": "package org.odk.collect.android.savepoints\n\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.SchedulerAsyncTaskMimic\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport org.odk.collect.shared.files.FileExt.saveToFile\nimport timber.log.Timber\nimport java.io.File\n\nclass SavepointTask(\n    private var listener: SavepointListener?,\n    private val formController: FormController,\n    private val formDbId: Long,\n    private val instanceDbId: Long?,\n    private val cacheDir: String,\n    private val savepointsRepository: SavepointsRepository,\n    val scheduler: Scheduler\n) : SchedulerAsyncTaskMimic<Unit, Unit, String?>(scheduler) {\n    private var priority: Int = ++lastPriorityUsed\n\n    override fun onPreExecute() = Unit\n\n    override fun doInBackground(vararg params: Unit): String? {\n        if (priority < lastPriorityUsed) {\n            return null\n        }\n\n        return try {\n            val savepointFile = File(cacheDir, \"${formController.getInstanceFile()!!.name}.save\")\n            val savepoint = Savepoint(formDbId, instanceDbId, savepointFile.absolutePath, formController.getInstanceFile()!!.absolutePath)\n\n            if (priority == lastPriorityUsed) {\n                savepointFile.saveToFile(formController.getFilledInFormXml().payloadStream)\n                savepointsRepository.save(savepoint)\n            }\n\n            null\n        } catch (e: Exception) {\n            Timber.e(e.message)\n            e.message\n        }\n    }\n\n    override fun onPostExecute(result: String?) {\n        if (result != null) {\n            listener?.onSavePointError(result)\n            listener = null\n        }\n    }\n\n    override fun onProgressUpdate(vararg values: Unit) = Unit\n\n    override fun onCancelled() = Unit\n\n    companion object {\n        private var lastPriorityUsed: Int = 0\n    }\n}\n\ninterface SavepointListener {\n    fun onSavePointError(errorMessage: String?)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/savepoints/SavepointUseCases.kt",
    "content": "package org.odk.collect.android.savepoints\n\nimport android.net.Uri\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.android.utilities.ContentUriHelper\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport java.io.File\n\nobject SavepointUseCases {\n    fun getSavepoint(\n        uri: Uri,\n        uriMimeType: String,\n        formsRepository: FormsRepository,\n        instanceRepository: InstancesRepository,\n        savepointsRepository: SavepointsRepository\n    ): Savepoint? {\n        return if (uriMimeType == FormsContract.CONTENT_ITEM_TYPE) {\n            val selectedForm = formsRepository.get(ContentUriHelper.getIdFromUri(uri))!!\n\n            formsRepository.getAllByFormId(selectedForm.formId)\n                .filter { it.date <= selectedForm.date }\n                .sortedByDescending { it.date }\n                .forEach { form ->\n                    val savepoint = savepointsRepository.get(form.dbId, null)\n                    if (savepoint != null && File(savepoint.savepointFilePath).exists()) {\n                        return savepoint\n                    }\n                }\n            null\n        } else {\n            val instance = instanceRepository.get(ContentUriHelper.getIdFromUri(uri))!!\n            val form = formsRepository.getLatestByFormIdAndVersion(instance.formId, instance.formVersion)!!\n\n            val savepoint = savepointsRepository.get(form.dbId, instance.dbId)\n            if (savepoint != null &&\n                File(savepoint.savepointFilePath).exists() &&\n                File(savepoint.savepointFilePath).lastModified() > instance.lastStatusChangeDate\n            ) {\n                savepoint\n            } else {\n                null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/state/DataKeys.kt",
    "content": "package org.odk.collect.android.state\n\nobject DataKeys {\n    const val PROJECT = \"project\"\n    const val INSTANCES_EDITABLE_COUNT = \"instancesEditableCount\"\n    const val INSTANCES_SENDABLE_COUNT = \"instancesSendableCount\"\n    const val INSTANCES_SENT_COUNT = \"instancesSentCount\"\n    const val INSTANCES_SUCCESSFULLY_SENT_COUNT = \"instancesSuccessfullySentCount\"\n    const val INSTANCES_UNSENT_COUNT = \"instancesUnsentCount\"\n    const val INSTANCES = \"instances\"\n\n    const val FORMS = \"forms\"\n    const val FORMS_COUNT = \"formsCount\"\n    const val SYNC_STATUS_SYNCING = \"syncStatusSyncing\"\n    const val SYNC_STATUS_ERROR = \"syncStatusError\"\n    const val DISK_ERROR = \"diskError\"\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/storage/StoragePathProvider.kt",
    "content": "package org.odk.collect.android.storage\n\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.shared.PathUtils\nimport timber.log.Timber\nimport java.io.File\n\nclass StoragePathProvider(\n    private val projectsDataService: ProjectsDataService = DaggerUtils.getComponent(Collect.getInstance()).currentProjectProvider(),\n    private val projectsRepository: ProjectsRepository = DaggerUtils.getComponent(Collect.getInstance()).projectsRepository(),\n    val odkRootDirPath: String = Collect.getInstance().getExternalFilesDir(null)!!.absolutePath\n) : ProjectDependencyFactory<StoragePaths> {\n\n    @JvmOverloads\n    @Deprecated(message = \"Use create() instead\")\n    fun getProjectRootDirPath(projectId: String? = null): String {\n        val uuid = projectId ?: projectsDataService.requireCurrentProject().uuid\n        val path = getOdkDirPath(StorageSubdirectory.PROJECTS) + File.separator + uuid\n\n        if (!File(path).exists()) {\n            File(path).mkdirs()\n\n            try {\n                val sanitizedProjectName = PathUtils.getPathSafeFileName(projectsRepository.get(uuid)!!.name)\n                File(path + File.separator + sanitizedProjectName).createNewFile()\n            } catch (e: Exception) {\n                Timber.e(\n                    Error(\n                        FileUtils.getFilenameError(\n                            projectsRepository.get(uuid)!!.name\n                        )\n                    )\n                )\n            }\n        }\n\n        return path\n    }\n\n    @JvmOverloads\n    @Deprecated(message = \"Use create() instead\")\n    fun getOdkDirPath(subdirectory: StorageSubdirectory, projectId: String? = null): String {\n        val path = when (subdirectory) {\n            StorageSubdirectory.PROJECTS,\n            StorageSubdirectory.SHARED_LAYERS -> odkRootDirPath + File.separator + subdirectory.directoryName\n            StorageSubdirectory.FORMS,\n            StorageSubdirectory.INSTANCES,\n            StorageSubdirectory.CACHE,\n            StorageSubdirectory.METADATA,\n            StorageSubdirectory.LAYERS,\n            StorageSubdirectory.SETTINGS -> getProjectRootDirPath(projectId) + File.separator + subdirectory.directoryName\n        }\n\n        if (!File(path).exists()) {\n            File(path).mkdirs()\n        }\n\n        return path\n    }\n\n    @Deprecated(\n        message = \"Should use specific temp file or create a new file in StorageSubdirectory.CACHE instead\",\n        ReplaceWith(\n            \"getOdkDirPath(StorageSubdirectory.CACHE) + File.separator + \\\"tmp.jpg\\\"\"\n        )\n    )\n    fun getTmpImageFilePath(): String {\n        return getOdkDirPath(StorageSubdirectory.CACHE) + File.separator + \"tmp.jpg\"\n    }\n\n    @Deprecated(\n        message = \"Should use specific temp file or create a new file in StorageSubdirectory.CACHE instead\",\n        ReplaceWith(\n            \"getOdkDirPath(StorageSubdirectory.CACHE) + File.separator + \\\"tmp.mp4\\\"\"\n        )\n    )\n    fun getTmpVideoFilePath(): String {\n        return getOdkDirPath(StorageSubdirectory.CACHE) + File.separator + \"tmp.mp4\"\n    }\n\n    override fun create(projectId: String): StoragePaths {\n        return StoragePaths(getProjectRootDirPath(projectId),\n            getOdkDirPath(StorageSubdirectory.FORMS, projectId),\n            getOdkDirPath(StorageSubdirectory.INSTANCES, projectId),\n            getOdkDirPath(StorageSubdirectory.CACHE, projectId),\n            getOdkDirPath(StorageSubdirectory.METADATA, projectId),\n            getOdkDirPath(StorageSubdirectory.SETTINGS, projectId),\n            getOdkDirPath(StorageSubdirectory.LAYERS, projectId)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/storage/StoragePaths.kt",
    "content": "package org.odk.collect.android.storage\n\ndata class StoragePaths(\n    val rootDir: String,\n    val formsDir: String,\n    val instancesDir: String,\n    val cacheDir: String,\n    val metaDir: String,\n    val settingsDir: String,\n    val layersDir: String\n)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/storage/StorageSubdirectory.java",
    "content": "package org.odk.collect.android.storage;\n\npublic enum StorageSubdirectory {\n    FORMS(\"forms\"),\n    INSTANCES(\"instances\"),\n    CACHE(\".cache\"),\n    METADATA(\"metadata\"),\n    LAYERS(\"layers\"),\n    SETTINGS(\"settings\"),\n    PROJECTS(\"projects\"),\n    SHARED_LAYERS(\"layers\");\n\n    private final String directoryName;\n\n    StorageSubdirectory(String directoryName) {\n        this.directoryName = directoryName;\n    }\n\n    public String getDirectoryName() {\n        return directoryName;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/tasks/DownloadFormListTask.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.tasks;\n\nimport android.net.Uri;\nimport android.os.AsyncTask;\n\nimport androidx.core.util.Pair;\n\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.formmanagement.FormsDataService;\nimport org.odk.collect.android.formmanagement.ServerFormDetails;\nimport org.odk.collect.android.formmanagement.ServerFormUseCases;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.injection.config.ProjectDependencyModuleFactory;\nimport org.odk.collect.android.listeners.FormListDownloaderListener;\nimport org.odk.collect.android.projects.ProjectDependencyModule;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.android.utilities.WebCredentialsUtils;\nimport org.odk.collect.forms.FormSource;\nimport org.odk.collect.forms.FormSourceException;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.openrosa.forms.OpenRosaClient;\n\nimport java.util.HashMap;\nimport java.util.List;\n\nimport javax.inject.Inject;\n\n/**\n * Background task for downloading forms from urls or a formlist from a url. We overload this task\n * a bit so that we don't have to keep track of two separate downloading tasks and it simplifies\n * interfaces. If LIST_URL is passed to doInBackground(), we fetch a form list. If a hashmap\n * containing form/url pairs is passed, we download those forms.\n *\n * @author carlhartung\n *\n * @deprecated Server form list should be retrieved from {@link FormsDataService}\n */\n@Deprecated\npublic class DownloadFormListTask extends AsyncTask<Void, String, Pair<List<ServerFormDetails>, FormSourceException>> {\n\n    private final FormsRepository formsRepository;\n    private final FormSource formSource;\n    private final String projectId;\n\n    private FormListDownloaderListener stateListener;\n    private WebCredentialsUtils webCredentialsUtils;\n    private String url;\n    private String username;\n    private String password;\n\n    @Inject\n    FormsDataService formsDataService;\n\n    @Inject\n    ProjectsDataService projectsDataService;\n\n    @Inject\n    ProjectDependencyModuleFactory projectDependencyModuleFactory;\n\n    public DownloadFormListTask() {\n        DaggerUtils.getComponent(Collect.getInstance()).inject(this);\n\n        projectId = projectsDataService.requireCurrentProject().getUuid();\n        ProjectDependencyModule projectDependencyModule = projectDependencyModuleFactory.create(projectId);\n        formsRepository = projectDependencyModule.getFormsRepository();\n        formSource = projectDependencyModule.getFormSource();\n    }\n\n    @Override\n    protected Pair<List<ServerFormDetails>, FormSourceException> doInBackground(Void... values) {\n        formsDataService.refresh(projectId);\n\n        if (webCredentialsUtils != null) {\n            setTemporaryCredentials();\n        }\n\n        List<ServerFormDetails> formList = null;\n        FormSourceException exception = null;\n\n        try {\n            formList = ServerFormUseCases.fetchFormDetails(formsRepository, formSource);\n        } catch (FormSourceException e) {\n            exception = e;\n        } finally {\n            if (webCredentialsUtils != null) {\n                clearTemporaryCredentials();\n            }\n        }\n\n        return new Pair<>(formList, exception);\n    }\n\n    @Override\n    protected void onPostExecute(Pair<List<ServerFormDetails>, FormSourceException> result) {\n        synchronized (this) {\n            if (stateListener != null) {\n                if (result.first != null) {\n                    HashMap<String, ServerFormDetails> detailsHashMap = new HashMap<>();\n                    for (ServerFormDetails details : result.first) {\n                        detailsHashMap.put(details.getFormId(), details);\n                    }\n\n                    stateListener.formListDownloadingComplete(detailsHashMap, result.second);\n                } else {\n                    stateListener.formListDownloadingComplete(null, result.second);\n                }\n            }\n        }\n    }\n\n    public void setDownloaderListener(FormListDownloaderListener sl) {\n        synchronized (this) {\n            stateListener = sl;\n        }\n    }\n\n    public void setAlternateCredentials(WebCredentialsUtils webCredentialsUtils, String url, String username, String password) {\n        this.webCredentialsUtils = webCredentialsUtils;\n        OpenRosaClient openRosaClient = (OpenRosaClient) formSource;\n        openRosaClient.updateWebCredentialsUtils(webCredentialsUtils);\n\n        this.url = url;\n        if (url != null && !url.isEmpty()) {\n            openRosaClient.updateUrl(url);\n        }\n\n        this.username = username;\n        this.password = password;\n    }\n\n    private void setTemporaryCredentials() {\n        if (url != null) {\n            String host = Uri.parse(url).getHost();\n\n            if (host != null) {\n                if (username != null && password != null) {\n                    webCredentialsUtils.saveCredentials(url, username, password);\n                } else {\n                    webCredentialsUtils.clearCredentials(url);\n                }\n            }\n        }\n    }\n\n    private void clearTemporaryCredentials() {\n        if (url != null) {\n            String host = Uri.parse(url).getHost();\n\n            if (host != null) {\n                webCredentialsUtils.clearCredentials(url);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/tasks/DownloadFormsTask.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.tasks;\n\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.os.AsyncTask;\n\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.formmanagement.FormsDataService;\nimport org.odk.collect.android.formmanagement.ServerFormDetails;\nimport org.odk.collect.android.formmanagement.download.FormDownloadException;\nimport org.odk.collect.android.listeners.DownloadFormsTaskListener;\n\nimport java.util.ArrayList;\nimport java.util.Map;\n\n/**\n * Background task for downloading a given list of forms. We assume right now that the forms are\n * coming from the same server that presented the form list, but theoretically that won't always be\n * true.\n *\n * @author msundt\n * @author carlhartung\n *\n * @deprecated Server form should be downloaded using {@link FormsDataService}\n */\npublic class DownloadFormsTask extends\n        AsyncTask<ArrayList<ServerFormDetails>, String, Map<ServerFormDetails, FormDownloadException>> {\n\n    private final String projectId;\n    private final FormsDataService formsDataService;\n    private DownloadFormsTaskListener stateListener;\n\n    public DownloadFormsTask(String projectId, FormsDataService formsDataService) {\n        this.projectId = projectId;\n        this.formsDataService = formsDataService;\n    }\n\n    @Override\n    protected Map<ServerFormDetails, FormDownloadException> doInBackground(ArrayList<ServerFormDetails>... values) {\n        return formsDataService.downloadForms(projectId, values[0], (index, count) -> {\n            ServerFormDetails serverFormDetails = values[0].get(index);\n            String message = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.form_download_progress,\n                    serverFormDetails.getFormName(),\n                    String.valueOf(count),\n                    String.valueOf(serverFormDetails.getManifest().getMediaFiles().size())\n            );\n\n            publishProgress(message, String.valueOf(index), String.valueOf(values[0].size()));\n            return null;\n        }, this::isCancelled);\n    }\n\n    @Override\n    protected void onCancelled(Map<ServerFormDetails, FormDownloadException> formDetailsStringHashMap) {\n        synchronized (this) {\n            if (stateListener != null) {\n                stateListener.formsDownloadingCancelled();\n            }\n        }\n    }\n\n    @Override\n    protected void onPostExecute(Map<ServerFormDetails, FormDownloadException> value) {\n        synchronized (this) {\n            if (stateListener != null) {\n                stateListener.formsDownloadingComplete(value);\n            }\n        }\n    }\n\n    @Override\n    protected void onProgressUpdate(String... values) {\n        synchronized (this) {\n            if (stateListener != null) {\n                // update progress and total\n                stateListener.progressUpdate(values[0],\n                        Integer.parseInt(values[1]),\n                        Integer.parseInt(values[2]));\n            }\n        }\n\n    }\n\n    public void setDownloaderListener(DownloadFormsTaskListener sl) {\n        synchronized (this) {\n            stateListener = sl;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/tasks/FormLoaderTask.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.tasks;\n\nimport static org.odk.collect.android.utilities.FormUtils.setupReferenceManagerForForm;\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.content.Intent;\nimport android.database.Cursor;\nimport android.database.SQLException;\nimport android.net.Uri;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.opencsv.CSVReader;\nimport com.opencsv.exceptions.CsvValidationException;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.instance.InstanceInitializationFactory;\nimport org.javarosa.core.model.instance.TreeElement;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.javarosa.core.model.instance.utils.DefaultAnswerResolver;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryController;\nimport org.javarosa.xform.parse.XFormParser;\nimport org.javarosa.xform.util.XFormUtils;\nimport org.javarosa.xpath.XPathTypeMismatchException;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.dynamicpreload.ExternalAnswerResolver;\nimport org.odk.collect.android.dynamicpreload.ExternalDataManager;\nimport org.odk.collect.android.dynamicpreload.ExternalDataUseCases;\nimport org.odk.collect.android.external.FormsContract;\nimport org.odk.collect.android.external.InstancesContract;\nimport org.odk.collect.android.fastexternalitemset.ItemsetDbAdapter;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.javarosawrapper.JavaRosaFormController;\nimport org.odk.collect.android.listeners.FormLoaderListener;\nimport org.odk.collect.android.utilities.ContentUriHelper;\nimport org.odk.collect.android.utilities.ExternalizableFormDefCache;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\nimport org.odk.collect.android.utilities.ZipUtils;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.async.SchedulerAsyncTaskMimic;\nimport org.odk.collect.entities.javarosa.spec.UnrecognizedEntityVersionException;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.savepoints.Savepoint;\nimport org.odk.collect.forms.savepoints.SavepointsRepository;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.File;\nimport java.io.FileFilter;\nimport java.io.FileReader;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\n\nimport timber.log.Timber;\n\n/**\n * Background task for loading a form.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n * @author Yaw Anokwa (yanokwa@gmail.com)\n */\npublic class FormLoaderTask extends SchedulerAsyncTaskMimic<Void, String, FormLoaderTask.FECWrapper> {\n    private static final String ITEMSETS_CSV = \"itemsets.csv\";\n\n    private FormLoaderListener stateListener;\n    private String errorMsg;\n    private String warningMsg;\n    private String instancePath;\n    private final Uri uri;\n    private final String uriMimeType;\n    private final String xpath;\n    private final String waitingXPath;\n    private FormEntryControllerFactory formEntryControllerFactory;\n    private boolean pendingActivityResult;\n    private int requestCode;\n    private int resultCode;\n    private Intent intent;\n    private ExternalDataManager externalDataManager;\n    private FormDef formDef;\n    private Form form;\n    private Instance instance;\n    private Savepoint savepoint;\n    private final SavepointsRepository savepointsRepository;\n\n    @Override\n    protected void onPreExecute() {\n\n    }\n\n    public String getInstancePath() {\n        return instancePath;\n    }\n\n    public Form getForm() {\n        return form;\n    }\n\n    public Instance getInstance() {\n        return instance;\n    }\n\n    public static class FECWrapper {\n        FormController controller;\n        boolean usedSavepoint;\n\n        protected FECWrapper(FormController controller, boolean usedSavepoint) {\n            this.controller = controller;\n            this.usedSavepoint = usedSavepoint;\n        }\n\n        public FormController getController() {\n            return controller;\n        }\n\n        protected boolean hasUsedSavepoint() {\n            return usedSavepoint;\n        }\n\n        protected void free() {\n            controller = null;\n        }\n    }\n\n    FECWrapper data;\n\n    public FormLoaderTask(Uri uri, String uriMimeType, String xpath, String waitingXPath,\n                          FormEntryControllerFactory formEntryControllerFactory, Scheduler scheduler,\n                          SavepointsRepository savepointsRepository) {\n        super(scheduler);\n        this.uri = uri;\n        this.uriMimeType = uriMimeType;\n        this.xpath = xpath;\n        this.waitingXPath = waitingXPath;\n        this.formEntryControllerFactory = formEntryControllerFactory;\n        this.savepointsRepository = savepointsRepository;\n    }\n\n    /**\n     * Initialize {@link FormEntryController} with {@link FormDef} from binary or\n     * from XML. If given an instance, it will be used to fill the {@link FormDef}.\n     */\n    @Override\n    protected FECWrapper doInBackground(Void... ignored) {\n        errorMsg = null;\n\n        if (uriMimeType != null && uriMimeType.equals(InstancesContract.CONTENT_ITEM_TYPE)) {\n            instance = new InstancesRepositoryProvider(Collect.getInstance()).create().get(ContentUriHelper.getIdFromUri(uri));\n            instancePath = instance.getInstanceFilePath();\n\n            List<Form> candidateForms = new FormsRepositoryProvider(Collect.getInstance()).create().getAllByFormIdAndVersion(instance.getFormId(), instance.getFormVersion());\n\n            form = candidateForms.get(0);\n            savepoint = savepointsRepository.get(form.getDbId(), instance.getDbId());\n        } else if (uriMimeType != null && uriMimeType.equals(FormsContract.CONTENT_ITEM_TYPE)) {\n            form = new FormsRepositoryProvider(Collect.getInstance()).create().get(ContentUriHelper.getIdFromUri(uri));\n            if (form == null) {\n                Timber.e(new Error(\"form is null\"));\n                errorMsg = \"This form no longer exists, please email support@getodk.org with a description of what you were doing when this happened.\";\n                return null;\n            }\n\n            savepoint = savepointsRepository.get(form.getDbId(), null);\n            instancePath = savepoint != null ? savepoint.getInstanceFilePath() : null;\n        }\n\n        if (form.getFormFilePath() == null) {\n            Timber.e(new Error(\"formPath is null\"));\n            errorMsg = \"formPath is null, please email support@getodk.org with a description of what you were doing when this happened.\";\n            return null;\n        }\n\n        final File formXml = new File(form.getFormFilePath());\n        final File formMediaDir = FileUtils.getFormMediaDir(formXml);\n\n        unzipMediaFiles(formMediaDir);\n        setupReferenceManagerForForm(ReferenceManager.instance(), formMediaDir);\n\n        logFormDetails(formXml, formMediaDir);\n\n        FormDef formDef = null;\n        try {\n            formDef = createFormDefFromCacheOrXml(form.getFormFilePath(), formXml);\n        } catch (StackOverflowError e) {\n            Timber.e(e);\n            errorMsg = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.too_complex_form);\n        } catch (UnrecognizedEntityVersionException e) {\n            errorMsg = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.unrecognized_entity_version, e.getEntityVersion());\n        } catch (Exception e) {\n            Timber.w(e);\n            errorMsg = \"An unknown error has occurred. Please ask your project leadership to email support@getodk.org with information about this form.\";\n            errorMsg += \"\\n\\n\" + e.getMessage();\n        }\n\n        if (errorMsg != null || formDef == null) {\n            Timber.w(\"No exception loading form but errorMsg set\");\n            return null;\n        }\n\n        externalDataManager = Collect.getInstance().getExternalDataManager();\n\n        try {\n            ExternalDataUseCases.create(formDef, formMediaDir, this::isCancelled, progress -> {\n                publishProgress(progress.apply(Collect.getInstance().getResources()));\n            });\n        } catch (Exception e) {\n            Timber.e(e, \"Exception thrown while loading external data\");\n            errorMsg = e.getMessage();\n            return null;\n        }\n\n        if (isCancelled()) {\n            // that means that the user has cancelled, so no need to go further\n            return null;\n        }\n\n        // create FormEntryController from formdef\n        final FormEntryController fec = formEntryControllerFactory.create(formDef, formMediaDir, instance);\n\n        boolean usedSavepoint = false;\n\n        try {\n            Timber.i(\"Initializing form.\");\n            final long start = System.currentTimeMillis();\n            usedSavepoint = initializeForm(formDef, fec);\n            Timber.i(\"Form initialized in %.3f seconds.\", (System.currentTimeMillis() - start) / 1000F);\n        } catch (IOException | RuntimeException e) {\n            Timber.e(e);\n            if (e.getCause() instanceof XPathTypeMismatchException) {\n                // this is a case of\n                // https://bitbucket.org/m\n                // .sundt/javarosa/commits/e5d344783e7968877402bcee11828fa55fac69de\n                // the data are imported, the survey will be unusable\n                // but we should give the option to the user to edit the form\n                // otherwise the survey will be TOTALLY inaccessible.\n                Timber.w(\"We have a syntactically correct instance, but the data threw an exception inside JR. We should allow editing.\");\n            } else {\n                errorMsg = e.getMessage();\n                return null;\n            }\n        }\n\n        processItemSets(formMediaDir);\n\n        final FormController fc = new JavaRosaFormController(formMediaDir, fec, instancePath == null ? null\n                : new File(instancePath));\n        if (xpath != null) {\n            // we are resuming after having terminated -- set index to this\n            // position...\n            FormIndex idx = fc.getIndexFromXPath(xpath);\n            if (idx != null) {\n                fc.jumpToIndex(idx);\n            }\n        }\n        if (waitingXPath != null) {\n            FormIndex idx = fc.getIndexFromXPath(waitingXPath);\n            if (idx != null) {\n                fc.setIndexWaitingForData(idx);\n            }\n        }\n        data = new FECWrapper(fc, usedSavepoint);\n        return data;\n    }\n\n    private void logFormDetails(File formFile, File formMediaDir) {\n        long formFileBytesSize = formFile.length();\n\n        List<String> csvFileBytesSizes = new ArrayList<>();\n        File[] files = formMediaDir.listFiles();\n        if (files != null) {\n            for (File mediaFile : files) {\n                if (mediaFile.getName().endsWith(\".csv\")) {\n                    csvFileBytesSizes.add(mediaFile.length() + \"B\");\n                }\n            }\n        }\n\n        if (csvFileBytesSizes.isEmpty()) {\n            Timber.w(\n                    \"Attempting to load from %s, file is %dB\",\n                    formFile.getAbsolutePath(),\n                    formFileBytesSize\n            );\n        } else {\n            Timber.w(\n                    \"Attempting to load from %s, file is %dB and has CSV files %s\",\n                    formFile.getAbsolutePath(),\n                    formFileBytesSize,\n                    String.join(\", \", csvFileBytesSizes)\n            );\n        }\n    }\n\n    private static void unzipMediaFiles(File formMediaDir) {\n        File[] zipFiles = formMediaDir.listFiles(new FileFilter() {\n            @Override\n            public boolean accept(File file) {\n                return file.getName().toLowerCase(Locale.US).endsWith(\".zip\");\n            }\n        });\n\n        if (zipFiles != null) {\n            ZipUtils.unzip(zipFiles);\n            for (File zipFile : zipFiles) {\n                boolean deleted = zipFile.delete();\n                if (!deleted) {\n                    Timber.w(\"Cannot delete %s. It will be re-unzipped next time. :(\", zipFile.toString());\n                }\n            }\n        }\n    }\n\n    private FormDef createFormDefFromCacheOrXml(String formPath, File formXml) throws XFormParser.ParseException {\n        publishProgress(\n                getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.survey_loading_reading_form_message));\n\n        final FormDef formDefFromCache = new ExternalizableFormDefCache().readCache(formXml);\n        if (formDefFromCache != null) {\n            return formDefFromCache;\n        }\n\n        // no binary, read from xml\n        final long start = System.currentTimeMillis();\n        String lastSavedSrc = FileUtils.getOrCreateLastSavedSrc(formXml);\n        FormDef formDefFromXml = XFormUtils.getFormFromFormXml(formPath, lastSavedSrc);\n        if (formDefFromXml == null) {\n            Timber.w(\"Error reading XForm file\");\n            errorMsg = \"Error reading XForm file\";\n        } else {\n            Timber.i(\"Loaded in %.3f seconds.\",\n                    (System.currentTimeMillis() - start) / 1000F);\n            formDef = formDefFromXml;\n\n            try {\n                new ExternalizableFormDefCache().writeCache(formDef, formXml.getPath());\n            } catch (IOException e) {\n                Timber.e(e);\n            }\n\n            return formDefFromXml;\n        }\n\n        return null;\n    }\n\n    private void processItemSets(File formMediaDir) {\n        // for itemsets.csv, we only check to see if the itemset file has been\n        // updated\n        final File csv = new File(formMediaDir.getAbsolutePath() + \"/\" + ITEMSETS_CSV);\n        String csvmd5;\n        if (csv.exists()) {\n            csvmd5 = Md5.getMd5Hash(csv);\n            boolean readFile = false;\n            final ItemsetDbAdapter ida = new ItemsetDbAdapter();\n            ida.open();\n            // get the database entry (if exists) for this itemsets.csv, based\n            // on the path\n            final Cursor c = ida.getItemsets(csv.getAbsolutePath());\n            if (c != null) {\n                if (c.getCount() == 1) {\n                    c.moveToFirst(); // should be only one, ever, if any\n                    final String oldmd5 = c.getString(c.getColumnIndex(\"hash\"));\n                    if (oldmd5.equals(csvmd5)) {\n                        // they're equal, do nothing\n                    } else {\n                        // the csv has been updated, delete the old entries\n                        ida.dropTable(ItemsetDbAdapter.getMd5FromString(csv.getAbsolutePath()),\n                                csv.getAbsolutePath());\n                        // and read the new\n                        readFile = true;\n                    }\n                } else {\n                    // new csv, add it\n                    readFile = true;\n                }\n                c.close();\n            }\n            ida.close();\n            if (readFile) {\n                readCSV(csv, csvmd5, ItemsetDbAdapter.getMd5FromString(csv.getAbsolutePath()));\n            }\n        }\n    }\n\n    private boolean initializeForm(FormDef formDef, FormEntryController fec) throws IOException {\n        final InstanceInitializationFactory instanceInit = new InstanceInitializationFactory();\n        boolean usedSavepoint = false;\n\n        if (instancePath != null) {\n            File instanceXml = new File(instancePath);\n\n            if (savepoint != null) {\n                final File savepointFile = new File(savepoint.getSavepointFilePath());\n                usedSavepoint = true;\n                instanceXml = savepointFile;\n                Timber.w(\"Loading instance from savepoint file: %s\", savepointFile.getAbsolutePath());\n            }\n\n            if (instanceXml.exists()) {\n                // This order is important. Import data, then initialize.\n                try {\n                    Timber.i(\"Importing data\");\n                    publishProgress(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.survey_loading_reading_data_message));\n                    importData(instanceXml, fec);\n                    formDef.initialize(false, instanceInit);\n                } catch (IOException | RuntimeException e) {\n                    // Skip a savepoint file that is corrupted or 0-sized\n                    if (usedSavepoint && !(e.getCause() instanceof XPathTypeMismatchException)) {\n                        usedSavepoint = false;\n                        instancePath = null;\n                        formDef.initialize(true, instanceInit);\n                        Timber.e(e, \"Bad savepoint\");\n                    } else {\n                        // The saved instance is corrupted.\n                        Timber.e(e, \"Corrupt saved instance\");\n                        throw new RuntimeException(\"An unknown error has occurred. Please ask your project leadership to email support@getodk.org with information about this form.\"\n                            + \"\\n\\n\" + e.getMessage());\n                    }\n                }\n            } else {\n                formDef.initialize(true, instanceInit);\n            }\n        } else {\n            formDef.initialize(true, instanceInit);\n        }\n        return usedSavepoint;\n    }\n\n    @Override\n    protected void onProgressUpdate(String... values) {\n        synchronized (this) {\n            if (stateListener != null && values != null) {\n                if (values.length == 1) {\n                    stateListener.onProgressStep(values[0]);\n                }\n            }\n        }\n    }\n\n    // Copied from XFormParser.loadXmlInstance in order to set ExternalAnswerResolver for search()\n    public static void importData(File instanceFile, FormEntryController fec) throws IOException, RuntimeException {\n        // convert files into a byte array\n        byte[] fileBytes = org.apache.commons.io.FileUtils.readFileToByteArray(instanceFile);\n\n        // get the root of the saved and template instances\n        TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot();\n        TreeElement templateRoot = fec.getModel().getForm().getInstance().getRoot().deepCopy(true);\n\n        // weak check for matching forms\n        if (!savedRoot.getName().equals(templateRoot.getName()) || savedRoot.getMult() != 0) {\n            Timber.e(new Error(\"Saved form instance does not match template form definition\"));\n            return;\n        }\n\n        // populate the data model\n        TreeReference tr = TreeReference.rootRef();\n        tr.add(templateRoot.getName(), TreeReference.INDEX_UNBOUND);\n\n        // Here we set the Collect's implementation of the IAnswerResolver.\n        // We set it back to the default after select choices have been populated.\n        XFormParser.setAnswerResolver(new ExternalAnswerResolver());\n        templateRoot.populate(savedRoot, fec.getModel().getForm());\n        XFormParser.setAnswerResolver(new DefaultAnswerResolver());\n\n        // FormInstanceParser.parseInstance is responsible for initial creation of instances. It explicitly sets the\n        // main instance name to null so we force this again on deserialization because some code paths rely on the main\n        // instance not having a name. Must be before the call on setRoot because setRoot also sets the root's name.\n        fec.getModel().getForm().getInstance().setName(null);\n\n        // populated model to current form\n        fec.getModel().getForm().getInstance().setRoot(templateRoot);\n\n        // fix any language issues\n        // :\n        // http://bitbucket.org/javarosa/main/issue/5/itext-n-appearing-in-restored-instances\n        if (fec.getModel().getLanguages() != null) {\n            fec.getModel().getForm()\n                    .localeChanged(fec.getModel().getLanguage(),\n                            fec.getModel().getForm().getLocalizer());\n        }\n        Timber.i(\"Done importing data\");\n    }\n\n    @Override\n    protected void onCancelled() {\n        if (externalDataManager != null) {\n            externalDataManager.close();\n        }\n    }\n\n    @Override\n    protected void onPostExecute(FECWrapper wrapper) {\n        synchronized (this) {\n            try {\n                if (stateListener != null) {\n                    if (wrapper == null) {\n                        stateListener.loadingError(errorMsg);\n                    } else {\n                        stateListener.loadingComplete(this, formDef, warningMsg);\n                    }\n                }\n            } catch (Exception e) {\n                Timber.e(e);\n            }\n        }\n    }\n\n    public void setFormLoaderListener(FormLoaderListener sl) {\n        synchronized (this) {\n            stateListener = sl;\n        }\n    }\n\n    public FormController getFormController() {\n        return (data != null) ? data.getController() : null;\n    }\n\n    public boolean hasUsedSavepoint() {\n        return (data != null) && data.hasUsedSavepoint();\n    }\n\n    public void destroy() {\n        if (data != null) {\n            data.free();\n            data = null;\n        }\n    }\n\n    public boolean hasPendingActivityResult() {\n        return pendingActivityResult;\n    }\n\n    public int getRequestCode() {\n        return requestCode;\n    }\n\n    public int getResultCode() {\n        return resultCode;\n    }\n\n    public Intent getIntent() {\n        return intent;\n    }\n\n    public void setActivityResult(int requestCode, int resultCode, Intent intent) {\n        this.pendingActivityResult = true;\n        this.requestCode = requestCode;\n        this.resultCode = resultCode;\n        this.intent = intent;\n    }\n\n    private void readCSV(File csv, String formHash, String pathHash) {\n\n        CSVReader reader;\n        ItemsetDbAdapter ida = new ItemsetDbAdapter();\n        ida.open();\n        boolean withinTransaction = false;\n\n        try {\n            reader = new CSVReader(new FileReader(csv));\n\n            String[] nextLine;\n            String[] columnHeaders = null;\n            int lineNumber = 0;\n            while ((nextLine = reader.readNext()) != null) {\n                lineNumber++;\n                if (lineNumber == 1) {\n                    // first line of csv is column headers\n                    columnHeaders = nextLine;\n                    ida.createTable(formHash, pathHash, columnHeaders,\n                            csv.getAbsolutePath());\n                    continue;\n                }\n                // add the rest of the lines to the specified database\n                // nextLine[] is an array of values from the line\n                // System.out.println(nextLine[4] + \"etc...\");\n                if (lineNumber == 2) {\n                    // start a transaction for the inserts\n                    withinTransaction = true;\n                    ida.beginTransaction();\n                }\n                ida.addRow(pathHash, columnHeaders, nextLine);\n\n            }\n        } catch (IOException | SQLException | CsvValidationException e) {\n            warningMsg = e.getMessage();\n        } finally {\n            if (withinTransaction) {\n                ida.commit();\n            }\n            ida.close();\n        }\n    }\n\n    public FormDef getFormDef() {\n        return formDef;\n    }\n\n    public interface FormEntryControllerFactory {\n        FormEntryController create(@NonNull FormDef formDef, @NonNull File formMediaDir, @Nullable Instance instance);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/tasks/InstanceUploaderTask.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.tasks;\n\nimport static org.odk.collect.android.analytics.AnalyticsEvents.SUBMISSION;\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.net.Uri;\nimport android.os.AsyncTask;\n\nimport org.odk.collect.analytics.Analytics;\nimport org.odk.collect.android.analytics.AnalyticsEvents;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.instancemanagement.InstanceDeleter;\nimport org.odk.collect.android.instancemanagement.InstancesDataService;\nimport org.odk.collect.android.listeners.InstanceUploaderListener;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.android.upload.FormUploadAuthRequestedException;\nimport org.odk.collect.android.upload.FormUploadException;\nimport org.odk.collect.android.upload.InstanceServerUploader;\nimport org.odk.collect.android.utilities.InstanceAutoDeleteChecker;\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider;\nimport org.odk.collect.android.utilities.WebCredentialsUtils;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.metadata.PropertyManager;\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.keys.ProjectKeys;\n\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport javax.inject.Inject;\n\n/**\n * Background task for uploading completed forms.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n */\npublic class InstanceUploaderTask extends AsyncTask<Long, Integer, InstanceUploaderTask.Outcome> {\n    @Inject\n    OpenRosaHttpInterface httpInterface;\n\n    @Inject\n    WebCredentialsUtils webCredentialsUtils;\n\n    @Inject\n    PropertyManager propertyManager;\n\n    @Inject\n    InstancesDataService instancesDataService;\n\n    @Inject\n    ProjectsDataService projectsDataService;\n\n    // Custom submission URL, username and password that can be sent via intent extras by external\n    // applications\n    private String completeDestinationUrl;\n    private String referrer;\n    private String customUsername;\n    private String customPassword;\n    private InstancesRepository instancesRepository;\n    private FormsRepository formsRepository;\n    private SettingsProvider settingsProvider;\n    private InstanceUploaderListener stateListener;\n    private Boolean deleteInstanceAfterSubmission;\n\n    public InstanceUploaderTask() {\n        Collect.getInstance().getComponent().inject(this);\n    }\n\n    @Override\n    public Outcome doInBackground(Long... instanceIdsToUpload) {\n        Outcome outcome = new Outcome();\n\n        InstanceServerUploader uploader = new InstanceServerUploader(httpInterface, webCredentialsUtils, settingsProvider.getUnprotectedSettings(), instancesRepository);\n        List<Instance> instancesToUpload = uploader\n                .getInstancesFromIds(instanceIdsToUpload)\n                .stream()\n                .sorted(Comparator.comparing(Instance::getFinalizationDate))\n                .collect(Collectors.toList());\n\n        String deviceId = propertyManager.getSingularProperty(PropertyManager.PROPMGR_DEVICE_ID);\n\n        for (int i = 0; i < instancesToUpload.size(); i++) {\n            if (isCancelled()) {\n                return outcome;\n            }\n            Instance instance = instancesToUpload.get(i);\n\n            publishProgress(i + 1, instancesToUpload.size());\n\n            if (completeDestinationUrl != null) {\n                Analytics.log(AnalyticsEvents.INSTANCE_UPLOAD_CUSTOM_SERVER, \"label\", referrer != null ? referrer : \"\");\n            }\n\n            try {\n                String destinationUrl = uploader.getUrlToSubmitTo(instance, deviceId, completeDestinationUrl, null);\n                String customMessage = uploader.uploadOneSubmission(instance, destinationUrl);\n                outcome.messagesByInstanceId.put(instance.getDbId().toString(),\n                        customMessage != null ? customMessage : getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.success));\n\n                Analytics.log(SUBMISSION, \"HTTP\", Collect.getFormIdentifierHash(instance.getFormId(), instance.getFormVersion()));\n            } catch (FormUploadAuthRequestedException e) {\n                outcome.authRequestingServer = e.getAuthRequestingServer();\n                // Don't add the instance that caused an auth request to the map because we want to\n                // retry. Items present in the map are considered already attempted and won't be\n                // retried.\n            } catch (FormUploadException e) {\n                outcome.messagesByInstanceId.put(instance.getDbId().toString(),\n                        e.getMessage());\n            }\n        }\n\n        // Delete instances that were successfully sent and that need to be deleted\n        // either because app-level auto-delete is enabled or because the form\n        // specifies it.\n        Set<String> instanceIds = outcome.messagesByInstanceId.keySet();\n\n        boolean isFormAutoDeleteOptionEnabled;\n\n        // The custom configuration from the third party app overrides\n        // the app preferences set for delete after submission\n        if (deleteInstanceAfterSubmission != null) {\n            isFormAutoDeleteOptionEnabled = deleteInstanceAfterSubmission;\n        } else {\n            isFormAutoDeleteOptionEnabled = settingsProvider.getUnprotectedSettings().getBoolean(ProjectKeys.KEY_DELETE_AFTER_SEND);\n        }\n\n        Stream<Instance> instancesToDelete = instanceIds.stream()\n                .map(id -> new InstancesRepositoryProvider(Collect.getInstance()).create().get(Long.parseLong(id)))\n                .filter(instance -> instance.getStatus().equals(Instance.STATUS_SUBMITTED))\n                .filter(instance -> InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, isFormAutoDeleteOptionEnabled, instance));\n\n        InstanceDeleter instanceDeleter = new InstanceDeleter(instancesRepository, formsRepository);\n        instanceDeleter.delete(instancesToDelete.map(Instance::getDbId).toArray(Long[]::new));\n\n        instancesDataService.update(projectsDataService.requireCurrentProject().getUuid());\n        return outcome;\n    }\n\n    @Override\n    protected void onProgressUpdate(Integer... values) {\n        synchronized (this) {\n            if (stateListener != null) {\n                stateListener.progressUpdate(values[0], values[1]);\n            }\n        }\n    }\n\n    @Override\n    protected void onPostExecute(Outcome outcome) {\n        synchronized (this) {\n            if (outcome != null && stateListener != null) {\n                if (outcome.authRequestingServer != null) {\n                    stateListener.authRequest(outcome.authRequestingServer, outcome.messagesByInstanceId);\n                } else {\n                    stateListener.uploadingComplete(outcome.messagesByInstanceId);\n                }\n            }\n        }\n\n        // Clear temp credentials\n        clearTemporaryCredentials();\n    }\n\n    @Override\n    protected void onCancelled() {\n        clearTemporaryCredentials();\n    }\n\n    public void setUploaderListener(InstanceUploaderListener sl) {\n        synchronized (this) {\n            stateListener = sl;\n        }\n    }\n\n    public void setDeleteInstanceAfterSubmission(Boolean deleteInstanceAfterSubmission) {\n        this.deleteInstanceAfterSubmission = deleteInstanceAfterSubmission;\n    }\n\n    public void setRepositories(InstancesRepository instancesRepository, FormsRepository formsRepository, SettingsProvider settingsProvider) {\n        this.instancesRepository = instancesRepository;\n        this.formsRepository = formsRepository;\n        this.settingsProvider = settingsProvider;\n    }\n\n    public void setCompleteDestinationUrl(String completeDestinationUrl, String referrer, boolean clearPreviousConfig) {\n        this.completeDestinationUrl = completeDestinationUrl;\n        this.referrer = referrer;\n        if (clearPreviousConfig) {\n            setTemporaryCredentials();\n        }\n    }\n\n    public void setCustomUsername(String customUsername) {\n        this.customUsername = customUsername;\n        setTemporaryCredentials();\n    }\n\n    public void setCustomPassword(String customPassword) {\n        this.customPassword = customPassword;\n        setTemporaryCredentials();\n    }\n\n    private void setTemporaryCredentials() {\n        if (customUsername != null && customPassword != null) {\n            webCredentialsUtils.saveCredentials(completeDestinationUrl, customUsername, customPassword);\n        } else {\n            // In the case for anonymous logins, clear the previous credentials for that host\n            webCredentialsUtils.clearCredentials(completeDestinationUrl);\n        }\n    }\n\n    private void clearTemporaryCredentials() {\n        if (customUsername != null && customPassword != null) {\n            webCredentialsUtils.clearCredentials(completeDestinationUrl);\n        }\n    }\n\n    /**\n     * Represents the results of a submission attempt triggered by explicit user action (as opposed\n     * to auto-send). A submission attempt can include finalized forms going to several different\n     * servers because the app-level server configuration can be overridden by the blank form.\n     * <p>\n     * The user-facing message that describes the result of a submission attempt for each specific\n     * finalized form is written messages to {@link #messagesByInstanceId}. In the case of an\n     * authentication request from the server, {@link #authRequestingServer} is set instead.\n     */\n    public static class Outcome {\n        /**\n         * The URI for the server that requested authentication when the latest finalized form was\n         * attempted to be sent. This URI may not match the server specified in the app settings or\n         * the blank form because there could have been a redirect. It is included in the Outcome so\n         * that it can be shown to the user so s/he will know where the auth request came from.\n         * <p>\n         * When this field is set, the overall submission attempt is halted so that the user can be\n         * asked for credentials. Once credentials are provided, the submission attempt resumes.\n         */\n        public Uri authRequestingServer;\n\n\n        /**\n         * Map of database IDs for finalized forms to the user-facing status message for the latest\n         * submission attempt. Currently this can be either a localized message in the case of a\n         * common status or an English message in the case of a rare status that is needed for\n         * developer troubleshooting.\n         * <p>\n         * The keys in the map are also used to identify filled forms that were part of the ongoing\n         * submission attempt and don't need to be retried in the case of an authentication request.\n         * See {@link #authRequestingServer}.\n         * <p>\n         * TODO: Consider mapping to something machine-readable like a message ID or status ID\n         * instead of a mix of localized and non-localized user-facing strings.\n         */\n        public HashMap<String, String> messagesByInstanceId = new HashMap<>();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/tasks/MediaLoadingTask.java",
    "content": "package org.odk.collect.android.tasks;\n\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_IMAGE_SIZE;\n\nimport android.net.Uri;\nimport android.os.AsyncTask;\n\nimport org.odk.collect.android.activities.FormFillingActivity;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.utilities.ContentUriHelper;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.ImageCompressionController;\nimport org.odk.collect.android.widgets.BaseImageWidget;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\nimport org.odk.collect.settings.SettingsProvider;\n\nimport java.io.File;\nimport java.lang.ref.WeakReference;\n\nimport javax.inject.Inject;\n\npublic class MediaLoadingTask extends AsyncTask<Uri, Void, File> {\n\n    private final File instanceFile;\n    @Inject\n    SettingsProvider settingsProvider;\n\n    @Inject\n    ImageCompressionController imageCompressionController;\n\n    private WeakReference<FormFillingActivity> formFillingActivity;\n\n    public MediaLoadingTask(FormFillingActivity formFillingActivity, File instanceFile) {\n        this.instanceFile = instanceFile;\n        onAttach(formFillingActivity);\n    }\n\n    public void onAttach(FormFillingActivity formFillingActivity) {\n        this.formFillingActivity = new WeakReference<>(formFillingActivity);\n        DaggerUtils.getComponent(this.formFillingActivity.get()).inject(this);\n    }\n\n    @Override\n    protected File doInBackground(Uri... uris) {\n        if (instanceFile != null) {\n            String extension = ContentUriHelper.getFileExtensionFromUri(uris[0]);\n\n            File newFile = FileUtils.createDestinationMediaFile(instanceFile.getParent(), extension);\n            FileUtils.saveAnswerFileFromUri(uris[0], newFile, Collect.getInstance());\n            QuestionWidget questionWidget = formFillingActivity.get().getWidgetWaitingForBinaryData();\n\n            // apply image conversion if the widget is an image widget\n            if (questionWidget instanceof BaseImageWidget) {\n                String imageSizeMode = settingsProvider.getUnprotectedSettings().getString(KEY_IMAGE_SIZE);\n                imageCompressionController.execute(newFile.getPath(), questionWidget, formFillingActivity.get(), imageSizeMode);\n            }\n            return newFile;\n        }\n        return null;\n    }\n\n    @Override\n    protected void onPostExecute(File result) {\n        FormFillingActivity activity = this.formFillingActivity.get();\n        DialogFragmentUtils.dismissDialog(FormFillingActivity.TAG_PROGRESS_DIALOG_MEDIA_LOADING, activity.getSupportFragmentManager());\n        activity.setWidgetData(result);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/tasks/ProgressNotifier.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.tasks;\n\n/**\n * Author: Meletis Margaritis\n * Date: 19/10/2013\n * Time: 12:17 μμ\n */\npublic interface ProgressNotifier {\n\n    void onProgressStep(String stepMessage);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormIndexTask.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.tasks;\n\nimport android.os.AsyncTask;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.odk.collect.android.javarosawrapper.FormController;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\n\nimport timber.log.Timber;\n\npublic class SaveFormIndexTask extends AsyncTask<Void, Void, String> {\n\n    private final SaveFormIndexListener listener;\n    private final FormIndex formIndex;\n    private final File instanceFile;\n\n    public interface SaveFormIndexListener {\n        void onSaveFormIndexError(String errorMessage);\n    }\n\n    public SaveFormIndexTask(SaveFormIndexListener listener, FormIndex formIndex, File instanceFile) {\n        this.listener = listener;\n        this.formIndex = formIndex;\n        this.instanceFile = instanceFile;\n    }\n\n    @Override\n    protected String doInBackground(Void... params) {\n        long start = System.currentTimeMillis();\n\n        try {\n            File tempFormIndexFile = SaveFormToDisk.getFormIndexFile(instanceFile.getName());\n            exportFormIndexToFile(formIndex, tempFormIndexFile);\n\n            long end = System.currentTimeMillis();\n            Timber.i(\"SaveFormIndex ms: %s to %s\", Long.toString(end - start), tempFormIndexFile.toString());\n\n            return null;\n        } catch (Exception e) {\n            String msg = e.getMessage();\n            Timber.e(e);\n            return msg;\n        }\n    }\n\n    @Override\n    protected void onPostExecute(String errorMessage) {\n        super.onPostExecute(errorMessage);\n\n        if (listener != null && errorMessage != null) {\n            listener.onSaveFormIndexError(errorMessage);\n        }\n    }\n\n    public static void exportFormIndexToFile(FormIndex formIndex, File savepointIndexFile) {\n        try {\n            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(savepointIndexFile));\n            oos.writeObject(formIndex);\n            oos.flush();\n            oos.close();\n        } catch (Exception e) {\n            Timber.e(e);\n        }\n    }\n\n    public static FormIndex loadFormIndexFromFile(FormController formController) {\n        FormIndex formIndex = null;\n        try {\n            String instanceName = formController\n                    .getInstanceFile()\n                    .getName();\n            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SaveFormToDisk.getFormIndexFile(instanceName)));\n            formIndex = (FormIndex) ois.readObject();\n            ois.close();\n        } catch (Exception e) {\n            Timber.e(e);\n        }\n\n        return formIndex;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.tasks;\n\nimport static org.odk.collect.android.analytics.AnalyticsEvents.ENCRYPT_SUBMISSION;\nimport static org.odk.collect.geo.GeoUtils.toMapPoint;\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.content.ContentValues;\nimport android.net.Uri;\nimport android.util.Pair;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.condition.EvaluationContext;\nimport org.javarosa.core.model.data.GeoPointData;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.instance.FormInstance;\nimport org.javarosa.core.model.instance.TreeElement;\nimport org.javarosa.core.services.transport.payload.ByteArrayPayload;\nimport org.javarosa.xpath.XPathException;\nimport org.javarosa.xpath.XPathNodeset;\nimport org.javarosa.xpath.XPathParseTool;\nimport org.javarosa.xpath.expr.XPathExpression;\nimport org.javarosa.xpath.parser.XPathSyntaxException;\nimport org.jetbrains.annotations.Nullable;\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\nimport org.odk.collect.analytics.Analytics;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns;\nimport org.odk.collect.android.exception.EncryptionException;\nimport org.odk.collect.android.external.InstancesContract;\nimport org.odk.collect.android.formentry.FormEntryUseCases;\nimport org.odk.collect.android.formentry.saving.FormSaver;\nimport org.odk.collect.android.javarosawrapper.FailedValidationResult;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.javarosawrapper.ValidationResult;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.utilities.ContentUriHelper;\nimport org.odk.collect.android.utilities.EncryptionUtils;\nimport org.odk.collect.android.utilities.EncryptionUtils.EncryptedFormInformation;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.entities.storage.EntitiesRepository;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.maps.MapPoint;\nimport org.odk.collect.shared.DebugLogger;\nimport org.odk.collect.shared.files.FileExt;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.util.ArrayList;\n\nimport timber.log.Timber;\n\n/**\n * Background task for loading a form.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n * @author Yaw Anokwa (yanokwa@gmail.com)\n */\npublic class SaveFormToDisk {\n\n    private final boolean saveAndExit;\n    private final boolean shouldFinalize;\n    private final FormController formController;\n    private final MediaUtils mediaUtils;\n    private final InstancesRepository instancesRepository;\n    private final DebugLogger debugLogger;\n    private Uri uri;\n    private String instanceName;\n    private final ArrayList<String> tempFiles;\n    private final String currentProjectId;\n    private final EntitiesRepository entitiesRepository;\n\n    public static final int SAVED = 500;\n    public static final int SAVE_ERROR = 501;\n    public static final int SAVED_AND_EXIT = 504;\n    public static final int ENCRYPTION_ERROR = 505;\n\n    public SaveFormToDisk(FormController formController, MediaUtils mediaUtils, boolean saveAndExit, boolean shouldFinalize, String updatedName,\n                          Uri uri, ArrayList<String> tempFiles, String currentProjectId, EntitiesRepository entitiesRepository, InstancesRepository instancesRepository, DebugLogger debugLogger) {\n        this.formController = formController;\n        this.mediaUtils = mediaUtils;\n        this.uri = uri;\n        this.saveAndExit = saveAndExit;\n        this.shouldFinalize = shouldFinalize;\n        this.instanceName = updatedName;\n        this.tempFiles = tempFiles;\n        this.currentProjectId = currentProjectId;\n        this.entitiesRepository = entitiesRepository;\n        this.instancesRepository = instancesRepository;\n        this.debugLogger = debugLogger;\n    }\n\n    @Nullable\n    public SaveToDiskResult saveForm(FormSaver.ProgressListener progressListener) {\n        SaveToDiskResult saveToDiskResult = new SaveToDiskResult();\n\n        progressListener.onProgressUpdate(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.survey_saving_validating_message));\n\n        try {\n            ValidationResult validationResult = formController.validateAnswers(shouldFinalize);\n            if (shouldFinalize && validationResult instanceof FailedValidationResult) {\n                // validation failed, pass specific failure\n                saveToDiskResult.setSaveResult(((FailedValidationResult) validationResult).getStatus(), shouldFinalize);\n                return saveToDiskResult;\n            }\n\n            if (shouldFinalize) {\n                Instance instance = updateInstanceDatabase(true, true, validationResult);\n                FormEntryUseCases.finalizeFormController(instance, formController, instancesRepository, entitiesRepository, debugLogger);\n            }\n\n            // close all open databases of external data.\n            Collect.getInstance().getExternalDataManager().close();\n\n            // if there is a meta/instanceName field, be sure we are using the latest value\n            // just in case the validate somehow triggered an update.\n            String updatedSaveName = formController.getSubmissionMetadata().instanceName;\n            if (updatedSaveName != null) {\n                instanceName = updatedSaveName;\n            }\n\n            Instance instance = exportData(shouldFinalize, progressListener, validationResult);\n\n            if (formController.getInstanceFile() != null) {\n                removeIndexFile(formController.getInstanceFile().getName());\n            }\n\n            saveToDiskResult.setSaveResult(saveAndExit ? SAVED_AND_EXIT : SAVED, shouldFinalize);\n            saveToDiskResult.setInstance(instance);\n        } catch (EncryptionException e) {\n            saveToDiskResult.setSaveErrorMessage(e.getMessage());\n            saveToDiskResult.setSaveResult(ENCRYPTION_ERROR, shouldFinalize);\n        } catch (FileNotFoundException e) {\n            saveToDiskResult.setSaveErrorMessage(\"Form can't be found.\");\n            saveToDiskResult.setSaveResult(SAVE_ERROR, shouldFinalize);\n        } catch (Exception e) {\n            Timber.e(e);\n\n            saveToDiskResult.setSaveErrorMessage(e.getMessage());\n            saveToDiskResult.setSaveResult(SAVE_ERROR, shouldFinalize);\n        }\n\n        return saveToDiskResult;\n    }\n\n    /**\n     * Updates the status and editability for the database row corresponding to the instance that is\n     * currently managed by the {@link FormController}. There are three cases:\n     * - the instance was opened for edit so its database row already exists\n     * - a new instance was just created so its database row doesn't exist and needs to be created\n     * - a new instance was created at the start of this editing session but the user has already\n     * saved it so its database row already exists\n     * <p>\n     * Post-condition: the uri field is set to the URI of the instance database row that matches\n     * the instance currently managed by the {@link FormController}.\n     */\n    private Instance updateInstanceDatabase(boolean incomplete, boolean canEditAfterCompleted, ValidationResult validationResult) throws FileNotFoundException {\n        FormInstance formInstance = formController.getFormDef().getInstance();\n\n        String instancePath = formController.getInstanceFile().getAbsolutePath();\n        InstancesRepository instances = instancesRepository;\n        Instance instance = instances.getOneByPath(instancePath);\n\n        Instance.Builder instanceBuilder;\n        if (instance != null) {\n            instanceBuilder = new Instance.Builder(instance);\n        } else {\n            instanceBuilder = new Instance.Builder();\n        }\n\n        if (instanceName != null) {\n            instanceBuilder.displayName(instanceName);\n        }\n\n        if (!shouldFinalize) {\n            if (validationResult instanceof FailedValidationResult) {\n                instanceBuilder.status(Instance.STATUS_INVALID);\n            } else {\n                instanceBuilder.status(Instance.STATUS_VALID);\n            }\n        } else if (incomplete) {\n            instanceBuilder.status(Instance.STATUS_INCOMPLETE);\n        } else {\n            instanceBuilder.status(Instance.STATUS_COMPLETE);\n        }\n\n        instanceBuilder.canEditWhenComplete(canEditAfterCompleted);\n\n        if (instance != null) {\n            String geometryXpath = getGeometryXpathForInstance(instance);\n            Pair<String, String> geometryContentValues = extractGeometryContentValues(formInstance, geometryXpath);\n            if (geometryContentValues != null) {\n                instanceBuilder.geometryType(geometryContentValues.first);\n                instanceBuilder.geometry(geometryContentValues.second);\n            }\n\n            Instance newInstance = instancesRepository.save(instanceBuilder.build());\n            uri = InstancesContract.getUri(currentProjectId, newInstance.getDbId());\n        } else {\n            Timber.i(\"No instance found, creating\");\n            Form form = new FormsRepositoryProvider(Collect.getInstance()).create().get(ContentUriHelper.getIdFromUri(uri));\n            if (form == null) {\n                throw new FileNotFoundException();\n            }\n\n            // add missing fields into values\n            instanceBuilder.instanceFilePath(instancePath);\n            instanceBuilder.submissionUri(form.getSubmissionUri());\n            if (instanceName != null) {\n                instanceBuilder.displayName(instanceName);\n            } else {\n                instanceBuilder.displayName(form.getDisplayName());\n            }\n\n            instanceBuilder.formId(form.getFormId());\n            instanceBuilder.formVersion(form.getVersion());\n\n            Pair<String, String> geometryContentValues = extractGeometryContentValues(formInstance, form.getGeometryXpath());\n            if (geometryContentValues != null) {\n                instanceBuilder.geometryType(geometryContentValues.first);\n                instanceBuilder.geometry(geometryContentValues.second);\n            }\n        }\n\n        Instance newInstance = instancesRepository.save(instanceBuilder.build());\n        uri = InstancesContract.getUri(currentProjectId, newInstance.getDbId());\n        return newInstance;\n    }\n\n    /**\n     * Extracts geometry information from the given xpath path in the given instance.\n     * <p>\n     * Returns a ContentValues object with values set for InstanceColumns.GEOMETRY and\n     * InstanceColumns.GEOMETRY_TYPE. Those value are null if anything goes wrong with\n     * parsing the geometry and converting it to GeoJSON.\n     * <p>\n     * Returns null if the given XPath path is null.\n     */\n    private Pair<String, String> extractGeometryContentValues(FormInstance instance, String xpath) {\n        if (xpath == null) {\n            return null;\n        }\n\n        try {\n            XPathExpression expr = XPathParseTool.parseXPath(xpath);\n            EvaluationContext context = new EvaluationContext(instance);\n            Object result = expr.eval(instance, context);\n            if (result instanceof XPathNodeset) {\n                XPathNodeset nodes = (XPathNodeset) result;\n                if (nodes.size() == 0) {\n                    Timber.i(\"TreeElement is missing for xpath %s!, probably it's just not relevant\", xpath);\n                    return null;\n                }\n\n                // For now, only use the first node found.\n                TreeElement element = instance.resolveReference(nodes.getRefAt(0));\n                IAnswerData value = element.getValue();\n\n                if (value instanceof GeoPointData) {\n                    try {\n                        JSONObject json = toGeoJson((GeoPointData) value);\n                        Timber.i(\"Geometry for \\\"%s\\\" instance found at %s: %s\",\n                                instance.getName(), xpath, json);\n\n                        return new Pair<>(json.getString(\"type\"), json.toString());\n                    } catch (JSONException e) {\n                        Timber.w(\"Could not convert GeoPointData %s to GeoJSON\", value);\n                    }\n                }\n            }\n        } catch (XPathException | XPathSyntaxException e) {\n            Timber.w(e, \"Could not evaluate geometry XPath %s in instance\", xpath);\n        }\n\n        return new Pair<>(null, null);\n    }\n\n    @NonNull\n    private JSONObject toGeoJson(GeoPointData data) throws JSONException {\n        MapPoint mapPoint = toMapPoint(data);\n        double lat = mapPoint.latitude;\n        double lon = mapPoint.longitude;\n\n        // In GeoJSON, longitude comes before latitude.\n        JSONArray coordinates = new JSONArray();\n        coordinates.put(lon);\n        coordinates.put(lat);\n\n        JSONObject geometry = new JSONObject();\n        geometry.put(\"type\", \"Point\");\n        geometry.put(\"coordinates\", coordinates);\n        return geometry;\n    }\n\n    /**\n     * Return the formIndex file for a given instance.\n     */\n    public static File getFormIndexFile(String instanceName) {\n        File tempDir = new File(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.CACHE));\n        return new File(tempDir, instanceName + \".index\");\n    }\n\n    public static void removeIndexFile(String instanceName) {\n        File formIndexFile = getFormIndexFile(instanceName);\n        FileUtils.deleteAndReport(formIndexFile);\n    }\n\n    /**\n     * Write's the data to the sdcard, and updates the instances content provider.\n     * In theory we don't have to write to disk, and this is where you'd add\n     * other methods.\n     */\n    private Instance exportData(boolean markCompleted, FormSaver.ProgressListener progressListener, ValidationResult validationResult) throws IOException, EncryptionException {\n        progressListener.onProgressUpdate(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.survey_saving_collecting_message));\n\n        ByteArrayPayload payload = formController.getFilledInFormXml();\n        // write out xml\n        for (String fileName : tempFiles) {\n            mediaUtils.deleteMediaFile(fileName);\n        }\n\n        progressListener.onProgressUpdate(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.survey_saving_saving_message));\n\n        writeFile(payload, formController.getInstanceFile());\n\n        // Write last-saved instance\n        String lastSavedPath = formController.getLastSavedPath();\n        writeFile(payload, new File(lastSavedPath));\n\n        // update the uri. We have exported the reloadable instance, so update status...\n        // Since we saved a reloadable instance, it is flagged as re-openable so that if any error\n        // occurs during the packaging of the data for the server fails (e.g., encryption),\n        // we can still reopen the filled-out form and re-save it at a later time.\n        Instance instance = updateInstanceDatabase(true, true, validationResult);\n\n        if (markCompleted) {\n            // now see if the packaging of the data for the server would make it\n            // non-reopenable (e.g., encryption or other fraction of the form).\n            boolean canEditAfterCompleted = formController.isSubmissionEntireForm();\n            boolean isEncrypted = false;\n\n            // build a submission.xml to hold the data being submitted\n            // and (if appropriate) encrypt the files on the side\n\n            // pay attention to the ref attribute of the submission profile...\n            File instanceXml = formController.getInstanceFile();\n            File submissionXml = new File(instanceXml.getParentFile(), \"submission.xml\");\n\n            payload = formController.getSubmissionXml();\n\n            // write out submission.xml -- the data to actually submit to aggregate\n\n            progressListener.onProgressUpdate(\n                    getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.survey_saving_finalizing_message));\n\n            writeFile(payload, submissionXml);\n\n            // see if the form is encrypted and we can encrypt it...\n            EncryptedFormInformation formInfo = EncryptionUtils.getEncryptedFormInformation(uri, formController.getSubmissionMetadata());\n            if (formInfo != null) {\n                // if we are encrypting, the form cannot be reopened afterward\n                canEditAfterCompleted = false;\n                // and encrypt the submission (this is a one-way operation)...\n\n                progressListener.onProgressUpdate(\n                        getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.survey_saving_encrypting_message));\n\n                EncryptionUtils.generateEncryptedSubmission(instanceXml, submissionXml, formInfo);\n                isEncrypted = true;\n\n                Analytics.log(ENCRYPT_SUBMISSION, \"form\");\n            }\n\n            // At this point, we have:\n            // 1. the saved original instanceXml,\n            // 2. all the plaintext attachments\n            // 2. the submission.xml that is the completed xml (whether encrypting or not)\n            // 3. all the encrypted attachments if encrypting (isEncrypted = true).\n            //\n            // NEXT:\n            // 1. Update the instance database (with status complete).\n            // 2. Overwrite the instanceXml with the submission.xml\n            //    and remove the plaintext attachments if encrypting\n\n            instance = updateInstanceDatabase(false, canEditAfterCompleted, validationResult);\n\n            if (!canEditAfterCompleted) {\n                manageFilesAfterSavingEncryptedForm(instanceXml, submissionXml);\n            } else {\n                // try to delete the submissionXml file, since it is\n                // identical to the existing instanceXml file\n                // (we don't need to delete and rename anything).\n                if (!submissionXml.delete()) {\n                    String msg = \"Error deleting \" + submissionXml.getAbsolutePath()\n                            + \" (instance is re-openable)\";\n                    Timber.w(msg);\n                }\n            }\n\n            // if encrypted, delete all plaintext files\n            // (anything not named instanceXml or anything not ending in .enc)\n            if (isEncrypted) {\n                instance = instancesRepository.get(ContentUriHelper.getIdFromUri(uri));\n\n                // Clear the geometry. Done outside of updateInstanceDatabase to avoid multiple\n                // branches and because it has no knowledge of encryption status.\n                instancesRepository.save(new Instance.Builder(instance)\n                        .geometry(null)\n                        .geometryType(null)\n                        .build()\n                );\n\n                ContentValues values = new ContentValues();\n                values.put(DatabaseInstanceColumns.GEOMETRY, (String) null);\n                values.put(DatabaseInstanceColumns.GEOMETRY_TYPE, (String) null);\n\n                if (!EncryptionUtils.deletePlaintextFiles(instanceXml, new File(lastSavedPath))) {\n                    Timber.e(new Error(\"Error deleting plaintext files for \" + instanceXml.getAbsolutePath()));\n                }\n            }\n        }\n\n        return instance;\n    }\n\n    /**\n     * Returns the XPath path of the geo feature used for mapping that corresponds to the blank form\n     * that the instance with the given uri is an instance of.\n     */\n    private static String getGeometryXpathForInstance(Instance instance) {\n        Form form = new FormsRepositoryProvider(Collect.getInstance()).create().getLatestByFormIdAndVersion(instance.getFormId(), instance.getFormVersion());\n        if (form != null) {\n            return form.getGeometryXpath();\n        } else {\n            return null;\n        }\n    }\n\n    public static void manageFilesAfterSavingEncryptedForm(File instanceXml, File submissionXml) throws IOException {\n        // AT THIS POINT, there is no going back.  We are committed\n        // to returning \"success\" (true) whether or not we can\n        // rename \"submission.xml\" to instanceXml and whether or\n        // not we can delete the plaintext media files.\n        //\n        // Handle the fall-out for a failed \"submission.xml\" rename\n        // in the InstanceUploaderTask task.  Leftover plaintext media\n        // files are handled during form deletion.\n\n        // delete the restore Xml file.\n        if (!instanceXml.delete()) {\n            String msg = \"Error deleting \" + instanceXml.getAbsolutePath()\n                    + \" prior to renaming submission.xml\";\n            Timber.e(new Error(msg));\n            throw new IOException(msg);\n        }\n\n        // rename the submission.xml to be the instanceXml\n        if (!submissionXml.renameTo(instanceXml)) {\n            String msg =\n                    \"Error renaming submission.xml to \" + instanceXml.getAbsolutePath();\n            Timber.e(new Error(msg));\n            throw new IOException(msg);\n        }\n    }\n\n    /**\n     * Writes payload contents to the disk.\n     */\n    public static void writeFile(ByteArrayPayload payload, File file) throws IOException {\n        FileExt.saveToFile(file, payload.getPayloadStream());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/tasks/SaveToDiskResult.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.tasks;\n\nimport org.odk.collect.forms.instances.Instance;\n\n/**\n * Author: Meletis Margaritis\n * Date: 15/3/2013\n * Time: 2:53 μμ\n */\npublic class SaveToDiskResult {\n\n    private int saveResult;\n    private boolean complete;\n    private String saveErrorMessage;\n    private Instance instance;\n\n    public int getSaveResult() {\n        return saveResult;\n    }\n\n    public boolean isComplete() {\n        return complete;\n    }\n\n    public void setSaveResult(int saveResult, boolean complete) {\n        this.saveResult = saveResult;\n        this.complete = complete;\n    }\n\n    public void setSaveErrorMessage(String saveErrorMessage) {\n        this.saveErrorMessage = saveErrorMessage;\n    }\n\n    public String getSaveErrorMessage() {\n        return saveErrorMessage;\n    }\n\n    public void setInstance(Instance instance) {\n        this.instance = instance;\n    }\n\n    public Instance getInstance() {\n        return instance;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/upload/FormUploadException.kt",
    "content": "/*\n * Copyright (C) 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.upload\n\nimport android.net.Uri\nimport java.lang.Exception\n\n/**\n * Thrown to indicate that a problem with submitting the current finalized form has occurred.\n *\n * Throwing an UploadException makes the submission attempt move on to the next finalized form to\n * send except in the case of an [FormUploadAuthRequestedException] thrown when the submission\n * attempt was triggered manually by the user. In that case, the finalized form that resulted in the\n * exception will be re-tried after the user provides credentials.\n */\nopen class FormUploadException : Exception {\n    constructor(message: String) : super(message)\n    constructor(cause: Throwable) : super(cause)\n\n    override val message: String\n        get() = super.message\n            ?: cause?.message\n            ?: \"\"\n}\n\n/**\n * Thrown to indicate that the server an upload attempt was made to is requesting authentication.\n * This may lead to a re-try attempt if the upload was triggered manually by the user (as opposed to\n * auto-send).\n *\n * @param authRequestingServer The URI for the server that requested authentication. This URI may\n * not match the server specified in the app settings or the blank form because there could have\n * been a redirect. See also [org.odk.collect.android.tasks.InstanceUploaderTask.Outcome]\n */\nclass FormUploadAuthRequestedException(\n    message: String,\n    val authRequestingServer: Uri\n) : FormUploadException(message)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/upload/InstanceServerUploader.java",
    "content": "/*\n * Copyright (C) 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.upload;\n\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\nimport android.net.Uri;\n\nimport androidx.annotation.NonNull;\n\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.utilities.ResponseMessageParser;\nimport org.odk.collect.android.utilities.WebCredentialsUtils;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.openrosa.http.CaseInsensitiveHeaders;\nimport org.odk.collect.openrosa.http.HttpHeadResult;\nimport org.odk.collect.openrosa.http.HttpPostResult;\nimport org.odk.collect.openrosa.http.OpenRosaConstants;\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.io.File;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URI;\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.net.ssl.HttpsURLConnection;\n\nimport timber.log.Timber;\n\npublic class InstanceServerUploader extends InstanceUploader {\n    private static final String URL_PATH_SEP = \"/\";\n\n    private final OpenRosaHttpInterface httpInterface;\n    private final WebCredentialsUtils webCredentialsUtils;\n    private final Settings generalSettings;\n    private final Map<Uri, Uri> uriRemap = new HashMap<>();\n\n    public InstanceServerUploader(OpenRosaHttpInterface httpInterface,\n                                  WebCredentialsUtils webCredentialsUtils,\n                                  Settings generalSettings, InstancesRepository instancesRepository) {\n        super(instancesRepository);\n        this.httpInterface = httpInterface;\n        this.webCredentialsUtils = webCredentialsUtils;\n        this.generalSettings = generalSettings;\n    }\n\n    /**\n     * Uploads all files associated with an instance to the specified URL. Writes fail/success\n     * status to database.\n     * <p>\n     * Returns a custom success message if one is provided by the server.\n     */\n    @Override\n    public String uploadOneSubmission(Instance instance, String urlString) throws FormUploadException {\n        markSubmissionFailed(instance);\n\n        Uri submissionUri = Uri.parse(urlString);\n\n        long contentLength = 10000000L;\n\n        // We already issued a head request and got a response, so we know it was an\n        // OpenRosa-compliant server. We also know the proper URL to send the submission to and\n        // the proper scheme.\n        if (uriRemap.containsKey(submissionUri)) {\n            submissionUri = uriRemap.get(submissionUri);\n            Timber.i(\"Using Uri remap for submission %s. Now: %s\", instance.getDbId(),\n                    submissionUri.toString());\n        } else {\n            if (submissionUri.getHost() == null) {\n                throw new FormUploadException(FAIL + \"Host name may not be null\");\n            }\n\n            URI uri;\n            try {\n                uri = URI.create(submissionUri.toString());\n            } catch (IllegalArgumentException e) {\n                Timber.d(e.getMessage() != null ? e.getMessage() : e.toString());\n                throw new FormUploadException(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.url_error));\n            }\n\n            HttpHeadResult headResult;\n            CaseInsensitiveHeaders responseHeaders;\n            try {\n                headResult = httpInterface.executeHeadRequest(uri, webCredentialsUtils.getCredentials(uri));\n                responseHeaders = headResult.getHeaders();\n\n                if (responseHeaders.containsHeader(OpenRosaConstants.ACCEPT_CONTENT_LENGTH_HEADER)) {\n                    String contentLengthString = responseHeaders.getAnyValue(OpenRosaConstants.ACCEPT_CONTENT_LENGTH_HEADER);\n                    try {\n                        contentLength = Long.parseLong(contentLengthString);\n                    } catch (Exception e) {\n                        Timber.e(e, \"Exception thrown parsing contentLength %s\", contentLengthString);\n                    }\n                }\n\n            } catch (Exception e) {\n                throw new FormUploadException(FAIL\n                        + (e.getMessage() != null ? e.getMessage() : e.toString()));\n            }\n\n            if (headResult.getStatusCode() == HttpsURLConnection.HTTP_UNAUTHORIZED) {\n                throw new FormUploadAuthRequestedException(getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.server_auth_credentials, submissionUri.getHost()),\n                        submissionUri);\n            } else if (headResult.getStatusCode() == HttpsURLConnection.HTTP_NO_CONTENT) {\n                // Redirect header received\n                if (responseHeaders.containsHeader(\"Location\")) {\n                    try {\n                        Uri newURI = Uri.parse(URLDecoder.decode(responseHeaders.getAnyValue(\"Location\"), \"utf-8\"));\n                        // Allow redirects within same host. This could be redirecting to HTTPS.\n                        if (submissionUri.getHost().equalsIgnoreCase(newURI.getHost())) {\n                            // Re-add params if server didn't respond with params\n                            if (newURI.getQuery() == null) {\n                                newURI = newURI.buildUpon()\n                                        .encodedQuery(submissionUri.getEncodedQuery())\n                                        .build();\n                            }\n                            uriRemap.put(submissionUri, newURI);\n                            submissionUri = newURI;\n                        } else {\n                            // Don't follow a redirection attempt to a different host.\n                            // We can't tell if this is a spoof or not.\n                            throw new FormUploadException(FAIL\n                                    + \"Unexpected redirection attempt to a different host: \"\n                                    + newURI.toString());\n                        }\n                    } catch (Exception e) {\n                        throw new FormUploadException(FAIL + urlString + \" \" + e.toString());\n                    }\n                }\n            } else {\n                if (headResult.getStatusCode() >= HttpsURLConnection.HTTP_OK\n                        && headResult.getStatusCode() < HttpsURLConnection.HTTP_MULT_CHOICE) {\n                    throw new FormUploadException(\"Failed to send to \" + uri + \". Is this an OpenRosa \" +\n                            \"submission endpoint? If you have a web proxy you may need to log in to \" +\n                            \"your network.\\n\\nHEAD request result status code: \" + headResult.getStatusCode());\n                }\n            }\n        }\n\n        // When encrypting submissions, there is a failure window that may mark the submission as\n        // complete but leave the file-to-be-uploaded with the name \"submission.xml\" and the plaintext\n        // submission files on disk.  In this case, upload the submission.xml and all the files in\n        // the directory. This means the plaintext files and the encrypted files will be sent to the\n        // server and the server will have to figure out what to do with them.\n        File instanceFile = new File(instance.getInstanceFilePath());\n        File submissionFile = new File(instanceFile.getParentFile(), \"submission.xml\");\n        if (submissionFile.exists()) {\n            Timber.w(\"submission.xml will be uploaded instead of %s\", instanceFile.getAbsolutePath());\n        } else {\n            submissionFile = instanceFile;\n        }\n\n        if (!instanceFile.exists() && !submissionFile.exists()) {\n            throw new FormUploadException(FAIL + \"instance XML file does not exist!\");\n        }\n\n        List<File> files = getFilesInParentDirectory(instanceFile, submissionFile);\n\n        // TODO: when can this happen? It used to cause the whole submission attempt to fail. Should it?\n        if (files == null) {\n            throw new FormUploadException(\"Error reading files to upload\");\n        }\n\n        HttpPostResult postResult;\n        ResponseMessageParser messageParser = new ResponseMessageParser();\n\n        try {\n            URI uri = URI.create(submissionUri.toString());\n\n            postResult = httpInterface.uploadSubmissionAndFiles(submissionFile, files, uri,\n                    webCredentialsUtils.getCredentials(uri), contentLength);\n\n            int responseCode = postResult.getResponseCode();\n            messageParser.setMessageResponse(postResult.getHttpResponse());\n\n            if (responseCode != HttpsURLConnection.HTTP_CREATED && responseCode != HttpsURLConnection.HTTP_ACCEPTED) {\n                FormUploadException exception;\n                if (responseCode == HttpsURLConnection.HTTP_OK) {\n                    exception = new FormUploadException(FAIL + \"Network login failure? Again?\");\n                } else if (responseCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {\n                    exception = new FormUploadException(FAIL + postResult.getReasonPhrase()\n                            + \" (\" + responseCode + \") at \" + urlString);\n                } else {\n                    if (messageParser.isValid()) {\n                        exception = new FormUploadException(FAIL + messageParser.getMessageResponse());\n                    } else if (responseCode == HttpsURLConnection.HTTP_BAD_REQUEST) {\n                        Timber.w(FAIL + postResult.getReasonPhrase() + \" (\" + responseCode + \") at \" + urlString);\n                        exception = new FormUploadException(\"Failed to upload. Please make sure the form is configured to accept submissions on the server\");\n                    } else {\n                        exception = new FormUploadException(FAIL + postResult.getReasonPhrase() + \" (\" + responseCode + \") at \" + urlString);\n                    }\n\n                }\n                throw exception;\n            }\n\n        } catch (Exception e) {\n            throw new FormUploadException(e.getMessage() != null ? e.getMessage() : e.toString());\n        }\n\n        markSubmissionComplete(instance);\n\n        if (messageParser.isValid()) {\n            return messageParser.getMessageResponse();\n        }\n\n        return null;\n    }\n\n    private List<File> getFilesInParentDirectory(File instanceFile, File submissionFile) {\n        List<File> files = new ArrayList<>();\n\n        // find all files in parent directory\n        File[] allFiles = instanceFile.getParentFile().listFiles();\n        if (allFiles == null) {\n            return null;\n        }\n\n        for (File f : allFiles) {\n            String fileName = f.getName();\n\n            if (fileName.startsWith(\".\")) {\n                continue; // ignore invisible files\n            } else if (fileName.equals(instanceFile.getName())) {\n                continue; // the xml file has already been added\n            } else if (fileName.equals(submissionFile.getName())) {\n                continue; // the xml file has already been added\n            }\n\n            files.add(f);\n        }\n\n        return files;\n    }\n\n\n    /**\n     * Returns the URL this instance should be submitted to with appended deviceId.\n     * <p>\n     * If the upload was triggered by an external app and specified an override URL, use that one.\n     * Otherwise, use the submission URL configured in the form\n     * (https://getodk.github.io/xforms-spec/#submission-attributes). Finally, default to the\n     * URL configured at the app level.\n     */\n    @Override\n    @NonNull\n    public String getUrlToSubmitTo(Instance currentInstance, String deviceId, String overrideURL, String urlFromSettings) {\n        String urlString;\n\n        if (overrideURL != null) {\n            urlString = overrideURL;\n        } else if (currentInstance.getSubmissionUri() != null) {\n            urlString = currentInstance.getSubmissionUri().trim();\n        } else {\n            urlString = getServerSubmissionURL();\n        }\n\n        // add deviceID to request\n        try {\n            urlString += \"?deviceID=\" + URLEncoder.encode(deviceId != null ? deviceId : \"\", \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            Timber.i(e, \"Error encoding URL for device id : %s\", deviceId);\n        }\n\n        return urlString;\n    }\n\n    private String getServerSubmissionURL() {\n        String serverBase = generalSettings.getString(ProjectKeys.KEY_SERVER_URL);\n\n        if (serverBase.endsWith(URL_PATH_SEP)) {\n            serverBase = serverBase.substring(0, serverBase.length() - 1);\n        }\n\n        return serverBase + OpenRosaConstants.SUBMISSION;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/upload/InstanceUploader.java",
    "content": "/*\n * Copyright (C) 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.upload;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic abstract class InstanceUploader {\n\n    private final InstancesRepository instancesRepository;\n\n    public InstanceUploader(InstancesRepository instancesRepository) {\n        this.instancesRepository = instancesRepository;\n    }\n\n    public static final String FAIL = \"Error: \";\n\n    /**\n     * Uploads the specified instance to the specified destination URL. It may return a custom\n     * success message on completion or null if none is available. Errors result in an UploadException.\n     * <p>\n     * Updates the database status for the instance.\n     */\n    @Nullable\n    public abstract String uploadOneSubmission(Instance instance, String destinationUrl) throws FormUploadException;\n\n    @NonNull\n    public abstract String getUrlToSubmitTo(Instance currentInstance, String deviceId, String overrideURL, String urlFromSettings);\n\n    /**\n     * Returns a list of Instance objects corresponding to the database IDs passed in.\n     */\n    public List<Instance> getInstancesFromIds(Long... instanceDatabaseIds) {\n        List<Instance> instances = new ArrayList<>();\n\n        for (Long id : instanceDatabaseIds) {\n            instances.add(instancesRepository.get(id));\n        }\n\n        return instances;\n    }\n\n    public void markSubmissionFailed(Instance instance) {\n        instancesRepository\n                .save(new Instance.Builder(instance)\n                        .status(Instance.STATUS_SUBMISSION_FAILED)\n                        .build()\n                );\n    }\n\n    public void markSubmissionComplete(Instance instance) {\n        instancesRepository\n                .save(new Instance.Builder(instance)\n                        .status(Instance.STATUS_SUBMITTED)\n                        .build()\n                );\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ActionRegister.kt",
    "content": "package org.odk.collect.android.utilities\n\n/**\n * Used to allow tests to understand whether a UI action has been successfully detected or not\n */\nobject ActionRegister {\n\n    @JvmStatic\n    var isActionDetected = false\n        private set\n\n    @JvmStatic\n    fun attemptingAction() {\n        isActionDetected = false\n    }\n\n    @JvmStatic\n    fun actionDetected() {\n        isActionDetected = true\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/AdminPasswordProvider.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport static org.odk.collect.settings.keys.ProtectedProjectKeys.KEY_ADMIN_PW;\n\nimport org.odk.collect.shared.settings.Settings;\n\npublic class AdminPasswordProvider {\n    private final Settings adminSettings;\n\n    public AdminPasswordProvider(Settings adminSettings) {\n        this.adminSettings = adminSettings;\n    }\n\n    public boolean isAdminPasswordSet() {\n        String adminPassword = getAdminPassword();\n        return adminPassword != null && !adminPassword.isEmpty();\n    }\n\n    public String getAdminPassword() {\n        return adminSettings.getString(KEY_ADMIN_PW);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/AndroidUserAgent.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport org.odk.collect.android.BuildConfig;\nimport org.odk.collect.utilities.UserAgentProvider;\n\npublic final class AndroidUserAgent implements UserAgentProvider {\n\n    @Override\n    public String getUserAgent() {\n        return String.format(\"%s/%s %s\",\n                BuildConfig.APPLICATION_ID,\n                BuildConfig.VERSION_NAME,\n                System.getProperty(\"http.agent\"));\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/AnimationUtils.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport androidx.core.view.animation.PathInterpolatorCompat;\n\nimport android.content.Context;\nimport android.provider.Settings;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.animation.Animation;\nimport android.view.animation.Interpolator;\nimport android.view.animation.Transformation;\n\nimport org.odk.collect.android.listeners.Result;\n\n/**\n * Created by Ing. Oscar G. Medina Cruz on 18/06/2016.\n */\npublic final class AnimationUtils {\n\n    private static final Interpolator EASE_IN_OUT_QUART = PathInterpolatorCompat.create(0.77f, 0f, 0.175f, 1f);\n\n    private AnimationUtils() {\n\n    }\n\n    // Added animation related code and inspiration from this Stack Overflow Question\n    // https://stackoverflow.com/questions/4946295/android-expand-collapse-animation\n\n    public static Animation expand(final View view, Result<Boolean> result) {\n        int matchParentMeasureSpec = View.MeasureSpec.makeMeasureSpec(((View) view.getParent()).getWidth(), View.MeasureSpec.EXACTLY);\n        int wrapContentMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);\n        view.measure(matchParentMeasureSpec, wrapContentMeasureSpec);\n        final int targetHeight = view.getMeasuredHeight();\n\n        // Older versions of android (pre API 21) cancel animations for views with a height of 0 so use 1 instead.\n        view.getLayoutParams().height = 1;\n        view.setVisibility(View.VISIBLE);\n\n        Animation animation = new Animation() {\n            @Override\n            protected void applyTransformation(float interpolatedTime, Transformation t) {\n\n                view.getLayoutParams().height = interpolatedTime == 1\n                        ? ViewGroup.LayoutParams.WRAP_CONTENT\n                        : (int) (targetHeight * interpolatedTime);\n\n                view.requestLayout();\n            }\n\n            @Override\n            public boolean willChangeBounds() {\n                return true;\n            }\n        };\n\n        animation.setInterpolator(EASE_IN_OUT_QUART);\n        animation.setDuration(computeDurationFromHeight(view));\n        animation.setAnimationListener(new Animation.AnimationListener() {\n            @Override\n            public void onAnimationStart(Animation animation) {\n                //triggered when animation starts.\n            }\n\n            @Override\n            public void onAnimationEnd(Animation animation) {\n                result.onComplete(true);\n            }\n\n            @Override\n            public void onAnimationRepeat(Animation animation) {\n                //triggered when animation repeats.\n            }\n        });\n        view.startAnimation(animation);\n\n        return animation;\n    }\n\n    public static Animation collapse(final View view, Result<Boolean> result) {\n        final int initialHeight = view.getMeasuredHeight();\n\n        Animation a = new Animation() {\n            @Override\n            protected void applyTransformation(float interpolatedTime, Transformation t) {\n                if (interpolatedTime == 1) {\n                    view.setVisibility(View.GONE);\n                } else {\n                    view.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime);\n                    view.requestLayout();\n                }\n            }\n\n            @Override\n            public boolean willChangeBounds() {\n                return true;\n            }\n        };\n\n        a.setInterpolator(EASE_IN_OUT_QUART);\n\n        int durationMillis = computeDurationFromHeight(view);\n        a.setDuration(durationMillis);\n\n        a.setAnimationListener(new Animation.AnimationListener() {\n            @Override\n            public void onAnimationStart(Animation animation) {\n                //triggered when animation starts.\n\n            }\n\n            @Override\n            public void onAnimationEnd(Animation animation) {\n                result.onComplete(true);\n            }\n\n            @Override\n            public void onAnimationRepeat(Animation animation) {\n                //triggered when animation repeats.\n            }\n        });\n\n        view.startAnimation(a);\n\n        return a;\n    }\n\n    private static int computeDurationFromHeight(View view) {\n        // 1dp/ms * multiplier\n        return (int) (view.getMeasuredHeight() / view.getContext().getResources().getDisplayMetrics().density);\n    }\n\n    public static boolean areAnimationsEnabled(Context context) {\n        float duration = Settings.System.getFloat(\n                context.getContentResolver(),\n                Settings.System.ANIMATOR_DURATION_SCALE, 1);\n        float transition = Settings.System.getFloat(\n                context.getContentResolver(),\n                Settings.System.TRANSITION_ANIMATION_SCALE, 1);\n\n        return duration != 0 && transition != 0;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/Appearances.kt",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.utilities\n\nimport android.content.res.Configuration\nimport org.javarosa.core.model.Constants\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.dynamicpreload.ExternalDataUtil\nimport org.odk.collect.androidshared.utils.ScreenUtils\n\nobject Appearances {\n    // Date appearances\n    const val ETHIOPIAN = \"ethiopian\"\n    const val COPTIC = \"coptic\"\n    const val ISLAMIC = \"islamic\"\n    const val BIKRAM_SAMBAT = \"bikram-sambat\"\n    const val MYANMAR = \"myanmar\"\n    const val PERSIAN = \"persian\"\n    const val BUDDHIST = \"buddhist\"\n    const val NO_CALENDAR = \"no-calendar\"\n    const val MONTH_YEAR = \"month-year\"\n    const val YEAR = \"year\"\n\n    // Select one/multiple appearances\n    @Deprecated(\"\")\n    const val COMPACT = \"compact\"\n\n    @Deprecated(\"\")\n    const val COMPACT_N = \"compact-\"\n    const val MINIMAL = \"minimal\"\n    const val COLUMNS = \"columns\"\n    const val COLUMNS_N = \"columns-\"\n    const val COLUMNS_PACK = \"columns-pack\"\n\n    @Deprecated(\"\")\n    const val QUICKCOMPACT = \"quickcompact\"\n\n    @Deprecated(\"\")\n    const val SEARCH = \"search\"\n    const val AUTOCOMPLETE = \"autocomplete\"\n    const val LIST_NO_LABEL = \"list-nolabel\"\n    const val LIST = \"list\"\n    const val LIKERT = \"likert\"\n    const val LABEL = \"label\"\n    const val IMAGE_MAP = \"image-map\"\n    const val NO_BUTTONS = \"no-buttons\"\n    const val QUICK = \"quick\"\n    const val MAP = \"map\"\n\n    // Media appearances\n    const val SIGNATURE = \"signature\"\n    const val ANNOTATE = \"annotate\"\n    const val DRAW = \"draw\"\n\n    @Deprecated(\"\")\n    const val SELFIE = \"selfie\"\n    const val NEW_FRONT = \"new-front\"\n    const val NEW = \"new\"\n    const val FRONT = \"front\"\n    const val HIDDEN_ANSWER = \"hidden-answer\"\n\n    // Maps appearances\n    const val PLACEMENT_MAP = \"placement-map\"\n    const val MAPS = \"maps\"\n\n    // Groups and repeats\n    const val FIELD_LIST = \"field-list\"\n\n    // Other appearances\n    const val NO_APPEARANCE = \"\"\n    const val BEARING = \"bearing\"\n    const val EX = \"ex:\"\n    const val THOUSANDS_SEP = \"thousands-sep\"\n    const val PRINTER = \"printer\"\n    const val NUMBERS = \"numbers\"\n    const val URL = \"url\"\n    const val RATING = \"rating\"\n    const val MASKED = \"masked\"\n    const val COUNTER = \"counter\"\n    const val MULTILINE = \"multiline\"\n    const val X_TIMED_GRID = \"x-timed-grid\"\n\n    // Get appearance hint and clean it up so it is lower case, without the search function and never null.\n    @JvmStatic\n    fun getSanitizedAppearanceHint(fep: FormEntryPrompt): String {\n        var appearance = fep.appearanceHint\n        if (appearance == null) {\n            appearance = NO_APPEARANCE\n        } else {\n            // For now, all appearance tags are in English.\n            appearance = appearance.lowercase()\n\n            // Strip out the search() appearance/function which is handled in ExternalDataUtil so that\n            // it is not considered when matching other appearances. For example, a file named list.csv\n            // used as a parameter to search() should not be interpreted as a list appearance.\n            appearance = ExternalDataUtil.SEARCH_FUNCTION_REGEX.matcher(appearance).replaceAll(\"\")\n        }\n        return appearance\n    }\n\n    /** Returns whether an appearance is present.  (Appearances are the constants above.)  */\n    @JvmStatic\n    fun hasAppearance(fep: FormEntryPrompt, appearance: String): Boolean {\n        return getSanitizedAppearanceHint(fep).contains(appearance)\n    }\n\n    /*\n    Gets the number of columns that choices should be displayed in. In the case of the columns-n\n    appearance (compact-n for backwards compatibility), that number is determined from the n in the\n    appearance string. For columns (without any number), this is determined by the device screen size.\n     */\n    @JvmStatic\n    fun getNumberOfColumns(formEntryPrompt: FormEntryPrompt, screenUtils: ScreenUtils): Int {\n        var numColumns = 1\n        var appearance = getSanitizedAppearanceHint(formEntryPrompt)\n        if (appearance.contains(COLUMNS_N) || appearance.contains(COMPACT_N)) {\n            try {\n                val columnsAppearance = if (appearance.contains(COLUMNS_N)) COLUMNS_N else COMPACT_N\n                appearance = appearance.substring(appearance.indexOf(columnsAppearance))\n                val idx = appearance.indexOf(columnsAppearance)\n                if (idx != -1) {\n                    val substringFromNumColumns = appearance.substring(idx + columnsAppearance.length)\n                    numColumns = substringFromNumColumns.substring(\n                        0,\n                        if (substringFromNumColumns.contains(\" \")) {\n                            substringFromNumColumns.indexOf(\n                                ' '\n                            )\n                        } else {\n                            substringFromNumColumns.length\n                        }\n                    ).toInt()\n                    if (numColumns < 1) {\n                        numColumns = 1\n                    }\n                }\n            } catch (e: Exception) {\n                // ignore\n            }\n        } else if (appearance.contains(COLUMNS)) {\n            numColumns = when (screenUtils.screenSizeConfiguration) {\n                Configuration.SCREENLAYOUT_SIZE_SMALL -> 2\n                Configuration.SCREENLAYOUT_SIZE_NORMAL -> 3\n                Configuration.SCREENLAYOUT_SIZE_LARGE -> 4\n                Configuration.SCREENLAYOUT_SIZE_XLARGE -> 5\n                else -> 3\n            }\n        }\n        return numColumns\n    }\n\n    @JvmStatic\n    fun isNoButtonsAppearance(prompt: FormEntryPrompt): Boolean {\n        return getSanitizedAppearanceHint(prompt).contains(NO_BUTTONS)\n    }\n\n    @JvmStatic\n    fun isCompactAppearance(prompt: FormEntryPrompt): Boolean {\n        return getSanitizedAppearanceHint(prompt).contains(COMPACT)\n    }\n\n    @JvmStatic\n    fun useThousandSeparator(prompt: FormEntryPrompt): Boolean {\n        return getSanitizedAppearanceHint(prompt).contains(THOUSANDS_SEP) && !isMasked(prompt)\n    }\n\n    @JvmStatic\n    fun isFrontCameraAppearance(prompt: FormEntryPrompt): Boolean {\n        val appearance = getSanitizedAppearanceHint(prompt)\n        return appearance.contains(FRONT) || appearance.contains(NEW_FRONT) || appearance.contains(SELFIE)\n    }\n\n    @JvmStatic\n    fun isFlexAppearance(prompt: FormEntryPrompt): Boolean {\n        val appearance = getSanitizedAppearanceHint(prompt)\n        return !appearance.contains(COMPACT_N) &&\n            (appearance.contains(COMPACT) || appearance.contains(QUICKCOMPACT) || appearance.contains(COLUMNS_PACK))\n    }\n\n    @JvmStatic\n    fun isAutocomplete(prompt: FormEntryPrompt): Boolean {\n        val appearance = getSanitizedAppearanceHint(prompt)\n        return appearance.contains(SEARCH) || appearance.contains(AUTOCOMPLETE)\n    }\n\n    @JvmStatic\n    fun isMasked(prompt: FormEntryPrompt): Boolean {\n        val appearance = getSanitizedAppearanceHint(prompt)\n        return appearance.contains(MASKED) &&\n            !appearance.contains(NUMBERS) &&\n            prompt.dataType == Constants.DATATYPE_TEXT\n    }\n\n    @JvmStatic\n    fun isMultiline(prompt: FormEntryPrompt): Boolean {\n        val appearance = getSanitizedAppearanceHint(prompt)\n        return appearance.contains(MULTILINE)\n    }\n\n    @JvmStatic\n    fun isQuick(prompt: FormEntryPrompt): Boolean {\n        return if (prompt.controlType == Constants.CONTROL_SELECT_ONE) {\n            val appearance = getSanitizedAppearanceHint(prompt)\n            appearance.contains(QUICK)\n        } else {\n            false\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ApplicationConstants.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport com.google.zxing.integration.android.IntentIntegrator;\n\npublic class ApplicationConstants {\n\n    private ApplicationConstants() {\n\n    }\n\n    public abstract static class BundleKeys {\n        public static final String SUCCESS_KEY = \"SUCCESSFUL\";\n        public static final String FORM_IDS = \"FORM_IDS\";\n        public static final String MESSAGE = \"MESSAGE\";\n        public static final String USERNAME = \"USERNAME\";\n        public static final String PASSWORD = \"PASSWORD\";\n        public static final String URL = \"URL\";\n        public static final String DELETE_INSTANCE_AFTER_SUBMISSION = \"DELETE_INSTANCE_AFTER_SUBMISSION\";\n    }\n\n    public abstract static class SortingOrder {\n        public static final int BY_NAME_ASC = 0;\n        public static final int BY_NAME_DESC = 1;\n        public static final int BY_DATE_DESC = 2;\n        public static final int BY_DATE_ASC = 3;\n        public static final int BY_STATUS_ASC = 4;\n        public static final int BY_STATUS_DESC = 5;\n    }\n\n    public abstract static class RequestCodes {\n        public static final int IMAGE_CAPTURE = 1;\n        public static final int BARCODE_CAPTURE = IntentIntegrator.REQUEST_CODE;\n        public static final int AUDIO_CAPTURE = 3;\n        public static final int VIDEO_CAPTURE = 4;\n        public static final int LOCATION_CAPTURE = 5;\n        public static final int HIERARCHY_ACTIVITY = 6;\n        public static final int IMAGE_CHOOSER = 7;\n        public static final int AUDIO_CHOOSER = 8;\n        public static final int VIDEO_CHOOSER = 9;\n        public static final int EX_STRING_CAPTURE = 10;\n        public static final int EX_INT_CAPTURE = 11;\n        public static final int EX_DECIMAL_CAPTURE = 12;\n        public static final int DRAW_IMAGE = 13;\n        public static final int SIGNATURE_CAPTURE = 14;\n        public static final int ANNOTATE_IMAGE = 15;\n        public static final int ALIGNED_IMAGE = 16;\n        public static final int BEARING_CAPTURE = 17;\n        public static final int EX_GROUP_CAPTURE = 18;\n        public static final int OSM_CAPTURE = 19;\n        public static final int GEOSHAPE_CAPTURE = 20;\n        public static final int GEOTRACE_CAPTURE = 21;\n        public static final int ARBITRARY_FILE_CHOOSER = 22;\n        public static final int EX_ARBITRARY_FILE_CHOOSER  = 23;\n        public static final int EX_VIDEO_CHOOSER  = 24;\n        public static final int EX_IMAGE_CHOOSER  = 25;\n        public static final int EX_AUDIO_CHOOSER  = 26;\n        public static final int CHANGE_SETTINGS = 27;\n        public static final int MEDIA_FILE_PATH = 28;\n    }\n\n    public abstract static class Namespaces {\n        public static final String XML_OPENROSA_NAMESPACE = \"http://openrosa.org/xforms\";\n        public static final String XML_OPENDATAKIT_NAMESPACE = \"http://www.opendatakit.org/xforms\";\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ArrayUtils.java",
    "content": "package org.odk.collect.android.utilities;\n\npublic final class ArrayUtils {\n\n    /**\n     * An empty immutable {@code Long} array.\n     */\n    private static final Long[] EMPTY_LONG_OBJECT_ARRAY = new Long[0];\n\n    /**\n     * An empty immutable {@code long} array.\n     */\n    private static final long[] EMPTY_LONG_ARRAY = new long[0];\n\n    private ArrayUtils() {\n\n    }\n\n    /**\n     * <p>Converts an array of primitive longs to objects.</p>\n     *\n     * @param array a {@code long} array\n     * @return a {@code Long} array\n     */\n    @SuppressWarnings(\"PMD.AvoidArrayLoops\")\n    public static Long[] toObject(long[] array) {\n        if (array == null || array.length == 0) {\n            return EMPTY_LONG_OBJECT_ARRAY;\n        }\n        final Long[] result = new Long[array.length];\n        for (int i = 0; i < array.length; i++) {\n            result[i] = array[i];\n        }\n        return result;\n    }\n\n    /**\n     * <p>Converts an array of object Longs to primitives.</p>\n     *\n     * @param array a {@code Long} array\n     * @return a {@code long} array\n     */\n    @SuppressWarnings(\"PMD.AvoidArrayLoops\")\n    public static long[] toPrimitive(Long[] array) {\n        if (array == null || array.length == 0) {\n            return EMPTY_LONG_ARRAY;\n        }\n        final long[] result = new long[array.length];\n        for (int i = 0; i < array.length; i++) {\n            result[i] = array[i];\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/AuthDialogUtility.java",
    "content": "/*\n * Copyright 2016 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport androidx.appcompat.app.AlertDialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.EditText;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.preferences.dialogs.ServerAuthDialogFragment;\nimport org.odk.collect.metadata.PropertyManager;\n\nimport javax.inject.Inject;\n\n/**\n * Used to present auth dialog and update credentials in the system as needed.\n */\npublic class AuthDialogUtility {\n\n    private EditText username;\n    private EditText password;\n\n    private String customUsername;\n    private String customPassword;\n\n    @Inject\n    WebCredentialsUtils webCredentialsUtils;\n    @Inject\n    PropertyManager propertyManager;\n\n    public AuthDialogUtility() {\n        Collect.getInstance().getComponent().inject(this);\n    }\n\n    /**\n     * @deprecated should use {@link ServerAuthDialogFragment} instead\n     */\n    @Deprecated\n    public AlertDialog createDialog(final Context context,\n                                    final AuthDialogUtilityResultListener resultListener, String url) {\n\n        final View dialogView = LayoutInflater.from(context)\n                .inflate(R.layout.server_auth_dialog, null);\n\n        String overriddenUrl = null;\n        if (url != null) {\n            if (!url.startsWith(webCredentialsUtils.getServerUrlFromPreferences())) {\n                overriddenUrl = url;\n                if (overriddenUrl.contains(\"?deviceID=\")) {\n                    overriddenUrl = overriddenUrl.substring(0, overriddenUrl.indexOf(\"?deviceID=\"));\n                }\n            }\n        }\n\n        username = dialogView.findViewById(R.id.username_edit);\n        password = dialogView.findViewById(R.id.password_edit);\n\n        // The custom username\\password take precedence\n        username.setText(customUsername != null ? customUsername : overriddenUrl != null ? null : webCredentialsUtils.getUserNameFromPreferences());\n        password.setText(customPassword != null ? customPassword : overriddenUrl != null ? null : webCredentialsUtils.getPasswordFromPreferences());\n\n        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);\n        builder.setTitle(context.getString(org.odk.collect.strings.R.string.server_requires_auth));\n        builder.setMessage(context.getString(org.odk.collect.strings.R.string.server_auth_credentials, overriddenUrl != null ? overriddenUrl : webCredentialsUtils.getServerUrlFromPreferences()));\n        builder.setView(dialogView);\n        String finalOverriddenUrl = overriddenUrl;\n        builder.setPositiveButton(context.getString(org.odk.collect.strings.R.string.ok), new DialogInterface.OnClickListener() {\n            @Override\n            public void onClick(DialogInterface dialog, int which) {\n                String userNameValue = username.getText().toString();\n                String passwordValue = password.getText().toString();\n\n                // If custom username, password were passed via intent extras, only keep them for\n                // the current submission. If the URL and credentials were set from user preferences,\n                // save the credentials provided in the dialog to user preferences.\n                if (customUsername != null && customPassword != null) {\n                    webCredentialsUtils.saveCredentials(finalOverriddenUrl != null ? finalOverriddenUrl : webCredentialsUtils.getServerUrlFromPreferences(), userNameValue, passwordValue);\n                } else if (finalOverriddenUrl == null) {\n                    webCredentialsUtils.saveCredentialsPreferences(userNameValue, passwordValue, propertyManager);\n                } else {\n                    webCredentialsUtils.saveCredentials(finalOverriddenUrl, userNameValue, passwordValue);\n                }\n\n                resultListener.updatedCredentials();\n            }\n        });\n        builder.setNegativeButton(context.getString(org.odk.collect.strings.R.string.cancel),\n                new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int which) {\n                        resultListener.cancelledUpdatingCredentials();\n                    }\n                });\n\n        builder.setCancelable(false);\n\n        return builder.create();\n    }\n\n    public void setCustomUsername(String customUsername) {\n        this.customUsername = customUsername;\n    }\n\n    public void setCustomPassword(String customPassword) {\n        this.customPassword = customPassword;\n    }\n\n    public interface AuthDialogUtilityResultListener {\n        void updatedCredentials();\n\n        void cancelledUpdatingCredentials();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/CSVUtils.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport org.jetbrains.annotations.Contract;\n\n/**\n * Escape strings according to https://tools.ietf.org/html/rfc4180\n */\npublic final class CSVUtils {\n\n    private CSVUtils() {\n\n    }\n\n    @Contract(value = \"null -> null; !null -> !null\", pure = true)\n    public static String getEscapedValueForCsv(String value) {\n        if (value == null) {\n            return null;\n        }\n\n        return quoteStringIfNeeded(escapeDoubleQuote(value));\n    }\n\n    @Contract(value = \"null -> null; !null -> !null\", pure = true)\n    private static String escapeDoubleQuote(String value) {\n        if (value == null) {\n            return null;\n        }\n\n        return value.replaceAll(\"\\\"\", \"\\\"\\\"\");\n    }\n\n    @Contract(value = \"null -> null; !null -> !null\", pure = true)\n    private static String quoteStringIfNeeded(String value) {\n        if (value == null) {\n            return null;\n        }\n\n        if (value.contains(\",\") || value.contains(\"\\n\") || value.contains(\"\\\"\")) {\n            return \"\\\"\" + value + \"\\\"\";\n        } else {\n            return value;\n        }\n    }\n}"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ChangeLockProvider.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.shared.locks.ChangeLock\nimport org.odk.collect.shared.locks.ThreadSafeBooleanChangeLock\nimport javax.inject.Singleton\n\n@Singleton\nclass ChangeLockProvider(private val changeLockFactory: () -> ChangeLock = { ThreadSafeBooleanChangeLock() }) :\n    ProjectDependencyFactory<ChangeLocks> {\n\n    private val locks: MutableMap<String, ChangeLock> = mutableMapOf()\n\n    @Deprecated(message = \"Use create() instead\")\n    fun getFormLock(projectId: String): ChangeLock {\n        return locks.getOrPut(\"form:$projectId\") { changeLockFactory() }\n    }\n\n    @Deprecated(message = \"Use create() instead\")\n    fun getInstanceLock(projectId: String): ChangeLock {\n        return locks.getOrPut(\"instance:$projectId\") { changeLockFactory() }\n    }\n\n    override fun create(projectId: String): ChangeLocks {\n        return ChangeLocks(getFormLock(projectId), getInstanceLock(projectId))\n    }\n}\n\ndata class ChangeLocks(val formsLock: ChangeLock, val instancesLock: ChangeLock)\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/CollectStrictMode.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.os.StrictMode\nimport android.os.StrictMode.ThreadPolicy\nimport org.odk.collect.android.BuildConfig\n\nobject CollectStrictMode {\n\n    @JvmStatic\n    fun enable() {\n        if (BuildConfig.DEBUG) {\n            val policyBuilder = ThreadPolicy.Builder()\n                .detectAll()\n                .permitDiskReads() // shared preferences are being read on main thread (`GetAndSubmitFormTest`)\n                .permitDiskWrites() // files are being created on the fly (`GetAndSubmitFormTest`)\n                .penaltyDeath()\n\n            policyBuilder.permitUnbufferedIo() // `ObjectInputStream#readObject` calls\n            StrictMode.setThreadPolicy(policyBuilder.build())\n        }\n    }\n\n    @JvmStatic\n    fun disable() {\n        StrictMode.setThreadPolicy(ThreadPolicy.Builder().permitCustomSlowCalls().build())\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ContentUriHelper.kt",
    "content": "/*\n * Copyright (C) 2018 Shobhit Agarwal\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.utilities\n\nimport android.content.ContentResolver\nimport android.net.Uri\nimport android.provider.MediaStore\nimport android.webkit.MimeTypeMap\nimport org.odk.collect.android.application.Collect\n\nobject ContentUriHelper {\n\n    @JvmStatic\n    fun getIdFromUri(contentUri: Uri): Long {\n        val lastSegmentIndex = contentUri.pathSegments.size - 1\n        val idSegment = contentUri.pathSegments[lastSegmentIndex]\n        return idSegment.toLong()\n    }\n\n    @JvmStatic\n    fun getFileExtensionFromUri(fileUri: Uri): String? {\n        val mimeType = Collect.getInstance().contentResolver.getType(fileUri)\n        var extension = if (fileUri.scheme != null && fileUri.scheme == ContentResolver.SCHEME_CONTENT) MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) else MimeTypeMap.getFileExtensionFromUrl(fileUri.toString())\n        if (extension == null || extension.isEmpty()) {\n            Collect.getInstance().contentResolver.query(fileUri, null, null, null, null).use { cursor ->\n                var name: String? = null\n                if (cursor != null && cursor.moveToFirst()) {\n                    name = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))\n                }\n                extension = name?.substring(name.lastIndexOf('.') + 1) ?: \"\"\n            }\n        }\n\n        if (extension!!.isEmpty() && mimeType != null && mimeType.contains(\"/\")) {\n            extension = mimeType.substring(mimeType.lastIndexOf('/') + 1)\n        }\n\n        return extension\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ContentUriProvider.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Build;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.FileProvider;\n\nimport org.apache.commons.io.IOUtils;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\nimport timber.log.Timber;\n\npublic final class ContentUriProvider {\n    private static final String HUAWEI_MANUFACTURER = \"Huawei\";\n\n    // https://stackoverflow.com/a/41309223/5479029\n    public Uri getUriForFile(@NonNull Context context, @NonNull String authority, @NonNull File file) {\n        if (HUAWEI_MANUFACTURER.equalsIgnoreCase(Build.MANUFACTURER)) {\n            Timber.w(\"%s: %s\", ContentUriProvider.class.getSimpleName(), \"Using a Huawei device Increased likelihood of failure...\");\n            try {\n                return FileProvider.getUriForFile(context, authority, file);\n            } catch (IllegalArgumentException e) {\n                Timber.w(e, \"%s: %s\", ContentUriProvider.class.getSimpleName(), \"ANR Risk -- Copying the file the location cache to avoid Huawei 'external-files-path' bug for N+ devices\");\n                // Note: Periodically clear this cache\n                final File cacheFolder = new File(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.CACHE), HUAWEI_MANUFACTURER);\n                final File cacheLocation = new File(cacheFolder, file.getName());\n                InputStream in = null;\n                OutputStream out = null;\n                try {\n                    in = new FileInputStream(file);\n                    out = new FileOutputStream(cacheLocation); // appending output stream\n                    IOUtils.copy(in, out);\n                    Timber.i(\"%s: %s\", ContentUriProvider.class.getSimpleName(), \"Completed Android N+ Huawei file copy. Attempting to return the cached file\");\n                    return FileProvider.getUriForFile(context, authority, cacheLocation);\n                } catch (IOException e1) {\n                    Timber.i(e1, \"%s: %s\", ContentUriProvider.class.getSimpleName(), \"Failed to copy the Huawei file. Re-throwing exception\");\n                    return null;\n                } finally {\n                    IOUtils.closeQuietly(in);\n                    IOUtils.closeQuietly(out);\n                }\n            }\n        } else {\n            return FileProvider.getUriForFile(context, authority, file);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ControllableLifecyleOwner.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.LifecycleRegistry\n\nclass ControllableLifecyleOwner : LifecycleOwner {\n    private var lifecycleRegistry = LifecycleRegistry(this).apply {\n        this.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)\n    }\n\n    override val lifecycle: LifecycleRegistry = lifecycleRegistry\n\n    fun start() {\n        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)\n    }\n\n    fun destroy() {\n        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/DialogUtils.java",
    "content": "/*\n * Copyright 2017 Yura Laguta\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport android.app.Activity;\nimport android.app.Dialog;\n\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.fragment.app.FragmentManager;\n\nimport timber.log.Timber;\n\n/**\n * Reusable code between dialogs for keeping consistency\n */\n\npublic final class DialogUtils {\n\n    private DialogUtils() {\n    }\n\n    /**\n     * Ensures that a dialog is shown safely and doesn't causes a crash. Useful in the event\n     * of a screen rotation, async operations or activity navigation.\n     *\n     * @param dialog   that needs to be shown\n     * @param activity that has the dialog\n     */\n    public static void showDialog(Dialog dialog, Activity activity) {\n\n        if (activity == null || activity.isFinishing()) {\n            return;\n        }\n        if (dialog == null || dialog.isShowing()) {\n            return;\n        }\n\n        try {\n            dialog.show();\n        } catch (Exception e) {\n            Timber.e(e);\n        }\n    }\n\n    /**\n     * It can be a bad idea to interact with Fragment instances. As much as possible we\n     * should be using arguments (for static data the dialog needs), Dagger (for dependencies) or\n     * ViewModel (for non static data) to get things into fragments so as to avoid crashes or\n     * weirdness when they are recreated.\n     */\n    @Deprecated\n    @Nullable\n    public static <T extends DialogFragment> T getDialog(Class<T> dialogClass, FragmentManager fragmentManager) {\n        return (T) fragmentManager.findFragmentByTag(dialogClass.getName());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/EncryptionUtils.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport android.net.Uri;\nimport android.util.Base64;\n\nimport androidx.annotation.Nullable;\n\nimport org.apache.commons.io.IOUtils;\nimport org.kxml2.io.KXmlSerializer;\nimport org.kxml2.kdom.Document;\nimport org.kxml2.kdom.Element;\nimport org.kxml2.kdom.Node;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.exception.EncryptionException;\nimport org.odk.collect.android.external.FormsContract;\nimport org.odk.collect.android.external.InstancesContract;\nimport org.odk.collect.android.javarosawrapper.InstanceMetadata;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.RandomAccessFile;\nimport java.io.UnsupportedEncodingException;\nimport java.security.InvalidAlgorithmParameterException;\nimport java.security.InvalidKeyException;\nimport java.security.KeyFactory;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.NoSuchProviderException;\nimport java.security.PublicKey;\nimport java.security.SecureRandom;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.X509EncodedKeySpec;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport javax.crypto.BadPaddingException;\nimport javax.crypto.Cipher;\nimport javax.crypto.CipherOutputStream;\nimport javax.crypto.IllegalBlockSizeException;\nimport javax.crypto.NoSuchPaddingException;\nimport javax.crypto.spec.IvParameterSpec;\nimport javax.crypto.spec.SecretKeySpec;\n\nimport timber.log.Timber;\n\nimport static org.odk.collect.android.utilities.ApplicationConstants.Namespaces.XML_OPENROSA_NAMESPACE;\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\n/**\n * Utility class for encrypting submissions during the SaveFormToDisk.\n *\n * @author mitchellsundt@gmail.com\n */\npublic class EncryptionUtils {\n    public static final String RSA_ALGORITHM = \"RSA\";\n    // the symmetric key we are encrypting with RSA is only 256 bits... use SHA-256\n    public static final String ASYMMETRIC_ALGORITHM = \"RSA/NONE/OAEPWithSHA256AndMGF1Padding\";\n    public static final String SYMMETRIC_ALGORITHM = \"AES/CFB/PKCS5Padding\";\n    public static final String UTF_8 = \"UTF-8\";\n    public static final int SYMMETRIC_KEY_LENGTH = 256;\n    public static final int IV_BYTE_LENGTH = 16;\n\n    // tags in the submission manifest\n\n    private static final String XML_ENCRYPTED_TAG_NAMESPACE =\n            \"http://www.opendatakit.org/xforms/encrypted\";\n    private static final String DATA = \"data\";\n    private static final String ID = \"id\";\n    private static final String VERSION = \"version\";\n    private static final String ENCRYPTED = \"encrypted\";\n    private static final String BASE64_ENCRYPTED_KEY = \"base64EncryptedKey\";\n    private static final String ENCRYPTED_XML_FILE = \"encryptedXmlFile\";\n    private static final String META = \"meta\";\n    private static final String INSTANCE_ID = \"instanceID\";\n    private static final String MEDIA = \"media\";\n    private static final String FILE = \"file\";\n    private static final String BASE64_ENCRYPTED_ELEMENT_SIGNATURE =\n            \"base64EncryptedElementSignature\";\n    private static final String NEW_LINE = \"\\n\";\n    private static final String ENCRYPTION_PROVIDER = \"BC\";\n\n    private EncryptionUtils() {\n    }\n\n    public static final class EncryptedFormInformation {\n        public final String formId;\n        public final String formVersion;\n        public final InstanceMetadata instanceMetadata;\n        public final PublicKey rsaPublicKey;\n        public final String base64RsaEncryptedSymmetricKey;\n        public final SecretKeySpec symmetricKey;\n        public final byte[] ivSeedArray;\n        private int ivCounter;\n        public final StringBuilder elementSignatureSource = new StringBuilder();\n        private boolean isNotBouncyCastle;\n\n        EncryptedFormInformation(String formId, String formVersion,\n                                 InstanceMetadata instanceMetadata, PublicKey rsaPublicKey) {\n            this.formId = formId;\n            this.formVersion = formVersion;\n            this.instanceMetadata = instanceMetadata;\n            this.rsaPublicKey = rsaPublicKey;\n\n            // generate the symmetric key from random bits...\n\n            SecureRandom r = new SecureRandom();\n            byte[] key = new byte[SYMMETRIC_KEY_LENGTH / 8];\n            r.nextBytes(key);\n            symmetricKey = new SecretKeySpec(key, SYMMETRIC_ALGORITHM);\n\n            // construct the fixed portion of the iv -- the ivSeedArray\n            // this is the md5 hash of the instanceID and the symmetric key\n            try {\n                MessageDigest md = MessageDigest.getInstance(\"MD5\");\n                md.update(instanceMetadata.instanceId.getBytes(UTF_8));\n                md.update(key);\n                byte[] messageDigest = md.digest();\n                ivSeedArray = Arrays.copyOf(messageDigest, IV_BYTE_LENGTH);\n            } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {\n                Timber.e(e, \"Unable to set md5 hash for instanceid and symmetric key.\");\n                throw new IllegalArgumentException(e.getMessage());\n            }\n\n            // construct the base64-encoded RSA-encrypted symmetric key\n            try {\n                Cipher pkCipher;\n                pkCipher = Cipher.getInstance(ASYMMETRIC_ALGORITHM);\n                // write AES key\n                pkCipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey);\n                byte[] pkEncryptedKey = pkCipher.doFinal(key);\n                String alg = pkCipher.getAlgorithm();\n                Timber.i(\"Algorithm Used: %s\", alg);\n                base64RsaEncryptedSymmetricKey = Base64.encodeToString(pkEncryptedKey, Base64.NO_WRAP);\n\n            } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {\n                Timber.e(e, \"Unable to encrypt the symmetric key.\");\n                throw new IllegalArgumentException(e.getMessage());\n            }\n\n            // start building elementSignatureSource...\n            appendElementSignatureSource(formId);\n            if (formVersion != null) {\n                appendElementSignatureSource(formVersion);\n            }\n            appendElementSignatureSource(base64RsaEncryptedSymmetricKey);\n\n            appendElementSignatureSource(instanceMetadata.instanceId);\n        }\n\n        public void appendElementSignatureSource(String value) {\n            elementSignatureSource.append(value).append('\\n');\n        }\n\n        public void appendFileSignatureSource(File file) {\n            String md5Hash = Md5.getMd5Hash(file);\n            appendElementSignatureSource(file.getName() + \"::\" + md5Hash);\n        }\n\n        public String getBase64EncryptedElementSignature() {\n            // Step 0: construct the text of the elements in elementSignatureSource (done)\n            //     Where...\n            //      * Elements are separated by newline characters.\n            //      * Filename is the unencrypted filename (no .enc suffix).\n            //      * Md5 hashes of the unencrypted files' contents are converted\n            //        to zero-padded 32-character strings before concatenation.\n            //      Assumes this is in the order:\n            //          formId\n            //          version   (omitted if null)\n            //          base64RsaEncryptedSymmetricKey\n            //          instanceId\n            //          for each media file { filename \"::\" md5Hash }\n            //          submission.xml \"::\" md5Hash\n\n            // Step 1: construct the (raw) md5 hash of Step 0.\n            byte[] messageDigest;\n            try {\n                MessageDigest md = MessageDigest.getInstance(\"MD5\");\n                md.update(elementSignatureSource.toString().getBytes(UTF_8));\n                messageDigest = md.digest();\n            } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {\n                Timber.e(e, \"Exception thrown while constructing md5 hash.\");\n                throw new IllegalArgumentException(e.getMessage());\n            }\n\n            // Step 2: construct the base64-encoded RSA-encrypted md5\n            try {\n                Cipher pkCipher;\n                pkCipher = Cipher.getInstance(ASYMMETRIC_ALGORITHM);\n                // write AES key\n                pkCipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey);\n                byte[] pkEncryptedKey = pkCipher.doFinal(messageDigest);\n                return Base64.encodeToString(pkEncryptedKey, Base64.NO_WRAP);\n\n            } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {\n                Timber.e(e, \"Unable to encrypt the symmetric key.\");\n                throw new IllegalArgumentException(e.getMessage());\n            }\n        }\n\n        public Cipher getCipher() throws InvalidKeyException,\n                InvalidAlgorithmParameterException, NoSuchAlgorithmException,\n                NoSuchPaddingException {\n            ++ivSeedArray[ivCounter % ivSeedArray.length];\n            ++ivCounter;\n            IvParameterSpec baseIv = new IvParameterSpec(ivSeedArray);\n            Cipher c;\n            try {\n                c = Cipher.getInstance(EncryptionUtils.SYMMETRIC_ALGORITHM, \"BC\");\n                isNotBouncyCastle = false;\n            } catch (NoSuchProviderException e) {\n                Timber.w(e, \"Unable to obtain BouncyCastle provider! Decryption may fail.\");\n                isNotBouncyCastle = true;\n                c = Cipher.getInstance(EncryptionUtils.SYMMETRIC_ALGORITHM);\n            }\n            c.init(Cipher.ENCRYPT_MODE, symmetricKey, baseIv);\n            return c;\n        }\n\n        public boolean isNotBouncyCastle() {\n            return isNotBouncyCastle;\n        }\n    }\n\n    /**\n     * Retrieve the encryption information for this uri.\n     *\n     * @param uri              an Instance uri\n     * @param instanceMetadata the metadata for this instance used to check if the form definition\n     *                         defines an instanceID\n     * @return an {@link EncryptedFormInformation} object if the form definition requests encryption\n     * and the record can be encrypted. {@code null} if the form definition does not request\n     * encryption or if the BouncyCastle implementation is not present.\n     * @throws EncryptionException if the form definition requests encryption but the record can't\n     *                             be encrypted\n     */\n    public static EncryptedFormInformation getEncryptedFormInformation(Uri uri, InstanceMetadata instanceMetadata) throws EncryptionException {\n        // fetch the form information\n        String formId;\n        String formVersion;\n        PublicKey pk;\n\n        Form form = null;\n\n        if (InstancesContract.CONTENT_ITEM_TYPE.equals(Collect.getInstance().getContentResolver().getType(uri))) {\n            Instance instance = new InstancesRepositoryProvider(Collect.getInstance()).create().get(ContentUriHelper.getIdFromUri(uri));\n            if (instance == null) {\n                String msg = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.not_exactly_one_record_for_this_instance);\n                Timber.e(new Error(msg));\n                throw new EncryptionException(msg, null);\n            }\n\n            formId = instance.getFormId();\n            formVersion = instance.getFormVersion();\n\n            List<Form> forms = new FormsRepositoryProvider(Collect.getInstance()).create().getAllByFormIdAndVersion(formId, formVersion);\n\n            // OK to finalize with form definition that was soft-deleted. OK if there are multiple\n            // forms with the same formid/version as long as only one is active (not deleted).\n            if (forms.isEmpty() || new FormsRepositoryProvider(Collect.getInstance()).create().getAllNotDeletedByFormIdAndVersion(formId, formVersion).size() > 1) {\n                String msg = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.not_exactly_one_blank_form_for_this_form_id);\n                Timber.d(msg);\n                throw new EncryptionException(msg, null);\n            }\n\n            form = forms.get(0);\n        } else if (FormsContract.CONTENT_ITEM_TYPE.equals(Collect.getInstance().getContentResolver().getType(uri))) {\n            throw new IllegalArgumentException(\"Can't get encryption info for Form URI!\");\n        }\n\n        formId = form.getFormId();\n        if (formId == null || formId.length() == 0) {\n            String msg = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.no_form_id_specified);\n            Timber.d(msg);\n            throw new EncryptionException(msg, null);\n        }\n        formVersion = form.getVersion();\n        String base64RsaPublicKey = form.getBASE64RSAPublicKey();\n\n        if (base64RsaPublicKey == null) {\n            return null; // this is legitimately not an encrypted form\n        }\n\n        byte[] publicKey = Base64.decode(base64RsaPublicKey, Base64.NO_WRAP);\n        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKey);\n        KeyFactory kf;\n        try {\n            kf = KeyFactory.getInstance(RSA_ALGORITHM);\n        } catch (NoSuchAlgorithmException e) {\n            String msg = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.phone_does_not_support_rsa);\n            Timber.d(e, \"%s due to %s \", msg, e.getMessage());\n            throw new EncryptionException(msg, e);\n        }\n        try {\n            pk = kf.generatePublic(publicKeySpec);\n        } catch (InvalidKeySpecException e) {\n            String msg = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.invalid_rsa_public_key);\n            Timber.d(e, \"%s due to %s \", msg, e.getMessage());\n            throw new EncryptionException(msg, e);\n        }\n\n        // submission must have an OpenRosa metadata block with a non-null instanceID\n        if (instanceMetadata.instanceId == null) {\n            throw new EncryptionException(\"This form does not specify an instanceID. You must specify one to enable encryption.\", null);\n        }\n\n        // For now, prevent encryption if the BouncyCastle implementation is not present.\n        // https://code.google.com/p/opendatakit/issues/detail?id=918\n        try {\n            Cipher.getInstance(EncryptionUtils.SYMMETRIC_ALGORITHM, ENCRYPTION_PROVIDER);\n        } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {\n            String msg;\n            if (e instanceof NoSuchAlgorithmException) {\n                msg = \"No BouncyCastle implementation of symmetric algorithm!\";\n            } else if (e instanceof NoSuchProviderException) {\n                msg = \"No BouncyCastle provider implementation of symmetric algorithm!\";\n            } else {\n                msg = \"No BouncyCastle provider for padding implementation of symmetric algorithm!\";\n            }\n            Timber.d(msg);\n            return null;\n        }\n\n        return new EncryptedFormInformation(formId, formVersion, instanceMetadata, pk);\n    }\n\n    private static void encryptFile(File file, EncryptedFormInformation formInfo)\n            throws IOException, EncryptionException {\n        File encryptedFile = new File(file.getParentFile(), file.getName()\n                + \".enc\");\n\n        if (encryptedFile.exists() && !encryptedFile.delete()) {\n            throw new IOException(\"Cannot overwrite \" + encryptedFile.getAbsolutePath()\n                    + \". Perhaps the file is locked?\");\n        }\n\n        // add elementSignatureSource for this file...\n        formInfo.appendFileSignatureSource(file);\n\n        RandomAccessFile randomAccessFile = null;\n        CipherOutputStream cipherOutputStream = null;\n        try {\n            Cipher c = formInfo.getCipher();\n\n            randomAccessFile = new RandomAccessFile(encryptedFile, \"rws\");\n            ByteArrayOutputStream encryptedData = new ByteArrayOutputStream();\n            cipherOutputStream = new CipherOutputStream(encryptedData, c);\n            InputStream fin = new FileInputStream(file);\n            byte[] buffer = new byte[2048];\n            int len = fin.read(buffer);\n            while (len != -1) {\n                cipherOutputStream.write(buffer, 0, len);\n                len = fin.read(buffer);\n            }\n            fin.close();\n            cipherOutputStream.flush();\n            cipherOutputStream.close();\n\n            randomAccessFile.write(encryptedData.toByteArray());\n\n            Timber.i(\"Encrpyted:%s -> %s\", file.getName(), encryptedFile.getName());\n        } catch (Exception e) {\n            String msg = \"Error encrypting: \" + file.getName() + \" -> \"\n                    + encryptedFile.getName();\n            Timber.e(e, \"%s due to %s \", msg, e.getMessage());\n            throw new EncryptionException(msg, e);\n        } finally {\n            IOUtils.closeQuietly(cipherOutputStream);\n\n            if (randomAccessFile != null) {\n                randomAccessFile.close();\n            }\n        }\n    }\n\n    public static boolean deletePlaintextFiles(File instanceXml, @Nullable File lastSaved) {\n        // NOTE: assume the directory containing the instanceXml contains ONLY\n        // files related to this one instance.\n        File instanceDir = instanceXml.getParentFile();\n\n        boolean allSuccessful = true;\n\n        // Delete files that do not end with \".enc\", and do not start with \".\";\n        // ignore directories\n        File[] instanceFiles = instanceDir.listFiles();\n        for (File f : instanceFiles) {\n            if (f.equals(instanceXml)) {\n                continue; // don't touch instance file\n            }\n            if (f.isDirectory()) {\n                continue; // don't handle directories\n            }\n            if (!f.getName().endsWith(\".enc\")) {\n                // not an encrypted file -- delete it!\n                allSuccessful = allSuccessful & f.delete(); // DO NOT\n                // short-circuit\n            }\n        }\n\n        // Delete the last-saved instance, if one exists.\n        if (lastSaved != null && lastSaved.exists()) {\n            allSuccessful &= lastSaved.delete();\n        }\n\n        return allSuccessful;\n    }\n\n    private static List<File> encryptSubmissionFiles(File instanceXml,\n                                                     File submissionXml, EncryptedFormInformation formInfo)\n            throws IOException, EncryptionException {\n        // NOTE: assume the directory containing the instanceXml contains ONLY\n        // files related to this one instance.\n        File instanceDir = instanceXml.getParentFile();\n\n        // encrypt files that do not end with \".enc\", and do not start with \".\";\n        // ignore directories\n        File[] allFiles = instanceDir.listFiles();\n        List<File> filesToProcess = new ArrayList<>();\n        for (File f : allFiles) {\n            if (f.equals(instanceXml)) {\n                continue; // don't touch restore file\n            }\n            if (f.equals(submissionXml)) {\n                continue; // handled last\n            }\n            if (f.isDirectory()) {\n                continue; // don't handle directories\n            }\n            if (f.getName().startsWith(\".\")) {\n                continue; // MacOSX garbage\n            }\n            if (f.getName().endsWith(\".enc\")) {\n                f.delete(); // try to delete this (leftover junk)\n            } else {\n                filesToProcess.add(f);\n            }\n        }\n        // encrypt here...\n        for (File f : filesToProcess) {\n            encryptFile(f, formInfo);\n        }\n\n        // encrypt the submission.xml as the last file...\n        encryptFile(submissionXml, formInfo);\n\n        return filesToProcess;\n    }\n\n    /**\n     * Constructs the encrypted attachments, encrypted form xml, and the\n     * plaintext submission manifest (with signature) for the form submission.\n     * <p>\n     * Does not delete any of the original files.\n     */\n    public static void generateEncryptedSubmission(File instanceXml,\n                                                   File submissionXml, EncryptedFormInformation formInfo)\n            throws IOException, EncryptionException {\n        // submissionXml is the submission data to be published to Aggregate\n        if (!submissionXml.exists() || !submissionXml.isFile()) {\n            throw new IOException(\"No submission.xml found\");\n        }\n\n        // TODO: confirm that this xml is not already encrypted...\n\n        // Step 1: encrypt the submission and all the media files...\n        List<File> mediaFiles = encryptSubmissionFiles(instanceXml,\n                submissionXml, formInfo);\n\n        // Step 2: build the encrypted-submission manifest (overwrites\n        // submission.xml)...\n        writeSubmissionManifest(formInfo, submissionXml, mediaFiles);\n    }\n\n    private static void writeSubmissionManifest(\n            EncryptedFormInformation formInfo,\n            File submissionXml, List<File> mediaFiles) throws EncryptionException {\n\n        Document d = new Document();\n        d.setStandalone(true);\n        d.setEncoding(UTF_8);\n        Element e = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, DATA);\n        e.setPrefix(null, XML_ENCRYPTED_TAG_NAMESPACE);\n        e.setAttribute(null, ID, formInfo.formId);\n        if (formInfo.formVersion != null) {\n            e.setAttribute(null, VERSION, formInfo.formVersion);\n        }\n        e.setAttribute(null, ENCRYPTED, \"yes\");\n        d.addChild(0, Node.ELEMENT, e);\n\n        int idx = 0;\n        Element c;\n        c = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, BASE64_ENCRYPTED_KEY);\n        c.addChild(0, Node.TEXT, formInfo.base64RsaEncryptedSymmetricKey);\n        e.addChild(idx++, Node.ELEMENT, c);\n\n        c = d.createElement(XML_OPENROSA_NAMESPACE, META);\n        c.setPrefix(\"orx\", XML_OPENROSA_NAMESPACE);\n        {\n            Element instanceTag = d.createElement(XML_OPENROSA_NAMESPACE, INSTANCE_ID);\n            instanceTag.addChild(0, Node.TEXT, formInfo.instanceMetadata.instanceId);\n            c.addChild(0, Node.ELEMENT, instanceTag);\n        }\n        e.addChild(idx++, Node.ELEMENT, c);\n        e.addChild(idx++, Node.IGNORABLE_WHITESPACE, NEW_LINE);\n\n        if (mediaFiles != null) {\n            for (File file : mediaFiles) {\n                c = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, MEDIA);\n                Element fileTag = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, FILE);\n                fileTag.addChild(0, Node.TEXT, file.getName() + \".enc\");\n                c.addChild(0, Node.ELEMENT, fileTag);\n                e.addChild(idx++, Node.ELEMENT, c);\n                e.addChild(idx++, Node.IGNORABLE_WHITESPACE, NEW_LINE);\n            }\n        }\n\n        c = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, ENCRYPTED_XML_FILE);\n        c.addChild(0, Node.TEXT, submissionXml.getName() + \".enc\");\n        e.addChild(idx++, Node.ELEMENT, c);\n\n        c = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, BASE64_ENCRYPTED_ELEMENT_SIGNATURE);\n        c.addChild(0, Node.TEXT, formInfo.getBase64EncryptedElementSignature());\n        e.addChild(idx, Node.ELEMENT, c);\n\n        FileOutputStream fout = null;\n        try {\n            fout = new FileOutputStream(submissionXml);\n\n            KXmlSerializer serializer = new KXmlSerializer();\n            serializer.setOutput(fout, UTF_8);\n            // setting the response content type emits the xml header.\n            // just write the body here...\n            d.writeChildren(serializer);\n            serializer.flush();\n            fout.getChannel().force(true);\n        } catch (Exception ex) {\n            String msg = \"Error writing submission.xml for encrypted submission: \"\n                    + submissionXml.getParentFile().getName();\n            Timber.e(ex, \"%s due to : %s \", msg, ex.getMessage());\n            throw new EncryptionException(msg, ex);\n        } finally {\n            IOUtils.closeQuietly(fout);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ExternalAppIntentProvider.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.net.Uri;\n\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.javarosa.xpath.parser.XPathSyntaxException;\nimport org.odk.collect.android.exception.ExternalParamsException;\nimport org.odk.collect.android.dynamicpreload.ExternalAppsUtils;\nimport org.odk.collect.android.javarosawrapper.FormController;\n\nimport java.util.Map;\n\npublic class ExternalAppIntentProvider {\n    // If an extra with this key is specified, it will be parsed as a URI and used as intent data\n    private static final String URI_KEY = \"uri_data\";\n\n    public Intent getIntentToRunExternalApp(FormController formController, FormEntryPrompt formEntryPrompt) throws ExternalParamsException, XPathSyntaxException {\n        String appearance = formEntryPrompt.getAppearanceHint();\n\n        String exSpec = appearance.substring(appearance.indexOf(Appearances.EX));\n        if (exSpec.contains(\")\")) {\n            exSpec = exSpec.substring(0, exSpec.lastIndexOf(')') + 1);\n        } else if (exSpec.contains(\" \")) {\n            exSpec = exSpec.substring(0, exSpec.indexOf(' '));\n        }\n        exSpec = exSpec.replaceFirst(\"^ex[:]\", \"\");\n\n        final String intentName = ExternalAppsUtils.extractIntentName(exSpec);\n        final Map<String, String> exParams = ExternalAppsUtils.extractParameters(exSpec);\n\n        Intent intent = new Intent(intentName);\n\n        // Use special \"uri_data\" key to set intent data. This must be done before checking if an\n        // activity is available to handle implicit intents.\n        if (exParams.containsKey(URI_KEY)) {\n            String uriValue = (String) ExternalAppsUtils.getValueRepresentedBy(exParams.get(URI_KEY),\n                    formEntryPrompt.getIndex().getReference(), formController);\n            intent.setData(Uri.parse(uriValue));\n            exParams.remove(URI_KEY);\n        }\n\n        ExternalAppsUtils.populateParameters(intent, exParams, formEntryPrompt.getIndex().getReference(), formController);\n        return intent;\n    }\n\n    // https://github.com/getodk/collect/issues/4194\n    public Intent getIntentToRunExternalAppWithoutDefaultCategory(FormController formController, FormEntryPrompt formEntryPrompt, PackageManager packageManager) throws ExternalParamsException {\n        String exSpec = formEntryPrompt.getAppearanceHint().replaceFirst(\"^ex[:]\", \"\");\n        final String intentName = ExternalAppsUtils.extractIntentName(exSpec);\n        final Map<String, String> exParams = ExternalAppsUtils.extractParameters(exSpec);\n\n        Intent intent = packageManager.getLaunchIntentForPackage(intentName);\n        if (intent != null) {\n            // Make sure FLAG_ACTIVITY_NEW_TASK is not set because it doesn't work with startActivityForResult\n            intent.setFlags(0);\n            ExternalAppsUtils.populateParameters(intent, exParams, formEntryPrompt.getIndex().getReference(), formController);\n        }\n\n        return intent;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ExternalizableFormDefCache.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.util.externalizable.ExtUtil;\nimport org.odk.collect.android.formentry.FormDefCache;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.DataInputStream;\nimport java.io.DataOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\n\nimport timber.log.Timber;\n\n/** Methods for reading from and writing to the FormDef cache */\npublic final class ExternalizableFormDefCache implements FormDefCache {\n\n    /**\n     * Serializes a FormDef and saves it in the cache. To avoid problems from two callers\n     * trying to cache the same file at the same time, we serialize into a temporary file,\n     * and rename it when done.\n     *\n     * @param formDef  - The FormDef to be cached\n     * @param formPath - The form XML file\n     */\n    @Override\n    public void writeCache(FormDef formDef, String formPath) throws IOException {\n        final long formSaveStart = System.currentTimeMillis();\n        File cachedFormDefFile = ExternalizableFormDefCache.getCacheFile(new File(formPath));\n        final File tempCacheFile = File.createTempFile(\"cache\", null,\n                new File(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.CACHE)));\n        Timber.i(\"Started saving %s to the cache via temp file %s\",\n                formDef.getTitle(), tempCacheFile.getName());\n\n        Exception caughtException = null;\n        try {\n            DataOutputStream dos = new DataOutputStream(new FileOutputStream(tempCacheFile));\n            formDef.writeExternal(dos);\n            dos.close();\n        } catch (IOException exception) {\n            caughtException = exception;\n        }\n\n        final boolean tempFileNeedsDeleting = caughtException != null; // There was an error creating it\n\n        // Delete or rename the temp file\n        if (tempFileNeedsDeleting) {\n            Timber.i(\"Deleting no-longer-wanted temp cache file %s for form %s\",\n                    tempCacheFile.getName(), formDef.getTitle());\n            if (!tempCacheFile.delete()) {\n                Timber.e(new Error(\"Unable to delete \" + tempCacheFile.getName()));\n            }\n        } else {\n            if (tempCacheFile.renameTo(cachedFormDefFile)) {\n                Timber.i(\"Renamed %s to %s\",\n                        tempCacheFile.getName(), cachedFormDefFile.getName());\n                Timber.i(\"Caching %s took %.3f seconds.\", formDef.getTitle(),\n                        (System.currentTimeMillis() - formSaveStart) / 1000F);\n            } else {\n                Timber.e(new Error(\"Unable to rename temporary file \" + tempCacheFile + \" to cache file \" + cachedFormDefFile));\n            }\n        }\n\n        if (caughtException != null) { // The client is no longer there, so log the exception\n            Timber.e(caughtException);\n        }\n    }\n\n    /**\n     * If a form is present in the cache, deserializes and returns it as as FormDef.\n     * @param formXml a File containing the XML version of the form\n     * @return a FormDef, or null if the form is not present in the cache\n     */\n    @Override\n    public FormDef readCache(File formXml) {\n        final File cachedForm = getCacheFile(formXml);\n        if (cachedForm.exists()) {\n            Timber.i(\"Attempting to load %s from cached file: %s.\", formXml.getName(), cachedForm.getName());\n            final long start = System.currentTimeMillis();\n\n            try {\n                final FormDef deserializedFormDef = deserializeFormDef(cachedForm);\n                if (deserializedFormDef != null) {\n                    Timber.i(\"Loaded in %.3f seconds.\", (System.currentTimeMillis() - start) / 1000F);\n                    return deserializedFormDef;\n                }\n            } catch (Exception e) {\n                // New .formdef will be created from XML\n                Timber.w(\"Deserialization FAILED! Deleting cache file: %s\", cachedForm.getAbsolutePath());\n                Timber.e(e, \"Deserialization FAILED!\");\n                cachedForm.delete();\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Builds and returns a File object for the cached version of a form.\n     * @param formXml the File containing the XML form\n     * @return a File object\n     */\n    private static File getCacheFile(File formXml) {\n        return new File(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.CACHE) + File.separator +\n                Md5.getMd5Hash(formXml) + \".formdef\");\n    }\n\n    private static FormDef deserializeFormDef(File serializedFormDef) throws Exception {\n        FormDef fd;\n        try (DataInputStream dis = new DataInputStream(new FileInputStream(serializedFormDef))) {\n            fd = new FormDef();\n            fd.readExternal(dis, ExtUtil.defaultPrototypes());\n        }\n\n        return fd;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/FileProvider.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport android.net.Uri;\n\npublic interface FileProvider {\n    Uri getURIForFile(String filePath);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/FileUtils.java",
    "content": "/*\n * Copyright (C) 2017 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\nimport static java.util.Arrays.asList;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.webkit.MimeTypeMap;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.google.common.base.CharMatcher;\n\nimport org.apache.commons.io.IOUtils;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.async.OngoingWorkListener;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.FileNameMap;\nimport java.net.URLConnection;\nimport java.nio.channels.FileChannel;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport timber.log.Timber;\n\n/**\n * Static methods used for common file operations.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n */\npublic final class FileUtils {\n    /** Suffix for the form media directory. */\n    public static final String MEDIA_SUFFIX = \"-media\";\n\n    /** Filename of the last-saved instance data. */\n    public static final String LAST_SAVED_FILENAME = \"last-saved.xml\";\n\n    /** Valid XML stub that can be parsed without error. */\n    public static final String STUB_XML = \"<?xml version='1.0' ?><stub />\";\n\n    private FileUtils() {\n    }\n\n    public static void saveAnswerFileFromUri(Uri uri, File destFile, Context context) {\n        try (InputStream fileInputStream = context.getContentResolver().openInputStream(uri);\n             OutputStream fileOutputStream = new FileOutputStream(destFile)) {\n            IOUtils.copy(fileInputStream, fileOutputStream);\n        } catch (IOException e) {\n            Timber.e(e);\n        }\n    }\n\n    public static File createDestinationMediaFile(String fileLocation, String fileExtension) {\n        return new File(fileLocation\n                + File.separator\n                + System.currentTimeMillis()\n                + \".\"\n                + fileExtension);\n    }\n\n    public static boolean createFolder(String path) {\n        File dir = new File(path);\n        return dir.exists() || dir.mkdirs();\n    }\n\n    public static String copyFile(File sourceFile, File destFile) {\n        if (sourceFile.exists()) {\n            String errorMessage = actualCopy(sourceFile, destFile);\n            if (errorMessage != null) {\n                try {\n                    Thread.sleep(500);\n                    Timber.e(new Error(\"Retrying to copy the file after 500ms: \" + sourceFile.getAbsolutePath()));\n                    errorMessage = actualCopy(sourceFile, destFile);\n                } catch (InterruptedException e) {\n                    Timber.i(e);\n                }\n            }\n            return errorMessage;\n        } else {\n            String msg = \"Source file does not exist: \" + sourceFile.getAbsolutePath();\n            Timber.e(new Error(msg));\n            return msg;\n        }\n    }\n\n    private static String actualCopy(File sourceFile, File destFile) {\n        FileInputStream fileInputStream = null;\n        FileOutputStream fileOutputStream = null;\n        FileChannel src = null;\n        FileChannel dst = null;\n        try {\n            fileInputStream = new FileInputStream(sourceFile);\n            src = fileInputStream.getChannel();\n            fileOutputStream = new FileOutputStream(destFile);\n            dst = fileOutputStream.getChannel();\n            dst.transferFrom(src, 0, src.size());\n            dst.force(true);\n            return null;\n        } catch (Exception e) {\n            if (e instanceof FileNotFoundException) {\n                Timber.e(e, \"FileNotFoundException while copying file\");\n            } else if (e instanceof IOException) {\n                Timber.e(e, \"IOException while copying file\");\n            } else {\n                Timber.e(e, \"Exception while copying file\");\n            }\n            return e.getMessage();\n        } finally {\n            IOUtils.closeQuietly(fileInputStream);\n            IOUtils.closeQuietly(fileOutputStream);\n            IOUtils.closeQuietly(src);\n            IOUtils.closeQuietly(dst);\n        }\n    }\n\n    public static void deleteAndReport(File file) {\n        if (file != null && file.exists()) {\n            // remove garbage\n            if (!file.delete()) {\n                Timber.d(\"%s will be deleted upon exit.\", file.getAbsolutePath());\n                file.deleteOnExit();\n            } else {\n                Timber.d(\"%s has been deleted.\", file.getAbsolutePath());\n            }\n        }\n    }\n\n    public static String getFormBasename(File formXml) {\n        return getFormBasename(formXml.getName());\n    }\n\n    public static String getFormBasename(String formFilePath) {\n        return formFilePath.substring(0, formFilePath.lastIndexOf('.'));\n    }\n\n    public static String constructMediaPath(String formFilePath) {\n        return getFormBasename(formFilePath) + MEDIA_SUFFIX;\n    }\n\n    public static File getFormMediaDir(File formXml) {\n        final String formFileName = getFormBasename(formXml);\n        return new File(formXml.getParent(), formFileName + MEDIA_SUFFIX);\n    }\n\n    public static String getFormBasenameFromMediaFolder(File mediaFolder) {\n        /*\n         * TODO (from commit 37e3467): Apparently the form name is neither\n         * in the formController nor the formDef. In fact, it doesn't seem to\n         * be saved into any object in JavaRosa. However, the mediaFolder\n         * has the substring of the file name in it, so we extract the file name\n         * from here. Awkward...\n         */\n        return mediaFolder.getName().split(MEDIA_SUFFIX)[0];\n    }\n\n    public static File getLastSavedFile(File formXml) {\n        return new File(getFormMediaDir(formXml), LAST_SAVED_FILENAME);\n    }\n\n    public static String getLastSavedPath(File mediaFolder) {\n        return mediaFolder.getAbsolutePath() + File.separator + LAST_SAVED_FILENAME;\n    }\n\n    /**\n     * Returns the path to the last-saved file for this form,\n     * creating a valid stub if it doesn't yet exist.\n     */\n    public static String getOrCreateLastSavedSrc(File formXml) {\n        File lastSavedFile = getLastSavedFile(formXml);\n\n        if (!lastSavedFile.exists()) {\n            write(lastSavedFile, STUB_XML.getBytes(StandardCharsets.UTF_8));\n        }\n\n        return \"jr://file/\" + LAST_SAVED_FILENAME;\n    }\n\n    /**\n     * @param mediaDir the media folder\n     */\n    public static void checkMediaPath(File mediaDir) {\n        if (mediaDir.exists() && mediaDir.isFile()) {\n            Timber.e(new Error(\"The media folder is already there and it is a FILE!! We will need to delete it and create a folder instead\"));\n            boolean deleted = mediaDir.delete();\n            if (!deleted) {\n                throw new RuntimeException(\n                        getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.fs_delete_media_path_if_file_error,\n                                mediaDir.getAbsolutePath()));\n            }\n        }\n\n        // the directory case\n        boolean createdOrExisted = createFolder(mediaDir.getAbsolutePath());\n        if (!createdOrExisted) {\n            throw new RuntimeException(\n                    getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.fs_create_media_folder_error,\n                            mediaDir.getAbsolutePath()));\n        }\n    }\n\n    public static void purgeMediaPath(String mediaPath) {\n        File tempMediaFolder = new File(mediaPath);\n        File[] tempMediaFiles = tempMediaFolder.listFiles();\n\n        if (tempMediaFiles != null) {\n            for (File tempMediaFile : tempMediaFiles) {\n                deleteAndReport(tempMediaFile);\n            }\n        }\n\n        deleteAndReport(tempMediaFolder);\n    }\n\n    public static byte[] read(File file) {\n        byte[] bytes = new byte[(int) file.length()];\n        try (InputStream is = new FileInputStream(file)) {\n            is.read(bytes);\n        } catch (IOException e) {\n            Timber.e(e);\n        }\n        return bytes;\n    }\n\n    public static void write(File file, byte[] data) {\n        // Make sure the directory path to this file exists.\n        file.getParentFile().mkdirs();\n\n        try (FileOutputStream fos = new FileOutputStream(file)) {\n            fos.write(data);\n        } catch (IOException e) {\n            Timber.e(e);\n        }\n    }\n\n    public static void grantFilePermissions(Intent intent, Uri uri, Context context) {\n        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);\n        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);\n    }\n\n    @SuppressWarnings(\"PMD.DoNotHardCodeSDCard\")\n    public static String expandAndroidStoragePath(String path) {\n        if (path != null && path.startsWith(\"/storage/emulated/0/\")) {\n            return \"/sdcard/\" + path.substring(\"/storage/emulated/0/\".length());\n        }\n\n        return path;\n    }\n\n    public static String getMimeType(File file) {\n        String extension = MimeTypeMap.getFileExtensionFromUrl(file.getAbsolutePath());\n        String mimeType = extension != null ? MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) : null;\n\n        if (mimeType == null || mimeType.isEmpty()) {\n            FileNameMap fileNameMap = URLConnection.getFileNameMap();\n            mimeType = fileNameMap.getContentTypeFor(file.getAbsolutePath());\n        }\n\n        if (mimeType == null || mimeType.isEmpty()) {\n            mimeType = URLConnection.guessContentTypeFromName(file.getName());\n        }\n\n        return mimeType;\n    }\n\n    public static List<File> listFiles(File file) {\n        if (file != null && file.exists()) {\n            return asList(file.listFiles());\n        } else {\n            return new ArrayList<>();\n        }\n    }\n\n    public static String getFilenameError(String filename) {\n        String possiblyRestricted = \"?:\\\"*|/\\\\<>\\u0000\";\n        boolean containsAt = filename.contains(\"@\");\n        boolean containsNonAscii = CharMatcher.ascii().matchesAllOf(filename);\n        boolean containsPossiblyRestricted = CharMatcher.anyOf(possiblyRestricted).matchesAnyOf(possiblyRestricted);\n        return \"Problem with project name file. Contains @: \" + containsAt + \", Contains non-ascii: \" + containsNonAscii + \", Contains restricted: \" + containsPossiblyRestricted;\n    }\n\n    /**\n     * Common routine to take a downloaded document save the contents in the file\n     * 'file'.\n     * <p>\n     * The file is saved into a temp folder and is moved to the final place if everything\n     * is okay, so that garbage is not left over on cancel.\n     */\n    public static void interuptablyWriteFile(InputStream inputStream, File destinationFile, File tempDir, OngoingWorkListener listener)\n            throws IOException, InterruptedException {\n\n        File tempFile = File.createTempFile(\n                destinationFile.getName(),\n                \".tempDownload\",\n                tempDir\n        );\n\n        // WiFi network connections can be renegotiated during a large download sequence.\n        // This will cause intermittent download failures.  Silently retry once after each\n        // failure.  Only if there are two consecutive failures do we abort.\n        boolean success = false;\n        int attemptCount = 0;\n        final int maxAttemptCount = 2;\n        while (!success && ++attemptCount <= maxAttemptCount) {\n            // write connection to file\n            InputStream is = null;\n            OutputStream os = null;\n\n            try {\n                is = inputStream;\n                os = new FileOutputStream(tempFile);\n\n                byte[] buf = new byte[4096];\n                int len;\n                while ((len = is.read(buf)) > 0 && (listener == null || !listener.isCancelled())) {\n                    os.write(buf, 0, len);\n                }\n                os.flush();\n                success = true;\n\n            } catch (Exception e) {\n                Timber.e(e);\n                // silently retry unless this is the last attempt,\n                // in which case we rethrow the exception.\n\n                FileUtils.deleteAndReport(tempFile);\n\n                if (attemptCount == maxAttemptCount) {\n                    throw e;\n                }\n            } finally {\n                if (os != null) {\n                    try {\n                        os.close();\n                    } catch (Exception e) {\n                        Timber.e(e);\n                    }\n                }\n                if (is != null) {\n                    try {\n                        // ensure stream is consumed...\n                        final long count = 1024L;\n                        while (is.skip(count) == count) {\n                            // skipping to the end of the http entity\n                        }\n                    } catch (Exception e) {\n                        // no-op\n                    }\n                    try {\n                        is.close();\n                    } catch (Exception e) {\n                        Timber.w(e);\n                    }\n                }\n            }\n\n            if (listener != null && listener.isCancelled()) {\n                FileUtils.deleteAndReport(tempFile);\n                throw new InterruptedException();\n            }\n        }\n\n        Timber.d(\"Completed downloading of %s. It will be moved to the proper path...\", tempFile.getAbsolutePath());\n\n        FileUtils.deleteAndReport(destinationFile);\n\n        String errorMessage = FileUtils.copyFile(tempFile, destinationFile);\n\n        if (destinationFile.exists()) {\n            Timber.d(\"Copied %s over %s\", tempFile.getAbsolutePath(), destinationFile.getAbsolutePath());\n            FileUtils.deleteAndReport(tempFile);\n        } else {\n            String msg = Collect.getInstance().getString(org.odk.collect.strings.R.string.fs_file_copy_error,\n                    tempFile.getAbsolutePath(), destinationFile.getAbsolutePath(), errorMessage);\n            throw new RuntimeException(msg);\n        }\n    }\n\n    public static void copyFileFromAssets(Context context, String fileDestPath, String fileSourcePath) throws IOException {\n        copyStreamToPath(getAssetAsStream(context, fileSourcePath), fileDestPath);\n    }\n\n    public static File copyFileFromResources(String fileSourcePath, File fileDest) throws IOException {\n        copyStreamToPath(getResourceAsStream(fileSourcePath), fileDest.getAbsolutePath());\n        return fileDest;\n    }\n\n    public static void copyFileFromResources(String fileSourcePath, String fileDestPath) throws IOException {\n        copyStreamToPath(getResourceAsStream(fileSourcePath), fileDestPath);\n    }\n\n    @NonNull\n    public static InputStream getAssetAsStream(Context context, String fileSourcePath) throws IOException {\n        return context.getAssets().open(fileSourcePath);\n    }\n\n    @Nullable\n    public static InputStream getResourceAsStream(String fileSourcePath) {\n        return FileUtils.class.getResourceAsStream(\"/\" + fileSourcePath);\n    }\n\n    private static void copyStreamToPath(InputStream inputStream, String fileDestPath) throws IOException {\n        try (InputStream input = inputStream;\n             OutputStream output = new FileOutputStream(fileDestPath)) {\n            IOUtils.copy(input, output);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/FormEntryPromptUtils.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport android.text.SpannableStringBuilder;\n\nimport org.javarosa.core.model.instance.TreeElement;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport java.util.List;\nimport java.util.Optional;\n\nimport javax.annotation.Nullable;\n\npublic final class FormEntryPromptUtils {\n\n    private FormEntryPromptUtils() {\n    }\n\n    public static CharSequence styledQuestionText(String questionText, boolean isRequired) {\n        CharSequence styledQuestionText = HtmlUtils.textToHtml(questionText);\n        return isRequired\n               /*\n                Question text should be added first, then the asterisk mark which represents\n                required questions. If the order is changed some styling might not work well.\n                */\n               ? new SpannableStringBuilder(styledQuestionText)\n                    .insert(0, \" \")\n                    .insert(0, HtmlUtils.textToHtml(\"<span style=\\\"color:#F44336\\\">*</span>\"))\n               : styledQuestionText;\n    }\n\n    @Nullable\n    public static String getBindAttribute(FormEntryPrompt prompt, String attributeName) {\n        List<TreeElement> attributes = prompt.getBindAttributes();\n        Optional<TreeElement> attribute = attributes.stream().filter(attr ->\n                attr.getName().equals(attributeName)\n        ).findAny();\n\n        return attribute.map(TreeElement::getAttributeValue).orElse(null);\n    }\n\n    @Nullable\n    public static String getAdditionalAttribute(FormEntryPrompt formEntryPrompt, String attributeName) {\n        String value = formEntryPrompt.getQuestion().getAdditionalAttribute(null, attributeName);\n        return value != null && !value.isEmpty() ? value : null;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/FormNameUtils.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic final class FormNameUtils {\n\n    private static final String CONTROL_CHAR_REGEX = \"[\\\\p{Cntrl}]\";\n    private static final Pattern CONTROL_CHAR_PATTERN = Pattern.compile(CONTROL_CHAR_REGEX);\n\n    private FormNameUtils() {\n    }\n\n    public static String normalizeFormName(String formName, boolean returnNullIfNothingChanged) {\n        if (formName == null) {\n            return null;\n        }\n\n        Matcher matcher = CONTROL_CHAR_PATTERN.matcher(formName);\n        return matcher.find()\n                ? matcher.replaceAll(\" \")\n                : (returnNullIfNothingChanged ? null : formName);\n    }\n\n    // Create a sanitized filename prefix from the given form name. No extension is added.\n    public static String formatFilenameFromFormName(String formName) {\n        if (formName == null) {\n            return null;\n        }\n        // Keep alphanumerics, replace others with a space\n        String fileName = formName.replaceAll(\"[^\\\\p{L}\\\\p{Digit}]\", \" \");\n        // Replace consecutive whitespace characters with single space\n        fileName = fileName.replaceAll(\"\\\\p{javaWhitespace}+\", \" \");\n        return fileName.trim();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/FormUtils.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.core.reference.RootTranslator;\nimport org.odk.collect.android.logic.FileReferenceFactory;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.forms.Form;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic final class FormUtils {\n\n    private FormUtils() {\n\n    }\n\n    @NonNull\n    public static List<File> getMediaFiles(@NonNull Form form) {\n        String formMediaPath = form.getFormMediaPath();\n        return formMediaPath == null\n                ? new ArrayList<>()\n                : FileUtils.listFiles(new File(formMediaPath));\n    }\n\n    /**\n     * @deprecated Use {@link FormUtils#setupReferenceManagerForForm(ReferenceManager, File, File)} instead\n     */\n    @Deprecated\n    public static void setupReferenceManagerForForm(ReferenceManager referenceManager, File formMediaDir) {\n        setupReferenceManagerForForm(referenceManager, new File(new StoragePathProvider().getProjectRootDirPath()), formMediaDir);\n    }\n\n    /**\n     * Configures the given reference manager to resolve jr:// URIs to a folder in the root ODK forms\n     * directory with name matching the name of the directory represented by {@code formMediaDir}.\n     * <p>\n     * E.g. if /foo/bar/baz is passed in as {@code formMediaDir}, jr:// URIs will be resolved to\n     * projectRoot/forms/baz.\n     */\n    public static void setupReferenceManagerForForm(ReferenceManager referenceManager, File projectRootDir, File formMediaDir) {\n        referenceManager.reset();\n\n        // Always build URIs against the project root, regardless of the absolute path of formMediaDir\n        referenceManager.addReferenceFactory(new FileReferenceFactory(projectRootDir.getAbsolutePath()));\n\n        addSessionRootTranslators(referenceManager,\n                buildSessionRootTranslators(formMediaDir.getName(), enumerateHostStrings()));\n    }\n\n    public static String[] enumerateHostStrings() {\n        return new String[]{\"images\", \"image\", \"audio\", \"video\", \"file-csv\", \"file\"};\n    }\n\n    public static List<RootTranslator> buildSessionRootTranslators(String formMediaDir, String[] hostStrings) {\n        List<RootTranslator> rootTranslators = new ArrayList<>();\n        // Set jr://... to point to <projectRoot>/forms/formBasename-media/\n        final String translatedPrefix = String.format(\"jr://file/forms/\" + formMediaDir + \"/\");\n        for (String t : hostStrings) {\n            rootTranslators.add(new RootTranslator(String.format(\"jr://%s/\", t), translatedPrefix));\n        }\n        return rootTranslators;\n    }\n\n    public static void addSessionRootTranslators(ReferenceManager referenceManager, List<RootTranslator> rootTranslators) {\n        for (RootTranslator rootTranslator : rootTranslators) {\n            referenceManager.addSessionRootTranslator(rootTranslator);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/FormsDownloadResultInterpreter.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.content.Context\nimport org.odk.collect.android.formmanagement.ServerFormDetails\nimport org.odk.collect.android.formmanagement.download.FormDownloadException\nimport org.odk.collect.android.formmanagement.download.FormDownloadExceptionMapper\nimport org.odk.collect.errors.ErrorItem\nimport org.odk.collect.strings.localization.getLocalizedString\n\nobject FormsDownloadResultInterpreter {\n    fun getFailures(result: Map<ServerFormDetails, FormDownloadException?>, context: Context) = result.filter {\n        it.value != null\n    }.map {\n        ErrorItem(\n            it.key.formName ?: \"\",\n            context.getLocalizedString(org.odk.collect.strings.R.string.form_details, it.key.formId ?: \"\", it.key.formVersion ?: \"\"),\n            FormDownloadExceptionMapper(context).getMessage(it.value)\n        )\n    }\n\n    fun getNumberOfFailures(result: Map<ServerFormDetails, FormDownloadException?>) = result.count {\n        it.value != null\n    }\n\n    fun allFormsDownloadedSuccessfully(result: Map<ServerFormDetails, FormDownloadException?>) = result.values.all {\n        it == null\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/FormsRepositoryProvider.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.content.Context\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.database.forms.DatabaseFormsRepository\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.storage.StoragePaths\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport org.odk.collect.projects.ProjectDependencyFactory\n\nclass FormsRepositoryProvider @JvmOverloads constructor(\n    private val context: Context,\n    private val storagePathFactory: ProjectDependencyFactory<StoragePaths> = StoragePathProvider(),\n    private val savepointsRepositoryProvider: ProjectDependencyFactory<SavepointsRepository> = SavepointsRepositoryProvider(\n        context,\n        storagePathFactory\n    )\n) : ProjectDependencyFactory<FormsRepository> {\n\n    private val clock = { System.currentTimeMillis() }\n\n    override fun create(projectId: String): FormsRepository {\n        val storagePaths = storagePathFactory.create(projectId)\n        return DatabaseFormsRepository(\n            context,\n            storagePaths.metaDir,\n            storagePaths.formsDir,\n            storagePaths.cacheDir,\n            clock,\n            savepointsRepositoryProvider.create(projectId)\n        )\n    }\n\n    @Deprecated(\"Creating dependency without specified project is dangerous\")\n    fun create(): FormsRepository {\n        val currentProject =\n            DaggerUtils.getComponent(Collect.getInstance()).currentProjectProvider()\n                .requireCurrentProject()\n        return create(currentProject.uuid)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/FormsUploadResultInterpreter.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.content.Context\nimport org.odk.collect.android.R\nimport org.odk.collect.android.instancemanagement.userVisibleInstanceName\nimport org.odk.collect.android.upload.FormUploadException\nimport org.odk.collect.errors.ErrorItem\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.strings.localization.getLocalizedString\n\nobject FormsUploadResultInterpreter {\n    fun getFailures(result: Map<Instance, FormUploadException?>, context: Context) = result.filter {\n        it.value != null\n    }.map {\n        ErrorItem(\n            it.key.userVisibleInstanceName(context.resources),\n            context.getLocalizedString(org.odk.collect.strings.R.string.form_details, it.key.formId ?: \"\", it.key.formVersion ?: \"\"),\n            it.value?.message ?: \"\"\n        )\n    }\n\n    fun getNumberOfFailures(result: Map<Instance, FormUploadException?>) = result.count {\n        it.value != null\n    }\n\n    fun allFormsUploadedSuccessfully(result: Map<Instance, FormUploadException?>) = result.values.all {\n        it == null\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/HtmlUtils.java",
    "content": "/*\n * Copyright (C) 2017 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport static org.odk.collect.shared.strings.StringUtils.trim;\n\nimport android.text.Html;\n\nimport java.util.regex.MatchResult;\n\npublic final class HtmlUtils {\n\n    private static ReplaceCallback.Callback createHeader = match -> {\n        int level = match.group(1).length();\n        return \"<h\" + level + \">\" + match.group(2).replaceAll(\"#+$\", \"\").trim() + \"</h\" + level\n                + \">\";\n    };\n\n    private static ReplaceCallback.Callback createParagraph = match -> {\n        String trimmed = match.group(1).trim();\n        if (trimmed.matches(\"(?i)^<\\\\/?(h|p|bl)\")) {\n            return match.group(1);\n        }\n        return \"<p>\" + trimmed + \"</p>\";\n    };\n\n    private static ReplaceCallback.Callback createSpan = new ReplaceCallback.Callback() {\n        public String matchFound(MatchResult match) {\n            String attributes = sanitizeAttributes(match.group(1));\n            return \"<font\" + attributes + \">\" + match.group(2).trim() + \"</font>\";\n        }\n\n        // throw away all styles except for color and font-family\n        private String sanitizeAttributes(String attributes) {\n\n            String stylesText = attributes.replaceAll(\"style=[\\\"'](.*?)[\\\"']\", \"$1\");\n            String[] styles = stylesText.trim().split(\";\");\n            StringBuffer stylesOutput = new StringBuffer();\n\n            for (String style : styles) {\n                String[] stylesAttributes = style.trim().split(\":\");\n                if (stylesAttributes.length == 2) {\n                    if (stylesAttributes[0].equals(\"color\")) {\n                        stylesOutput.append(\" color=\\\"\" + stylesAttributes[1] + \"\\\"\");\n                    }\n                    if (stylesAttributes[0].equals(\"font-family\")) {\n                        stylesOutput.append(\" face=\\\"\" + stylesAttributes[1] + \"\\\"\");\n                    }\n                }\n            }\n\n            return stylesOutput.toString();\n        }\n    };\n\n    private HtmlUtils() {\n\n    }\n\n    public static String markdownToHtml(String text) {\n\n        text = text.replaceAll(\"<([^a-zA-Z/])\", \"&lt;$1\");\n        // https://github.com/enketo/enketo-transformer/blob/master/src/markdown.js\n\n        // span - replaced &lt; and &gt; with <>\n        text = ReplaceCallback.replace(\"(?s)<\\\\s?span([^\\\\/\\n]*)>((?:(?!<\\\\/).)+)<\\\\/\\\\s?span\\\\s?>\",\n                text, createSpan);\n\n        //intermediary replacements keys for special characters, N/B: These symbols are not meant to be interpreted as markdown\n        text = text.replaceAll(\"\\\\\\\\#\", \"&#35;\");\n        text = text.replaceAll(\"\\\\\\\\\\\\\\\\\", \"&#92;\");\n        text = text.replaceAll(\"\\\\\\\\_\", \"&#95;\");\n        text = text.replaceAll(\"\\\\\\\\\\\\*\", \"&#42;\");\n\n        // strong\n        text = text.replaceAll(\"(?s)__(.*?)__\", \"<strong>$1</strong>\");\n        text = text.replaceAll(\"(?s)\\\\*\\\\*(.*?)\\\\*\\\\*\", \"<strong>$1</strong>\");\n\n        // emphasis using underscore\n        text = text.replaceAll(\"(^|[\\\\s]|[^\\\\w])_([^\\\\s][^_\\n]*)_($|[\\\\s]|[^\\\\w])\", \"$1<em>$2</em>$3\");\n\n        // emphasis using asterisk\n        text = text.replaceAll(\"\\\\*([^\\\\s][^\\\\*\\n]*)\\\\*\", \"<em>$1</em>\");\n\n        // links\n        text = text.replaceAll(\"\\\\[([^\\\\]]*)\\\\]\\\\(([^\\\\)]+)\\\\)\",\n                \"<a href=\\\"$2\\\" target=\\\"_blank\\\">$1</a>\");\n        // headers - requires ^ or breaks <font color=\"#f58a1f\">color</font>\n        text = ReplaceCallback.replace(\"^(#+)([^\\n]*)$\", text, createHeader);\n        // paragraphs\n        text = ReplaceCallback.replace(\"([^\\n]+)\\n\", text, createParagraph);\n\n        // replacing intermediary keys with the proper markdown symbols\n        text = text.replaceAll(\"&#35;\", \"#\");\n        text = text.replaceAll(\"&#42;\", \"*\");\n        text = text.replaceAll(\"&#95;\", \"_\");\n        text = text.replaceAll(\"&#92;\", \"\\\\\\\\\");\n        return text;\n    }\n\n    public static CharSequence textToHtml(String text) {\n        return text == null ? \"\" : trim(Html.fromHtml(markdownToHtml(text)));\n    }\n}\n\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressionController.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.content.Context\nimport org.odk.collect.android.R\nimport org.odk.collect.android.widgets.QuestionWidget\nimport org.odk.collect.androidshared.bitmap.ImageCompressor\nimport timber.log.Timber\n\nclass ImageCompressionController(private val imageCompressor: ImageCompressor) {\n    fun execute(\n        imagePath: String,\n        questionWidget: QuestionWidget,\n        context: Context,\n        imageSizeMode: String\n    ) {\n        var maxPixels: Int?\n        maxPixels = getMaxPixelsFromFormIfDefined(questionWidget)\n        if (maxPixels == null) {\n            maxPixels = getMaxPixelsFromSettings(context, imageSizeMode)\n        }\n        if (maxPixels != null && maxPixels > 0) {\n            imageCompressor.execute(imagePath, maxPixels)\n        }\n    }\n\n    private fun getMaxPixelsFromFormIfDefined(questionWidget: QuestionWidget): Int? {\n        for (bindAttribute in questionWidget.formEntryPrompt.bindAttributes) {\n            if (\"max-pixels\" == bindAttribute.name && ApplicationConstants.Namespaces.XML_OPENROSA_NAMESPACE == bindAttribute.namespace) {\n                try {\n                    return bindAttribute.attributeValue?.toInt()\n                } catch (e: NumberFormatException) {\n                    Timber.i(e)\n                }\n            }\n        }\n\n        return null\n    }\n\n    private fun getMaxPixelsFromSettings(context: Context, imageSizeMode: String): Int? {\n        val imageEntryValues = context.resources.getStringArray(R.array.image_size_entry_values)\n        return when (imageSizeMode) {\n            imageEntryValues[1] -> 640\n            imageEntryValues[2] -> 1024\n            imageEntryValues[3] -> 2048\n            imageEntryValues[4] -> 3072\n            else -> null\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/InstanceAutoDeleteChecker.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.instances.Instance\nimport java.util.Locale\n\nobject InstanceAutoDeleteChecker {\n\n    /**\n     * Returns whether instances of the form specified should be auto-deleted after successful\n     * update.\n     *\n     * If the form explicitly sets the auto-delete property, then it overrides the preference.\n     */\n    @JvmStatic\n    fun shouldInstanceBeDeleted(\n        formsRepository: FormsRepository,\n        isAutoDeleteEnabledInProjectSettings: Boolean,\n        instance: Instance\n    ): Boolean {\n        formsRepository.getLatestByFormIdAndVersion(instance.formId, instance.formVersion)?.let { form ->\n            return if (isAutoDeleteEnabledInProjectSettings) {\n                form.autoDelete == null || form.autoDelete.trim().lowercase(Locale.US) != \"false\"\n            } else {\n                form.autoDelete != null && form.autoDelete.trim().lowercase(Locale.US) == \"true\"\n            }\n        }\n\n        return false\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/InstanceUploaderUtils.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport android.content.Context;\nimport android.content.res.Resources;\n\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.instancemanagement.InstanceExtKt;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\n\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Set;\n\nimport static org.odk.collect.strings.localization.LocalizedApplicationKt.getLocalizedString;\n\npublic final class InstanceUploaderUtils {\n\n    public static final String DEFAULT_SUCCESSFUL_TEXT = \"full submission upload was successful!\";\n\n    private InstanceUploaderUtils() {\n    }\n\n    /**\n     * Returns a formatted message including submission results for all the filled forms accessible\n     * through instancesProcessed in the following structure:\n     *\n     * Instance name 1 - result\n     *\n     * Instance name 2 - result\n     */\n    public static String getUploadResultMessage(InstancesRepository instancesRepository, Context context, Map<String, String> result) {\n        Set<String> keys = result.keySet();\n        Iterator<String> it = keys.iterator();\n        StringBuilder message = new StringBuilder();\n\n        while (it.hasNext()) {\n            Instance instance = instancesRepository.get(Long.valueOf(it.next()));\n            message.append(getUploadResultMessageForInstances(instance, context.getResources(), result));\n        }\n\n        if (message.length() == 0) {\n            message = new StringBuilder(context.getString(org.odk.collect.strings.R.string.no_forms_uploaded));\n        }\n\n        return message.toString().trim();\n    }\n\n    private static String getUploadResultMessageForInstances(Instance instance, Resources resources, Map<String, String> resultMessagesByInstanceId) {\n        StringBuilder uploadResultMessage = new StringBuilder();\n        if (instance != null) {\n            String name = InstanceExtKt.userVisibleInstanceName(instance, resources);\n            String text = localizeDefaultAggregateSuccessfulText(resultMessagesByInstanceId.get(instance.getDbId().toString()));\n            uploadResultMessage\n                    .append(name)\n                    .append(\" - \")\n                    .append(text)\n                    .append(\"\\n\\n\");\n        }\n        return uploadResultMessage.toString();\n    }\n\n    private static String localizeDefaultAggregateSuccessfulText(String text) {\n        if (text != null && text.equals(DEFAULT_SUCCESSFUL_TEXT)) {\n            text = getLocalizedString(Collect.getInstance(), org.odk.collect.strings.R.string.success);\n        }\n        return text;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/InstancesRepositoryProvider.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.content.Context\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.database.instances.DatabaseInstancesRepository\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.storage.StoragePaths\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.projects.ProjectDependencyFactory\n\nclass InstancesRepositoryProvider @JvmOverloads constructor(\n    private val context: Context,\n    private val storagePathFactory: ProjectDependencyFactory<StoragePaths> = StoragePathProvider()\n) : ProjectDependencyFactory<InstancesRepository> {\n\n    override fun create(projectId: String): InstancesRepository {\n        val storagePaths = storagePathFactory.create(projectId)\n        return DatabaseInstancesRepository(\n            context,\n            storagePaths.metaDir,\n            storagePaths.instancesDir,\n            System::currentTimeMillis\n        )\n    }\n\n    @Deprecated(\"Creating dependency without specified project is dangerous\")\n    fun create(): InstancesRepository {\n        val currentProject =\n            DaggerUtils.getComponent(Collect.getInstance()).currentProjectProvider()\n                .requireCurrentProject()\n        return create(currentProject.uuid)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/LocaleHelper.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.shared.strings.StringUtils.isBlank\nimport java.util.Locale\nimport java.util.TreeMap\n\nobject LocaleHelper {\n    private val TRANSLATIONS_AVAILABLE = arrayOf(\n        \"af\", \"am\", \"ar\", \"bg\", \"bn\", \"ca\", \"cs\", \"da\", \"de\", \"en\", \"es\", \"et\", \"fa\", \"fa_AF\", \"fi\", \"fr\",\n        \"hi\", \"in\", \"it\", \"ja\", \"ht\", \"ka\", \"km\", \"ln\", \"lo_LA\", \"lt\", \"mg\", \"ml\", \"mr\", \"ms\", \"my\",\n        \"ne_NP\", \"nl\", \"no\", \"pl\", \"ps\", \"pt\", \"ro\", \"ru\", \"rw\", \"si\", \"sl\", \"so\", \"sq\", \"sr\",\n        \"sv_SE\", \"sw\", \"sw_KE\", \"te\", \"th_TH\", \"ti\", \"tl\", \"tr\", \"uk\", \"ur\", \"ur_PK\", \"vi\", \"zh\",\n        \"zh_TW\", \"zu\"\n    )\n\n    @JvmStatic\n    fun languageList(): TreeMap<String, String> {\n        val languageList = TreeMap<String, String>(java.lang.String.CASE_INSENSITIVE_ORDER)\n        for (localeCode in TRANSLATIONS_AVAILABLE) {\n            val locale = getLocale(localeCode)\n            languageList[locale.getDisplayName(locale)] = localeCode\n        }\n        return languageList\n    }\n\n    @JvmStatic\n    fun getLocale(localeCode: String?): Locale {\n        val sanitizedLocaleCode =\n            if (localeCode == null || isBlank(localeCode)) Collect.defaultSysLanguage else localeCode\n\n        return if (sanitizedLocaleCode.contains(\"_\")) {\n            val args = sanitizedLocaleCode.split(\"_\").toTypedArray()\n            Locale(args[0], args[1])\n        } else {\n            Locale(sanitizedLocaleCode)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/MediaUtils.kt",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.android.utilities\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport timber.log.Timber\nimport java.io.File\n\n/**\n * Consolidate all interactions with media providers here.\n *\n *\n * The functionality of getPath() was provided by paulburke as described here:\n * See\n * http://stackoverflow.com/questions/20067508/get-real-path-from-uri-android\n * -kitkat-new-storage-access-framework for details\n *\n * @author mitchellsundt@gmail.com\n * @author paulburke\n */\nclass MediaUtils(private val intentLauncher: IntentLauncher, private val contentUriProvider: ContentUriProvider) {\n    fun deleteMediaFile(imageFile: String) {\n        FileUtils.deleteAndReport(File(imageFile))\n    }\n\n    fun openFile(context: Context, file: File, expectedMimeType: String?) {\n        if (!file.exists()) {\n            val errorMsg: String = context.getString(org.odk.collect.strings.R.string.file_missing, file)\n            Timber.d(\"File %s is missing\", file)\n            ToastUtils.showLongToast(errorMsg)\n            return\n        }\n\n        val contentUri = contentUriProvider.getUriForFile(\n            context,\n            BuildConfig.APPLICATION_ID + \".provider\",\n            file\n        )\n\n        if (contentUri == null) {\n            ToastUtils.showLongToast(\"Can't open file. If you are on a Huawei device, this is expected and will not be fixed.\")\n            return\n        }\n\n        val intent = Intent().apply {\n            action = Intent.ACTION_VIEW\n            setDataAndType(contentUri, getMimeType(file, expectedMimeType))\n            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        }\n\n        intentLauncher.launch(context, intent) {\n            val message = context.getString(\n                org.odk.collect.strings.R.string.activity_not_found,\n                context.getString(org.odk.collect.strings.R.string.open_file)\n            )\n            ToastUtils.showLongToast(message)\n            Timber.w(message)\n        }\n    }\n\n    private fun getMimeType(file: File, expectedMimeType: String?) =\n        if (expectedMimeType == null || expectedMimeType.isEmpty()) {\n            FileUtils.getMimeType(file)\n        } else {\n            expectedMimeType\n        }\n\n    fun pickFile(activity: Activity, mimeType: String, requestCode: Int) {\n        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {\n            addCategory(Intent.CATEGORY_OPENABLE)\n            type = mimeType\n        }\n\n        intentLauncher.launchForResult(activity, intent, requestCode) {\n            // Do nothing\n        }\n    }\n\n    fun isVideoFile(file: File): Boolean {\n        return FileUtils.getMimeType(file).startsWith(\"video\")\n    }\n\n    fun isImageFile(file: File): Boolean {\n        return FileUtils.getMimeType(file).startsWith(\"image\")\n    }\n\n    fun isAudioFile(file: File): Boolean {\n        return FileUtils.getMimeType(file).startsWith(\"audio\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/MyanmarDateUtils.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport org.joda.time.LocalDateTime;\n\nimport mmcalendar.Language;\nimport mmcalendar.MyanmarCalendarKernel;\nimport mmcalendar.MyanmarDate;\nimport mmcalendar.MyanmarDateKernel;\nimport mmcalendar.Thingyan;\nimport mmcalendar.WesternDate;\n\npublic final class MyanmarDateUtils {\n\n    private MyanmarDateUtils() {\n    }\n\n    public static MyanmarDate gregorianDateToMyanmarDate(LocalDateTime localDateTime) {\n        return MyanmarDate.of(\n                localDateTime.getYear(),\n                localDateTime.getMonthOfYear(),\n                localDateTime.getDayOfMonth(),\n                localDateTime.getHourOfDay(),\n                localDateTime.getMinuteOfHour(),\n                localDateTime.getSecondOfMinute());\n    }\n\n    public static LocalDateTime myanmarDateToGregorianDate(MyanmarDate myanmarDate) {\n        WesternDate westernDate = WesternDate.of(myanmarDate);\n        return new LocalDateTime()\n                .withYear(westernDate.getYear())\n                .withMonthOfYear(westernDate.getMonth())\n                .withDayOfMonth(westernDate.getDay())\n                .withHourOfDay(0)\n                .withMinuteOfHour(0)\n                .withSecondOfMinute(0)\n                .withMillisOfSecond(0);\n    }\n\n    public static MyanmarDate createMyanmarDate(int myanmarYear, int myanmarMonthIndex, int myanmarMonthDay) {\n        return MyanmarDateKernel.julianToMyanmarDate(MyanmarDateKernel.myanmarDateToJulian(myanmarYear, myanmarMonthIndex, myanmarMonthDay));\n    }\n\n    public static String[] getMyanmarMonthsArray(int myanmarYear) {\n        return MyanmarCalendarKernel\n                .calculateRelatedMyanmarMonths(myanmarYear, 1)\n                .getMonthNameList(Language.MYANMAR)\n                .toArray(new String[0]);\n    }\n\n    public static int getFirstMonthDay(MyanmarDate myanmarDate) {\n        return isFirstYearMonth(myanmarDate) ? getNewYearsDay(myanmarDate.getYearValue()) : 1;\n    }\n\n    private static int getFirstMonthDay(int myanmarYear, int monthIndex) {\n        return isFirstYearMonth(myanmarYear, monthIndex) ? getNewYearsDay(myanmarYear) : 1;\n    }\n\n    public static int getMonthId(MyanmarDate myanmarDate) {\n        return MyanmarCalendarKernel\n                .calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1)\n                .getMonthNameList(Language.MYANMAR)\n                .indexOf(myanmarDate.getMonthName(Language.MYANMAR));\n    }\n\n    public static int getMonthLength(MyanmarDate myanmarDate) {\n        int newYearsDayOfNextYear = getNewYearsDay(myanmarDate.getYearValue() + 1);\n        return isLastMonthInYear(myanmarDate) && newYearsDayOfNextYear > 1\n                ? newYearsDayOfNextYear - 1\n                : myanmarDate.lengthOfMonth();\n    }\n\n    public static int getMonthLength(int myanmarYear, int monthIndex) {\n        MyanmarDate myanmarDate = MyanmarDateUtils.createMyanmarDate(myanmarYear, monthIndex, MyanmarDateUtils.getFirstMonthDay(myanmarYear, monthIndex));\n        return getMonthLength(myanmarDate);\n    }\n\n    private static int getNewYearsDay(int myanmarYear) {\n        return MyanmarDateKernel\n                .julianToMyanmarDate(Thingyan.of(myanmarYear).getMyanmarNewYearDay())\n                .getDayOfMonth();\n    }\n\n    private static boolean isLastMonthInYear(MyanmarDate myanmarDate) {\n        return getMonthId(myanmarDate) == getMyanmarMonthsArray(myanmarDate.getYearValue()).length - 1;\n    }\n\n    private static boolean isFirstYearMonth(MyanmarDate myanmarDate) {\n        return getMonthId(myanmarDate) == 0;\n    }\n\n    private static boolean isFirstYearMonth(int myanmarYear, int monthIndex) {\n        return monthIndex == MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarYear, 1).getMonthList().get(0);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/QuestionMediaManager.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.LiveData;\n\nimport org.odk.collect.utilities.Result;\n\nimport java.io.File;\n\n/**\n * Provides an interface for widgets to manage answer files. This lets them delete answer files\n * when an answer is cleared, replace answer files when an answer is changed and also access answer\n * files.\n */\npublic interface QuestionMediaManager {\n\n    LiveData<Result<File>> createAnswerFile(File file);\n\n    @Nullable\n    File getAnswerFile(String fileName);\n\n    void deleteAnswerFile(String questionIndex, String fileName);\n\n    void replaceAnswerFile(String questionIndex, String fileName);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/RankingItemTouchHelperCallback.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport android.graphics.Canvas;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.ItemTouchHelper;\n\nimport org.odk.collect.android.adapters.RankingListAdapter;\n\npublic class RankingItemTouchHelperCallback extends ItemTouchHelper.Callback {\n\n    private final RankingListAdapter adapter;\n\n    public RankingItemTouchHelperCallback(RankingListAdapter adapter) {\n        this.adapter = adapter;\n    }\n\n    @Override\n    public boolean isLongPressDragEnabled() {\n        return true;\n    }\n\n    @Override\n    public boolean isItemViewSwipeEnabled() {\n        return false;\n    }\n\n    @Override\n    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {\n        return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END);\n    }\n\n    @Override\n    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {\n        if (source.getItemViewType() != target.getItemViewType()) {\n            return false;\n        }\n\n        adapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());\n        return true;\n    }\n\n    @Override\n    public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {\n    }\n\n    @Override\n    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {\n        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { // change only the active item\n            ((RankingListAdapter.ItemViewHolder) viewHolder).onItemSelected();\n        }\n\n        super.onSelectedChanged(viewHolder, actionState);\n    }\n\n    @Override\n    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {\n        super.clearView(recyclerView, viewHolder);\n\n        ((RankingListAdapter.ItemViewHolder) viewHolder).onItemClear();\n    }\n\n    @Override\n    // Prevent out of bounds dragging\n    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float x, float y, int actionState, boolean isCurrentlyActive) {\n        float topY = viewHolder.itemView.getTop() + y;\n        float bottomY = topY + viewHolder.itemView.getHeight();\n        if (topY < 0) {\n            y = 0;\n        } else if (bottomY > recyclerView.getHeight()) {\n            y = recyclerView.getHeight() - viewHolder.itemView.getHeight() - viewHolder.itemView.getTop();\n        }\n        super.onChildDraw(c, recyclerView, viewHolder, x, y, actionState, isCurrentlyActive);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ReplaceCallback.java",
    "content": "/*\n * Copyright (C) 2016 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.regex.MatchResult;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n// http://stackoverflow.com/a/2563382/152938\n\npublic class ReplaceCallback {\n\n    private ReplaceCallback() {\n    }\n\n    /**\n     * Replaces with callback, with no limit to the number of replacements.\n     * Probably what you want most of the time.\n     */\n    public static String replace(String pattern, String subject, Callback callback) {\n        return replace(pattern, subject, -1, null, callback);\n    }\n\n    public static String replace(String pattern, String subject, int limit, Callback callback) {\n        return replace(pattern, subject, limit, null, callback);\n    }\n\n    /**\n     * @param regex    The regular expression pattern to search on.\n     * @param subject  The string to be replaced.\n     * @param limit    The maximum number of replacements to make. A negative value\n     *                 indicates replace all.\n     * @param count    If this is not null, it will be set to the number of\n     *                 replacements made.\n     * @param callback Callback function\n     */\n    public static String replace(String regex, String subject, int limit,\n                                 AtomicInteger count, Callback callback) {\n        StringBuffer sb = new StringBuffer();\n        Matcher matcher = Pattern.compile(regex).matcher(subject);\n        int i;\n        for (i = 0; (limit < 0 || i < limit) && matcher.find(); i++) {\n            String replacement = callback.matchFound(matcher.toMatchResult());\n            replacement = Matcher.quoteReplacement(replacement); //probably what you want...\n            matcher.appendReplacement(sb, replacement);\n        }\n        matcher.appendTail(sb);\n\n        if (count != null) {\n            count.set(i);\n        }\n        return sb.toString();\n    }\n\n    public interface Callback {\n        /**\n         * This function is called when a match is made. The string which was matched\n         * can be obtained via match.group(), and the individual groupings via\n         * match.group(n).\n         */\n        String matchFound(MatchResult match);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ResponseMessageParser.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport org.w3c.dom.Document;\nimport org.xml.sax.SAXException;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\n\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport timber.log.Timber;\n\npublic class ResponseMessageParser {\n\n    private static final String MESSAGE_XML_TAG = \"message\";\n\n    private boolean isValid;\n    private String messageResponse;\n\n    public boolean isValid() {\n        return this.isValid;\n    }\n\n    public String getMessageResponse() {\n        return messageResponse;\n    }\n\n    public void setMessageResponse(String response) {\n        isValid = false;\n        try {\n            if (response.contains(\"OpenRosaResponse\")) {\n                DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();\n                Document doc = builder.parse(new ByteArrayInputStream(response.getBytes()));\n                doc.getDocumentElement().normalize();\n\n                if (doc.getElementsByTagName(MESSAGE_XML_TAG).item(0) != null) {\n                    messageResponse = doc.getElementsByTagName(MESSAGE_XML_TAG).item(0).getTextContent();\n                    isValid = true;\n                }\n            }\n\n        } catch (SAXException | IOException | ParserConfigurationException e) {\n            Timber.e(e, \"Error parsing XML message due to %s \", e.getMessage());\n        }\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/SavepointsRepositoryProvider.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.content.Context\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.database.savepoints.DatabaseSavepointsRepository\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.storage.StoragePaths\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport org.odk.collect.projects.ProjectDependencyFactory\n\nclass SavepointsRepositoryProvider(\n    private val context: Context,\n    private val storagePathFactory: ProjectDependencyFactory<StoragePaths>\n) : ProjectDependencyFactory<SavepointsRepository> {\n\n    override fun create(projectId: String): SavepointsRepository {\n        val storagePaths = storagePathFactory.create(projectId)\n        return DatabaseSavepointsRepository(\n            context,\n            storagePaths.metaDir,\n            storagePaths.cacheDir,\n            storagePaths.instancesDir\n        )\n    }\n\n    @Deprecated(\"Creating dependency without specified project is dangerous\")\n    fun create(): SavepointsRepository {\n        val currentProject =\n            DaggerUtils.getComponent(Collect.getInstance()).currentProjectProvider()\n                .requireCurrentProject()\n        return create(currentProject.uuid)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/SelectOneWidgetUtils.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.form.api.FormEntryPrompt;\n\nimport java.util.List;\n\npublic final class SelectOneWidgetUtils {\n\n    private SelectOneWidgetUtils() {\n\n    }\n\n    public static Selection getSelectedItem(FormEntryPrompt prompt, List<SelectChoice> items) {\n        IAnswerData answer = prompt.getAnswerValue();\n        if (answer == null) {\n            return null;\n        } else if (answer instanceof SelectOneData) {\n            return (Selection) answer.getValue();\n        } else if (answer instanceof StringData) { // Fast external itemset\n            for (SelectChoice item : items) {\n                if (answer.getValue().equals(item.selection().xmlValue)) {\n                    return item.selection();\n                }\n            }\n            return null;\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/SoftKeyboardController.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.content.Context\nimport android.os.Handler\nimport android.os.Looper\nimport android.view.View\nimport android.view.inputmethod.InputMethodManager\n\nobject SoftKeyboardController {\n    fun showSoftKeyboard(view: View) {\n        // Subtle delay in displaying the keyboard is necessary for the keyboard to be displayed at all,\n        // for example, when returning from the hierarchy view.\n        Handler(Looper.getMainLooper()).postDelayed({\n            if (view.requestFocus()) {\n                val inputMethodManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager\n                inputMethodManager.showSoftInput(view, 0)\n            }\n        }, 100)\n    }\n\n    fun hideSoftKeyboard(view: View) {\n        val inputMethodManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager\n        inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ThemeUtils.java",
    "content": "/*\n * Copyright (C) 2018 Shobhit Agarwal\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport static org.odk.collect.androidshared.system.ContextExt.getThemeAttributeValue;\nimport static org.odk.collect.androidshared.system.ContextExt.isDarkTheme;\n\nimport android.content.Context;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.StyleRes;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.settings.SettingsProvider;\n\nimport javax.inject.Inject;\n\n/**\n * @deprecated Use\n * {@link org.odk.collect.androidshared.system.ContextExt#getThemeAttributeValue(Context, int)}\n * instead.\n */\n@Deprecated\npublic final class ThemeUtils {\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    private final Context context;\n\n    public ThemeUtils(Context context) {\n        DaggerUtils.getComponent(context).inject(this);\n        this.context = context;\n    }\n\n    @DrawableRes\n    public int getDivider() {\n        return isDarkTheme(context) ? android.R.drawable.divider_horizontal_dark : android.R.drawable.divider_horizontal_bright;\n    }\n\n    public boolean isSpinnerDatePickerDialogTheme(int theme) {\n        return theme == R.style.Theme_Collect_Dark_Spinner_DatePicker_Dialog ||\n                theme == R.style.Theme_Collect_Light_Spinner_DatePicker_Dialog;\n    }\n\n    @StyleRes\n    public int getCalendarDatePickerDialogTheme() {\n        return isDarkTheme(context)\n                ? R.style.Theme_Collect_Dark_Calendar_DatePicker_Dialog\n                : R.style.Theme_Collect_Light_Calendar_DatePicker_Dialog;\n    }\n\n    @StyleRes\n    public int getSpinnerDatePickerDialogTheme() {\n        return isDarkTheme(context) ?\n                R.style.Theme_Collect_Dark_Spinner_DatePicker_Dialog :\n                R.style.Theme_Collect_Light_Spinner_DatePicker_Dialog;\n    }\n\n    @StyleRes\n    public int getSpinnerTimePickerDialogTheme() {\n        return isDarkTheme(context) ?\n                R.style.Theme_Collect_Dark_Spinner_TimePicker_Dialog :\n                R.style.Theme_Collect_Light_Spinner_TimePicker_Dialog;\n    }\n\n    /**\n     * @return Text color for the current {@link android.content.res.Resources.Theme}\n     */\n    @ColorInt\n    public int getColorOnSurface() {\n        return getThemeAttributeValue(context, com.google.android.material.R.attr.colorOnSurface);\n    }\n\n    @ColorInt\n    public int getColorPrimary() {\n        return getThemeAttributeValue(context, androidx.appcompat.R.attr.colorPrimary);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/UnderlyingValuesConcat.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport com.google.common.base.Joiner;\nimport com.google.common.collect.FluentIterable;\n\nimport org.javarosa.core.model.SelectChoice;\n\nimport java.util.List;\n\npublic class UnderlyingValuesConcat {\n    public String asString(List<SelectChoice> items) {\n        return Joiner\n                .on(\", \")\n                .join(FluentIterable\n                        .from(items)\n                        .transform(item -> item.getValue())\n                );\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ViewUtils.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.content.Context\nimport kotlin.math.roundToInt\n\nobject ViewUtils {\n    @JvmStatic\n    fun dpFromPx(context: Context, px: Float): Int {\n        return (px / context.resources.displayMetrics.density).roundToInt()\n    }\n\n    @JvmStatic\n    fun pxFromDp(context: Context, dp: Float): Int {\n        return (dp * context.resources.displayMetrics.density).roundToInt()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/WebCredentialsUtils.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport android.net.Uri;\n\nimport androidx.annotation.NonNull;\n\nimport org.odk.collect.metadata.PropertyManager;\nimport org.odk.collect.openrosa.forms.OpenRosaXmlFetcher;\nimport org.odk.collect.openrosa.http.HttpCredentials;\nimport org.odk.collect.openrosa.http.HttpCredentialsInterface;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport javax.inject.Singleton;\n\n@Singleton\npublic class WebCredentialsUtils implements OpenRosaXmlFetcher.WebCredentialsProvider {\n\n    private final Settings generalSettings;\n\n    private static final Map<String, HttpCredentialsInterface> HOST_CREDENTIALS = new HashMap<>();\n\n    public WebCredentialsUtils(Settings generalSettings) {\n        this.generalSettings = generalSettings;\n    }\n\n    public void saveCredentials(@NonNull String url, @NonNull String username, @NonNull String password) {\n        if (username.isEmpty()) {\n            return;\n        }\n\n        String host = Uri.parse(url).getHost();\n        HOST_CREDENTIALS.put(host, new HttpCredentials(username, password));\n    }\n\n    public void saveCredentialsPreferences(String userName, String password, PropertyManager propertyManager) {\n        generalSettings.save(ProjectKeys.KEY_USERNAME, userName);\n        generalSettings.save(ProjectKeys.KEY_PASSWORD, password);\n\n        propertyManager.reload();\n    }\n\n    /**\n     * Forgets the temporary credentials saved in memory for a particular host. This is used when an\n     * activity that does some work requiring authentication is called with intent extras specifying\n     * credentials. Once the work is done, the temporary credentials are cleared so that different\n     * ones can be used on a later request.\n     *\n     * TODO: is this necessary in all cases it's used? Maybe it's needed if we want to be able to do\n     * an authenticated call followed by an anonymous one but even then, can't we pass in null\n     * username and password if the intent extras aren't set?\n     */\n    public void clearCredentials(@NonNull String url) {\n        if (url.isEmpty()) {\n            return;\n        }\n\n        String host = Uri.parse(url).getHost();\n        if (host != null) {\n            HOST_CREDENTIALS.remove(host);\n        }\n    }\n\n    public static void clearAllCredentials() {\n        HOST_CREDENTIALS.clear();\n    }\n\n    public String getServerUrlFromPreferences() {\n        return generalSettings.getString(ProjectKeys.KEY_SERVER_URL);\n    }\n\n    public String getPasswordFromPreferences() {\n        return generalSettings.getString(ProjectKeys.KEY_PASSWORD);\n    }\n\n    public String getUserNameFromPreferences() {\n        return generalSettings.getString(ProjectKeys.KEY_USERNAME);\n    }\n\n    /**\n     * Returns a credentials object from the url\n     *\n     * @param url to find the credentials object\n     * @return either null or an instance of HttpCredentialsInterface\n     */\n    public @NonNull HttpCredentialsInterface getCredentials(@NonNull URI url) {\n        String host = url.getHost();\n        String serverPrefsUrl = getServerUrlFromPreferences();\n        String prefsServerHost = (serverPrefsUrl == null) ? null : Uri.parse(serverPrefsUrl).getHost();\n\n        HttpCredentialsInterface hostCredentials = HOST_CREDENTIALS.get(host);\n\n        // URL host is the same as the host in preferences\n        if (prefsServerHost != null && prefsServerHost.equalsIgnoreCase(host)) {\n            // Use the temporary credentials if they exist, otherwise use the credentials saved to preferences\n            if (hostCredentials != null) {\n                return hostCredentials;\n            } else {\n                return new HttpCredentials(getUserNameFromPreferences(), getPasswordFromPreferences());\n            }\n        } else {\n            if (hostCredentials != null) {\n                return hostCredentials;\n            } else {\n                return new HttpCredentials(\"\", \"\");\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/utilities/ZipUtils.java",
    "content": "/*\n * Copyright (C) 2014 University of Washington\n *\n * Originally developed by Dobility, Inc. (as part of SurveyCTO)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n * use this file except in compliance with the License. You may obtain a copy of\n * the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport org.apache.commons.io.IOUtils;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipInputStream;\n\nimport timber.log.Timber;\n\n/**\n * Author: Meletis Margaritis\n * Date: 2/12/14\n * Time: 1:48 PM\n */\npublic final class ZipUtils {\n\n    private ZipUtils() {\n\n    }\n\n    public static void unzip(File[] zipFiles) {\n        for (File zipFile : zipFiles) {\n            try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zipFile))) {\n                ZipEntry zipEntry;\n                while ((zipEntry = zipInputStream.getNextEntry()) != null) {\n                    doExtractInTheSameFolder(zipFile, zipInputStream, zipEntry);\n                }\n            } catch (Exception e) {\n                Timber.e(e);\n            }\n        }\n    }\n\n    private static void doExtractInTheSameFolder(File zipFile, ZipInputStream zipInputStream,\n                                                 ZipEntry zipEntry) throws IOException {\n        File targetFile;\n        String fileName = zipEntry.getName();\n\n        Timber.i(\"Found zipEntry with name: %s\", fileName);\n\n        if (fileName.contains(\"/\") || fileName.contains(\"\\\\\")) {\n            // that means that this is a directory of a file inside a directory, so ignore it\n            Timber.w(\"Ignored: %s\", fileName);\n            return;\n        }\n\n        // extract the new file\n        targetFile = new File(zipFile.getParentFile(), fileName);\n        try (FileOutputStream fileOutputStream = new FileOutputStream(targetFile)) {\n            IOUtils.copy(zipInputStream, fileOutputStream);\n        }\n\n        Timber.i(\"Extracted file \\\"%s\\\" out of %s\", fileName, zipFile.getName());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/version/VersionDescriptionProvider.java",
    "content": "package org.odk.collect.android.version;\n\npublic interface VersionDescriptionProvider {\n    String getVersionDescription();\n}"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/version/VersionInformation.java",
    "content": "package org.odk.collect.android.version;\n\nimport androidx.annotation.Nullable;\n\npublic class VersionInformation {\n\n    private final VersionDescriptionProvider versionDescriptionProvider;\n\n    @Nullable\n    private String[] splitDescription;\n\n    public VersionInformation(VersionDescriptionProvider versionDescriptionProvider) {\n        this.versionDescriptionProvider = versionDescriptionProvider;\n    }\n\n    public String getSemanticVersion() {\n        return getVersionDescriptionComponents()[0];\n    }\n\n    @Nullable\n    public Integer getBetaNumber() {\n        if (isBeta()) {\n            String description = versionDescriptionProvider.getVersionDescription();\n            return Integer.parseInt(description.split(\"beta\")[1].substring(1, 2));\n        } else {\n            return null;\n        }\n    }\n\n    @Nullable\n    public String getCommitSHA() {\n         String[] components = getVersionDescriptionComponents();\n\n        if (isBeta() && components.length > 3) {\n            return components[3];\n        } else if (!isBeta() && components.length > 2) {\n            return components[2];\n        } else {\n            return null;\n        }\n    }\n\n    @Nullable\n    public Integer getCommitCount() {\n        String[] components = getVersionDescriptionComponents();\n\n        if (isBeta() && components.length > 3) {\n            return Integer.parseInt(components[2]);\n        } else if (!isBeta() && components.length > 2) {\n            return Integer.parseInt(components[1]);\n        } else {\n            return null;\n        }\n    }\n\n    public String getVersionToDisplay() {\n        if (getBetaNumber() != null) {\n            return getSemanticVersion() + \" Beta \" + getBetaNumber();\n        } else {\n            return getSemanticVersion();\n        }\n    }\n\n    public boolean isRelease() {\n        return getVersionDescriptionComponents().length == 1;\n    }\n\n    public boolean isBeta() {\n        return versionDescriptionProvider.getVersionDescription().contains(\"beta\");\n    }\n\n    public boolean isDirty() {\n        return versionDescriptionProvider.getVersionDescription().contains(\"dirty\");\n    }\n\n    private String[] getVersionDescriptionComponents() {\n        if (splitDescription == null) {\n            splitDescription = versionDescriptionProvider.getVersionDescription().split(\"-\");\n        }\n\n        return splitDescription;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/ChoicesRecyclerView.java",
    "content": "package org.odk.collect.android.views;\n\nimport android.content.Context;\nimport android.graphics.Rect;\nimport android.graphics.drawable.Drawable;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.ContextCompat;\nimport androidx.recyclerview.widget.DividerItemDecoration;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.google.android.flexbox.FlexboxLayoutManager;\n\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.adapters.AbstractSelectListAdapter;\nimport org.odk.collect.androidshared.utils.ScreenUtils;\n\npublic class ChoicesRecyclerView extends RecyclerView {\n    /**\n     * A list of choices can have thousands of items. To increase loading and scrolling performance,\n     * a RecyclerView is used. Because it is nested inside a ScrollView, by default, all of\n     * the RecyclerView's items are loaded and there is no performance benefit over a ListView.\n     * This constant is used to bound the number of items loaded. The value 40 was chosen because\n     * it is around the maximum number of elements that can be shown on a large tablet.\n     */\n    private static final int MAX_ITEMS_WITHOUT_SCREEN_BOUND = 40;\n\n    public ChoicesRecyclerView(@NonNull Context context) {\n        super(context);\n    }\n\n    public ChoicesRecyclerView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public void initRecyclerView(AbstractSelectListAdapter adapter, boolean isFlex) {\n        if (isFlex) {\n            enableFlexboxLayout();\n        } else {\n            enableGridLayout(adapter.getNumColumns());\n        }\n        setAdapter(adapter);\n        adjustRecyclerViewSize();\n    }\n\n    private void enableFlexboxLayout() {\n        FlexboxLayoutManager layoutManager = new FlexboxLayoutManager(getContext());\n        setLayoutManager(layoutManager);\n        int marginBetweenItems = getResources().getDimensionPixelSize(org.odk.collect.androidshared.R.dimen.margin_standard);\n        addItemDecoration(new FlexItemDecoration(marginBetweenItems));\n    }\n\n    private void enableGridLayout(int numColumns) {\n        if (numColumns == 1) {\n            enableDivider();\n        }\n\n        setLayoutManager(new GridLayoutManager(getContext(), numColumns));\n    }\n\n    private void enableDivider() {\n        DividerItemDecoration divider = new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL);\n        Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.inset_divider_64dp);\n        divider.setDrawable(drawable);\n        addItemDecoration(divider);\n    }\n\n    private void adjustRecyclerViewSize() {\n        if (getAdapter().getItemCount() > MAX_ITEMS_WITHOUT_SCREEN_BOUND) {\n            // Only let the RecyclerView take up 90% of the screen height in order to speed up loading if there are many items\n            getLayoutParams().height = (int) (ScreenUtils.getScreenHeight(getContext()) * 0.9);\n        }\n    }\n\n    static class FlexItemDecoration extends RecyclerView.ItemDecoration {\n        private final int margin;\n\n        FlexItemDecoration(int margin) {\n            this.margin = margin;\n        }\n\n        @Override\n        public void getItemOffsets(Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {\n            outRect.bottom = margin;\n            outRect.right = margin;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/CustomNumberPicker.kt",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.views\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.EditText\nimport android.widget.NumberPicker\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils\n\nclass CustomNumberPicker(context: Context, attrs: AttributeSet) : NumberPicker(context, attrs) {\n    override fun addView(view: View) {\n        super.addView(view)\n        updateView(view)\n    }\n\n    override fun addView(view: View, index: Int, params: ViewGroup.LayoutParams) {\n        super.addView(view, index, params)\n        updateView(view)\n    }\n\n    override fun addView(view: View, params: ViewGroup.LayoutParams) {\n        super.addView(view, params)\n        updateView(view)\n    }\n\n    private fun updateView(view: View) {\n        if (view is EditText) {\n            view.textSize = QuestionFontSizeUtils.getQuestionFontSize().toFloat()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/CustomWebView.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.webkit.WebView;\n\npublic class CustomWebView extends WebView {\n\n    public CustomWebView(Context context) {\n        super(context);\n    }\n\n    public CustomWebView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    private boolean suppressFlingGesture;\n\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        requestDisallowInterceptTouchEvent(true);\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                suppressFlingGesture = true;\n                break;\n            case MotionEvent.ACTION_UP:\n                suppressFlingGesture = false;\n                break;\n        }\n        return super.onTouchEvent(event);\n    }\n\n    public void setClickable(boolean enabled) {\n        setOnTouchListener((v, event) -> !enabled);\n        super.setClickable(enabled);\n    }\n\n    public boolean suppressFlingGesture() {\n        return suppressFlingGesture;\n    }\n}"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/DayNightProgressDialog.java",
    "content": "package org.odk.collect.android.views;\n\nimport static org.odk.collect.androidshared.system.ContextExt.isDarkTheme;\n\nimport android.app.ProgressDialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\n\nimport org.odk.collect.android.utilities.ThemeUtils;\nimport org.odk.collect.material.MaterialProgressDialogFragment;\n\n/**\n * {@link ProgressDialog} that uses dark or light themes correctly. This will not use correct theme\n * colors/styles however and just use Material defaults instead. Button colors are corrected to\n * prevent them being invisible.\n *\n * @deprecated {@link ProgressDialog} is deprecated and should be replaced by\n * {@link androidx.appcompat.app.AlertDialog} or\n * {@link MaterialProgressDialogFragment}\n */\n\n@Deprecated\npublic class DayNightProgressDialog extends ProgressDialog {\n\n    public DayNightProgressDialog(Context context) {\n        super(context, isDarkTheme(context) ? android.R.style.Theme_Material_Dialog_Alert : android.R.style.Theme_Material_Light_Dialog_Alert);\n\n        setOnShowListener(dialog -> {\n            ((ProgressDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(new ThemeUtils(((ProgressDialog) dialog).getContext()).getColorPrimary());\n            ((ProgressDialog) dialog).getButton(DialogInterface.BUTTON_NEGATIVE).setTextColor(new ThemeUtils(((ProgressDialog) dialog).getContext()).getColorPrimary());\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/DecoratedBarcodeView.kt",
    "content": "package org.odk.collect.android.views\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.journeyapps.barcodescanner.DecoratedBarcodeView\n\nclass DecoratedBarcodeView : DecoratedBarcodeView {\n    constructor(context: Context) : super(context) {\n        initialize()\n    }\n\n    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {\n        initialize()\n    }\n\n    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(\n        context,\n        attrs,\n        defStyleAttr\n    ) {\n        initialize()\n    }\n\n    private fun initialize() {\n        if (test) {\n            visibility = INVISIBLE\n        }\n    }\n\n    companion object {\n        var test = false\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/SlidingTabLayout.java",
    "content": "package org.odk.collect.android.views;\n/*\n * Copyright 2014 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport android.content.Context;\nimport android.graphics.Typeface;\nimport android.util.AttributeSet;\nimport android.util.SparseArray;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.HorizontalScrollView;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.core.content.ContextCompat;\nimport androidx.fragment.app.Fragment;\nimport androidx.viewpager.widget.PagerAdapter;\nimport androidx.viewpager.widget.ViewPager;\n\n/**\n * To be used with ViewPager to provide a tab indicator component which give constant feedback as to\n * the user's scroll progress.\n * <p>\n * To use the component, simply add it to your view hierarchy. Then in your\n * {@link android.app.Activity} or {@link Fragment} call\n * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for.\n * <p>\n * The colors can be customized in two ways. The first and simplest is to provide an array of colors\n * via {@link #setSelectedIndicatorColors(int...)}. The\n * alternative is via the {@link TabColorizer} interface which provides you complete control over\n * which color is used for any individual position.\n * <p>\n * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)},\n * providing the layout ID of your custom layout.\n */\npublic class SlidingTabLayout extends HorizontalScrollView {\n    private static final int TITLE_OFFSET_DIPS = 24;\n    private static final int TAB_VIEW_PADDING_DIPS = 10;\n    private static final int TAB_VIEW_TEXT_SIZE_SP = 21;\n    private final SlidingTabStrip tabStrip;\n    private int titleOffset;\n\n    private int tabViewLayoutId;\n    private int tabViewTextViewId;\n    private boolean distributeEvenly;\n\n    private ViewPager viewPager;\n    private final SparseArray<String> contentDescriptions = new SparseArray<>();\n    private ViewPager.OnPageChangeListener viewPagerPageChangeListener;\n    private int titleFontSize = -1;\n    private int titleFontColor = -1;\n\n    public SlidingTabLayout(Context context) {\n        this(context, null);\n    }\n\n    public SlidingTabLayout(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n\n        // Disable the Scroll Bar\n        setHorizontalScrollBarEnabled(false);\n        // Make sure that the Tab Strips fills this View\n        setFillViewport(true);\n\n        titleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density);\n\n        tabStrip = new SlidingTabStrip(context);\n        addView(tabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);\n    }\n\n    /**\n     * Set the custom {@link TabColorizer} to be used.\n     * <p>\n     * If you only require simple custmisation then you can use\n     * {@link #setSelectedIndicatorColors(int...)} to achieve\n     * similar effects.\n     */\n    public void setCustomTabColorizer(TabColorizer tabColorizer) {\n        tabStrip.setCustomTabColorizer(tabColorizer);\n    }\n\n    public void setDistributeEvenly(boolean distributeEvenly) {\n        this.distributeEvenly = distributeEvenly;\n    }\n\n    /**\n     * Sets the colors to be used for indicating the selected tab. These colors are treated as a\n     * circular array. Providing one color will mean that all tabs are indicated with the same color.\n     */\n    public void setSelectedIndicatorColors(int... colors) {\n        tabStrip.setSelectedIndicatorColors(colors);\n    }\n\n    /**\n     * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are\n     * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so\n     * that the layout can update it's scroll position correctly.\n     *\n     * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener)\n     */\n    public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {\n        viewPagerPageChangeListener = listener;\n    }\n\n    /**\n     * Set the custom layout to be inflated for the tab views.\n     *\n     * @param layoutResId Layout id to be inflated\n     * @param textViewId  id of the {@link TextView} in the inflated view\n     */\n    public void setCustomTabView(int layoutResId, int textViewId) {\n        tabViewLayoutId = layoutResId;\n        tabViewTextViewId = textViewId;\n    }\n\n    /**\n     * Sets the associated view pager. Note that the assumption here is that the pager content\n     * (number of tabs and tab titles) does not change after this call has been made.\n     */\n    public void setViewPager(ViewPager viewPager) {\n        tabStrip.removeAllViews();\n\n        this.viewPager = viewPager;\n        if (viewPager != null) {\n            viewPager.addOnPageChangeListener(new InternalViewPagerListener());\n            populateTabStrip();\n        }\n    }\n\n    /**\n     * Create a default view to be used for tabs. This is called if a custom tab view is not set via\n     * {@link #setCustomTabView(int, int)}.\n     */\n    protected TextView createDefaultTabView(Context context) {\n        TextView textView = new TextView(context);\n        textView.setGravity(Gravity.CENTER);\n        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP);\n        textView.setTypeface(Typeface.DEFAULT_BOLD);\n        textView.setLayoutParams(new LinearLayout.LayoutParams(\n                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));\n\n        TypedValue outValue = new TypedValue();\n        getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground,\n                outValue, true);\n        textView.setBackgroundResource(outValue.resourceId);\n        textView.setAllCaps(false);\n\n        int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density);\n        textView.setPadding(padding, padding, padding, padding);\n\n        return textView;\n    }\n\n    private void populateTabStrip() {\n        final PagerAdapter adapter = viewPager.getAdapter();\n        final View.OnClickListener tabClickListener = new TabClickListener();\n\n        for (int i = 0; i < adapter.getCount(); i++) {\n            View tabView = null;\n            TextView tabTitleView = null;\n\n            if (tabViewLayoutId != 0) {\n                // If there is a custom tab view layout id set, try and inflate it\n                tabView = LayoutInflater.from(getContext()).inflate(tabViewLayoutId, tabStrip,\n                        false);\n                tabTitleView = tabView.findViewById(tabViewTextViewId);\n            }\n\n            if (tabView == null) {\n                tabView = createDefaultTabView(getContext());\n            }\n\n            if (tabTitleView == null && TextView.class.isInstance(tabView)) {\n                tabTitleView = (TextView) tabView;\n            }\n\n            if (distributeEvenly) {\n                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabView.getLayoutParams();\n                lp.width = 0;\n                lp.weight = 1;\n            }\n\n            if (titleFontSize != -1) {\n                tabTitleView.setTextSize(titleFontSize);\n            }\n\n            if (titleFontColor != -1) {\n                tabTitleView.setTextColor(ContextCompat.getColor(getContext(), titleFontColor));\n            }\n\n            tabTitleView.setText(adapter.getPageTitle(i));\n            tabView.setOnClickListener(tabClickListener);\n            String desc = contentDescriptions.get(i, null);\n            if (desc != null) {\n                tabView.setContentDescription(desc);\n            }\n\n            tabStrip.addView(tabView);\n            if (i == viewPager.getCurrentItem()) {\n                tabView.setSelected(true);\n            }\n        }\n    }\n\n    public void setFontSize(int fontSize) {\n        titleFontSize = fontSize;\n    }\n\n    public void setFontColor(int color) {\n        titleFontColor = color;\n    }\n\n    public void setContentDescription(int i, String desc) {\n        contentDescriptions.put(i, desc);\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n\n        if (viewPager != null) {\n            scrollToTab(viewPager.getCurrentItem(), 0);\n        }\n    }\n\n    private void scrollToTab(int tabIndex, int positionOffset) {\n        final int tabStripChildCount = tabStrip.getChildCount();\n        if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) {\n            return;\n        }\n\n        View selectedChild = tabStrip.getChildAt(tabIndex);\n        if (selectedChild != null) {\n            int targetScrollX = selectedChild.getLeft() + positionOffset;\n\n            if (tabIndex > 0 || positionOffset > 0) {\n                // If we're not at the first child and are mid-scroll, make sure we obey the offset\n                targetScrollX -= titleOffset;\n            }\n\n            scrollTo(targetScrollX, 0);\n        }\n    }\n\n    /**\n     * Allows complete control over the colors drawn in the tab layout. Set with\n     * {@link #setCustomTabColorizer(TabColorizer)}.\n     */\n    public interface TabColorizer {\n\n        /**\n         * @return return the color of the indicator used when {@code position} is selected.\n         */\n        int getIndicatorColor(int position);\n\n    }\n\n    private class InternalViewPagerListener implements ViewPager.OnPageChangeListener {\n        private int scrollState;\n\n        @Override\n        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {\n            int tabStripChildCount = tabStrip.getChildCount();\n            if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {\n                return;\n            }\n\n            tabStrip.onViewPagerPageChanged(position, positionOffset);\n\n            View selectedTitle = tabStrip.getChildAt(position);\n            int extraOffset = (selectedTitle != null)\n                    ? (int) (positionOffset * selectedTitle.getWidth())\n                    : 0;\n            scrollToTab(position, extraOffset);\n\n            if (viewPagerPageChangeListener != null) {\n                viewPagerPageChangeListener.onPageScrolled(position, positionOffset,\n                        positionOffsetPixels);\n            }\n        }\n\n        @Override\n        public void onPageScrollStateChanged(int state) {\n            scrollState = state;\n\n            if (viewPagerPageChangeListener != null) {\n                viewPagerPageChangeListener.onPageScrollStateChanged(state);\n            }\n        }\n\n        @Override\n        public void onPageSelected(int position) {\n            if (scrollState == ViewPager.SCROLL_STATE_IDLE) {\n                tabStrip.onViewPagerPageChanged(position, 0f);\n                scrollToTab(position, 0);\n            }\n            for (int i = 0; i < tabStrip.getChildCount(); i++) {\n                tabStrip.getChildAt(i).setSelected(position == i);\n            }\n            if (viewPagerPageChangeListener != null) {\n                viewPagerPageChangeListener.onPageSelected(position);\n            }\n        }\n\n    }\n\n    private class TabClickListener implements View.OnClickListener {\n        @Override\n        public void onClick(View v) {\n            for (int i = 0; i < tabStrip.getChildCount(); i++) {\n                if (v == tabStrip.getChildAt(i)) {\n                    viewPager.setCurrentItem(i);\n                    return;\n                }\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/SlidingTabStrip.java",
    "content": "package org.odk.collect.android.views;\n\n/*\n * Copyright 2014 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.util.AttributeSet;\nimport android.util.TypedValue;\nimport android.view.View;\nimport android.widget.LinearLayout;\n\nclass SlidingTabStrip extends LinearLayout {\n\n    private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 0;\n    private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26;\n    private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 3;\n    private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5;\n\n    private final int bottomBorderThickness;\n    private final Paint bottomBorderPaint;\n\n    private final int selectedIndicatorThickness;\n    private final Paint selectedIndicatorPaint;\n\n    private final SimpleTabColorizer defaultTabColorizer;\n    private int selectedPosition;\n    private float selectionOffset;\n    private SlidingTabLayout.TabColorizer customTabColorizer;\n\n    SlidingTabStrip(Context context) {\n        this(context, null);\n    }\n\n    SlidingTabStrip(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        setWillNotDraw(false);\n\n        final float density = getResources().getDisplayMetrics().density;\n\n        TypedValue outValue = new TypedValue();\n        context.getTheme().resolveAttribute(android.R.attr.colorForeground, outValue, true);\n        final int themeForegroundColor = outValue.data;\n\n        final int defaultBottomBorderColor = setColorAlpha(themeForegroundColor,\n                DEFAULT_BOTTOM_BORDER_COLOR_ALPHA);\n\n        defaultTabColorizer = new SimpleTabColorizer();\n        defaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR);\n\n        bottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density);\n        bottomBorderPaint = new Paint();\n        bottomBorderPaint.setColor(defaultBottomBorderColor);\n\n        selectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density);\n        selectedIndicatorPaint = new Paint();\n    }\n\n    /**\n     * Set the alpha value of the {@code color} to be the given {@code alpha} value.\n     */\n    private static int setColorAlpha(int color, byte alpha) {\n        return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));\n    }\n\n    /**\n     * Blend {@code color1} and {@code color2} using the given ratio.\n     *\n     * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend,\n     *              0.0 will return {@code color2}.\n     */\n    private static int blendColors(int color1, int color2, float ratio) {\n        final float inverseRation = 1f - ratio;\n        float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation);\n        float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation);\n        float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation);\n        return Color.rgb((int) r, (int) g, (int) b);\n    }\n\n    void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) {\n        this.customTabColorizer = customTabColorizer;\n        invalidate();\n    }\n\n    void setSelectedIndicatorColors(int... colors) {\n        // Make sure that the custom colorizer is removed\n        customTabColorizer = null;\n        defaultTabColorizer.setIndicatorColors(colors);\n        invalidate();\n    }\n\n    void onViewPagerPageChanged(int position, float positionOffset) {\n        selectedPosition = position;\n        selectionOffset = positionOffset;\n        invalidate();\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        final int height = getHeight();\n        final int childCount = getChildCount();\n        final SlidingTabLayout.TabColorizer tabColorizer = customTabColorizer != null\n                ? customTabColorizer\n                : defaultTabColorizer;\n\n        // Thick colored underline below the current selection\n        if (childCount > 0) {\n            View selectedTitle = getChildAt(selectedPosition);\n            int left = selectedTitle.getLeft();\n            int right = selectedTitle.getRight();\n            int color = tabColorizer.getIndicatorColor(selectedPosition);\n\n            if (selectionOffset > 0f && selectedPosition < (getChildCount() - 1)) {\n                int nextColor = tabColorizer.getIndicatorColor(selectedPosition + 1);\n                if (color != nextColor) {\n                    color = blendColors(nextColor, color, selectionOffset);\n                }\n\n                // Draw the selection partway between the tabs\n                View nextTitle = getChildAt(selectedPosition + 1);\n                left = (int) (selectionOffset * nextTitle.getLeft()\n                        + (1.0f - selectionOffset) * left);\n                right = (int) (selectionOffset * nextTitle.getRight()\n                        + (1.0f - selectionOffset) * right);\n            }\n\n            selectedIndicatorPaint.setColor(color);\n\n            canvas.drawRect(left, height - selectedIndicatorThickness, right,\n                    height, selectedIndicatorPaint);\n        }\n\n        // Thin underline along the entire bottom edge\n        canvas.drawRect(0, height - bottomBorderThickness, getWidth(), height, bottomBorderPaint);\n    }\n\n    private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer {\n        private int[] indicatorColors;\n\n        @Override\n        public final int getIndicatorColor(int position) {\n            return indicatorColors[position % indicatorColors.length];\n        }\n\n        void setIndicatorColors(int... colors) {\n            indicatorColors = colors;\n        }\n    }\n}"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/TrackingTouchSlider.kt",
    "content": "package org.odk.collect.android.views\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.res.ColorStateList\nimport android.util.AttributeSet\nimport com.google.android.material.color.MaterialColors\nimport com.google.android.material.slider.Slider\n\n@SuppressLint(\"ClickableViewAccessibility\")\nclass TrackingTouchSlider(\n    context: Context,\n    attrs: AttributeSet?\n) : Slider(context, attrs), Slider.OnSliderTouchListener {\n    private var listener: OnChangeListener? = null\n\n    private var defaultTickActiveTintList: ColorStateList = tickActiveTintList\n    private var defaultThumbWidth = thumbWidth\n    private var defaultThumbTrackGapSize = thumbTrackGapSize\n\n    var isTrackingTouch: Boolean = false\n        private set\n\n    init {\n        addOnSliderTouchListener(this)\n        setLabelFormatter(null)\n        addOnSliderTouchListener(object : OnSliderTouchListener {\n            override fun onStartTrackingTouch(slider: Slider) {}\n\n            override fun onStopTrackingTouch(slider: Slider) {\n                setTickActiveTintList(defaultTickActiveTintList)\n                setThumbWidth(defaultThumbWidth)\n                setThumbTrackGapSize(defaultThumbTrackGapSize)\n                listener?.onValueChange(this@TrackingTouchSlider, value, true)\n            }\n        })\n    }\n\n    @SuppressLint(\"RestrictedApi\")\n    override fun onStopTrackingTouch(slider: Slider) {\n        isTrackingTouch = false\n    }\n\n    @SuppressLint(\"RestrictedApi\")\n    override fun onStartTrackingTouch(slider: Slider) {\n        isTrackingTouch = true\n    }\n\n    fun reset() {\n        value = valueFrom\n        setTickActiveTintList(\n            ColorStateList.valueOf(\n                MaterialColors.getColor(\n                    this,\n                    androidx.appcompat.R.attr.colorPrimary\n                )\n            )\n        )\n        setThumbWidth(0)\n        setThumbTrackGapSize(0)\n    }\n\n    fun setListener(listener: OnChangeListener) {\n        this.listener = listener\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/TransparentProgressScreen.kt",
    "content": "package org.odk.collect.android.views\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.widget.FrameLayout\nimport org.odk.collect.android.R\nimport org.odk.collect.androidshared.ui.Animations.createAlphaAnimation\n\nclass TransparentProgressScreen(context: Context, attrs: AttributeSet?) :\n    FrameLayout(context, attrs) {\n\n    init {\n        inflate(context, R.layout.transparent_progress_screen, this)\n    }\n\n    override fun setVisibility(visibility: Int) {\n        when (visibility) {\n            GONE -> {\n                super.setVisibility(visibility)\n            }\n\n            INVISIBLE -> {\n                super.setVisibility(visibility)\n            }\n\n            VISIBLE -> {\n                alpha = 0f\n                super.setVisibility(VISIBLE)\n                createAlphaAnimation(0f, 1f, 60).start()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/TwoItemMultipleChoiceView.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.widget.CheckBox;\nimport android.widget.Checkable;\n\nimport androidx.constraintlayout.widget.ConstraintLayout;\n\nimport org.odk.collect.android.R;\n\npublic class TwoItemMultipleChoiceView extends ConstraintLayout implements Checkable {\n\n    public TwoItemMultipleChoiceView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n    }\n\n    public TwoItemMultipleChoiceView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public TwoItemMultipleChoiceView(Context context) {\n        super(context);\n    }\n\n    @Override\n    public boolean isChecked() {\n        CheckBox c = findViewById(R.id.checkbox);\n        return c.isChecked();\n    }\n\n    @Override\n    public void setChecked(boolean checked) {\n        CheckBox c = findViewById(R.id.checkbox);\n        c.setChecked(checked);\n    }\n\n    @Override\n    public void toggle() {\n        CheckBox c = findViewById(R.id.checkbox);\n        c.setChecked(!c.isChecked());\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/views/WidgetAnswerText.kt",
    "content": "package org.odk.collect.android.views\n\nimport android.content.Context\nimport android.text.Editable\nimport android.text.InputFilter\nimport android.text.InputFilter.LengthFilter\nimport android.text.InputType\nimport android.text.Selection\nimport android.text.TextWatcher\nimport android.text.method.DigitsKeyListener\nimport android.text.method.PasswordTransformationMethod\nimport android.text.method.TextKeyListener\nimport android.util.AttributeSet\nimport android.util.TypedValue\nimport android.view.Gravity\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.FrameLayout\nimport org.odk.collect.android.databinding.WidgetAnswerTextBinding\nimport org.odk.collect.android.listeners.ThousandsSeparatorTextWatcher\nimport org.odk.collect.android.utilities.SoftKeyboardController\nimport java.text.NumberFormat\nimport java.util.Locale\n\n/**\n * Custom view for answer fields in text widgets with dual states: EDITABLE and READ_ONLY.\n * In the EDITABLE state, a text field is presented for users to manually input answers.\n * However, in the READ_ONLY state, only a text view is shown, preventing manual changes.\n */\nclass WidgetAnswerText(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {\n\n    constructor(context: Context) : this(context, null)\n\n    val binding = WidgetAnswerTextBinding.inflate(LayoutInflater.from(context), this, true)\n\n    fun init(fontSize: Float, readOnly: Boolean, numberOfRows: Int?, isMultiline: Boolean, isMasked: Boolean, afterTextChanged: Runnable) {\n        binding.editText.id = generateViewId()\n        binding.textView.id = generateViewId()\n\n        binding.editText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize)\n        binding.textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize)\n\n        binding.editText.keyListener = TextKeyListener(TextKeyListener.Capitalize.SENTENCES, false)\n        binding.editText.setHorizontallyScrolling(false)\n        binding.editText.isSingleLine = false\n        binding.editText.gravity = Gravity.TOP or Gravity.START\n\n        updateState(readOnly)\n\n        if (numberOfRows != null && numberOfRows > 0) {\n            binding.editText.minLines = numberOfRows\n        } else if (isMultiline) {\n            binding.editText.minLines = 4\n            binding.editText.maxLines = 4\n        }\n\n        binding.editText.addTextChangedListener(object : TextWatcher {\n            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit\n            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit\n            override fun afterTextChanged(s: Editable) {\n                afterTextChanged.run()\n            }\n        })\n        if (isMasked) {\n            binding.editText.inputType = binding.editText.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD\n            binding.editText.transformationMethod = PasswordTransformationMethod.getInstance()\n            binding.textView.transformationMethod = PasswordTransformationMethod.getInstance()\n        }\n        setError(null)\n    }\n\n    fun updateState(readOnly: Boolean) {\n        binding.root.visibility = VISIBLE\n        if (readOnly) {\n            binding.textInputLayout.visibility = GONE\n            binding.textView.visibility = VISIBLE\n        } else {\n            binding.textInputLayout.visibility = VISIBLE\n            binding.textView.visibility = GONE\n        }\n    }\n\n    fun setIntegerType(useThousandSeparator: Boolean, answer: Int?) {\n        if (useThousandSeparator) {\n            binding.editText.addTextChangedListener(ThousandsSeparatorTextWatcher(binding.editText))\n        }\n\n        binding.editText.keyListener = DigitsKeyListener(true, false) // only allows numbers and no periods\n\n        // ints can only hold 2,147,483,648. we allow 999,999,999\n        val fa = arrayOfNulls<InputFilter>(1)\n        fa[0] = LengthFilter(9)\n        if (useThousandSeparator) {\n            // 11 since for a nine digit number , their will be 2 separators.\n            fa[0] = LengthFilter(11)\n        }\n        binding.editText.filters = fa\n\n        if (answer != null) {\n            setAnswer(String.format(Locale.US, \"%d\", answer))\n        }\n    }\n\n    fun setStringNumberType(useThousandSeparator: Boolean, answer: String?) {\n        if (useThousandSeparator) {\n            binding.editText.addTextChangedListener(ThousandsSeparatorTextWatcher(binding.editText))\n        }\n\n        binding.editText.keyListener = object : DigitsKeyListener() {\n            override fun getAcceptedChars(): CharArray {\n                return charArrayOf(\n                    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-', '+', ' ', ','\n                )\n            }\n        }\n\n        if (answer != null) {\n            setAnswer(answer)\n        }\n    }\n\n    fun setDecimalType(useThousandSeparator: Boolean, answer: Double?) {\n        if (useThousandSeparator) {\n            binding.editText.addTextChangedListener(ThousandsSeparatorTextWatcher(binding.editText))\n        }\n\n        binding.editText.keyListener = DigitsKeyListener(true, true) // only numbers are allowed\n\n        // only 15 characters allowed\n        val fa = arrayOfNulls<InputFilter>(1)\n        fa[0] = LengthFilter(15)\n        if (useThousandSeparator) {\n            fa[0] = LengthFilter(19)\n        }\n        binding.editText.filters = fa\n\n        if (answer != null) {\n            // truncate to 15 digits max in US locale use US locale because DigitsKeyListener can't be localized before API 26\n            val nf = NumberFormat.getNumberInstance(Locale.US)\n            nf.maximumFractionDigits = 15\n            nf.maximumIntegerDigits = 15\n            nf.isGroupingUsed = false\n            val formattedValue: String = nf.format(answer)\n            setAnswer(formattedValue)\n        }\n    }\n\n    fun getAnswer(): String {\n        return binding.editText.text.toString()\n    }\n\n    fun setAnswer(answer: String?) {\n        binding.editText.setText(answer)\n        binding.textView.text = binding.editText.text\n        Selection.setSelection(binding.editText.text, binding.editText.text.toString().length)\n\n        if (answer == null && !isEditableState()) {\n            binding.root.visibility = GONE\n        } else {\n            binding.root.visibility = VISIBLE\n        }\n    }\n\n    fun clearAnswer() {\n        binding.editText.text = null\n        binding.textView.text = null\n        if (!isEditableState()) {\n            binding.root.visibility = GONE\n        }\n    }\n\n    fun setError(error: String?) {\n        binding.textInputLayout.post {\n            binding.textInputLayout.error = error\n        }\n    }\n\n    fun setFocus(focus: Boolean) {\n        if (focus) {\n            SoftKeyboardController.showSoftKeyboard(binding.editText)\n        } else {\n            SoftKeyboardController.hideSoftKeyboard(binding.editText)\n        }\n    }\n\n    fun isEditableState(): Boolean {\n        return binding.textInputLayout.visibility == View.VISIBLE\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/AnnotateWidget.java",
    "content": "/*\n * Copyright (C) 2012 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;\nimport static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.view.View;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.AnnotateWidgetBinding;\nimport org.odk.collect.draw.DrawActivity;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.utilities.ImageCaptureIntentCreator;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport java.io.File;\nimport java.util.Locale;\n\n/**\n * Image widget that supports annotations on the image.\n *\n * @author BehrAtherton@gmail.com\n * @author Carl Hartung (carlhartung@gmail.com)\n * @author Yaw Anokwa (yanokwa@gmail.com)\n */\n@SuppressLint(\"ViewConstructor\")\npublic class AnnotateWidget extends BaseImageWidget {\n    AnnotateWidgetBinding binding;\n\n    public AnnotateWidget(Context context, QuestionDetails prompt, QuestionMediaManager questionMediaManager, WaitingForDataRegistry waitingForDataRegistry, String tmpImageFilePath, Dependencies dependencies) {\n        super(context, prompt, questionMediaManager, waitingForDataRegistry, tmpImageFilePath, dependencies);\n        imageClickHandler = new DrawImageClickHandler(DrawActivity.OPTION_ANNOTATE, RequestCodes.ANNOTATE_IMAGE, org.odk.collect.strings.R.string.annotate_image);\n        imageCaptureHandler = new ImageCaptureHandler();\n\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = AnnotateWidgetBinding.inflate(((Activity) context).getLayoutInflater());\n        errorTextView = binding.errorMessage;\n        imageView = binding.image;\n        updateAnswer();\n\n        if (getFormEntryPrompt().getAppearanceHint() != null && getFormEntryPrompt().getAppearanceHint().toLowerCase(Locale.ENGLISH).contains(Appearances.NEW)) {\n            binding.chooseButton.setVisibility(View.GONE);\n        }\n\n        if (binaryName == null || binding.image.getVisibility() == GONE) {\n            binding.annotateButton.setEnabled(false);\n        }\n\n        binding.captureButton.setOnClickListener(v -> getPermissionsProvider().requestCameraPermission((Activity) getContext(), () -> {\n            Intent intent = ImageCaptureIntentCreator.imageCaptureIntent(getFormEntryPrompt(), getContext(), tmpImageFilePath);\n            imageCaptureHandler.captureImage(intent, RequestCodes.IMAGE_CAPTURE, org.odk.collect.strings.R.string.annotate_image);\n        }));\n        binding.chooseButton.setOnClickListener(v -> imageCaptureHandler.chooseImage(org.odk.collect.strings.R.string.annotate_image));\n        binding.annotateButton.setOnClickListener(v -> imageClickHandler.clickImage(\"annotateButton\"));\n        binding.image.setOnClickListener(v -> imageClickHandler.clickImage(\"viewImage\"));\n\n        if (questionDetails.isReadOnly()) {\n            binding.captureButton.setVisibility(View.GONE);\n            binding.chooseButton.setVisibility(View.GONE);\n            binding.annotateButton.setVisibility(View.GONE);\n        }\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public Intent addExtrasToIntent(Intent intent) {\n        Bitmap bmp = null;\n        if (binding.image.getDrawable() != null) {\n            bmp = ((BitmapDrawable) binding.image.getDrawable()).getBitmap();\n        }\n\n        int screenOrientation =  bmp != null && bmp.getHeight() > bmp.getWidth() ?\n                SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE;\n\n        intent.putExtra(DrawActivity.SCREEN_ORIENTATION, screenOrientation);\n        return intent;\n    }\n\n    @Override\n    protected boolean doesSupportDefaultValues() {\n        return true;\n    }\n\n    @Override\n    public void clearAnswer() {\n        super.clearAnswer();\n        binding.annotateButton.setEnabled(false);\n        binding.captureButton.setText(getContext().getString(org.odk.collect.strings.R.string.capture_image));\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.captureButton.setOnLongClickListener(l);\n        binding.chooseButton.setOnLongClickListener(l);\n        binding.annotateButton.setOnLongClickListener(l);\n        super.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.captureButton.cancelLongPress();\n        binding.chooseButton.cancelLongPress();\n        binding.annotateButton.cancelLongPress();\n    }\n\n    @Override\n    public void setData(Object newImageObj) {\n        if (newImageObj instanceof File) {\n            String mimeType = FileUtils.getMimeType((File) newImageObj);\n            if (\"image/gif\".equals(mimeType)) {\n                ToastUtils.showLongToast(org.odk.collect.strings.R.string.gif_not_supported);\n            } else {\n                super.setData(newImageObj);\n                binding.annotateButton.setEnabled(binaryName != null);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/AudioWidget.java",
    "content": "/*\n * Copyright (C) 2018 Shobhit Agarwal\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.media.MediaMetadataRetriever;\nimport android.view.LayoutInflater;\nimport android.view.View;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.audio.AudioControllerView;\nimport org.odk.collect.android.databinding.AudioWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.interfaces.FileWidget;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.AudioFileRequester;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.android.widgets.utilities.RecordingRequester;\nimport org.odk.collect.android.widgets.utilities.RecordingStatusHandler;\nimport org.odk.collect.audioclips.Clip;\n\nimport java.io.File;\nimport java.util.Locale;\n\nimport timber.log.Timber;\n\nimport static org.odk.collect.strings.format.LengthFormatterKt.formatLength;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\n/**\n * Widget that allows user to take pictures, sounds or video and add them to the\n * form.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n * @author Yaw Anokwa (yanokwa@gmail.com)\n */\n\n@SuppressLint(\"ViewConstructor\")\npublic class AudioWidget extends QuestionWidget implements FileWidget, WidgetDataReceiver {\n\n    AudioWidgetAnswerBinding binding;\n\n    private final AudioPlayer audioPlayer;\n    private final RecordingRequester recordingRequester;\n    private final QuestionMediaManager questionMediaManager;\n    private final AudioFileRequester audioFileRequester;\n\n    private boolean recordingInProgress;\n    private String binaryName;\n\n    public AudioWidget(Context context, QuestionDetails questionDetails, QuestionMediaManager questionMediaManager, AudioPlayer audioPlayer, RecordingRequester recordingRequester, AudioFileRequester audioFileRequester, RecordingStatusHandler recordingStatusHandler, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        this.audioPlayer = audioPlayer;\n\n        this.questionMediaManager = questionMediaManager;\n        this.recordingRequester = recordingRequester;\n        this.audioFileRequester = audioFileRequester;\n\n        binaryName = questionDetails.getPrompt().getAnswerText();\n        updateVisibilities();\n        updatePlayerMedia();\n\n        recordingStatusHandler.onBlockedStatusChange(isRecordingBlocked -> {\n            binding.recordAudioButton.setEnabled(!isRecordingBlocked);\n            binding.chooseAudioButton.setEnabled(!isRecordingBlocked);\n        });\n\n        recordingStatusHandler.onRecordingStatusChange(getFormEntryPrompt(), session -> {\n            if (session != null) {\n                recordingInProgress = true;\n                updateVisibilities();\n\n                binding.audioPlayer.recordingDuration.setText(formatLength(session.first));\n                binding.audioPlayer.waveform.addAmplitude(session.second);\n            } else {\n                recordingInProgress = false;\n                updateVisibilities();\n                updatePlayerMedia();\n            }\n        });\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = AudioWidgetAnswerBinding.inflate(LayoutInflater.from(context));\n\n        binding.recordAudioButton.setOnClickListener(v -> {\n            hideError();\n            binding.audioPlayer.waveform.clear();\n            recordingRequester.requestRecording(getFormEntryPrompt());\n        });\n        binding.chooseAudioButton.setOnClickListener(v -> audioFileRequester.requestFile(getFormEntryPrompt()));\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public void deleteFile() {\n        audioPlayer.stop();\n        questionMediaManager.deleteAnswerFile(getFormEntryPrompt().getIndex().toString(), getAudioFile().getAbsolutePath());\n        binaryName = null;\n    }\n\n    @Override\n    public void clearAnswer() {\n        deleteFile();\n        widgetValueChanged();\n        updateVisibilities();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        if (binaryName != null) {\n            return new StringData(binaryName);\n        } else {\n            return null;\n        }\n    }\n\n    @Override\n    public void setData(Object object) {\n        if (object instanceof File) {\n            File newAudio = (File) object;\n            if (newAudio.exists()) {\n                if (binaryName != null) {\n                    deleteFile();\n                }\n\n                questionMediaManager.replaceAnswerFile(getFormEntryPrompt().getIndex().toString(), newAudio.getAbsolutePath());\n                binaryName = newAudio.getName();\n                updateVisibilities();\n                updatePlayerMedia();\n                widgetValueChanged();\n            } else {\n                Timber.e(new Error(\"NO AUDIO EXISTS at: \" + newAudio.getAbsolutePath()));\n            }\n        } else {\n            Timber.e(new Error(\"AudioWidget's setBinaryData must receive a File object.\"));\n        }\n    }\n\n    private void updateVisibilities() {\n        if (recordingInProgress) {\n            binding.recordAudioButton.setVisibility(GONE);\n            binding.chooseAudioButton.setVisibility(GONE);\n            binding.audioPlayer.recordingDuration.setVisibility(VISIBLE);\n            binding.audioPlayer.waveform.setVisibility(VISIBLE);\n            binding.audioPlayer.audioController.setVisibility(GONE);\n        } else if (getAnswer() == null) {\n            binding.recordAudioButton.setVisibility(VISIBLE);\n            binding.chooseAudioButton.setVisibility(VISIBLE);\n            binding.audioPlayer.recordingDuration.setVisibility(GONE);\n            binding.audioPlayer.waveform.setVisibility(GONE);\n            binding.audioPlayer.audioController.setVisibility(GONE);\n        } else {\n            binding.recordAudioButton.setVisibility(GONE);\n            binding.chooseAudioButton.setVisibility(GONE);\n            binding.audioPlayer.recordingDuration.setVisibility(GONE);\n            binding.audioPlayer.waveform.setVisibility(GONE);\n            binding.audioPlayer.audioController.setVisibility(VISIBLE);\n        }\n\n        if (questionDetails.isReadOnly()) {\n            binding.recordAudioButton.setVisibility(GONE);\n            binding.chooseAudioButton.setVisibility(GONE);\n        }\n\n        if (getFormEntryPrompt().getAppearanceHint() != null && getFormEntryPrompt().getAppearanceHint().toLowerCase(Locale.ENGLISH).contains(Appearances.NEW)) {\n            binding.chooseAudioButton.setVisibility(GONE);\n        }\n    }\n\n    private void updatePlayerMedia() {\n        if (binaryName != null) {\n            Clip clip = new Clip(\"audio:\" + getFormEntryPrompt().getIndex().toString(), getAudioFile().getAbsolutePath());\n\n            audioPlayer.onPlayingChanged(clip.getClipID(), binding.audioPlayer.audioController::setPlaying);\n            audioPlayer.onPositionChanged(clip.getClipID(), binding.audioPlayer.audioController::setPosition);\n            binding.audioPlayer.audioController.setDuration(getDurationOfFile(clip.getURI()));\n            binding.audioPlayer.audioController.setListener(new AudioControllerView.Listener() {\n                @Override\n                public void onPlayClicked() {\n                    audioPlayer.play(clip);\n                }\n\n                @Override\n                public void onPauseClicked() {\n                    audioPlayer.pause();\n                }\n\n                @Override\n                public void onPositionChanged(Integer newPosition) {\n                    audioPlayer.setPosition(clip.getClipID(), newPosition);\n                }\n\n                @Override\n                public void onRemoveClicked() {\n                    new MaterialAlertDialogBuilder(getContext())\n                            .setTitle(org.odk.collect.strings.R.string.delete_answer_file_question)\n                            .setMessage(org.odk.collect.strings.R.string.answer_file_delete_warning)\n                            .setPositiveButton(org.odk.collect.strings.R.string.delete_answer_file, (dialog, which) -> clearAnswer())\n                            .setNegativeButton(org.odk.collect.strings.R.string.cancel, null)\n                            .show();\n                }\n            });\n\n        }\n    }\n\n    private Integer getDurationOfFile(String uri) {\n        MediaMetadataRetriever retriever = new MediaMetadataRetriever();\n        retriever.setDataSource(uri);\n        String durationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);\n        return durationString != null ? Integer.parseInt(durationString) : 0;\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.recordAudioButton.setOnLongClickListener(l);\n        binding.chooseAudioButton.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.recordAudioButton.cancelLongPress();\n        binding.chooseAudioButton.cancelLongPress();\n    }\n\n    /**\n     * Returns the audio file added to the widget for the current instance\n     */\n    private File getAudioFile() {\n        return questionMediaManager.getAnswerFile(binaryName);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/BaseImageWidget.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.app.Activity;\nimport android.content.ActivityNotFoundException;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.view.View;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.annotation.IdRes;\nimport androidx.annotation.Nullable;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.odk.collect.draw.DrawActivity;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.ApplicationConstants;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.interfaces.FileWidget;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard;\nimport org.odk.collect.imageloader.GlideImageLoader;\n\nimport java.io.File;\n\nimport timber.log.Timber;\n\npublic abstract class BaseImageWidget extends QuestionWidget implements FileWidget, WidgetDataReceiver {\n\n    protected ImageView imageView;\n    protected String binaryName;\n    protected TextView errorTextView;\n    protected LinearLayout answerLayout;\n\n    protected ImageClickHandler imageClickHandler;\n    protected ExternalImageCaptureHandler imageCaptureHandler;\n\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    private final QuestionMediaManager questionMediaManager;\n    protected final String tmpImageFilePath;\n\n    public BaseImageWidget(Context context, QuestionDetails prompt, QuestionMediaManager questionMediaManager,\n                           WaitingForDataRegistry waitingForDataRegistry, String tmpImageFilePath, Dependencies dependencies) {\n        super(context, dependencies, prompt);\n        this.questionMediaManager = questionMediaManager;\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        this.tmpImageFilePath = tmpImageFilePath;\n\n        binaryName = getFormEntryPrompt().getAnswerText();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return binaryName == null ? null : new StringData(binaryName);\n    }\n\n    @Override\n    public void clearAnswer() {\n        deleteFile();\n        imageView.setImageDrawable(null);\n        imageView.setVisibility(View.GONE);\n        errorTextView.setVisibility(View.GONE);\n        widgetValueChanged();\n    }\n\n    @Override\n    public void deleteFile() {\n        questionMediaManager.deleteAnswerFile(getFormEntryPrompt().getIndex().toString(), binaryName);\n        binaryName = null;\n    }\n\n    @Override\n    public void setData(Object object) {\n        if (binaryName != null) {\n            deleteFile();\n        }\n\n        if (object instanceof File) {\n            File newImage = (File) object;\n            if (newImage.exists()) {\n                questionMediaManager.replaceAnswerFile(getFormEntryPrompt().getIndex().toString(), newImage.getAbsolutePath());\n                binaryName = newImage.getName();\n                updateAnswer();\n                widgetValueChanged();\n            } else {\n                Timber.e(new Error(\"NO IMAGE EXISTS at: \" + newImage.getAbsolutePath()));\n            }\n        } else {\n            Timber.e(new Error(\"ImageWidget's setBinaryData must receive a File object.\"));\n        }\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        if (imageView != null) {\n            imageView.setOnLongClickListener(l);\n        }\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        if (imageView != null) {\n            imageView.cancelLongPress();\n        }\n    }\n\n    protected void updateAnswer() {\n        imageView.setVisibility(View.GONE);\n        errorTextView.setVisibility(View.GONE);\n\n        if (binaryName != null) {\n            File f = getFile();\n            if (f != null && f.exists()) {\n                imageView.setVisibility(View.VISIBLE);\n                imageLoader.loadImage(imageView, f, ImageView.ScaleType.FIT_CENTER, new GlideImageLoader.ImageLoaderCallback() {\n                    @Override\n                    public void onLoadFailed() {\n                        imageView.setVisibility(View.GONE);\n                        errorTextView.setVisibility(View.VISIBLE);\n                    }\n\n                    @Override\n                    public void onLoadSucceeded() {\n                    }\n                });\n            }\n        }\n    }\n\n    /**\n     * Enables a subclass to add extras to the intent before launching the draw activity.\n     *\n     * @param intent to add extras\n     * @return intent with added extras\n     */\n    public abstract Intent addExtrasToIntent(Intent intent);\n\n    /**\n     * Interface for Clicking on Images\n     */\n    protected interface ImageClickHandler {\n        void clickImage(String context);\n    }\n\n    /**\n     * Class to implement launching of viewing an image Activity\n     */\n    protected class ViewImageClickHandler implements ImageClickHandler {\n\n        @Override\n        public void clickImage(String context) {\n            mediaUtils.openFile(getContext(), questionMediaManager.getAnswerFile(binaryName),\n                    \"image/*\");\n        }\n    }\n\n    /**\n     * Class to implement launching of drawing image Activity when clicked\n     */\n    protected class DrawImageClickHandler implements ImageClickHandler {\n\n        private final String drawOption;\n        private final int requestCode;\n        private final int stringResourceId;\n\n        public DrawImageClickHandler(String option, final int code, final int resourceId) {\n            drawOption = option;\n            requestCode = code;\n            stringResourceId = resourceId;\n        }\n\n        @Override\n        public void clickImage(String context) {\n            if (MultiClickGuard.allowClick(getClass().getName())) {\n                launchDrawActivity();\n            }\n        }\n\n        private void launchDrawActivity() {\n            Intent i = new Intent(getContext(), DrawActivity.class);\n            i.putExtra(DrawActivity.OPTION, drawOption);\n            File file = getFile();\n            if (file != null && file.exists()) {\n                i.putExtra(DrawActivity.REF_IMAGE, Uri.fromFile(file));\n            }\n\n            i.putExtra(DrawActivity.EXTRA_OUTPUT, Uri.fromFile(new File(tmpImageFilePath)));\n            i = addExtrasToIntent(i);\n            launchActivityForResult(i, requestCode, stringResourceId);\n        }\n    }\n\n    /**\n     * Interface for choosing or capturing a new image\n     */\n    protected interface ExternalImageCaptureHandler {\n        void captureImage(Intent intent, int requestCode, int stringResource);\n\n        void chooseImage(int stringResource);\n    }\n\n    /**\n     * Class for launching the image capture or choose image activities\n     */\n    protected class ImageCaptureHandler implements ExternalImageCaptureHandler {\n\n        @Override\n        public void captureImage(Intent intent, final int requestCode, int stringResource) {\n            launchActivityForResult(intent, requestCode, stringResource);\n        }\n\n        @Override\n        public void chooseImage(@IdRes final int stringResource) {\n            Intent i = new Intent(Intent.ACTION_GET_CONTENT);\n            i.setType(\"image/*\");\n            launchActivityForResult(i, ApplicationConstants.RequestCodes.IMAGE_CHOOSER, stringResource);\n        }\n    }\n\n    /**\n     * Standard method for launching an Activity.\n     *\n     * @param intent              - The Intent to start\n     * @param resourceCode        - Code to return when Activity exits\n     * @param errorStringResource - String resource for error toast\n     */\n    protected void launchActivityForResult(Intent intent, final int resourceCode, final int errorStringResource) {\n        try {\n            waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex());\n            ((Activity) getContext()).startActivityForResult(intent, resourceCode);\n        } catch (ActivityNotFoundException e) {\n            Toast.makeText(getContext(),\n                    getContext().getString(org.odk.collect.strings.R.string.activity_not_found, getContext().getString(errorStringResource)),\n                    Toast.LENGTH_SHORT).show();\n            waitingForDataRegistry.cancelWaitingForData();\n        }\n    }\n\n    @Nullable\n    private File getFile() {\n        if (binaryName == null) {\n            return null;\n        }\n\n        File file = questionMediaManager.getAnswerFile(binaryName);\n        if ((file == null || !file.exists()) && doesSupportDefaultValues()) {\n            String filePath = getDefaultFilePath();\n            if (filePath != null) {\n                return new File(getDefaultFilePath());\n            } else {\n                return null;\n            }\n        }\n\n        return file;\n    }\n\n    private String getDefaultFilePath() {\n        try {\n            return referenceManager.deriveReference(binaryName).getLocalURI();\n        } catch (InvalidReferenceException e) {\n            Timber.w(e);\n        }\n\n        return null;\n    }\n\n    protected abstract boolean doesSupportDefaultValues();\n\n    public ImageView getImageView() {\n        return imageView;\n    }\n\n    public TextView getErrorTextView() {\n        return errorTextView;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/BearingWidget.java",
    "content": "/*\n * Copyright (C) 2013 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.hardware.Sensor;\nimport android.hardware.SensorManager;\nimport android.view.View;\n\nimport androidx.core.content.ContextCompat;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.activities.BearingActivity;\nimport org.odk.collect.android.databinding.BearingWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.utilities.StringWidgetUtils;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\n/**\n * BearingWidget is the widget that allows the user to get a compass heading.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class BearingWidget extends QuestionWidget implements WidgetDataReceiver {\n    BearingWidgetAnswerBinding binding;\n\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    private final SensorManager sensorManager;\n\n    public BearingWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry, SensorManager sensorManager, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        this.sensorManager = sensorManager;\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = BearingWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n\n        if (prompt.isReadOnly()) {\n            binding.bearingButton.setVisibility(GONE);\n        } else {\n            binding.bearingButton.setOnClickListener(v -> onButtonClick());\n        }\n        binding.widgetAnswerText.init(answerFontSize, true, null, false, false, this::widgetValueChanged);\n        Double answer = StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue());\n        binding.widgetAnswerText.setDecimalType(false, answer);\n\n        String answerText = prompt.getAnswerText();\n        binding.widgetAnswerText.setAnswer(answerText);\n        if (answerText != null && !answerText.isEmpty()) {\n            binding.bearingButton.setText(getContext().getString(org.odk.collect.strings.R.string.replace_bearing));\n        }\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public void clearAnswer() {\n        binding.bearingButton.setText(getContext().getString(org.odk.collect.strings.R.string.get_bearing));\n        binding.widgetAnswerText.setAnswer(null);\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        String answerText = binding.widgetAnswerText.getAnswer();\n        return answerText.isEmpty() ? null : new StringData(answerText);\n    }\n\n    @Override\n    public void setData(Object answer) {\n        binding.widgetAnswerText.setAnswer((String) answer);\n        binding.bearingButton.setText(getContext().getString(org.odk.collect.strings.R.string.replace_bearing));\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.bearingButton.setOnLongClickListener(l);\n        binding.widgetAnswerText.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.bearingButton.cancelLongPress();\n        binding.widgetAnswerText.cancelLongPress();\n    }\n\n    private boolean areSensorsAvailable() {\n        return sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null\n                && sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) != null;\n    }\n\n    private void onButtonClick() {\n        if (areSensorsAvailable()) {\n            Intent intent = new Intent(getContext(), BearingActivity.class);\n            ((Activity) getContext()).startActivityForResult(intent, RequestCodes.BEARING_CAPTURE);\n\n            waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex());\n        } else {\n            ToastUtils.showLongToast(org.odk.collect.strings.R.string.bearing_lack_of_sensors);\n\n            binding.bearingButton.setEnabled(false);\n            binding.widgetAnswerText.updateState(false);\n        }\n    }\n\n    @Override\n    public void hideError() {\n        super.hideError();\n        binding.widgetAnswerText.setError(null);\n    }\n\n    @Override\n    public void displayError(String errorMessage) {\n        hideError();\n\n        if (binding.widgetAnswerText.isEditableState()) {\n            binding.widgetAnswerText.setError(errorMessage);\n            setBackground(ContextCompat.getDrawable(getContext(), R.drawable.question_with_error_border));\n        } else {\n            super.displayError(errorMessage);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/CounterWidget.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.databinding.CounterWidgetBinding\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.widgets.utilities.StringWidgetUtils\nimport java.util.Locale\n\nclass CounterWidget(\n    context: Context,\n    questionDetails: QuestionDetails,\n    dependencies: Dependencies\n) : QuestionWidget(context, dependencies, questionDetails) {\n    lateinit var binding: CounterWidgetBinding\n\n    private var value: Int? = null\n        set(newValue) {\n            field = newValue?.takeIf { it in 0..MAX_VALUE }\n\n            val formattedValue = field?.let {\n                String.format(Locale.getDefault(), \"%d\", it)\n            }.orEmpty()\n\n            binding.value.text = formattedValue\n            updateButtonStates(field)\n        }\n\n    init {\n        render()\n    }\n\n    private fun updateButtonStates(value: Int?) {\n        binding.minusButton.isEnabled = value != null && value > 0\n        binding.plusButton.isEnabled = value == null || value < MAX_VALUE\n    }\n\n    override fun onCreateWidgetView(\n        context: Context,\n        prompt: FormEntryPrompt,\n        answerFontSize: Int\n    ): View {\n        binding = CounterWidgetBinding.inflate(LayoutInflater.from(context))\n        value = formEntryPrompt.answerValue?.value as Int?\n\n        if (formEntryPrompt.isReadOnly) {\n            binding.minusButton.isEnabled = false\n            binding.plusButton.isEnabled = false\n        }\n\n        binding.minusButton.setOnClickListener {\n            value = value?.minus(1)\n            widgetValueChanged()\n        }\n\n        binding.plusButton.setOnClickListener {\n            value = value?.plus(1) ?: 1\n            widgetValueChanged()\n        }\n        return binding.root\n    }\n\n    override fun setOnLongClickListener(listener: OnLongClickListener?) = Unit\n\n    override fun getAnswer(): IAnswerData? {\n        return StringWidgetUtils.getIntegerData(value?.toString().orEmpty(), formEntryPrompt)\n    }\n\n    override fun clearAnswer() {\n        value = null\n        widgetValueChanged()\n    }\n\n    companion object {\n        const val MAX_VALUE = 999999999\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/DecimalWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.utilities.StringWidgetUtils;\n\n/**\n * A widget that restricts values to floating point numbers.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class DecimalWidget extends StringWidget {\n\n    public DecimalWidget(Context context, QuestionDetails questionDetails, Dependencies dependencies) {\n        super(context, questionDetails, dependencies);\n\n        boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt());\n        Double answer = StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue());\n        widgetAnswerText.setDecimalType(useThousandSeparator, answer);\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return StringWidgetUtils.getDecimalData(getAnswerText(), getFormEntryPrompt());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/DrawWidget.java",
    "content": "/*\n * Copyright (C) 2012 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.View;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.DrawWidgetBinding;\nimport org.odk.collect.draw.DrawActivity;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\n\n/**\n * Free drawing widget.\n *\n * @author BehrAtherton@gmail.com\n */\n@SuppressLint(\"ViewConstructor\")\npublic class DrawWidget extends BaseImageWidget {\n    DrawWidgetBinding binding;\n\n    public DrawWidget(Context context, QuestionDetails prompt, QuestionMediaManager questionMediaManager, WaitingForDataRegistry waitingForDataRegistry, String tmpImageFilePath, Dependencies dependencies) {\n        super(context, prompt, questionMediaManager, waitingForDataRegistry, tmpImageFilePath, dependencies);\n        imageClickHandler = new DrawImageClickHandler(DrawActivity.OPTION_DRAW, RequestCodes.DRAW_IMAGE, org.odk.collect.strings.R.string.draw_image);\n\n        render();\n        updateAnswer();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = DrawWidgetBinding.inflate(((Activity) context).getLayoutInflater());\n        binding.drawButton.setOnClickListener(v -> imageClickHandler.clickImage(\"drawButton\"));\n        binding.image.setOnClickListener(v -> imageClickHandler.clickImage(\"viewImage\"));\n\n        if (questionDetails.isReadOnly()) {\n            binding.drawButton.setVisibility(View.GONE);\n        }\n\n        errorTextView = binding.errorMessage;\n        imageView = binding.image;\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public Intent addExtrasToIntent(Intent intent) {\n        return intent;\n    }\n\n    @Override\n    protected boolean doesSupportDefaultValues() {\n        return true;\n    }\n\n    @Override\n    public void clearAnswer() {\n        super.clearAnswer();\n        binding.drawButton.setText(getContext().getString(org.odk.collect.strings.R.string.draw_image));\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.drawButton.setOnLongClickListener(l);\n        super.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.drawButton.cancelLongPress();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/ExAudioWidget.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.media.MediaMetadataRetriever;\nimport android.view.View;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.audio.AudioControllerView;\nimport org.odk.collect.android.databinding.ExAudioWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.ApplicationConstants;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.interfaces.FileWidget;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.android.widgets.utilities.FileRequester;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.audioclips.Clip;\n\nimport java.io.File;\n\nimport timber.log.Timber;\n\n@SuppressLint(\"ViewConstructor\")\npublic class ExAudioWidget extends QuestionWidget implements FileWidget, WidgetDataReceiver {\n    ExAudioWidgetAnswerBinding binding;\n\n    private final AudioPlayer audioPlayer;\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    private final QuestionMediaManager questionMediaManager;\n    private final FileRequester fileRequester;\n\n    File answerFile;\n\n    public ExAudioWidget(Context context, QuestionDetails questionDetails, QuestionMediaManager questionMediaManager,\n                         AudioPlayer audioPlayer, WaitingForDataRegistry waitingForDataRegistry, FileRequester fileRequester, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n\n        this.audioPlayer = audioPlayer;\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        this.questionMediaManager = questionMediaManager;\n        this.fileRequester = fileRequester;\n\n        render();\n\n        updateVisibilities();\n        updatePlayerMedia();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        setupAnswerFile(prompt.getAnswerText());\n\n        binding = ExAudioWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n        binding.launchExternalAppButton.setOnClickListener(view -> launchExternalApp());\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public void deleteFile() {\n        audioPlayer.stop();\n        questionMediaManager.deleteAnswerFile(getFormEntryPrompt().getIndex().toString(), answerFile.getAbsolutePath());\n        answerFile = null;\n    }\n\n    @Override\n    public void clearAnswer() {\n        deleteFile();\n        updateVisibilities();\n        widgetValueChanged();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return answerFile != null ? new StringData(answerFile.getName()) : null;\n    }\n\n    @Override\n    public void setData(Object object) {\n        if (answerFile != null) {\n            clearAnswer();\n        }\n\n        if (object instanceof File && mediaUtils.isAudioFile((File) object)) {\n            answerFile = (File) object;\n            if (answerFile.exists()) {\n                questionMediaManager.replaceAnswerFile(getFormEntryPrompt().getIndex().toString(), answerFile.getAbsolutePath());\n                updateVisibilities();\n                updatePlayerMedia();\n                widgetValueChanged();\n            } else {\n                Timber.e(new Error(\"Inserting Audio file FAILED\"));\n            }\n        } else if (object != null) {\n            if (object instanceof File) {\n                ToastUtils.showLongToast(org.odk.collect.strings.R.string.invalid_file_type);\n                mediaUtils.deleteMediaFile(((File) object).getAbsolutePath());\n                Timber.e(new Error(\"ExAudioWidget's setBinaryData must receive a audio file but received: \" + FileUtils.getMimeType((File) object)));\n            } else {\n                Timber.e(new Error(\"ExAudioWidget's setBinaryData must receive a audio file but received: \" + object.getClass()));\n            }\n        }\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.launchExternalAppButton.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.launchExternalAppButton.cancelLongPress();\n    }\n\n    private void updateVisibilities() {\n        if (answerFile == null) {\n            binding.launchExternalAppButton.setVisibility(VISIBLE);\n            binding.audioPlayer.recordingDuration.setVisibility(GONE);\n            binding.audioPlayer.waveform.setVisibility(GONE);\n            binding.audioPlayer.audioController.setVisibility(GONE);\n        } else {\n            binding.launchExternalAppButton.setVisibility(GONE);\n            binding.audioPlayer.recordingDuration.setVisibility(GONE);\n            binding.audioPlayer.waveform.setVisibility(GONE);\n            binding.audioPlayer.audioController.setVisibility(VISIBLE);\n        }\n\n        if (questionDetails.isReadOnly()) {\n            binding.launchExternalAppButton.setVisibility(GONE);\n        }\n    }\n\n    private void updatePlayerMedia() {\n        if (answerFile != null) {\n            Clip clip = new Clip(\"audio:\" + getFormEntryPrompt().getIndex().toString(), answerFile.getAbsolutePath());\n\n            audioPlayer.onPlayingChanged(clip.getClipID(), binding.audioPlayer.audioController::setPlaying);\n            audioPlayer.onPositionChanged(clip.getClipID(), binding.audioPlayer.audioController::setPosition);\n            binding.audioPlayer.audioController.setDuration(getDurationOfFile(clip.getURI()));\n            binding.audioPlayer.audioController.setListener(new AudioControllerView.Listener() {\n                @Override\n                public void onPlayClicked() {\n                    audioPlayer.play(clip);\n                }\n\n                @Override\n                public void onPauseClicked() {\n                    audioPlayer.pause();\n                }\n\n                @Override\n                public void onPositionChanged(Integer newPosition) {\n                    audioPlayer.setPosition(clip.getClipID(), newPosition);\n                }\n\n                @Override\n                public void onRemoveClicked() {\n                    new MaterialAlertDialogBuilder(getContext())\n                            .setTitle(org.odk.collect.strings.R.string.delete_answer_file_question)\n                            .setMessage(org.odk.collect.strings.R.string.answer_file_delete_warning)\n                            .setPositiveButton(org.odk.collect.strings.R.string.delete_answer_file, (dialog, which) -> clearAnswer())\n                            .setNegativeButton(org.odk.collect.strings.R.string.cancel, null)\n                            .show();\n                }\n            });\n\n        }\n    }\n\n    private Integer getDurationOfFile(String uri) {\n        MediaMetadataRetriever retriever = new MediaMetadataRetriever();\n        retriever.setDataSource(uri);\n        String durationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);\n        return durationString != null ? Integer.parseInt(durationString) : 0;\n    }\n\n    private void launchExternalApp() {\n        waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex());\n        fileRequester.launch((Activity) getContext(), ApplicationConstants.RequestCodes.EX_AUDIO_CHOOSER, getFormEntryPrompt());\n    }\n\n    private void setupAnswerFile(String fileName) {\n        if (fileName != null && !fileName.isEmpty()) {\n            answerFile = questionMediaManager.getAnswerFile(fileName);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/ExDecimalWidget.java",
    "content": "/*\n * Copyright (C) 2012 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n\nimport org.javarosa.core.model.data.DecimalData;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.odk.collect.android.dynamicpreload.ExternalAppsUtils;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.utilities.StringRequester;\nimport org.odk.collect.android.widgets.utilities.StringWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\n\nimport java.io.Serializable;\n\n/**\n * Launch an external app to supply a decimal value. If the app\n * does not launch, enable the text area for regular data entry.\n * <p>\n * See {@link org.odk.collect.android.widgets.ExStringWidget} for usage.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class ExDecimalWidget extends ExStringWidget {\n\n    public ExDecimalWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry, StringRequester stringRequester, Dependencies dependencies) {\n        super(context, questionDetails, waitingForDataRegistry, stringRequester, dependencies);\n\n        boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt());\n        Double answer = StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue());\n        binding.widgetAnswerText.setDecimalType(useThousandSeparator, answer);\n    }\n\n    @Override\n    protected Serializable getAnswerForIntent() {\n        return StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(getFormEntryPrompt().getAnswerValue());\n    }\n\n    @Override\n    protected int getRequestCode() {\n        return RequestCodes.EX_DECIMAL_CAPTURE;\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return StringWidgetUtils.getDecimalData(binding.widgetAnswerText.getAnswer(), getFormEntryPrompt());\n    }\n\n    @Override\n    public void setData(Object answer) {\n        DecimalData decimalData = ExternalAppsUtils.asDecimalData(answer);\n        binding.widgetAnswerText.setAnswer(decimalData == null ? null : decimalData.getValue().toString());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/ExImageWidget.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.view.View;\nimport android.widget.ImageView;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.ExImageWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.ApplicationConstants;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.interfaces.FileWidget;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.FileRequester;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.androidshared.ui.ToastUtils;\n\nimport java.io.File;\n\nimport timber.log.Timber;\n\n@SuppressLint(\"ViewConstructor\")\npublic class ExImageWidget extends QuestionWidget implements FileWidget, WidgetDataReceiver {\n    ExImageWidgetAnswerBinding binding;\n\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    private final QuestionMediaManager questionMediaManager;\n    private final FileRequester fileRequester;\n\n    File answerFile;\n\n    public ExImageWidget(Context context, QuestionDetails questionDetails, QuestionMediaManager questionMediaManager,\n                         WaitingForDataRegistry waitingForDataRegistry, FileRequester fileRequester, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        this.questionMediaManager = questionMediaManager;\n        this.fileRequester = fileRequester;\n\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        setupAnswerFile(prompt.getAnswerText());\n\n        binding = ExImageWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n\n        binding.launchExternalAppButton.setVisibility(questionDetails.isReadOnly() ? GONE : VISIBLE);\n        binding.launchExternalAppButton.setOnClickListener(view -> launchExternalApp());\n        binding.imageView.setOnClickListener(view -> mediaUtils.openFile(getContext(), answerFile, \"image/*\"));\n        if (answerFile != null) {\n            displayImage();\n        } else {\n            binding.imageView.setVisibility(GONE);\n        }\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public void deleteFile() {\n        questionMediaManager.deleteAnswerFile(getFormEntryPrompt().getIndex().toString(), answerFile.getAbsolutePath());\n        answerFile = null;\n    }\n\n    @Override\n    public void clearAnswer() {\n        deleteFile();\n        binding.imageView.setVisibility(GONE);\n        widgetValueChanged();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return answerFile != null ? new StringData(answerFile.getName()) : null;\n    }\n\n    @Override\n    public void setData(Object object) {\n        if (answerFile != null) {\n            clearAnswer();\n        }\n\n        if (object instanceof File && mediaUtils.isImageFile((File) object)) {\n            answerFile = (File) object;\n            if (answerFile.exists()) {\n                questionMediaManager.replaceAnswerFile(getFormEntryPrompt().getIndex().toString(), answerFile.getAbsolutePath());\n                displayImage();\n                widgetValueChanged();\n            } else {\n                Timber.e(new Error(\"Inserting Image file FAILED\"));\n            }\n        } else if (object != null) {\n            if (object instanceof File) {\n                ToastUtils.showLongToast(org.odk.collect.strings.R.string.invalid_file_type);\n                mediaUtils.deleteMediaFile(((File) object).getAbsolutePath());\n                Timber.e(new Error(\"ExImageWidget's setBinaryData must receive an image file but received: \" + FileUtils.getMimeType((File) object)));\n            } else {\n                Timber.e(new Error(\"ExImageWidget's setBinaryData must receive an image file but received: \" + object.getClass()));\n            }\n        }\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.launchExternalAppButton.setOnLongClickListener(l);\n        binding.imageView.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.launchExternalAppButton.cancelLongPress();\n        binding.imageView.cancelLongPress();\n    }\n\n    private void launchExternalApp() {\n        waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex());\n        fileRequester.launch((Activity) getContext(), ApplicationConstants.RequestCodes.EX_IMAGE_CHOOSER, getFormEntryPrompt());\n    }\n\n    private void setupAnswerFile(String fileName) {\n        if (fileName != null && !fileName.isEmpty()) {\n            answerFile = questionMediaManager.getAnswerFile(fileName);\n        }\n    }\n\n    private void displayImage() {\n        imageLoader.loadImage(binding.imageView, answerFile, ImageView.ScaleType.FIT_CENTER, null);\n        binding.imageView.setVisibility(VISIBLE);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/ExIntegerWidget.java",
    "content": "/*\n * Copyright (C) 2012 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.IntegerData;\nimport org.odk.collect.android.dynamicpreload.ExternalAppsUtils;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.utilities.StringRequester;\nimport org.odk.collect.android.widgets.utilities.StringWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\nimport java.io.Serializable;\n\n/**\n * Launch an external app to supply an integer value. If the app\n * does not launch, enable the text area for regular data entry.\n * <p>\n * See {@link org.odk.collect.android.widgets.ExStringWidget} for usage.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class ExIntegerWidget extends ExStringWidget {\n\n    public ExIntegerWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry, StringRequester stringRequester, Dependencies dependencies) {\n        super(context, questionDetails, waitingForDataRegistry, stringRequester, dependencies);\n\n        boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt());\n        Integer answer = StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue());\n        binding.widgetAnswerText.setIntegerType(useThousandSeparator, answer);\n    }\n\n    @Override\n    protected Serializable getAnswerForIntent()  {\n        return StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(getFormEntryPrompt().getAnswerValue());\n    }\n\n    @Override\n    protected int getRequestCode() {\n        return RequestCodes.EX_INT_CAPTURE;\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return StringWidgetUtils.getIntegerData(binding.widgetAnswerText.getAnswer(), getFormEntryPrompt());\n    }\n\n    @Override\n    public void setData(Object answer) {\n        IntegerData integerData = ExternalAppsUtils.asIntegerData(answer);\n        binding.widgetAnswerText.setAnswer(integerData == null ? null : integerData.getValue().toString());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/ExStringWidget.java",
    "content": "/*\n * Copyright (C) 2012 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport static org.odk.collect.android.utilities.Appearances.hasAppearance;\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.core.content.ContextCompat;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.ExStringQuestionTypeBinding;\nimport org.odk.collect.android.dynamicpreload.ExternalAppsUtils;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.android.widgets.utilities.StringRequester;\nimport org.odk.collect.android.widgets.utilities.StringWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\nimport java.io.Serializable;\n\nimport timber.log.Timber;\n\n/**\n * <p>Launch an external app to supply a string value. If the app\n * does not launch, enable the text area for regular data entry.</p>\n * <p>\n * <p>The default button text is \"Launch\"\n * <p>\n * <p>You may override the button text and the error text that is\n * displayed when the app is missing by using jr:itext() values.\n * <p>\n * <p>To use this widget, define an appearance on the &lt;input/&gt;\n * tag that begins \"ex:\" and then contains the intent action to lauch.\n * <p>\n * <p>e.g.,\n * <p>\n * <pre>\n * &lt;input appearance=\"ex:change.uw.android.TEXTANSWER\" ref=\"/form/passPhrase\" &gt;\n * </pre>\n * <p>or, to customize the button text and error strings with itext:\n * <pre>\n *      ...\n *      &lt;bind nodeset=\"/form/passPhrase\" type=\"string\" /&gt;\n *      ...\n *      &lt;itext&gt;\n *        &lt;translation lang=\"English\"&gt;\n *          &lt;text id=\"textAnswer\"&gt;\n *            &lt;value form=\"short\"&gt;Text question&lt;/value&gt;\n *            &lt;value form=\"long\"&gt;Enter your pass phrase&lt;/value&gt;\n *            &lt;value form=\"buttonText\"&gt;Get Pass Phrase&lt;/value&gt;\n *            &lt;value form=\"noAppErrorString\"&gt;Pass Phrase Tool is not installed!\n *             Please proceed to manually enter pass phrase.&lt;/value&gt;\n *          &lt;/text&gt;\n *        &lt;/translation&gt;\n *      &lt;/itext&gt;\n *    ...\n *    &lt;input appearance=\"ex:change.uw.android.TEXTANSWER\" ref=\"/form/passPhrase\"&gt;\n *      &lt;label ref=\"jr:itext('textAnswer')\"/&gt;\n *    &lt;/input&gt;\n * </pre>\n */\n@SuppressLint(\"ViewConstructor\")\npublic class ExStringWidget extends QuestionWidget implements WidgetDataReceiver {\n    public ExStringQuestionTypeBinding binding;\n    private final WaitingForDataRegistry waitingForDataRegistry;\n\n    private boolean hasExApp = true;\n    private final StringRequester stringRequester;\n\n    public ExStringWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry, StringRequester stringRequester, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        this.stringRequester = stringRequester;\n    }\n\n    @Override\n    protected View onCreateWidgetView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) {\n        binding = ExStringQuestionTypeBinding.inflate(LayoutInflater.from(context));\n        binding.launchAppButton.setText(getButtonText());\n        binding.launchAppButton.setOnClickListener(v -> {\n            waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex());\n            stringRequester.launch((Activity) getContext(), getRequestCode(), getFormEntryPrompt(), getAnswerForIntent(), (String errorMsg) -> {\n                onException(errorMsg);\n                return null;\n            });\n        });\n        if (questionDetails.isReadOnly()) {\n            binding.launchAppButton.setVisibility(GONE);\n        }\n        binding.widgetAnswerText.init(\n                QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.HEADLINE_6),\n                true,\n                StringWidgetUtils.getNumberOfRows(questionDetails.getPrompt()),\n                Appearances.isMultiline(prompt),\n                Appearances.isMasked(prompt),\n                this::widgetValueChanged\n        );\n        binding.widgetAnswerText.setAnswer(getFormEntryPrompt().getAnswerText());\n        if (hasAppearance(getFormEntryPrompt(), Appearances.HIDDEN_ANSWER)) {\n            binding.widgetAnswerText.setVisibility(GONE);\n        }\n\n        return binding.getRoot();\n    }\n\n    private String getButtonText() {\n        String v = getFormEntryPrompt().getSpecialFormQuestionText(\"buttonText\");\n        return v != null ? v : getContext().getString(org.odk.collect.strings.R.string.launch_app);\n    }\n\n    protected Serializable getAnswerForIntent() {\n        return getFormEntryPrompt().getAnswerText();\n    }\n\n    protected int getRequestCode() {\n        return RequestCodes.EX_STRING_CAPTURE;\n    }\n\n    @Nullable\n    @Override\n    public IAnswerData getAnswer() {\n        String answer = binding.widgetAnswerText.getAnswer();\n        return !answer.isEmpty() ? new StringData(answer) : null;\n    }\n\n    @Override\n    public void clearAnswer() {\n        binding.widgetAnswerText.clearAnswer();\n    }\n\n    @Override\n    public void setData(Object answer) {\n        StringData stringData = ExternalAppsUtils.asStringData(answer);\n        binding.widgetAnswerText.setAnswer(stringData == null ? null : stringData.getValue().toString());\n    }\n\n    @Override\n    public void setFocus(Context context) {\n        if (hasExApp) {\n            binding.widgetAnswerText.setFocus(false);\n            // focus on launch button\n            binding.launchAppButton.requestFocus();\n        } else {\n            if (!getFormEntryPrompt().isReadOnly()) {\n                binding.widgetAnswerText.setFocus(true);\n            /*\n             * If you do a multi-question screen after a \"add another group\" dialog, this won't\n             * automatically pop up. It's an Android issue.\n             *\n             * That is, if I have an edit text in an activity, and pop a dialog, and in that\n             * dialog's button's OnClick() I call edittext.requestFocus() and\n             * showSoftInput(edittext, 0), showSoftinput() returns false. However, if the\n             * edittext\n             * is focused before the dialog pops up, everything works fine. great.\n             */\n            } else {\n                binding.widgetAnswerText.setFocus(false);\n            }\n        }\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.widgetAnswerText.setOnLongClickListener(l);\n        binding.launchAppButton.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.widgetAnswerText.cancelLongPress();\n        binding.launchAppButton.cancelLongPress();\n    }\n\n    /**\n     * Registers all subviews except for the answer_container (which contains the EditText) to clear on long press.\n     * This makes it possible to long-press to paste or perform other text editing functions.\n     */\n    @Override\n    protected void registerToClearAnswerOnLongPress(Activity activity, ViewGroup viewGroup) {\n        ViewGroup view = findViewById(R.id.question_widget_container);\n        for (int i = 0; i < view.getChildCount(); i++) {\n            View childView = view.getChildAt(i);\n            if (childView.getId() != R.id.answer_container) {\n                childView.setTag(childView.getId());\n                childView.setId(getId());\n                activity.registerForContextMenu(childView);\n            }\n        }\n    }\n\n    private void focusAnswer() {\n        binding.widgetAnswerText.setFocus(true);\n    }\n\n    private void onException(String toastText) {\n        hasExApp = false;\n        if (!getFormEntryPrompt().isReadOnly()) {\n            binding.widgetAnswerText.updateState(false);\n        }\n        binding.launchAppButton.setEnabled(false);\n        binding.launchAppButton.setFocusable(false);\n        waitingForDataRegistry.cancelWaitingForData();\n\n        Toast.makeText(getContext(),\n                toastText, Toast.LENGTH_SHORT)\n                .show();\n        Timber.d(toastText);\n        focusAnswer();\n    }\n\n    @Override\n    public void hideError() {\n        super.hideError();\n        binding.widgetAnswerText.setError(null);\n    }\n\n    @Override\n    public void displayError(String errorMessage) {\n        hideError();\n\n        if (binding.widgetAnswerText.isEditableState()) {\n            binding.widgetAnswerText.setError(errorMessage);\n            setBackground(ContextCompat.getDrawable(getContext(), R.drawable.question_with_error_border));\n        } else {\n            ((TextView) errorLayout.findViewById(R.id.error_message)).setText(errorMessage);\n            errorLayout.setVisibility(VISIBLE);\n            setBackground(ContextCompat.getDrawable(getContext(), R.drawable.question_with_error_border));\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/GeoPointMapWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport static org.odk.collect.geo.GeoUtils.parseGeometryPoint;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.View;\n\nimport org.javarosa.core.model.data.GeoPointData;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.form.api.FormEntryPrompt;\n\nimport org.odk.collect.android.databinding.GeopointQuestionBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.interfaces.GeoDataRequester;\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\n@SuppressLint(\"ViewConstructor\")\npublic class GeoPointMapWidget extends QuestionWidget implements WidgetDataReceiver {\n    GeopointQuestionBinding binding;\n\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    private final GeoDataRequester geoDataRequester;\n\n    private String answerText;\n\n    public GeoPointMapWidget(Context context, QuestionDetails questionDetails,\n                             WaitingForDataRegistry waitingForDataRegistry, GeoDataRequester geoDataRequester, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        this.geoDataRequester = geoDataRequester;\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = GeopointQuestionBinding.inflate(((Activity) context).getLayoutInflater());\n\n        binding.geoAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n\n        binding.simpleButton.setOnClickListener(v -> geoDataRequester.requestGeoPoint(prompt, answerText, waitingForDataRegistry));\n\n        answerText = prompt.getAnswerText();\n        String answerToDisplay = GeoWidgetUtils.getGeoPointAnswerToDisplay(getContext(), answerText);\n\n        if (answerToDisplay.isEmpty()) {\n            if (getFormEntryPrompt().isReadOnly()) {\n                binding.simpleButton.setVisibility(View.GONE);\n            } else {\n                binding.simpleButton.setText(org.odk.collect.strings.R.string.get_point);\n            }\n            answerText = null;\n        } else {\n            if (getFormEntryPrompt().isReadOnly()) {\n                binding.simpleButton.setText(org.odk.collect.strings.R.string.view_point);\n            } else {\n                binding.simpleButton.setText(org.odk.collect.strings.R.string.view_or_change_point);\n            }\n\n            binding.geoAnswerText.setText(answerToDisplay);\n        }\n        binding.geoAnswerText.setVisibility(binding.geoAnswerText.getText().toString().isBlank() ? GONE : VISIBLE);\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        double[] parsedGeometryPoint = parseGeometryPoint(answerText);\n        return parsedGeometryPoint == null\n                ? null\n                : new GeoPointData(parsedGeometryPoint);\n    }\n\n    @Override\n    public void clearAnswer() {\n        answerText = null;\n        binding.geoAnswerText.setText(null);\n        binding.geoAnswerText.setVisibility(GONE);\n        binding.simpleButton.setText(org.odk.collect.strings.R.string.get_point);\n        widgetValueChanged();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.simpleButton.setOnLongClickListener(l);\n        binding.geoAnswerText.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.simpleButton.cancelLongPress();\n        binding.geoAnswerText.cancelLongPress();\n    }\n\n    @Override\n    public void setData(Object answer) {\n        String answerToDisplay = GeoWidgetUtils.getGeoPointAnswerToDisplay(getContext(), answer.toString());\n        if (answerToDisplay.isEmpty()) {\n            answerText = null;\n            binding.geoAnswerText.setText(\"\");\n            binding.geoAnswerText.setVisibility(GONE);\n            binding.simpleButton.setText(org.odk.collect.strings.R.string.get_point);\n        } else {\n            answerText = answer.toString();\n            binding.geoAnswerText.setText(answerToDisplay);\n            binding.geoAnswerText.setVisibility(VISIBLE);\n            binding.simpleButton.setText(org.odk.collect.strings.R.string.view_or_change_point);\n        }\n        widgetValueChanged();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/GeoPointWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport static org.odk.collect.geo.GeoUtils.parseGeometryPoint;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.View;\n\nimport org.javarosa.core.model.data.GeoPointData;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.GeopointQuestionBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.interfaces.GeoDataRequester;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\n@SuppressLint(\"ViewConstructor\")\npublic class GeoPointWidget extends QuestionWidget implements WidgetDataReceiver {\n    GeopointQuestionBinding binding;\n\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    private final GeoDataRequester geoDataRequester;\n\n    private String answerText;\n\n    public GeoPointWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry,\n                          GeoDataRequester geoDataRequester, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        this.geoDataRequester = geoDataRequester;\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = GeopointQuestionBinding.inflate(((Activity) context).getLayoutInflater());\n\n        binding.geoAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n        if (prompt.isReadOnly()) {\n            binding.simpleButton.setVisibility(GONE);\n        } else {\n            binding.simpleButton.setOnClickListener(v -> geoDataRequester.requestGeoPoint(prompt, answerText, waitingForDataRegistry));\n        }\n\n        answerText = prompt.getAnswerText();\n\n        String answerToDisplay = GeoWidgetUtils.getGeoPointAnswerToDisplay(getContext(), answerText);\n        if (answerToDisplay.isEmpty()) {\n            binding.simpleButton.setText(org.odk.collect.strings.R.string.get_point);\n            answerText = null;\n        } else {\n            binding.geoAnswerText.setText(answerToDisplay);\n            binding.simpleButton.setText(org.odk.collect.strings.R.string.change_point);\n        }\n        binding.geoAnswerText.setVisibility(binding.geoAnswerText.getText().toString().isBlank() ? GONE : VISIBLE);\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        double[] parsedGeometryPoint = parseGeometryPoint(answerText);\n        return parsedGeometryPoint == null\n                ? null\n                : new GeoPointData(parsedGeometryPoint);\n    }\n\n    @Override\n    public void clearAnswer() {\n        answerText = null;\n        binding.geoAnswerText.setText(null);\n        binding.geoAnswerText.setVisibility(GONE);\n        binding.simpleButton.setText(org.odk.collect.strings.R.string.get_point);\n        widgetValueChanged();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.simpleButton.setOnLongClickListener(l);\n        binding.geoAnswerText.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.simpleButton.cancelLongPress();\n        binding.geoAnswerText.cancelLongPress();\n    }\n\n    @Override\n    public void setData(Object answer) {\n        String answerToDisplay = GeoWidgetUtils.getGeoPointAnswerToDisplay(getContext(), answer.toString());\n        if (answerToDisplay.isEmpty()) {\n            answerText = null;\n            binding.geoAnswerText.setText(\"\");\n            binding.geoAnswerText.setVisibility(GONE);\n            binding.simpleButton.setText(org.odk.collect.strings.R.string.get_point);\n        } else {\n            answerText = answer.toString();\n            binding.geoAnswerText.setText(answerToDisplay);\n            binding.geoAnswerText.setVisibility(VISIBLE);\n            binding.simpleButton.setText(org.odk.collect.strings.R.string.change_point);\n        }\n        widgetValueChanged();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/GeoShapeWidget.java",
    "content": "/*\n * Copyright (C) 2014 GeoODK\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.View;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.GeoshapeQuestionBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.interfaces.GeoDataRequester;\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils;\n\n@SuppressLint(\"ViewConstructor\")\npublic class GeoShapeWidget extends QuestionWidget {\n    GeoshapeQuestionBinding binding;\n\n    private final GeoDataRequester geoDataRequester;\n\n    public GeoShapeWidget(Context context, QuestionDetails questionDetails,\n                          GeoDataRequester geoDataRequester, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        this.geoDataRequester = geoDataRequester;\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = GeoshapeQuestionBinding.inflate(((Activity) context).getLayoutInflater());\n\n        binding.geoAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n\n        binding.simpleButton.setOnClickListener(v -> geoDataRequester.requestGeoPoly(prompt));\n\n        String stringAnswer = GeoWidgetUtils.getGeoPolyAnswerToDisplay(prompt.getAnswerText());\n        binding.geoAnswerText.setText(stringAnswer);\n        binding.geoAnswerText.setVisibility(binding.geoAnswerText.getText().toString().isBlank() ? GONE : VISIBLE);\n\n        boolean dataAvailable = stringAnswer != null && !stringAnswer.isEmpty();\n\n        if (getFormEntryPrompt().isReadOnly()) {\n            if (dataAvailable) {\n                binding.simpleButton.setText(org.odk.collect.strings.R.string.view_polygon);\n            } else {\n                binding.simpleButton.setVisibility(View.GONE);\n            }\n        } else {\n            if (dataAvailable) {\n                binding.simpleButton.setText(org.odk.collect.strings.R.string.view_or_change_polygon);\n            } else {\n                binding.simpleButton.setText(org.odk.collect.strings.R.string.get_polygon);\n            }\n        }\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return getFormEntryPrompt().getAnswerValue();\n    }\n\n    @Override\n    public void clearAnswer() {}\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.simpleButton.setOnLongClickListener(l);\n        binding.geoAnswerText.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.simpleButton.cancelLongPress();\n        binding.geoAnswerText.cancelLongPress();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/GeoTraceWidget.java",
    "content": "/*\n * Copyright (C) 2015 GeoODK\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.View;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.GeotraceQuestionBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.interfaces.GeoDataRequester;\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils;\nimport org.odk.collect.maps.MapConfigurator;\n\n/**\n * GeoTraceWidget allows the user to collect a trace of GPS points as the\n * device moves along a path.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class GeoTraceWidget extends QuestionWidget {\n    GeotraceQuestionBinding binding;\n\n    private final MapConfigurator mapConfigurator;\n    private final GeoDataRequester geoDataRequester;\n\n    public GeoTraceWidget(Context context, QuestionDetails questionDetails,\n                          MapConfigurator mapConfigurator, GeoDataRequester geoDataRequester, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        this.mapConfigurator = mapConfigurator;\n        this.geoDataRequester = geoDataRequester;\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = GeotraceQuestionBinding.inflate(((Activity) context).getLayoutInflater());\n\n        binding.geoAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n\n        binding.simpleButton.setOnClickListener(v -> {\n            if (mapConfigurator.isAvailable(context)) {\n                geoDataRequester.requestGeoPoly(prompt);\n            } else {\n                mapConfigurator.showUnavailableMessage(context);\n            }\n        });\n\n        String stringAnswer = GeoWidgetUtils.getGeoPolyAnswerToDisplay(prompt.getAnswerText());\n        binding.geoAnswerText.setText(stringAnswer);\n        binding.geoAnswerText.setVisibility(binding.geoAnswerText.getText().toString().isBlank() ? GONE : VISIBLE);\n\n        boolean dataAvailable = stringAnswer != null && !stringAnswer.isEmpty();\n\n        if (getFormEntryPrompt().isReadOnly()) {\n            if (dataAvailable) {\n                binding.simpleButton.setText(org.odk.collect.strings.R.string.view_line);\n            } else {\n                binding.simpleButton.setVisibility(View.GONE);\n            }\n        } else {\n            if (dataAvailable) {\n                binding.simpleButton.setText(org.odk.collect.strings.R.string.view_or_change_line);\n            } else {\n                binding.simpleButton.setText(org.odk.collect.strings.R.string.get_line);\n            }\n        }\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return getFormEntryPrompt().getAnswerValue();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.simpleButton.setOnLongClickListener(l);\n        binding.geoAnswerText.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void clearAnswer() {}\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.simpleButton.cancelLongPress();\n        binding.geoAnswerText.cancelLongPress();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/ImageWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.View;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.ImageWidgetBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.utilities.ImageCaptureIntentCreator;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.androidshared.system.CameraUtils;\nimport org.odk.collect.selfiecamera.CaptureSelfieActivity;\nimport java.util.Locale;\n\n/**\n * Widget that allows user to take pictures, sounds or video and add them to the form.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n * @author Yaw Anokwa (yanokwa@gmail.com)\n */\n\n@SuppressLint(\"ViewConstructor\")\npublic class ImageWidget extends BaseImageWidget {\n    ImageWidgetBinding binding;\n\n    private boolean selfie;\n\n    public ImageWidget(Context context, final QuestionDetails prompt, QuestionMediaManager questionMediaManager, WaitingForDataRegistry waitingForDataRegistry, String tmpImageFilePath, Dependencies dependencies) {\n        super(context, prompt, questionMediaManager, waitingForDataRegistry, tmpImageFilePath, dependencies);\n        imageClickHandler = new ViewImageClickHandler();\n        imageCaptureHandler = new ImageCaptureHandler();\n\n        render();\n        updateAnswer();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = ImageWidgetBinding.inflate(((Activity) context).getLayoutInflater());\n\n        String appearance = prompt.getAppearanceHint();\n        selfie = Appearances.isFrontCameraAppearance(prompt);\n        if (selfie || ((appearance != null && appearance.toLowerCase(Locale.ENGLISH).contains(Appearances.NEW)))) {\n            binding.chooseButton.setVisibility(View.GONE);\n        }\n\n        binding.captureButton.setOnClickListener(v -> getPermissionsProvider().requestCameraPermission((Activity) getContext(), this::captureImage));\n        binding.chooseButton.setOnClickListener(v -> imageCaptureHandler.chooseImage(org.odk.collect.strings.R.string.choose_image));\n        binding.image.setOnClickListener(v -> imageClickHandler.clickImage(\"viewImage\"));\n\n        if (questionDetails.isReadOnly()) {\n            binding.captureButton.setVisibility(View.GONE);\n            binding.chooseButton.setVisibility(View.GONE);\n        }\n\n        errorTextView = binding.errorMessage;\n        imageView = binding.image;\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public Intent addExtrasToIntent(Intent intent) {\n        return intent;\n    }\n\n    @Override\n    protected boolean doesSupportDefaultValues() {\n        return false;\n    }\n\n    @Override\n    public void clearAnswer() {\n        super.clearAnswer();\n        binding.captureButton.setText(getContext().getString(org.odk.collect.strings.R.string.capture_image));\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.captureButton.setOnLongClickListener(l);\n        binding.chooseButton.setOnLongClickListener(l);\n        super.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.captureButton.cancelLongPress();\n        binding.chooseButton.cancelLongPress();\n    }\n\n    private void captureImage() {\n        if (selfie && new CameraUtils().isFrontCameraAvailable(getContext())) {\n            Intent intent = new Intent(getContext(), CaptureSelfieActivity.class);\n            intent.putExtra(CaptureSelfieActivity.EXTRA_TMP_PATH, new StoragePathProvider().getOdkDirPath(StorageSubdirectory.CACHE));\n            imageCaptureHandler.captureImage(intent, RequestCodes.MEDIA_FILE_PATH, org.odk.collect.strings.R.string.capture_image);\n        } else {\n            Intent intent = ImageCaptureIntentCreator.imageCaptureIntent(getFormEntryPrompt(), getContext(), tmpImageFilePath);\n            imageCaptureHandler.captureImage(intent, RequestCodes.IMAGE_CAPTURE, org.odk.collect.strings.R.string.capture_image);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/IntegerWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.utilities.StringWidgetUtils;\n\n/**\n * Widget that restricts values to integers.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class IntegerWidget extends StringWidget {\n\n    public IntegerWidget(Context context, QuestionDetails questionDetails, Dependencies dependencies) {\n        super(context, questionDetails, dependencies);\n\n        boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt());\n        Integer answer = StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(questionDetails.getPrompt().getAnswerValue());\n        widgetAnswerText.setIntegerType(useThousandSeparator, answer);\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return StringWidgetUtils.getIntegerData(getAnswerText(), getFormEntryPrompt());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/MediaWidgetAnswerViewModel.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport android.content.Context\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.lifecycle.ViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport org.odk.collect.android.utilities.MediaUtils\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.androidshared.utils.getVideoThumbnail\nimport org.odk.collect.async.Scheduler\n\nclass MediaWidgetAnswerViewModel(\n    private val scheduler: Scheduler,\n    private val questionMediaManager: QuestionMediaManager,\n    private val mediaUtils: MediaUtils\n) : ViewModel() {\n    fun getFrame(answer: String?, context: Context): StateFlow<ImageBitmap?> {\n        val bitmapState = MutableStateFlow<ImageBitmap?>(null)\n\n        val file = questionMediaManager.getAnswerFile(answer)\n        if (file != null) {\n            scheduler.immediate {\n                val thumbnail = file.getVideoThumbnail(context)?.asImageBitmap()\n                bitmapState.value = thumbnail\n            }\n        }\n\n        return bitmapState\n    }\n\n    fun openFile(context: Context, answer: String?, mimeType: String? = null) {\n        val file = questionMediaManager.getAnswerFile(answer)\n        if (file != null) {\n            mediaUtils.openFile(context, file, mimeType)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/OSMWidget.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.content.Intent;\nimport android.view.View;\n\nimport androidx.appcompat.app.AlertDialog;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.model.osm.OSMTag;\nimport org.javarosa.core.model.osm.OSMTagItem;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.OsmWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.androidshared.system.IntentLauncher;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Widget that allows the user to launch OpenMapKit to get an OSM Feature with a\n * predetermined set of tags that are edited in the application.\n *\n * @author Nicholas Hallahan nhallahan@spatialdev.com\n */\n@SuppressLint(\"ViewConstructor\")\npublic class OSMWidget extends QuestionWidget implements WidgetDataReceiver {\n    OsmWidgetAnswerBinding binding;\n\n    public static final String FORM_ID = \"FORM_ID\";\n    public static final String INSTANCE_ID = \"INSTANCE_ID\";\n    public static final String INSTANCE_DIR = \"INSTANCE_DIR\";\n    public static final String FORM_FILE_NAME = \"FORM_FILE_NAME\";\n    public static final String OSM_EDIT_FILE_NAME = \"OSM_EDIT_FILE_NAME\";\n\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    private final IntentLauncher intentLauncher;\n\n    private final List<OSMTag> osmRequiredTags;\n    private final String instanceId;\n    private final String instanceDirectory;\n    private final String formFileName;\n    private final int formId;\n\n    public OSMWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry,\n                     IntentLauncher intentLauncher, FormController formController, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        this.intentLauncher = intentLauncher;\n\n        formFileName = FileUtils.getFormBasenameFromMediaFolder(formController.getMediaFolder());\n\n        instanceDirectory = formController.getInstanceFile().getParent();\n        instanceId = formController.getSubmissionMetadata().instanceId;\n        formId = formController.getFormDef().getID();\n\n        // Determine the tags required\n        osmRequiredTags = questionDetails.getPrompt().getQuestion().getOsmTags();\n\n        // If an OSM File has already been saved, get the name.\n        String osmFileName = questionDetails.getPrompt().getAnswerText();\n\n        if (osmFileName != null) {\n            binding.launchOpenMapKitButton.setText(getContext().getString(org.odk.collect.strings.R.string.recapture_osm));\n            binding.osmFileText.setText(osmFileName);\n        } else {\n            binding.osmFileHeaderText.setVisibility(View.GONE);\n        }\n        binding.osmFileText.setVisibility(binding.osmFileText.getText().toString().isBlank() ? GONE : VISIBLE);\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = OsmWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n\n        if (prompt.isReadOnly()) {\n            binding.launchOpenMapKitButton.setVisibility(GONE);\n        } else {\n            binding.launchOpenMapKitButton.setOnClickListener(v -> onButtonClick());\n        }\n\n        return binding.getRoot();\n    }\n\n    private void launchOpenMapKit() {\n        try {\n            //launch with intent that sends plain text\n            Intent launchIntent = new Intent(Intent.ACTION_SEND);\n            launchIntent.setType(\"text/plain\");\n\n            //send form id\n            launchIntent.putExtra(FORM_ID, String.valueOf(formId));\n\n            //send instance id\n            launchIntent.putExtra(INSTANCE_ID, instanceId);\n\n            //send instance directory\n            launchIntent.putExtra(INSTANCE_DIR, instanceDirectory);\n\n            //send form file name\n            launchIntent.putExtra(FORM_FILE_NAME, formFileName);\n\n            //send OSM file name if there was a previous edit\n            String osmFileName = binding.osmFileText.getText().toString();\n            if (!osmFileName.isEmpty()) {\n                launchIntent.putExtra(OSM_EDIT_FILE_NAME, osmFileName);\n            }\n\n            //send encode tag data structure to intent\n            writeOsmRequiredTagsToExtras(launchIntent);\n\n            waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex());\n            intentLauncher.launchForResult((Activity) getContext(), launchIntent, RequestCodes.OSM_CAPTURE, () -> {\n                waitingForDataRegistry.cancelWaitingForData();\n                binding.errorText.setVisibility(View.VISIBLE);\n                return null;\n            });\n        } catch (Exception ex) {\n            MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext());\n            builder.setTitle(org.odk.collect.strings.R.string.alert);\n            builder.setMessage(org.odk.collect.strings.R.string.install_openmapkit);\n            DialogInterface.OnClickListener okClickListener = (dialog, id) -> {\n                //TODO: launch to app store?\n            };\n            builder.setPositiveButton(getContext().getString(org.odk.collect.strings.R.string.ok), okClickListener);\n            AlertDialog dialog = builder.create();\n            dialog.show();\n        }\n    }\n\n    @Override\n    public void setData(Object answer) {\n        // show file name of saved osm data\n        binding.osmFileText.setText((String) answer);\n        binding.osmFileText.setVisibility(binding.osmFileText.getText().toString().isBlank() ? GONE : VISIBLE);\n        binding.osmFileHeaderText.setVisibility(View.VISIBLE);\n        binding.launchOpenMapKitButton.setText(getContext().getString(org.odk.collect.strings.R.string.recapture_osm));\n        widgetValueChanged();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        String osmFileName = binding.osmFileText.getText().toString();\n        return osmFileName.isEmpty() ? null : new StringData(osmFileName);\n    }\n\n    @Override\n    public void clearAnswer() {\n        binding.osmFileText.setText(null);\n        binding.osmFileText.setVisibility(GONE);\n        binding.osmFileHeaderText.setVisibility(View.GONE);\n        binding.launchOpenMapKitButton.setText(getContext().getString(org.odk.collect.strings.R.string.capture_osm));\n        widgetValueChanged();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.osmFileText.setOnLongClickListener(l);\n        binding.launchOpenMapKitButton.setOnLongClickListener(l);\n    }\n\n    private void onButtonClick() {\n        binding.errorText.setVisibility(View.GONE);\n        launchOpenMapKit();\n    }\n\n    /**\n     * See: https://github.com/AmericanRedCross/openmapkit/wiki/ODK-Collect-Tag-Intent-Extras\n     */\n    private void writeOsmRequiredTagsToExtras(Intent intent) {\n        ArrayList<String> tagKeys = new ArrayList<>();\n        for (OSMTag tag : osmRequiredTags) {\n            tagKeys.add(tag.key);\n            if (tag.label != null) {\n                intent.putExtra(\"TAG_LABEL.\" + tag.key, tag.label);\n            }\n            ArrayList<String> tagValues = new ArrayList<>();\n            if (tag.items != null) {\n                for (OSMTagItem item : tag.items) {\n                    tagValues.add(item.value);\n                    if (item.label != null) {\n                        intent.putExtra(\"TAG_VALUE_LABEL.\" + tag.key + \".\" + item.value,\n                                item.label);\n                    }\n                }\n            }\n            intent.putStringArrayListExtra(\"TAG_VALUES.\" + tag.key, tagValues);\n        }\n        intent.putStringArrayListExtra(\"TAG_KEYS\", tagKeys);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/PrinterWidget.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport android.app.Activity\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.android.widgets.interfaces.Printer\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n\nclass PrinterWidget(\n    context: Context,\n    questionDetails: QuestionDetails,\n    private val printer: Printer,\n    private val questionMediaManager: QuestionMediaManager,\n    dependencies: Dependencies\n) : QuestionWidget(context, dependencies, questionDetails) {\n\n    init {\n        render()\n    }\n\n    override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View {\n        val answerView = LayoutInflater.from(context).inflate(R.layout.printer_widget, null)\n        answerView\n            .findViewById<MultiClickSafeMaterialButton>(R.id.printer_button)\n            .setOnClickListener {\n                print()\n            }\n        return answerView\n    }\n\n    override fun setOnLongClickListener(listener: OnLongClickListener?) = Unit\n\n    override fun getAnswer(): IAnswerData? = formEntryPrompt.answerValue\n\n    override fun clearAnswer() = Unit\n\n    override fun registerToClearAnswerOnLongPress(activity: Activity?, viewGroup: ViewGroup?) = Unit\n\n    private fun print() {\n        formEntryPrompt.answerText?.let {\n            printer.parseAndPrint(it, questionMediaManager, context)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/QuestionWidget.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getClipID;\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getPlayColor;\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getPlayableAudioURI;\nimport static org.odk.collect.android.injection.DaggerUtils.getComponent;\nimport static org.odk.collect.settings.enums.StringIdEnumUtils.getGuidanceHintMode;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.text.method.LinkMovementMethod;\nimport android.util.TypedValue;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Button;\nimport android.widget.FrameLayout;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.ContextCompat;\n\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.utilities.AnimationUtils;\nimport org.odk.collect.android.utilities.FormEntryPromptUtils;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.android.utilities.SoftKeyboardController;\nimport org.odk.collect.android.utilities.ThemeUtils;\nimport org.odk.collect.android.widgets.interfaces.Widget;\nimport org.odk.collect.android.widgets.items.SelectImageMapWidget;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.androidshared.utils.ScreenUtils;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.imageloader.ImageLoader;\nimport org.odk.collect.permissions.PermissionsProvider;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.enums.GuidanceHintMode;\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.io.File;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\npublic abstract class QuestionWidget extends FrameLayout implements Widget {\n\n    private final FormEntryPrompt formEntryPrompt;\n    private final AudioVideoImageTextLabel audioVideoImageTextLabel;\n    protected final QuestionDetails questionDetails;\n    private final TextView helpTextView;\n    private final View guidanceTextLayout;\n    private final View textLayout;\n    private final TextView warningText;\n    public final View errorLayout;\n    protected final Settings settings;\n    private AtomicBoolean expanded;\n    protected final ThemeUtils themeUtils;\n    private WidgetValueChangedListener valueChangedListener;\n\n    @Inject\n    public ReferenceManager referenceManager;\n\n    @Inject\n    public ScreenUtils screenUtils;\n\n    @Inject\n    public SoftKeyboardController softKeyboardController;\n\n    @Inject\n    PermissionsProvider permissionsProvider;\n\n    @Inject\n    protected SettingsProvider settingsProvider;\n\n    @Inject\n    protected\n    MediaUtils mediaUtils;\n\n    @Inject\n    ImageLoader imageLoader;\n\n    @Inject\n    protected Scheduler scheduler;\n\n    protected AudioPlayer audioPlayer;\n\n    public QuestionWidget(Context context, Dependencies dependencies, QuestionDetails questionDetails) {\n        super(context);\n\n        audioPlayer = dependencies.getAudioPlayer();\n        getComponent(context).inject(this);\n        setId(View.generateViewId());\n        settings = settingsProvider.getUnprotectedSettings();\n\n        themeUtils = new ThemeUtils(context);\n\n        this.questionDetails = questionDetails;\n        formEntryPrompt = questionDetails.getPrompt();\n\n        ViewGroup containerView = inflate(context, getLayout(), this).findViewById(R.id.question_widget_container);\n        audioVideoImageTextLabel = containerView.findViewById(R.id.question_label);\n        setupQuestionLabel();\n\n        View helpTextLayout = findViewById(R.id.help_text);\n        guidanceTextLayout = helpTextLayout.findViewById(R.id.guidance_text_layout);\n        textLayout = helpTextLayout.findViewById(R.id.text_layout);\n        warningText = helpTextLayout.findViewById(R.id.warning_text);\n        helpTextView = setupHelpText(helpTextLayout.findViewById(R.id.help_text_view), formEntryPrompt);\n        errorLayout = findViewById(R.id.error_message_container);\n        setupGuidanceTextAndLayout(helpTextLayout.findViewById(R.id.guidance_text_view), formEntryPrompt);\n\n        if (context instanceof Activity && !questionDetails.isReadOnly()) {\n            registerToClearAnswerOnLongPress((Activity) context, this);\n        }\n\n        hideAnswerContainerIfNeeded();\n    }\n\n    public void render() {\n        View answerView = onCreateWidgetView(getContext(),\n                questionDetails.getPrompt(),\n                QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.HEADLINE_6)\n        );\n\n        if (answerView != null) {\n            ViewGroup answerContainer = findViewById(R.id.answer_container);\n\n            RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(\n                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);\n            params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);\n\n            answerContainer.addView(answerView, params);\n\n            adjustButtonFontSize(answerContainer);\n        }\n    }\n\n    /**\n     * Returns the `View` object that represents the interface for answering the question. This\n     * will be rendered underneath the question's `label`, `hint` and `guidance_hint`. This method\n     * is passed the question itself (as a `FormEntryPrompt`) which will often be needed in\n     * rendering the widget. It is also passed the size to be used for question text.\n     */\n    @SuppressWarnings(\"PMD.EmptyMethodInAbstractClassShouldBeAbstract\")\n    protected View onCreateWidgetView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) {\n        return null;\n    }\n\n    /**\n     * Used to make sure clickable views in the widget work with the long click feature (shows\n     * the \"Edit Prompt\" menu). The passed listener should be set as the long click listener on\n     * clickable views in the widget.\n     */\n    public abstract void setOnLongClickListener(OnLongClickListener l);\n\n    protected int getLayout() {\n        return R.layout.question_widget;\n    }\n\n    private void setupQuestionLabel() {\n        audioVideoImageTextLabel.setTag(getClipID(formEntryPrompt));\n        audioVideoImageTextLabel.setText(formEntryPrompt.getLongText(), formEntryPrompt.isRequired(), QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.TITLE_LARGE));\n        audioVideoImageTextLabel.setMediaUtils(mediaUtils);\n\n        String imageURI = this instanceof SelectImageMapWidget ? null : formEntryPrompt.getImageText();\n        String videoURI = formEntryPrompt.getSpecialFormQuestionText(\"video\");\n        String bigImageURI = formEntryPrompt.getSpecialFormQuestionText(\"big-image\");\n        String playableAudioURI = getPlayableAudioURI(formEntryPrompt, referenceManager);\n        try {\n            if (imageURI != null) {\n                audioVideoImageTextLabel.setImage(new File(referenceManager.deriveReference(imageURI).getLocalURI()), imageLoader);\n            }\n            if (bigImageURI != null) {\n                audioVideoImageTextLabel.setBigImage(new File(referenceManager.deriveReference(bigImageURI).getLocalURI()));\n            }\n            if (videoURI != null) {\n                audioVideoImageTextLabel.setVideo(new File(referenceManager.deriveReference(videoURI).getLocalURI()));\n            }\n            if (playableAudioURI != null) {\n                audioVideoImageTextLabel.setAudio(playableAudioURI, audioPlayer);\n            }\n        } catch (InvalidReferenceException e) {\n            Timber.d(e, \"Invalid media reference due to %s \", e.getMessage());\n        }\n\n        audioVideoImageTextLabel.setPlayTextColor(getPlayColor(formEntryPrompt, themeUtils));\n    }\n\n    private TextView setupGuidanceTextAndLayout(TextView guidanceTextView, FormEntryPrompt prompt) {\n        TextView guidance;\n        GuidanceHintMode setting = getGuidanceHintMode(settings, getContext());\n\n        if (setting.equals(GuidanceHintMode.NO)) {\n            return null;\n        }\n\n        String guidanceHint = prompt.getSpecialFormQuestionText(prompt.getQuestion().getHelpTextID(), \"guidance\");\n\n        if (android.text.TextUtils.isEmpty(guidanceHint)) {\n            return null;\n        }\n\n        guidance = configureGuidanceTextView(guidanceTextView, guidanceHint);\n\n        expanded = new AtomicBoolean(false);\n\n        if (setting.equals(GuidanceHintMode.YES)) {\n            guidanceTextLayout.setVisibility(VISIBLE);\n        } else if (setting.equals(GuidanceHintMode.YES_COLLAPSED)) {\n            guidanceTextLayout.setVisibility(expanded.get() ? VISIBLE : GONE);\n\n            View icon = textLayout.findViewById(R.id.help_icon);\n            icon.setVisibility(VISIBLE);\n\n            /**\n             * Added click listeners to the individual views because the TextView\n             * intercepts click events when they are being passed to the parent layout.\n             */\n            icon.setOnClickListener(v -> {\n                if (!expanded.get()) {\n                    AnimationUtils.expand(guidanceTextLayout, result -> expanded.set(true));\n                } else {\n                    AnimationUtils.collapse(guidanceTextLayout, result -> expanded.set(false));\n                }\n            });\n\n            getHelpTextView().setOnClickListener(v -> {\n                if (!expanded.get()) {\n                    AnimationUtils.expand(guidanceTextLayout, result -> expanded.set(true));\n                } else {\n                    AnimationUtils.collapse(guidanceTextLayout, result -> expanded.set(false));\n                }\n            });\n        }\n\n        return guidance;\n    }\n\n    private TextView configureGuidanceTextView(TextView guidanceTextView, String guidance) {\n        guidanceTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.SUBTITLE_1));\n        guidanceTextView.setHorizontallyScrolling(false);\n\n        guidanceTextView.setText(HtmlUtils.textToHtml(guidance));\n\n        guidanceTextView.setMovementMethod(LinkMovementMethod.getInstance());\n        return guidanceTextView;\n    }\n\n    public TextView getHelpTextView() {\n        return helpTextView;\n    }\n\n    public FormEntryPrompt getFormEntryPrompt() {\n        return formEntryPrompt;\n    }\n\n    public QuestionDetails getQuestionDetails() {\n        return questionDetails;\n    }\n\n    public void setFocus(Context context) {\n        softKeyboardController.hideSoftKeyboard(this);\n    }\n\n    /**\n     * Override this to implement fling gesture suppression (e.g. for embedded WebView treatments).\n     *\n     * @return true if the fling gesture should be suppressed\n     */\n    public boolean shouldSuppressFlingGesture() {\n        return false;\n    }\n\n    private TextView setupHelpText(TextView helpText, FormEntryPrompt prompt) {\n        String s = prompt.getHelpText();\n\n        if (s != null && !s.equals(\"\")) {\n            helpText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.SUBTITLE_1));\n            // wrap to the widget of vi\n            helpText.setHorizontallyScrolling(false);\n            if (prompt.getLongText() == null || prompt.getLongText().isEmpty()) {\n                helpText.setText(FormEntryPromptUtils.styledQuestionText(s, prompt.isRequired()));\n            } else {\n                helpText.setText(HtmlUtils.textToHtml(s));\n            }\n            helpText.setMovementMethod(LinkMovementMethod.getInstance());\n            return helpText;\n        } else {\n            helpText.setVisibility(View.GONE);\n            return helpText;\n        }\n    }\n\n    private void hideAnswerContainerIfNeeded() {\n        if (questionDetails.isReadOnly() && formEntryPrompt.getAnswerValue() == null) {\n            findViewById(R.id.answer_container).setVisibility(GONE);\n        }\n    }\n\n    public void showAnswerContainer() {\n        findViewById(R.id.answer_container).setVisibility(VISIBLE);\n    }\n\n    /**\n     * Register this widget's child views to pop up a context menu to clear the widget when the\n     * user long presses on it. Widget subclasses may override this if some or all of their\n     * components need to intercept long presses.\n     */\n    protected void registerToClearAnswerOnLongPress(Activity activity, ViewGroup viewGroup) {\n        activity.registerForContextMenu(this);\n    }\n\n    /**\n     * Every subclassed widget should override this, adding any views they may contain, and calling\n     * super.cancelLongPress()\n     */\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        if (getAudioVideoImageTextLabel() != null) {\n            getAudioVideoImageTextLabel().cancelLongPress();\n        }\n        if (getHelpTextView() != null) {\n            getHelpTextView().cancelLongPress();\n        }\n    }\n\n    public void showWarning(String warningBody) {\n        warningText.setVisibility(View.VISIBLE);\n        warningText.setText(warningBody);\n    }\n\n    public AudioVideoImageTextLabel getAudioVideoImageTextLabel() {\n        return audioVideoImageTextLabel;\n    }\n\n    public ReferenceManager getReferenceManager() {\n        return referenceManager;\n    }\n\n    public PermissionsProvider getPermissionsProvider() {\n        return permissionsProvider;\n    }\n\n    public void setPermissionsProvider(PermissionsProvider permissionsProvider) {\n        this.permissionsProvider = permissionsProvider;\n    }\n\n    public void setValueChangedListener(WidgetValueChangedListener valueChangedListener) {\n        this.valueChangedListener = valueChangedListener;\n    }\n\n    public void widgetValueChanged() {\n        hideError();\n        if (valueChangedListener != null) {\n            valueChangedListener.widgetValueChanged(this);\n        }\n    }\n\n    /*\n    Loop through each child view to identify buttons and dynamically adjust their font size based on\n    the current settings. This efficient approach eliminates the need to manually adjust button font\n    sizes for individual widgets. Furthermore, this method lays the groundwork for potential future\n    enhancements, such as extending font size adjustments to other view types like text views etc.\n     */\n    private void adjustButtonFontSize(ViewGroup view) {\n        for (int i = 0; i < view.getChildCount(); i++) {\n            View childView = view.getChildAt(i);\n            if (childView instanceof ViewGroup) {\n                adjustButtonFontSize((ViewGroup) childView);\n            } else if (childView instanceof Button) {\n                ((Button) childView).setTextSize(QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.BODY_LARGE));\n            }\n        }\n    }\n\n    public void hideError() {\n        errorLayout.setVisibility(GONE);\n        setBackground(null);\n    }\n\n    public void displayError(String errorMessage) {\n        ((TextView) errorLayout.findViewById(R.id.error_message)).setText(errorMessage);\n        errorLayout.setVisibility(VISIBLE);\n        setBackground(ContextCompat.getDrawable(getContext(), R.drawable.question_with_error_border));\n    }\n\n    public static class Dependencies {\n\n        private final AudioPlayer audioPlayer;\n        private final MediaWidgetAnswerViewModel mediaWidgetAnswerViewModel;\n\n        public Dependencies(AudioPlayer audioPlayer, MediaWidgetAnswerViewModel mediaWidgetAnswerViewModel) {\n            this.audioPlayer = audioPlayer;\n            this.mediaWidgetAnswerViewModel = mediaWidgetAnswerViewModel;\n        }\n\n        public AudioPlayer getAudioPlayer() {\n            return audioPlayer;\n        }\n\n        public MediaWidgetAnswerViewModel getMediaWidgetAnswerViewModel() {\n            return mediaWidgetAnswerViewModel;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/RatingWidget.java",
    "content": "/*\n * Copyright (C) 2018 Akshay Patel\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.view.MotionEvent;\nimport android.view.View;\n\nimport org.javarosa.core.model.RangeQuestion;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.IntegerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.RatingWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.ViewUtils;\nimport org.odk.collect.androidshared.utils.ScreenUtils;\n\n@SuppressLint(\"ViewConstructor\")\npublic class RatingWidget extends QuestionWidget {\n    private static final int ASSUMED_TOTAL_MARGIN_AROUND_WIDGET = 40;\n    private static final int STANDARD_WIDTH_OF_STAR = 48;\n\n    RatingWidgetAnswerBinding binding;\n\n    public RatingWidget(Context context, QuestionDetails questionDetails, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = RatingWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n        View answerView = binding.getRoot();\n\n        int numberOfStars = getTotalStars((RangeQuestion) prompt.getQuestion());\n        int maxNumberOfStars = calculateMaximumStarsInOneLine();\n\n        if (maxNumberOfStars < numberOfStars) {\n            binding.ratingBar1.setNumStars(maxNumberOfStars);\n            binding.ratingBar1.setMax(maxNumberOfStars);\n            binding.ratingBar2.setNumStars(Math.min(numberOfStars - maxNumberOfStars, maxNumberOfStars));\n            binding.ratingBar2.setMax(Math.min(numberOfStars - maxNumberOfStars, maxNumberOfStars));\n\n            binding.ratingBar2.setVisibility(View.VISIBLE);\n        } else {\n            binding.ratingBar1.setNumStars(numberOfStars);\n            binding.ratingBar1.setMax(numberOfStars);\n        }\n\n        if (prompt.isReadOnly()) {\n            binding.ratingBar1.setEnabled(false);\n            binding.ratingBar2.setEnabled(false);\n        } else {\n            setUpRatingBar(maxNumberOfStars);\n        }\n\n        if (prompt.getAnswerText() != null) {\n            int rating = Integer.parseInt(prompt.getAnswerText());\n            if (rating > maxNumberOfStars) {\n                binding.ratingBar2.setRating(rating - maxNumberOfStars);\n            } else {\n                binding.ratingBar1.setRating(rating);\n            }\n        }\n        return answerView;\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener listener) {\n        binding.ratingBar1.setOnLongClickListener(listener);\n        binding.ratingBar2.setOnLongClickListener(listener);\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return binding.ratingBar1.getRating() == 0.0F\n                ? null\n                : new IntegerData((int) (binding.ratingBar1.getRating() + binding.ratingBar2.getRating()));\n    }\n\n    @Override\n    public void clearAnswer() {\n        binding.ratingBar1.setRating(0.0F);\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    private void setUpRatingBar(int maxNumberOfStars) {\n        // to quickly change rating on other rating bar in case onRatingChange listener is not called\n        binding.ratingBar1.setOnTouchListener((v, event) -> {\n            if (event.getAction() == MotionEvent.ACTION_DOWN) {\n                binding.ratingBar2.setRating(0);\n            }\n            return false;\n        });\n\n        binding.ratingBar2.setOnTouchListener((v, event) -> {\n            if (event.getAction() == MotionEvent.ACTION_DOWN) {\n                binding.ratingBar1.setRating(maxNumberOfStars);\n            }\n            return false;\n        });\n\n        binding.ratingBar1.setOnRatingBarChangeListener((ratingBar, rating, fromUser) -> {\n            binding.ratingBar2.setRating(0);\n            binding.ratingBar1.setRating(rating);\n            widgetValueChanged();\n        });\n\n        binding.ratingBar2.setOnRatingBarChangeListener((ratingBar, rating, fromUser) -> {\n            binding.ratingBar1.setRating(maxNumberOfStars);\n            binding.ratingBar2.setRating(rating);\n            widgetValueChanged();\n        });\n    }\n\n    private int calculateMaximumStarsInOneLine() {\n        return (int) ((ScreenUtils.getScreenWidth(getContext()) - ASSUMED_TOTAL_MARGIN_AROUND_WIDGET)\n                / ViewUtils.pxFromDp(getContext(), STANDARD_WIDTH_OF_STAR));\n    }\n\n    private int getTotalStars(RangeQuestion rangeQuestion) {\n        return rangeQuestion.getRangeEnd().intValue();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/SignatureWidget.java",
    "content": "/*\n * Copyright (C) 2012 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.View;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.SignatureWidgetBinding;\nimport org.odk.collect.draw.DrawActivity;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\n/**\n * Signature widget.\n *\n * @author BehrAtherton@gmail.com\n */\n@SuppressLint(\"ViewConstructor\")\npublic class SignatureWidget extends BaseImageWidget {\n    SignatureWidgetBinding binding;\n\n    public SignatureWidget(Context context, QuestionDetails prompt, QuestionMediaManager questionMediaManager, WaitingForDataRegistry waitingForDataRegistry, String tmpImageFilePath, Dependencies dependencies) {\n        super(context, prompt, questionMediaManager, waitingForDataRegistry, tmpImageFilePath, dependencies);\n        imageClickHandler = new DrawImageClickHandler(DrawActivity.OPTION_SIGNATURE, RequestCodes.SIGNATURE_CAPTURE, org.odk.collect.strings.R.string.signature_capture);\n\n        render();\n        updateAnswer();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = SignatureWidgetBinding.inflate(((Activity) context).getLayoutInflater());\n        binding.signButton.setOnClickListener(v -> imageClickHandler.clickImage(\"signButton\"));\n        binding.image.setOnClickListener(v -> imageClickHandler.clickImage(\"viewImage\"));\n\n        if (questionDetails.isReadOnly()) {\n            binding.signButton.setVisibility(View.GONE);\n        }\n\n        errorTextView = binding.errorMessage;\n        imageView = binding.image;\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public Intent addExtrasToIntent(Intent intent) {\n        return intent;\n    }\n\n    @Override\n    protected boolean doesSupportDefaultValues() {\n        return true;\n    }\n\n    @Override\n    public void clearAnswer() {\n        super.clearAnswer();\n        binding.signButton.setText(getContext().getString(org.odk.collect.strings.R.string.sign_button));\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.signButton.setOnLongClickListener(l);\n        super.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.signButton.cancelLongPress();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/StringNumberWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.utilities.StringWidgetUtils;\n\n/**\n * Widget that restricts values to integers.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class StringNumberWidget extends StringWidget {\n\n    public StringNumberWidget(Context context, QuestionDetails questionDetails, Dependencies dependencies) {\n        super(context, questionDetails, dependencies);\n\n        boolean useThousandSeparator = Appearances.useThousandSeparator(questionDetails.getPrompt());\n        String answer = questionDetails.getPrompt().getAnswerValue() == null\n                ? null\n                : questionDetails.getPrompt().getAnswerValue().getValue().toString();\n        widgetAnswerText.setStringNumberType(useThousandSeparator, answer);\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return StringWidgetUtils.getStringNumberData(getAnswerText(), getFormEntryPrompt());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/StringWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.ContextCompat;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.views.WidgetAnswerText;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.android.widgets.utilities.StringWidgetUtils;\n\n/**\n * The most basic widget that allows for entry of any text.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class StringWidget extends QuestionWidget {\n    public final WidgetAnswerText widgetAnswerText;\n\n    protected StringWidget(Context context, QuestionDetails questionDetails, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n\n        widgetAnswerText = new WidgetAnswerText(context);\n        widgetAnswerText.init(\n                QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.HEADLINE_6),\n                questionDetails.isReadOnly(),\n                StringWidgetUtils.getNumberOfRows(questionDetails.getPrompt()),\n                Appearances.isMultiline(questionDetails.getPrompt()),\n                Appearances.isMasked(questionDetails.getPrompt()),\n                this::widgetValueChanged\n        );\n\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) {\n        setDisplayValueFromModel();\n        return widgetAnswerText;\n    }\n\n    @Override\n    public void clearAnswer() {\n        widgetAnswerText.clearAnswer();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        String answer = getAnswerText();\n        return !answer.isEmpty() ? new StringData(answer) : null;\n    }\n\n    @NonNull\n    public String getAnswerText() {\n        return widgetAnswerText.getAnswer();\n    }\n\n    @Override\n    public void setFocus(Context context) {\n        widgetAnswerText.setFocus(!questionDetails.isReadOnly());\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        widgetAnswerText.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        widgetAnswerText.cancelLongPress();\n    }\n\n    /**\n     * Registers all subviews except for the answer_container (which contains the EditText) to clear on long press.\n     * This makes it possible to long-press to paste or perform other text editing functions.\n     */\n    @Override\n    protected void registerToClearAnswerOnLongPress(Activity activity, ViewGroup viewGroup) {\n        ViewGroup view = findViewById(R.id.question_widget_container);\n        for (int i = 0; i < view.getChildCount(); i++) {\n            View childView = view.getChildAt(i);\n            if (childView.getId() != R.id.answer_container) {\n                childView.setTag(childView.getId());\n                childView.setId(getId());\n                activity.registerForContextMenu(childView);\n            }\n        }\n    }\n\n    public void setDisplayValueFromModel() {\n        String currentAnswer = getFormEntryPrompt().getAnswerText();\n\n        if (currentAnswer != null) {\n            widgetAnswerText.setAnswer(currentAnswer);\n        }\n    }\n\n    @Override\n    public void hideError() {\n        widgetAnswerText.setError(null);\n        setBackground(null);\n    }\n\n    @Override\n    public void displayError(String errorMessage) {\n        widgetAnswerText.setError(errorMessage);\n        setBackground(ContextCompat.getDrawable(getContext(), R.drawable.question_with_error_border));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/TextWidgetAnswer.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.fromHtml\nimport androidx.compose.ui.unit.sp\nimport org.odk.collect.android.utilities.HtmlUtils\nimport org.odk.collect.androidshared.R.dimen\nimport org.odk.collect.androidshared.ui.compose.marginSmall\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\n@Composable\nfun TextWidgetAnswer(\n    modifier: Modifier,\n    icon: ImageVector?,\n    answer: String,\n    fontSize: Int? = null,\n    horizontalArrangement: Arrangement.Horizontal = Arrangement.Center,\n    onClick: () -> Unit = {},\n    onLongClick: () -> Unit,\n    onClickLabel: String? = null\n) {\n    val annotatedAnswer = remember(answer) {\n        AnnotatedString.fromHtml(HtmlUtils.markdownToHtml(answer))\n    }\n    val hasFormatting = annotatedAnswer.spanStyles.isNotEmpty()\n\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .combinedClickable(\n                interactionSource = remember { MutableInteractionSource() },\n                indication = null,\n                onClick = {\n                    if (MultiClickGuard.allowClick()) {\n                        onClick()\n                    }\n                },\n                onLongClick = onLongClick,\n                onClickLabel = onClickLabel\n            ),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = horizontalArrangement\n    ) {\n        if (icon != null) {\n            Icon(\n                modifier = Modifier.padding(end = marginSmall()),\n                imageVector = icon,\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.onSurface.copy(\n                    alpha = dimen.high_emphasis.toFloat()\n                )\n            )\n        }\n        Text(\n            text = annotatedAnswer,\n            style = MaterialTheme.typography.bodyLarge.copy(\n                fontSize = fontSize?.sp ?: MaterialTheme.typography.bodyLarge.fontSize,\n                color = MaterialTheme.colorScheme.onSurface.copy(\n                    alpha = if (!hasFormatting) dimen.high_emphasis.toFloat() else 1f\n                )\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/TimedGridWidget.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.core.model.IFormElement\nimport org.javarosa.core.model.SelectChoice\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.activities.FormFillingActivity\nimport org.odk.collect.android.formentry.FormEntryViewModel\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.widgets.items.ItemsWidgetUtils\nimport org.odk.collect.timedgrid.FormAnswerRefresher\nimport org.odk.collect.timedgrid.FormControllerFacade\nimport org.odk.collect.timedgrid.NavigationAwareWidget\nimport org.odk.collect.timedgrid.NavigationWarning\nimport org.odk.collect.timedgrid.TimedGridWidgetDelegate\n\n@SuppressLint(\"ViewConstructor\")\nclass TimedGridWidget(\n    context: Context,\n    questionDetails: QuestionDetails,\n    dependencies: Dependencies,\n    formEntryViewModel: FormEntryViewModel\n) : QuestionWidget(context, dependencies, questionDetails), NavigationAwareWidget {\n    private val widgetDelegate = TimedGridWidgetDelegate(\n        context,\n        questionDetails.prompt,\n        object : FormControllerFacade {\n            override fun getFormElements(): List<IFormElement>? {\n                return formEntryViewModel.formController.getFormDef()?.children\n            }\n\n            override fun getItems(): List<SelectChoice> {\n                return ItemsWidgetUtils.loadItemsAndHandleErrors(\n                    this@TimedGridWidget, questionDetails.prompt, formEntryViewModel\n                )\n            }\n\n            override fun saveAnswer(index: FormIndex, answer: IAnswerData) {\n                formEntryViewModel.formController.saveOneScreenAnswer(index, answer, false)\n            }\n        },\n        object : FormAnswerRefresher {\n            override fun refreshAnswer(index: FormIndex) {\n                val activity = context as? FormFillingActivity ?: return\n                val odkView = activity.currentViewIfODKView ?: return\n\n                val widget = odkView.widgets\n                    .filterIsInstance<StringWidget>()\n                    .firstOrNull { it.formEntryPrompt.index == index }\n                    ?: return\n\n                widget.apply {\n                    setDisplayValueFromModel()\n                    widgetValueChanged()\n                    showAnswerContainer()\n                }\n            }\n        }\n    ) {\n        widgetValueChanged()\n    }\n\n    init {\n        render()\n    }\n\n    override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int) = widgetDelegate.onCreateWidgetView(this)\n\n    override fun getAnswer() = widgetDelegate.getAnswer()\n\n    override fun clearAnswer() {}\n\n    override fun setOnLongClickListener(l: OnLongClickListener?) {}\n\n    override fun onDetachedFromWindow() {\n        super.onDetachedFromWindow()\n        widgetDelegate.onDetachedFromWindow()\n    }\n\n    override fun shouldBlockNavigation(): NavigationWarning? = widgetDelegate.shouldBlockNavigation()\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/TriggerWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CheckBox;\n\nimport androidx.appcompat.widget.AppCompatCheckBox;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\n\n@SuppressLint(\"ViewConstructor\")\npublic class TriggerWidget extends QuestionWidget {\n\n    private static final String OK_TEXT = \"OK\";\n\n    private AppCompatCheckBox triggerButton;\n\n    public TriggerWidget(Context context, QuestionDetails prompt, Dependencies dependencies) {\n        super(context, dependencies, prompt);\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerTextSize) {\n        ViewGroup answerView = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.trigger_widget_answer, null);\n\n        triggerButton = answerView.findViewById(R.id.check_box);\n        triggerButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerTextSize);\n        triggerButton.setEnabled(!prompt.isReadOnly());\n        triggerButton.setChecked(OK_TEXT.equals(prompt.getAnswerText()));\n        triggerButton.setOnCheckedChangeListener((buttonView, isChecked) -> widgetValueChanged());\n        triggerButton.setId(View.generateViewId());\n\n        return answerView;\n    }\n\n    @Override\n    public void clearAnswer() {\n        triggerButton.setChecked(false);\n        widgetValueChanged();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return triggerButton.isChecked() ? new StringData(OK_TEXT) : null;\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        triggerButton.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        triggerButton.cancelLongPress();\n    }\n\n    public CheckBox getCheckBox() {\n        return triggerButton;\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/UrlWidget.java",
    "content": "/*\n * Copyright (C) 2013 Nafundi LLC\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.net.Uri;\nimport android.view.View;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.UrlWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.webpage.WebPageService;\n\n@SuppressLint(\"ViewConstructor\")\npublic class UrlWidget extends QuestionWidget {\n    UrlWidgetAnswerBinding binding;\n\n    private final WebPageService webPageService;\n\n    public UrlWidget(Context context, QuestionDetails questionDetails, WebPageService webPageService, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        this.webPageService = webPageService;\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = UrlWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n        binding.urlButton.setOnClickListener(v -> onButtonClick());\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public void clearAnswer() {\n        ToastUtils.showShortToast(\"URL is readonly\");\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return getFormEntryPrompt().getAnswerValue() == null\n                ? null\n                : new StringData(getFormEntryPrompt().getAnswerText());\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.urlButton.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.urlButton.cancelLongPress();\n    }\n\n    public void onButtonClick() {\n        if (getFormEntryPrompt().getAnswerValue() != null) {\n            Activity activity = (Activity) getContext();\n            webPageService.openWebPage(activity, Uri.parse(getFormEntryPrompt().getAnswerText()));\n        } else {\n            ToastUtils.showShortToast(\"No URL set\");\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/WidgetAnswer.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.AttachFile\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.res.vectorResource\nimport org.javarosa.core.model.Constants\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.widgets.video.VideoWidgetAnswer\nimport org.odk.collect.icons.R\n\n@Composable\nfun WidgetAnswer(\n    modifier: Modifier = Modifier,\n    prompt: FormEntryPrompt,\n    answer: String?,\n    fontSize: Int? = null,\n    summaryView: Boolean = false,\n    mediaWidgetAnswerViewModel: MediaWidgetAnswerViewModel,\n    onClick: () -> Unit = {},\n    onLongClick: () -> Unit = {}\n) {\n    if (!answer.isNullOrEmpty()) {\n        when (prompt.controlType) {\n            Constants.CONTROL_INPUT -> {\n                when (prompt.dataType) {\n                    Constants.DATATYPE_BARCODE -> TextWidgetAnswer(\n                        modifier,\n                        ImageVector.vectorResource(R.drawable.ic_baseline_barcode_scanner_white_24),\n                        answer,\n                        fontSize,\n                        if (summaryView) Arrangement.Start else Arrangement.Center,\n                        onClick,\n                        onLongClick\n                    )\n                    else -> TextWidgetAnswer(\n                        modifier,\n                        null,\n                        answer,\n                        fontSize,\n                        if (summaryView) Arrangement.Start else Arrangement.Center,\n                        onClick,\n                        onLongClick\n                    )\n                }\n            }\n            Constants.CONTROL_VIDEO_CAPTURE -> VideoWidgetAnswer(modifier, answer, mediaWidgetAnswerViewModel, onLongClick)\n            Constants.CONTROL_FILE_CAPTURE -> {\n                val context = LocalContext.current\n\n                TextWidgetAnswer(\n                    modifier,\n                    Icons.Default.AttachFile,\n                    answer,\n                    fontSize,\n                    if (summaryView) Arrangement.Start else Arrangement.Center,\n                    { mediaWidgetAnswerViewModel.openFile(context, answer) },\n                    onLongClick,\n                    stringResource(org.odk.collect.strings.R.string.open_file)\n                )\n            }\n            else -> TextWidgetAnswer(\n                modifier,\n                null,\n                answer,\n                fontSize,\n                if (summaryView) Arrangement.Start else Arrangement.Center,\n                onClick,\n                onLongClick\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/WidgetAnswerView.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport android.content.Context\nimport android.widget.FrameLayout\n\nabstract class WidgetAnswerView(context: Context) : FrameLayout(context) {\n    abstract fun setAnswer(answer: String?)\n\n    abstract fun setFontSize()\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/WidgetFactory.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets;\n\nimport static org.odk.collect.android.utilities.Appearances.MAPS;\nimport static org.odk.collect.android.utilities.Appearances.PLACEMENT_MAP;\nimport static org.odk.collect.android.utilities.Appearances.hasAppearance;\n\nimport android.content.Context;\nimport android.hardware.SensorManager;\n\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.lifecycle.LifecycleOwner;\n\nimport org.javarosa.core.model.Constants;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.formentry.FormEntryViewModel;\nimport org.odk.collect.android.formentry.PrinterWidgetViewModel;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.geo.MapConfiguratorProvider;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.arbitraryfile.ArbitraryFileWidget;\nimport org.odk.collect.android.widgets.arbitraryfile.ExArbitraryFileWidget;\nimport org.odk.collect.android.widgets.barcode.BarcodeWidget;\nimport org.odk.collect.android.widgets.datetime.DateTimeWidget;\nimport org.odk.collect.android.widgets.datetime.DateWidget;\nimport org.odk.collect.android.widgets.datetime.TimeWidget;\nimport org.odk.collect.android.widgets.items.LabelWidget;\nimport org.odk.collect.android.widgets.items.LikertWidget;\nimport org.odk.collect.android.widgets.items.ListMultiWidget;\nimport org.odk.collect.android.widgets.items.ListWidget;\nimport org.odk.collect.android.widgets.items.RankingWidget;\nimport org.odk.collect.android.widgets.items.SelectMultiImageMapWidget;\nimport org.odk.collect.android.widgets.items.SelectMultiMinimalWidget;\nimport org.odk.collect.android.widgets.items.SelectMultiWidget;\nimport org.odk.collect.android.widgets.items.SelectOneFromMapWidget;\nimport org.odk.collect.android.widgets.items.SelectOneImageMapWidget;\nimport org.odk.collect.android.widgets.items.SelectOneMinimalWidget;\nimport org.odk.collect.android.widgets.items.SelectOneWidget;\nimport org.odk.collect.android.widgets.range.RangeDecimalWidget;\nimport org.odk.collect.android.widgets.range.RangeIntegerWidget;\nimport org.odk.collect.android.widgets.range.RangePickerWidget;\nimport org.odk.collect.android.widgets.utilities.ActivityGeoDataRequester;\nimport org.odk.collect.android.widgets.utilities.AudioRecorderRecordingStatusHandler;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.FileRequester;\nimport org.odk.collect.android.widgets.utilities.GetContentAudioFileRequester;\nimport org.odk.collect.android.widgets.utilities.RecordingRequester;\nimport org.odk.collect.android.widgets.utilities.RecordingRequesterProvider;\nimport org.odk.collect.android.widgets.utilities.StringRequester;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.android.widgets.video.ExVideoWidget;\nimport org.odk.collect.android.widgets.video.VideoWidget;\nimport org.odk.collect.androidshared.system.CameraUtils;\nimport org.odk.collect.androidshared.system.IntentLauncherImpl;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.permissions.PermissionsProvider;\nimport org.odk.collect.webpage.CustomTabsWebPageService;\n\n/**\n * Convenience class that handles creation of widgets.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n */\npublic class WidgetFactory {\n\n    private static final String PICKER_APPEARANCE = \"picker\";\n\n    private final FragmentActivity activity;\n    private final boolean useExternalRecorder;\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    private final QuestionMediaManager questionMediaManager;\n    private final AudioPlayer audioPlayer;\n    private final RecordingRequesterProvider recordingRequesterProvider;\n    private final FormEntryViewModel formEntryViewModel;\n    private final PrinterWidgetViewModel printerWidgetViewModel;\n    private final AudioRecorder audioRecorder;\n    private final LifecycleOwner viewLifecycle;\n    private final FileRequester fileRequester;\n    private final StringRequester stringRequester;\n    private final FormController formController;\n    private final MediaWidgetAnswerViewModel mediaWidgetAnswerViewModel;\n\n    public WidgetFactory(FragmentActivity activity,\n                         boolean useExternalRecorder,\n                         WaitingForDataRegistry waitingForDataRegistry,\n                         QuestionMediaManager questionMediaManager,\n                         AudioPlayer audioPlayer,\n                         RecordingRequesterProvider recordingRequesterProvider,\n                         FormEntryViewModel formEntryViewModel,\n                         PrinterWidgetViewModel printerWidgetViewModel,\n                         AudioRecorder audioRecorder,\n                         LifecycleOwner viewLifecycle,\n                         FileRequester fileRequester,\n                         StringRequester stringRequester,\n                         FormController formController,\n                         MediaWidgetAnswerViewModel mediaWidgetAnswerViewModel\n    ) {\n        this.activity = activity;\n        this.useExternalRecorder = useExternalRecorder;\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        this.questionMediaManager = questionMediaManager;\n        this.audioPlayer = audioPlayer;\n        this.recordingRequesterProvider = recordingRequesterProvider;\n        this.formEntryViewModel = formEntryViewModel;\n        this.printerWidgetViewModel = printerWidgetViewModel;\n        this.audioRecorder = audioRecorder;\n        this.viewLifecycle = viewLifecycle;\n        this.fileRequester = fileRequester;\n        this.stringRequester = stringRequester;\n        this.formController = formController;\n        this.mediaWidgetAnswerViewModel = mediaWidgetAnswerViewModel;\n    }\n\n    public QuestionWidget createWidgetFromPrompt(FormEntryPrompt prompt, PermissionsProvider permissionsProvider) {\n        return createWidgetFromPrompt(prompt, permissionsProvider, false);\n    }\n\n    public QuestionWidget createWidgetFromPrompt(FormEntryPrompt prompt, PermissionsProvider permissionsProvider, boolean readOnlyOverride) {\n        String appearance = Appearances.getSanitizedAppearanceHint(prompt);\n        QuestionDetails questionDetails = new QuestionDetails(prompt, readOnlyOverride);\n        QuestionWidget.Dependencies dependencies = new QuestionWidget.Dependencies(audioPlayer, mediaWidgetAnswerViewModel);\n\n        final QuestionWidget questionWidget;\n        switch (prompt.getControlType()) {\n            case Constants.CONTROL_INPUT:\n                switch (prompt.getDataType()) {\n                    case Constants.DATATYPE_DATE_TIME:\n                        questionWidget = new DateTimeWidget(activity, questionDetails, new DateTimeWidgetUtils(), waitingForDataRegistry, dependencies);\n                        break;\n                    case Constants.DATATYPE_DATE:\n                        questionWidget = new DateWidget(activity, questionDetails, new DateTimeWidgetUtils(), waitingForDataRegistry, dependencies);\n                        break;\n                    case Constants.DATATYPE_TIME:\n                        questionWidget = new TimeWidget(activity, questionDetails, new DateTimeWidgetUtils(), waitingForDataRegistry, dependencies);\n                        break;\n                    case Constants.DATATYPE_DECIMAL:\n                        if (appearance.contains(Appearances.EX)) {\n                            questionWidget = new ExDecimalWidget(activity, questionDetails, waitingForDataRegistry, stringRequester, dependencies);\n                        } else if (appearance.equals(Appearances.BEARING)) {\n                            questionWidget = new BearingWidget(activity, questionDetails, waitingForDataRegistry,\n                                    (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE), dependencies);\n                        } else {\n                            questionWidget = new DecimalWidget(activity, questionDetails, dependencies);\n                        }\n                        break;\n                    case Constants.DATATYPE_INTEGER:\n                        if (appearance.equals(Appearances.COUNTER)) {\n                            questionWidget = new CounterWidget(activity, questionDetails, dependencies);\n                        } else if (appearance.contains(Appearances.EX)) {\n                            questionWidget = new ExIntegerWidget(activity, questionDetails, waitingForDataRegistry, stringRequester, dependencies);\n                        } else {\n                            questionWidget = new IntegerWidget(activity, questionDetails, dependencies);\n                        }\n                        break;\n                    case Constants.DATATYPE_GEOPOINT:\n                        if (hasAppearance(questionDetails.getPrompt(), PLACEMENT_MAP) || hasAppearance(questionDetails.getPrompt(), MAPS)) {\n                            questionWidget = new GeoPointMapWidget(activity, questionDetails, waitingForDataRegistry,\n                                    new ActivityGeoDataRequester(permissionsProvider, activity), dependencies);\n                        } else {\n                            questionWidget = new GeoPointWidget(activity, questionDetails, waitingForDataRegistry,\n                                    new ActivityGeoDataRequester(permissionsProvider, activity), dependencies);\n                        }\n                        break;\n                    case Constants.DATATYPE_GEOSHAPE:\n                        questionWidget = new GeoShapeWidget(activity, questionDetails,\n                                new ActivityGeoDataRequester(permissionsProvider, activity), dependencies);\n                        break;\n                    case Constants.DATATYPE_GEOTRACE:\n                        questionWidget = new GeoTraceWidget(activity, questionDetails,\n                                MapConfiguratorProvider.getConfigurator(), new ActivityGeoDataRequester(permissionsProvider, activity), dependencies);\n                        break;\n                    case Constants.DATATYPE_BARCODE:\n                        questionWidget = new BarcodeWidget(activity, questionDetails, dependencies, waitingForDataRegistry, new CameraUtils());\n                        break;\n                    case Constants.DATATYPE_TEXT:\n                        String query = prompt.getQuestion().getAdditionalAttribute(null, \"query\");\n                        if (query != null) {\n                            questionWidget = getSelectOneWidget(appearance, questionDetails, dependencies);\n                        } else if (appearance.equals(Appearances.PRINTER)) {\n                            questionWidget = new PrinterWidget(activity, questionDetails, printerWidgetViewModel, questionMediaManager, dependencies);\n                        } else if (appearance.contains(Appearances.EX)) {\n                            questionWidget = new ExStringWidget(activity, questionDetails, waitingForDataRegistry, stringRequester, dependencies);\n                        } else if (appearance.contains(Appearances.NUMBERS)) {\n                            questionWidget = new StringNumberWidget(activity, questionDetails, dependencies);\n                        } else if (appearance.equals(Appearances.URL)) {\n                            questionWidget = new UrlWidget(activity, questionDetails, CustomTabsWebPageService.INSTANCE, dependencies);\n                        } else {\n                            questionWidget = new StringWidget(activity, questionDetails, dependencies);\n                        }\n                        break;\n                    default:\n                        questionWidget = new StringWidget(activity, questionDetails, dependencies);\n                        break;\n                }\n                break;\n            case Constants.CONTROL_FILE_CAPTURE:\n                if (appearance.startsWith(Appearances.EX)) {\n                    questionWidget = new ExArbitraryFileWidget(activity, questionDetails, dependencies, questionMediaManager, waitingForDataRegistry, fileRequester);\n                } else {\n                    questionWidget = new ArbitraryFileWidget(activity, questionDetails, dependencies, questionMediaManager, waitingForDataRegistry);\n                }\n                break;\n            case Constants.CONTROL_IMAGE_CHOOSE:\n                if (appearance.equals(Appearances.SIGNATURE)) {\n                    questionWidget = new SignatureWidget(activity, questionDetails, questionMediaManager, waitingForDataRegistry, new StoragePathProvider().getTmpImageFilePath(), dependencies);\n                } else if (appearance.contains(Appearances.ANNOTATE)) {\n                    questionWidget = new AnnotateWidget(activity, questionDetails, questionMediaManager, waitingForDataRegistry, new StoragePathProvider().getTmpImageFilePath(), dependencies);\n                } else if (appearance.equals(Appearances.DRAW)) {\n                    questionWidget = new DrawWidget(activity, questionDetails, questionMediaManager, waitingForDataRegistry, new StoragePathProvider().getTmpImageFilePath(), dependencies);\n                } else if (appearance.startsWith(Appearances.EX)) {\n                    questionWidget = new ExImageWidget(activity, questionDetails, questionMediaManager, waitingForDataRegistry, fileRequester, dependencies);\n                } else {\n                    questionWidget = new ImageWidget(activity, questionDetails, questionMediaManager, waitingForDataRegistry, new StoragePathProvider().getTmpImageFilePath(), dependencies);\n                }\n                break;\n            case Constants.CONTROL_OSM_CAPTURE:\n                questionWidget = new OSMWidget(activity, questionDetails, waitingForDataRegistry,\n                        IntentLauncherImpl.INSTANCE, formController, dependencies);\n                break;\n            case Constants.CONTROL_AUDIO_CAPTURE:\n                RecordingRequester recordingRequester = recordingRequesterProvider.create(prompt, useExternalRecorder);\n                GetContentAudioFileRequester audioFileRequester = new GetContentAudioFileRequester(activity, IntentLauncherImpl.INSTANCE, waitingForDataRegistry);\n\n                if (appearance.startsWith(Appearances.EX)) {\n                    questionWidget = new ExAudioWidget(activity, questionDetails, questionMediaManager, audioPlayer, waitingForDataRegistry, fileRequester, dependencies);\n                } else {\n                    questionWidget = new AudioWidget(activity, questionDetails, questionMediaManager, audioPlayer, recordingRequester, audioFileRequester, new AudioRecorderRecordingStatusHandler(audioRecorder, formEntryViewModel, viewLifecycle), dependencies);\n                }\n                break;\n            case Constants.CONTROL_VIDEO_CAPTURE:\n                if (appearance.startsWith(Appearances.EX)) {\n                    questionWidget = new ExVideoWidget(activity, questionDetails, dependencies, questionMediaManager, waitingForDataRegistry, fileRequester);\n                } else {\n                    questionWidget = new VideoWidget(activity, questionDetails, dependencies, questionMediaManager, waitingForDataRegistry);\n                }\n                break;\n            case Constants.CONTROL_SELECT_ONE:\n                questionWidget = getSelectOneWidget(appearance, questionDetails, dependencies);\n                break;\n            case Constants.CONTROL_SELECT_MULTI:\n                // search() appearance/function (not part of XForms spec) added by SurveyCTO gets\n                // considered in each widget by calls to ExternalDataUtil.getSearchXPathExpression.\n                if (appearance.contains(Appearances.MINIMAL)) {\n                    questionWidget = new SelectMultiMinimalWidget(activity, questionDetails, waitingForDataRegistry, formEntryViewModel, dependencies);\n                } else if (appearance.contains(Appearances.LIST_NO_LABEL)) {\n                    questionWidget = new ListMultiWidget(activity, questionDetails, false, formEntryViewModel, dependencies);\n                } else if (appearance.contains(Appearances.LIST)) {\n                    questionWidget = new ListMultiWidget(activity, questionDetails, true, formEntryViewModel, dependencies);\n                } else if (appearance.contains(Appearances.LABEL)) {\n                    questionWidget = new LabelWidget(activity, questionDetails, formEntryViewModel, dependencies);\n                } else if (appearance.contains(Appearances.IMAGE_MAP)) {\n                    questionWidget = new SelectMultiImageMapWidget(activity, questionDetails, formEntryViewModel, dependencies);\n                } else if (appearance.startsWith(Appearances.X_TIMED_GRID)) {\n                    questionWidget = new TimedGridWidget(activity, questionDetails, dependencies, formEntryViewModel);\n                } else {\n                    questionWidget = new SelectMultiWidget(activity, questionDetails, formEntryViewModel, dependencies);\n                }\n                break;\n            case Constants.CONTROL_RANK:\n                questionWidget = new RankingWidget(activity, questionDetails, waitingForDataRegistry, formEntryViewModel, dependencies);\n                break;\n            case Constants.CONTROL_TRIGGER:\n                questionWidget = new TriggerWidget(activity, questionDetails, dependencies);\n                break;\n            case Constants.CONTROL_RANGE:\n                if (appearance.startsWith(Appearances.RATING)) {\n                    questionWidget = new RatingWidget(activity, questionDetails, dependencies);\n                } else {\n                    switch (prompt.getDataType()) {\n                        case Constants.DATATYPE_INTEGER:\n                            if (prompt.getAppearanceHint() != null && prompt.getAppearanceHint().contains(PICKER_APPEARANCE)) {\n                                questionWidget = new RangePickerWidget(activity, questionDetails, dependencies, false);\n                            } else {\n                                questionWidget = new RangeIntegerWidget(activity, questionDetails, dependencies);\n                            }\n                            break;\n                        case Constants.DATATYPE_DECIMAL:\n                            if (prompt.getAppearanceHint() != null && prompt.getAppearanceHint().contains(PICKER_APPEARANCE)) {\n                                questionWidget = new RangePickerWidget(activity, questionDetails, dependencies, true);\n                            } else {\n                                questionWidget = new RangeDecimalWidget(activity, questionDetails, dependencies);\n                            }\n                            break;\n                        default:\n                            questionWidget = new StringWidget(activity, questionDetails, dependencies);\n                            break;\n                    }\n                }\n                break;\n            default:\n                questionWidget = new StringWidget(activity, questionDetails, dependencies);\n                break;\n        }\n\n        return questionWidget;\n    }\n\n    private QuestionWidget getSelectOneWidget(String appearance, QuestionDetails questionDetails, QuestionWidget.Dependencies dependencies) {\n        final QuestionWidget questionWidget;\n        boolean isQuick = appearance.contains(Appearances.QUICK);\n        // search() appearance/function (not part of XForms spec) added by SurveyCTO gets\n        // considered in each widget by calls to ExternalDataUtil.getSearchXPathExpression.\n        if (appearance.contains(Appearances.MINIMAL)) {\n            questionWidget = new SelectOneMinimalWidget(activity, questionDetails, isQuick, waitingForDataRegistry, formEntryViewModel, dependencies);\n        } else if (appearance.contains(Appearances.LIKERT)) {\n            questionWidget = new LikertWidget(activity, questionDetails, formEntryViewModel, dependencies);\n        } else if (appearance.contains(Appearances.LIST_NO_LABEL)) {\n            questionWidget = new ListWidget(activity, questionDetails, false, isQuick, formEntryViewModel, dependencies);\n        } else if (appearance.contains(Appearances.LIST)) {\n            questionWidget = new ListWidget(activity, questionDetails, true, isQuick, formEntryViewModel, dependencies);\n        } else if (appearance.contains(Appearances.LABEL)) {\n            questionWidget = new LabelWidget(activity, questionDetails, formEntryViewModel, dependencies);\n        } else if (appearance.contains(Appearances.IMAGE_MAP)) {\n            questionWidget = new SelectOneImageMapWidget(activity, questionDetails, isQuick, formEntryViewModel, dependencies);\n        } else if (appearance.contains(Appearances.MAP)) {\n            questionWidget = new SelectOneFromMapWidget(activity, questionDetails, dependencies);\n        } else {\n            questionWidget = new SelectOneWidget(activity, questionDetails, isQuick, formController, formEntryViewModel, dependencies);\n        }\n        return questionWidget;\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/WidgetIconButton.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport org.odk.collect.androidshared.R.dimen\nimport org.odk.collect.androidshared.ui.compose.marginSmall\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\n@Composable\nfun WidgetIconButton(\n    icon: ImageVector,\n    text: String,\n    fontSize: Int,\n    onClick: () -> Unit,\n    onLongClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true\n) {\n    val backgroundColor = if (enabled) {\n        MaterialTheme.colorScheme.primary\n    } else {\n        MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)\n    }\n\n    val contentColor = if (enabled) {\n        MaterialTheme.colorScheme.onPrimary\n    } else {\n        MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)\n    }\n\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .clip(CircleShape)\n            .background(backgroundColor)\n            .combinedClickable(\n                enabled = enabled,\n                onClick = {\n                    if (MultiClickGuard.allowClick()) {\n                        onClick()\n                    }\n                },\n                onLongClick = onLongClick,\n                onClickLabel = text\n            )\n            .padding(vertical = marginSmall()),\n        horizontalArrangement = Arrangement.Center,\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Icon(\n            imageVector = icon,\n            tint = contentColor,\n            contentDescription = text,\n            modifier = Modifier\n                .padding(end = dimensionResource(id = dimen.margin_extra_small))\n                .size(20.dp)\n        )\n        Text(\n            text = text,\n            color = contentColor,\n            fontSize = fontSize.sp,\n            style = MaterialTheme.typography.bodyLarge\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidget.kt",
    "content": "package org.odk.collect.android.widgets.arbitraryfile\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.view.View\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.ComposeView\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.android.widgets.QuestionWidget\nimport org.odk.collect.android.widgets.interfaces.FileWidget\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\n\n@SuppressLint(\"ViewConstructor\")\nclass ArbitraryFileWidget(\n    context: Context,\n    questionDetails: QuestionDetails,\n    private val dependencies: Dependencies,\n    questionMediaManager: QuestionMediaManager,\n    private val waitingForDataRegistry: WaitingForDataRegistry\n) : QuestionWidget(context, dependencies, questionDetails), FileWidget, WidgetDataReceiver {\n    private val arbitraryFileWidgetDelegate = ArbitraryFileWidgetDelegate(questionMediaManager)\n    private var answer by mutableStateOf<String?>(formEntryPrompt.answerText)\n\n    init {\n        render()\n    }\n\n    override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View {\n        return ComposeView(context).apply {\n            val readOnly = questionDetails.isReadOnly\n            val buttonFontSize = QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.BODY_LARGE)\n\n            setContextThemedContent {\n                ArbitraryFileWidgetContent(\n                    dependencies.mediaWidgetAnswerViewModel,\n                    formEntryPrompt,\n                    answer,\n                    readOnly,\n                    buttonFontSize,\n                    answerFontSize,\n                    onChooseFileClick = { onButtonClick() },\n                    onLongClick = { showContextMenu() }\n                )\n            }\n        }\n    }\n\n    override fun getAnswer() = arbitraryFileWidgetDelegate.getAnswer(answer)\n\n    override fun deleteFile() {\n        arbitraryFileWidgetDelegate.deleteFile(formEntryPrompt.index.toString(), answer)\n        answer = null\n    }\n\n    override fun setData(answer: Any) {\n        arbitraryFileWidgetDelegate.setData(formEntryPrompt.index.toString(), this.answer, answer) {\n            this.answer = it\n            widgetValueChanged()\n        }\n    }\n\n    override fun clearAnswer() {\n        deleteFile()\n        widgetValueChanged()\n    }\n\n    private fun onButtonClick() {\n        waitingForDataRegistry.waitForData(formEntryPrompt.index)\n        mediaUtils.pickFile(\n            (context as Activity),\n            \"*/*\",\n            ApplicationConstants.RequestCodes.ARBITRARY_FILE_CHOOSER\n        )\n    }\n\n    override fun setOnLongClickListener(listener: OnLongClickListener?) = Unit\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetContent.kt",
    "content": "package org.odk.collect.android.widgets.arbitraryfile\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.AttachFile\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel\nimport org.odk.collect.android.widgets.WidgetAnswer\nimport org.odk.collect.android.widgets.WidgetIconButton\nimport org.odk.collect.androidshared.R.dimen\nimport org.odk.collect.androidshared.ui.compose.marginStandard\nimport org.odk.collect.strings.R.string\n\n@Composable\nfun ArbitraryFileWidgetContent(\n    mediaWidgetAnswerViewModel: MediaWidgetAnswerViewModel,\n    formEntryPrompt: FormEntryPrompt,\n    answer: String?,\n    readOnly: Boolean,\n    buttonFontSize: Int,\n    answerFontSize: Int,\n    onChooseFileClick: () -> Unit,\n    onLongClick: () -> Unit\n) {\n    Column {\n        if (!readOnly) {\n            WidgetIconButton(\n                Icons.Default.AttachFile,\n                stringResource(string.choose_file),\n                buttonFontSize,\n                onChooseFileClick,\n                onLongClick\n            )\n        }\n\n        WidgetAnswer(\n            Modifier.padding(top = marginStandard()),\n            formEntryPrompt,\n            answer,\n            answerFontSize,\n            mediaWidgetAnswerViewModel = mediaWidgetAnswerViewModel,\n            onLongClick = onLongClick\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetDelegate.kt",
    "content": "package org.odk.collect.android.widgets.arbitraryfile\n\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.core.model.data.StringData\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport timber.log.Timber\nimport java.io.File\n\nclass ArbitraryFileWidgetDelegate(\n    private val questionMediaManager: QuestionMediaManager\n) {\n    fun getAnswer(answer: String?): IAnswerData? {\n        return if (answer.isNullOrEmpty()) null else StringData(answer)\n    }\n\n    fun deleteFile(formEntryPromptIndex: String, answer: String?) {\n        questionMediaManager.deleteAnswerFile(\n            formEntryPromptIndex,\n            questionMediaManager.getAnswerFile(answer)!!.absolutePath\n        )\n    }\n\n    fun setData(\n        formEntryPromptIndex: String,\n        previousAnswer: String?,\n        newAnswer: Any,\n        onSuccess: (String) -> Unit\n    ) {\n        if (previousAnswer != null) {\n            deleteFile(formEntryPromptIndex, previousAnswer)\n        }\n\n        if (newAnswer is File) {\n            if (newAnswer.exists()) {\n                questionMediaManager.replaceAnswerFile(\n                    formEntryPromptIndex,\n                    newAnswer.absolutePath\n                )\n                onSuccess(newAnswer.name)\n            } else {\n                Timber.e(Error(\"Inserting Arbitrary file FAILED\"))\n            }\n        } else {\n            Timber.e(Error(\"FileWidget's setBinaryData must receive a File but received: \" + newAnswer.javaClass))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ExArbitraryFileWidget.kt",
    "content": "package org.odk.collect.android.widgets.arbitraryfile\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.view.View\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.ComposeView\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.android.widgets.QuestionWidget\nimport org.odk.collect.android.widgets.interfaces.FileWidget\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver\nimport org.odk.collect.android.widgets.utilities.FileRequester\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\n\n@SuppressLint(\"ViewConstructor\")\nclass ExArbitraryFileWidget(\n    context: Context,\n    questionDetails: QuestionDetails,\n    private val dependencies: Dependencies,\n    questionMediaManager: QuestionMediaManager,\n    private val waitingForDataRegistry: WaitingForDataRegistry,\n    private val fileRequester: FileRequester,\n) : QuestionWidget(context, dependencies, questionDetails), FileWidget, WidgetDataReceiver {\n    private val arbitraryFileWidgetDelegate = ArbitraryFileWidgetDelegate(questionMediaManager)\n    private var answer by mutableStateOf<String?>(formEntryPrompt.answerText)\n\n    init {\n        render()\n    }\n\n    override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View {\n        return ComposeView(context).apply {\n            val readOnly = questionDetails.isReadOnly\n            val buttonFontSize = QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.BODY_LARGE)\n\n            setContextThemedContent {\n                ExArbitraryFileWidgetContent(\n                    dependencies.mediaWidgetAnswerViewModel,\n                    formEntryPrompt,\n                    answer,\n                    readOnly,\n                    buttonFontSize,\n                    answerFontSize,\n                    onLaunchClick = { onButtonClick() },\n                    onLongClick = { showContextMenu() }\n                )\n            }\n        }\n    }\n\n    override fun getAnswer() = arbitraryFileWidgetDelegate.getAnswer(answer)\n\n    override fun deleteFile() {\n        arbitraryFileWidgetDelegate.deleteFile(formEntryPrompt.index.toString(), answer)\n        answer = null\n    }\n\n    override fun setData(answer: Any) {\n        arbitraryFileWidgetDelegate.setData(formEntryPrompt.index.toString(), this.answer, answer) {\n            this.answer = it\n            widgetValueChanged()\n        }\n    }\n\n    override fun clearAnswer() {\n        deleteFile()\n        widgetValueChanged()\n    }\n\n    override fun setOnLongClickListener(listener: OnLongClickListener?) = Unit\n\n    private fun onButtonClick() {\n        waitingForDataRegistry.waitForData(formEntryPrompt.index)\n        fileRequester.launch(\n            (context as Activity), ApplicationConstants.RequestCodes.EX_ARBITRARY_FILE_CHOOSER,\n            formEntryPrompt\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ExArbitraryFileWidgetContent.kt",
    "content": "package org.odk.collect.android.widgets.arbitraryfile\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.OpenInNew\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel\nimport org.odk.collect.android.widgets.WidgetAnswer\nimport org.odk.collect.android.widgets.WidgetIconButton\nimport org.odk.collect.androidshared.R.dimen\nimport org.odk.collect.androidshared.ui.compose.marginStandard\nimport org.odk.collect.strings.R.string\n\n@Composable\nfun ExArbitraryFileWidgetContent(\n    mediaWidgetAnswerViewModel: MediaWidgetAnswerViewModel,\n    formEntryPrompt: FormEntryPrompt,\n    answer: String?,\n    readOnly: Boolean,\n    buttonFontSize: Int,\n    answerFontSize: Int,\n    onLaunchClick: () -> Unit,\n    onLongClick: () -> Unit\n) {\n    Column {\n        if (!readOnly) {\n            WidgetIconButton(\n                Icons.AutoMirrored.Filled.OpenInNew,\n                stringResource(string.launch_app),\n                buttonFontSize,\n                onLaunchClick,\n                onLongClick\n            )\n        }\n\n        WidgetAnswer(\n            Modifier.padding(top = marginStandard()),\n            formEntryPrompt,\n            answer,\n            answerFontSize,\n            mediaWidgetAnswerViewModel = mediaWidgetAnswerViewModel,\n            onLongClick = onLongClick\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/barcode/BarcodeWidget.kt",
    "content": "package org.odk.collect.android.widgets.barcode\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.view.View\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.ComposeView\nimport com.google.zxing.integration.android.IntentIntegrator\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.activities.ScannerWithFlashlightActivity\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.utilities.Appearances\nimport org.odk.collect.android.utilities.Appearances.hasAppearance\nimport org.odk.collect.android.utilities.Appearances.isFrontCameraAppearance\nimport org.odk.collect.android.widgets.QuestionWidget\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry\nimport org.odk.collect.androidshared.system.CameraUtils\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\nimport org.odk.collect.androidshared.ui.ToastUtils.showLongToast\nimport org.odk.collect.permissions.PermissionListener\nimport org.odk.collect.strings.R\n\n@SuppressLint(\"ViewConstructor\")\nclass BarcodeWidget(\n    context: Context,\n    questionDetails: QuestionDetails,\n    private val dependencies: Dependencies,\n    private val waitingForDataRegistry: WaitingForDataRegistry,\n    private val cameraUtils: CameraUtils\n) : QuestionWidget(context, dependencies, questionDetails), WidgetDataReceiver {\n    private var answer by mutableStateOf<String?>(formEntryPrompt.answerText)\n\n    init {\n        render()\n    }\n\n    override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View {\n        return ComposeView(context).apply {\n            val readOnly = questionDetails.isReadOnly\n            val isAnswerHidden = hasAppearance(formEntryPrompt, Appearances.HIDDEN_ANSWER)\n            val buttonFontSize = QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.BODY_LARGE)\n\n            setContextThemedContent {\n                BarcodeWidgetContent(\n                    dependencies.mediaWidgetAnswerViewModel,\n                    formEntryPrompt,\n                    answer,\n                    readOnly,\n                    isAnswerHidden,\n                    buttonFontSize,\n                    answerFontSize,\n                    onGetBarcodeClick = { onButtonClick() },\n                    onLongClick = { showContextMenu() }\n                )\n            }\n        }\n    }\n\n    override fun clearAnswer() {\n        answer = null\n        widgetValueChanged()\n    }\n\n    override fun getAnswer(): IAnswerData? {\n        return if (answer.isNullOrEmpty()) null else StringData(answer!!)\n    }\n\n    override fun setData(answer: Any) {\n        this.answer = stripInvalidCharacters(answer as String)\n        widgetValueChanged()\n    }\n\n    override fun setOnLongClickListener(listener: OnLongClickListener?) = Unit\n\n    private fun onButtonClick() {\n        getPermissionsProvider().requestCameraPermission(\n            (context as Activity),\n            object : PermissionListener {\n                override fun granted() {\n                    waitingForDataRegistry.waitForData(formEntryPrompt.index)\n                    val intent = IntentIntegrator(context as Activity)\n                        .setCaptureActivity(ScannerWithFlashlightActivity::class.java)\n                    setCameraIdIfNeeded(formEntryPrompt, intent)\n                    intent.initiateScan()\n                }\n            }\n        )\n    }\n\n    private fun setCameraIdIfNeeded(prompt: FormEntryPrompt, intent: IntentIntegrator) {\n        if (isFrontCameraAppearance(prompt)) {\n            if (cameraUtils.isFrontCameraAvailable(context)) {\n                intent.addExtra(Appearances.FRONT, true)\n            } else {\n                showLongToast(R.string.error_front_camera_unavailable)\n            }\n        }\n    }\n\n    // Remove control characters, invisible characters and unused code points.\n    private fun stripInvalidCharacters(data: String?): String? {\n        return data?.replace(\"\\\\p{C}\".toRegex(), \"\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/barcode/BarcodeWidgetContent.kt",
    "content": "package org.odk.collect.android.widgets.barcode\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.res.vectorResource\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel\nimport org.odk.collect.android.widgets.WidgetAnswer\nimport org.odk.collect.android.widgets.WidgetIconButton\nimport org.odk.collect.androidshared.R.dimen\nimport org.odk.collect.androidshared.ui.compose.marginStandard\nimport org.odk.collect.icons.R\nimport org.odk.collect.strings.R.string\n\n@Composable\nfun BarcodeWidgetContent(\n    mediaWidgetAnswerViewModel: MediaWidgetAnswerViewModel,\n    formEntryPrompt: FormEntryPrompt,\n    answer: String?,\n    readOnly: Boolean,\n    isAnswerHidden: Boolean,\n    buttonFontSize: Int,\n    answerFontSize: Int,\n    onGetBarcodeClick: () -> Unit,\n    onLongClick: () -> Unit\n) {\n    Column {\n        if (!readOnly) {\n            WidgetIconButton(\n                ImageVector.vectorResource(R.drawable.ic_baseline_barcode_scanner_white_24),\n                if (answer == null) {\n                    stringResource(string.get_barcode)\n                } else {\n                    stringResource(string.replace_barcode)\n                },\n                buttonFontSize,\n                onGetBarcodeClick,\n                onLongClick\n            )\n        }\n\n        if (!isAnswerHidden) {\n            WidgetAnswer(\n                Modifier.padding(top = marginStandard()),\n                formEntryPrompt,\n                answer,\n                answerFontSize,\n                mediaWidgetAnswerViewModel = mediaWidgetAnswerViewModel,\n                onLongClick = onLongClick\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/DatePickerDetails.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.datetime;\n\nimport java.io.Serializable;\n\npublic class DatePickerDetails implements Serializable {\n    public enum DatePickerType {\n        GREGORIAN, ETHIOPIAN, COPTIC, ISLAMIC, BIKRAM_SAMBAT, MYANMAR, PERSIAN, BUDDHIST\n    }\n\n    public enum DatePickerMode {\n        CALENDAR, SPINNERS, MONTH_YEAR, YEAR\n    }\n\n    private final DatePickerType datePickerType;\n    private final DatePickerMode datePickerMode;\n\n    public DatePickerDetails(DatePickerType datePickerType, DatePickerMode datePickerMode) {\n        this.datePickerType = datePickerType;\n        this.datePickerMode = datePickerMode;\n    }\n\n    public boolean isCalendarMode() {\n        return datePickerMode == DatePickerMode.CALENDAR;\n    }\n\n    public boolean isSpinnerMode() {\n        return datePickerMode == DatePickerMode.SPINNERS;\n    }\n\n    public boolean isMonthYearMode() {\n        return datePickerMode == DatePickerMode.MONTH_YEAR;\n    }\n\n    public boolean isYearMode() {\n        return datePickerMode == DatePickerMode.YEAR;\n    }\n\n    public DatePickerType getDatePickerType() {\n        return datePickerType;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj == this) {\n            return true;\n        }\n        if (obj == null) {\n            return false;\n        }\n        if (!(obj instanceof DatePickerDetails)) {\n            return false;\n        }\n        DatePickerDetails datePickerDetails = (DatePickerDetails) obj;\n        return this.datePickerType == datePickerDetails.datePickerType && this.datePickerMode == datePickerDetails.datePickerMode;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = 17;\n        result = 31 * result + datePickerType.hashCode();\n        result = 31 * result + datePickerMode.hashCode();\n        return result;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/DateTimeUtils.java",
    "content": "package org.odk.collect.android.widgets.datetime;\n\nimport org.javarosa.core.model.data.TimeData;\nimport org.joda.time.DateTime;\nimport org.joda.time.DateTimeZone;\nimport org.joda.time.LocalDateTime;\nimport org.joda.time.chrono.GregorianChronology;\n\npublic final class DateTimeUtils {\n\n    private DateTimeUtils() {\n\n    }\n\n    public static LocalDateTime getCurrentDateTime() {\n        return new LocalDateTime()\n                .withDate(DateTime.now().getYear(), DateTime.now().getMonthOfYear(), DateTime.now().getDayOfMonth())\n                .withTime(DateTime.now().getHourOfDay(), DateTime.now().getMinuteOfHour(), 0, 0);\n    }\n\n    public static LocalDateTime getSelectedDate(LocalDateTime selectedDate, LocalDateTime currentTime) {\n        return new LocalDateTime()\n                .withDate(selectedDate.getYear(), selectedDate.getMonthOfYear(), selectedDate.getDayOfMonth())\n                .withTime(currentTime.getHourOfDay(), currentTime.getMinuteOfHour(), 0, 0);\n    }\n\n    public static LocalDateTime getDateAsGregorian(LocalDateTime date) {\n        return skipDaylightSavingGapIfExists(date)\n                .toDateTime()\n                .withChronology(GregorianChronology.getInstance())\n                .toLocalDateTime();\n    }\n\n    public static LocalDateTime getSelectedTime(LocalDateTime selectedTime, LocalDateTime currentDate) {\n        return new LocalDateTime()\n                .withDate(currentDate.getYear(), currentDate.getMonthOfYear(), currentDate.getDayOfMonth())\n                .withTime(selectedTime.getHourOfDay(), selectedTime.getMinuteOfHour(), 0, 0);\n    }\n\n    public static TimeData getTimeData(DateTime dateTime) {\n        return new TimeData(dateTime.toDate());\n    }\n\n    public static LocalDateTime getDateWithSkippedDaylightSavingGapIfExists(LocalDateTime date) {\n        return skipDaylightSavingGapIfExists(date)\n                .toDateTime()\n                .toLocalDateTime();\n    }\n\n    public static LocalDateTime skipDaylightSavingGapIfExists(LocalDateTime date) {\n        final DateTimeZone dtz = DateTimeZone.getDefault();\n\n        if (dtz != null) {\n            while (dtz.isLocalDateTimeGap(date)) {\n                date = date.plusMinutes(1);\n            }\n        }\n        return date;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/DateTimeWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets.datetime;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.View;\n\nimport org.javarosa.core.model.data.DateTimeData;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.TimeData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.joda.time.DateTime;\nimport org.joda.time.LocalDateTime;\nimport org.odk.collect.android.databinding.DateTimeWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\n/**\n * Displays a DatePicker widget. DateWidget handles leap years and does not allow dates that do not\n * exist.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class DateTimeWidget extends QuestionWidget implements WidgetDataReceiver {\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    DateTimeWidgetAnswerBinding binding;\n\n    private final DateTimeWidgetUtils widgetUtils;\n\n    private LocalDateTime selectedDateTime;\n    private DatePickerDetails datePickerDetails;\n\n    public DateTimeWidget(Context context, QuestionDetails prompt, DateTimeWidgetUtils widgetUtils, WaitingForDataRegistry waitingForDataRegistry, Dependencies dependencies) {\n        super(context, dependencies, prompt);\n        render();\n\n        this.widgetUtils = widgetUtils;\n        this.waitingForDataRegistry = waitingForDataRegistry;\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = DateTimeWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n        datePickerDetails = DateTimeWidgetUtils.getDatePickerDetails(prompt.getAppearanceHint());\n\n        if (prompt.isReadOnly()) {\n            binding.dateWidget.dateButton.setVisibility(GONE);\n            binding.timeWidget.timeButton.setVisibility(GONE);\n        } else {\n            binding.dateWidget.dateButton.setOnClickListener(v -> {\n                waitingForDataRegistry.waitForData(prompt.getIndex());\n                widgetUtils.showDatePickerDialog(context, datePickerDetails, selectedDateTime);\n            });\n\n            binding.timeWidget.timeButton.setOnClickListener(v -> {\n                waitingForDataRegistry.waitForData(prompt.getIndex());\n                widgetUtils.showTimePickerDialog(context, selectedDateTime);\n            });\n        }\n        binding.dateWidget.dateAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n        binding.timeWidget.timeAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n\n        selectedDateTime = DateTimeUtils.getCurrentDateTime();\n\n        if (getFormEntryPrompt().getAnswerValue() != null) {\n            LocalDateTime selectedDate = new LocalDateTime(getFormEntryPrompt().getAnswerValue().getValue());\n            selectedDateTime = DateTimeUtils.getSelectedDate(selectedDate, selectedDateTime);\n            binding.dateWidget.dateAnswerText.setText(DateTimeWidgetUtils.getDateTimeLabel(\n                    selectedDate.toDate(), datePickerDetails, false, context));\n\n            DateTime selectedTime = new DateTime(getFormEntryPrompt().getAnswerValue().getValue());\n            selectedDateTime = DateTimeUtils.getSelectedTime(selectedTime.toLocalDateTime(), selectedDateTime);\n            binding.timeWidget.timeAnswerText.setText(DateTimeUtils.getTimeData(selectedTime).getDisplayText());\n        }\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        if (isNullValue()) {\n            return null;\n        } else {\n            if (isTimeNull()) {\n                selectedDateTime = DateTimeUtils.getSelectedDate(selectedDateTime, LocalDateTime.now());\n            } else if (isDateNull()) {\n                selectedDateTime = DateTimeUtils.getSelectedTime(selectedDateTime, LocalDateTime.now());\n            }\n            return new DateTimeData(selectedDateTime.toDate());\n        }\n    }\n\n    @Override\n    public void clearAnswer() {\n        resetAnswerFields();\n        widgetValueChanged();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.dateWidget.dateButton.setOnLongClickListener(l);\n        binding.dateWidget.dateAnswerText.setOnLongClickListener(l);\n\n        binding.timeWidget.timeButton.setOnLongClickListener(l);\n        binding.timeWidget.timeAnswerText.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.dateWidget.dateButton.cancelLongPress();\n        binding.dateWidget.dateAnswerText.cancelLongPress();\n\n        binding.timeWidget.timeButton.cancelLongPress();\n        binding.timeWidget.timeAnswerText.cancelLongPress();\n    }\n\n    @Override\n    public void setData(Object answer) {\n        if (answer instanceof LocalDateTime) {\n            selectedDateTime = DateTimeUtils.getSelectedDate((LocalDateTime) answer, selectedDateTime);\n            binding.dateWidget.dateAnswerText.setText(DateTimeWidgetUtils.getDateTimeLabel(\n                    selectedDateTime.toDate(), datePickerDetails, false, getContext()));\n            widgetValueChanged();\n        }\n        if (answer instanceof DateTime) {\n            selectedDateTime = DateTimeUtils.getSelectedTime(((DateTime) answer).toLocalDateTime(), selectedDateTime);\n            binding.timeWidget.timeAnswerText.setText(new TimeData(selectedDateTime.toDate()).getDisplayText());\n            widgetValueChanged();\n        }\n    }\n\n    private void resetAnswerFields() {\n        selectedDateTime = DateTimeUtils.getCurrentDateTime();\n        binding.dateWidget.dateAnswerText.setText(org.odk.collect.strings.R.string.no_date_selected);\n        binding.timeWidget.timeAnswerText.setText(org.odk.collect.strings.R.string.no_time_selected);\n    }\n\n    private boolean isNullValue() {\n        return getFormEntryPrompt().isRequired()\n                ? isDateNull() || isTimeNull()\n                : isDateNull() && isTimeNull();\n    }\n\n    private boolean isDateNull() {\n        return binding.dateWidget.dateAnswerText.getText().equals(getContext().getString(org.odk.collect.strings.R.string.no_date_selected));\n    }\n\n    private boolean isTimeNull() {\n        return binding.timeWidget.timeAnswerText.getText().equals(getContext().getString(org.odk.collect.strings.R.string.no_time_selected));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/DateWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets.datetime;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.View;\n\nimport org.javarosa.core.model.data.DateData;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.joda.time.LocalDateTime;\nimport org.odk.collect.android.databinding.DateWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\n@SuppressLint(\"ViewConstructor\")\npublic class DateWidget extends QuestionWidget implements WidgetDataReceiver {\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    DateWidgetAnswerBinding binding;\n\n    private final DateTimeWidgetUtils widgetUtils;\n\n    private LocalDateTime selectedDate;\n    private DatePickerDetails datePickerDetails;\n\n    public DateWidget(Context context, QuestionDetails prompt, DateTimeWidgetUtils widgetUtils, WaitingForDataRegistry waitingForDataRegistry, Dependencies dependencies) {\n        super(context, dependencies, prompt);\n        render();\n\n        this.widgetUtils = widgetUtils;\n        this.waitingForDataRegistry = waitingForDataRegistry;\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = DateWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n        datePickerDetails = DateTimeWidgetUtils.getDatePickerDetails(prompt.getAppearanceHint());\n\n        if (prompt.isReadOnly()) {\n            binding.dateButton.setVisibility(GONE);\n        } else {\n            binding.dateButton.setOnClickListener(v -> {\n                waitingForDataRegistry.waitForData(prompt.getIndex());\n                widgetUtils.showDatePickerDialog(context, datePickerDetails, selectedDate);\n            });\n        }\n        binding.dateAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n\n        if (prompt.getAnswerValue() == null) {\n            selectedDate = DateTimeUtils.getCurrentDateTime();\n        } else {\n            selectedDate = DateTimeUtils.getSelectedDate(new LocalDateTime(getFormEntryPrompt().getAnswerValue().getValue()),\n                    LocalDateTime.now());\n            binding.dateAnswerText.setText(DateTimeWidgetUtils.getDateTimeLabel(selectedDate.toDate(),\n                    datePickerDetails, false, context));\n        }\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return binding.dateAnswerText.getText().equals(getContext().getString(org.odk.collect.strings.R.string.no_date_selected))\n                ? null\n                : new DateData(selectedDate.toDate());\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.dateButton.setOnLongClickListener(l);\n        binding.dateAnswerText.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.dateButton.cancelLongPress();\n        binding.dateAnswerText.cancelLongPress();\n    }\n\n    @Override\n    public void clearAnswer() {\n        selectedDate = DateTimeUtils.getCurrentDateTime();\n        binding.dateAnswerText.setText(org.odk.collect.strings.R.string.no_date_selected);\n        widgetValueChanged();\n    }\n\n    @Override\n    public void setData(Object answer) {\n        if (answer instanceof LocalDateTime) {\n            selectedDate = DateTimeUtils.getSelectedDate((LocalDateTime) answer, LocalDateTime.now());\n            binding.dateAnswerText.setText(DateTimeWidgetUtils.getDateTimeLabel(\n                    selectedDate.toDate(), datePickerDetails, false, getContext()));\n            widgetValueChanged();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/TimeWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets.datetime;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.View;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.TimeData;\nimport org.javarosa.form.api.FormEntryPrompt;\n\nimport org.joda.time.DateTime;\nimport org.joda.time.LocalDateTime;\nimport org.odk.collect.android.databinding.TimeWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\n@SuppressLint(\"ViewConstructor\")\npublic class TimeWidget extends QuestionWidget implements WidgetDataReceiver {\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    TimeWidgetAnswerBinding binding;\n\n    private final DateTimeWidgetUtils widgetUtils;\n\n    private LocalDateTime selectedTime;\n\n    public TimeWidget(Context context, final QuestionDetails prompt, DateTimeWidgetUtils widgetUtils, WaitingForDataRegistry waitingForDataRegistry, Dependencies dependencies) {\n        super(context, dependencies, prompt);\n        render();\n\n        this.widgetUtils = widgetUtils;\n        this.waitingForDataRegistry = waitingForDataRegistry;\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = TimeWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n\n        if (prompt.isReadOnly()) {\n            binding.timeButton.setVisibility(GONE);\n        } else {\n            binding.timeButton.setOnClickListener(v -> {\n                waitingForDataRegistry.waitForData(prompt.getIndex());\n                widgetUtils.showTimePickerDialog(context, selectedTime);\n            });\n        }\n        binding.timeAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n\n        if (prompt.getAnswerValue() == null) {\n            selectedTime = DateTimeUtils.getCurrentDateTime();\n        } else {\n            DateTime dateTime = new DateTime(getFormEntryPrompt().getAnswerValue().getValue());\n            selectedTime = DateTimeUtils.getSelectedTime(dateTime.toLocalDateTime(), LocalDateTime.now());\n            binding.timeAnswerText.setText(DateTimeUtils.getTimeData(dateTime).getDisplayText());\n        }\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public void clearAnswer() {\n        selectedTime = DateTimeUtils.getCurrentDateTime();\n        binding.timeAnswerText.setText(org.odk.collect.strings.R.string.no_time_selected);\n        widgetValueChanged();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return binding.timeAnswerText.getText().equals(getContext().getString(org.odk.collect.strings.R.string.no_time_selected))\n                ? null\n                : new TimeData(selectedTime.toDateTime().toDate());\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.timeButton.setOnLongClickListener(l);\n        binding.timeAnswerText.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.timeButton.cancelLongPress();\n        binding.timeAnswerText.cancelLongPress();\n    }\n\n    @Override\n    public void setData(Object answer) {\n        if (answer instanceof DateTime) {\n            selectedTime = DateTimeUtils.getSelectedTime(((DateTime) answer).toLocalDateTime(), LocalDateTime.now());\n            binding.timeAnswerText.setText(new TimeData(selectedTime.toDate()).getDisplayText());\n            widgetValueChanged();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/pickers/BikramSambatDatePickerDialog.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.datetime.pickers;\n\nimport org.joda.time.LocalDateTime;\n\nimport java.util.Arrays;\n\nimport bikramsambat.BikramSambatDate;\nimport bikramsambat.BsCalendar;\nimport bikramsambat.BsException;\nimport bikramsambat.BsGregorianDate;\nimport timber.log.Timber;\n\npublic class BikramSambatDatePickerDialog extends CustomDatePickerDialog {\n    private static final int MIN_SUPPORTED_YEAR = 1970; //1913 in Gregorian calendar\n    private static final int MAX_SUPPORTED_YEAR = 2090; //2033 in Gregorian calendar\n\n    private final String[] monthsArray = BsCalendar.MONTH_NAMES.toArray(new String[0]);\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        setUpValues();\n    }\n\n    @Override\n    protected void updateDays() {\n        BikramSambatDate bikramSambatDate = new BikramSambatDate(getYear(), Arrays.asList(monthsArray).indexOf(getMonth()) + 1, getDay());\n        int daysInMonth = 0;\n        try {\n            daysInMonth = BsCalendar.getInstance().daysInMonth(bikramSambatDate.year, bikramSambatDate.month);\n        } catch (BsException e) {\n            Timber.e(e);\n        }\n        setUpDayPicker(bikramSambatDate.day, daysInMonth);\n    }\n\n    @Override\n    protected LocalDateTime getOriginalDate() {\n        BsGregorianDate bsGregorianDate = null;\n        try {\n            bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(getYear(), Arrays.asList(monthsArray).indexOf(getMonth()) + 1, getDay()));\n        } catch (BsException e) {\n            Timber.e(e);\n        }\n\n        return new LocalDateTime()\n                .withYear(bsGregorianDate.year)\n                .withMonthOfYear(bsGregorianDate.month)\n                .withDayOfMonth(bsGregorianDate.day)\n                .withHourOfDay(0)\n                .withMinuteOfHour(0)\n                .withSecondOfMinute(0)\n                .withMillisOfSecond(0);\n    }\n\n    private void setUpDatePicker() {\n        LocalDateTime localDateTime = getDate();\n        try {\n            BikramSambatDate bikramSambatDate = BsCalendar.getInstance().toBik(localDateTime.getYear(), localDateTime.getMonthOfYear(), localDateTime.getDayOfMonth());\n            setUpDayPicker(bikramSambatDate.day, BsCalendar.getInstance().daysInMonth(bikramSambatDate.year, bikramSambatDate.month));\n            setUpMonthPicker(bikramSambatDate.month, monthsArray);\n            setUpYearPicker(bikramSambatDate.year, MIN_SUPPORTED_YEAR, MAX_SUPPORTED_YEAR);\n        } catch (BsException e) {\n            Timber.e(e);\n        }\n    }\n\n    private void setUpValues() {\n        setUpDatePicker();\n        updateGregorianDateLabel();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/pickers/BuddhistDatePickerDialog.kt",
    "content": "package org.odk.collect.android.widgets.datetime.pickers\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport org.joda.time.LocalDateTime\nimport org.joda.time.chrono.BuddhistChronology\nimport org.odk.collect.android.R\nimport org.odk.collect.android.widgets.datetime.DateTimeUtils\n\nclass BuddhistDatePickerDialog : CustomDatePickerDialog() {\n    private lateinit var monthsArray: Array<String>\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View? {\n        monthsArray = resources.getStringArray(R.array.buddhist_months)\n        return super.onCreateView(inflater, container, savedInstanceState)\n    }\n\n    override fun onResume() {\n        super.onResume()\n        setUpValues()\n    }\n\n    override fun updateDays() {\n        val localDateTime = getCurrentBuddhistDate()\n        setUpDayPicker(localDateTime.dayOfMonth, localDateTime.dayOfMonth().maximumValue)\n    }\n\n    override fun getOriginalDate(): LocalDateTime {\n        return getCurrentBuddhistDate()\n    }\n\n    private fun setUpDatePicker() {\n        val buddhistDate = DateTimeUtils\n            .skipDaylightSavingGapIfExists(date)\n            .toDateTime()\n            .withChronology(BuddhistChronology.getInstance())\n            .toLocalDateTime()\n\n        setUpDayPicker(buddhistDate.dayOfMonth, buddhistDate.dayOfMonth().maximumValue)\n        setUpMonthPicker(buddhistDate.monthOfYear, monthsArray)\n        setUpYearPicker(buddhistDate.year, MIN_SUPPORTED_YEAR, MAX_SUPPORTED_YEAR)\n    }\n\n    private fun setUpValues() {\n        setUpDatePicker()\n        updateGregorianDateLabel()\n    }\n\n    private fun getCurrentBuddhistDate(): LocalDateTime {\n        var buddhistDay = day\n        val buddhistMonth = monthsArray.indexOf(month)\n        val buddhistYear = year\n\n        val buddhistDate = LocalDateTime(\n            buddhistYear,\n            buddhistMonth + 1,\n            1,\n            0,\n            0,\n            0,\n            0,\n            BuddhistChronology.getInstance()\n        )\n        if (buddhistDay > buddhistDate.dayOfMonth().maximumValue) {\n            buddhistDay = buddhistDate.dayOfMonth().maximumValue\n        }\n\n        return LocalDateTime(\n            buddhistYear,\n            buddhistMonth + 1,\n            buddhistDay,\n            0,\n            0,\n            0,\n            0,\n            BuddhistChronology.getInstance()\n        )\n    }\n\n    companion object {\n        const val MIN_SUPPORTED_YEAR = 2443 // 1900 in Gregorian calendar\n        const val MAX_SUPPORTED_YEAR = 2643 // 2100 in Gregorian calendar\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/pickers/CopticDatePickerDialog.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.datetime.pickers;\n\nimport org.joda.time.LocalDateTime;\nimport org.joda.time.chrono.CopticChronology;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.widgets.datetime.DateTimeUtils;\n\nimport java.util.Arrays;\n\npublic class CopticDatePickerDialog extends CustomDatePickerDialog {\n    private static final int MIN_SUPPORTED_YEAR = 1617; //1900 in Gregorian calendar\n    private static final int MAX_SUPPORTED_YEAR = 1817; //2100 in Gregorian calendar\n\n    private String[] monthsArray;\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        monthsArray = getResources().getStringArray(R.array.coptic_months);\n        setUpValues();\n    }\n\n    @Override\n    protected void updateDays() {\n        LocalDateTime localDateTime = getCurrentCopticDate();\n        setUpDayPicker(localDateTime.getDayOfMonth(), localDateTime.dayOfMonth().getMaximumValue());\n    }\n\n    @Override\n    protected LocalDateTime getOriginalDate() {\n        return getCurrentCopticDate();\n    }\n\n    private void setUpDatePicker() {\n        LocalDateTime copticDate = DateTimeUtils\n                .skipDaylightSavingGapIfExists(getDate())\n                .toDateTime()\n                .withChronology(CopticChronology.getInstance())\n                .toLocalDateTime();\n        setUpDayPicker(copticDate.getDayOfMonth(), copticDate.dayOfMonth().getMaximumValue());\n        setUpMonthPicker(copticDate.getMonthOfYear(), monthsArray);\n        setUpYearPicker(copticDate.getYear(), MIN_SUPPORTED_YEAR, MAX_SUPPORTED_YEAR);\n    }\n\n    private void setUpValues() {\n        setUpDatePicker();\n        updateGregorianDateLabel();\n    }\n\n    private LocalDateTime getCurrentCopticDate() {\n        int copticDay = getDay();\n        int copticMonth = Arrays.asList(monthsArray).indexOf(getMonth());\n        int copticYear = getYear();\n\n        LocalDateTime copticDate = new LocalDateTime(copticYear, copticMonth + 1, 1, 0, 0, 0, 0, CopticChronology.getInstance());\n        if (copticDay > copticDate.dayOfMonth().getMaximumValue()) {\n            copticDay = copticDate.dayOfMonth().getMaximumValue();\n        }\n\n        if (copticDay < copticDate.dayOfMonth().getMinimumValue()) {\n            copticDay = copticDate.dayOfMonth().getMinimumValue();\n        }\n\n        return new LocalDateTime(copticYear, copticMonth + 1, copticDay, 0, 0, 0, 0, CopticChronology.getInstance());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/pickers/CustomDatePickerDialog.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.datetime.pickers;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.NumberPicker;\nimport android.widget.TextView;\n\nimport androidx.fragment.app.DialogFragment;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.joda.time.LocalDateTime;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.widgets.datetime.DateTimeUtils;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.android.widgets.viewmodels.DateTimeViewModel;\n\n/**\n * @author Grzegorz Orczykowski (gorczykowski@soldevelo.com)\n */\npublic abstract class CustomDatePickerDialog extends DialogFragment {\n    private NumberPicker dayPicker;\n    private NumberPicker monthPicker;\n    private NumberPicker yearPicker;\n\n    private TextView gregorianDateText;\n\n    private DateTimeViewModel viewModel;\n    private DateChangeListener dateChangeListener;\n\n    public interface DateChangeListener {\n        void onDateChanged(LocalDateTime selectedDate);\n    }\n\n    @Override\n    public void onAttach(Context context) {\n        super.onAttach(context);\n\n        if (context instanceof DateChangeListener) {\n            dateChangeListener = (DateChangeListener) context;\n        }\n\n        viewModel = new ViewModelProvider(this).get(DateTimeViewModel.class);\n        if (viewModel.getLocalDateTime() == null) {\n            viewModel.setLocalDateTime((LocalDateTime) getArguments().getSerializable(DateTimeWidgetUtils.DATE));\n        }\n        viewModel.setDatePickerDetails((DatePickerDetails) getArguments().getSerializable(DateTimeWidgetUtils.DATE_PICKER_DETAILS));\n\n        viewModel.getSelectedDate().observe(this, localDateTime -> {\n            if (localDateTime != null && dateChangeListener != null) {\n                dateChangeListener.onDateChanged(localDateTime);\n            }\n        });\n    }\n\n    @Override\n    public Dialog onCreateDialog(Bundle savedInstanceState) {\n        return new MaterialAlertDialogBuilder(getActivity())\n                .setTitle(org.odk.collect.strings.R.string.select_date)\n                .setView(R.layout.custom_date_picker_dialog)\n                .setPositiveButton(org.odk.collect.strings.R.string.ok, (dialog, id) -> {\n                    LocalDateTime date = DateTimeUtils.getDateAsGregorian(getOriginalDate());\n                    viewModel.setSelectedDate(date.getYear(), date.getMonthOfYear() - 1, date.getDayOfMonth());\n                    dismiss();\n                })\n                .setNegativeButton(org.odk.collect.strings.R.string.cancel, (dialog, id) -> dismiss())\n                .create();\n    }\n\n    @Override\n    public void onDestroyView() {\n        viewModel.setLocalDateTime(DateTimeUtils.getDateAsGregorian(getOriginalDate()));\n        super.onDestroyView();\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        gregorianDateText = getDialog().findViewById(R.id.date_gregorian);\n        setUpPickers();\n    }\n\n    private void setUpPickers() {\n        dayPicker = getDialog().findViewById(R.id.day_picker);\n        dayPicker.setOnValueChangedListener((picker, oldVal, newVal) -> updateGregorianDateLabel());\n        monthPicker = getDialog().findViewById(R.id.month_picker);\n        monthPicker.setOnValueChangedListener((picker, oldVal, newVal) -> monthUpdated());\n        yearPicker = getDialog().findViewById(R.id.year_picker);\n        yearPicker.setOnValueChangedListener((picker, oldVal, newVal) -> yearUpdated());\n\n        hidePickersIfNeeded();\n    }\n\n    private void hidePickersIfNeeded() {\n        if (viewModel.getDatePickerDetails().isMonthYearMode()) {\n            dayPicker.setVisibility(View.GONE);\n        } else if (viewModel.getDatePickerDetails().isYearMode()) {\n            dayPicker.setVisibility(View.GONE);\n            monthPicker.setVisibility(View.GONE);\n        }\n    }\n\n    protected void updateGregorianDateLabel() {\n        String label = DateTimeWidgetUtils.getDateTimeLabel(DateTimeUtils.getDateAsGregorian(getOriginalDate()).toDate(),\n                viewModel.getDatePickerDetails(), false, getContext());\n        gregorianDateText.setText(label);\n    }\n\n    protected void setUpDayPicker(int dayOfMonth, int daysInMonth) {\n        setUpDayPicker(1, dayOfMonth, daysInMonth);\n    }\n\n    protected void setUpDayPicker(int minDay, int dayOfMonth, int daysInMonth) {\n        dayPicker.setMinValue(minDay);\n        dayPicker.setMaxValue(daysInMonth);\n        if (viewModel.getDatePickerDetails().isSpinnerMode()) {\n            dayPicker.setValue(dayOfMonth);\n        }\n    }\n\n    protected void setUpMonthPicker(int monthOfYear, String[] monthsArray) {\n        // In Myanmar calendar we don't have specified amount of months, it's dynamic so clear\n        // values first to avoid ArrayIndexOutOfBoundsException\n        monthPicker.setDisplayedValues(null);\n        monthPicker.setMaxValue(monthsArray.length - 1);\n        monthPicker.setDisplayedValues(monthsArray);\n        if (!viewModel.getDatePickerDetails().isYearMode()) {\n            monthPicker.setValue(monthOfYear - 1);\n        }\n    }\n\n    protected void setUpYearPicker(int year, int minSupportedYear, int maxSupportedYear) {\n        yearPicker.setMinValue(minSupportedYear);\n        yearPicker.setMaxValue(maxSupportedYear);\n        yearPicker.setValue(year);\n    }\n\n    protected void monthUpdated() {\n        updateDays();\n        updateGregorianDateLabel();\n    }\n\n    protected void yearUpdated() {\n        updateDays();\n        updateGregorianDateLabel();\n    }\n\n    public int getDay() {\n        return dayPicker.getValue();\n    }\n\n    public String getMonth() {\n        return monthPicker.getDisplayedValues()[monthPicker.getValue()];\n    }\n\n    public int getMonthId() {\n        return monthPicker.getValue();\n    }\n\n    public int getYear() {\n        return yearPicker.getValue();\n    }\n\n    public LocalDateTime getDate() {\n        return getDay() == 0 ? viewModel.getLocalDateTime() : getOriginalDate();\n    }\n\n    protected abstract void updateDays();\n\n    protected abstract LocalDateTime getOriginalDate();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/pickers/CustomTimePickerDialog.java",
    "content": "package org.odk.collect.android.widgets.datetime.pickers;\n\nimport android.app.Dialog;\nimport android.app.TimePickerDialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.os.Bundle;\nimport android.text.format.DateFormat;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.joda.time.DateTime;\nimport org.joda.time.LocalDateTime;\nimport org.odk.collect.android.utilities.ThemeUtils;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.android.widgets.viewmodels.DateTimeViewModel;\n\npublic class CustomTimePickerDialog extends DialogFragment {\n    private DateTimeViewModel viewModel;\n    private TimeChangeListener timeChangeListener;\n\n    public interface TimeChangeListener {\n        void onTimeChanged(DateTime selectedTime);\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n\n        if (context instanceof TimeChangeListener) {\n            timeChangeListener = (TimeChangeListener) context;\n        }\n\n        viewModel = new ViewModelProvider(this).get(DateTimeViewModel.class);\n        viewModel.setLocalDateTime((LocalDateTime) getArguments().getSerializable(DateTimeWidgetUtils.TIME));\n        viewModel.setDialogTheme(getArguments().getInt(DateTimeWidgetUtils.DIALOG_THEME));\n\n        viewModel.getSelectedTime().observe(this, dateTime -> {\n            timeChangeListener.onTimeChanged(dateTime);\n        });\n    }\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        TimePickerDialog dialog = new TimePickerDialog(requireContext(), viewModel.getDialogTheme(), viewModel.getTimeSetListener(),\n                viewModel.getLocalDateTime().getHourOfDay(), viewModel.getLocalDateTime().getMinuteOfHour(), DateFormat.is24HourFormat(requireContext()));\n\n        dialog.setTitle(requireContext().getString(org.odk.collect.strings.R.string.select_time));\n        return dialog;\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n\n        // Needed because the TimePickerDialog doesn't pick up theme colors properly for some reason\n        TimePickerDialog dialog = (TimePickerDialog) getDialog();\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(new ThemeUtils(getContext()).getColorPrimary());\n        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setTextColor(new ThemeUtils(getContext()).getColorPrimary());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/pickers/EthiopianDatePickerDialog.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.datetime.pickers;\n\nimport org.joda.time.LocalDateTime;\nimport org.joda.time.chrono.EthiopicChronology;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.widgets.datetime.DateTimeUtils;\n\nimport java.util.Arrays;\n\n/**\n * @author Grzegorz Orczykowski (gorczykowski@soldevelo.com)\n * @author Aurelio Di Pasquale (aurelio.dipasquale@unibas.ch)\n */\npublic class EthiopianDatePickerDialog extends CustomDatePickerDialog {\n    private static final int MIN_SUPPORTED_YEAR = 1893; //1900 in Gregorian calendar\n    private static final int MAX_SUPPORTED_YEAR = 2093; //2100 in Gregorian calendar\n\n    private String[] monthsArray;\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        monthsArray = getResources().getStringArray(R.array.ethiopian_months);\n        setUpValues();\n    }\n\n    @Override\n    protected void updateDays() {\n        LocalDateTime localDateTime = getCurrentEthiopianDate();\n        setUpDayPicker(localDateTime.getDayOfMonth(), localDateTime.dayOfMonth().getMaximumValue());\n    }\n\n    @Override\n    protected LocalDateTime getOriginalDate() {\n        return getCurrentEthiopianDate();\n    }\n\n    private void setUpDatePicker() {\n        LocalDateTime ethiopianDate = DateTimeUtils\n                .skipDaylightSavingGapIfExists(getDate())\n                .toDateTime()\n                .withChronology(EthiopicChronology.getInstance())\n                .toLocalDateTime();\n        setUpDayPicker(ethiopianDate.getDayOfMonth(), ethiopianDate.dayOfMonth().getMaximumValue());\n        setUpMonthPicker(ethiopianDate.getMonthOfYear(), monthsArray);\n        setUpYearPicker(ethiopianDate.getYear(), MIN_SUPPORTED_YEAR, MAX_SUPPORTED_YEAR);\n    }\n\n    private void setUpValues() {\n        setUpDatePicker();\n        updateGregorianDateLabel();\n    }\n\n    private LocalDateTime getCurrentEthiopianDate() {\n        int ethiopianDay = getDay();\n        int ethiopianMonth = Arrays.asList(monthsArray).indexOf(getMonth());\n        int ethiopianYear = getYear();\n\n        LocalDateTime ethiopianDate = new LocalDateTime(ethiopianYear, ethiopianMonth + 1, 1, 0, 0, 0, 0, EthiopicChronology.getInstance());\n        if (ethiopianDay > ethiopianDate.dayOfMonth().getMaximumValue()) {\n            ethiopianDay = ethiopianDate.dayOfMonth().getMaximumValue();\n        }\n        if (ethiopianDay < ethiopianDate.dayOfMonth().getMinimumValue()) {\n            ethiopianDay = ethiopianDate.dayOfMonth().getMinimumValue();\n        }\n\n        return new LocalDateTime(ethiopianYear, ethiopianMonth + 1, ethiopianDay, 0, 0, 0, 0, EthiopicChronology.getInstance());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/pickers/FixedDatePickerDialog.java",
    "content": "package org.odk.collect.android.widgets.datetime.pickers;\n\nimport android.app.DatePickerDialog;\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.content.res.Resources;\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.joda.time.LocalDateTime;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.utilities.ThemeUtils;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.android.widgets.viewmodels.DateTimeViewModel;\n\npublic class FixedDatePickerDialog extends DialogFragment {\n    private ThemeUtils themeUtils;\n    private DateTimeViewModel viewModel;\n    private CustomDatePickerDialog.DateChangeListener dateChangeListener;\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        themeUtils = new ThemeUtils(context);\n\n        if (context instanceof CustomDatePickerDialog.DateChangeListener) {\n            dateChangeListener = (CustomDatePickerDialog.DateChangeListener) context;\n        }\n\n        viewModel = new ViewModelProvider(this).get(DateTimeViewModel.class);\n        viewModel.setDialogTheme(getArguments().getInt(DateTimeWidgetUtils.DIALOG_THEME));\n        viewModel.setLocalDateTime((LocalDateTime) getArguments().getSerializable(DateTimeWidgetUtils.DATE));\n        viewModel.setDatePickerDetails((DatePickerDetails) getArguments().getSerializable(DateTimeWidgetUtils.DATE_PICKER_DETAILS));\n\n        viewModel.getSelectedDate().observe(this, localDateTime -> {\n            if (localDateTime != null) {\n                dateChangeListener.onDateChanged(localDateTime);\n            }\n        });\n    }\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        DatePickerDialog dialog = new DatePickerDialog(requireActivity(), viewModel.getDialogTheme(), viewModel.getDateSetListener(),\n                viewModel.getLocalDateTime().getYear(), viewModel.getLocalDateTime().getMonthOfYear() - 1, viewModel.getLocalDateTime().getDayOfMonth());\n\n        if (themeUtils.isSpinnerDatePickerDialogTheme(viewModel.getDialogTheme())) {\n            dialog.setTitle(requireContext().getString(org.odk.collect.strings.R.string.select_date));\n            hidePickersIfNeeded(dialog, viewModel.getLocalDateTime());\n\n            //noinspection deprecation\n            dialog.getDatePicker().setCalendarViewShown(false);\n        }\n\n        return dialog;\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n\n        // Needed because the DatePickerDialog doesn't pick up theme colors properly for some reason\n        DatePickerDialog dialog = (DatePickerDialog) getDialog();\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(new ThemeUtils(getContext()).getColorPrimary());\n        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setTextColor(new ThemeUtils(getContext()).getColorPrimary());\n    }\n\n    private void hidePickersIfNeeded(DatePickerDialog dialog, LocalDateTime date) {\n        if (viewModel.getDatePickerDetails().isYearMode()) {\n            dialog.getDatePicker().findViewById(Resources.getSystem().getIdentifier(\"day\", \"id\", \"android\"))\n                    .setVisibility(View.GONE);\n\n            dialog.getDatePicker().findViewById(Resources.getSystem().getIdentifier(\"month\", \"id\", \"android\"))\n                    .setVisibility(View.GONE);\n            dialog.getDatePicker().updateDate(date.getYear(), 0, 1);\n        } else if (viewModel.getDatePickerDetails().isMonthYearMode()) {\n            dialog.getDatePicker().findViewById(Resources.getSystem().getIdentifier(\"day\", \"id\", \"android\"))\n                    .setVisibility(View.GONE);\n            dialog.getDatePicker().updateDate(date.getYear(), date.getMonthOfYear() - 1, 1);\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/pickers/IslamicDatePickerDialog.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.datetime.pickers;\n\nimport org.joda.time.LocalDateTime;\nimport org.joda.time.chrono.IslamicChronology;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.widgets.datetime.DateTimeUtils;\n\nimport java.util.Arrays;\n\npublic class IslamicDatePickerDialog extends CustomDatePickerDialog {\n\n    private static final int MIN_SUPPORTED_YEAR = 1318; //1900 in Gregorian calendar\n    private static final int MAX_SUPPORTED_YEAR = 1524; //2100 in Gregorian calendar\n\n    private String[] monthsArray;\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        monthsArray = getResources().getStringArray(R.array.islamic_months);\n        setUpValues();\n    }\n\n    @Override\n    protected void updateDays() {\n        LocalDateTime localDateTime = getCurrentIslamicDate();\n        setUpDayPicker(localDateTime.getDayOfMonth(), localDateTime.dayOfMonth().getMaximumValue());\n    }\n\n    @Override\n    protected LocalDateTime getOriginalDate() {\n        return getCurrentIslamicDate();\n    }\n\n    private void setUpDatePicker() {\n        LocalDateTime islamicDate = DateTimeUtils\n                .skipDaylightSavingGapIfExists(getDate())\n                .toDateTime()\n                .withChronology(IslamicChronology.getInstance())\n                .toLocalDateTime();\n        setUpDayPicker(islamicDate.getDayOfMonth(), islamicDate.dayOfMonth().getMaximumValue());\n        setUpMonthPicker(islamicDate.getMonthOfYear(), monthsArray);\n        setUpYearPicker(islamicDate.getYear(), MIN_SUPPORTED_YEAR, MAX_SUPPORTED_YEAR);\n    }\n\n    private void setUpValues() {\n        setUpDatePicker();\n        updateGregorianDateLabel();\n    }\n\n    private LocalDateTime getCurrentIslamicDate() {\n        int islamicDay = getDay();\n        int islamicMonth = Arrays.asList(monthsArray).indexOf(getMonth());\n        int islamicYear = getYear();\n\n        LocalDateTime islamicDate = new LocalDateTime(islamicYear, islamicMonth + 1, 1, 0, 0, 0, 0, IslamicChronology.getInstance());\n        if (islamicDay > islamicDate.dayOfMonth().getMaximumValue()) {\n            islamicDay = islamicDate.dayOfMonth().getMaximumValue();\n        }\n        if (islamicDay < islamicDate.dayOfMonth().getMinimumValue()) {\n            islamicDay = islamicDate.dayOfMonth().getMinimumValue();\n        }\n\n        return new LocalDateTime(islamicYear, islamicMonth + 1, islamicDay, 0, 0, 0, 0, IslamicChronology.getInstance());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/pickers/MyanmarDatePickerDialog.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.datetime.pickers;\n\nimport org.joda.time.LocalDateTime;\nimport org.odk.collect.android.widgets.datetime.DateTimeUtils;\nimport org.odk.collect.android.utilities.MyanmarDateUtils;\n\nimport java.util.List;\n\nimport mmcalendar.MyanmarCalendarKernel;\nimport mmcalendar.MyanmarDate;\n\npublic class MyanmarDatePickerDialog extends CustomDatePickerDialog {\n    private static final int MIN_SUPPORTED_YEAR = 1261; //1900 in Gregorian calendar\n    private static final int MAX_SUPPORTED_YEAR = 1462; //2100 in Gregorian calendar\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        setUpDatePicker();\n        updateGregorianDateLabel();\n    }\n\n    @Override\n    protected void updateDays() {\n        MyanmarDate myanmarDate = getCurrentMyanmarDate();\n        setUpDayPicker(MyanmarDateUtils.getFirstMonthDay(myanmarDate), myanmarDate.getDayOfMonth(), MyanmarDateUtils.getMonthLength(myanmarDate));\n    }\n\n    @Override\n    protected void yearUpdated() {\n        MyanmarDate myanmarDate = getCurrentMyanmarDate();\n        setUpMonthPicker(MyanmarDateUtils.getMonthId(myanmarDate) + 1, MyanmarDateUtils.getMyanmarMonthsArray(myanmarDate.getYearValue()));\n        super.yearUpdated();\n    }\n\n    @Override\n    protected LocalDateTime getOriginalDate() {\n        return MyanmarDateUtils.myanmarDateToGregorianDate(getCurrentMyanmarDate());\n    }\n\n    private void setUpDatePicker() {\n        MyanmarDate myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(DateTimeUtils.getDateWithSkippedDaylightSavingGapIfExists(getDate()));\n\n        setUpDayPicker(MyanmarDateUtils.getFirstMonthDay(myanmarDate), myanmarDate.getDayOfMonth(), MyanmarDateUtils.getMonthLength(myanmarDate));\n        setUpMonthPicker(MyanmarDateUtils.getMonthId(myanmarDate) + 1, MyanmarDateUtils.getMyanmarMonthsArray(myanmarDate.getYearValue()));\n        setUpYearPicker(myanmarDate.getYearValue(), MIN_SUPPORTED_YEAR, MAX_SUPPORTED_YEAR);\n    }\n\n    private MyanmarDate getCurrentMyanmarDate() {\n        List<Integer> monthIndexes = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(getYear(), 1).getMonthList();\n        int monthIndex = getMonthId() < monthIndexes.size() ? monthIndexes.get(getMonthId()) : monthIndexes.get(monthIndexes.size() - 1);\n        int monthLength = MyanmarDateUtils.getMonthLength(getYear(), monthIndex);\n        int dayOfMonth = getDay() > monthLength ? monthLength : getDay();\n\n        return MyanmarDateUtils.createMyanmarDate(getYear(), monthIndex, dayOfMonth);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/datetime/pickers/PersianDatePickerDialog.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.datetime.pickers;\n\nimport org.joda.time.LocalDateTime;\nimport org.joda.time.chrono.PersianChronologyKhayyamBorkowski;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.widgets.datetime.DateTimeUtils;\n\nimport java.util.Arrays;\n\npublic class PersianDatePickerDialog extends CustomDatePickerDialog {\n    private static final int MIN_SUPPORTED_YEAR = 1278; //1900 in Gregorian calendar\n    private static final int MAX_SUPPORTED_YEAR = 1478; //2100 in Gregorian calendar\n\n    private String[] monthsArray;\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        monthsArray = getResources().getStringArray(R.array.persian_months);\n        setUpValues();\n    }\n\n    @Override\n    protected void updateDays() {\n        LocalDateTime localDateTime = getCurrentPersianDate();\n        setUpDayPicker(localDateTime.getDayOfMonth(), localDateTime.dayOfMonth().getMaximumValue());\n    }\n\n    @Override\n    protected LocalDateTime getOriginalDate() {\n        return getCurrentPersianDate();\n    }\n\n    private void setUpDatePicker() {\n        LocalDateTime persianDate = DateTimeUtils\n                .skipDaylightSavingGapIfExists(getDate())\n                .toDateTime()\n                .withChronology(PersianChronologyKhayyamBorkowski.getInstance())\n                .toLocalDateTime();\n        setUpDayPicker(persianDate.getDayOfMonth(), persianDate.dayOfMonth().getMaximumValue());\n        setUpMonthPicker(persianDate.getMonthOfYear(), monthsArray);\n        setUpYearPicker(persianDate.getYear(), MIN_SUPPORTED_YEAR, MAX_SUPPORTED_YEAR);\n    }\n\n    private void setUpValues() {\n        setUpDatePicker();\n        updateGregorianDateLabel();\n    }\n\n    private LocalDateTime getCurrentPersianDate() {\n        int persianDay = getDay();\n        int persianMonth = Arrays.asList(monthsArray).indexOf(getMonth());\n        int persianYear = getYear();\n\n        LocalDateTime persianDate = new LocalDateTime(persianYear, persianMonth + 1, 1, 0, 0, 0, 0, PersianChronologyKhayyamBorkowski.getInstance());\n        if (persianDay > persianDate.dayOfMonth().getMaximumValue()) {\n            persianDay = persianDate.dayOfMonth().getMaximumValue();\n        }\n        if (persianDay < persianDate.dayOfMonth().getMinimumValue()) {\n            persianDay = persianDate.dayOfMonth().getMinimumValue();\n        }\n\n        return new LocalDateTime(persianYear, persianMonth + 1, persianDay, 0, 0, 0, 0, PersianChronologyKhayyamBorkowski.getInstance());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/FileWidget.java",
    "content": "package org.odk.collect.android.widgets.interfaces;\n\n/**\n * @author James Knight\n */\npublic interface FileWidget extends Widget {\n    void deleteFile();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/GeoDataRequester.kt",
    "content": "package org.odk.collect.android.widgets.interfaces\n\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry\n\ninterface GeoDataRequester {\n    fun requestGeoPoint(\n        prompt: FormEntryPrompt,\n        answerText: String?,\n        waitingForDataRegistry: WaitingForDataRegistry\n    )\n\n    fun requestGeoPoly(prompt: FormEntryPrompt)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/MultiChoiceWidget.java",
    "content": "package org.odk.collect.android.widgets.interfaces;\n\n/**\n * @author James Knight\n */\npublic interface MultiChoiceWidget extends Widget {\n    int getChoiceCount();\n\n    void setChoiceSelected(int choiceIndex, boolean isSelected);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/Printer.kt",
    "content": "package org.odk.collect.android.widgets.interfaces\n\nimport android.content.Context\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\n\ninterface Printer {\n    fun parseAndPrint(\n        htmlDocument: String,\n        questionMediaManager: QuestionMediaManager,\n        context: Context\n    )\n\n    fun isLoading(): NonNullLiveData<Boolean>\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/SelectChoiceLoader.kt",
    "content": "package org.odk.collect.android.widgets.interfaces\n\nimport org.javarosa.core.model.SelectChoice\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.javarosa.xpath.parser.XPathSyntaxException\nimport org.odk.collect.android.exception.ExternalDataException\nimport java.io.FileNotFoundException\n\ninterface SelectChoiceLoader {\n\n    @Throws(FileNotFoundException::class, XPathSyntaxException::class, ExternalDataException::class)\n    fun loadSelectChoices(prompt: FormEntryPrompt): List<SelectChoice>\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/Widget.java",
    "content": "package org.odk.collect.android.widgets.interfaces;\n\nimport org.javarosa.core.model.data.IAnswerData;\n\nimport javax.annotation.Nullable;\n\n/**\n * @author James Knight\n */\npublic interface Widget {\n\n    @Nullable\n    IAnswerData getAnswer();\n\n    void clearAnswer();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/interfaces/WidgetDataReceiver.kt",
    "content": "package org.odk.collect.android.widgets.interfaces\n\ninterface WidgetDataReceiver {\n    /**\n     * Allows the answer to be set externally in [org.odk.collect.android.activities.FormFillingActivity].\n     */\n    fun setData(answer: Any)\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/BaseSelectListWidget.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.util.TypedValue;\nimport android.view.View;\n\nimport androidx.activity.ComponentActivity;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.adapters.AbstractSelectListAdapter;\nimport org.odk.collect.android.databinding.SelectListWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.SelectItemClickListener;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.MultiChoiceWidget;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.android.widgets.utilities.SearchQueryViewModel;\n\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getPlayableAudioURI;\n\nimport java.util.List;\n\npublic abstract class BaseSelectListWidget extends QuestionWidget implements MultiChoiceWidget, SelectItemClickListener {\n\n    SelectListWidgetAnswerBinding binding;\n    protected AbstractSelectListAdapter recyclerViewAdapter;\n\n    final List<SelectChoice> items;\n\n    public BaseSelectListWidget(Context context, QuestionDetails questionDetails, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        items = ItemsWidgetUtils.loadItemsAndHandleErrors(this, questionDetails.getPrompt(), selectChoiceLoader);\n\n        logAnalytics(questionDetails);\n        binding.choicesRecyclerView.initRecyclerView(setUpAdapter(), Appearances.isFlexAppearance(getQuestionDetails().getPrompt()));\n        if (Appearances.isAutocomplete(getQuestionDetails().getPrompt())) {\n            setUpSearchBox();\n        }\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = SelectListWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n        return binding.getRoot();\n    }\n\n    @Override\n    public void setFocus(Context context) {\n        if (Appearances.isAutocomplete(getQuestionDetails().getPrompt()) && !questionDetails.isReadOnly()) {\n            softKeyboardController.showSoftKeyboard(binding.choicesSearchBox);\n        }\n    }\n\n    @Override\n    public void clearAnswer() {\n        recyclerViewAdapter.clearAnswer();\n        widgetValueChanged();\n    }\n\n    @Override\n    public int getChoiceCount() {\n        return recyclerViewAdapter.getItemCount();\n    }\n\n    private void setUpSearchBox() {\n        ComponentActivity activity = (ComponentActivity) getContext();\n        SearchQueryViewModel searchQueryViewModel = new ViewModelProvider(activity).get(SearchQueryViewModel.class);\n\n        binding.choicesSearchBox.setVisibility(View.VISIBLE);\n        binding.choicesSearchBox.setTextSize(TypedValue.COMPLEX_UNIT_DIP, QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.HEADLINE_6));\n        binding.choicesSearchBox.addTextChangedListener(new TextWatcher() {\n            private String oldText = \"\";\n\n            @Override\n            public void afterTextChanged(Editable s) {\n                if (!s.toString().equals(oldText)) {\n                    recyclerViewAdapter.getFilter().filter(s.toString());\n                    searchQueryViewModel.setQuery(getFormEntryPrompt().getIndex().toString(), s.toString());\n                }\n            }\n\n            @Override\n            public void beforeTextChanged(CharSequence s, int start, int count, int after) {\n                oldText = s.toString();\n            }\n\n            @Override\n            public void onTextChanged(CharSequence s, int start, int before, int count) {\n            }\n        });\n\n        binding.choicesSearchBox.setText(searchQueryViewModel.getQuery(getFormEntryPrompt().getIndex().toString()));\n    }\n\n    private void logAnalytics(QuestionDetails questionDetails) {\n        if (items != null) {\n            for (SelectChoice choice : items) {\n                String audioURI = getPlayableAudioURI(questionDetails.getPrompt(), choice, getReferenceManager());\n\n                if (audioURI != null) {\n                    break;\n                }\n            }\n        }\n    }\n\n    protected abstract AbstractSelectListAdapter setUpAdapter();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/ItemsWidgetUtils.kt",
    "content": "package org.odk.collect.android.widgets.items\n\nimport org.javarosa.core.model.SelectChoice\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.javarosa.xpath.parser.XPathSyntaxException\nimport org.odk.collect.android.R\nimport org.odk.collect.android.exception.ExternalDataException\nimport org.odk.collect.android.widgets.QuestionWidget\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader\nimport java.io.FileNotFoundException\n\nobject ItemsWidgetUtils {\n\n    @JvmStatic\n    fun loadItemsAndHandleErrors(\n        widget: QuestionWidget,\n        prompt: FormEntryPrompt,\n        selectChoiceLoader: SelectChoiceLoader\n    ): List<SelectChoice> {\n        return try {\n            selectChoiceLoader.loadSelectChoices(prompt)\n        } catch (e: FileNotFoundException) {\n            widget.showWarning(widget.context.getString(org.odk.collect.strings.R.string.file_missing, e.message))\n            emptyList()\n        } catch (e: XPathSyntaxException) {\n            widget.showWarning(widget.context.getString(org.odk.collect.strings.R.string.parser_exception, e.message))\n            emptyList()\n        } catch (e: ExternalDataException) {\n            widget.showWarning(e.message)\n            emptyList()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/LabelWidget.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets.items;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.util.DisplayMetrics;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.widget.ImageView;\nimport android.widget.ImageView.ScaleType;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.dynamicpreload.ExternalSelectChoice;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.androidshared.bitmap.ImageFileUtils;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning;\n\nimport java.io.File;\nimport java.util.List;\n\nimport timber.log.Timber;\n\n/**\n * The Label Widget does not return an answer. The purpose of this widget is to be the top entry in\n * a field-list with a bunch of list widgets below. This widget provides the labels, so that the\n * list widgets can hide their labels and reduce the screen clutter. This class is essentially\n * ListWidget with all the answer generating code removed.\n *\n * @author Jeff Beorse\n */\n@SuppressLint(\"ViewConstructor\")\npublic class LabelWidget extends QuestionWidget {\n\n    private final List<SelectChoice> items;\n\n    public LabelWidget(Context context, QuestionDetails questionDetails, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        items = ItemsWidgetUtils.loadItemsAndHandleErrors(this, questionDetails.getPrompt(), selectChoiceLoader);\n        addItems(context, questionDetails);\n        SpacesInUnderlyingValuesWarning.forQuestionWidget(this).renderWarningIfNecessary(items);\n    }\n\n    private void addItems(Context context, QuestionDetails questionDetails) {\n        // Layout holds the horizontal list of buttons\n        LinearLayout listItems = findViewById(R.id.answer_container);\n\n        if (items != null) {\n            for (int i = 0; i < items.size(); i++) {\n\n                String imageURI;\n                if (items.get(i) instanceof ExternalSelectChoice) {\n                    imageURI = ((ExternalSelectChoice) items.get(i)).getImage();\n                } else {\n                    imageURI = questionDetails.getPrompt().getSpecialFormSelectChoiceText(items.get(i),\n                            FormEntryCaption.TEXT_FORM_IMAGE);\n                }\n\n                // build image view (if an image is provided)\n                ImageView imageView = null;\n                TextView missingImage = null;\n\n                final int labelId = View.generateViewId();\n\n                // Now set up the image view\n                String errorMsg = null;\n                if (imageURI != null) {\n                    try {\n                        String imageFilename =\n                                ReferenceManager.instance().deriveReference(imageURI).getLocalURI();\n                        final File imageFile = new File(imageFilename);\n                        if (imageFile.exists()) {\n                            Bitmap b = null;\n                            try {\n                                DisplayMetrics metrics = context.getResources().getDisplayMetrics();\n                                int screenWidth = metrics.widthPixels;\n                                int screenHeight = metrics.heightPixels;\n                                b = ImageFileUtils.getBitmapScaledToDisplay(imageFile, screenHeight, screenWidth);\n                            } catch (OutOfMemoryError e) {\n                                Timber.e(e);\n                                errorMsg = \"ERROR: \" + e.getMessage();\n                            }\n\n                            if (b != null) {\n                                imageView = new ImageView(getContext());\n                                imageView.setPadding(2, 2, 2, 2);\n                                imageView.setAdjustViewBounds(true);\n                                imageView.setImageBitmap(b);\n                                imageView.setId(labelId);\n                            } else if (errorMsg == null) {\n                                // An error hasn't been logged and loading the image failed, so it's\n                                // likely\n                                // a bad file.\n                                errorMsg = getContext().getString(org.odk.collect.strings.R.string.file_invalid, imageFile);\n\n                            }\n                        } else {\n                            // An error hasn't been logged. We should have an image, but the file\n                            // doesn't\n                            // exist.\n                            errorMsg = getContext().getString(org.odk.collect.strings.R.string.file_missing, imageFile);\n                        }\n\n                        if (errorMsg != null) {\n                            // errorMsg is only set when an error has occured\n                            Timber.e(new Error(errorMsg));\n                            missingImage = new TextView(getContext());\n                            missingImage.setText(errorMsg);\n\n                            missingImage.setPadding(2, 2, 2, 2);\n                            missingImage.setId(labelId);\n                        }\n\n                    } catch (InvalidReferenceException e) {\n                        Timber.e(e, \"Invalid image reference\");\n                    }\n\n                }\n\n                // build text label. Don't assign the text to the built in label to he\n                // button because it aligns horizontally, and we want the label on top\n                TextView label = new TextView(getContext());\n                label.setText(HtmlUtils.textToHtml(questionDetails.getPrompt().getSelectChoiceText(items.get(i))));\n                label.setTextSize(TypedValue.COMPLEX_UNIT_DIP, QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.HEADLINE_6));\n                label.setGravity(Gravity.CENTER_HORIZONTAL);\n\n                // answer layout holds the label text/image on top and the radio button on bottom\n                LinearLayout answer = new LinearLayout(getContext());\n                answer.setOrientation(LinearLayout.VERTICAL);\n                LinearLayout.LayoutParams headerParams =\n                        new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,\n                                LayoutParams.WRAP_CONTENT);\n                headerParams.gravity = Gravity.CENTER_HORIZONTAL;\n\n                LinearLayout.LayoutParams buttonParams =\n                        new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,\n                                LayoutParams.WRAP_CONTENT);\n                buttonParams.gravity = Gravity.CENTER_HORIZONTAL;\n\n                if (imageView != null) {\n                    imageView.setScaleType(ScaleType.CENTER);\n                    answer.addView(imageView, headerParams);\n                } else if (missingImage != null) {\n                    answer.addView(missingImage, headerParams);\n                } else {\n                    label.setId(labelId);\n                    answer.addView(label, headerParams);\n                }\n                answer.setPadding(4, 0, 4, 0);\n\n                // Each button gets equal weight\n                LinearLayout.LayoutParams answerParams =\n                        new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,\n                                LayoutParams.MATCH_PARENT);\n                answerParams.weight = 1;\n                listItems.addView(answer, answerParams);\n            }\n        }\n    }\n\n    @Override\n    protected int getLayout() {\n        return R.layout.label_widget;\n    }\n\n    @Override\n    public void clearAnswer() {\n        // Do nothing, no answers to clear\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return null;\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/LikertWidget.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport static android.widget.RelativeLayout.CENTER_HORIZONTAL;\nimport static android.widget.RelativeLayout.CENTER_IN_PARENT;\nimport static android.widget.RelativeLayout.TRUE;\nimport static org.odk.collect.android.utilities.ViewUtils.pxFromDp;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.util.DisplayMetrics;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.RadioButton;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport com.google.android.material.radiobutton.MaterialRadioButton;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.dynamicpreload.ExternalSelectChoice;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.androidshared.bitmap.ImageFileUtils;\n\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.List;\n\nimport timber.log.Timber;\n\n@SuppressLint(\"ViewConstructor\")\npublic class LikertWidget extends QuestionWidget {\n    LinearLayout view;\n    private RadioButton checkedButton;\n    private final LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, 1);\n    private final RelativeLayout.LayoutParams textViewParams = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);\n    private final RelativeLayout.LayoutParams imageViewParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);\n    private final RelativeLayout.LayoutParams radioButtonsParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);\n    private final RelativeLayout.LayoutParams buttonViewParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);\n    private final List<SelectChoice> items;\n\n    HashMap<RadioButton, String> buttonsToName;\n\n    public LikertWidget(Context context, QuestionDetails questionDetails, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        items = ItemsWidgetUtils.loadItemsAndHandleErrors(this, questionDetails.getPrompt(), selectChoiceLoader);\n\n        setMainViewLayoutParameters();\n        setStructures();\n        setButtonListener();\n        setSavedButton();\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) {\n        return view;\n    }\n\n    public void setMainViewLayoutParameters() {\n        view = new LinearLayout(getContext());\n        view.setOrientation(LinearLayout.HORIZONTAL);\n        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(\n                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);\n        view.setLayoutParams(params);\n    }\n\n    // Inserts the selected button from a saved state\n    public void setSavedButton() {\n        if (getFormEntryPrompt().getAnswerValue() != null) {\n            String name = ((Selection) getFormEntryPrompt().getAnswerValue()\n                    .getValue()).getValue();\n            for (RadioButton bu : buttonsToName.keySet()) {\n                if (buttonsToName.get(bu).equals(name)) {\n                    checkedButton = bu;\n                    checkedButton.setChecked(true);\n                }\n            }\n        }\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        if (checkedButton == null) {\n            return null;\n        } else {\n            int selectedIndex = -1;\n            for (int i = 0; i < items.size(); i++) {\n                if (items.get(i).getValue().equals(buttonsToName.get(checkedButton))) {\n                    selectedIndex = i;\n                }\n            }\n            if (selectedIndex == -1) {\n                return null;\n            }\n            SelectChoice sc = items.get(selectedIndex);\n            return new SelectOneData(new Selection(sc));\n        }\n    }\n\n    public void setStructures() {\n        buttonsToName = new HashMap<>();\n        for (int i = 0; i < items.size(); i++) {\n            RelativeLayout buttonView = new RelativeLayout(this.getContext());\n            buttonViewParams.addRule(CENTER_IN_PARENT, TRUE);\n            buttonView.setLayoutParams(buttonViewParams);\n            RadioButton button = getRadioButton(i);\n\n            buttonsToName.put(button, items.get(i).getValue());\n            buttonView.addView(button);\n\n            LinearLayout optionView = getLinearLayout();\n            optionView.addView(buttonView);\n\n            ImageView imgView = getImageAsImageView(i);\n            // checks if image is set or valid\n            if (imgView != null) {\n                optionView.addView(imgView);\n            }\n            TextView choice = getTextView();\n            choice.setText(HtmlUtils.textToHtml(getFormEntryPrompt().getSelectChoiceText(items.get(i))));\n\n            optionView.addView(choice);\n\n            optionView.setEnabled(!getFormEntryPrompt().isReadOnly());\n            optionView.setOnClickListener(new OnClickListener() {\n\n                @Override\n                public void onClick(View v) {\n                    RadioButton r = button;\n                    if (checkedButton != null) {\n                        checkedButton.setChecked(false);\n                    }\n                    checkedButton = r;\n                    checkedButton.setChecked(true);\n                    widgetValueChanged();\n                }\n            });\n            view.addView(optionView);\n        }\n    }\n\n    // Creates image view for choice\n    public ImageView getImageView() {\n        ImageView view = new ImageView(getContext());\n        view.setLayoutParams(imageViewParams);\n        return view;\n    }\n\n    public RadioButton getRadioButton(int i) {\n        MaterialRadioButton button = new MaterialRadioButton(getContext());\n        button.setId(View.generateViewId());\n        button.setEnabled(!getFormEntryPrompt().isReadOnly());\n        button.setFocusable(!getFormEntryPrompt().isReadOnly());\n        radioButtonsParams.addRule(CENTER_HORIZONTAL, TRUE);\n        radioButtonsParams.setMargins(0, pxFromDp(getContext(), 16), 0, pxFromDp(getContext(), 8));\n        button.setLayoutParams(radioButtonsParams);\n\n        // Remove radio button padding - this needs minHeight/Width rather than padding\n        button.setMinHeight(0);\n        button.setMinWidth(0);\n        button.setMinimumHeight(0);\n        button.setMinimumWidth(0);\n\n        button.setGravity(Gravity.CENTER);\n        return button;\n    }\n\n    // Creates text view for choice\n    public TextView getTextView() {\n        TextView view = new TextView(getContext());\n        view.setGravity(Gravity.CENTER);\n        view.setPadding(2, 2, 2, 2);\n        view.setLayoutParams(textViewParams);\n        return view;\n    }\n\n    // Linear Layout for new choice\n    public LinearLayout getLinearLayout() {\n        LinearLayout optionView = new LinearLayout(getContext());\n        optionView.setGravity(Gravity.CENTER);\n        optionView.setLayoutParams(linearLayoutParams);\n        linearLayoutParams.setMargins(-1, 0, -1, 0);\n        optionView.setOrientation(LinearLayout.VERTICAL);\n        return optionView;\n    }\n\n    public void setButtonListener() {\n        for (RadioButton button : buttonsToName.keySet()) {\n            button.setOnClickListener(new OnClickListener() {\n\n                @Override\n                public void onClick(View v) {\n                    RadioButton r = (RadioButton) v;\n                    if (checkedButton != null) {\n                        checkedButton.setChecked(false);\n                    }\n                    checkedButton = r;\n                    checkedButton.setChecked(true);\n                    widgetValueChanged();\n                }\n            });\n        }\n    }\n\n    public ImageView getImageAsImageView(int index) {\n        ImageView view = getImageView();\n        String imageURI;\n        if (items.get(index) instanceof ExternalSelectChoice) {\n            imageURI = ((ExternalSelectChoice) items.get(index)).getImage();\n        } else {\n            imageURI = getFormEntryPrompt().getSpecialFormSelectChoiceText(items.get(index),\n                    FormEntryCaption.TEXT_FORM_IMAGE);\n        }\n        if (imageURI != null) {\n            String error = setImageFromOtherSource(imageURI, view);\n            if (error != null) {\n                return null;\n            }\n            return view;\n        } else {\n            return null;\n        }\n    }\n\n    public String setImageFromOtherSource(String imageURI, ImageView imageView) {\n        String errorMsg = null;\n        try {\n            String imageFilename =\n                    ReferenceManager.instance().deriveReference(imageURI).getLocalURI();\n            final File imageFile = new File(imageFilename);\n            if (imageFile.exists()) {\n                Bitmap b = null;\n                try {\n                    DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();\n                    int screenWidth = metrics.widthPixels;\n                    int screenHeight = metrics.heightPixels;\n                    b = ImageFileUtils.getBitmapScaledToDisplay(imageFile, screenHeight, screenWidth);\n                } catch (OutOfMemoryError e) {\n                    errorMsg = \"ERROR: \" + e.getMessage();\n                }\n\n                if (b != null) {\n                    imageView.setAdjustViewBounds(true);\n                    imageView.setImageBitmap(b);\n                } else if (errorMsg == null) {\n                    // Loading the image failed. The image work when in .jpg format\n                    errorMsg = getContext().getString(org.odk.collect.strings.R.string.file_invalid, imageFile);\n\n                }\n            } else {\n                errorMsg = getContext().getString(org.odk.collect.strings.R.string.file_missing, imageFile);\n            }\n            if (errorMsg != null) {\n                Timber.e(new Error(errorMsg));\n            }\n        } catch (InvalidReferenceException e) {\n            Timber.d(e, \"Invalid image reference due to %s \", e.getMessage());\n        }\n        return errorMsg;\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        for (RadioButton r : buttonsToName.keySet()) {\n            r.setOnLongClickListener(l);\n        }\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        for (RadioButton r : buttonsToName.keySet()) {\n            r.cancelLongPress();\n        }\n    }\n\n    @Override\n    public void clearAnswer() {\n        if (checkedButton != null) {\n            checkedButton.setChecked(false);\n        }\n        checkedButton = null;\n        widgetValueChanged();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/ListMultiWidget.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets.items;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.util.DisplayMetrics;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.widget.CheckBox;\nimport android.widget.CompoundButton;\nimport android.widget.ImageView;\nimport android.widget.ImageView.ScaleType;\nimport android.widget.LinearLayout;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.appcompat.widget.AppCompatCheckBox;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectMultiData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.dynamicpreload.ExternalSelectChoice;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.androidshared.bitmap.ImageFileUtils;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.MultiChoiceWidget;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport timber.log.Timber;\n\n/**\n * ListMultiWidget handles multiple selection fields using check boxes. The check boxes are aligned\n * horizontally. They are typically meant to be used in a field list, where multiple questions with\n * the same multiple choice answers can sit on top of each other and make a grid of buttons that is\n * easy to navigate quickly. Optionally, you can turn off the labels. This would be done if a label\n * widget was at the top of your field list to provide the labels. If audio or video are specified\n * in the select answers they are ignored. This class is almost identical to ListWidget, except it\n * uses checkboxes. It also did not require a custom clickListener class.\n *\n * @author Jeff Beorse (jeff@beorse.net)\n */\n@SuppressLint(\"ViewConstructor\")\npublic class ListMultiWidget extends QuestionWidget implements MultiChoiceWidget {\n\n    final ArrayList<CheckBox> checkBoxes;\n    private final boolean displayLabel;\n    private final List<SelectChoice> items;\n\n    @SuppressWarnings(\"unchecked\")\n    public ListMultiWidget(Context context, QuestionDetails questionDetails, boolean displayLabel, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        items = ItemsWidgetUtils.loadItemsAndHandleErrors(this, questionDetails.getPrompt(), selectChoiceLoader);\n\n        checkBoxes = new ArrayList<>();\n        this.displayLabel = displayLabel;\n\n        // Layout holds the horizontal list of buttons\n        LinearLayout buttonLayout = findViewById(R.id.answer_container);\n\n        List<Selection> ve = new ArrayList<>();\n        if (questionDetails.getPrompt().getAnswerValue() != null) {\n            ve = (List<Selection>) questionDetails.getPrompt().getAnswerValue().getValue();\n        }\n\n        if (items != null) {\n            for (int i = 0; i < items.size(); i++) {\n\n                AppCompatCheckBox c = new AppCompatCheckBox(getContext());\n                c.setTag(i);\n                c.setId(View.generateViewId());\n                c.setFocusable(!questionDetails.getPrompt().isReadOnly());\n                c.setEnabled(!questionDetails.getPrompt().isReadOnly());\n\n                for (int vi = 0; vi < ve.size(); vi++) {\n                    // match based on value, not key\n                    if (items.get(i).getValue().equals(ve.get(vi).getValue())) {\n                        c.setChecked(true);\n                        break;\n                    }\n\n                }\n                checkBoxes.add(c);\n\n                // when clicked, check for readonly before toggling\n                c.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {\n                    @Override\n                    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {\n                        if (getFormEntryPrompt().isReadOnly()) {\n                            if (buttonView.isChecked()) {\n                                buttonView.setChecked(false);\n                            } else {\n                                buttonView.setChecked(true);\n                            }\n                        }\n\n                        widgetValueChanged();\n                    }\n                });\n\n                String imageURI;\n                if (items.get(i) instanceof ExternalSelectChoice) {\n                    imageURI = ((ExternalSelectChoice) items.get(i)).getImage();\n                } else {\n                    imageURI = questionDetails.getPrompt().getSpecialFormSelectChoiceText(items.get(i),\n                            FormEntryCaption.TEXT_FORM_IMAGE);\n                }\n\n                // build image view (if an image is provided)\n                ImageView imageView = null;\n                TextView missingImage = null;\n\n                final int labelId = View.generateViewId();\n\n                // Now set up the image view\n                String errorMsg = null;\n                if (imageURI != null) {\n                    try {\n                        String imageFilename =\n                                ReferenceManager.instance().deriveReference(imageURI).getLocalURI();\n                        final File imageFile = new File(imageFilename);\n                        if (imageFile.exists()) {\n                            Bitmap b = null;\n                            try {\n                                DisplayMetrics metrics = context.getResources().getDisplayMetrics();\n                                int screenWidth = metrics.widthPixels;\n                                int screenHeight = metrics.heightPixels;\n                                b = ImageFileUtils.getBitmapScaledToDisplay(imageFile, screenHeight, screenWidth);\n                            } catch (OutOfMemoryError e) {\n                                errorMsg = \"ERROR: \" + e.getMessage();\n                            }\n\n                            if (b != null) {\n                                imageView = new ImageView(getContext());\n                                imageView.setPadding(2, 2, 2, 2);\n                                imageView.setAdjustViewBounds(true);\n                                imageView.setImageBitmap(b);\n                                imageView.setId(labelId);\n                            } else if (errorMsg == null) {\n                                // An error hasn't been logged and loading the image failed, so it's\n                                // likely\n                                // a bad file.\n                                errorMsg = getContext().getString(org.odk.collect.strings.R.string.file_invalid, imageFile);\n\n                            }\n                        } else {\n                            // An error hasn't been logged. We should have an image, but the file\n                            // doesn't\n                            // exist.\n                            errorMsg = getContext().getString(org.odk.collect.strings.R.string.file_missing, imageFile);\n                        }\n\n                        if (errorMsg != null) {\n                            // errorMsg is only set when an error has occured\n                            Timber.e(new Error(errorMsg));\n                            missingImage = new TextView(getContext());\n                            missingImage.setText(errorMsg);\n\n                            missingImage.setPadding(2, 2, 2, 2);\n                            missingImage.setId(labelId);\n                        }\n\n                    } catch (InvalidReferenceException e) {\n                        Timber.d(e, \"Invalid image reference due to %s \", e.getMessage());\n                    }\n                }\n\n                // build text label. Don't assign the text to the built in label to he\n                // button because it aligns horizontally, and we want the label on top\n                TextView label = new TextView(getContext());\n                label.setText(HtmlUtils.textToHtml(questionDetails.getPrompt().getSelectChoiceText(items.get(i))));\n                label.setTextSize(TypedValue.COMPLEX_UNIT_DIP, QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.HEADLINE_6));\n                label.setGravity(Gravity.CENTER_HORIZONTAL);\n                if (!displayLabel) {\n                    label.setVisibility(View.GONE);\n                }\n\n                // answer layout holds the label text/image on top and the radio button on bottom\n                RelativeLayout answer = new RelativeLayout(getContext());\n                RelativeLayout.LayoutParams headerParams =\n                        new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT,\n                                LayoutParams.WRAP_CONTENT);\n                headerParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);\n                headerParams.addRule(RelativeLayout.CENTER_HORIZONTAL);\n\n                RelativeLayout.LayoutParams buttonParams =\n                        new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT,\n                                LayoutParams.WRAP_CONTENT);\n                buttonParams.addRule(RelativeLayout.CENTER_HORIZONTAL);\n\n                if (imageView != null) {\n                    imageView.setScaleType(ScaleType.CENTER);\n                    if (!displayLabel) {\n                        imageView.setVisibility(View.GONE);\n                    }\n                    answer.addView(imageView, headerParams);\n                } else if (missingImage != null) {\n                    answer.addView(missingImage, headerParams);\n                } else {\n                    if (displayLabel) {\n                        label.setId(labelId);\n                        answer.addView(label, headerParams);\n                    }\n\n                }\n                if (displayLabel) {\n                    buttonParams.addRule(RelativeLayout.BELOW, labelId);\n                }\n                answer.addView(c, buttonParams);\n                answer.setPadding(4, 0, 4, 0);\n\n                // /Each button gets equal weight\n                LinearLayout.LayoutParams answerParams =\n                        new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,\n                                LayoutParams.MATCH_PARENT);\n                answerParams.weight = 1;\n\n                buttonLayout.addView(answer, answerParams);\n            }\n\n            SpacesInUnderlyingValuesWarning\n                    .forQuestionWidget(this)\n                    .renderWarningIfNecessary(items);\n        }\n    }\n\n    @Override\n    public void clearAnswer() {\n        for (int i = 0; i < checkBoxes.size(); i++) {\n            CheckBox c = checkBoxes.get(i);\n            if (c.isChecked()) {\n                c.setChecked(false);\n            }\n        }\n        widgetValueChanged();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        List<Selection> vc = new ArrayList<>();\n        for (int i = 0; i < checkBoxes.size(); i++) {\n            CheckBox c = checkBoxes.get(i);\n            if (c.isChecked()) {\n                vc.add(new Selection(items.get(i)));\n            }\n        }\n\n        if (vc.isEmpty()) {\n            return null;\n        } else {\n            return new SelectMultiData(vc);\n        }\n\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        for (CheckBox c : checkBoxes) {\n            c.setOnLongClickListener(l);\n        }\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        for (CheckBox c : checkBoxes) {\n            c.cancelLongPress();\n        }\n    }\n\n    @Override\n    public int getChoiceCount() {\n        return checkBoxes.size();\n    }\n\n    @Override\n    public void setChoiceSelected(int choiceIndex, boolean isSelected) {\n        checkBoxes.get(choiceIndex).setChecked(isSelected);\n    }\n\n    @Override\n    protected int getLayout() {\n        return R.layout.label_widget;\n    }\n\n    public boolean shouldDisplayLabel() {\n        return displayLabel;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/ListWidget.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets.items;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.util.DisplayMetrics;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.widget.CompoundButton;\nimport android.widget.CompoundButton.OnCheckedChangeListener;\nimport android.widget.ImageView;\nimport android.widget.ImageView.ScaleType;\nimport android.widget.LinearLayout;\nimport android.widget.RadioButton;\nimport android.widget.TextView;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.AppCompatRadioButton;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.dynamicpreload.ExternalSelectChoice;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.AdvanceToNextListener;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.androidshared.bitmap.ImageFileUtils;\nimport org.odk.collect.android.utilities.SelectOneWidgetUtils;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.MultiChoiceWidget;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport timber.log.Timber;\n\n/**\n * ListWidget handles select-one fields using radio buttons. The radio buttons are aligned\n * horizontally. They are typically meant to be used in a field list, where multiple questions with\n * the same multiple choice answers can sit on top of each other and make a grid of buttons that is\n * easy to navigate quickly. Optionally, you can turn off the labels. This would be done if a label\n * widget was at the top of your field list to provide the labels. If audio or video are specified\n * in the select answers they are ignored.\n *\n * @author Jeff Beorse (jeff@beorse.net)\n */\n@SuppressLint(\"ViewConstructor\")\npublic class ListWidget extends QuestionWidget implements MultiChoiceWidget, OnCheckedChangeListener {\n\n    @Nullable\n    private AdvanceToNextListener listener;\n\n    private final boolean autoAdvance;\n\n    ArrayList<RadioButton> buttons;\n    private final boolean displayLabel;\n    private final List<SelectChoice> items;\n\n    public ListWidget(Context context, QuestionDetails questionDetails, boolean displayLabel, boolean autoAdvance, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, dependencies, questionDetails);\n        render();\n\n        items = ItemsWidgetUtils.loadItemsAndHandleErrors(this, questionDetails.getPrompt(), selectChoiceLoader);\n\n        this.autoAdvance = autoAdvance;\n        this.displayLabel = displayLabel;\n\n        if (context instanceof AdvanceToNextListener) {\n            listener = (AdvanceToNextListener) context;\n        }\n\n        buttons = new ArrayList<>();\n\n        // Layout holds the horizontal list of buttons\n        LinearLayout buttonLayout = findViewById(R.id.answer_container);\n\n        Selection selectedItem = SelectOneWidgetUtils.getSelectedItem(getQuestionDetails().getPrompt(), items);\n        String s = selectedItem == null ? null : selectedItem.getValue();\n\n        if (items != null) {\n            for (int i = 0; i < items.size(); i++) {\n                AppCompatRadioButton r = new AppCompatRadioButton(getContext());\n                r.setId(View.generateViewId());\n                r.setTag(i);\n                r.setEnabled(!questionDetails.getPrompt().isReadOnly());\n                r.setFocusable(!questionDetails.getPrompt().isReadOnly());\n\n                buttons.add(r);\n\n                if (items.get(i).getValue().equals(s)) {\n                    r.setChecked(true);\n                }\n                r.setOnCheckedChangeListener(this);\n\n                String imageURI;\n                if (items.get(i) instanceof ExternalSelectChoice) {\n                    imageURI = ((ExternalSelectChoice) items.get(i)).getImage();\n                } else {\n                    imageURI = questionDetails.getPrompt().getSpecialFormSelectChoiceText(items.get(i),\n                            FormEntryCaption.TEXT_FORM_IMAGE);\n                }\n\n                // build image view (if an image is provided)\n                ImageView imageView = null;\n                TextView missingImage = null;\n\n                final int labelId = View.generateViewId();\n\n                // Now set up the image view\n                String errorMsg = null;\n                if (imageURI != null) {\n                    try {\n                        String imageFilename =\n                                ReferenceManager.instance().deriveReference(imageURI).getLocalURI();\n                        final File imageFile = new File(imageFilename);\n                        if (imageFile.exists()) {\n                            Bitmap b = null;\n                            try {\n                                DisplayMetrics metrics = context.getResources().getDisplayMetrics();\n                                int screenWidth = metrics.widthPixels;\n                                int screenHeight = metrics.heightPixels;\n                                b = ImageFileUtils.getBitmapScaledToDisplay(imageFile, screenHeight, screenWidth);\n                            } catch (OutOfMemoryError e) {\n                                errorMsg = \"ERROR: \" + e.getMessage();\n                            }\n\n                            if (b != null) {\n                                imageView = new ImageView(getContext());\n                                imageView.setPadding(2, 2, 2, 2);\n                                imageView.setAdjustViewBounds(true);\n                                imageView.setImageBitmap(b);\n                                imageView.setId(labelId);\n                            } else if (errorMsg == null) {\n                                // An error hasn't been logged and loading the image failed, so it's\n                                // likely\n                                // a bad file.\n                                errorMsg = getContext().getString(org.odk.collect.strings.R.string.file_invalid, imageFile);\n\n                            }\n                        } else {\n                            // An error hasn't been logged. We should have an image, but the file\n                            // doesn't\n                            // exist.\n                            errorMsg = getContext().getString(org.odk.collect.strings.R.string.file_missing, imageFile);\n                        }\n\n                        if (errorMsg != null) {\n                            // errorMsg is only set when an error has occured\n                            Timber.e(new Error(errorMsg));\n                            missingImage = new TextView(getContext());\n                            missingImage.setText(errorMsg);\n\n                            missingImage.setPadding(2, 2, 2, 2);\n                            missingImage.setId(labelId);\n                        }\n\n                    } catch (InvalidReferenceException e) {\n                        Timber.d(e, \"Invalid image reference due to %s \", e.getMessage());\n                    }\n                }\n\n                // build text label. Don't assign the text to the built in label to he\n                // button because it aligns horizontally, and we want the label on top\n                TextView label = new TextView(getContext());\n                label.setText(HtmlUtils.textToHtml(questionDetails.getPrompt().getSelectChoiceText(items.get(i))));\n                label.setTextSize(TypedValue.COMPLEX_UNIT_DIP, QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.HEADLINE_6));\n                label.setGravity(Gravity.CENTER_HORIZONTAL);\n                if (!displayLabel) {\n                    label.setVisibility(View.GONE);\n                }\n\n                // answer layout holds the label text/image on top and the radio button on bottom\n                LinearLayout answer = new LinearLayout(getContext());\n                answer.setOrientation(LinearLayout.VERTICAL);\n                LinearLayout.LayoutParams headerParams =\n                        new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,\n                                LayoutParams.WRAP_CONTENT);\n                headerParams.gravity = Gravity.CENTER_HORIZONTAL;\n\n                LinearLayout.LayoutParams buttonParams =\n                        new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,\n                                LayoutParams.WRAP_CONTENT);\n                buttonParams.gravity = Gravity.CENTER_HORIZONTAL;\n\n                if (imageView != null) {\n                    imageView.setScaleType(ScaleType.CENTER);\n                    if (!displayLabel) {\n                        imageView.setVisibility(View.GONE);\n                    }\n                    answer.addView(imageView, headerParams);\n                } else if (missingImage != null) {\n                    answer.addView(missingImage, headerParams);\n                } else {\n                    if (displayLabel) {\n                        label.setId(labelId);\n                        answer.addView(label, headerParams);\n                    }\n\n                }\n                answer.addView(r, buttonParams);\n                answer.setPadding(4, 0, 4, 0);\n\n                // Each button gets equal weight\n                LinearLayout.LayoutParams answerParams =\n                        new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,\n                                LayoutParams.MATCH_PARENT);\n                answerParams.weight = 1;\n\n                buttonLayout.addView(answer, answerParams);\n            }\n        }\n    }\n\n    @Override\n    public void clearAnswer() {\n        for (RadioButton button : this.buttons) {\n            if (button.isChecked()) {\n                button.setChecked(false);\n                widgetValueChanged();\n                return;\n            }\n        }\n        widgetValueChanged();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        int i = getCheckedId();\n        if (i == -1) {\n            return null;\n        } else {\n            SelectChoice sc = items.get(i);\n            return new SelectOneData(new Selection(sc));\n        }\n    }\n\n    public int getCheckedId() {\n        for (int i = 0; i < buttons.size(); ++i) {\n            RadioButton button = buttons.get(i);\n            if (button.isChecked()) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    @Override\n    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {\n        if (!isChecked) {\n            // If it got unchecked, we don't care.\n            return;\n        }\n\n        for (RadioButton button : this.buttons) {\n            if (button.isChecked() && buttonView != button) {\n                button.setChecked(false);\n            }\n        }\n\n        if (autoAdvance && listener != null) {\n            listener.advance();\n        }\n\n        widgetValueChanged();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        for (RadioButton r : buttons) {\n            r.setOnLongClickListener(l);\n        }\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        for (RadioButton r : buttons) {\n            r.cancelLongPress();\n        }\n    }\n\n    @Override\n    public int getChoiceCount() {\n        return buttons.size();\n    }\n\n    @Override\n    public void setChoiceSelected(int choiceIndex, boolean isSelected) {\n        RadioButton button = buttons.get(choiceIndex);\n        button.setChecked(true);\n\n        onCheckedChanged(button, true);\n    }\n\n    @Override\n    protected int getLayout() {\n        return R.layout.label_widget;\n    }\n\n    public boolean shouldDisplayLabel() {\n        return displayLabel;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/RankingWidget.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.items;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.View;\nimport androidx.annotation.NonNull;\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectMultiData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.activities.FormFillingActivity;\nimport org.odk.collect.android.databinding.RankingWidgetBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.fragments.dialogs.RankingWidgetDialog;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@SuppressLint(\"ViewConstructor\")\npublic class RankingWidget extends QuestionWidget implements WidgetDataReceiver {\n\n    private final WaitingForDataRegistry waitingForDataRegistry;\n    private List<SelectChoice> savedItems;\n    private final List<SelectChoice> items;\n    RankingWidgetBinding binding;\n\n    public RankingWidget(Context context, QuestionDetails prompt, WaitingForDataRegistry waitingForDataRegistry, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, dependencies, prompt);\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        items = ItemsWidgetUtils.loadItemsAndHandleErrors(this, questionDetails.getPrompt(), selectChoiceLoader);\n        readSavedItems();\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(@NonNull Context context, @NonNull FormEntryPrompt prompt, int answerFontSize) {\n        binding = RankingWidgetBinding.inflate(((Activity) context).getLayoutInflater());\n\n        binding.rankItemsButton.setOnClickListener(v -> {\n            waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex());\n            RankingWidgetDialog rankingWidgetDialog = new RankingWidgetDialog(savedItems == null ? items : savedItems, getFormEntryPrompt());\n            rankingWidgetDialog.show(((FormFillingActivity) getContext()).getSupportFragmentManager(), \"RankingDialog\");\n        });\n        binding.answer.setText(getAnswerText());\n        binding.answer.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n        binding.answer.setVisibility(binding.answer.getText().toString().isBlank() ? GONE : VISIBLE);\n\n        if (questionDetails.isReadOnly()) {\n            binding.rankItemsButton.setVisibility(View.GONE);\n        }\n\n        SpacesInUnderlyingValuesWarning\n                .forQuestionWidget(this)\n                .renderWarningIfNecessary(savedItems == null ? items : savedItems);\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        List<Selection> orderedItems = new ArrayList<>();\n        if (savedItems != null) {\n            for (SelectChoice selectChoice : savedItems) {\n                orderedItems.add(new Selection(selectChoice));\n            }\n        }\n\n        return orderedItems.isEmpty() ? null : new SelectMultiData(orderedItems);\n    }\n\n    @Override\n    public void clearAnswer() {\n        savedItems = null;\n        binding.answer.setText(null);\n        binding.answer.setVisibility(GONE);\n        widgetValueChanged();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.rankItemsButton.setOnLongClickListener(l);\n        binding.answer.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        binding.rankItemsButton.cancelLongPress();\n        binding.answer.cancelLongPress();\n    }\n\n    @Override\n    public void setData(Object values) {\n        savedItems = (List<SelectChoice>) values;\n        binding.answer.setText(getAnswerText());\n        binding.answer.setVisibility(binding.answer.getText().toString().isBlank() ? GONE : VISIBLE);\n        widgetValueChanged();\n    }\n\n    private void readSavedItems() {\n        List<Selection> savedOrderedItems =\n                getFormEntryPrompt().getAnswerValue() == null\n                ? new ArrayList<>()\n                : (List<Selection>) getFormEntryPrompt().getAnswerValue().getValue();\n\n        if (!savedOrderedItems.isEmpty()) {\n            savedItems = new ArrayList<>();\n            for (Selection selection : savedOrderedItems) {\n                for (SelectChoice selectChoice : items) {\n                    if (selection.getValue().equals(selectChoice.getValue())) {\n                        savedItems.add(selectChoice);\n                        break;\n                    }\n                }\n            }\n\n            for (SelectChoice selectChoice : items) {\n                if (!savedItems.contains(selectChoice)) {\n                    savedItems.add(selectChoice);\n                }\n            }\n        }\n    }\n\n    private CharSequence getAnswerText() {\n        StringBuilder answerText = new StringBuilder();\n        if (savedItems != null) {\n            for (SelectChoice item : savedItems) {\n                answerText\n                        .append(savedItems.indexOf(item) + 1)\n                        .append(\". \")\n                        .append(getFormEntryPrompt().getSelectChoiceText(item));\n                if ((savedItems.size() - 1) > savedItems.indexOf(item)) {\n                    answerText.append(\"<br>\");\n                }\n            }\n        }\n        return HtmlUtils.textToHtml(answerText.toString());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectImageMapWidget.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.items;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.TypedValue;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\nimport android.widget.LinearLayout;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.SelectImageMapWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport javax.xml.transform.Transformer;\nimport javax.xml.transform.TransformerException;\nimport javax.xml.transform.TransformerFactory;\nimport javax.xml.transform.dom.DOMSource;\nimport javax.xml.transform.stream.StreamResult;\n\nimport timber.log.Timber;\n\n/**\n * A base widget class which is responsible for sharing the code used by image map select widgets like\n * {@link SelectOneImageMapWidget} and {@link SelectMultiImageMapWidget}.\n */\npublic abstract class SelectImageMapWidget extends QuestionWidget {\n    private static final String WEB_VIEW_CONTENT =\n            \"<!DOCTYPE html> <html>\\n\" +\n                    \"    <body>\\n\" +\n                    \"           %s\" + //inject an svg map here\n                    \"        <script src=\\\"file:///android_asset/svg_map_helper.js\\\"></script>\\n\" +\n                    \"    </body>\\n\" +\n                    \"</html>\";\n    private final boolean isSingleSelect;\n    protected List<Selection> selections = new ArrayList<>();\n    private String imageMapFilePath;\n    SelectImageMapWidgetAnswerBinding binding;\n\n    final List<SelectChoice> items;\n\n    public SelectImageMapWidget(Context context, QuestionDetails prompt, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, dependencies, prompt);\n        render();\n\n        items = ItemsWidgetUtils.loadItemsAndHandleErrors(this, questionDetails.getPrompt(), selectChoiceLoader);\n\n        isSingleSelect = this instanceof SelectOneImageMapWidget;\n\n        try {\n            imageMapFilePath = getReferenceManager().deriveReference(prompt.getPrompt().getImageText()).getLocalURI();\n        } catch (InvalidReferenceException e) {\n            Timber.w(e);\n        }\n        setUpWebView();\n    }\n\n    private static String convertDocumentToString(Document doc) {\n        TransformerFactory transformerFactory = TransformerFactory.newInstance();\n        Transformer transformer;\n        try {\n            transformer = transformerFactory.newTransformer();\n            StringWriter writer = new StringWriter();\n            transformer.transform(new DOMSource(doc), new StreamResult(writer));\n\n            return writer.getBuffer().toString();\n        } catch (TransformerException e) {\n            Timber.w(e);\n        }\n\n        return null;\n    }\n\n    @Override\n    public void clearAnswer() {\n        selections.clear();\n        binding.imageMap.loadUrl(\"javascript:clearAreas()\");\n        binding.selectedElements.setVisibility(GONE);\n        widgetValueChanged();\n    }\n\n    @Override\n    public boolean shouldSuppressFlingGesture() {\n        return binding.imageMap.suppressFlingGesture();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = SelectImageMapWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n        binding.selectedElements.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n        binding.selectedElements.setVisibility(binding.selectedElements.getText().toString().isBlank() ? GONE : VISIBLE);\n        return binding.getRoot();\n    }\n\n    @SuppressLint({\"SetJavaScriptEnabled\", \"AddJavascriptInterface\"})\n    private void setUpWebView() {\n        String svgMap = null;\n        if (imageMapFilePath != null && !imageMapFilePath.isEmpty()) {\n            svgMap = getParsedSVGFile();\n        }\n        if (svgMap != null) {\n            binding.imageMap.getSettings().setJavaScriptEnabled(true);\n            binding.imageMap.getSettings().setBuiltInZoomControls(true);\n            binding.imageMap.getSettings().setDisplayZoomControls(false);\n            binding.imageMap.addJavascriptInterface(new JavaScriptInterface(), \"imageMapInterface\");\n            binding.imageMap.loadDataWithBaseURL(null, String.format(WEB_VIEW_CONTENT, svgMap), \"text/html\", \"UTF-8\", null);\n            binding.imageMap.setInitialScale(1);\n            binding.imageMap.getSettings().setUseWideViewPort(true);\n            int height = (int) (getResources().getDisplayMetrics().heightPixels / 1.7); // about 60% of a screen\n            binding.imageMap.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height));\n            binding.imageMap.setClickable(!getFormEntryPrompt().isReadOnly());\n            binding.imageMap.setWebViewClient(new WebViewClient() {\n                @Override\n                public void onPageFinished(WebView view, String url) {\n                    view.loadUrl(\"javascript:setSelectMode(\" + isSingleSelect + \")\");\n                    for (SelectChoice selectChoice : items) {\n                        view.loadUrl(\"javascript:addArea('\" + selectChoice.getValue() + \"')\");\n                    }\n                    highlightSelections(view);\n                }\n            });\n        }\n    }\n\n    protected void selectArea(String areaId) {\n        SelectChoice selectChoice = null;\n        for (SelectChoice sc : items) {\n            if (areaId.equalsIgnoreCase(sc.getValue())) {\n                selectChoice = sc;\n            }\n        }\n        if (selectChoice != null) {\n            selections.add(new Selection(selectChoice));\n        }\n        ((Activity) getContext()).runOnUiThread(this::widgetValueChanged);\n    }\n\n    private void unselectArea(String areaId) {\n        Selection selectionToRemove = null;\n\n        for (Selection selection : selections) {\n            if (areaId.equalsIgnoreCase(selection.getValue())) {\n                selectionToRemove = selection;\n            }\n        }\n\n        selections.remove(selectionToRemove);\n        ((Activity) getContext()).runOnUiThread(this::widgetValueChanged);\n    }\n\n    private void notifyChanges() {\n        refreshSelectedItemsLabel();\n    }\n\n    private String getParsedSVGFile() {\n        File initialFile = new File(imageMapFilePath);\n\n        try (FileInputStream inputStream = new FileInputStream(initialFile)) {\n            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();\n            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();\n            Document document = documentBuilder.parse(inputStream);\n\n            Element element = document.getDocumentElement();\n            element.normalize();\n\n            // Add default svg size if not specified\n            NodeList nodeList = document.getElementsByTagName(\"svg\");\n            addSizeAttributesIfNeeded(nodeList);\n\n            // Add onClick attributes\n            nodeList = document.getElementsByTagName(\"g\");\n            addOnClickAttributes(nodeList);\n            nodeList = document.getElementsByTagName(\"path\");\n            addOnClickAttributes(nodeList);\n            nodeList = document.getElementsByTagName(\"rect\");\n            addOnClickAttributes(nodeList);\n            nodeList = document.getElementsByTagName(\"circle\");\n            addOnClickAttributes(nodeList);\n            nodeList = document.getElementsByTagName(\"ellipse\");\n            addOnClickAttributes(nodeList);\n            nodeList = document.getElementsByTagName(\"polygon\");\n            addOnClickAttributes(nodeList);\n            return convertDocumentToString(document);\n        } catch (Exception e) {\n            Timber.w(e);\n            return getContext().getString(org.odk.collect.strings.R.string.svg_file_does_not_exist);\n        }\n    }\n\n    private void addOnClickAttributes(NodeList nodes) {\n        for (int i = 0; i < nodes.getLength(); i++) {\n            Node node = nodes.item(i);\n            Node elementId = node.getAttributes().getNamedItem(\"id\");\n            if (node.getNodeType() == Node.ELEMENT_NODE && elementId != null && doesElementExistInDataSet(elementId.getNodeValue())) {\n                ((Element) node).setAttribute(\"onClick\", \"clickOnArea(this.id)\");\n            }\n        }\n    }\n\n    private boolean doesElementExistInDataSet(String elementId) {\n        for (SelectChoice item : items) {\n            if (item.getValue().equals(elementId)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private void addSizeAttributesIfNeeded(NodeList nodes) {\n        Node svg = nodes.item(0);\n        if (svg.getAttributes().getNamedItem(\"width\") == null) {\n            ((Element) svg).setAttribute(\"width\", \"1000\");\n        }\n        if (svg.getAttributes().getNamedItem(\"height\") == null) {\n            ((Element) svg).setAttribute(\"height\", \"1000\");\n        }\n    }\n\n    protected void refreshSelectedItemsLabel() {\n        StringBuilder stringBuilder = new StringBuilder();\n        if (!selections.isEmpty()) {\n            stringBuilder\n                    .append(\"<b>\")\n                    .append(getContext().getString(org.odk.collect.strings.R.string.selected_title))\n                    .append(\"</b> \");\n            for (Selection selection : selections) {\n                String answer = getFormEntryPrompt().getSelectChoiceText(selection.choice);\n                stringBuilder.append(answer);\n                if (selections.indexOf(selection) < selections.size() - 1) {\n                    stringBuilder.append(\", \");\n                }\n            }\n        }\n\n        ((Activity) getContext()).runOnUiThread(() -> {\n            binding.selectedElements.setText(HtmlUtils.textToHtml(stringBuilder.toString()));\n            binding.selectedElements.setVisibility(binding.selectedElements.getText().toString().isBlank() ? GONE : VISIBLE);\n        });\n    }\n\n    protected abstract void highlightSelections(WebView view);\n\n    private class JavaScriptInterface {\n        @android.webkit.JavascriptInterface\n        public void selectArea(String areaId) {\n            SelectImageMapWidget.this.selectArea(areaId);\n        }\n\n        @android.webkit.JavascriptInterface\n        public void unselectArea(String areaId) {\n            SelectImageMapWidget.this.unselectArea(areaId);\n        }\n\n        @android.webkit.JavascriptInterface\n        public void notifyChanges() {\n            SelectImageMapWidget.this.notifyChanges();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectMinimalWidget.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.view.View;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.SelectMinimalWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.MultiChoiceWidget;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\nimport java.util.List;\n\npublic abstract class SelectMinimalWidget extends QuestionWidget implements WidgetDataReceiver, MultiChoiceWidget {\n\n    final List<SelectChoice> items;\n\n    SelectMinimalWidgetAnswerBinding binding;\n    private final WaitingForDataRegistry waitingForDataRegistry;\n\n    public SelectMinimalWidget(Context context, QuestionDetails prompt, WaitingForDataRegistry waitingForDataRegistry, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, dependencies, prompt);\n        this.waitingForDataRegistry = waitingForDataRegistry;\n        items = ItemsWidgetUtils.loadItemsAndHandleErrors(this, questionDetails.getPrompt(), selectChoiceLoader);\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = SelectMinimalWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n        binding.answer.setTextSize(QuestionFontSizeUtils.getQuestionFontSize());\n        if (prompt.isReadOnly()) {\n            binding.answer.setEnabled(false);\n        } else {\n            binding.answer.setOnClickListener(v -> {\n                waitingForDataRegistry.waitForData(prompt.getIndex());\n                showDialog();\n            });\n        }\n        return binding.getRoot();\n    }\n\n    @Override\n    public void clearAnswer() {\n        binding.answer.setText(org.odk.collect.strings.R.string.select_answer);\n        widgetValueChanged();\n    }\n\n    @Override\n    public int getChoiceCount() {\n        return items.size();\n    }\n\n    protected abstract void showDialog();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectMultiImageMapWidget.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.items;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.webkit.WebView;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectMultiData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning;\n\nimport java.util.List;\n\n/**\n * A widget which is responsible for single select questions represented by\n * an svg map. You can use maps of the world, countries, human body etc.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class SelectMultiImageMapWidget extends SelectImageMapWidget {\n    public SelectMultiImageMapWidget(Context context, QuestionDetails questionDetails, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, questionDetails, selectChoiceLoader, dependencies);\n\n        if (questionDetails.getPrompt().getAnswerValue() != null) {\n            selections = (List<Selection>) getFormEntryPrompt().getAnswerValue().getValue();\n            refreshSelectedItemsLabel();\n        }\n        SpacesInUnderlyingValuesWarning.forQuestionWidget(this).renderWarningIfNecessary(items);\n    }\n\n    @Override\n    protected void highlightSelections(WebView view) {\n        for (Selection selection : selections) {\n            view.loadUrl(\"javascript:addSelectedArea('\" + selection.getValue() + \"')\");\n        }\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return selections.size() == 0 ? null : new SelectMultiData(selections);\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectMultiMinimalWidget.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getPlayColor;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectMultiData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.odk.collect.android.activities.FormFillingActivity;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.fragments.dialogs.SelectMinimalDialog;\nimport org.odk.collect.android.fragments.dialogs.SelectMultiMinimalDialog;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@SuppressLint(\"ViewConstructor\")\npublic class SelectMultiMinimalWidget extends SelectMinimalWidget {\n    private List<Selection> selectedItems;\n\n    public SelectMultiMinimalWidget(Context context, QuestionDetails prompt, WaitingForDataRegistry waitingForDataRegistry, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, prompt, waitingForDataRegistry, selectChoiceLoader, dependencies);\n        render();\n\n        selectedItems = getFormEntryPrompt().getAnswerValue() == null\n                ? new ArrayList<>() :\n                (List<Selection>) getFormEntryPrompt().getAnswerValue().getValue();\n        updateAnswerLabel();\n        SpacesInUnderlyingValuesWarning\n                .forQuestionWidget(this)\n                .renderWarningIfNecessary(items);\n    }\n\n    @Override\n    protected void showDialog() {\n        int numColumns = Appearances.getNumberOfColumns(getFormEntryPrompt(), screenUtils);\n        boolean noButtonsMode = Appearances.isCompactAppearance(getFormEntryPrompt()) || Appearances.isNoButtonsAppearance(getFormEntryPrompt());\n\n        SelectMultiMinimalDialog dialog = new SelectMultiMinimalDialog(new ArrayList<>(selectedItems),\n                Appearances.isFlexAppearance(getFormEntryPrompt()),\n                Appearances.isAutocomplete(getFormEntryPrompt()), getContext(), items,\n                getFormEntryPrompt(), getReferenceManager(),\n                getPlayColor(getFormEntryPrompt(), themeUtils), numColumns, noButtonsMode, mediaUtils);\n\n        DialogFragmentUtils.showIfNotShowing(dialog, SelectMinimalDialog.class, ((FormFillingActivity) getContext()).getSupportFragmentManager());\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return selectedItems.isEmpty()\n                ? null\n                : new SelectMultiData(selectedItems);\n    }\n\n    @Override\n    public void clearAnswer() {\n        selectedItems = new ArrayList<>();\n        super.clearAnswer();\n    }\n\n    @Override\n    public void setData(Object answer) {\n        selectedItems = (List<Selection>) answer;\n        updateAnswerLabel();\n        widgetValueChanged();\n    }\n\n    @Override\n    public void setChoiceSelected(int choiceIndex, boolean isSelected) {\n        if (isSelected) {\n            selectedItems.add(items.get(choiceIndex).selection());\n        } else {\n            selectedItems.remove(items.get(choiceIndex).selection());\n        }\n    }\n\n    private void updateAnswerLabel() {\n        if (selectedItems.isEmpty()) {\n            binding.answer.setText(org.odk.collect.strings.R.string.select_answer);\n        } else {\n            StringBuilder builder = new StringBuilder();\n            for (Selection selectedItem : selectedItems) {\n                builder.append(getFormEntryPrompt().getSelectItemText(selectedItem));\n                if (selectedItems.size() - 1 > selectedItems.indexOf(selectedItem)) {\n                    builder.append(\", \");\n                }\n            }\n            binding.answer.setText(HtmlUtils.textToHtml(builder.toString()));\n        }\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectMultiWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets.items;\n\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getPlayColor;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectMultiData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.odk.collect.android.adapters.AbstractSelectListAdapter;\nimport org.odk.collect.android.adapters.SelectMultipleListAdapter;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * SelectMultiWidget handles multiple selection fields using checkboxes.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n * @author Yaw Anokwa (yanokwa@gmail.com)\n */\n@SuppressLint(\"ViewConstructor\")\npublic class SelectMultiWidget extends BaseSelectListWidget {\n    public SelectMultiWidget(Context context, QuestionDetails prompt, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, prompt, selectChoiceLoader, dependencies);\n        SpacesInUnderlyingValuesWarning\n                .forQuestionWidget(this)\n                .renderWarningIfNecessary(items);\n    }\n\n    @Override\n    protected AbstractSelectListAdapter setUpAdapter() {\n        int numColumns = Appearances.getNumberOfColumns(getFormEntryPrompt(), screenUtils);\n        boolean noButtonsMode = Appearances.isCompactAppearance(getFormEntryPrompt()) || Appearances.isNoButtonsAppearance(getFormEntryPrompt());\n\n        recyclerViewAdapter = new SelectMultipleListAdapter(getSelectedItems(), this, getContext(),\n                items, getFormEntryPrompt(), getReferenceManager(), audioPlayer,\n                getPlayColor(getFormEntryPrompt(), themeUtils), numColumns, noButtonsMode, mediaUtils);\n        return recyclerViewAdapter;\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        List<Selection> selectedItems = recyclerViewAdapter.getSelectedItems();\n        return selectedItems.isEmpty()\n                ? null\n                : new SelectMultiData(selectedItems);\n    }\n\n    @Override\n    public void setChoiceSelected(int choiceIndex, boolean isSelected) {\n        if (isSelected) {\n            ((SelectMultipleListAdapter) recyclerViewAdapter).addItem(items.get(choiceIndex).selection());\n        } else {\n            ((SelectMultipleListAdapter) recyclerViewAdapter).removeItem(items.get(choiceIndex).selection());\n        }\n    }\n\n    private List<Selection> getSelectedItems() {\n        return getFormEntryPrompt().getAnswerValue() == null\n                ? new ArrayList<>() :\n                (List<Selection>) getFormEntryPrompt().getAnswerValue().getValue();\n    }\n\n    @Override\n    public void onItemClicked() {\n        widgetValueChanged();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragment.kt",
    "content": "package org.odk.collect.android.widgets.items\n\nimport android.content.Context\nimport android.content.res.Resources\nimport androidx.activity.ComponentDialog\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModelProvider\nimport org.javarosa.core.model.SelectChoice\nimport org.javarosa.core.model.data.SelectOneData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.utilities.Appearances\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils\nimport org.odk.collect.android.widgets.utilities.WidgetAnswerDialogFragment\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.entities.javarosa.parse.EntitySchema\nimport org.odk.collect.geo.geopoly.GeoPolyUtils.parseGeometry\nimport org.odk.collect.geo.selection.IconifiedText\nimport org.odk.collect.geo.selection.MappableSelectItem\nimport org.odk.collect.geo.selection.SelectionMapData\nimport org.odk.collect.geo.selection.SelectionMapFragment\nimport org.odk.collect.geo.selection.SelectionMapFragment.Companion.REQUEST_SELECT_ITEM\nimport javax.inject.Inject\n\nclass SelectOneFromMapDialogFragment(viewModelFactory: ViewModelProvider.Factory) :\n    WidgetAnswerDialogFragment<SelectionMapFragment>(\n        SelectionMapFragment::class,\n        viewModelFactory\n    ) {\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        DaggerUtils.getComponent(context).inject(this)\n    }\n\n    override fun onCreateFragment(prompt: FormEntryPrompt): SelectionMapFragment {\n        childFragmentManager.setFragmentResultListener(REQUEST_SELECT_ITEM, this) { _, result ->\n            val selectedIndex = result.getLong(SelectionMapFragment.RESULT_SELECTED_ITEM).toInt()\n            val selectedChoice = prompt.selectChoices[selectedIndex]\n            onAnswer(SelectOneData(selectedChoice.selection()))\n        }\n\n        val selectedIndex = requireArguments().getSerializable(ARG_SELECTED_INDEX) as Int?\n        return SelectionMapFragment(\n            SelectChoicesMapData(this.resources, scheduler, prompt, selectedIndex),\n            skipSummary = Appearances.hasAppearance(prompt, Appearances.QUICK),\n            zoomToFitItems = false,\n            showNewItemButton = false,\n            onBackPressedDispatcher = { (requireDialog() as ComponentDialog).onBackPressedDispatcher }\n        )\n    }\n\n    companion object {\n        const val ARG_SELECTED_INDEX = \"selected_index\"\n    }\n}\n\ninternal class SelectChoicesMapData(\n    private val resources: Resources,\n    scheduler: Scheduler,\n    prompt: FormEntryPrompt,\n    private val selectedIndex: Int?\n) : SelectionMapData {\n\n    private val mapTitle = MutableLiveData(prompt.longText)\n    private val itemCount = MutableNonNullLiveData(0)\n    private val items = MutableLiveData<List<MappableSelectItem>?>(null)\n    private val isLoading = MutableNonNullLiveData(true)\n\n    init {\n        isLoading.value = true\n\n        scheduler.immediate(\n            background = {\n                loadItemsFromChoices(prompt.selectChoices, prompt)\n            },\n            foreground = {\n                itemCount.value = prompt.selectChoices.size\n                items.value = it\n                isLoading.value = false\n            }\n        )\n    }\n\n    private fun loadItemsFromChoices(\n        selectChoices: MutableList<SelectChoice>,\n        prompt: FormEntryPrompt\n    ): List<MappableSelectItem> {\n        return selectChoices.foldIndexed(emptyList()) { index, list, selectChoice ->\n            val geometry = selectChoice.getChild(GEOMETRY)\n\n            if (geometry != null) {\n                try {\n                    val points = parseGeometry(geometry)\n                    if (points.isNotEmpty()) {\n                        val withinBounds = points.all {\n                            GeoWidgetUtils.isWithinMapBounds(it)\n                        }\n\n                        if (withinBounds) {\n                            val properties = selectChoice.additionalChildren.filterNot {\n                                FILTERED_PROPERTIES.contains(it.first)\n                            }.map {\n                                IconifiedText(null, \"${it.first}: ${it.second}\")\n                            }\n\n                            if (points.size == 1) {\n                                val markerColor =\n                                    getPropertyValue(selectChoice, MARKER_COLOR)\n                                val markerSymbol =\n                                    getPropertyValue(selectChoice, MARKER_SYMBOL)\n\n                                list + MappableSelectItem.MappableSelectPoint(\n                                    index.toLong(),\n                                    prompt.getSelectChoiceText(selectChoice),\n                                    properties,\n                                    selectChoice.index == selectedIndex,\n                                    point = points[0],\n                                    smallIcon = if (markerSymbol.isNullOrBlank()) org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small else org.odk.collect.icons.R.drawable.ic_map_marker_small,\n                                    largeIcon = if (markerSymbol.isNullOrBlank()) org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big else org.odk.collect.icons.R.drawable.ic_map_marker_big,\n                                    color = markerColor,\n                                    symbol = markerSymbol,\n                                    action = IconifiedText(\n                                        org.odk.collect.icons.R.drawable.ic_save,\n                                        resources.getString(org.odk.collect.strings.R.string.select_item)\n                                    )\n                                )\n                            } else if (points.first() != points.last()) {\n                                list + MappableSelectItem.MappableSelectLine(\n                                    index.toLong(),\n                                    prompt.getSelectChoiceText(selectChoice),\n                                    properties,\n                                    selectChoice.index == selectedIndex,\n                                    points = points,\n                                    action = IconifiedText(\n                                        org.odk.collect.icons.R.drawable.ic_save,\n                                        resources.getString(org.odk.collect.strings.R.string.select_item)\n                                    ),\n                                    strokeWidth = getPropertyValue(selectChoice, STROKE_WIDTH),\n                                    strokeColor = getPropertyValue(selectChoice, STROKE)\n                                )\n                            } else {\n                                list + MappableSelectItem.MappableSelectPolygon(\n                                    index.toLong(),\n                                    prompt.getSelectChoiceText(selectChoice),\n                                    properties,\n                                    selectChoice.index == selectedIndex,\n                                    points = points,\n                                    action = IconifiedText(\n                                        org.odk.collect.icons.R.drawable.ic_save,\n                                        resources.getString(org.odk.collect.strings.R.string.select_item)\n                                    ),\n                                    strokeWidth = getPropertyValue(selectChoice, STROKE_WIDTH),\n                                    strokeColor = getPropertyValue(selectChoice, STROKE),\n                                    fillColor = getPropertyValue(selectChoice, FILL)\n                                )\n                            }\n                        } else {\n                            list\n                        }\n                    } else {\n                        list\n                    }\n                } catch (_: NumberFormatException) {\n                    list\n                }\n            } else {\n                list\n            }\n        }\n    }\n\n    private fun getPropertyValue(selectChoice: SelectChoice, propertyName: String): String? {\n        return selectChoice.additionalChildren.firstOrNull { it.first == propertyName }?.second\n    }\n\n    override fun isLoading(): NonNullLiveData<Boolean> {\n        return isLoading\n    }\n\n    override fun getMapTitle(): LiveData<String?> {\n        return mapTitle\n    }\n\n    override fun getItemType(): String {\n        return resources.getString(org.odk.collect.strings.R.string.choices)\n    }\n\n    override fun getItemCount(): NonNullLiveData<Int> {\n        return itemCount\n    }\n\n    override fun getMappableItems(): LiveData<List<MappableSelectItem>?> {\n        return items\n    }\n\n    companion object PropertyNames {\n        const val GEOMETRY = \"geometry\"\n        const val MARKER_COLOR = \"marker-color\"\n        const val MARKER_SYMBOL = \"marker-symbol\"\n        const val STROKE = \"stroke\"\n        const val STROKE_WIDTH = \"stroke-width\"\n        const val FILL = \"fill\"\n\n        private val FILTERED_PROPERTIES = arrayOf(\n            GEOMETRY,\n            MARKER_COLOR,\n            MARKER_SYMBOL,\n            STROKE,\n            STROKE_WIDTH,\n            FILL,\n            EntitySchema.VERSION,\n            EntitySchema.TRUNK_VERSION,\n            EntitySchema.BRANCH_ID\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectOneFromMapWidget.kt",
    "content": "package org.odk.collect.android.widgets.items\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.os.Bundle\nimport android.util.TypedValue\nimport android.view.LayoutInflater\nimport android.view.View\nimport androidx.fragment.app.FragmentActivity\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.core.model.data.SelectOneData\nimport org.javarosa.core.model.data.helper.Selection\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.databinding.SelectOneFromMapWidgetAnswerBinding\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.widgets.QuestionWidget\nimport org.odk.collect.android.widgets.items.SelectOneFromMapDialogFragment.Companion.ARG_SELECTED_INDEX\nimport org.odk.collect.android.widgets.utilities.WidgetAnswerDialogFragment.Companion.ARG_FORM_INDEX\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.permissions.PermissionListener\n\n@SuppressLint(\"ViewConstructor\")\nclass SelectOneFromMapWidget(\n    context: Context,\n    questionDetails: QuestionDetails,\n    dependencies: Dependencies\n) : QuestionWidget(context, dependencies, questionDetails) {\n\n    init {\n        render()\n    }\n\n    lateinit var binding: SelectOneFromMapWidgetAnswerBinding\n    private var answer: SelectOneData? = null\n\n    override fun onCreateWidgetView(\n        context: Context,\n        prompt: FormEntryPrompt,\n        answerFontSize: Int\n    ): View {\n        binding = SelectOneFromMapWidgetAnswerBinding.inflate(LayoutInflater.from(context))\n\n        binding.button.setOnClickListener {\n            permissionsProvider.requestEnabledLocationPermissions(\n                context as Activity,\n                object : PermissionListener {\n                    override fun granted() {\n                        DialogFragmentUtils.showIfNotShowing(\n                            SelectOneFromMapDialogFragment::class.java,\n                            Bundle().also {\n                                it.putSerializable(ARG_FORM_INDEX, prompt.index)\n                                (answer?.value as? Selection)?.index?.let { index ->\n                                    it.putInt(ARG_SELECTED_INDEX, index)\n                                }\n                            },\n                            (context as FragmentActivity).supportFragmentManager\n                        )\n                    }\n                }\n            )\n        }\n\n        binding.answer.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize.toFloat())\n        updateAnswer(questionDetails.prompt.answerValue as? SelectOneData)\n\n        return binding.root\n    }\n\n    override fun getAnswer(): IAnswerData? {\n        return answer\n    }\n\n    override fun clearAnswer() {\n        updateAnswer(null)\n        widgetValueChanged()\n    }\n\n    override fun setOnLongClickListener(l: OnLongClickListener?) {}\n\n    private fun updateAnswer(answer: SelectOneData?) {\n        this.answer = answer\n\n        binding.answer.text = if (answer != null) {\n            val choice = (answer.value as Selection).choice\n            formEntryPrompt.getSelectChoiceText(choice)\n        } else {\n            \"\"\n        }\n        if (binding.answer.text.isBlank()) {\n            binding.answer.visibility = GONE\n        } else {\n            binding.answer.visibility = VISIBLE\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectOneImageMapWidget.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.items;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.webkit.WebView;\n\nimport androidx.annotation.Nullable;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.AdvanceToNextListener;\nimport org.odk.collect.android.utilities.SelectOneWidgetUtils;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\n\n/**\n * A widget which is responsible for multi select questions represented by\n * an svg map. You can use maps of the world, countries, human body etc.\n */\n@SuppressLint(\"ViewConstructor\")\npublic class SelectOneImageMapWidget extends SelectImageMapWidget {\n\n    @Nullable\n    private AdvanceToNextListener listener;\n\n    private final boolean autoAdvance;\n\n    public SelectOneImageMapWidget(Context context, QuestionDetails questionDetails, boolean autoAdvance, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, questionDetails, selectChoiceLoader, dependencies);\n\n        this.autoAdvance = autoAdvance;\n\n        if (context instanceof AdvanceToNextListener) {\n            listener = (AdvanceToNextListener) context;\n        }\n\n        if (questionDetails.getPrompt().getAnswerValue() != null) {\n\n            selections.add(SelectOneWidgetUtils.getSelectedItem(questionDetails.getPrompt(), items));\n            refreshSelectedItemsLabel();\n        }\n    }\n\n    @Override\n    protected void highlightSelections(WebView view) {\n        if (!selections.isEmpty()) {\n            view.loadUrl(\"javascript:addSelectedArea('\" + selections.get(0).getValue() + \"')\");\n        }\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return selections.isEmpty() ? null\n                : new SelectOneData(selections.get(0));\n    }\n\n    @Override\n    protected void selectArea(String areaId) {\n        super.selectArea(areaId);\n\n        ((Activity) getContext()).runOnUiThread(() -> {\n            if (autoAdvance && listener != null) {\n                listener.advance();\n            }\n        });\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectOneMinimalWidget.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getPlayColor;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.odk.collect.android.activities.FormFillingActivity;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.fragments.dialogs.SelectMinimalDialog;\nimport org.odk.collect.android.fragments.dialogs.SelectOneMinimalDialog;\nimport org.odk.collect.android.listeners.AdvanceToNextListener;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.HtmlUtils;\nimport org.odk.collect.android.utilities.SelectOneWidgetUtils;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\n\nimport java.util.List;\n\n@SuppressLint(\"ViewConstructor\")\npublic class SelectOneMinimalWidget extends SelectMinimalWidget {\n    private Selection selectedItem;\n    private final boolean autoAdvance;\n    private AdvanceToNextListener autoAdvanceListener;\n\n    public SelectOneMinimalWidget(Context context, QuestionDetails prompt, boolean autoAdvance, WaitingForDataRegistry waitingForDataRegistry, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, prompt, waitingForDataRegistry, selectChoiceLoader, dependencies);\n        render();\n\n        selectedItem = SelectOneWidgetUtils.getSelectedItem(prompt.getPrompt(), items);\n        this.autoAdvance = autoAdvance;\n        if (context instanceof AdvanceToNextListener) {\n            autoAdvanceListener = (AdvanceToNextListener) context;\n        }\n        updateAnswer();\n    }\n\n    @Override\n    protected void showDialog() {\n        int numColumns = Appearances.getNumberOfColumns(getFormEntryPrompt(), screenUtils);\n        boolean noButtonsMode = Appearances.isCompactAppearance(getFormEntryPrompt()) || Appearances.isNoButtonsAppearance(getFormEntryPrompt());\n\n        SelectOneMinimalDialog dialog = new SelectOneMinimalDialog(getSavedSelectedValue(),\n                Appearances.isFlexAppearance(getFormEntryPrompt()),\n                Appearances.isAutocomplete(getFormEntryPrompt()), getContext(), items,\n                getFormEntryPrompt(), getReferenceManager(),\n                getPlayColor(getFormEntryPrompt(), themeUtils), numColumns, noButtonsMode, mediaUtils);\n\n        DialogFragmentUtils.showIfNotShowing(dialog, SelectMinimalDialog.class, ((FormFillingActivity) getContext()).getSupportFragmentManager());\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return selectedItem == null\n                ? null\n                : new SelectOneData(selectedItem);\n    }\n\n    @Override\n    public void clearAnswer() {\n        selectedItem = null;\n        super.clearAnswer();\n    }\n\n    @Override\n    public void setData(Object answer) {\n        List<Selection> answers = (List<Selection>) answer;\n        selectedItem = answers.isEmpty() ? null : answers.get(0);\n        updateAnswer();\n        widgetValueChanged();\n\n        if (autoAdvance && autoAdvanceListener != null) {\n            autoAdvanceListener.advance();\n        }\n    }\n\n    @Override\n    public void setChoiceSelected(int choiceIndex, boolean isSelected) {\n        selectedItem = isSelected\n                ? items.get(choiceIndex).selection()\n                : null;\n    }\n\n    private void updateAnswer() {\n        if (selectedItem == null) {\n            binding.answer.setText(org.odk.collect.strings.R.string.select_answer);\n        } else {\n            binding.answer.setText(HtmlUtils.textToHtml(getFormEntryPrompt().getSelectItemText(selectedItem)));\n        }\n    }\n\n    private String getSavedSelectedValue() {\n        return selectedItem == null\n                ? null\n                : selectedItem.getValue();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectOneWidget.java",
    "content": "/*\n * Copyright (C) 2009 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.android.widgets.items;\n\nimport static org.odk.collect.android.formentry.media.FormMediaUtils.getPlayColor;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.widget.RadioButton;\n\nimport androidx.annotation.Nullable;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.odk.collect.android.adapters.AbstractSelectListAdapter;\nimport org.odk.collect.android.adapters.SelectOneListAdapter;\nimport org.odk.collect.android.exception.JavaRosaException;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.listeners.AdvanceToNextListener;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.SelectOneWidgetUtils;\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader;\n\nimport timber.log.Timber;\n\n/**\n * SelectOneWidgets handles select-one fields using radio buttons.\n *\n * @author Carl Hartung (carlhartung@gmail.com)\n * @author Yaw Anokwa (yanokwa@gmail.com)\n */\n@SuppressLint(\"ViewConstructor\")\npublic class SelectOneWidget extends BaseSelectListWidget {\n\n    @Nullable\n    private AdvanceToNextListener listener;\n\n    private final boolean autoAdvance;\n    private final FormController formController;\n\n    public SelectOneWidget(Context context, QuestionDetails questionDetails, boolean autoAdvance, FormController formController, SelectChoiceLoader selectChoiceLoader, Dependencies dependencies) {\n        super(context, questionDetails, selectChoiceLoader, dependencies);\n        this.autoAdvance = autoAdvance;\n        this.formController = formController;\n        if (context instanceof AdvanceToNextListener) {\n            listener = (AdvanceToNextListener) context;\n        }\n    }\n\n    @Override\n    protected AbstractSelectListAdapter setUpAdapter() {\n        int numColumns = Appearances.getNumberOfColumns(getFormEntryPrompt(), screenUtils);\n        boolean noButtonsMode = Appearances.isCompactAppearance(getFormEntryPrompt()) || Appearances.isNoButtonsAppearance(getFormEntryPrompt());\n\n        recyclerViewAdapter = new SelectOneListAdapter(getSelectedValue(), this, getContext(), items,\n                getFormEntryPrompt(), getReferenceManager(), audioPlayer,\n                getPlayColor(getFormEntryPrompt(), themeUtils), numColumns, noButtonsMode, mediaUtils);\n        return recyclerViewAdapter;\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        Selection selectedItem = ((SelectOneListAdapter) recyclerViewAdapter).getSelectedItem();\n        return selectedItem == null\n                ? null\n                : new SelectOneData(selectedItem);\n    }\n\n    protected String getSelectedValue() {\n        Selection selectedItem = SelectOneWidgetUtils.getSelectedItem(getQuestionDetails().getPrompt(), items);\n        return selectedItem == null ? null : selectedItem.getValue();\n    }\n\n    @Override\n    public void setChoiceSelected(int choiceIndex, boolean isSelected) {\n        RadioButton button = new RadioButton(getContext());\n        button.setTag(choiceIndex);\n        button.setChecked(isSelected);\n\n        ((SelectOneListAdapter) recyclerViewAdapter).onCheckedChanged(button, isSelected);\n    }\n\n    @Override\n    public void onItemClicked() {\n        if (autoAdvance && listener != null) {\n            listener.advance();\n        }\n\n        clearFollowingItemsetWidgets();\n        widgetValueChanged();\n    }\n\n    @Override\n    public void clearAnswer() {\n        clearFollowingItemsetWidgets();\n        super.clearAnswer();\n    }\n\n    /**\n     * If there are \"fast external itemset\" selects right after this select, assume that they are linked to the current question and clear them.\n     */\n    private void clearFollowingItemsetWidgets() {\n        if (formController == null) {\n            return;\n        }\n\n        if (formController.currentCaptionPromptIsQuestion()) {\n            try {\n                FormIndex startFormIndex = formController.getQuestionPrompt().getIndex();\n                formController.stepToNextScreenEvent();\n                while (formController.currentCaptionPromptIsQuestion()\n                        && formController.getQuestionPrompt().getFormElement().getAdditionalAttribute(null, \"query\") != null) {\n                    formController.saveAnswer(formController.getQuestionPrompt().getIndex(), null);\n                    formController.stepToNextScreenEvent();\n                }\n                formController.jumpToIndex(startFormIndex);\n            } catch (JavaRosaException e) {\n                Timber.d(e);\n            }\n        }\n    }\n\n    public void setListener(AdvanceToNextListener listener) {\n        this.listener = listener;\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/range/RangeDecimalWidget.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.range;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport com.google.android.material.slider.Slider;\n\nimport org.javarosa.core.model.data.DecimalData;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.views.TrackingTouchSlider;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.utilities.RangeWidgetUtils;\n\nimport java.math.BigDecimal;\n\n@SuppressLint(\"ViewConstructor\")\npublic class RangeDecimalWidget extends QuestionWidget implements Slider.OnChangeListener {\n    TrackingTouchSlider slider;\n    TextView currentValue;\n\n    public RangeDecimalWidget(Context context, QuestionDetails prompt, Dependencies dependencies) {\n        super(context, dependencies, prompt);\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        RangeWidgetUtils.RangeWidgetLayoutElements layoutElements = RangeWidgetUtils.setUpLayoutElements(context, prompt);\n        slider = layoutElements.getSlider();\n        currentValue = layoutElements.getCurrentValue();\n\n        setUpActualValueLabel(RangeWidgetUtils.setUpSlider(prompt, slider, false));\n\n        if (slider.isEnabled()) {\n            slider.setListener(this);\n        }\n        return layoutElements.getAnswerView();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        String stringAnswer = currentValue.getText().toString();\n        return stringAnswer.isEmpty() ? null : new DecimalData(Double.parseDouble(stringAnswer));\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n    }\n\n    @Override\n    public boolean shouldSuppressFlingGesture() {\n        return slider.isTrackingTouch();\n    }\n\n    @Override\n    public void clearAnswer() {\n        setUpActualValueLabel(null);\n        widgetValueChanged();\n    }\n\n    @SuppressLint(\"RestrictedApi\")\n    @Override\n    public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {\n        if (fromUser) {\n            BigDecimal actualValue = RangeWidgetUtils.getActualValue(getFormEntryPrompt(), value);\n            setUpActualValueLabel(actualValue);\n            widgetValueChanged();\n        }\n    }\n\n    private void setUpActualValueLabel(BigDecimal actualValue) {\n        if (actualValue != null) {\n            currentValue.setText(String.valueOf(actualValue.doubleValue()));\n        } else {\n            currentValue.setText(\"\");\n            slider.reset();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/range/RangeIntegerWidget.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.range;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport com.google.android.material.slider.Slider;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.IntegerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.views.TrackingTouchSlider;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.utilities.RangeWidgetUtils;\n\nimport java.math.BigDecimal;\n\n@SuppressLint(\"ViewConstructor\")\npublic class RangeIntegerWidget extends QuestionWidget implements Slider.OnChangeListener {\n    TrackingTouchSlider slider;\n    TextView currentValue;\n\n    public RangeIntegerWidget(Context context, QuestionDetails prompt, Dependencies dependencies) {\n        super(context, dependencies, prompt);\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        RangeWidgetUtils.RangeWidgetLayoutElements layoutElements = RangeWidgetUtils.setUpLayoutElements(context, prompt);\n        slider = layoutElements.getSlider();\n        currentValue = layoutElements.getCurrentValue();\n\n        setUpActualValueLabel(RangeWidgetUtils.setUpSlider(prompt, slider, true));\n\n        if (slider.isEnabled()) {\n            slider.setListener(this);\n        }\n        return layoutElements.getAnswerView();\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        String stringAnswer = currentValue.getText().toString();\n        return stringAnswer.isEmpty() ? null : new IntegerData(Integer.parseInt(stringAnswer));\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n    }\n\n    @Override\n    public boolean shouldSuppressFlingGesture() {\n        return slider.isTrackingTouch();\n    }\n\n    @Override\n    public void clearAnswer() {\n        setUpActualValueLabel(null);\n        widgetValueChanged();\n    }\n\n    @SuppressLint(\"RestrictedApi\")\n    @Override\n    public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {\n        if (fromUser) {\n            BigDecimal actualValue = RangeWidgetUtils.getActualValue(getFormEntryPrompt(), value);\n            setUpActualValueLabel(actualValue);\n            widgetValueChanged();\n        }\n    }\n\n    private void setUpActualValueLabel(BigDecimal actualValue) {\n        if (actualValue != null) {\n            currentValue.setText(String.valueOf(actualValue.intValue()));\n        } else {\n            currentValue.setText(\"\");\n            slider.reset();\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/range/RangePickerWidget.java",
    "content": "package org.odk.collect.android.widgets.range;\n\nimport static org.odk.collect.android.fragments.dialogs.RangePickerDialogFragment.ARG_DECIMAL;\nimport static org.odk.collect.android.fragments.dialogs.RangePickerDialogFragment.ARG_FORM_INDEX;\nimport static org.odk.collect.android.fragments.dialogs.RangePickerDialogFragment.ARG_SELECTED;\nimport static org.odk.collect.android.fragments.dialogs.RangePickerDialogFragment.ARG_VALUES;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.util.TypedValue;\nimport android.view.View;\n\nimport androidx.fragment.app.FragmentActivity;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.RangePickerWidgetAnswerBinding;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.fragments.dialogs.RangePickerDialogFragment;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.utilities.RangeWidgetUtils;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\n\npublic class RangePickerWidget extends QuestionWidget {\n\n    private final boolean decimal;\n    RangePickerWidgetAnswerBinding binding;\n\n    private int progress;\n\n    private IAnswerData answer;\n\n    public RangePickerWidget(Context context, QuestionDetails questionDetails, Dependencies dependencies, boolean decimal) {\n        super(context, dependencies, questionDetails);\n        this.decimal = decimal;\n\n        answer = questionDetails.getPrompt().getAnswerValue();\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerFontSize) {\n        binding = RangePickerWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());\n\n        binding.widgetAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);\n\n        String[] displayedValuesForNumberPicker = RangePickerWidgetUtils.getNumbersFromRangeAsc(prompt);\n        RangeWidgetUtils.setUpRangePickerWidget(context, binding, prompt);\n\n        progress = RangePickerWidgetUtils.getProgressFromPrompt(prompt, displayedValuesForNumberPicker);\n        binding.widgetButton.setOnClickListener(v -> {\n            Bundle args = new Bundle();\n            args.putSerializable(ARG_FORM_INDEX, prompt.getIndex());\n            args.putInt(ARG_SELECTED, progress);\n            args.putSerializable(ARG_VALUES, displayedValuesForNumberPicker);\n            args.putBoolean(ARG_DECIMAL, decimal);\n            DialogFragmentUtils.showIfNotShowing(RangePickerDialogFragment.class, args, ((FragmentActivity) context).getSupportFragmentManager());\n        });\n\n        return binding.getRoot();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        binding.widgetButton.setOnLongClickListener(l);\n        binding.widgetAnswerText.setOnLongClickListener(l);\n    }\n\n    @Override\n    public IAnswerData getAnswer() {\n        return answer;\n    }\n\n    @Override\n    public void clearAnswer() {\n        setUpNullValue();\n        widgetValueChanged();\n    }\n\n    private void setUpNullValue() {\n        answer = null;\n        progress = 0;\n        binding.widgetAnswerText.setText(getContext().getString(org.odk.collect.strings.R.string.no_value_selected));\n        binding.widgetButton.setText(getContext().getString(org.odk.collect.strings.R.string.select_value));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/range/RangePickerWidgetUtils.kt",
    "content": "package org.odk.collect.android.widgets.range\n\nimport org.javarosa.core.model.Constants.DATATYPE_INTEGER\nimport org.javarosa.core.model.RangeQuestion\nimport org.javarosa.form.api.FormEntryPrompt\nimport java.math.BigDecimal\n\nobject RangePickerWidgetUtils {\n\n    @JvmStatic\n    fun getNumbersFromRangeAsc(formEntryPrompt: FormEntryPrompt): Array<String> {\n        val rangeQuestion = formEntryPrompt.question as RangeQuestion\n        val rangeStart = rangeQuestion.rangeStart\n        val rangeEnd = rangeQuestion.rangeEnd\n        val rangeStep = rangeQuestion.rangeStep.abs()\n\n        val displayedValuesForNumberPicker = mutableListOf<String>()\n        var index = 0\n        var firstElement = if (rangeStart.compareTo(rangeEnd) < 1) rangeStart else rangeEnd\n        val lastElement = if (rangeStart.compareTo(rangeEnd) < 1) rangeEnd else rangeStart\n        while (firstElement.compareTo(lastElement) < 1) {\n            displayedValuesForNumberPicker.add(\n                if (formEntryPrompt.dataType == DATATYPE_INTEGER) {\n                    firstElement.toInt().toString()\n                } else {\n                    firstElement.toDouble().toString()\n                }\n            )\n            index++\n            firstElement = firstElement.plus(rangeStep.abs())\n        }\n        return displayedValuesForNumberPicker.toTypedArray<String>()\n    }\n\n    @JvmStatic\n    fun getProgressFromPrompt(prompt: FormEntryPrompt, listOfValues: Array<String>): Int {\n        var actualValue: BigDecimal? = null\n        val answerValue = prompt.answerValue\n        if (answerValue != null) {\n            actualValue = BigDecimal(answerValue.value.toString())\n        }\n        var progress = 0\n        if (actualValue != null) {\n            progress = listOfValues.indexOf(actualValue.toString())\n        }\n        return if (progress == -1) 0 else progress\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/ActivityGeoDataRequester.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.core.os.bundleOf\nimport androidx.fragment.app.FragmentActivity\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.utilities.Appearances\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.android.utilities.FormEntryPromptUtils\nimport org.odk.collect.android.widgets.interfaces.GeoDataRequester\nimport org.odk.collect.android.widgets.utilities.BindAttributes.ALLOW_MOCK_ACCURACY\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.geo.Constants.EXTRA_DRAGGABLE_ONLY\nimport org.odk.collect.geo.Constants.EXTRA_READ_ONLY\nimport org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY\nimport org.odk.collect.geo.geopoint.GeoPointActivity\nimport org.odk.collect.geo.geopoint.GeoPointMapActivity\nimport org.odk.collect.geo.geopoly.GeoPolyUtils.parseGeometry\nimport org.odk.collect.permissions.PermissionListener\nimport org.odk.collect.permissions.PermissionsProvider\nimport java.lang.Boolean.parseBoolean\n\nclass ActivityGeoDataRequester(\n    private val permissionsProvider: PermissionsProvider,\n    private val activity: FragmentActivity\n) : GeoDataRequester {\n\n    override fun requestGeoPoint(\n        prompt: FormEntryPrompt,\n        answerText: String?,\n        waitingForDataRegistry: WaitingForDataRegistry\n    ) {\n        permissionsProvider.requestEnabledLocationPermissions(\n            activity,\n            object : PermissionListener {\n                override fun granted() {\n                    waitingForDataRegistry.waitForData(prompt.index)\n\n                    val bundle = Bundle().also {\n                        val parsedGeometry = parseGeometry(answerText)\n                        if (parsedGeometry.isNotEmpty()) {\n                            it.putParcelable(\n                                GeoPointMapActivity.EXTRA_LOCATION,\n                                parsedGeometry[0]\n                            )\n                        }\n\n                        val accuracyThreshold =\n                            FormEntryPromptUtils.getAdditionalAttribute(prompt, \"accuracyThreshold\")\n                        val unacceptableAccuracyThreshold =\n                            FormEntryPromptUtils.getAdditionalAttribute(\n                                prompt,\n                                \"unacceptableAccuracyThreshold\"\n                            )\n\n                        it.putFloat(\n                            GeoPointActivity.EXTRA_ACCURACY_THRESHOLD,\n                            accuracyThreshold?.toFloatOrNull() ?: DEFAULT_ACCURACY_THRESHOLD\n                        )\n\n                        it.putFloat(\n                            GeoPointActivity.EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD,\n                            unacceptableAccuracyThreshold?.toFloatOrNull()\n                                ?: DEFAULT_UNACCEPTABLE_ACCURACY_THRESHOLD\n                        )\n\n                        it.putBoolean(EXTRA_RETAIN_MOCK_ACCURACY, getAllowMockAccuracy(prompt))\n                        it.putBoolean(EXTRA_READ_ONLY, prompt.isReadOnly)\n                        it.putBoolean(EXTRA_DRAGGABLE_ONLY, hasPlacementMapAppearance(prompt))\n                    }\n\n                    val intent = Intent(\n                        activity,\n                        if (isMapsAppearance(prompt)) GeoPointMapActivity::class.java else GeoPointActivity::class.java\n                    ).also {\n                        it.putExtras(bundle)\n                    }\n\n                    activity.startActivityForResult(\n                        intent,\n                        ApplicationConstants.RequestCodes.LOCATION_CAPTURE\n                    )\n                }\n            }\n        )\n    }\n\n    override fun requestGeoPoly(prompt: FormEntryPrompt) {\n        permissionsProvider.requestEnabledLocationPermissions(\n            activity,\n            object : PermissionListener {\n                override fun granted() {\n                    DialogFragmentUtils.showIfNotShowing(\n                        GeoPolyDialogFragment::class.java,\n                        bundleOf(WidgetAnswerDialogFragment.ARG_FORM_INDEX to prompt.index),\n                        activity.supportFragmentManager\n                    )\n                }\n            }\n        )\n    }\n\n    private fun getAllowMockAccuracy(prompt: FormEntryPrompt): Boolean {\n        return parseBoolean(\n            FormEntryPromptUtils.getBindAttribute(\n                prompt,\n                ALLOW_MOCK_ACCURACY\n            )\n        )\n    }\n\n    private fun isMapsAppearance(prompt: FormEntryPrompt): Boolean {\n        return hasMapsAppearance(prompt) || hasPlacementMapAppearance(prompt)\n    }\n\n    private fun hasMapsAppearance(prompt: FormEntryPrompt): Boolean {\n        return Appearances.hasAppearance(prompt, Appearances.MAPS)\n    }\n\n    private fun hasPlacementMapAppearance(prompt: FormEntryPrompt): Boolean {\n        return Appearances.hasAppearance(prompt, Appearances.PLACEMENT_MAP)\n    }\n\n    companion object {\n        const val DEFAULT_ACCURACY_THRESHOLD = 5f\n        const val DEFAULT_UNACCEPTABLE_ACCURACY_THRESHOLD = 100f\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/AdditionalAttributes.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nobject AdditionalAttributes {\n    const val INCREMENTAL = \"incremental\"\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/AudioFileRequester.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport org.javarosa.form.api.FormEntryPrompt;\n\npublic interface AudioFileRequester {\n    void requestFile(FormEntryPrompt prompt);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/AudioRecorderRecordingStatusHandler.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport android.util.Pair;\n\nimport androidx.lifecycle.LifecycleOwner;\n\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.formentry.FormEntryViewModel;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\n\nimport java.util.function.Consumer;\n\npublic class AudioRecorderRecordingStatusHandler implements RecordingStatusHandler {\n\n    private final AudioRecorder audioRecorder;\n    private final FormEntryViewModel formEntryViewModel;\n    private final LifecycleOwner lifecycleOwner;\n\n    public AudioRecorderRecordingStatusHandler(AudioRecorder audioRecorder, FormEntryViewModel formEntryViewModel, LifecycleOwner lifecycleOwner) {\n        this.audioRecorder = audioRecorder;\n        this.formEntryViewModel = formEntryViewModel;\n        this.lifecycleOwner = lifecycleOwner;\n    }\n\n    @Override\n    public void onBlockedStatusChange(Consumer<Boolean> blockedStatusListener) {\n        audioRecorder.getCurrentSession().observe(lifecycleOwner, session -> {\n            if (formEntryViewModel.hasBackgroundRecording().getValue()) {\n                blockedStatusListener.accept(true);\n            } else {\n                blockedStatusListener.accept(session != null && session.getFile() == null);\n            }\n        });\n    }\n\n    @Override\n    public void onRecordingStatusChange(FormEntryPrompt prompt, Consumer<Pair<Long, Integer>> statusListener) {\n        audioRecorder.getCurrentSession().observe(lifecycleOwner, session -> {\n            if (session != null && session.getId().equals(prompt.getIndex())) {\n                statusListener.accept(new Pair<>(session.getDuration(), session.getAmplitude()));\n            } else {\n                statusListener.accept(null);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/BindAttributes.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nobject BindAttributes {\n    const val ALLOW_MOCK_ACCURACY = \"allow-mock-accuracy\"\n    const val QUALITY = \"quality\"\n\n    enum class Quality(val value: String) {\n        NORMAL(\"normal\"),\n        LOW(\"low\"),\n        VOICE_ONLY(\"voice-only\"),\n        EXTERNAL(\"external\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/DateTimeWidgetUtils.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport android.content.Context;\nimport android.os.Bundle;\n\nimport androidx.fragment.app.FragmentActivity;\n\nimport org.joda.time.DateTime;\nimport org.joda.time.LocalDateTime;\nimport org.joda.time.chrono.BuddhistChronology;\nimport org.joda.time.chrono.CopticChronology;\nimport org.joda.time.chrono.EthiopicChronology;\nimport org.joda.time.chrono.IslamicChronology;\nimport org.joda.time.chrono.PersianChronologyKhayyamBorkowski;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.widgets.datetime.pickers.BikramSambatDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.BuddhistDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.CopticDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.CustomTimePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.EthiopianDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.FixedDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.IslamicDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.MyanmarDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.PersianDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.MyanmarDateUtils;\nimport org.odk.collect.android.utilities.ThemeUtils;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\n\nimport java.text.DateFormat;\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Locale;\n\nimport bikramsambat.BikramSambatDate;\nimport bikramsambat.BsCalendar;\nimport bikramsambat.BsException;\nimport bikramsambat.BsGregorianDate;\nimport mmcalendar.MyanmarDate;\nimport timber.log.Timber;\n\npublic class DateTimeWidgetUtils {\n    public static final String DATE = \"date\";\n    public static final String TIME = \"time\";\n    public static final String DIALOG_THEME = \"dialog_theme\";\n    public static final String DATE_PICKER_DETAILS = \"date_picker_details\";\n\n    public static DatePickerDetails getDatePickerDetails(String appearance) {\n        DatePickerDetails.DatePickerType datePickerType = DatePickerDetails.DatePickerType.GREGORIAN;\n        DatePickerDetails.DatePickerMode datePickerMode = DatePickerDetails.DatePickerMode.CALENDAR;\n        if (appearance != null) {\n            appearance = appearance.toLowerCase(Locale.US);\n            if (appearance.contains(Appearances.ETHIOPIAN)) {\n                datePickerType = DatePickerDetails.DatePickerType.ETHIOPIAN;\n                datePickerMode = DatePickerDetails.DatePickerMode.SPINNERS;\n            } else if (appearance.contains(Appearances.COPTIC)) {\n                datePickerType = DatePickerDetails.DatePickerType.COPTIC;\n                datePickerMode = DatePickerDetails.DatePickerMode.SPINNERS;\n            } else if (appearance.contains(Appearances.ISLAMIC)) {\n                datePickerType = DatePickerDetails.DatePickerType.ISLAMIC;\n                datePickerMode = DatePickerDetails.DatePickerMode.SPINNERS;\n            } else if (appearance.contains(Appearances.BIKRAM_SAMBAT)) {\n                datePickerType = DatePickerDetails.DatePickerType.BIKRAM_SAMBAT;\n                datePickerMode = DatePickerDetails.DatePickerMode.SPINNERS;\n            } else if (appearance.contains(Appearances.MYANMAR)) {\n                datePickerType = DatePickerDetails.DatePickerType.MYANMAR;\n                datePickerMode = DatePickerDetails.DatePickerMode.SPINNERS;\n            } else if (appearance.contains(Appearances.PERSIAN)) {\n                datePickerType = DatePickerDetails.DatePickerType.PERSIAN;\n                datePickerMode = DatePickerDetails.DatePickerMode.SPINNERS;\n            } else if (appearance.contains(Appearances.BUDDHIST)) {\n                datePickerType = DatePickerDetails.DatePickerType.BUDDHIST;\n                datePickerMode = DatePickerDetails.DatePickerMode.SPINNERS;\n            } else if (appearance.contains(Appearances.NO_CALENDAR)) {\n                datePickerMode = DatePickerDetails.DatePickerMode.SPINNERS;\n            }\n\n            if (appearance.contains(Appearances.MONTH_YEAR)) {\n                datePickerMode = DatePickerDetails.DatePickerMode.MONTH_YEAR;\n            } else if (appearance.contains(Appearances.YEAR)) {\n                datePickerMode = DatePickerDetails.DatePickerMode.YEAR;\n            }\n        }\n\n        return new DatePickerDetails(datePickerType, datePickerMode);\n    }\n\n    public static String getDateTimeLabel(Date date, DatePickerDetails datePickerDetails, boolean containsTime, Context context) {\n        String gregorianDateText = getGregorianDateTimeLabel(date, datePickerDetails, containsTime, Locale.getDefault());\n\n        DateTime customDate;\n        String[] monthArray;\n\n        switch (datePickerDetails.getDatePickerType()) {\n            case GREGORIAN:\n                return gregorianDateText;\n            case ETHIOPIAN:\n                customDate = new DateTime(date).withChronology(EthiopicChronology.getInstance());\n                monthArray = context.getResources().getStringArray(R.array.ethiopian_months);\n                break;\n            case COPTIC:\n                customDate = new DateTime(date).withChronology(CopticChronology.getInstance());\n                monthArray = context.getResources().getStringArray(R.array.coptic_months);\n                break;\n            case ISLAMIC:\n                customDate = new DateTime(date).withChronology(IslamicChronology.getInstance());\n                monthArray = context.getResources().getStringArray(R.array.islamic_months);\n                break;\n            case BIKRAM_SAMBAT:\n                customDate = new DateTime(date);\n                monthArray = BsCalendar.MONTH_NAMES.toArray(new String[0]);\n                break;\n            case MYANMAR:\n                customDate = new DateTime(date);\n                MyanmarDate myanmarDate = MyanmarDate.of(customDate.getYear(),\n                        customDate.getMonthOfYear(), customDate.getDayOfMonth(), customDate.getHourOfDay(),\n                        customDate.getMinuteOfHour(), customDate.getSecondOfMinute());\n                monthArray = MyanmarDateUtils.getMyanmarMonthsArray(myanmarDate.getYearValue());\n                break;\n            case PERSIAN:\n                customDate = new DateTime(date).withChronology(PersianChronologyKhayyamBorkowski.getInstance());\n                monthArray = context.getResources().getStringArray(R.array.persian_months);\n                break;\n            case BUDDHIST:\n                customDate = new DateTime(date).withChronology(BuddhistChronology.getInstance());\n                monthArray = context.getResources().getStringArray(R.array.buddhist_months);\n                break;\n            default:\n                Timber.w(\"Not supported date type.\");\n                return null;\n        }\n\n        String customDateText = \"\";\n\n        SimpleDateFormat df = new SimpleDateFormat(\"HH:mm\", Locale.getDefault());\n        switch (datePickerDetails.getDatePickerType()) {\n            case BIKRAM_SAMBAT:\n                BikramSambatDate bikramSambatDate;\n                try {\n                    Calendar calendar = Calendar.getInstance();\n                    calendar.setTime(date);\n                    bikramSambatDate = BsCalendar.getInstance().toBik(new BsGregorianDate(\n                            calendar.get(Calendar.YEAR),\n                            calendar.get(Calendar.MONTH) + 1,\n                            calendar.get(Calendar.DAY_OF_MONTH)));\n                    String day = datePickerDetails.isSpinnerMode() ? bikramSambatDate.day + \" \" : \"\";\n                    String month = datePickerDetails.isSpinnerMode() || datePickerDetails.isMonthYearMode() ? monthArray[bikramSambatDate.month - 1] + \" \" : \"\";\n\n                    if (containsTime) {\n                        customDateText = day + month + bikramSambatDate.year + \", \" + df.format(customDate.toDate());\n                    } else {\n                        customDateText = day + month + bikramSambatDate.year;\n                    }\n                } catch (BsException e) {\n                    Timber.e(e);\n                }\n                break;\n            case MYANMAR: {\n                MyanmarDate myanmarDate = MyanmarDate.of(customDate.getYear(),\n                        customDate.getMonthOfYear(), customDate.getDayOfMonth(), customDate.getHourOfDay(),\n                        customDate.getMinuteOfHour(), customDate.getSecondOfMinute());\n\n                String day = datePickerDetails.isSpinnerMode() ? myanmarDate.getDayOfMonth() + \" \" : \"\";\n                String month = datePickerDetails.isSpinnerMode() || datePickerDetails.isMonthYearMode() ? monthArray[MyanmarDateUtils.getMonthId(myanmarDate)] + \" \" : \"\";\n\n                if (containsTime) {\n                    customDateText = day + month + myanmarDate.getYearValue() + \", \" + df.format(customDate.toDate());\n                } else {\n                    customDateText = day + month + myanmarDate.getYearValue();\n                }\n                break;\n            }\n            default:\n                String day = datePickerDetails.isSpinnerMode() ? customDate.getDayOfMonth() + \" \" : \"\";\n                String month = datePickerDetails.isSpinnerMode() || datePickerDetails.isMonthYearMode() ? monthArray[customDate.getMonthOfYear() - 1] + \" \" : \"\";\n\n                if (containsTime) {\n                    customDateText = day + month + customDate.getYear() + \", \" + df.format(customDate.toDate());\n                } else {\n                    customDateText = day + month + customDate.getYear();\n                }\n        }\n\n        return String.format(context.getString(org.odk.collect.strings.R.string.custom_date), customDateText, gregorianDateText);\n    }\n\n    public void showTimePickerDialog(Context context, LocalDateTime dateTime) {\n        ThemeUtils themeUtils = new ThemeUtils(context);\n\n        Bundle bundle = new Bundle();\n        bundle.putInt(DIALOG_THEME, themeUtils.getSpinnerTimePickerDialogTheme());\n        bundle.putSerializable(TIME, dateTime);\n\n        DialogFragmentUtils.showIfNotShowing(CustomTimePickerDialog.class, bundle, ((FragmentActivity) context).getSupportFragmentManager());\n    }\n\n    public void showDatePickerDialog(Context context, DatePickerDetails datePickerDetails, LocalDateTime date) {\n        ThemeUtils themeUtils = new ThemeUtils(context);\n\n        Bundle bundle = new Bundle();\n        bundle.putInt(DIALOG_THEME, getDatePickerTheme(themeUtils, datePickerDetails));\n        bundle.putSerializable(DATE, date);\n        bundle.putSerializable(DATE_PICKER_DETAILS, datePickerDetails);\n\n        DialogFragmentUtils.showIfNotShowing(getClass(datePickerDetails.getDatePickerType()), bundle, ((FragmentActivity) context).getSupportFragmentManager());\n    }\n\n    private static Class getClass(DatePickerDetails.DatePickerType datePickerType) {\n        switch (datePickerType) {\n            case ETHIOPIAN:\n                return EthiopianDatePickerDialog.class;\n            case COPTIC:\n                return CopticDatePickerDialog.class;\n            case ISLAMIC:\n                return IslamicDatePickerDialog.class;\n            case BIKRAM_SAMBAT:\n                return BikramSambatDatePickerDialog.class;\n            case MYANMAR:\n                return MyanmarDatePickerDialog.class;\n            case PERSIAN:\n                return PersianDatePickerDialog.class;\n            case BUDDHIST:\n                return BuddhistDatePickerDialog.class;\n            default:\n                return FixedDatePickerDialog.class;\n        }\n    }\n\n    private static int getDatePickerTheme(ThemeUtils themeUtils, DatePickerDetails datePickerDetails) {\n        int theme = themeUtils.getCalendarDatePickerDialogTheme();\n        if (!datePickerDetails.isCalendarMode()) {\n            theme = themeUtils.getSpinnerDatePickerDialogTheme();\n        }\n\n        return theme;\n    }\n\n    private static String getGregorianDateTimeLabel(Date date, DatePickerDetails datePickerDetails, boolean containsTime, Locale locale) {\n        DateFormat dateFormatter;\n        locale = locale == null ? Locale.getDefault() : locale;\n        String format = android.text.format.DateFormat.getBestDateTimePattern(locale, getDateTimeSkeleton(containsTime, datePickerDetails));\n        dateFormatter = new SimpleDateFormat(format, locale);\n        return dateFormatter.format(date);\n    }\n\n    private static String getDateTimeSkeleton(boolean containsTime, DatePickerDetails datePickerDetails) {\n        String dateSkeleton;\n        if (containsTime) {\n            dateSkeleton = \"yyyyMMMdd HHmm\";\n        } else {\n            dateSkeleton = \"yyyyMMMdd\";\n        }\n        if (datePickerDetails.isMonthYearMode()) {\n            dateSkeleton = \"yyyyMMM\";\n        } else if (datePickerDetails.isYearMode()) {\n            dateSkeleton = \"yyyy\";\n        }\n        return dateSkeleton;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/ExternalAppRecordingRequester.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.provider.MediaStore\nimport android.widget.Toast\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.R\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.permissions.PermissionListener\nimport org.odk.collect.permissions.PermissionsProvider\n\nclass ExternalAppRecordingRequester(\n    private val activity: Activity,\n    private val intentLauncher: IntentLauncher,\n    private val waitingForDataRegistry: WaitingForDataRegistry,\n    private val permissionsProvider: PermissionsProvider\n) : RecordingRequester {\n\n    override fun requestRecording(prompt: FormEntryPrompt) {\n        permissionsProvider.requestRecordAudioPermission(\n            activity,\n            object : PermissionListener {\n                override fun granted() {\n                    val intent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION)\n                    intent.putExtra(\n                        MediaStore.EXTRA_OUTPUT,\n                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString()\n                    )\n                    waitingForDataRegistry.waitForData(prompt.index)\n                    intentLauncher.launchForResult(\n                        activity,\n                        intent,\n                        ApplicationConstants.RequestCodes.AUDIO_CAPTURE\n                    ) {\n                        Toast.makeText(\n                            activity,\n                            activity.getString(\n                                org.odk.collect.strings.R.string.activity_not_found,\n                                activity.getString(org.odk.collect.strings.R.string.capture_audio)\n                            ),\n                            Toast.LENGTH_SHORT\n                        ).show()\n                        waitingForDataRegistry.cancelWaitingForData()\n                    }\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/FileRequester.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.app.Activity\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.utilities.ExternalAppIntentProvider\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidshared.ui.ToastUtils.showLongToast\nimport java.lang.Error\nimport java.lang.Exception\n\nclass FileRequesterImpl(\n    val intentLauncher: IntentLauncher,\n    val externalAppIntentProvider: ExternalAppIntentProvider,\n    private val formController: FormController\n) : FileRequester {\n\n    override fun launch(\n        activity: Activity,\n        requestCode: Int,\n        formEntryPrompt: FormEntryPrompt\n    ) {\n        try {\n            val intent = externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)\n            val intentWithoutDefaultCategory =\n                externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                    formController,\n                    formEntryPrompt,\n                    activity.packageManager\n                )\n\n            intentLauncher.launchForResult(\n                activity,\n                intent,\n                requestCode\n            ) {\n                intentLauncher.launchForResult(\n                    activity,\n                    intentWithoutDefaultCategory,\n                    requestCode\n                ) {\n                    showLongToast(getErrorMessage(formEntryPrompt, activity))\n                }\n            }\n        } catch (e: Exception) {\n            showLongToast(e.message!!)\n        } catch (e: Error) {\n            showLongToast(e.message!!)\n        }\n    }\n\n    private fun getErrorMessage(formEntryPrompt: FormEntryPrompt, activity: Activity): String {\n        val customErrorMessage = formEntryPrompt.getSpecialFormQuestionText(\"noAppErrorString\")\n        return customErrorMessage ?: activity.getString(org.odk.collect.strings.R.string.no_app)\n    }\n}\n\ninterface FileRequester {\n    fun launch(\n        activity: Activity,\n        requestCode: Int,\n        formEntryPrompt: FormEntryPrompt\n    )\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/FormControllerWaitingForDataRegistry.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.odk.collect.android.javarosawrapper.FormController;\n\nimport java.util.function.Supplier;\n\nimport timber.log.Timber;\n\npublic class FormControllerWaitingForDataRegistry implements WaitingForDataRegistry {\n\n    private final Supplier<FormController> formControllerProvider;\n\n    public FormControllerWaitingForDataRegistry(Supplier<FormController> formControllerProvider) {\n        this.formControllerProvider = formControllerProvider;\n    }\n\n    @Override\n    public void waitForData(FormIndex index) {\n        FormController formController = formControllerProvider.get();\n        if (formController == null) {\n            Timber.w(\"Can not call setIndexWaitingForData() because of null formController\");\n            return;\n        }\n\n        formController.setIndexWaitingForData(index);\n    }\n\n    @Override\n    public boolean isWaitingForData(FormIndex index) {\n        FormController formController = formControllerProvider.get();\n        if (formController == null) {\n            return false;\n        }\n\n        return index.equals(formController.getIndexWaitingForData());\n    }\n\n    @Override\n    public void cancelWaitingForData() {\n        FormController formController = formControllerProvider.get();\n        if (formController == null) {\n            return;\n        }\n\n        formController.setIndexWaitingForData(null);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/GeoPolyDialogFragment.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport androidx.activity.ComponentDialog\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.map\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.data.GeoShapeData\nimport org.javarosa.core.model.data.GeoTraceData\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.javarosawrapper.FailedValidationResult\nimport org.odk.collect.android.utilities.FormEntryPromptUtils\nimport org.odk.collect.android.widgets.utilities.AdditionalAttributes.INCREMENTAL\nimport org.odk.collect.android.widgets.utilities.BindAttributes.ALLOW_MOCK_ACCURACY\nimport org.odk.collect.androidshared.ui.DisplayString\nimport org.odk.collect.geo.GeoUtils.toMapPoint\nimport org.odk.collect.geo.geopoly.GeoPolyFragment\nimport org.odk.collect.geo.geopoly.GeoPolyFragment.OutputMode\n\nclass GeoPolyDialogFragment(viewModelFactory: ViewModelProvider.Factory) :\n    WidgetAnswerDialogFragment<GeoPolyFragment>(\n        GeoPolyFragment::class,\n        viewModelFactory\n    ) {\n\n    override fun onCreateFragment(prompt: FormEntryPrompt): GeoPolyFragment {\n        val outputMode = when (prompt.dataType) {\n            Constants.DATATYPE_GEOSHAPE -> OutputMode.GEOSHAPE\n            Constants.DATATYPE_GEOTRACE -> OutputMode.GEOTRACE\n            else -> throw IllegalArgumentException()\n        }\n\n        childFragmentManager.setFragmentResultListener(\n            GeoPolyFragment.REQUEST_GEOPOLY,\n            this\n        ) { _, result ->\n            val geopolyChange = result.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE)\n            val geopoly = result.getString(GeoPolyFragment.RESULT_GEOPOLY)\n\n            if (geopolyChange != null) {\n                val incremental = FormEntryPromptUtils.getAdditionalAttribute(prompt, INCREMENTAL)\n                if (incremental == \"true\") {\n                    onValidate(geopolyChange, outputMode)\n                }\n            } else if (geopoly != null) {\n                onAnswer(geopoly, outputMode)\n            } else {\n                dismiss()\n            }\n        }\n\n        val retainMockAccuracy =\n            FormEntryPromptUtils.getBindAttribute(prompt, ALLOW_MOCK_ACCURACY).toBoolean()\n\n        val inputPolygon = when (val answer = prompt.answerValue) {\n            is GeoTraceData -> answer.points.map { it.toMapPoint() }\n            is GeoShapeData -> answer.points.map { it.toMapPoint() }\n            null -> emptyList()\n            else -> throw IllegalArgumentException()\n        }\n\n        return GeoPolyFragment(\n            { (requireDialog() as ComponentDialog).onBackPressedDispatcher },\n            outputMode,\n            prompt.isReadOnly,\n            retainMockAccuracy,\n            inputPolygon,\n            constraintValidationResult.map {\n                if (it is FailedValidationResult) {\n                    if (it.customErrorMessage != null) {\n                        DisplayString.Raw(it.customErrorMessage)\n                    } else {\n                        DisplayString.Resource(it.defaultErrorMessage)\n                    }\n                } else {\n                    null\n                }\n            }\n        )\n    }\n\n    private fun onValidate(geoString: String, outputMode: OutputMode) {\n        val answer = getAnswerData(geoString, outputMode)\n        onValidate(answer)\n    }\n\n    private fun onAnswer(geoString: String, outputMode: OutputMode) {\n        val answer = getAnswerData(geoString, outputMode)\n        onAnswer(answer)\n    }\n\n    private fun getAnswerData(geoString: String, outputMode: OutputMode): IAnswerData? {\n        return if (geoString.isBlank()) {\n            null\n        } else {\n            when (outputMode) {\n                OutputMode.GEOTRACE -> GeoTraceData().also { it.value = geoString }\n                OutputMode.GEOSHAPE -> GeoShapeData().also { it.value = geoString }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/GeoWidgetUtils.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.content.Context\nimport android.location.Location\nimport org.odk.collect.android.R\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.shared.strings.StringUtils.removeEnd\nimport timber.log.Timber\nimport java.text.DecimalFormat\nimport kotlin.math.abs\nimport kotlin.math.absoluteValue\n\nobject GeoWidgetUtils {\n\n    @JvmStatic\n    fun getGeoPointAnswerToDisplay(context: Context, answer: String?): String {\n        try {\n            if (answer != null && answer.isNotEmpty()) {\n                val parts = answer.split(\" \").toTypedArray()\n                return context.getString(\n                    org.odk.collect.strings.R.string.gps_result,\n                    convertCoordinatesIntoDegreeFormat(context, parts[0].toDouble(), \"lat\"),\n                    convertCoordinatesIntoDegreeFormat(context, parts[1].toDouble(), \"lon\"),\n                    truncateDouble(parts[2]),\n                    truncateDouble(parts[3])\n                )\n            }\n        } catch (e: NumberFormatException) {\n            return \"\"\n        }\n\n        return \"\"\n    }\n\n    @JvmStatic\n    fun getGeoPolyAnswerToDisplay(answer: String?): String? {\n        return if (answer != null && answer.isNotEmpty()) {\n            removeEnd(answer.trim(), \";\")\n        } else {\n            answer\n        }\n    }\n\n    fun isWithinMapBounds(point: MapPoint): Boolean {\n        return point.latitude.absoluteValue <= 90 && point.longitude.absoluteValue <= 180\n    }\n\n    @JvmStatic\n    fun convertCoordinatesIntoDegreeFormat(\n        context: Context,\n        coordinate: Double,\n        type: String\n    ): String {\n        val coordinateDegrees = Location.convert(abs(coordinate), Location.FORMAT_SECONDS)\n        val coordinateSplit = coordinateDegrees.split(\":\").toTypedArray()\n        val degrees = floor(coordinateSplit[0]) + \"°\"\n        val mins = floor(coordinateSplit[1]) + \"'\"\n        val secs = floor(coordinateSplit[2]) + '\"'\n        return String.format(getCardinalDirection(context, coordinate, type), degrees, mins, secs)\n    }\n\n    @JvmStatic\n    fun floor(value: String?): String {\n        if (value == null || value.isEmpty()) {\n            return \"\"\n        }\n        return if (value.contains(\".\")) value.substring(0, value.indexOf('.')) else value\n    }\n\n    @JvmStatic\n    fun truncateDouble(string: String?): String {\n        val df = DecimalFormat(\"#.##\")\n        try {\n            return df.format(string?.toDouble())\n        } catch (e: Throwable) {\n            Timber.w(e)\n        }\n        return \"\"\n    }\n\n    private fun getCardinalDirection(context: Context, coordinate: Double, type: String): String {\n        return if (type.equals(\"lon\", ignoreCase = true)) {\n            if (coordinate < 0) {\n                context.getString(org.odk.collect.strings.R.string.west)\n            } else {\n                context.getString(org.odk.collect.strings.R.string.east)\n            }\n        } else if (coordinate < 0) {\n            context.getString(org.odk.collect.strings.R.string.south)\n        } else {\n            context.getString(org.odk.collect.strings.R.string.north)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/GetContentAudioFileRequester.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.widget.Toast\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.R\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.androidshared.system.IntentLauncher\n\nclass GetContentAudioFileRequester(\n    private val activity: Activity,\n    private val intentLauncher: IntentLauncher,\n    private val waitingForDataRegistry: WaitingForDataRegistry\n) : AudioFileRequester {\n\n    override fun requestFile(prompt: FormEntryPrompt) {\n        val intent = Intent(Intent.ACTION_GET_CONTENT)\n        intent.type = \"audio/*\"\n        waitingForDataRegistry.waitForData(prompt.index)\n        intentLauncher.launchForResult(\n            activity,\n            intent,\n            ApplicationConstants.RequestCodes.AUDIO_CHOOSER\n        ) {\n            Toast.makeText(\n                activity,\n                activity.getString(\n                    org.odk.collect.strings.R.string.activity_not_found,\n                    activity.getString(org.odk.collect.strings.R.string.choose_sound)\n                ),\n                Toast.LENGTH_SHORT\n            ).show()\n            waitingForDataRegistry.cancelWaitingForData()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/ImageCaptureIntentCreator.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.content.Context\nimport android.content.Intent\nimport android.provider.MediaStore\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.android.utilities.ContentUriProvider\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.android.utilities.FormEntryPromptUtils\nimport timber.log.Timber\nimport java.io.File\n\nobject ImageCaptureIntentCreator {\n    @JvmStatic\n    fun imageCaptureIntent(prompt: FormEntryPrompt, context: Context, tmpImageFilePath: String): Intent {\n        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)\n        val packageName = FormEntryPromptUtils.getAdditionalAttribute(prompt, \"intent\")\n        if (packageName != null) {\n            intent.setPackage(packageName)\n        }\n\n        // The Android Camera application saves a full-size photo if you give it a file to save into.\n        // You must provide a fully qualified file name where the camera app should save the photo.\n        // https://developer.android.com/training/camera-deprecated/photobasics\n        try {\n            val uri = ContentUriProvider().getUriForFile(\n                context,\n                BuildConfig.APPLICATION_ID + \".provider\",\n                File(tmpImageFilePath)\n            )\n            // if this gets modified, the onActivityResult in\n            // FormEntyActivity will also need to be updated.\n            intent.putExtra(MediaStore.EXTRA_OUTPUT, uri)\n            FileUtils.grantFilePermissions(intent, uri, context)\n        } catch (e: IllegalArgumentException) {\n            Timber.e(e)\n        }\n\n        return intent\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/InternalRecordingRequester.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport static org.odk.collect.android.widgets.utilities.BindAttributes.QUALITY;\n\nimport androidx.activity.ComponentActivity;\n\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.utilities.FormEntryPromptUtils;\nimport org.odk.collect.audiorecorder.recorder.Output;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.permissions.PermissionsProvider;\n\npublic class InternalRecordingRequester implements RecordingRequester {\n\n    private final ComponentActivity activity;\n    private final AudioRecorder audioRecorder;\n    private final PermissionsProvider permissionsProvider;\n\n    public InternalRecordingRequester(ComponentActivity activity, AudioRecorder audioRecorder, PermissionsProvider permissionsProvider) {\n        this.activity = activity;\n        this.audioRecorder = audioRecorder;\n        this.permissionsProvider = permissionsProvider;\n    }\n\n    @Override\n    public void requestRecording(FormEntryPrompt prompt) {\n        permissionsProvider.requestRecordAudioPermission(activity, () -> {\n            String quality = FormEntryPromptUtils.getBindAttribute(prompt, QUALITY);\n            if (quality != null && quality.equals(BindAttributes.Quality.VOICE_ONLY.getValue())) {\n                audioRecorder.start(prompt.getIndex(), Output.AMR);\n            } else if (quality != null && quality.equals(BindAttributes.Quality.LOW.getValue())) {\n                audioRecorder.start(prompt.getIndex(), Output.AAC_LOW);\n            } else {\n                audioRecorder.start(prompt.getIndex(), Output.AAC);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/QuestionFontSizeUtils.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\nobject QuestionFontSizeUtils {\n    const val DEFAULT_FONT_SIZE = 21\n    private const val HEADLINE_6_DIFF = -1\n    private const val SUBTITLE_1_DIFF = -5\n    private const val BODY_MEDIUM_DIFF = -7\n    private const val BODY_LARGE_DIFF = -5\n    private const val TITLE_LARGE_DIFF = -1\n\n    @JvmStatic\n    fun getFontSize(settings: Settings, fontSize: FontSize?): Int {\n        val settingsValue = settings.getString(ProjectKeys.KEY_FONT_SIZE)!!.toInt()\n\n        return when (fontSize) {\n            FontSize.HEADLINE_6 -> settingsValue + HEADLINE_6_DIFF\n            FontSize.SUBTITLE_1 -> settingsValue + SUBTITLE_1_DIFF\n            FontSize.BODY_MEDIUM -> settingsValue + BODY_MEDIUM_DIFF\n            FontSize.BODY_LARGE -> settingsValue + BODY_LARGE_DIFF\n            FontSize.TITLE_LARGE -> settingsValue + TITLE_LARGE_DIFF\n            else -> throw IllegalArgumentException()\n        }\n    }\n\n    @JvmStatic\n    @Deprecated(\"Use {@link QuestionFontSizeUtils#getFontSize(Settings, FontSize)} instead\")\n    fun getQuestionFontSize(): Int {\n        return try {\n            val fontSize = DaggerUtils.getComponent(Collect.getInstance()).settingsProvider().getUnprotectedSettings().getString(ProjectKeys.KEY_FONT_SIZE)!!.toInt()\n            fontSize + HEADLINE_6_DIFF\n        } catch (e: Exception) {\n            DEFAULT_FONT_SIZE\n        } catch (e: Error) {\n            DEFAULT_FONT_SIZE\n        }\n    }\n\n    enum class FontSize {\n        HEADLINE_6, SUBTITLE_1, BODY_MEDIUM, BODY_LARGE, TITLE_LARGE\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/RangeWidgetUtils.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.TextView;\n\nimport org.javarosa.core.model.RangeQuestion;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.databinding.RangePickerWidgetAnswerBinding;\nimport org.odk.collect.android.databinding.RangeWidgetHorizontalBinding;\nimport org.odk.collect.android.databinding.RangeWidgetVerticalBinding;\nimport org.odk.collect.android.views.TrackingTouchSlider;\nimport org.odk.collect.androidshared.ui.ToastUtils;\n\nimport java.math.BigDecimal;\n\npublic class RangeWidgetUtils {\n    private static final String VERTICAL_APPEARANCE = \"vertical\";\n    private static final String NO_TICKS_APPEARANCE = \"no-ticks\";\n\n    private RangeWidgetUtils() {\n    }\n\n    public static class RangeWidgetLayoutElements {\n        private final View answerView;\n        private final TrackingTouchSlider slider;\n        private final TextView currentValue;\n\n        public RangeWidgetLayoutElements(View answerView, TrackingTouchSlider slider, TextView currentValue) {\n            this.answerView = answerView;\n            this.slider = slider;\n            this.currentValue = currentValue;\n        }\n\n        public View getAnswerView() {\n            return answerView;\n        }\n\n        public TrackingTouchSlider getSlider() {\n            return slider;\n        }\n\n        public TextView getCurrentValue() {\n            return currentValue;\n        }\n    }\n\n    public static RangeWidgetLayoutElements setUpLayoutElements(Context context, FormEntryPrompt prompt) {\n        View answerView;\n        TrackingTouchSlider slider;\n        TextView currentValue;\n        TextView minValue;\n        TextView maxValue;\n\n        String appearance = prompt.getAppearanceHint();\n\n        if (appearance != null && appearance.contains(VERTICAL_APPEARANCE)) {\n            RangeWidgetVerticalBinding rangeWidgetVerticalBinding = RangeWidgetVerticalBinding\n                    .inflate(((Activity) context).getLayoutInflater());\n            answerView = rangeWidgetVerticalBinding.getRoot();\n\n            slider = rangeWidgetVerticalBinding.slider;\n            currentValue = rangeWidgetVerticalBinding.currentValue;\n            minValue = rangeWidgetVerticalBinding.minValue;\n            maxValue = rangeWidgetVerticalBinding.maxValue;\n        } else {\n            RangeWidgetHorizontalBinding rangeWidgetHorizontalBinding = RangeWidgetHorizontalBinding\n                    .inflate(((Activity) context).getLayoutInflater());\n            answerView = rangeWidgetHorizontalBinding.getRoot();\n\n            slider = rangeWidgetHorizontalBinding.slider;\n            currentValue = rangeWidgetHorizontalBinding.currentValue;\n            minValue = rangeWidgetHorizontalBinding.minValue;\n            maxValue = rangeWidgetHorizontalBinding.maxValue;\n        }\n\n        setUpWidgetParameters((RangeQuestion) prompt.getQuestion(), minValue, maxValue);\n        if (prompt.isReadOnly()) {\n            slider.setEnabled(false);\n        }\n\n        slider.setId(View.generateViewId());\n\n        return new RangeWidgetLayoutElements(answerView, slider, currentValue);\n    }\n\n    static void setUpWidgetParameters(RangeQuestion rangeQuestion, TextView minValue, TextView maxValue) {\n        BigDecimal rangeStart = rangeQuestion.getRangeStart();\n        BigDecimal rangeEnd = rangeQuestion.getRangeEnd();\n\n        minValue.setText(String.valueOf(rangeStart));\n        maxValue.setText(String.valueOf(rangeEnd));\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    public static BigDecimal setUpSlider(FormEntryPrompt prompt, TrackingTouchSlider slider, boolean isIntegerType) {\n        RangeQuestion rangeQuestion = (RangeQuestion) prompt.getQuestion();\n        BigDecimal rangeStart = rangeQuestion.getRangeStart();\n        BigDecimal rangeEnd = rangeQuestion.getRangeEnd();\n        BigDecimal rangeStep = rangeQuestion.getRangeStep().abs() != null ? rangeQuestion.getRangeStep().abs() : BigDecimal.valueOf(0.5);\n\n        BigDecimal actualValue = null;\n        if (prompt.getAnswerValue() != null) {\n            actualValue = new BigDecimal(prompt.getAnswerValue().getValue().toString());\n\n            if (!isValueInRange(actualValue, rangeStart, rangeEnd)) {\n                actualValue = null;\n            }\n        }\n\n        if (isRangeSliderWidgetValid(rangeQuestion, slider)) {\n            if (prompt.getAppearanceHint() != null && prompt.getAppearanceHint().contains(NO_TICKS_APPEARANCE)) {\n                slider.setTickVisible(false);\n                slider.setTrackStopIndicatorSize(0);\n            }\n\n            if (rangeEnd.compareTo(rangeStart) > -1) {\n                slider.setValueFrom(rangeStart.floatValue());\n                slider.setValueTo(rangeEnd.floatValue());\n            } else {\n                slider.setValueFrom(rangeEnd.floatValue());\n                slider.setValueTo(rangeStart.floatValue());\n            }\n\n            if (isIntegerType) {\n                slider.setStepSize(rangeStep.intValue());\n            } else {\n                slider.setStepSize(rangeStep.floatValue());\n            }\n\n            if (actualValue != null) {\n                if (rangeEnd.compareTo(rangeStart) > -1) {\n                    slider.setValue(actualValue.floatValue());\n                } else {\n                    slider.setValue(rangeStart.add(rangeEnd).subtract(actualValue).floatValue());\n                }\n            }\n        }\n\n        return actualValue;\n    }\n\n    private static boolean isValueInRange(BigDecimal value, BigDecimal rangeStart, BigDecimal rangeEnd) {\n        return rangeStart.compareTo(rangeEnd) < 0\n                ? value.compareTo(rangeStart) > -1 && value.compareTo(rangeEnd) < 1\n                : value.compareTo(rangeStart) < 1 && value.compareTo(rangeEnd) > -1;\n    }\n\n    public static void setUpRangePickerWidget(Context context, RangePickerWidgetAnswerBinding binding, FormEntryPrompt prompt) {\n        if (RangeWidgetUtils.isRangePickerWidgetValid((RangeQuestion) prompt.getQuestion(), binding.widgetButton)) {\n            if (prompt.getAnswerValue() != null) {\n                BigDecimal actualValue = new BigDecimal(prompt.getAnswerValue().getValue().toString());\n                binding.widgetAnswerText.setText(String.valueOf(actualValue));\n                binding.widgetButton.setText(context.getString(org.odk.collect.strings.R.string.edit_value));\n            }\n        }\n        if (prompt.isReadOnly()) {\n            binding.widgetButton.setVisibility(View.GONE);\n        }\n    }\n\n    public static BigDecimal getActualValue(FormEntryPrompt prompt, float value) {\n        RangeQuestion rangeQuestion = (RangeQuestion) prompt.getQuestion();\n        BigDecimal rangeStart = rangeQuestion.getRangeStart();\n        BigDecimal rangeEnd = rangeQuestion.getRangeEnd();\n        BigDecimal actualValue = BigDecimal.valueOf(value);\n\n        if (rangeEnd.compareTo(rangeStart) < 0) {\n            actualValue = rangeEnd.add(rangeStart).subtract(actualValue);\n        }\n\n        return actualValue;\n    }\n\n    public static int getNumberPickerProgress(RangePickerWidgetAnswerBinding binding, BigDecimal rangeStart, BigDecimal rangeStep,\n                                               BigDecimal rangeEnd, int value) {\n        BigDecimal actualValue;\n        int elementCount = rangeEnd.subtract(rangeStart).abs().divide(rangeStep).intValue();\n        BigDecimal multiply = new BigDecimal(elementCount - value).multiply(rangeStep);\n\n        if (rangeStart.compareTo(rangeEnd) < 0) {\n            actualValue = rangeStart.add(multiply);\n        } else {\n            actualValue = rangeStart.subtract(multiply);\n        }\n        binding.widgetAnswerText.setText(String.valueOf(actualValue));\n        binding.widgetButton.setText(org.odk.collect.strings.R.string.edit_value);\n\n        return actualValue.subtract(rangeStart).abs().divide(rangeStep).intValue();\n    }\n\n    public static boolean isRangeSliderWidgetValid(RangeQuestion rangeQuestion, TrackingTouchSlider slider) {\n        if (!isWidgetValid(slider.getContext(), rangeQuestion)) {\n            slider.setEnabled(false);\n        }\n        return isWidgetValid(slider.getContext(), rangeQuestion);\n    }\n\n    static boolean isWidgetValid(Context context, RangeQuestion rangeQuestion) {\n        BigDecimal rangeStart = rangeQuestion.getRangeStart();\n        BigDecimal rangeEnd = rangeQuestion.getRangeEnd();\n        BigDecimal rangeStep = rangeQuestion.getRangeStep().abs();\n\n        boolean result = true;\n        if (rangeStep.compareTo(BigDecimal.ZERO) == 0\n                || rangeEnd.subtract(rangeStart).remainder(rangeStep).compareTo(BigDecimal.ZERO) != 0) {\n            ToastUtils.showLongToast(org.odk.collect.strings.R.string.invalid_range_widget);\n            result = false;\n        }\n        return result;\n    }\n\n    private static boolean isRangePickerWidgetValid(RangeQuestion rangeQuestion, Button widgetButton) {\n        if (!isWidgetValid(widgetButton.getContext(), rangeQuestion)) {\n            widgetButton.setEnabled(false);\n        }\n        return isWidgetValid(widgetButton.getContext(), rangeQuestion);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/RecordingRequester.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport org.javarosa.form.api.FormEntryPrompt;\n\npublic interface RecordingRequester {\n    void requestRecording(FormEntryPrompt prompt);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/RecordingRequesterProvider.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport static org.odk.collect.android.widgets.utilities.BindAttributes.QUALITY;\n\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.utilities.FormEntryPromptUtils;\nimport org.odk.collect.android.widgets.utilities.BindAttributes.Quality;\n\npublic class RecordingRequesterProvider {\n\n    private final InternalRecordingRequester internalRecordingRequester;\n    private final ExternalAppRecordingRequester externalAppRecordingRequester;\n\n    public RecordingRequesterProvider(InternalRecordingRequester internalRecordingRequester, ExternalAppRecordingRequester externalAppRecordingRequester) {\n        this.internalRecordingRequester = internalRecordingRequester;\n        this.externalAppRecordingRequester = externalAppRecordingRequester;\n    }\n\n    public RecordingRequester create(FormEntryPrompt prompt, boolean externalRecorderPreferred) {\n        String audioQuality = FormEntryPromptUtils.getBindAttribute(prompt, QUALITY);\n\n        if (audioQuality != null && (audioQuality.equals(Quality.NORMAL.getValue()) || audioQuality.equals(Quality.VOICE_ONLY.getValue()) || audioQuality.equals(Quality.LOW.getValue()))) {\n            return internalRecordingRequester;\n        } else if (audioQuality != null && audioQuality.equals(Quality.EXTERNAL.getValue())) {\n            return externalAppRecordingRequester;\n        } else if (externalRecorderPreferred) {\n            return externalAppRecordingRequester;\n        } else {\n            return internalRecordingRequester;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/RecordingStatusHandler.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport android.util.Pair;\n\nimport org.javarosa.form.api.FormEntryPrompt;\n\nimport java.util.function.Consumer;\n\npublic interface RecordingStatusHandler {\n\n    void onBlockedStatusChange(Consumer<Boolean> blockedStatusListener);\n\n    void onRecordingStatusChange(FormEntryPrompt prompt, Consumer<Pair<Long, Integer>> statusListener);\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/SearchQueryViewModel.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport androidx.lifecycle.ViewModel;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class SearchQueryViewModel extends ViewModel {\n\n    private final Map<String, String> queries = new HashMap<>();\n\n    public String getQuery(String id) {\n        return queries.getOrDefault(id, \"\");\n    }\n\n    public void setQuery(String id, String query) {\n        queries.put(id, query);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/StringRequester.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.app.Activity\nimport android.content.Intent\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.R\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.utilities.ExternalAppIntentProvider\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport java.io.Serializable\nimport java.lang.Error\nimport java.lang.Exception\n\nclass StringRequesterImpl(\n    val intentLauncher: IntentLauncher,\n    val externalAppIntentProvider: ExternalAppIntentProvider,\n    private val formController: FormController\n) : StringRequester {\n\n    override fun launch(\n        activity: Activity,\n        requestCode: Int,\n        formEntryPrompt: FormEntryPrompt,\n        value: Serializable?,\n        onError: (String) -> Unit\n    ) {\n        try {\n            val intent = externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)?.apply {\n                putExtra(\"value\", value)\n            }\n            val intentWithoutDefaultCategory =\n                externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                    formController,\n                    formEntryPrompt,\n                    activity.packageManager\n                )?.apply {\n                    putExtra(\"value\", value)\n                }\n\n            // ACTION_SENDTO used for sending text messages or emails doesn't require any results\n            if (intent != null && Intent.ACTION_SENDTO == intent.action) {\n                intentLauncher.launch(activity, intent) {\n                    intentLauncher.launch(\n                        activity,\n                        intentWithoutDefaultCategory\n                    ) {\n                        onError(getErrorMessage(formEntryPrompt, activity))\n                    }\n                }\n            } else {\n                intentLauncher.launchForResult(activity, intent, requestCode) {\n                    intentLauncher.launchForResult(\n                        activity,\n                        intentWithoutDefaultCategory,\n                        requestCode\n                    ) {\n                        onError(getErrorMessage(formEntryPrompt, activity))\n                    }\n                }\n            }\n        } catch (e: Exception) {\n            onError(e.message!!)\n        } catch (e: Error) {\n            onError(e.message!!)\n        }\n    }\n\n    private fun getErrorMessage(formEntryPrompt: FormEntryPrompt, activity: Activity): String {\n        val customErrorMessage = formEntryPrompt.getSpecialFormQuestionText(\"noAppErrorString\")\n        return customErrorMessage ?: activity.getString(org.odk.collect.strings.R.string.no_app)\n    }\n}\n\ninterface StringRequester {\n    fun launch(\n        activity: Activity,\n        requestCode: Int,\n        formEntryPrompt: FormEntryPrompt,\n        value: Serializable?,\n        onError: (String) -> Unit\n    )\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/StringWidgetUtils.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport org.javarosa.core.model.data.DecimalData;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.IntegerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.listeners.ThousandsSeparatorTextWatcher;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.FormEntryPromptUtils;\n\nimport timber.log.Timber;\n\npublic final class StringWidgetUtils {\n\n    private StringWidgetUtils() {\n    }\n\n    public static Integer getIntegerAnswerValueFromIAnswerData(IAnswerData dataHolder) {\n        if (dataHolder != null) {\n            Object dataValue = dataHolder.getValue();\n            if (dataValue instanceof Double) {\n                return ((Double) dataValue).intValue();\n            } else if (dataValue instanceof Integer) {\n                return (Integer) dataValue;\n            } else if (dataValue instanceof String) {\n                try {\n                    return Integer.parseInt((String) dataValue);\n                } catch (NumberFormatException ignored) {\n                    // ignored\n                }\n            }\n        }\n        return null;\n    }\n\n    public static Double getDoubleAnswerValueFromIAnswerData(IAnswerData dataHolder) {\n        if (dataHolder != null) {\n            Object dataValue = dataHolder.getValue();\n            if (dataValue instanceof Double) {\n                return (Double) dataValue;\n            } else if (dataValue instanceof Integer) {\n                return Double.valueOf((Integer) dataValue);\n            } else if (dataValue instanceof String) {\n                try {\n                    return Double.parseDouble((String) dataValue);\n                } catch (NumberFormatException ignored) {\n                    // ignored\n                }\n            }\n        }\n        return null;\n    }\n\n    public static IntegerData getIntegerData(String answer, FormEntryPrompt prompt) {\n        if (Appearances.useThousandSeparator(prompt)) {\n            answer = ThousandsSeparatorTextWatcher.getOriginalString(answer);\n        }\n\n        if (answer.isEmpty()) {\n            return null;\n        } else {\n            try {\n                return new IntegerData(Integer.parseInt(answer));\n            } catch (Exception numberFormatException) {\n                return null;\n            }\n        }\n    }\n\n    public static DecimalData getDecimalData(String answer, FormEntryPrompt prompt) {\n        if (Appearances.useThousandSeparator(prompt)) {\n            answer = ThousandsSeparatorTextWatcher.getOriginalString(answer);\n        }\n\n        if (answer.isEmpty()) {\n            return null;\n\n        } else {\n            try {\n                return new DecimalData(Double.parseDouble(answer));\n            } catch (Exception numberFormatException) {\n                return null;\n            }\n        }\n    }\n\n    public static StringData getStringNumberData(String answer, FormEntryPrompt prompt) {\n        if (Appearances.useThousandSeparator(prompt)) {\n            answer = ThousandsSeparatorTextWatcher.getOriginalString(answer);\n        }\n\n        if (answer.isEmpty()) {\n            return null;\n        } else {\n            try {\n                return new StringData(answer);\n            } catch (Exception numberFormatException) {\n                return null;\n            }\n        }\n    }\n\n    public static Integer getNumberOfRows(FormEntryPrompt prompt) {\n        String rows = FormEntryPromptUtils.getAdditionalAttribute(prompt, \"rows\");\n        if (rows != null && !rows.isEmpty()) {\n            try {\n                return Integer.parseInt(rows);\n            } catch (Exception e) {\n                Timber.e(new Error(\"Unable to process the rows setting for the answerText field: \" + e));\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/ViewModelAudioPlayer.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.media.MediaPlayer\nimport androidx.activity.ComponentActivity\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.androidshared.data.consume\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.audioclips.AudioClipViewModel\nimport org.odk.collect.audioclips.AudioPlayer\nimport org.odk.collect.audioclips.AudioPlayerFactory\nimport org.odk.collect.audioclips.Clip\nimport java.util.function.Consumer\n\nclass ViewModelAudioPlayer(\n    private val viewModel: AudioClipViewModel,\n    private val lifecycleOwner: LifecycleOwner\n) : AudioPlayer {\n    override fun isLoading(): LiveData<Boolean> {\n        return viewModel.isLoading()\n    }\n\n    override fun play(clip: Clip) {\n        viewModel.play(clip)\n    }\n\n    override fun pause() {\n        viewModel.pause()\n    }\n\n    override fun setPosition(clipId: String, position: Int) {\n        viewModel.setPosition(clipId, position)\n    }\n\n    override fun onPlayingChanged(clipID: String, playingConsumer: Consumer<Boolean>) {\n        viewModel.isPlaying(clipID).observe(lifecycleOwner) {\n            playingConsumer.accept(it)\n        }\n    }\n\n    override fun onPositionChanged(clipID: String, positionConsumer: Consumer<Int>) {\n        viewModel.getPosition(clipID).observe(lifecycleOwner) {\n            positionConsumer.accept(it)\n        }\n    }\n\n    override fun onPlaybackError(errorConsumer: Consumer<Exception>) {\n        viewModel.getError().consume(lifecycleOwner) { e: Exception ->\n            errorConsumer.accept(e)\n        }\n    }\n\n    override fun stop() {\n        viewModel.stop()\n    }\n\n    override fun playInOrder(clips: List<Clip>) {\n        viewModel.playInOrder(clips)\n    }\n}\n\nclass ViewModelAudioPlayerFactory(private val scheduler: Scheduler) : AudioPlayerFactory {\n    override fun create(\n        activity: ComponentActivity,\n        lifecycleOwner: LifecycleOwner\n    ): AudioPlayer {\n        val factory = AudioClipViewModel.Factory(::MediaPlayer, scheduler)\n        val viewModel = ViewModelProvider(activity, factory)[AudioClipViewModel::class.java]\n        return ViewModelAudioPlayer(viewModel, lifecycleOwner)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/WaitingForDataRegistry.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport org.javarosa.core.model.FormIndex;\n\npublic interface WaitingForDataRegistry {\n\n    void waitForData(FormIndex index);\n\n    boolean isWaitingForData(FormIndex index);\n\n    void cancelWaitingForData();\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/utilities/WidgetAnswerDialogFragment.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.Toolbar\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.activityViewModels\nimport androidx.fragment.app.commit\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.ViewModelProvider\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.R\nimport org.odk.collect.android.databinding.WidgetAnswerDialogLayoutBinding\nimport org.odk.collect.android.formentry.FormEntryViewModel\nimport org.odk.collect.android.widgets.viewmodels.QuestionViewModel\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.material.MaterialFullScreenDialogFragment\nimport kotlin.reflect.KClass\n\nabstract class WidgetAnswerDialogFragment<T : Fragment>(\n    private val type: KClass<T>,\n    private val viewModelFactory: ViewModelProvider.Factory\n) : MaterialFullScreenDialogFragment() {\n\n    private val formEntryViewModel: FormEntryViewModel by activityViewModels { viewModelFactory }\n    private val questionViewModel: QuestionViewModel by viewModels { viewModelFactory }\n    private val prompt: FormEntryPrompt by lazy {\n        formEntryViewModel.getQuestionPrompt(requireArguments().getSerializable(ARG_FORM_INDEX) as FormIndex)\n    }\n    protected val constraintValidationResult by lazy {\n        questionViewModel.constraintValidationResult\n    }\n\n    abstract fun onCreateFragment(prompt: FormEntryPrompt): T\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        childFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(type) { onCreateFragment(prompt) }\n            .build()\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        val binding = WidgetAnswerDialogLayoutBinding.inflate(inflater)\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        if (childFragmentManager.fragments.isEmpty()) {\n            childFragmentManager.commit {\n                add(R.id.answer_fragment, type.java, null)\n            }\n        }\n    }\n\n    override fun getToolbar(): Toolbar? {\n        return null\n    }\n\n    override fun onBackPressed() {\n        dismiss()\n    }\n\n    override fun onCloseClicked() {\n        // No toolbar so not relevant\n    }\n\n    fun onValidate(answer: IAnswerData?) {\n        questionViewModel.validate(prompt.index, answer)\n    }\n\n    fun onAnswer(answer: IAnswerData?) {\n        formEntryViewModel.answerQuestion(prompt.index, answer)\n        dismiss()\n    }\n\n    companion object {\n        const val ARG_FORM_INDEX = \"form_index\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/video/ExVideoWidget.kt",
    "content": "package org.odk.collect.android.widgets.video\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.view.View\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.ComposeView\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.android.widgets.QuestionWidget\nimport org.odk.collect.android.widgets.interfaces.FileWidget\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver\nimport org.odk.collect.android.widgets.utilities.FileRequester\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\nimport org.odk.collect.androidshared.ui.ToastUtils.showLongToast\nimport org.odk.collect.strings.R\nimport timber.log.Timber\nimport java.io.File\n\n@SuppressLint(\"ViewConstructor\")\nclass ExVideoWidget(\n    context: Context,\n    questionDetails: QuestionDetails,\n    private val dependencies: Dependencies,\n    private val questionMediaManager: QuestionMediaManager,\n    private val waitingForDataRegistry: WaitingForDataRegistry,\n    private val fileRequester: FileRequester\n) : QuestionWidget(context, dependencies, questionDetails), FileWidget, WidgetDataReceiver {\n    private var binaryName by mutableStateOf<String?>(formEntryPrompt.getAnswerText())\n\n    init {\n        render()\n    }\n\n    override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View {\n        val readOnly = questionDetails.isReadOnly\n        val buttonFontSize = QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.BODY_LARGE)\n\n        return ComposeView(context).apply {\n            setContextThemedContent {\n                ExVideoWidgetContent(\n                    dependencies.mediaWidgetAnswerViewModel,\n                    formEntryPrompt,\n                    binaryName,\n                    readOnly,\n                    buttonFontSize,\n                    onLaunchClick = { launchExternalApp() },\n                    onLongClick = { showContextMenu() }\n                )\n            }\n        }\n    }\n\n    override fun deleteFile() {\n        questionMediaManager.deleteAnswerFile(\n            formEntryPrompt.getIndex().toString(),\n            questionMediaManager.getAnswerFile(binaryName)!!.absolutePath\n        )\n        binaryName = null\n    }\n\n    override fun clearAnswer() {\n        deleteFile()\n        widgetValueChanged()\n    }\n\n    override fun getAnswer(): IAnswerData? {\n        return binaryName?.let { StringData(it) }\n    }\n\n    override fun setData(answer: Any) {\n        if (binaryName != null) {\n            deleteFile()\n        }\n\n        if (answer is File && mediaUtils.isVideoFile(answer)) {\n            if (answer.exists()) {\n                questionMediaManager.replaceAnswerFile(\n                    formEntryPrompt.getIndex().toString(),\n                    answer.absolutePath\n                )\n                binaryName = answer.name\n                widgetValueChanged()\n            } else {\n                Timber.e(Error(\"Inserting Video file FAILED\"))\n            }\n        } else {\n            if (answer is File) {\n                showLongToast(R.string.invalid_file_type)\n                mediaUtils.deleteMediaFile(answer.absolutePath)\n                Timber.e(\n                    Error(\n                        \"ExVideoWidget's setBinaryData must receive a video file but received: \" + FileUtils.getMimeType(\n                            answer\n                        )\n                    )\n                )\n            } else {\n                Timber.e(Error(\"ExVideoWidget's setBinaryData must receive a video file but received: \" + answer.javaClass))\n            }\n        }\n    }\n\n    override fun setOnLongClickListener(listener: OnLongClickListener?) = Unit\n\n    private fun launchExternalApp() {\n        waitingForDataRegistry.waitForData(formEntryPrompt.getIndex())\n        fileRequester.launch(\n            context as Activity,\n            ApplicationConstants.RequestCodes.EX_VIDEO_CHOOSER,\n            formEntryPrompt\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/video/ExVideoWidgetContent.kt",
    "content": "package org.odk.collect.android.widgets.video\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.OpenInNew\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel\nimport org.odk.collect.android.widgets.WidgetAnswer\nimport org.odk.collect.android.widgets.WidgetIconButton\nimport org.odk.collect.androidshared.R.dimen\nimport org.odk.collect.androidshared.ui.compose.marginStandard\nimport org.odk.collect.strings.R.string\n\n@Composable\nfun ExVideoWidgetContent(\n    mediaWidgetAnswerViewModel: MediaWidgetAnswerViewModel,\n    formEntryPrompt: FormEntryPrompt,\n    answer: String?,\n    readOnly: Boolean,\n    buttonFontSize: Int,\n    onLaunchClick: () -> Unit,\n    onLongClick: () -> Unit\n) {\n    Column {\n        if (!readOnly) {\n            WidgetIconButton(\n                Icons.AutoMirrored.Filled.OpenInNew,\n                stringResource(string.launch_app),\n                buttonFontSize,\n                onLaunchClick,\n                onLongClick\n            )\n        }\n\n        WidgetAnswer(\n            Modifier.padding(top = marginStandard()),\n            formEntryPrompt,\n            answer,\n            mediaWidgetAnswerViewModel = mediaWidgetAnswerViewModel,\n            onLongClick = onLongClick\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/video/VideoWidget.kt",
    "content": "package org.odk.collect.android.widgets.video\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.ActivityNotFoundException\nimport android.content.Context\nimport android.content.Intent\nimport android.provider.MediaStore\nimport android.view.View\nimport android.widget.Toast\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.ComposeView\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.utilities.Appearances\nimport org.odk.collect.android.utilities.ApplicationConstants.RequestCodes\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.android.widgets.QuestionWidget\nimport org.odk.collect.android.widgets.interfaces.FileWidget\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\nimport org.odk.collect.permissions.PermissionListener\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.strings.R\nimport timber.log.Timber\nimport java.io.File\n\n@SuppressLint(\"ViewConstructor\")\nclass VideoWidget(\n    context: Context,\n    questionDetails: QuestionDetails,\n    private val dependencies: Dependencies,\n    private val questionMediaManager: QuestionMediaManager,\n    private val waitingForDataRegistry: WaitingForDataRegistry\n) : QuestionWidget(context, dependencies, questionDetails), FileWidget, WidgetDataReceiver {\n    private var binaryName by mutableStateOf<String?>(formEntryPrompt.getAnswerText())\n\n    init {\n        render()\n    }\n\n    override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View {\n        val readOnly = questionDetails.isReadOnly\n        val newVideoOnly = formEntryPrompt.appearanceHint?.lowercase()?.contains(Appearances.NEW) ?: false\n        val buttonFontSize = QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.BODY_LARGE)\n\n        return ComposeView(context).apply {\n            setContextThemedContent {\n                VideoWidgetContent(\n                    dependencies.mediaWidgetAnswerViewModel,\n                    formEntryPrompt,\n                    binaryName,\n                    readOnly,\n                    newVideoOnly,\n                    buttonFontSize,\n                    onRecordClick = {\n                        getPermissionsProvider().requestCameraPermission(\n                            context as Activity,\n                            object : PermissionListener {\n                                override fun granted() {\n                                    captureVideo()\n                                }\n                            }\n                        )\n                    },\n                    onChooseClick = { chooseVideo() },\n                    onLongClick = { showContextMenu() }\n                )\n            }\n        }\n    }\n\n    override fun deleteFile() {\n        questionMediaManager.deleteAnswerFile(\n            formEntryPrompt.getIndex().toString(),\n            questionMediaManager.getAnswerFile(binaryName)!!.absolutePath\n        )\n        binaryName = null\n    }\n\n    override fun clearAnswer() {\n        deleteFile()\n        widgetValueChanged()\n    }\n\n    override fun getAnswer(): IAnswerData? {\n        return binaryName?.let { StringData(it) }\n    }\n\n    override fun setData(answer: Any) {\n        if (binaryName != null) {\n            deleteFile()\n        }\n\n        if (answer is File) {\n            if (answer.exists()) {\n                questionMediaManager.replaceAnswerFile(\n                    formEntryPrompt.getIndex().toString(),\n                    answer.absolutePath\n                )\n                binaryName = answer.name\n                widgetValueChanged()\n            } else {\n                Timber.e(Error(\"Inserting Video file FAILED\"))\n            }\n        } else {\n            Timber.e(Error(\"VideoWidget's setBinaryData must receive a File or Uri object.\"))\n        }\n    }\n\n    override fun setOnLongClickListener(listener: OnLongClickListener?) = Unit\n\n    private fun captureVideo() {\n        val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)\n        val requestCode = RequestCodes.VIDEO_CAPTURE\n\n        // request high resolution if configured for that...\n        val highResolution =\n            settingsProvider.getUnprotectedSettings().getBoolean(ProjectKeys.KEY_HIGH_RESOLUTION)\n        if (highResolution) {\n            intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1)\n        }\n\n        try {\n            waitingForDataRegistry.waitForData(formEntryPrompt.getIndex())\n            (context as Activity).startActivityForResult(intent, requestCode)\n        } catch (_: ActivityNotFoundException) {\n            Toast.makeText(\n                context,\n                context.getString(\n                    R.string.activity_not_found,\n                    context.getString(R.string.capture_video)\n                ), Toast.LENGTH_SHORT\n            ).show()\n            waitingForDataRegistry.cancelWaitingForData()\n        }\n    }\n\n    private fun chooseVideo() {\n        val intent = Intent(Intent.ACTION_GET_CONTENT)\n        intent.type = \"video/*\"\n        try {\n            waitingForDataRegistry.waitForData(formEntryPrompt.getIndex())\n            (context as Activity).startActivityForResult(\n                intent,\n                RequestCodes.VIDEO_CHOOSER\n            )\n        } catch (_: ActivityNotFoundException) {\n            Toast.makeText(\n                context,\n                context.getString(\n                    R.string.activity_not_found,\n                    context.getString(R.string.choose_video)\n                ), Toast.LENGTH_SHORT\n            ).show()\n\n            waitingForDataRegistry.cancelWaitingForData()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/video/VideoWidgetAnswer.kt",
    "content": "package org.odk.collect.android.widgets.video\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.PlayCircle\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\n@Composable\nfun VideoWidgetAnswer(\n    modifier: Modifier,\n    answer: String,\n    mediaWidgetAnswerViewModel: MediaWidgetAnswerViewModel,\n    onLongClick: () -> Unit\n) {\n    val context = LocalContext.current\n\n    val bitmapFlow = remember(answer) { mediaWidgetAnswerViewModel.getFrame(answer, context) }\n    val bitmap by bitmapFlow.collectAsStateWithLifecycle()\n\n    Box(\n        modifier = modifier\n            .fillMaxWidth()\n            .heightIn(max = 200.dp)\n            .clip(MaterialTheme.shapes.large)\n            .combinedClickable(\n                onClick = {\n                    if (MultiClickGuard.allowClick()) {\n                        mediaWidgetAnswerViewModel.openFile(context, answer, \"video/*\")\n                    }\n                },\n                onLongClick = onLongClick,\n                onClickLabel = stringResource(org.odk.collect.strings.R.string.play_video)\n            ),\n        contentAlignment = Alignment.Center\n    ) {\n        bitmap?.let {\n            Image(\n                bitmap = it,\n                contentDescription = null,\n                contentScale = ContentScale.Crop,\n                modifier = Modifier.fillMaxSize()\n            )\n        } ?: Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .background(Color.Gray)\n        )\n\n        Icon(\n            imageVector = Icons.Default.PlayCircle,\n            contentDescription = null,\n            tint = MaterialTheme.colorScheme.onPrimary,\n            modifier = Modifier.size(64.dp)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/video/VideoWidgetContent.kt",
    "content": "package org.odk.collect.android.widgets.video\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.VideoLibrary\nimport androidx.compose.material.icons.filled.Videocam\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel\nimport org.odk.collect.android.widgets.WidgetAnswer\nimport org.odk.collect.android.widgets.WidgetIconButton\nimport org.odk.collect.androidshared.R.dimen\nimport org.odk.collect.androidshared.ui.compose.marginStandard\nimport org.odk.collect.strings.R.string\n\n@Composable\nfun VideoWidgetContent(\n    mediaWidgetAnswerViewModel: MediaWidgetAnswerViewModel,\n    formEntryPrompt: FormEntryPrompt,\n    answer: String?,\n    readOnly: Boolean,\n    newVideoOnly: Boolean,\n    buttonFontSize: Int,\n    onRecordClick: () -> Unit,\n    onChooseClick: () -> Unit,\n    onLongClick: () -> Unit\n) {\n    Column {\n        if (!readOnly) {\n            WidgetIconButton(\n                Icons.Default.Videocam,\n                stringResource(string.capture_video),\n                buttonFontSize,\n                onRecordClick,\n                onLongClick\n            )\n        }\n\n        if (!readOnly && !newVideoOnly) {\n            WidgetIconButton(\n                Icons.Default.VideoLibrary,\n                stringResource(string.choose_video),\n                buttonFontSize,\n                onChooseClick,\n                onLongClick,\n                Modifier\n                    .padding(top = marginStandard())\n            )\n        }\n\n        WidgetAnswer(\n            Modifier.padding(top = marginStandard()),\n            formEntryPrompt,\n            answer,\n            mediaWidgetAnswerViewModel = mediaWidgetAnswerViewModel,\n            onLongClick = onLongClick\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/viewmodels/DateTimeViewModel.java",
    "content": "package org.odk.collect.android.widgets.viewmodels;\n\nimport android.app.DatePickerDialog;\nimport android.app.TimePickerDialog;\n\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.lifecycle.ViewModel;\n\nimport org.joda.time.DateTime;\nimport org.joda.time.LocalDateTime;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.widgets.datetime.DateTimeUtils;\n\npublic class DateTimeViewModel extends ViewModel {\n    private final MutableLiveData<LocalDateTime> selectedDate = new MutableLiveData<>();\n    private final MutableLiveData<DateTime> selectedTime = new MutableLiveData<>();\n\n    private final DatePickerDialog.OnDateSetListener dateSetListener = (view, year, monthOfYear, dayOfMonth) ->\n            setSelectedDate(year, monthOfYear, dayOfMonth);\n\n    private final TimePickerDialog.OnTimeSetListener timeSetListener = (view, hourOfDay, minuteOfHour) -> {\n        view.clearFocus();\n        hourOfDay = view.getCurrentHour();\n        minuteOfHour = view.getCurrentMinute();\n        setSelectedTime(hourOfDay, minuteOfHour);\n    };\n\n    private LocalDateTime localDateTime;\n    private DatePickerDetails datePickerDetails;\n    private int dialogTheme;\n\n    public LiveData<LocalDateTime> getSelectedDate() {\n        return selectedDate;\n    }\n\n    public LiveData<DateTime> getSelectedTime() {\n        return selectedTime;\n    }\n\n    public void setSelectedDate(int year, int month, int day) {\n        this.selectedDate.postValue(DateTimeUtils.getSelectedDate(new LocalDateTime().withDate(year, month + 1, day), LocalDateTime.now()));\n    }\n\n    public void setSelectedTime(int hourOfDay, int minuteOfHour) {\n        selectedTime.postValue(new DateTime().withTime(hourOfDay, minuteOfHour, 0, 0));\n    }\n\n    public DatePickerDialog.OnDateSetListener getDateSetListener() {\n        return dateSetListener;\n    }\n\n    public TimePickerDialog.OnTimeSetListener getTimeSetListener() {\n        return timeSetListener;\n    }\n\n    public LocalDateTime getLocalDateTime() {\n        return localDateTime;\n    }\n\n    public void setLocalDateTime(LocalDateTime localDateTime) {\n        this.localDateTime = localDateTime;\n    }\n\n    public DatePickerDetails getDatePickerDetails() {\n        return datePickerDetails;\n    }\n\n    public void setDatePickerDetails(DatePickerDetails datePickerDetails) {\n        this.datePickerDetails = datePickerDetails;\n    }\n\n    public int getDialogTheme() {\n        return dialogTheme;\n    }\n\n    public void setDialogTheme(int dialogTheme) {\n        this.dialogTheme = dialogTheme;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/viewmodels/QuestionViewModel.kt",
    "content": "package org.odk.collect.android.widgets.viewmodels\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.core.model.data.IAnswerData\nimport org.odk.collect.android.formentry.FormSession\nimport org.odk.collect.android.formentry.FormSessionRepository\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.javarosawrapper.ValidationResult\nimport org.odk.collect.androidshared.livedata.LiveDataUtils\nimport org.odk.collect.async.Scheduler\n\nclass QuestionViewModel(\n    private val scheduler: Scheduler,\n    formSessionRepository: FormSessionRepository,\n    sessionId: String\n) : ViewModel() {\n    private val _constraintValidationResult: MutableLiveData<ValidationResult> = MutableLiveData()\n    val constraintValidationResult: LiveData<ValidationResult> = _constraintValidationResult\n    private var formController: FormController? = null\n    private var formSessionObserver = LiveDataUtils.observe(\n        formSessionRepository.get(sessionId)\n    ) { formSession: FormSession ->\n        formController = formSession.formController\n    }\n\n    fun validate(index: FormIndex, answer: IAnswerData?) {\n        scheduler.immediate {\n            formController?.validateAnswerConstraint(index, answer)?.let {\n                _constraintValidationResult.postValue(it)\n            }\n        }\n    }\n\n    override fun onCleared() {\n        formSessionObserver.cancel()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/android/widgets/warnings/SpacesInUnderlyingValuesWarning.java",
    "content": "package org.odk.collect.android.widgets.warnings;\n\nimport android.content.Context;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.collect.FluentIterable;\nimport com.google.common.collect.Lists;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.odk.collect.android.utilities.UnderlyingValuesConcat;\nimport org.odk.collect.android.widgets.QuestionWidget;\n\nimport java.util.List;\n\n/*\n * For questions with multiple selection, the responses chosen by the user are stored\n * as a space-separated list of the response values. Most form builders either warn\n * or error when a user tries to build a form with multiple selection questions\n * that have values with spaces but it's possible to include them when writing\n * XML by hand or using external choices.\n *\n * This class is responsible for checking if the underlying values in the set of answers\n * contain spaces. If the values contain spaces, the warning is rendered in QuestionWidget.\n *\n * The class is composed of 2 ingredients:\n * - SpacesInUnderlyingValues: check if the underlying values contain spaces\n * - WarningRenderer: create and display the warning text\n *\n * Both classes are combined in the {@link #renderWarningIfNecessary(List)}\n * that is responsible for applying the checker to values, and rendering warning if checker\n * detects an issue.\n *\n * The class can be extended to render warnings into other widgets or in other way.\n * To do that one should implement a new WarningRenderer, and create a new static\n * initializer that initializes SpacesInUnderlyingValuesWarning with the new renderer\n * as in {@link ##forQuestionWidget(QuestionWidget)}\n *\n * Created as fix for https://github.com/getodk/collect/issues/1964\n *\n */\npublic class SpacesInUnderlyingValuesWarning {\n\n    private final UnderlyingValuesChecker valuesChecker;\n    private final WarningRenderer warningRenderer;\n\n    public static SpacesInUnderlyingValuesWarning forQuestionWidget(QuestionWidget questionWidget) {\n        WarningRenderer renderer = new RenderIntoQuestionWidget(questionWidget);\n        UnderlyingValuesChecker valuesChecker = new SpacesInUnderlyingValues();\n        return new SpacesInUnderlyingValuesWarning(valuesChecker, renderer);\n    }\n\n    @VisibleForTesting\n    SpacesInUnderlyingValuesWarning(UnderlyingValuesChecker valuesChecker, WarningRenderer warningRenderer) {\n        this.valuesChecker = valuesChecker;\n        this.warningRenderer = warningRenderer;\n    }\n\n    public void renderWarningIfNecessary(List<SelectChoice> items) {\n        valuesChecker.check(items);\n\n        if (valuesChecker.hasInvalidValues()) {\n            warningRenderer.render(valuesChecker.getInvalidValues());\n        }\n    }\n\n    interface WarningRenderer {\n        void render(List<SelectChoice> items);\n    }\n\n    private static class RenderIntoQuestionWidget implements WarningRenderer {\n\n        private final QuestionWidget questionWidget;\n        private final WarningTextCreator warningCreator;\n\n        RenderIntoQuestionWidget(QuestionWidget questionWidget) {\n            this.questionWidget = questionWidget;\n            warningCreator = new SpacesInUnderlyingValuesTextCreator();\n        }\n\n        @Override\n        public void render(List<SelectChoice> invalidItems) {\n            questionWidget.showWarning(warningCreator.create(invalidItems, questionWidget.getContext()));\n        }\n    }\n\n    interface UnderlyingValuesChecker {\n        void check(List<SelectChoice> items);\n\n        boolean hasInvalidValues();\n\n        List<SelectChoice> getInvalidValues();\n    }\n\n    public static class SpacesInUnderlyingValues implements UnderlyingValuesChecker {\n\n        private List<SelectChoice> invalidValues = Lists.newArrayList();\n        private boolean checked;\n\n        @Override\n        public void check(List<SelectChoice> items) {\n            invalidValues = FluentIterable\n                    .from(items)\n                    .filter(item -> item.getValue() != null && item.getValue().contains(\" \"))\n                    .toList();\n            checked = true;\n        }\n\n        @Override\n        public boolean hasInvalidValues() {\n            checkInitialization();\n            return !invalidValues.isEmpty();\n        }\n\n        @Override\n        public List<SelectChoice> getInvalidValues() {\n            checkInitialization();\n            return invalidValues;\n        }\n\n        private void checkInitialization() {\n            if (!checked) {\n                throw new IllegalStateException(\"check() must be called before other methods first\");\n            }\n        }\n    }\n\n    public interface WarningTextCreator {\n        String create(List<SelectChoice> invalidValues, Context context);\n    }\n\n    private static class SpacesInUnderlyingValuesTextCreator implements WarningTextCreator {\n\n        private final UnderlyingValuesConcat formatter = new UnderlyingValuesConcat();\n\n        @Override\n        public String create(List<SelectChoice> invalidValues, Context context) {\n            return context.getResources().getString(\n                    invalidValues.size() > 1 ? org.odk.collect.strings.R.string.invalid_space_in_answer_plural : org.odk.collect.strings.R.string.invalid_space_in_answer_singular,\n                    formatter.asString(invalidValues));\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/utilities/Result.java",
    "content": "package org.odk.collect.utilities;\n\nimport javax.annotation.Nullable;\n\n/**\n * Based on https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/ which is not\n * available to Java.\n *\n * @deprecated use {@link org.odk.collect.shared.result.Result} instead\n */\n@Deprecated\npublic class Result<T> {\n\n    @Nullable\n    private final T value;\n\n    public Result(@Nullable T value) {\n        this.value = value;\n    }\n\n    public T getOrNull() {\n        return value;\n    }\n\n    public boolean isSuccess() {\n        return value != null;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/main/java/org/odk/collect/utilities/UserAgentProvider.java",
    "content": "package org.odk.collect.utilities;\n\npublic interface UserAgentProvider {\n\n\n    String getUserAgent();\n}\n"
  },
  {
    "path": "collect_app/src/main/res/anim/fade_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright (C) 2009 University of Washington\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n-->\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <alpha\n        android:duration=\"300\"\n        android:fromAlpha=\"0.0\"\n        android:toAlpha=\"1.0\" />\n</set>\n"
  },
  {
    "path": "collect_app/src/main/res/anim/fade_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright (C) 2009 University of Washington\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n-->\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <alpha\n        android:duration=\"300\"\n        android:fromAlpha=\"1.0\"\n        android:toAlpha=\"0.0\" />\n</set>\n"
  },
  {
    "path": "collect_app/src/main/res/anim/push_left_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright (C) 2009 University of Washington\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n-->\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <translate\n        android:duration=\"300\"\n        android:fromXDelta=\"100%p\"\n        android:toXDelta=\"0\" />\n    <alpha\n        android:duration=\"300\"\n        android:fromAlpha=\"0.0\"\n        android:toAlpha=\"1.0\" />\n</set>\n"
  },
  {
    "path": "collect_app/src/main/res/anim/push_left_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2009 University of Washington\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n-->\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <translate\n        android:duration=\"300\"\n        android:fromXDelta=\"0\"\n        android:toXDelta=\"-100%p\" />\n    <alpha\n        android:duration=\"300\"\n        android:fromAlpha=\"1.0\"\n        android:toAlpha=\"0.0\" />\n</set>\n"
  },
  {
    "path": "collect_app/src/main/res/anim/push_right_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2009 University of Washington\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n-->\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <translate\n        android:duration=\"300\"\n        android:fromXDelta=\"-100%p\"\n        android:toXDelta=\"0\" />\n    <alpha\n        android:duration=\"300\"\n        android:fromAlpha=\"0.0\"\n        android:toAlpha=\"1.0\" />\n</set>\n"
  },
  {
    "path": "collect_app/src/main/res/anim/push_right_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2009 University of Washington\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n-->\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <translate\n        android:duration=\"300\"\n        android:fromXDelta=\"0\"\n        android:toXDelta=\"100%p\" />\n    <alpha\n        android:duration=\"300\"\n        android:fromAlpha=\"1.0\"\n        android:toAlpha=\"0.0\" />\n</set>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/counter_minus_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"@color/color_on_surface_low_emphasis\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <corners\n                android:topLeftRadius=\"?dialogCornerRadius\"\n                android:bottomLeftRadius=\"?dialogCornerRadius\" />\n            <solid android:color=\"?colorSurfaceContainerHighest\" />\n        </shape>\n    </item>\n</ripple>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/counter_plus_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"@color/color_on_surface_low_emphasis\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <corners\n                android:topRightRadius=\"?dialogCornerRadius\"\n                android:bottomRightRadius=\"?dialogCornerRadius\" />\n            <solid android:color=\"?colorSurfaceContainerHighest\" />\n        </shape>\n    </item>\n</ripple>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/counter_value_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\" >\n    <item>\n        <shape android:shape=\"rectangle\">\n            <stroke\n                android:width=\"2dp\"\n                android:color=\"?colorSurfaceContainerHighest\" />\n        </shape>\n    </item>\n</layer-list>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_access_time.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_add_circle_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z\" />\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_arrow_back.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_arrow_drop_down.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M7,10l5,5 5,-5z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_arrow_up.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"120\"\n    android:viewportHeight=\"120\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M95 52.771a1.902 1.902 0 0 0-0.52-1.263L61.226 16.9a1.685 1.685 0 0 0-2.452 0L25.512 51.508a1.927 1.927 0 0 0-0.393 2.022c0.27 0.695 0.912 1.15 1.623 1.148h16.9v48.494c0 1.021 0.783 1.849 1.75 1.85h29.216c0.967-0.001 1.75-0.829 1.75-1.85L76.35 54.678h16.908a1.707 1.707 0 0 0 1.257-0.562 1.91 1.91 0 0 0 0.493-1.345H95z\" />\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_delete_72.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"72dp\"\n    android:height=\"72dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_delete_outline_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8,9h8v10L8,19L8,9zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_download_72.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"72dp\"\n    android:height=\"72dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_edit_72.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"72dp\"\n    android:height=\"72dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_error_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorError\">\n    <path\n        android:fillColor=\"?colorError\"\n        android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_help_outline_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_inbox_72.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"72dp\"\n    android:height=\"72dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M19,3L4.99,3c-1.11,0 -1.98,0.89 -1.98,2L3,19c0,1.1 0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.11 -0.9,-2 -2,-2zM19,15h-4c0,1.66 -1.35,3 -3,3s-3,-1.34 -3,-3L4.99,15L4.99,5L19,5v10z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_note_72.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"72dp\"\n    android:height=\"72dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M240,880q-33,0 -56.5,-23.5T160,800v-640q0,-33 23.5,-56.5T240,80h320l240,240v480q0,33 -23.5,56.5T720,880L240,880ZM520,360h200L520,160v200Z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_notifications_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?colorOnSurface\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z\" />\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_qr_code_scanner_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_refresh_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_refresh_error_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M3,12c0,2.21 0.91,4.2 2.36,5.64L3,20h6v-6l-2.24,2.24C5.68,15.15 5,13.66 5,12c0,-2.61 1.67,-4.83 4,-5.65L9,4.26C5.55,5.15 3,8.27 3,12zM11,17h2v-2h-2v2zM21,4h-6v6l2.24,-2.24C18.32,8.85 19,10.34 19,12c0,2.61 -1.67,4.83 -4,5.65v2.09c3.45,-0.89 6,-4.01 6,-7.74 0,-2.21 -0.91,-4.2 -2.36,-5.64L21,4zM11,13h2L13,7h-2v6z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_search_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_send_72.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"72dp\"\n    android:height=\"72dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_baseline_sort_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_cancel.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_chat_bubble_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?colorOnSurface\"\n        android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M20,2H4c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_check_circle_24.xml",
    "content": "<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_download_24.xml",
    "content": "<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_edit_24.xml",
    "content": "<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_folder_open.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_form_state_blank.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"120\"\n    android:viewportHeight=\"120\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n\n    <path\n            android:fillColor=\"?colorOnSurface\"\n            android:pathData=\"M67.195,26.83h-26.83c-3.688,0 -6.673,2.985 -6.673,6.633L33.66,86.537c0,3.648 2.984,6.634 6.673,6.634L80.61,93.171c3.689,0 6.707,-2.986 6.707,-6.634L87.317,46.732L67.195,26.829zM40.365,86.537L40.365,33.463h23.476L63.841,50.05L80.61,50.05v36.488L40.366,86.538z\"/>\n\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_form_state_finalized.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"120\"\n    android:viewportHeight=\"120\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n\n    <path\n        android:pathData=\"M67.195,26.83h-26.83c-3.688,0 -6.673,2.985 -6.673,6.633L33.66,86.537c0,3.648 2.984,6.634 6.673,6.634L80.61,93.171c3.689,0 6.707,-2.986 6.707,-6.634L87.317,46.732L67.195,26.829zM40.365,86.537L40.365,33.463h23.476L63.841,50.05L80.61,50.05v36.488L40.366,86.538z\"\n        android:fillColor=\"?colorOnSurface\"/>\n    <path\n        android:pathData=\"M49.261,67.52L56.824,75M56.824,75l15.124,-14.96\"\n        android:strokeWidth=\"8\"\n        android:strokeColor=\"?colorOnSurface\"\n        android:strokeLineCap=\"square\"/>\n\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_form_state_saved.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"120\"\n    android:viewportHeight=\"120\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n\n    <path\n        android:pathData=\"M67.195,26.83h-26.83c-3.688,0 -6.673,2.985 -6.673,6.633L33.66,86.537c0,3.648 2.984,6.634 6.673,6.634L80.61,93.171c3.689,0 6.707,-2.986 6.707,-6.634L87.317,46.732L67.195,26.829zM40.365,86.537L40.365,33.463h23.476L63.841,50.05L80.61,50.05v36.488L40.366,86.538z\"\n        android:fillColor=\"?colorOnSurface\"/>\n    <path\n        android:pathData=\"M45,80.5h20.667v-5L45,75.5v5zM45,56v5h31v-5L45,56zM45,70.75h31v-5L45,65.75v5z\"\n        android:fillColor=\"?colorOnSurface\"/>\n\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_form_state_submission_failed.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"120\"\n    android:viewportHeight=\"120\"\n    android:tint=\"?colorError\"\n    android:autoMirrored=\"true\">\n\n    <path\n            android:pathData=\"M88.175,48.487C85.568,35.262 73.953,25.333 60,25.333c-11.078,0 -20.7,6.287 -25.492,15.487C22.97,42.047 14,51.822 14,63.667c0,12.688 10.312,23 23,23h49.833C97.413,86.667 106,78.08 106,67.5c0,-10.12 -7.858,-18.323 -17.825,-19.013zM86.833,79H37c-8.472,0 -15.333,-6.862 -15.333,-15.333 0,-8.472 6.861,-15.334 15.333,-15.334h2.722C42.252,39.478 50.34,33 60,33c11.653,0 21.083,9.43 21.083,21.083V56h5.75c6.364,0 11.5,5.137 11.5,11.5S93.197,79 86.833,79z\"\n            android:fillColor=\"?colorError\"/>\n    <path\n            android:pathData=\"M64,73h-8v-7h8v7M64,60h-8V39h8v21\"\n            android:fillColor=\"?colorError\"/>\n\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_form_state_submitted.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"120\"\n    android:viewportHeight=\"120\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n\n    <path\n        android:pathData=\"M88.175,47.487C85.568,34.262 73.953,24.333 60,24.333c-11.078,0 -20.7,6.287 -25.492,15.487C22.97,41.047 14,50.822 14,62.667c0,12.688 10.312,23 23,23h49.833C97.413,85.667 106,77.08 106,66.5c0,-10.12 -7.858,-18.323 -17.825,-19.013zM86.833,78H37c-8.472,0 -15.333,-6.862 -15.333,-15.333 0,-8.472 6.861,-15.334 15.333,-15.334h2.722C42.252,38.478 50.34,32 60,32c11.653,0 21.083,9.43 21.083,21.083V55h5.75c6.364,0 11.5,5.137 11.5,11.5S93.197,78 86.833,78z\"\n        android:fillColor=\"?colorOnSurface\"/>\n    <path\n        android:pathData=\"M43.514,58.118L52.588,67M52.588,67l18.15,-17.764\"\n        android:strokeWidth=\"8\"\n        android:strokeColor=\"?colorOnSurface\"\n        android:strokeLineCap=\"square\"/>\n\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_information_outline.xml",
    "content": "<!-- drawable/information_outline.xml -->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z\" />\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_map.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\"\n        android:tint=\"@android:color/black\"\n        android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M20.5,3l-0.16,0.03L15,5.1 9,3 3.36,4.9c-0.21,0.07 -0.36,0.25 -0.36,0.48V20.5c0,0.28 0.22,0.5 0.5,0.5l0.16,-0.03L9,18.9l6,2.1 5.64,-1.9c0.21,-0.07 0.36,-0.25 0.36,-0.48V3.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM15,19l-6,-2.11V5l6,2.11V19z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_menu_share.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M18 16.08c-0.76 0-1.44 0.3 -1.96 0.77 L8.91 12.7c0.05-0.23 0.09 -0.46 0.09\n-0.7s-0.04-0.47-0.09-0.7l7.05-4.11c0.54 0.5 1.25 0.81 2.04 0.81 1.66 0 3-1.34\n3-3s-1.34-3-3-3-3 1.34-3 3c0 0.24 0.04 0.47 0.09 0.7L8.04 9.81C7.5 9.31 6.79 9 6\n9c-1.66 0-3 1.34-3 3s1.34 3 3 3c0.79 0 1.5-0.31 2.04-0.81l7.12 4.16c-0.05 0.21 -0.08\n0.43 -0.08 0.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31\n2.92-2.92s-1.31-2.92-2.92-2.92z\" />\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_navigate_back.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:strokeWidth=\"0.5\"\n        android:strokeColor=\"?colorOnSurface\"\n        android:pathData=\"M15.41,16.09l-4.58,-4.59 4.58,-4.59L14,5.5l-6,6 6,6z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_navigate_forward.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:strokeWidth=\"0.5\"\n        android:strokeColor=\"?colorOnSurface\"\n        android:pathData=\"M8.59,16.34l4.58,-4.59 -4.58,-4.59L10,5.75l6,6 -6,6z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_ondemand_video_black_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"@android:color/black\"\n        android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M21,3L3,3c-1.11,0 -2,0.89 -2,2v12c0,1.1 0.89,2 2,2h5v2h8v-2h5c1.1,0 1.99,-0.9 1.99,-2L23,5c0,-1.11 -0.9,-2 -2,-2zM21,17L3,17L3,5h18v12zM16,11l-7,4L9,7z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_assignment_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M7,15h7v2L7,17zM7,11h10v2L7,13zM7,7h10v2L7,9zM19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-0.14,0 -0.27,0.01 -0.4,0.04 -0.39,0.08 -0.74,0.28 -1.01,0.55 -0.18,0.18 -0.33,0.4 -0.43,0.64 -0.1,0.23 -0.16,0.49 -0.16,0.77v14c0,0.27 0.06,0.54 0.16,0.78s0.25,0.45 0.43,0.64c0.27,0.27 0.62,0.47 1.01,0.55 0.13,0.02 0.26,0.03 0.4,0.03h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,2.75c0.41,0 0.75,0.34 0.75,0.75s-0.34,0.75 -0.75,0.75 -0.75,-0.34 -0.75,-0.75 0.34,-0.75 0.75,-0.75zM19,19L5,19L5,5h14v14z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_cloud_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M12,6c2.62,0 4.88,1.86 5.39,4.43l0.3,1.5 1.53,0.11c1.56,0.1 2.78,1.41 2.78,2.96 0,1.65 -1.35,3 -3,3H6c-2.21,0 -4,-1.79 -4,-4 0,-2.05 1.53,-3.76 3.56,-3.97l1.07,-0.11 0.5,-0.95C8.08,7.14 9.94,6 12,6m0,-2C9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96C18.67,6.59 15.64,4 12,4z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_color_lens_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M12,22C6.49,22 2,17.51 2,12S6.49,2 12,2s10,4.04 10,9c0,3.31 -2.69,6 -6,6h-1.77c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.12 0.05,0.23 0.13,0.33 0.41,0.47 0.64,1.06 0.64,1.67 0,1.38 -1.12,2.5 -2.5,2.5zM12,4c-4.41,0 -8,3.59 -8,8s3.59,8 8,8c0.28,0 0.5,-0.22 0.5,-0.5 0,-0.16 -0.08,-0.28 -0.14,-0.35 -0.41,-0.46 -0.63,-1.05 -0.63,-1.65 0,-1.38 1.12,-2.5 2.5,-2.5L16,15c2.21,0 4,-1.79 4,-4 0,-3.86 -3.59,-7 -8,-7z\"/>\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M6.5,11.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0\"/>\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M9.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0\"/>\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M14.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0\"/>\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M17.5,11.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_delete_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M16,9v10H8V9h8m-1.5,-6h-5l-1,1H5v2h14V4h-3.5l-1,-1zM18,7H6v12c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_edit_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportHeight=\"24\"\n        android:viewportWidth=\"24\"\n        android:tint=\"@android:color/white\"\n        android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M14.06,9.02l0.92,0.92L5.92,19L5,19v-0.92l9.06,-9.06M17.66,3c-0.25,0 -0.51,0.1 -0.7,0.29l-1.83,1.83 3.75,3.75 1.83,-1.83c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.2,-0.2 -0.45,-0.29 -0.71,-0.29zM14.06,6.19L3,17.25L3,21h3.75L17.81,9.94l-3.75,-3.75z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_face_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M10.25,13c0,0.69 -0.56,1.25 -1.25,1.25S7.75,13.69 7.75,13s0.56,-1.25 1.25,-1.25 1.25,0.56 1.25,1.25zM15,11.75c-0.69,0 -1.25,0.56 -1.25,1.25s0.56,1.25 1.25,1.25 1.25,-0.56 1.25,-1.25 -0.56,-1.25 -1.25,-1.25zM22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12 6.48,2 12,2s10,4.48 10,10zM10.66,4.12C12.06,6.44 14.6,8 17.5,8c0.46,0 0.91,-0.05 1.34,-0.12C17.44,5.56 14.9,4 12,4c-0.46,0 -0.91,0.05 -1.34,0.12zM4.42,9.47c1.71,-0.97 3.03,-2.55 3.66,-4.44C6.37,6 5.05,7.58 4.42,9.47zM20,12c0,-0.78 -0.12,-1.53 -0.33,-2.24 -0.7,0.15 -1.42,0.24 -2.17,0.24 -3.13,0 -5.92,-1.44 -7.76,-3.69C8.69,8.87 6.6,10.88 4,11.86c0.01,0.04 0,0.09 0,0.14 0,4.41 3.59,8 8,8s8,-3.59 8,-8z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_forum_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M15,4v7L5.17,11L4,12.17L4,4h11m1,-2L3,2c-0.55,0 -1,0.45 -1,1v14l4,-4h10c0.55,0 1,-0.45 1,-1L17,3c0,-0.55 -0.45,-1 -1,-1zM21,6h-2v9L6,15v2c0,0.55 0.45,1 1,1h11l4,4L22,7c0,-0.55 -0.45,-1 -1,-1z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_info_small.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:tint=\"@android:color/white\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_lock_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM9,6c0,-1.66 1.34,-3 3,-3s3,1.34 3,3v2L9,8L9,6zM18,20L6,20L6,10h12v10zM12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_lock_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM9,6c0,-1.66 1.34,-3 3,-3s3,1.34 3,3v2L9,8L9,6zM18,20L6,20L6,10h12v10zM12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_lock_open_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h2c0,-1.66 1.34,-3 3,-3s3,1.34 3,3v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10zM12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_map_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M20.5,3l-0.16,0.03L15,5.1 9,3 3.36,4.9c-0.21,0.07 -0.36,0.25 -0.36,0.48L3,20.5c0,0.28 0.22,0.5 0.5,0.5l0.16,-0.03L9,18.9l6,2.1 5.64,-1.9c0.21,-0.07 0.36,-0.25 0.36,-0.48L21,3.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM10,5.47l4,1.4v11.66l-4,-1.4L10,5.47zM5,6.46l3,-1.01v11.7l-3,1.16L5,6.46zM19,17.54l-3,1.01L16,6.86l3,-1.16v11.84z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_menu_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_phonelink_setup_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M7,3v3h2L9,4h10v16L9,20v-2L7,18v3c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L21,3c0,-1.1 -0.9,-2 -2,-2L9,1c-1.1,0 -2,0.9 -2,2zM9.5,15.5c0.29,-0.12 0.55,-0.29 0.8,-0.48l-0.02,0.03 1.01,0.39c0.23,0.09 0.49,0 0.61,-0.22l0.84,-1.46c0.12,-0.21 0.07,-0.49 -0.12,-0.64l-0.85,-0.68 -0.02,0.03c0.02,-0.16 0.05,-0.32 0.05,-0.48s-0.03,-0.32 -0.05,-0.48l0.02,0.03 0.85,-0.68c0.19,-0.15 0.24,-0.43 0.12,-0.64l-0.84,-1.46c-0.12,-0.21 -0.38,-0.31 -0.61,-0.22l-1.01,0.39 0.02,0.03c-0.25,-0.17 -0.51,-0.34 -0.8,-0.46l-0.17,-1.08C9.3,7.18 9.09,7 8.84,7L7.16,7c-0.25,0 -0.46,0.18 -0.49,0.42L6.5,8.5c-0.29,0.12 -0.55,0.29 -0.8,0.48l0.02,-0.03 -1.02,-0.39c-0.23,-0.09 -0.49,0 -0.61,0.22l-0.84,1.46c-0.12,0.21 -0.07,0.49 0.12,0.64l0.85,0.68 0.02,-0.03c-0.02,0.15 -0.05,0.31 -0.05,0.47s0.03,0.32 0.05,0.48l-0.02,-0.03 -0.85,0.68c-0.19,0.15 -0.24,0.43 -0.12,0.64l0.84,1.46c0.12,0.21 0.38,0.31 0.61,0.22l1.01,-0.39 -0.01,-0.04c0.25,0.19 0.51,0.36 0.8,0.48l0.17,1.07c0.03,0.25 0.24,0.43 0.49,0.43h1.68c0.25,0 0.46,-0.18 0.49,-0.42l0.17,-1.08zM6,12c0,-1.1 0.9,-2 2,-2s2,0.9 2,2 -0.9,2 -2,2 -2,-0.9 -2,-2z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_qr_code_scanner_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_rate_review_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,16L5.17,16l-0.59,0.59 -0.58,0.58L4,4h16v12zM10.5,14L18,14v-2h-5.5zM14.36,8.13c0.2,-0.2 0.2,-0.51 0,-0.71l-1.77,-1.77c-0.2,-0.2 -0.51,-0.2 -0.71,0L6,11.53L6,14h2.47l5.89,-5.87z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_refresh_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_settings_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98 0,-0.34 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.09,-0.16 -0.26,-0.25 -0.44,-0.25 -0.06,0 -0.12,0.01 -0.17,0.03l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.06,-0.02 -0.12,-0.03 -0.18,-0.03 -0.17,0 -0.34,0.09 -0.43,0.25l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98 0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.09,0.16 0.26,0.25 0.44,0.25 0.06,0 0.12,-0.01 0.17,-0.03l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.06,0.02 0.12,0.03 0.18,0.03 0.17,0 0.34,-0.09 0.43,-0.25l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM17.45,11.27c0.04,0.31 0.05,0.52 0.05,0.73 0,0.21 -0.02,0.43 -0.05,0.73l-0.14,1.13 0.89,0.7 1.08,0.84 -0.7,1.21 -1.27,-0.51 -1.04,-0.42 -0.9,0.68c-0.43,0.32 -0.84,0.56 -1.25,0.73l-1.06,0.43 -0.16,1.13 -0.2,1.35h-1.4l-0.19,-1.35 -0.16,-1.13 -1.06,-0.43c-0.43,-0.18 -0.83,-0.41 -1.23,-0.71l-0.91,-0.7 -1.06,0.43 -1.27,0.51 -0.7,-1.21 1.08,-0.84 0.89,-0.7 -0.14,-1.13c-0.03,-0.31 -0.05,-0.54 -0.05,-0.74s0.02,-0.43 0.05,-0.73l0.14,-1.13 -0.89,-0.7 -1.08,-0.84 0.7,-1.21 1.27,0.51 1.04,0.42 0.9,-0.68c0.43,-0.32 0.84,-0.56 1.25,-0.73l1.06,-0.43 0.16,-1.13 0.2,-1.35h1.39l0.19,1.35 0.16,1.13 1.06,0.43c0.43,0.18 0.83,0.41 1.23,0.71l0.91,0.7 1.06,-0.43 1.27,-0.51 0.7,1.21 -1.07,0.85 -0.89,0.7 0.14,1.13zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_settings_applications_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M6.21,13.97l1.2,2.07c0.08,0.13 0.23,0.18 0.37,0.13l1.49,-0.6c0.31,0.24 0.64,0.44 1.01,0.59l0.22,1.59c0.03,0.14 0.15,0.25 0.3,0.25h2.4c0.15,0 0.27,-0.11 0.3,-0.26l0.22,-1.59c0.36,-0.15 0.7,-0.35 1.01,-0.59l1.49,0.6c0.14,0.05 0.29,0 0.37,-0.13l1.2,-2.07c0.08,-0.13 0.04,-0.29 -0.07,-0.39l-1.27,-0.99c0.03,-0.19 0.04,-0.39 0.04,-0.58 0,-0.2 -0.02,-0.39 -0.04,-0.59l1.27,-0.99c0.11,-0.09 0.15,-0.26 0.07,-0.39l-1.2,-2.07c-0.08,-0.13 -0.23,-0.18 -0.37,-0.13l-1.49,0.6c-0.31,-0.24 -0.64,-0.44 -1.01,-0.59l-0.22,-1.59c-0.03,-0.14 -0.15,-0.25 -0.3,-0.25h-2.4c-0.15,0 -0.27,0.11 -0.3,0.26l-0.22,1.59c-0.36,0.15 -0.71,0.34 -1.01,0.58l-1.49,-0.6c-0.14,-0.05 -0.29,0 -0.37,0.13l-1.2,2.07c-0.08,0.13 -0.04,0.29 0.07,0.39l1.27,0.99c-0.03,0.2 -0.05,0.39 -0.05,0.59 0,0.2 0.02,0.39 0.04,0.59l-1.27,0.99c-0.11,0.1 -0.14,0.26 -0.06,0.39zM12,10.29c0.94,0 1.71,0.77 1.71,1.71s-0.77,1.71 -1.71,1.71 -1.71,-0.77 -1.71,-1.71 0.77,-1.71 1.71,-1.71zM19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM19,19L5,19L5,5h14v14z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_share_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92c0,-1.61 -1.31,-2.92 -2.92,-2.92zM18,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM6,13c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM18,20.02c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_stars_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM19.47,9.16l-5.01,-0.43 -2,-4.71c3.21,0.19 5.91,2.27 7.01,5.14zM14.4,15.42L12,13.98l-2.39,1.44 0.63,-2.72 -2.11,-1.83 2.78,-0.24L12,8.06l1.09,2.56 2.78,0.24 -2.11,1.83 0.64,2.73zM11.54,4.02l-2,4.72 -5.02,0.43c1.1,-2.88 3.8,-4.97 7.02,-5.15zM4,12c0,-0.64 0.08,-1.26 0.23,-1.86l3.79,3.28 -1.11,4.75C5.13,16.7 4,14.48 4,12zM7.84,18.82L12,16.31l4.16,2.5c-1.22,0.75 -2.64,1.19 -4.17,1.19 -1.52,0 -2.94,-0.44 -4.15,-1.18zM17.09,18.17l-1.11,-4.75 3.79,-3.28c0.14,0.59 0.23,1.22 0.23,1.86 0,2.48 -1.14,4.7 -2.91,6.17z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_vpn_key_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M22,19h-6v-4h-2.68c-1.14,2.42 -3.6,4 -6.32,4 -3.86,0 -7,-3.14 -7,-7s3.14,-7 7,-7c2.72,0 5.17,1.58 6.32,4L24,9v6h-2v4zM18,17h2v-4h2v-2L11.94,11l-0.23,-0.67C11.01,8.34 9.11,7 7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5c2.11,0 4.01,-1.34 4.71,-3.33l0.23,-0.67L18,13v4zM7,15c-1.65,0 -3,-1.35 -3,-3s1.35,-3 3,-3 3,1.35 3,3 -1.35,3 -3,3zM7,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_warning_accent_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_outline_website_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorPrimary\">\n  <path\n      android:fillColor=\"?colorPrimary\"\n      android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2s0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2s0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2s-0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2s-0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_pause_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:width=\"24dp\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z\" />\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_person_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_play_arrow_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M8,5v14l11,-7z\" />\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_repeat.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_room_form_state_complete_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\"\n        android:tint=\"@color/formStateComplete\">\n    <path\n        android:fillColor=\"@color/formStateComplete\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_room_form_state_complete_48dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@color/formStateComplete\">\n    <path\n        android:fillColor=\"@color/formStateComplete\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_room_form_state_incomplete_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\"\n        android:tint=\"@color/formStateIncomplete\">\n    <path\n        android:fillColor=\"@color/formStateIncomplete\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_room_form_state_incomplete_48dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@color/formStateIncomplete\">\n    <path\n        android:fillColor=\"@color/formStateIncomplete\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_room_form_state_submission_failed_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\"\n        android:tint=\"@color/formStateSubmissionFailed\">\n    <path\n        android:fillColor=\"@color/formStateSubmissionFailed\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_room_form_state_submission_failed_48dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@color/formStateSubmissionFailed\">\n    <path\n        android:fillColor=\"@color/formStateSubmissionFailed\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_room_form_state_submitted_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\"\n        android:tint=\"@color/formStateSubmitted\">\n    <path\n        android:fillColor=\"@color/formStateSubmitted\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_room_form_state_submitted_48dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@color/formStateSubmitted\">\n    <path\n        android:fillColor=\"@color/formStateSubmitted\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_save_menu_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z\" />\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_send_24.xml",
    "content": "<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z\"/>\n</vector>"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_sort.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z\" />\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_sort_by_alpha.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M14.94,4.66h-4.72l2.36,-2.36zM10.25,19.37h4.66l-2.33,2.33zM6.1,6.27L1.6,17.73h1.84l0.92,-2.45h5.11l0.92,2.45h1.84L7.74,6.27L6.1,6.27zM4.97,13.64l1.94,-5.18 1.94,5.18L4.97,13.64zM15.73,16.14h6.12v1.59h-8.53v-1.29l5.92,-8.56h-5.88v-1.6h8.3v1.26l-5.93,8.6z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_sort_by_last_saved.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM19,19L5,19L5,5h11.17L19,7.83L19,19zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3zM6,6h9v4L6,10z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_splash_screen_icon.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"288dp\"\n    android:height=\"288dp\"\n    android:viewportWidth=\"76.2\"\n    android:viewportHeight=\"76.2\">\n  <path\n      android:pathData=\"M39.2339,126.9244m-25.4,0a25.4,25.4 0,1 1,50.8 0a25.4,25.4 0,1 1,-50.8 0\"\n      android:strokeLineJoin=\"miter\"\n      android:strokeWidth=\"32.59319305\"\n      android:fillColor=\"#00ff00\"\n      android:strokeColor=\"#00000000\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"M23.1828,22.0289h30.1931v31.7921h-30.1931z\"\n      android:strokeLineJoin=\"miter\"\n      android:strokeWidth=\"19.0265255\"\n      android:fillColor=\"#ffffff\"\n      android:strokeColor=\"#00000000\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"M34.5327,19.2988L41.6581,19.2988A0.255,0.255 135,0 1,41.9131 19.5538L41.9131,19.7617A0.255,0.255 45,0 1,41.6581 20.0166L34.5327,20.0166A0.255,0.255 45,0 1,34.2777 19.7617L34.2777,19.5538A0.255,0.255 135,0 1,34.5327 19.2988z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.8336795\"\n      android:fillColor=\"#b2b2a9\"\n      android:strokeColor=\"#00000000\"\n      android:strokeLineCap=\"butt\"/>\n  <path\n      android:pathData=\"m39.7792,17.9921c-0.4932,-0.0004 -1.0622,0.0014 -1.7184,0.0014 -4.4205,0 -4.4208,0 -4.624,0.2901 -0.1286,0.1836 -0.2037,0.5788 -0.2052,1.0768 -0.001,0.4327 -0.0451,0.8534 -0.0979,0.935 -0.0539,0.0835 -0.5562,0.1692 -1.1477,0.1958 -0.9197,0.0414 -1.1053,0.0925 -1.4783,0.4063l-0.4267,0.3589l-3.5857,0c-2.2013,0.6532 -3.708,1.7993 -4.712,3.3326 0.3916,0.0442 0.7811,0.0999 1.1647,0.1886 0.0941,0.0218 -0.0052,0.1931 -0.0072,0.2896 -0.0097,0.5107 -0.0101,1.0216 -0.0061,1.5323 -0.0093,-0.0864 -0.0086,-0.0536 -0.0059,0.0014 0.0934,0.4137 0.1229,0.9314 0.1519,1.3269 0.0773,0.4037 0.1406,0.813 0.153,1.2246 0.0082,0.2733 -0.0216,0.5419 -0.0426,0.8134 0.002,0.4708 0.0111,0.9414 0.0101,1.4123 -0.0035,0.4955 -0.002,0.9912 -0.002,1.4867 0.0001,0.7808 -0,1.5616 0,2.3425 0,0.5547 0,1.1092 0,1.6639l0,1.7454c0.0249,0.9191 -0.1495,1.8243 -0.3146,2.7248 0.0368,0.1798 0.0602,0.3618 0.0512,0.546 0.002,0.4733 -0.003,0.9467 -0.0051,1.4199 0.0092,0.4237 0.0517,0.8529 -0.0635,1.2631 -0.0005,0.1312 0.0001,0.2627 0.0041,0.3938 0.0094,0.5046 0.0146,1.0074 -0.0141,1.5117 -0.0061,0.1068 -0.0075,0.2146 -0.0237,0.3204 -0.0025,0.016 -0.0122,-0.0303 -0.0184,-0.0453 0.0077,0.1071 0.0243,0.214 0.0232,0.3215 -0.0039,0.3889 -0.0523,0.6208 -0.1833,0.9807 -0.0896,0.2463 -0.0305,0.1329 -0.0869,0.2368 -0.0235,0.0999 -0.1123,0.48 -0.1287,0.5318 -0.0114,0.0362 -0.0147,-0.2014 -0.0313,0.0211 -0.0103,0.137 0.0094,0.2752 0,0.4122 -0.0353,0.5104 -0.1518,0.9268 -0.3179,1.2514 0.0247,0.1701 0.0633,0.3348 0.0691,0.5086 0.003,0.0938 -0.0039,0.1877 -0.0059,0.2814 -0.019,0.1496 -0.0167,0.3008 -0.0128,0.452 0.0052,0.0084 0.0122,0.0251 0.0153,0.0332 0.0231,0.0604 0.0485,0.1205 0.0627,0.1835 0.0344,0.1528 0.0485,0.3099 0.0831,0.4627 0.0089,0.0394 -0.0069,-0.0829 0.0053,-0.1213 0.137,0.3578 0.3251,0.4623 0.4165,0.9187 0.0329,0.1641 0.0293,0.3334 0.0439,0.5001 -0.01,0.1819 -0.0497,0.3175 -0.0964,0.4754 -0.0959,0.3246 -0.2209,0.5955 -0.3633,0.8211 0.0656,0.0452 0.1366,0.097 0.1999,0.1443 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0614 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0467 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2553,0.017 0.4874,0.0313 0.6947,0.1081 0.0217,-0.0619 0.0907,-0.1185 0.2162,-0.1384 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0168 0.0179,-0.0365 0.0194,-0.0369 0.039,-0.067 -0.003,0.0042 -0.0101,0.0179 -0.0084,0.0127 0.0025,-0.007 0.0076,-0.0141 0.011,-0.0204 0.001,-0.0014 0.0041,-0.0056 0.0025,-0.007 -0.0015,-0.0014 -0.0046,0.007 -0.0041,0.0056 0.0052,-0.0255 0.0142,-0.0501 0.0212,-0.0752 0.0082,-0.0294 0.0219,-0.0569 0.0311,-0.0859 0.0035,-0.0113 0.0086,-0.0384 0.0107,-0.0484 0.011,-0.0508 0.0038,-0.0127 0.0122,-0.0629 0.003,-0.0619 0.0104,-0.1239 0.0229,-0.1846 0.0086,-0.0414 0.019,-0.0826 0.028,-0.1239 0.001,-0.0127 0.0031,-0.0255 0.0035,-0.0384 0.0005,-0.0182 -0.0031,-0.058 -0.0046,-0.0749 -0.0035,-0.0522 -0.0053,-0.104 -0.0053,-0.1563 0.003,-0.0963 0.002,-0.1927 0.0015,-0.2891 -0.0001,-0.0494 -0.0001,-0.099 0,-0.1484 0,-0.0149 0.002,-0.03 0,-0.0449 -0.0005,-0.0028 -0.0056,-0.0042 -0.0072,-0.007 -0.0049,-0.0099 -0.008,-0.022 -0.012,-0.0329 -0.0334,-0.0638 -0.0421,-0.1351 -0.0579,-0.2042 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0369 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1315 0.0247,-0.0056 0.0707,0.0084 0.1323,0.0346 0.0193,-2.2966 0.0217,-6.0324 0.0219,-12.4875 0.0005,-17.4274 -0.1905,-16.2475 -1.0438,-16.7678 -0.4058,-0.2474 -0.4149,-0.4883 -3.8014,-0.4888 -3.1019,-0.0004 -3.6295,-0.0289 -3.7744,-0.2034 -0.3182,-0.3834 -0.7141,-0.514 -1.7561,-0.5797l-1.0442,-0.0657 -0.0339,-0.7179c-0.0748,-1.5864 0.1779,-1.6958 -3.2747,-1.6983zM38.0551,19.5598c2.2675,0 3.5317,0.0467 3.5317,0.1305 0,0.0838 -1.2374,0.1305 -3.4511,0.1305 -2.1599,0 -3.4812,-0.0488 -3.5317,-0.1305 -0.053,-0.0857 1.1299,-0.1305 3.4511,-0.1305zM44.393,22.2819 L44.8172,22.6811c0.2333,0.2195 0.4242,0.5126 0.4242,0.6513 0,0.1451 -0.1802,0.361 -0.4242,0.5081 -0.4012,0.2418 -0.7692,0.2594 -6.7862,0.3258 -4.0789,0.045 -6.39,0.0244 -6.4406,-0.0573 -0.0434,-0.0701 -0.2201,-0.1812 -0.3926,-0.2468 -0.4306,-0.1637 -0.3932,-0.4483 0.1297,-0.9868l0.4436,-0.4566 6.3108,-0.0683zM47.062,23.778l0,0.0005c0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1199,0.0988 0.2104,0.1789 0.2868,0.2516 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.0229,0.0642 0.0494,0.1274 0.0737,0.1912 0.0803,0.1401 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0062,0.0504 -0.0149,0.1007 -0.0232,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0831,0.057 -0.1668,0.113 -0.2503,0.1693 0,0.0099 0,0.0191 0,0.0289 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.099c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3393 0,0.5088 -0,0.0988 -0.0001,0.1978 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0341 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1566,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1198 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0227,0.1934 0.0143,0.283 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 -0.0001,0.0177 -0.0005,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 -0,0.1368 0,0.2052 0.961,0.3941 1.4647,1.192 0.4494,2.0121 0.378,0.275 0.7394,0.5676 1.0574,0.907 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0303 -0.0199,0.0604 -0.0227,0.0883 -0.0725,0.5046 -0.2549,1.1415 -0.6444,1.4982 -0.0496,0.0453 0.1489,-0.2233 0.1063,-0.1713 -0.2712,0.3311 -0.3266,0.4199 -0.3543,0.4662 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.5096,1.6831 -0.515,1.5013 -1.6149,2.0294 -0.4215,0.0974 -0.3939,0.1026 -0.9269,0.1499 -0.3268,0.029 -0.6609,-0.0222 -0.9883,0 -0.2486,0 -0.497,0 -0.7456,0 -0.3724,-0 -0.7449,0.0003 -1.1173,0.0003 -0.9812,-0 -1.9624,-0.0005 -2.9436,0 -1.1724,0.001 -2.3448,0.0004 -3.5171,-0.0028 -0.9206,0.0028 -1.8416,0.0334 -2.7618,-0.0084 -0.3559,-0.0166 -0.7157,-0.0505 -1.0722,-0.0245 -0.7803,0.057 -1.5516,0.2104 -2.3363,0.2144 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -3.5018,-0.5896 -3.2037,-3.8131 -0.2514,-3.9564 -0.0006,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0006,0.0127 -0.0035,0.0191 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0018,-0.0428 -0.0028,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.0014,-0.232 0.001,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.003,-0.1273 0.0122,-0.1978 0.0377,-0.2868 0.0001,-0.0005 0.0001,-0.001 0.0003,-0.0014 0.0169,-0.059 0.0351,-0.115 0.0668,-0.2391 -0.0015,-0.0056 -0.0031,-0.0084 -0.0051,-0.0113 -0.0055,0.0162 -0.0094,0.0329 -0.0166,0.0484 -0.0094,0.0203 -0.007,0.001 -0.0013,-0.0286 -0.0101,0.0279 -0.0205,0.0787 -0.0298,0.133 0.0069,-0.0477 0.0164,-0.104 0.0349,-0.1563 0.0079,-0.0356 0.0184,-0.0781 0.0247,-0.1002 0.0223,-0.0771 0.0092,-0.0311 0.0153,-0.0512l0.0148,-5.9501 0.0319,-12.8661c-0.0135,-0.049 -0.0147,-0.0546 -0.0224,-0.0839 -0.3408,0.0828 -0.3286,0.0904 -0.3742,-0.1848 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1327 0.0025,-0.0004 0.0046,-0.0014 0.0072,-0.0014 0.2194,-0.0335 0.4733,-0.0833 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0328,-0 0.0656,0 0.0984,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,-0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,-0 0.0489,0l0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.028,0.0528 0.0304,0.1153 0.0344,0.1759l0.0543,0l0.0693,0.7179c0.0086,0.0883 0.0187,0.1695 0.0298,0.2483 0.0301,0.0529 0.0706,0.1346 0.0785,0.1507 0.0531,0.0876 0.0716,0.1876 0.0928,0.2863 0.0321,0.0624 0.0596,0.1258 0.0678,0.1879 0.0032,0.0084 0.0066,0.0176 0.01,0.0265 0.0038,0.0141 0.0009,0.0042 0.0038,0.0148 0.0051,0.0099 0.0046,0.007 0.013,0.026 0.0376,0.0863 0.0668,0.1783 0.0897,0.2692 0.001,0.0056 0.0018,0.0113 0.0028,0.017 0.0812,0.0999 0.1763,0.1877 0.2904,0.2677 0.2526,0.1769 1.1845,0.2024 7.3858,0.2024 6.3012,0 7.1367,-0.0238 7.4539,-0.2111 0.2271,-0.1342 0.3896,-0.2815 0.5083,-0.4785 0.0004,-0.0113 0,-0.0218 0.0005,-0.0327 -0.0003,-0.0929 -0.001,-0.1857 -0.001,-0.2786 0.0004,-0.1083 0.0003,-0.2165 0.0003,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0767,-0.1278 0.1311,-0.2289 0.1787,-0.321 -0.023,-0.0113 -0.0463,-0.022 -0.0673,-0.0362 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.0442,0.001 -0.0907,0.0028 -0.1313,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.017 0.0156,-0.007 0.0101,-0.0056 0.0235,-0.0113 0.0901,-0.0991 0.1829,-0.1941 0.3018,-0.2503 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0787 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458zM47.4377,23.9256c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9397c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8561c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8571c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.891c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.3937,24.0646c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1023c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5123c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM26.7714,25.0374c-0.0062,0.0014 -0.0144,0.0028 -0.0204,0.0056 0.0075,0.0259 0.0053,0.0169 0.0201,0.0695zM49.7736,36.5162c0.0101,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4533l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.9729,37.7612c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8621c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8677c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2253,49.821c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM54.2873,51.1048c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.222c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#b3a693\"/>\n  <path\n      android:pathData=\"m39.7792,17.9921c-0.4932,-0.0004 -1.062,0.0014 -1.7182,0.0014 -4.4205,0 -4.4211,0 -4.6243,0.2901 -0.1286,0.1836 -0.2037,0.5788 -0.2052,1.0768 -0.001,0.4327 -0.0451,0.8534 -0.0979,0.935 -0.0539,0.0835 -0.5562,0.1692 -1.1477,0.1958 -0.9197,0.0414 -1.1053,0.0925 -1.4783,0.4063l-0.4267,0.3589l-3.4529,0c-3.8459,0 -4.1556,0.0542 -4.6166,0.8078 -0.2621,0.4284 -0.264,0.5664 -0.2312,16.5081l0.0392,15.7483 0.3053,0.4561c0.0207,0.001 0.1152,0.1212 0.1358,0.124 0.068,0.0113 0.1069,0.1605 0.2475,0.2656 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.018,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0614 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0362 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,-0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2554,0.017 0.4876,0.0311 0.6949,0.1081 0.0221,-0.0618 0.0907,-0.1185 0.2159,-0.1384 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0152,-0.027 0.0318,-0.0538 0.0451,-0.0818 0.0445,-0.362 0.0761,-0.8957 0.0992,-1.6412 -0.0023,-0.0113 -0.0045,-0.0214 -0.0069,-0.0321 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0976 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1316 0.0293,-2.4517 0.024,-6.3269 0.024,-12.588l0,-16.0429l-0.283,-0.3597c-0.5272,-0.6702 -0.8425,-0.7196 -4.5914,-0.7196 -2.9586,0 -3.4708,-0.0287 -3.6153,-0.2029 -0.3182,-0.3834 -0.7141,-0.514 -1.7561,-0.5797l-1.0442,-0.0657 -0.0339,-0.7179c-0.0748,-1.5864 0.1779,-1.6958 -3.2747,-1.6983zM38.0551,19.5598c2.2675,0 3.5317,0.0467 3.5317,0.1305 0,0.0838 -1.2374,0.1305 -3.4511,0.1305 -2.1599,0 -3.4812,-0.0488 -3.5317,-0.1305 -0.053,-0.0857 1.1299,-0.1305 3.4511,-0.1305zM44.393,22.2819 L44.8172,22.6811c0.2333,0.2195 0.4242,0.5126 0.4242,0.6513 0,0.1451 -0.1802,0.361 -0.4242,0.5081 -0.4012,0.2418 -0.7692,0.2594 -6.7862,0.3258 -4.0789,0.045 -6.39,0.0244 -6.4406,-0.0573 -0.0434,-0.0701 -0.2201,-0.1812 -0.3926,-0.2468 -0.4306,-0.1637 -0.3932,-0.4483 0.1297,-0.9868l0.4436,-0.4566 6.3108,-0.0683zM47.0612,23.778 L47.062,23.7785c0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1199,0.0988 0.2104,0.1789 0.2868,0.2516 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1274 0.0737,0.1912 0.0803,0.1401 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0504 -0.015,0.1007 -0.0232,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0831,0.057 -0.1668,0.113 -0.2503,0.1693 0,0.0099 0,0.0191 0,0.0289 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.099c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3393 0,0.5088 -0,0.0988 -0.0001,0.1978 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0341 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1566,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1198 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0227,0.1934 0.0143,0.283 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 -0.0001,0.0177 -0.0005,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 -0,0.1368 0,0.2052 0.961,0.3941 1.4647,1.192 0.4494,2.0121 0.378,0.275 0.7394,0.5676 1.0574,0.907 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0303 -0.0199,0.0604 -0.0227,0.0883 -0.0725,0.5046 -0.2549,1.1415 -0.6444,1.4982 -0.0496,0.0453 0.1489,-0.2233 0.1063,-0.1713 -0.2712,0.3311 -0.3266,0.4199 -0.3543,0.4662 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.5096,1.6831 -0.515,1.5013 -1.6149,2.0294 -0.4215,0.0974 -0.3939,0.1026 -0.9269,0.1499 -0.3268,0.029 -0.6609,-0.0222 -0.9883,0l-0.7456,0c-0.3724,-0 -0.7449,0.0003 -1.1173,0.0003 -0.9812,-0 -1.9624,-0.0005 -2.9436,0 -1.1724,0.001 -2.3448,0.0004 -3.5171,-0.0028 -0.9206,0.0028 -1.8416,0.0334 -2.7618,-0.0084 -0.3559,-0.0166 -0.7157,-0.0505 -1.0722,-0.0245 -0.7803,0.057 -1.5516,0.2104 -2.3363,0.2144 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -3.5018,-0.5896 -3.2037,-3.8131 -0.2514,-3.9564 -0.0006,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0006,0.0127 -0.0035,0.0191 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0018,-0.0428 -0.0028,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.003,-0.1273 0.0122,-0.1978 0.0377,-0.2868 0.0001,-0.0005 0.0001,-0.001 0.0003,-0.0014 0.0169,-0.059 0.0351,-0.115 0.0668,-0.2391 -0.0015,-0.0056 -0.0028,-0.0084 -0.0048,-0.0113 -0.0055,0.0162 -0.0094,0.0329 -0.0166,0.0484 -0.0094,0.0203 -0.007,0.001 -0.0015,-0.0286 -0.0101,0.0279 -0.0205,0.0787 -0.0298,0.133 0.0069,-0.0477 0.0165,-0.104 0.0349,-0.1563 0.0079,-0.0356 0.0184,-0.0781 0.0247,-0.1002 0.0223,-0.0771 0.0092,-0.0311 0.0153,-0.0512l0.0148,-5.9501 0.0319,-12.8661c-0.0134,-0.0488 -0.0144,-0.0543 -0.0222,-0.0836 -0.3405,0.0828 -0.3284,0.0901 -0.374,-0.1851 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1864,-0.0127 0.3311,0.0412 0.4135,0.1326 0.0023,-0.0004 0.0044,-0.0014 0.0066,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.033,-0 0.0659,0 0.0989,0 0.0284,-0.0849 0.1477,-0.0387 0.5631,0 0.032,-0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,-0 0.0489,0l0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.028,0.0528 0.0304,0.1153 0.0344,0.1759l0.0535,0l0.0693,0.7178c0.0084,0.0876 0.0186,0.1681 0.0296,0.2463 0.0302,0.0528 0.0715,0.1364 0.0795,0.1527 0.0531,0.0876 0.0716,0.1876 0.0928,0.2863 0.0321,0.0624 0.0596,0.1258 0.0678,0.1879 0.0032,0.0084 0.0066,0.0176 0.01,0.0266 0.0038,0.0141 0.0009,0.0042 0.0038,0.0148 0.0051,0.0099 0.0046,0.007 0.013,0.026 0.0376,0.0863 0.0668,0.1783 0.0897,0.2692 0.001,0.0056 0.002,0.0127 0.0031,0.0183 0.081,0.0994 0.1757,0.1868 0.2893,0.2664 0.2526,0.1769 1.1845,0.2024 7.3858,0.2024 6.3012,0 7.1368,-0.0238 7.4539,-0.2111 0.2271,-0.1342 0.3896,-0.2815 0.5083,-0.4785 0.0004,-0.0113 0.0001,-0.0218 0.0005,-0.0327 -0.0003,-0.0929 -0.001,-0.1857 -0.001,-0.2786 0.0004,-0.1083 0.0003,-0.2166 0.0003,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0769,-0.128 0.1313,-0.2293 0.179,-0.3215 -0.0227,-0.0113 -0.0459,-0.0215 -0.0668,-0.0356 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.0445,0.001 -0.0912,0.0028 -0.132,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0585,-0.0535 0.1126,-0.1137 0.1667,-0.1739 0.0008,-0.0056 0.0018,-0.0099 0.0025,-0.016 0.0146,-0.007 0.0094,-0.0056 0.0219,-0.0113 0.0904,-0.0994 0.1834,-0.1947 0.3026,-0.2511 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458zM47.4377,23.9256c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9397c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8561c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8571c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.891c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.3937,24.0646c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1023c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5123c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM26.7606,25.0399c-0.0031,0.001 -0.0072,0.0014 -0.0101,0.0028 0.0075,0.0259 0.0055,0.0168 0.0201,0.0694zM49.7736,36.5162c0.0101,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4533l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.9729,37.7612c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8621c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.99,37.8677c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2253,49.821c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM54.2872,51.1048c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5891,55.222c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#a4a59a\"/>\n  <path\n      android:pathData=\"m39.7792,17.9921c-0.4932,-0.0004 -1.062,0.0014 -1.7182,0.0014 -4.4205,0 -4.4211,0 -4.6243,0.2901 -0.1286,0.1836 -0.2037,0.5788 -0.2052,1.0768 -0.001,0.4327 -0.0451,0.8534 -0.0979,0.935 -0.0539,0.0835 -0.5562,0.1692 -1.1477,0.1958 -0.9197,0.0414 -1.1053,0.0925 -1.4783,0.4063l-0.4267,0.3589l-3.4529,0c-3.8459,0 -4.1556,0.0542 -4.6166,0.8078 -0.2621,0.4284 -0.264,0.5664 -0.2312,16.5081l0.0559,15.836 0.3079,0.4369c0.0207,0.001 -0.0206,-0.0028 0,0 0.068,0.0113 0.2234,0.2161 0.364,0.3212 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.018,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0614 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0362 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,-0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2554,0.017 0.4876,0.0311 0.6949,0.1081 0.0221,-0.0618 0.0907,-0.1185 0.2159,-0.1384 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0152,-0.027 0.0318,-0.0538 0.0451,-0.0818 0.0445,-0.362 0.0761,-0.8957 0.0992,-1.6412 -0.0023,-0.0113 -0.0045,-0.0214 -0.0069,-0.0321 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0976 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1316 0.0293,-2.4517 0.024,-6.3269 0.024,-12.588l0,-16.0429l-0.283,-0.3597c-0.5272,-0.6702 -0.8425,-0.7196 -4.5914,-0.7196 -2.9586,0 -3.4708,-0.0287 -3.6153,-0.2029 -0.3182,-0.3834 -0.7141,-0.514 -1.7561,-0.5797l-1.0442,-0.0657 -0.0339,-0.7179c-0.0748,-1.5864 0.1779,-1.6958 -3.2747,-1.6983zM38.0551,19.4293c3.4602,0 4.0395,0.0572 3.7297,0.3676 -0.1007,0.1009 -6.7959,0.1643 -7.1482,0.0676 -0.1256,-0.0345 -0.2284,-0.1464 -0.2284,-0.2488 0,-0.1568 0.5756,-0.1864 3.6469,-0.1864zM37.9499,22.0591c1.1677,-0.001 2.3547,0.0099 3.2793,0.0355 3.269,0.0883 3.3712,0.099 3.6872,0.3918 0.2392,0.2217 0.3248,0.435 0.3243,0.8083 -0.0005,0.4341 -0.0613,0.5415 -0.4206,0.7462 -0.3867,0.2202 -0.9241,0.2438 -6.8189,0.3013l-6.399,0.0625 -0.3589,-0.3064c-0.5077,-0.4333 -0.4877,-1.0111 0.0523,-1.5157l0.4109,-0.3842 3.0805,-0.0974c0.8471,-0.0267 1.9953,-0.0405 3.1631,-0.0418zM47.0612,23.778 L47.062,23.7785c0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1199,0.0988 0.2104,0.1789 0.2868,0.2516 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1274 0.0737,0.1912 0.0803,0.1401 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0504 -0.015,0.1007 -0.0232,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0831,0.057 -0.1668,0.113 -0.2503,0.1693 0,0.0099 0,0.0191 0,0.0289 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.099c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3393 0,0.5088 -0,0.0988 -0.0001,0.1978 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0341 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1566,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1198 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0227,0.1934 0.0143,0.283 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 -0.0001,0.0177 -0.0005,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 -0,0.1368 0,0.2052 0.961,0.3941 1.4647,1.192 0.4494,2.0121 0.378,0.275 0.7394,0.5676 1.0574,0.907 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0303 -0.0199,0.0604 -0.0227,0.0883 -0.0725,0.5046 -0.2549,1.1415 -0.6444,1.4982 -0.0496,0.0453 0.1489,-0.2233 0.1063,-0.1713 -0.2712,0.3311 -0.3266,0.4199 -0.3543,0.4662 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.5096,1.6831 -0.515,1.5013 -1.6149,2.0294 -0.4215,0.0974 -0.3939,0.1026 -0.9269,0.1499 -0.3268,0.029 -0.6609,-0.0222 -0.9883,0l-0.7456,0c-0.3724,-0 -0.7449,0.0003 -1.1173,0.0003 -0.9812,-0 -1.9624,-0.0005 -2.9436,0 -1.1724,0.001 -2.3448,0.0004 -3.5171,-0.0028 -0.9206,0.0028 -1.8416,0.0334 -2.7618,-0.0084 -0.3559,-0.0166 -0.7157,-0.0505 -1.0722,-0.0245 -0.7803,0.057 -1.5516,0.2104 -2.3363,0.2144 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -3.5016,-0.5895 -3.2037,-3.8128 -0.2519,-3.9564 -0.0008,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0006,0.0127 -0.0035,0.0191 -0.0015,0.0028 -0.0013,-0.0056 -0.0018,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.0013,-0.232 0.0008,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.003,-0.1273 0.0122,-0.1978 0.0377,-0.2868 0.0001,-0.0005 0.0001,-0.001 0.0003,-0.0014 0.0169,-0.059 0.0351,-0.115 0.0668,-0.2391 -0.0015,-0.0056 -0.0028,-0.0084 -0.0048,-0.0113 -0.0055,0.0162 -0.0094,0.0329 -0.0166,0.0484 -0.0094,0.0203 -0.007,0.001 -0.0015,-0.0286 -0.0101,0.0279 -0.0205,0.0787 -0.0298,0.133 0.0069,-0.0477 0.0164,-0.104 0.0349,-0.1563 0.0079,-0.0356 0.0184,-0.0781 0.0247,-0.1002 0.0317,-0.1098 0.0185,-0.0638 0.0217,-0.0729 -0.0001,-1.6618 0.0025,-3.5574 0.0084,-5.9662l0.0319,-12.8284c-0.0134,-0.0488 -0.0144,-0.0543 -0.0222,-0.0836 -0.3403,0.0826 -0.3281,0.0901 -0.3737,-0.1851 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1864,-0.0127 0.3311,0.0412 0.4135,0.1326 0.0023,-0.0004 0.0044,-0.0014 0.0066,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.033,-0 0.0659,0 0.0989,0 0.0284,-0.0849 0.1477,-0.0387 0.5631,0 0.032,-0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,-0 0.0489,0l0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.028,0.0528 0.0304,0.1153 0.0344,0.1759l0.0535,0l0.0653,0.7237c0.0073,0.0815 0.0189,0.1585 0.0308,0.2348 0.0305,0.0522 0.074,0.1414 0.0823,0.1583 0.0531,0.0876 0.0716,0.1876 0.0928,0.2863 0.0321,0.0624 0.0596,0.1258 0.0678,0.1879 0.0032,0.0084 0.0066,0.0176 0.01,0.0266 0.0038,0.0141 0.0009,0.0042 0.0038,0.0148 0.0051,0.0099 0.0046,0.007 0.013,0.026 0.0355,0.0814 0.0631,0.1678 0.0854,0.2536 0.1312,0.1784 0.2914,0.3072 0.4777,0.3781 0.1937,0.0736 3.2104,0.1246 7.3695,0.1246l7.0414,0l0.4578,-0.4242c0.1312,-0.1216 0.2256,-0.2228 0.2988,-0.3286 -0.0003,-0.0826 -0.0013,-0.1653 -0.0013,-0.248 0.0004,-0.1083 0.0003,-0.2166 0.0003,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0769,-0.128 0.1313,-0.2293 0.179,-0.3215 -0.0227,-0.0113 -0.0458,-0.0215 -0.0665,-0.0356 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.0446,0.001 -0.0913,0.0028 -0.1323,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.1321,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0586,-0.0536 0.1127,-0.1142 0.167,-0.1744 0.0008,-0.0056 0.0018,-0.0099 0.0025,-0.0155 0.0143,-0.007 0.0092,-0.0056 0.0214,-0.0113 0.0904,-0.0995 0.1835,-0.1949 0.3028,-0.2513 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0008,-0.0001 0.0013,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0787 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458zM47.4377,23.9256c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9397c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8561c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8571c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.891c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.3937,24.0646c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1023c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5123c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM26.7706,25.0377c-0.0062,0.0014 -0.0144,0.0028 -0.0204,0.0056 0.0075,0.0259 0.0055,0.0168 0.0201,0.0694zM49.7736,36.5162c0.0101,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4533l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.9729,37.7613c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8622c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8678c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2253,49.821c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM54.2873,51.1048c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.222c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#9ea099\"/>\n  <path\n      android:pathData=\"m36.4451,17.9921l0,0.0003c-3.3606,0.0014 -3.1163,0.0969 -3.2115,1.4724l-0.0653,0.9435 -1.0442,0.0657c-1.0421,0.0656 -1.438,0.1963 -1.7561,0.5797 -0.1447,0.1743 -0.663,0.2029 -3.6805,0.2029 -3.9104,0 -4.2139,0.0524 -4.676,0.8079 -0.2621,0.4284 -0.264,0.5663 -0.2312,16.5081l0.0559,15.8359 0.4264,0.3392c0.0207,0.001 -0.0861,0.124 -0.0654,0.1268 0.068,0.0113 0.1704,0.1871 0.311,0.2922 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.018,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2553,0.017 0.4874,0.0313 0.6947,0.1081 0.0221,-0.0618 0.0909,-0.1185 0.2162,-0.1384 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0042 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0601 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0351 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0152,-0.027 0.0318,-0.0538 0.0451,-0.0818 0.0445,-0.362 0.0761,-0.8957 0.0992,-1.6412 -0.0023,-0.0113 -0.0045,-0.0215 -0.0069,-0.0321 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1315 0.0293,-2.4517 0.024,-6.3271 0.024,-12.588l0,-16.0429l-0.283,-0.3597c-0.5272,-0.6702 -0.8425,-0.7196 -4.5914,-0.7196 -3.0482,0 -3.4676,-0.0249 -3.626,-0.2159 -0.305,-0.3677 -0.9808,-0.5672 -1.9218,-0.5672 -1.0381,0 -1.0588,-0.0256 -1.0617,-1.3284 -0.003,-1.2114 0.246,-1.1515 -4.7764,-1.1515 -0.6387,0 -1.1922,-0.0014 -1.6723,-0.0014zM36.6401,19.324c0.4496,-0.0014 0.9527,0.0001 1.4966,0.0056 3.5473,0.0338 3.6459,0.0417 3.6459,0.2957 0,0.2541 -0.0986,0.262 -3.6905,0.2957 -3.2664,0.0307 -3.6996,0.0113 -3.7698,-0.1718 -0.0436,-0.1136 -0.0235,-0.2625 0.0446,-0.3306 0.0554,-0.0555 0.9242,-0.0895 2.2731,-0.0943zM38.3287,21.8793c3.2129,-0.0028 6.3944,0.105 6.7345,0.3232 0.1873,0.1202 0.346,1.1441 0.2417,1.5596 -0.0489,0.1949 -0.2371,0.3752 -0.4953,0.4744 -0.3001,0.1153 -2.2031,0.1764 -6.812,0.219 -7.483,0.0691 -7.1147,0.1253 -7.1113,-1.0852 0.0025,-0.8283 0.0514,-0.9379 0.52,-1.1584 0.4652,-0.2188 3.7095,-0.3304 6.9224,-0.3327zM47.061,23.7783 L47.062,23.7785c0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1199,0.0988 0.2104,0.1789 0.2868,0.2516 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1274 0.0737,0.1912 0.0803,0.1399 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.015,0.1007 -0.0232,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0831,0.057 -0.1668,0.113 -0.2503,0.1693 0,0.0099 0,0.0191 0,0.0289 -0,0.1305 0,0.261 0,0.3916l0,0.3316 0,0.3238 0,0.6429 0,0.099c0.0083,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1566,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0062,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0227,0.1934 0.0143,0.283 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 -0.0001,0.0177 -0.0005,0.0266 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 -0,0.1368 0,0.2052 0.961,0.3941 1.4647,1.192 0.4494,2.0121 0.378,0.275 0.7394,0.5676 1.0574,0.907 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.0169 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0141 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0725,0.5046 -0.2549,1.1415 -0.6444,1.4982 -0.0496,0.0453 0.1489,-0.2233 0.1063,-0.1713 -0.2712,0.3311 -0.3266,0.4199 -0.3543,0.4662 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.5096,1.6831 -0.515,1.5013 -1.6149,2.0294 -0.4215,0.0974 -0.3939,0.1026 -0.9269,0.1499 -0.3268,0.029 -0.6609,-0.0222 -0.9883,0l-0.7456,0c-0.3724,-0 -0.7449,0.0003 -1.1173,0.0003 -0.9812,-0 -1.9624,-0.0005 -2.9436,0 -1.1724,0.001 -2.3448,0.0004 -3.5171,-0.0028 -0.9206,0.0028 -1.8416,0.0334 -2.7618,-0.0084 -0.3559,-0.0166 -0.7157,-0.0505 -1.0722,-0.0245 -0.7803,0.057 -1.5516,0.2104 -2.3363,0.2144 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -3.5018,-0.5896 -3.2037,-3.8132 -0.2514,-3.9564 -0.0006,-0.0253 -0.0017,-0.0507 -0.002,-0.076 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0006,0.0127 -0.0035,0.0191 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0018,-0.0428 -0.0028,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.003,-0.1273 0.0122,-0.1978 0.0377,-0.2868 0.0001,-0.0005 0.0001,-0.001 0.0003,-0.0014 0.0169,-0.059 0.0351,-0.115 0.0668,-0.2391 -0.0015,-0.0056 -0.0028,-0.0084 -0.0048,-0.0113 -0.0055,0.0162 -0.0094,0.0329 -0.0166,0.0484 -0.0094,0.0203 -0.007,0.001 -0.0015,-0.0286 -0.0101,0.0279 -0.0205,0.0787 -0.0298,0.133 0.0069,-0.0477 0.0165,-0.104 0.0349,-0.1563 0.0079,-0.0356 0.0184,-0.078 0.0247,-0.1002 0.0122,-0.0422 0.0015,-0.0056 0.0084,-0.028 -0.0061,-1.8401 -0.0104,-3.8346 -0.0104,-6.0289l0,-12.8847c-0.298,0.0726 -0.2885,0.0688 -0.3322,-0.1948 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1864,-0.0127 0.3311,0.0412 0.4135,0.1326 0.0023,-0.0004 0.0044,-0.0014 0.0066,-0.0014 0.2194,-0.0335 0.4733,-0.0831 0.6651,0.0286 0.0475,0.0276 0.0869,0.0531 0.1262,0.0786 0.0331,-0 0.0661,0 0.0992,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0278,0.0525 0.0305,0.1146 0.0344,0.1749l0.0535,0.0014 0.078,0.7247c0.0104,0.0964 0.024,0.1874 0.0395,0.2751 0.025,0.0474 0.0548,0.1043 0.0609,0.1167 0.0531,0.0876 0.0716,0.1876 0.0928,0.2863 0.0321,0.0624 0.0596,0.1258 0.0678,0.1879 0.0032,0.0084 0.0066,0.0176 0.01,0.0265 0.0038,0.0141 0.0009,0.0042 0.0038,0.0148 0.0051,0.0099 0.0046,0.007 0.013,0.026 0.0266,0.0611 0.0478,0.1253 0.0668,0.1897 0.1364,0.1969 0.3098,0.3383 0.5226,0.4265 0.2522,0.1045 2.4084,0.1367 7.4439,0.1111l7.0891,-0.0359 0.3806,-0.4262c0.0991,-0.1109 0.1717,-0.2064 0.2274,-0.3021 -0.0003,-0.0691 -0.001,-0.1381 -0.001,-0.2072 0.0004,-0.1083 0.0003,-0.2166 0.0003,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0726,-0.1208 0.1246,-0.2171 0.1705,-0.3054 0.0008,-0.007 0.0017,-0.0127 0.0025,-0.0194 -0.0206,-0.0099 -0.0417,-0.0197 -0.0607,-0.0327 -0.0021,-0.0014 -0.0032,-0.0028 -0.0053,-0.0042 -0.0451,0.0014 -0.0915,0.0028 -0.1326,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0586,-0.0536 0.1128,-0.114 0.167,-0.1741 0.0008,-0.0056 0.0018,-0.0099 0.0025,-0.016 0.0146,-0.007 0.0094,-0.0056 0.0219,-0.0113 0.0903,-0.0992 0.1832,-0.1945 0.3023,-0.2509 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458zM47.4377,23.9256c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9397c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8561c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8571c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.891c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.3937,24.0646c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1023c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5123c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5162c0.0101,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4533l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.9729,37.7613c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8622c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8678c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2253,49.821c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM54.2873,51.1048c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.222c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#989994\"/>\n  <path\n      android:pathData=\"m36.4277,17.9921l0,0.0003c-3.395,0.0028 -3.1508,0.1135 -3.2255,1.698l-0.0339,0.7179 -1.0442,0.0717c-1.0519,0.0721 -1.4467,0.201 -1.7561,0.5738 -0.1447,0.1743 -0.663,0.2029 -3.6805,0.2029 -3.9104,0 -4.2139,0.0524 -4.676,0.8079 -0.2621,0.4284 -0.264,0.5663 -0.2312,16.5081l0.0391,15.7482 0.4431,0.4269c0.0207,0.001 0.0004,0.1068 0.021,0.1097 0.068,0.0113 0.084,0.2042 0.2246,0.3093 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.018,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2553,0.017 0.4874,0.0313 0.6947,0.1081 0.0221,-0.0618 0.0909,-0.1185 0.2162,-0.1384 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0042 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0601 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0351 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0152,-0.0272 0.032,-0.0539 0.0454,-0.0821 0.0445,-0.3619 0.0759,-0.8956 0.0989,-1.6409 -0.0023,-0.0113 -0.0045,-0.0215 -0.0069,-0.0321 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1315 0.0293,-2.4517 0.024,-6.3271 0.024,-12.588l0,-16.0429l-0.283,-0.3597c-0.5273,-0.6704 -0.8419,-0.7196 -4.598,-0.7196 -2.9322,0 -3.4838,-0.0304 -3.654,-0.2006 -0.3622,-0.3623 -1.1013,-0.5825 -1.9537,-0.5825 -0.9748,0 -0.997,-0.0267 -0.997,-1.2076 0,-1.3441 0.2699,-1.2723 -4.7749,-1.2723 -0.6451,0 -1.2044,-0.0014 -1.6894,-0.0014zM34.2777,19.2988l3.7891,0c3.32,0 3.7989,0.0255 3.8677,0.2047 0.0432,0.1125 0.0092,0.274 -0.0757,0.3589 -0.1097,0.1099 -1.1207,0.1592 -3.5087,0.1713 -3.9837,0.0201 -4.0724,0.0113 -4.0724,-0.4173zM37.9713,21.756l0,0.0003c2.002,-0.0056 4.0247,0.0283 5.4349,0.1129l1.7541,0.1053 0.1185,0.4568c0.1283,0.4938 0.0679,1.7654 -0.0915,1.9249 -0.0539,0.0539 -1.5761,0.1363 -3.3828,0.1828l0,-0.0003c-4.3024,0.1108 -10.1182,0.1088 -10.5203,-0.0028 -0.2049,-0.0573 -0.3558,-0.2266 -0.4244,-0.4765 -0.1331,-0.4848 0.0089,-1.8628 0.2047,-1.9871 0.2907,-0.1844 3.5701,-0.3057 6.9068,-0.3156zM47.0612,23.7783 L47.062,23.7785c0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1199,0.0988 0.2104,0.1789 0.2868,0.2516 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1274 0.0737,0.1912 0.0803,0.1399 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.015,0.1007 -0.0232,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0831,0.057 -0.1668,0.113 -0.2503,0.1693 0,0.0099 0,0.0191 0,0.0289 -0,0.1305 0,0.261 0,0.3916l0,0.3316 0,0.3238 0,0.6429 0,0.099c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1566,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0227,0.1934 0.0143,0.283 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 -0.0001,0.0177 -0.0005,0.0266 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 -0,0.1368 0,0.2052 0.961,0.3941 1.4647,1.192 0.4494,2.0121 0.378,0.275 0.7394,0.5676 1.0574,0.907 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.0169 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0141 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0725,0.5046 -0.2549,1.1415 -0.6444,1.4982 -0.0496,0.0453 0.1489,-0.2233 0.1063,-0.1713 -0.2712,0.3311 -0.3266,0.4199 -0.3543,0.4662 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.5096,1.6831 -0.515,1.5013 -1.6149,2.0294 -0.4215,0.0974 -0.3939,0.1026 -0.9269,0.1499 -0.3268,0.029 -0.6609,-0.0222 -0.9883,0l-0.7456,0c-0.3724,-0 -0.7449,0.0003 -1.1173,0.0003 -0.9812,-0 -1.9624,-0.0005 -2.9436,0 -1.1724,0.001 -2.3448,0.0004 -3.5171,-0.0028 -0.9206,0.0028 -1.8416,0.0334 -2.7618,-0.0084 -0.3559,-0.0166 -0.7157,-0.0505 -1.0722,-0.0245 -0.7803,0.057 -1.5516,0.2104 -2.3363,0.2144 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -3.5018,-0.5896 -3.2037,-3.8132 -0.2514,-3.9564 -0.0008,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0004,0.0127 -0.0032,0.0191 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.001,-0.0532 0.0015,-0.0953 0.0059,-0.134 -0.0461,-7.1082 -0.0452,-17.4844 0.0296,-19.4321 -0.2195,0.051 -0.2185,0.0262 -0.258,-0.2123 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1864,-0.0127 0.3311,0.0412 0.4135,0.1326 0.0024,-0.0004 0.0045,-0.0014 0.0069,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0329,-0 0.0658,0 0.0987,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0179,0.0339 0.0251,0.0718 0.0293,0.1106l0.0918,0l0.0599,0.7232c0.0121,0.146 0.0328,0.2839 0.0617,0.413 0.0001,0.0003 0.0001,0.0005 0.0003,0.001 0.0069,0.0141 0.0211,0.0403 0.0235,0.0452 0.0531,0.0876 0.0716,0.1876 0.0928,0.2863 0.031,0.0602 0.0568,0.1215 0.066,0.1815 0.1524,0.2956 0.3711,0.5195 0.6544,0.6651 0.4861,0.2498 13.796,0.234 14.3969,-0.017 0.3352,-0.1401 0.5692,-0.3426 0.7079,-0.6149 -0.0003,-0.068 -0.001,-0.136 -0.001,-0.2039 0.0004,-0.1083 0.0005,-0.2166 0.0005,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0653,-0.1087 0.1134,-0.1966 0.1563,-0.2781 0.0006,-0.019 0.0013,-0.0363 0.002,-0.0541 -0.0155,-0.0084 -0.0319,-0.0151 -0.0464,-0.0249 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.0453,0.0014 -0.0912,0.0028 -0.132,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.1321,-0.0519 -0.2202,-0.0512 -0.2465,-0.1977 0.1244,-0.5374 0.0585,-0.0535 0.1126,-0.1137 0.1667,-0.1739 0.0008,-0.0056 0.0018,-0.0099 0.0025,-0.0163 0.0149,-0.007 0.0096,-0.0056 0.0224,-0.0113 0.0902,-0.0992 0.1831,-0.1943 0.3021,-0.2506 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458zM47.4377,23.9256c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9397c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8561c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8571c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.891c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.3937,24.0646c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1023c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5123c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5162c0.0101,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4533l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.9729,37.7613c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8622c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8678c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2253,49.821c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM54.2873,51.1048c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.222c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#95968a\"/>\n  <path\n      android:pathData=\"m36.3839,17.9921l0,0.0003c-3.3434,0.0028 -3.1071,0.115 -3.1817,1.698l-0.0339,0.7179 -1.0482,0.078c-1.0657,0.0794 -1.4504,0.204 -1.7521,0.5675 -0.1447,0.1743 -0.663,0.2029 -3.6805,0.2029 -3.9104,0 -4.2139,0.0524 -4.676,0.8079 -0.2621,0.4284 -0.264,0.5663 -0.2312,16.5081l0.0391,15.7482 0.3852,0.4411c0.0207,0.001 -0.04,0.0393 -0.0194,0.0421 0.068,0.0113 0.1822,0.2575 0.3228,0.3626 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.018,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2553,0.017 0.4874,0.0313 0.6947,0.1081 0.0221,-0.0618 0.0909,-0.1185 0.2162,-0.1384 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0042 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0601 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0351 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0152,-0.0272 0.032,-0.0539 0.0454,-0.0821 0.0445,-0.3619 0.0759,-0.8956 0.0989,-1.6409 -0.0023,-0.0113 -0.0045,-0.0215 -0.0069,-0.0321 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1315 0.0293,-2.4517 0.024,-6.3271 0.024,-12.588l0,-16.0429l-0.283,-0.3597c-0.5285,-0.6719 -0.8374,-0.7196 -4.6566,-0.7196 -3.0176,0 -3.5359,-0.0286 -3.6805,-0.2029 -0.3017,-0.3635 -0.6864,-0.4881 -1.7521,-0.5675l-1.0482,-0.078 -0.0653,-1.0222c-0.0519,-0.8135 -0.1171,-1.0601 -0.3187,-1.2073 -0.2064,-0.1507 -1.0455,-0.1851 -4.5261,-0.1851 -0.6351,0 -1.1857,-0.0014 -1.6634,-0.0014zM36.0629,19.1866c0.5196,-0.0003 1.1876,0.0042 2.0473,0.0127l3.8029,0.0346l0,0.3916 0,0.3916l-3.8083,0.0346c-3.6249,0.0329 -3.8121,0.0228 -3.886,-0.2101 -0.0427,-0.1346 -0.0403,-0.3418 0.0053,-0.4606 0.0562,-0.1465 0.2798,-0.1931 1.8387,-0.194zM38.1433,21.4524c6.6382,0 6.8639,0.0084 7.0432,0.2534 0.2573,0.3518 0.2575,2.5348 0.0003,2.8867 -0.1787,0.2445 -0.4305,0.2548 -7.1803,0.2924 -6.6321,0.037 -6.9999,0.0267 -7.0911,-0.1958 -0.0529,-0.1291 -0.0964,-0.8053 -0.0964,-1.5027 0,-1.0433 0.0412,-1.3093 0.233,-1.501 0.2178,-0.2176 0.683,-0.233 7.0914,-0.233zM47.0615,23.7783 L47.062,23.7785c0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1199,0.0988 0.2104,0.1789 0.2868,0.2516 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1274 0.0737,0.1912 0.0803,0.1399 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.015,0.1007 -0.0232,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0831,0.057 -0.1668,0.113 -0.2503,0.1693 0,0.0099 0,0.0191 0,0.0289 -0,0.1305 0,0.261 0,0.3916l0,0.3316 0,0.3238 0,0.6429 0,0.099c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1566,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0227,0.1934 0.0143,0.283 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 -0.0001,0.0177 -0.0005,0.0266 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 -0,0.1368 0,0.2052 0.961,0.3941 1.4647,1.192 0.4494,2.0121 0.378,0.275 0.7394,0.5676 1.0574,0.907 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.0169 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0141 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0725,0.5046 -0.2549,1.1415 -0.6444,1.4982 -0.0496,0.0453 0.1489,-0.2233 0.1063,-0.1713 -0.2712,0.3311 -0.3266,0.4199 -0.3543,0.4662 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.5096,1.6831 -0.515,1.5013 -1.6149,2.0294 -0.4215,0.0974 -0.3939,0.1026 -0.9269,0.1499 -0.3268,0.029 -0.6609,-0.0222 -0.9883,0l-0.7456,0c-0.3724,-0 -0.7449,0.0003 -1.1173,0.0003 -0.9812,-0 -1.9624,-0.0005 -2.9436,0 -1.1724,0.001 -2.3448,0.0004 -3.5171,-0.0028 -0.9206,0.0028 -1.8416,0.0334 -2.7618,-0.0084 -0.3559,-0.0166 -0.7157,-0.0505 -1.0722,-0.0245 -0.7803,0.057 -1.5516,0.2104 -2.3363,0.2144 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -3.5019,-0.5896 -3.2037,-3.8133 -0.2511,-3.9564 -0.0006,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0023,-0.362 0.0018,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0191,-0.1575 0.0025,-0.3147 0.0189,-0.4714 -0,-0.0151 0,-0.0366 0,-0.0518 -0.001,-0.0198 -0.0013,-0.0396 -0.0023,-0.0594 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.001,-0.0362 0.0001,-0.0669 0.002,-0.0956 -0.001,-2.0288 -0.0003,-4.2657 0.0066,-6.7735 0.1903,-2.7615 -0.0863,-10.2464 0.2529,-12.7504 -0.4514,0.1043 -0.4344,0.1423 -0.4843,-0.1591 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1064 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1327 0.0025,-0.0004 0.0046,-0.0014 0.0072,-0.0014 0.2194,-0.0335 0.4733,-0.0833 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0328,-0 0.0656,0 0.0984,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0171,0.0322 0.0245,0.0681 0.0288,0.105l0.0877,-0.0014 0.0755,0.7319c0.017,0.1647 0.04,0.3141 0.0701,0.4497 0.0006,0.0014 0.0039,0.007 0.0044,0.0084 0.0531,0.0876 0.0716,0.1876 0.0928,0.2863 0.0318,0.0618 0.0588,0.1249 0.0673,0.1863 0.1533,0.3034 0.3819,0.5025 0.712,0.6404 0.6537,0.2731 13.6912,0.2731 14.3449,0 0.3577,-0.1495 0.5707,-0.357 0.7008,-0.7288 -0.0001,-0.029 -0.0003,-0.0581 -0.0003,-0.0871 0.0004,-0.1083 0.0003,-0.2165 0.0003,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0691,-0.115 0.1192,-0.2071 0.1637,-0.2921l0.0032,-0.0355c-0.0186,-0.0099 -0.0381,-0.0179 -0.0553,-0.0296 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.0449,0.0014 -0.0908,0.0028 -0.1315,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.1321,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0583,-0.0534 0.112,-0.1135 0.166,-0.1733 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.0166 0.0153,-0.007 0.0099,-0.0056 0.0229,-0.0113 0.0902,-0.0992 0.1831,-0.1943 0.3021,-0.2506 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0003,-0.0001 0.0008,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0196 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.141 0.3288,-0.1456zM47.4377,23.9256c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9397c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8561c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8571c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.891c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.3937,24.0646c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1023c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5123c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5162c0.0101,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4533l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.9729,37.7613c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8622c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8678c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2253,49.821c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM54.2873,51.1048c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.222c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#a18966\"/>\n  <path\n      android:pathData=\"m36.3839,17.9921l0,0.0003c-3.3434,0.0028 -3.1071,0.115 -3.1817,1.698l-0.0339,0.7179 -0.5873,0.0324c-1.2471,0.0686 -1.6844,0.1822 -2.1617,0.5616l-0.4846,0.3849l-3.5146,0c-3.6485,0 -3.8728,0.0297 -4.3105,0.57 -0.2109,0.2604 -0.2254,1.3262 -0.2284,16.4778 -0.0028,15.1974 0.0202,16.0001 0.2435,16.342 0.0055,0.0005 0.2549,-0.0214 0.1546,0.0621 0.068,0.0113 0.0882,0.2229 0.2287,0.328 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0245 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,-0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2213,0.0148 0.4248,0.0279 0.6103,0.0805 0.03,-0.0003 0.0665,-0.0003 0.0964,-0.0005 0.0313,-0.0504 0.098,-0.0936 0.2042,-0.1104 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0042 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.041 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0251 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0169 0.0179,-0.0365 0.0194,-0.0369 0.039,-0.067 -0.003,0.0042 -0.0101,0.0179 -0.0084,0.0127 0.0025,-0.007 0.0076,-0.0141 0.011,-0.0204 0.001,-0.0014 0.0041,-0.0056 0.0025,-0.007 -0.0015,-0.0014 -0.0046,0.007 -0.0041,0.0056 0.0052,-0.0255 0.0142,-0.0501 0.0212,-0.0752 0.0082,-0.0294 0.0219,-0.0569 0.0311,-0.0859 0.0035,-0.0113 0.0086,-0.0384 0.0107,-0.0484 0.011,-0.0508 0.0038,-0.0127 0.0122,-0.0629 0.003,-0.0618 0.0104,-0.1239 0.0229,-0.1846 0.0035,-0.017 0.0076,-0.0338 0.0115,-0.0507l0,-0.8451c-0.0024,-0.007 -0.0049,-0.0141 -0.0073,-0.0204 -0.0334,-0.0638 -0.0421,-0.1351 -0.0579,-0.2042 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1315 0.0065,-0.0014 0.0156,-0.0001 0.024,0.0005l0,-12.4404c0,-17.1395 0.0287,-16.4429 -0.6954,-16.9173 -0.299,-0.1959 -0.7662,-0.2226 -3.9156,-0.2226L46.1908,21.3906l-0.4846,-0.3849c-0.4773,-0.3793 -0.9146,-0.493 -2.1617,-0.5616l-0.5873,-0.0324 -0.0653,-1.0222c-0.0519,-0.8135 -0.1171,-1.0601 -0.3187,-1.2073 -0.2064,-0.1507 -1.0455,-0.1851 -4.5261,-0.1851 -0.6351,0 -1.1857,-0.0014 -1.6634,-0.0014zM36.0629,19.1866c0.5196,-0.0003 1.1876,0.0042 2.0473,0.0127l3.8029,0.0346l0,0.3916 0,0.3916l-3.8083,0.0346c-3.6249,0.0329 -3.8121,0.0228 -3.886,-0.2101 -0.0427,-0.1346 -0.0403,-0.3418 0.0053,-0.4606 0.0562,-0.1465 0.2798,-0.1931 1.8387,-0.194zM38.1433,21.4524c6.6382,0 6.8639,0.0084 7.0432,0.2534 0.2573,0.3518 0.2575,2.5348 0.0003,2.8867 -0.1787,0.2445 -0.4305,0.2548 -7.1803,0.2924 -6.6321,0.037 -6.9999,0.0267 -7.0911,-0.1958 -0.0529,-0.1291 -0.0964,-0.8053 -0.0964,-1.5027 0,-1.0433 0.0412,-1.3093 0.233,-1.501 0.2178,-0.2176 0.683,-0.233 7.0914,-0.233zM27.7518,22.9686c0.3667,0 0.7334,0.0056 1.0115,0.0183 0.5563,0.0245 0.1011,0.0443 -1.0115,0.0443 -1.1127,0 -1.5679,-0.0198 -1.0115,-0.0443 0.2782,-0.0127 0.6448,-0.0183 1.0115,-0.0183zM25.5605,23.0935c0.0169,0.0005 0.0312,0.0056 0.0413,0.0155 0.0402,0.0403 -0.0324,0.113 -0.1611,0.1621 -0.186,0.0708 -0.2011,0.056 -0.0732,-0.0729 0.0664,-0.0669 0.1422,-0.1063 0.193,-0.1047zM47.0615,23.7782 L47.062,23.7785c0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1199,0.0988 0.2104,0.1789 0.2868,0.2516 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1274 0.0737,0.1912 0.0803,0.1399 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.015,0.1007 -0.0232,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0831,0.057 -0.1668,0.113 -0.2503,0.1693 0,0.0099 0,0.0191 0,0.0289 -0,0.1305 0,0.261 0,0.3916l0,0.3316 0,0.3238 0,0.6429 0,0.099c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1566,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0227,0.1934 0.0143,0.283 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 -0.0001,0.0177 -0.0005,0.0266 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 -0,0.1368 0,0.2052 0.961,0.3941 1.4647,1.192 0.4494,2.0121 0.378,0.275 0.7394,0.5676 1.0574,0.907 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.0169 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0141 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0725,0.5046 -0.2549,1.1415 -0.6444,1.4982 -0.0496,0.0453 0.1489,-0.2233 0.1063,-0.1713 -0.2712,0.3311 -0.3266,0.4199 -0.3543,0.4662 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.5096,1.6831 -0.515,1.5013 -1.6149,2.0294 -0.4215,0.0974 -0.3939,0.1026 -0.9269,0.1499 -0.3268,0.029 -0.6609,-0.0222 -0.9883,0l-0.7456,0c-0.3724,-0 -0.7449,0.0003 -1.1173,0.0003 -0.9812,-0 -1.9624,-0.0005 -2.9436,0 -1.1724,0.001 -2.3448,0.0004 -3.5171,-0.0028 -0.9206,0.0028 -1.8416,0.0334 -2.7618,-0.0084 -0.3559,-0.0166 -0.7157,-0.0505 -1.0722,-0.0245 -0.7803,0.057 -1.5516,0.2104 -2.3363,0.2144 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -3.5019,-0.5896 -3.2037,-3.8133 -0.2511,-3.9564 -0.0006,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0023,-0.362 0.0018,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0191,-0.1575 0.0025,-0.3147 0.0189,-0.4714 -0,-0.0151 0,-0.0366 0,-0.0518 -0.001,-0.0198 -0.0013,-0.0396 -0.0023,-0.0594 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.001,-0.0362 0.0001,-0.0669 0.002,-0.0956 -0.001,-2.0288 -0.0003,-4.2657 0.0066,-6.7735l0.0502,-12.7027c-0.2428,0.058 -0.2408,0.0398 -0.2817,-0.2068 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1064 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1327 0.0025,-0.0004 0.0046,-0.0014 0.0072,-0.0014 0.2194,-0.0335 0.4733,-0.0833 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0328,-0 0.0656,0 0.0984,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0171,0.0322 0.0245,0.0681 0.0288,0.105l0.0877,-0.0014 0.0755,0.7319c0.017,0.1647 0.04,0.3141 0.0701,0.4497 0.0006,0.0014 0.0039,0.007 0.0044,0.0084 0.0531,0.0876 0.0716,0.1876 0.0928,0.2863 0.0317,0.0615 0.0587,0.1243 0.0673,0.1856 0.1533,0.3038 0.3817,0.5031 0.712,0.6411 0.6537,0.2731 13.6912,0.2731 14.3449,0 0.3577,-0.1495 0.5707,-0.357 0.7008,-0.7288 -0.0001,-0.029 -0.0003,-0.0581 -0.0003,-0.0871 0.0004,-0.1083 0.0003,-0.2165 0.0003,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0691,-0.115 0.1192,-0.2071 0.1637,-0.2921l0.0031,-0.0355c-0.0185,-0.0099 -0.0379,-0.0179 -0.0551,-0.0296 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.045,0.0014 -0.091,0.0028 -0.1318,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0583,-0.0535 0.1122,-0.1137 0.1662,-0.1736 0.0008,-0.0056 0.0021,-0.0099 0.0028,-0.0166 0.0153,-0.007 0.0099,-0.0056 0.0229,-0.0113 0.0901,-0.0991 0.1829,-0.1941 0.3018,-0.2503 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458zM47.4377,23.9256c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9397c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8561c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.857c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.891c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.3937,24.0646c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1023c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5121c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5161c0.0101,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4532l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7611c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.862c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8677c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2253,49.8209c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM54.2873,51.1047c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2219c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#858883\"/>\n  <path\n      android:pathData=\"m38.0903,17.9956c-2.3482,0.001 -4.3392,0.0462 -4.4244,0.1004 -0.2622,0.1667 -0.4323,0.7919 -0.4323,1.5887 0,0.896 -0.0327,0.9192 -1.2963,0.9192 -0.7648,0 -0.9603,0.0521 -1.4576,0.389l-0.5743,0.3893 -3.5001,0.0028c-3.632,0.0028 -3.8585,0.0325 -4.2957,0.5723 -0.2109,0.2604 -0.2254,1.3262 -0.2284,16.4778 -0.0028,15.1974 -0.038,15.8958 0.1853,16.2376 0.0055,0.0005 0.1133,0.1304 0.1188,0.1313 0.068,0.0113 0.1822,0.258 0.3228,0.3631 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0615 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0614 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0467 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2213,0.0148 0.4248,0.0279 0.6103,0.0805 0.03,-0.0003 0.0665,-0.0003 0.0964,-0.0005 0.0313,-0.0504 0.098,-0.0936 0.2042,-0.1104 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0042 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.041 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0169 0.0179,-0.0365 0.0194,-0.0369 0.039,-0.067 -0.003,0.0042 -0.0101,0.0179 -0.0084,0.0127 0.0025,-0.007 0.0076,-0.0141 0.011,-0.0204 0.001,-0.0014 0.0041,-0.0056 0.0025,-0.007 -0.0015,-0.0014 -0.0046,0.007 -0.0041,0.0056 0.0052,-0.0255 0.0142,-0.0501 0.0212,-0.0752 0.0082,-0.0294 0.0219,-0.0569 0.0311,-0.0859 0.0035,-0.0113 0.0086,-0.0384 0.0107,-0.0484 0.011,-0.0508 0.0038,-0.0127 0.0122,-0.0629 0.003,-0.0618 0.0104,-0.1239 0.0229,-0.1846 0.0035,-0.0169 0.0076,-0.0338 0.0115,-0.0507l0,-0.8451c-0.0024,-0.007 -0.0049,-0.0141 -0.0073,-0.0204 -0.0334,-0.0638 -0.0421,-0.1351 -0.0579,-0.2042 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1315 0.0065,-0.0014 0.0156,-0.0001 0.024,0.0005l0,-12.4404c0,-17.1395 0.0287,-16.4431 -0.6954,-16.9175 -0.299,-0.1959 -0.7662,-0.2223 -3.9156,-0.2223l-3.5758,0l-0.4928,-0.3916c-0.4404,-0.35 -0.5887,-0.3916 -1.397,-0.3916 -1.3086,0 -1.4092,-0.0826 -1.4092,-1.1548 0,-0.7096 -0.0526,-0.9409 -0.2661,-1.1701 -0.2634,-0.2827 -0.3103,-0.2855 -4.5355,-0.2835zM39.266,19.0577c1.301,-0.0028 2.2884,0.0127 2.4031,0.04 0.3263,0.0797 0.4809,0.5332 0.298,0.8749 -0.1201,0.2244 -0.3728,0.2399 -3.9064,0.2399l-3.7779,0l-0.0872,-0.3464c-0.1886,-0.7515 -0.1512,-0.7596 3.6856,-0.7997 0.4826,-0.0056 0.9511,-0.0084 1.3847,-0.0084zM37.4686,21.2742c1.2387,-0.0014 2.537,0.0084 3.5957,0.0324 4.647,0.1049 4.3464,-0.0196 4.3591,1.8025 0.0049,0.6682 -0.0355,1.3765 -0.0892,1.5739l-0.0976,0.3589 -5.8384,0.0005c-3.211,0.0004 -6.409,0.0387 -7.1067,0.0849l-1.2682,0.0839 -0.1349,-0.3541c-0.0741,-0.1948 -0.1346,-0.9379 -0.1346,-1.6511 0,-1.8712 -0.1938,-1.7659 3.4764,-1.8874 0.8207,-0.0272 1.9998,-0.0422 3.2385,-0.0443zM27.7518,22.9687c0.3667,0 0.7334,0.0056 1.0115,0.0183 0.5563,0.0245 0.1011,0.0443 -1.0115,0.0443 -1.1127,0 -1.5679,-0.0198 -1.0115,-0.0443 0.2782,-0.0127 0.6448,-0.0183 1.0115,-0.0183zM26.8057,23.178l0.0005,0c1.2711,0.0042 2.8937,0.0771 2.8342,0.1733 -0.043,0.0697 -0.928,0.1242 -2.0129,0.1242 -1.6451,0 -1.9481,0.0311 -2.0154,0.2065 -0.0436,0.1136 -0.0793,6.592 -0.0793,14.3964 0,5.3516 0.009,8.8688 0.0324,11.0891 0.2854,-0.1045 0.614,-0.1724 0.986,-0.1904 -0.0006,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0023,-0.362 0.0018,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0004,0.0127 -0.0032,0.0191 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.0028,-0.147 -0.0018,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.0025,-0.104 0.0094,-0.1704 0.027,-0.2396l-0.0159,-6.2889c-0.0196,-7.8734 -0.0035,-11.7641 0.0599,-13.0463 -0.2557,0.0617 -0.2526,0.047 -0.2942,-0.2037 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1329 0.0025,-0.0004 0.0048,-0.0014 0.0073,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0327,-0 0.0654,0 0.0981,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,-0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,-0 0.0489,0l0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0263,0.0497 0.03,0.1077 0.0336,0.1649 0.2157,0.1455 0.2508,0.4035 0.2508,0.8614 0,0.1402 0.0122,0.27 0.0352,0.3903 0.0161,0.0522 0.0275,0.1063 0.039,0.1601 0.0004,0.0014 0.0009,0.0028 0.0013,0.0042 0.027,0.0525 0.0482,0.1057 0.0596,0.1583 0.1671,0.3723 0.4899,0.6129 0.9743,0.7247 0.73,0.1686 13.1841,0.161 13.794,-0.0084 0.4679,-0.1299 0.8119,-0.4223 0.9965,-0.8247 -0,-0.0141 -0.0003,-0.0275 -0.0003,-0.0411 0.0004,-0.1083 0.0003,-0.2165 0.0003,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0651,-0.1083 0.113,-0.1959 0.1558,-0.2771 0.0006,-0.0191 0.0017,-0.0367 0.0025,-0.0548 -0.0157,-0.0084 -0.0322,-0.0153 -0.0469,-0.0253 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.045,0.0014 -0.0906,0.0028 -0.1313,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.0173 0.0158,-0.007 0.0103,-0.0056 0.0237,-0.0127 0.09,-0.099 0.1828,-0.1937 0.3016,-0.2498 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1201,0.0991 0.2109,0.1793 0.2873,0.2521 0.0403,0.02 0.0805,0.0405 0.1499,0.0721 0.0229,0.0641 0.0492,0.1271 0.0734,0.1907 0.0805,0.1401 0.0841,0.2686 0.0857,0.478 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0251,0.1846 0.015,0.2654 -0.0063,0.0504 -0.0152,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0831,0.0569 -0.1667,0.113 -0.2503,0.1693 0,0.0099 0,0.0194 0,0.029 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0984c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0424,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.0681 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2068 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.2702,0.1108 0.5056,0.2521 0.6796,0.4186l-0.0495,-8.3787 -0.0653,-11.0289 -1.9252,-0.0362c-2.5414,-0.0477 -2.5766,-0.2901 -0.0421,-0.2901 1.538,0 1.9208,0.0377 2.0883,0.2052 0.184,0.184 0.2052,1.6874 0.2052,14.5978 0,2.2157 -0.002,3.6003 -0.0032,5.1886 0.1489,0.2526 0.1675,0.5401 -0.0005,0.842 -0.0003,0.2398 -0.0001,0.6056 -0.0005,0.8313 0.2181,0.1775 0.4277,0.3642 0.62,0.5695 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.0169 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0562 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0881 -0.0576,0.4013 -0.1868,0.8842 -0.4323,1.2453 -0.0291,2.299 -0.0878,2.5995 -0.2101,2.7101 -0.1061,0.096 -0.434,0.1494 -2.0019,0.1792 -0.2814,0.0663 -0.3351,0.0788 -0.7997,0.1201 -0.3268,0.029 -0.6609,-0.0222 -0.9883,0l-0.7456,0c-0.3724,-0 -0.7449,0.0003 -1.1173,0.0003 -0.9812,-0 -1.9624,-0.0005 -2.9436,0 -1.1724,0.001 -2.3448,0.0004 -3.5171,-0.0028 -0.9206,0.0028 -1.8416,0.0334 -2.7618,-0.0084 -0.3559,-0.0166 -0.7157,-0.0505 -1.0722,-0.0245 -0.7803,0.057 -1.5516,0.2104 -2.3363,0.2144 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -0.2819,-0.0474 -0.5361,-0.1139 -0.7694,-0.1925 -0.2611,-0.0215 -0.393,-0.0473 -0.4357,-0.0786 -0.0351,-0.0258 -0.0647,-0.0633 -0.0908,-0.141 -1.6794,-0.8622 -1.6,-2.5917 -0.1473,-3.2683 -0.0223,-1.9917 -0.034,-5.1487 -0.05,-10.3278 -0.023,-7.4564 -0.0566,-13.978 -0.0744,-14.4922 -0.0179,-0.5143 0.0058,-0.9112 0.0525,-0.8823 0.0468,0.0289 0.1652,-0.0459 0.2631,-0.1662 0.1577,-0.1939 0.1482,-0.2077 -0.0831,-0.1213 -0.1436,0.0536 -0.2023,0.0497 -0.1305,-0.0084 0.0704,-0.0569 0.7065,-0.0793 1.4691,-0.0767zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8568c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9185c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.6498,44.3197c-0.0677,0.0735 -0.1466,0.1475 -0.2384,0.2215 0.0813,0.0591 0.1614,0.1197 0.2409,0.1807zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.6386,50.099c-0.1793,0.2237 -0.2505,0.3252 -0.274,0.3645 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.4101,1.3545 -0.5028,1.5038 -1.0931,1.7847l1.2657,-0.0151 0.079,-1.3704c0.0167,-0.2898 0.026,-0.6427 0.0313,-1.0013 -0.0084,0.0084 -0.0166,0.017 -0.0252,0.0249 -0.0185,0.0169 0.0006,-0.0141 0.0257,-0.051 0.0001,-0.0056 0.0001,-0.0127 0.0003,-0.0186zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#848682\"/>\n  <path\n      android:pathData=\"m39.7577,18.0069c-2.5658,0.0084 -6.137,0.078 -6.2104,0.1514 -0.0546,0.0546 -0.134,0.5715 -0.1764,1.1484 -0.0527,0.7179 -0.1382,1.0875 -0.2705,1.1711 -0.1063,0.0671 -0.6146,0.1232 -1.1296,0.1242 -0.8295,0.0014 -0.9847,0.0429 -1.3623,0.361l-0.4265,0.3589 -3.7277,0.066c-2.0503,0.0363 -3.7486,0.1242 -3.8699,0.1835 -0.6583,0.3225 -0.7062,-0.5086 -0.7062,16.8916 0,15.1687 0.0231,15.9719 0.2463,16.3139 0.0055,0.0005 0.2549,-0.0215 0.1546,0.0621 0.068,0.0113 0.0881,0.223 0.2287,0.3281 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0615 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0.0001 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2217,0.0148 0.4256,0.0279 0.6113,0.0808 0.0296,-0.0003 0.0656,-0.0003 0.0951,-0.0005 0.0313,-0.0505 0.0981,-0.0938 0.2044,-0.1106 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0351 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0168 0.0179,-0.0365 0.0194,-0.0369 0.039,-0.067 -0.003,0.0042 -0.0101,0.0179 -0.0084,0.0127 0.0025,-0.007 0.0076,-0.0141 0.011,-0.0204 0.001,-0.0014 0.0041,-0.0056 0.0025,-0.007 -0.0015,-0.0014 -0.0046,0.007 -0.0041,0.0056 0.0052,-0.0255 0.0142,-0.0501 0.0212,-0.0752 0.0082,-0.0294 0.0219,-0.0569 0.0311,-0.0859 0.0035,-0.0113 0.0086,-0.0384 0.0107,-0.0484 0.011,-0.0508 0.0038,-0.0127 0.0122,-0.0629 0.003,-0.0619 0.0104,-0.1239 0.0229,-0.1846 0.0035,-0.017 0.0076,-0.0338 0.0115,-0.0507l0,-0.8451c-0.0024,-0.007 -0.0049,-0.0141 -0.0073,-0.0204 -0.0334,-0.0638 -0.0421,-0.1351 -0.0579,-0.2042 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0358 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0507 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.107 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1315 0.0065,-0.0014 0.0156,-0.0001 0.024,0.0005l0,-12.4404c0,-17.1395 0.0287,-16.4429 -0.6954,-16.9173 -0.2987,-0.1957 -0.7678,-0.2229 -3.9008,-0.2248l-3.5612,-0.0028 -0.5743,-0.389c-0.4973,-0.337 -0.6928,-0.3893 -1.4576,-0.3893 -1.2769,0 -1.2963,-0.0148 -1.2963,-0.9868 0,-0.9542 -0.1902,-1.4703 -0.5771,-1.567 -0.1055,-0.0263 -0.6991,-0.0397 -1.5321,-0.0434 -0.3124,-0.0014 -0.6585,-0.0014 -1.025,0zM36.9052,19.0579c0.4436,0.001 0.9225,0.0028 1.4156,0.0084l3.5862,0.0367 0.1234,0.351c0.0882,0.251 0.0751,0.4092 -0.0456,0.5547 -0.1459,0.1758 -0.6806,0.2037 -3.9161,0.2037 -2.061,0 -3.7867,-0.0391 -3.8345,-0.0869 -0.0478,-0.0479 -0.0869,-0.2819 -0.0869,-0.5198 0,-0.3403 0.0626,-0.4478 0.2937,-0.5042 0.1211,-0.0297 1.1336,-0.0459 2.4643,-0.0436zM37.9641,21.255c3.5958,-0.0003 6.9664,0.1322 7.2476,0.3378 0.1869,0.1365 0.2254,0.3831 0.2254,1.4477 0,0.7058 -0.0452,1.4448 -0.1004,1.6422l-0.1002,0.3589L39.4511,25.0417c-3.1819,0 -6.4046,0.0377 -7.1615,0.0842l-1.3763,0.0843 -0.1007,-0.4433c-0.1708,-0.7524 -0.093,-2.7207 0.1203,-3.0463 0.1801,-0.2748 0.3102,-0.2965 2.2573,-0.3773 1.4585,-0.0605 3.1394,-0.0881 4.7739,-0.0883zM27.7518,22.9686c0.3667,0 0.7334,0.0056 1.0115,0.0183 0.5563,0.0245 0.1011,0.0443 -1.0115,0.0443 -1.1127,0 -1.5679,-0.0198 -1.0115,-0.0443 0.2782,-0.0127 0.6448,-0.0183 1.0115,-0.0183zM26.7976,23.1818c1.2724,-0.0004 2.912,0.0688 2.912,0.1693 0,0.0726 -0.748,0.1244 -1.8023,0.1244 -1.2366,0 -1.8719,0.0512 -2.0241,0.1632 -0.2067,0.1521 -0.2195,1.1344 -0.1881,14.455l0.026,11.0202c0.2475,-0.0745 0.5247,-0.122 0.8303,-0.1368 -0.0006,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0477 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0006,0.0127 -0.0035,0.0191 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0018,-0.0428 -0.0028,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.0025,-0.1046 0.0096,-0.171 0.0273,-0.2406l-0.0158,-6.3593c-0.0203,-8.1972 -0.0087,-11.7347 0.0566,-12.9742 -0.2525,0.0607 -0.2497,0.0453 -0.2911,-0.2044 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1329 0.0025,-0.0004 0.0048,-0.0014 0.0073,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0327,-0 0.0654,0 0.0981,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,-0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,-0 0.0489,0l0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0156,0.0294 0.023,0.0621 0.0275,0.0956 0.0489,0.0127 0.0856,0.0262 0.1004,0.041 0.0861,0.0862 0.1565,0.4131 0.1565,0.7265 0,0.6438 0.164,1.0367 0.57,1.3656 0.2561,0.2074 0.8506,0.2259 7.4223,0.2284l7.1439,0.0028 0.4137,-0.3696c0.1767,-0.1579 0.282,-0.2773 0.35,-0.4392 -0.0001,-0.0204 -0.0003,-0.041 -0.0003,-0.0614 0.0004,-0.1083 0.0003,-0.2166 0.0003,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0597,-0.0992 0.1046,-0.1809 0.145,-0.257 0.0028,-0.0259 0.0058,-0.0518 0.0089,-0.0774 -0.0141,-0.0084 -0.0293,-0.0141 -0.0426,-0.0229 -0.0021,-0.0014 -0.0032,-0.0028 -0.0053,-0.0042 -0.0451,0.0014 -0.0908,0.0028 -0.1315,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.1321,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.002,-0.0113 0.0028,-0.0176 0.0161,-0.007 0.0106,-0.0056 0.0242,-0.0127 0.0899,-0.0988 0.1825,-0.1935 0.3011,-0.2496 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0042 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1199,0.0988 0.2104,0.1789 0.2868,0.2516 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.0229,0.0642 0.0494,0.1274 0.0737,0.1912 0.0803,0.1399 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0062,0.0504 -0.0152,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169l0,0.029c-0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0987c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1336 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.793 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0001,0.2318 -0.0008,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0432,0.158 0.0186,0.068 0.0104,0.0376 0.0246,0.0745 0.0316,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1566,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0122,0.0458 0.0184,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0003,0.3222 0.0005,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 -0.0004,0.0056 -0,-0.0172 -0.0015,-0.0166 0.0039,0.0966 0.0244,0.205 0.0156,0.2998 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 -0,0.1368 0,0.2052 0.2186,0.0897 0.4152,0.1984 0.5738,0.3255l0.0263,-8.1539c0.0314,-9.7327 0.0141,-10.8318 -0.1726,-11.0184 -0.1696,-0.1696 -0.5385,-0.2072 -2.0253,-0.2072 -1.125,0 -1.8178,-0.0497 -1.8178,-0.1305 0,-0.1977 3.9517,-0.1729 4.1506,0.026 0.1413,0.1413 0.1944,11.333 0.1601,19.8461 0.1419,0.2481 0.1581,0.5295 -0.0038,0.8247 -0.0014,0.2845 -0.003,0.5603 -0.0046,0.8369 0.2183,0.1776 0.428,0.3645 0.6205,0.57 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1567 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.0169 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0303 -0.0199,0.0604 -0.0227,0.0883 -0.0595,0.4146 -0.1955,0.9161 -0.4573,1.2805 -0.0265,1.6544 -0.0607,2.6923 -0.103,2.7345 -0.0509,0.051 -0.6719,0.0862 -2.0465,0.1106 -0.3169,0.0745 -0.3524,0.086 -0.8372,0.1289 -0.3268,0.029 -0.6609,-0.0222 -0.9883,0 -0.2486,0 -0.497,0 -0.7456,0 -0.3724,-0 -0.7449,0.0003 -1.1173,0.0003 -0.9812,-0.0001 -1.9624,-0.0005 -2.9436,0 -1.1724,0.001 -2.3448,0.0004 -3.5171,-0.0028 -0.9206,0.0028 -1.8416,0.0334 -2.7618,-0.0084 -0.3559,-0.0166 -0.7157,-0.0505 -1.0722,-0.0245 -0.7803,0.0572 -1.5516,0.2104 -2.3363,0.2144 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -0.2482,-0.0418 -0.4766,-0.0971 -0.687,-0.1634 -0.5801,-0.0369 -0.6618,-0.0925 -0.7069,-0.1767 -0.0051,-0.0099 -0.01,-0.059 -0.015,-0.133 -1.5485,-0.8732 -1.4556,-2.5192 -0.0699,-3.1911 -0.0996,-7.6178 -0.1499,-25.7886 -0.0331,-25.7164 0.0463,0.0286 0.119,-0.0389 0.1616,-0.1499 0.0519,-0.1353 0.0242,-0.1717 -0.0841,-0.1106 -0.1013,0.0572 -0.1129,0.0476 -0.0311,-0.0258 0.0657,-0.059 0.697,-0.0842 1.4604,-0.0843zM47.4377,23.9256c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9397c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8561c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8571c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.891c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.3937,24.0646c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1023c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5123c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5162c0.0101,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4533l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.9729,37.7613c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8622c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8678c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.5305,44.4389c-0.0372,0.0342 -0.0766,0.0684 -0.119,0.1028 0.0399,0.029 0.0788,0.0593 0.1183,0.0887zM50.2253,49.821c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.5611,50.1973c-0.1134,0.1455 -0.177,0.2339 -0.1965,0.2666 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.382,1.2616 -0.4808,1.4754 -0.9695,1.7258 0.7337,-0.0443 1.0464,-0.099 1.0974,-0.1782 0.0551,-0.0854 0.0867,-1.0055 0.0783,-2.0965zM54.2873,51.1048c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2221c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#878477\"/>\n  <path\n      android:pathData=\"m39.7577,18.0069c-2.5658,0.0084 -6.137,0.078 -6.2104,0.1514 -0.0546,0.0546 -0.134,0.5715 -0.1764,1.1484 -0.0527,0.7179 -0.1382,1.0875 -0.2705,1.1711 -0.1063,0.0671 -0.6111,0.1232 -1.1217,0.1242 -0.8334,0.0014 -0.9786,0.0421 -1.4209,0.3936l-0.4928,0.3916l-3.5798,0c-3.7167,0 -3.9373,0.0287 -4.3757,0.57 -0.2109,0.2604 -0.2254,1.3262 -0.2284,16.4778 -0.0028,15.1974 0.0202,16.0001 0.2435,16.342 0.0055,0.0005 0.2139,0.0006 0.0941,0.0557 0.068,0.0113 0.1487,0.2292 0.2893,0.3344 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0615 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0.0001 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2213,0.0148 0.4248,0.0279 0.6103,0.0805 0.03,-0.0003 0.0665,-0.0003 0.0964,-0.0005 0.0313,-0.0504 0.098,-0.0936 0.2042,-0.1104 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0351 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0251 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0168 0.0179,-0.0365 0.0194,-0.0369 0.039,-0.067 -0.003,0.0042 -0.0101,0.0179 -0.0084,0.0127 0.0025,-0.007 0.0076,-0.0141 0.011,-0.0204 0.001,-0.0014 0.0041,-0.0056 0.0025,-0.007 -0.0015,-0.0014 -0.0046,0.007 -0.0041,0.0056 0.0052,-0.0255 0.0142,-0.0501 0.0212,-0.0752 0.0082,-0.0294 0.0219,-0.0569 0.0311,-0.0859 0.0035,-0.0113 0.0086,-0.0384 0.0107,-0.0484 0.011,-0.0508 0.0038,-0.0127 0.0122,-0.0629 0.003,-0.0619 0.0104,-0.1239 0.0229,-0.1846 0.0035,-0.0169 0.0076,-0.0338 0.0115,-0.0507l0,-0.8451c-0.0024,-0.007 -0.0049,-0.0141 -0.0073,-0.0204 -0.0334,-0.0638 -0.0421,-0.1351 -0.0579,-0.2042 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1365 -0.0357,-0.2052 -0.0089,-0.0321 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0358 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1315 0.0065,-0.0014 0.0156,-0.0001 0.024,0.0005l0,-12.4404c0,-17.1395 0.0287,-16.4429 -0.6954,-16.9173 -0.2994,-0.1962 -0.7699,-0.2225 -3.9826,-0.2225 -3.1473,0 -3.6663,-0.0279 -3.8128,-0.2044 -0.3234,-0.3896 -0.8912,-0.5787 -1.7378,-0.5787 -1.2336,0 -1.257,-0.0186 -1.257,-0.9868 0,-0.9542 -0.1902,-1.4703 -0.5771,-1.567 -0.1055,-0.0263 -0.6991,-0.0397 -1.5321,-0.0434 -0.3124,-0.0014 -0.6585,-0.0014 -1.025,0zM38.2068,19.0378l3.6466,0l0.1481,0.3253c0.0974,0.2137 0.1062,0.4192 0.0257,0.5996 -0.12,0.269 -0.197,0.2747 -3.8773,0.2947 -2.2268,0.0127 -3.8053,-0.03 -3.8789,-0.1035 -0.2058,-0.2058 -0.1466,-0.9488 0.0826,-1.0368 0.1135,-0.0435 1.8475,-0.0793 3.8531,-0.0793zM38.1263,21.237c2.2292,-0.0001 4.457,0.0283 5.4441,0.0849 1.1502,0.0659 1.5687,0.1381 1.6968,0.2926 0.2091,0.2525 0.2306,2.9821 0.0263,3.3367 -0.1272,0.2208 -0.5364,0.2395 -6.4566,0.298 -3.4765,0.0343 -6.6627,0.0829 -7.0804,0.1081 -0.718,0.0432 -0.7669,0.027 -0.8935,-0.298 -0.0737,-0.1891 -0.1337,-0.9481 -0.1333,-1.6866 0.001,-1.9337 0.0069,-1.9399 1.945,-2.0506 0.9921,-0.0566 3.2225,-0.085 5.4517,-0.0852zM28.7602,22.9602l0.0005,0c0.965,0.0042 1.0872,0.0821 0.998,0.3146 -0.0641,0.1671 -0.3767,0.2006 -1.8655,0.2006 -1.2254,0 -1.8581,0.0514 -2.01,0.1632 -0.2067,0.1521 -0.2195,1.1344 -0.1881,14.455l0.026,11.0202c0.2475,-0.0745 0.5247,-0.122 0.8303,-0.1368 -0.0006,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0477 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0006,0.0127 -0.0035,0.0191 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0018,-0.0428 -0.0028,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.0025,-0.1046 0.0096,-0.171 0.0273,-0.2406l-0.0158,-6.3593c-0.0203,-8.1972 -0.0087,-11.7347 0.0566,-12.9742 -0.2525,0.0607 -0.2497,0.0453 -0.2911,-0.2044 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1329 0.0025,-0.0004 0.0048,-0.0014 0.0075,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0327,-0 0.0654,0 0.0981,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,-0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,-0 0.0489,0l0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0156,0.0294 0.023,0.0621 0.0275,0.0956 0.0489,0.0127 0.0856,0.0262 0.1004,0.041 0.0862,0.0862 0.1565,0.4131 0.1565,0.7265 0,0.6438 0.164,1.0364 0.57,1.3654 0.2561,0.2074 0.8506,0.226 7.4223,0.2284l7.1439,0.0028 0.4137,-0.3696c0.1767,-0.1579 0.282,-0.2773 0.35,-0.4392 -0.0001,-0.0204 -0.0003,-0.041 -0.0003,-0.0614 0.0004,-0.1083 0.0003,-0.2166 0.0003,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0597,-0.0992 0.1046,-0.1809 0.1451,-0.257 0.0028,-0.0258 0.0058,-0.0517 0.0089,-0.0773 -0.0141,-0.0084 -0.0293,-0.0141 -0.0426,-0.0229 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.045,0.0014 -0.0906,0.0028 -0.1313,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.0173 0.0158,-0.007 0.0103,-0.0056 0.0237,-0.0127 0.09,-0.099 0.1828,-0.1936 0.3016,-0.2498 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0427 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0042 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1199,0.0988 0.2104,0.1789 0.2868,0.2516 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1274 0.0737,0.1912 0.0803,0.1399 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0504 -0.0152,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169 0,0.0099 0,0.0194 0,0.029 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0984c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1336 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0001,0.2318 -0.0008,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0432,0.158 0.0186,0.0681 0.0104,0.0376 0.0247,0.0745 0.0316,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0122,0.0458 0.0184,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0003,0.3222 0.0005,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 -0.0004,0.0056 -0,-0.0172 -0.0015,-0.0166 0.0039,0.0966 0.0244,0.205 0.0155,0.2998 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.018 -0.0003,0.0267 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 -0,0.137 0,0.2055 0.2186,0.0897 0.4152,0.1984 0.5738,0.3255l0.0263,-8.1542c0.0314,-9.7327 0.0141,-10.8318 -0.1726,-11.0184 -0.1696,-0.1696 -0.5385,-0.2072 -2.0253,-0.2072 -1.5289,0 -1.8178,-0.0311 -1.8178,-0.1958 0,-0.1655 0.3096,-0.1958 2.0029,-0.1958 1.7745,0 2.0274,0.0272 2.2188,0.2386 0.1982,0.2191 0.2159,1.4288 0.2159,14.7285 0,2.9616 -0.0014,5.0561 -0.0044,6.9351 0.1711,0.1456 0.3356,0.2985 0.4894,0.4627 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4894 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0881 -0.0462,0.3215 -0.1378,0.696 -0.2983,1.0182 -0.0276,2.6163 -0.0859,2.9522 -0.2136,3.0677 -0.2186,0.1978 -1.285,0.2159 -12.6604,0.2159 -1.806,0 -2.8751,-0.0014 -4.1952,-0.0028 -0.518,0.0626 -1.0355,0.1315 -1.5588,0.1342 -1.64,0.0381 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -0.012,-0.0014 -0.0226,-0.0056 -0.0344,-0.007 -1.1469,-0.036 -1.2516,-0.1002 -1.3659,-0.2146 -0.0577,-0.0576 -0.1021,-0.1199 -0.1372,-0.3278 -1.3544,-0.8525 -1.2948,-2.3305 -0.104,-3.0274 -0.0184,-2.0913 -0.0199,-5.5023 -0.0199,-11.3684 0,-12.2744 0.0281,-14.501 0.1851,-14.7161 0.1664,-0.228 0.3832,-0.2577 2.1648,-0.2957 0.5323,-0.0113 0.9478,-0.0176 1.2695,-0.016zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.5305,44.4385c-0.0372,0.0342 -0.0766,0.0684 -0.119,0.1028 0.0399,0.029 0.0788,0.0593 0.1183,0.0887zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.5611,50.1969c-0.1134,0.1455 -0.177,0.2339 -0.1965,0.2666 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.382,1.2616 -0.4808,1.4754 -0.9695,1.7258 0.7337,-0.0443 1.0464,-0.099 1.0974,-0.1782 0.0551,-0.0854 0.0867,-1.0055 0.0783,-2.0965zM54.2873,51.1045c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2217c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#a6854d\"/>\n  <path\n      android:pathData=\"m39.7577,18.0069c-2.5658,0.0084 -6.137,0.078 -6.2104,0.1514 -0.0546,0.0546 -0.134,0.5715 -0.1764,1.1484 -0.0527,0.7179 -0.1382,1.0875 -0.2705,1.1711 -0.1063,0.0671 -0.6111,0.1232 -1.1217,0.1242 -0.8334,0.0014 -0.9786,0.0421 -1.4209,0.3936l-0.4928,0.3916l-3.5798,0c-3.7167,0 -3.9373,0.0287 -4.3757,0.57 -0.2109,0.2604 -0.2254,1.3262 -0.2284,16.4778 -0.0028,15.1974 0.0202,16.0001 0.2435,16.342 0.0055,0.0005 0.0907,0.0736 0.0962,0.0746 0.068,0.0113 0.1466,0.2105 0.2872,0.3156 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0615 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0.0001 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2213,0.0148 0.4248,0.0279 0.6103,0.0805 0.03,-0.0003 0.0665,-0.0003 0.0964,-0.0005 0.0313,-0.0504 0.098,-0.0936 0.2042,-0.1104 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0351 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0251 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0168 0.0179,-0.0365 0.0194,-0.0369 0.039,-0.067 -0.003,0.0042 -0.0101,0.0179 -0.0084,0.0127 0.0025,-0.007 0.0076,-0.0141 0.011,-0.0204 0.001,-0.0014 0.0041,-0.0056 0.0025,-0.007 -0.0015,-0.0014 -0.0046,0.007 -0.0041,0.0056 0.0052,-0.0255 0.0142,-0.0501 0.0212,-0.0752 0.0082,-0.0294 0.0219,-0.0569 0.0311,-0.0859 0.0035,-0.0113 0.0086,-0.0384 0.0107,-0.0484 0.011,-0.0508 0.0038,-0.0127 0.0122,-0.0629 0.003,-0.0619 0.0104,-0.1239 0.0229,-0.1846 0.0035,-0.0169 0.0076,-0.0338 0.0115,-0.0507l0,-0.8451c-0.0024,-0.007 -0.0049,-0.0141 -0.0073,-0.0204 -0.0334,-0.0638 -0.0421,-0.1351 -0.0579,-0.2042 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1365 -0.0357,-0.2052 -0.0089,-0.0321 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0358 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1315 0.0065,-0.0014 0.0156,-0.0001 0.024,0.0005l0,-12.4404c0,-17.1395 0.0287,-16.4429 -0.6954,-16.9173 -0.2994,-0.1962 -0.7699,-0.2225 -3.9826,-0.2225 -3.1473,0 -3.6663,-0.0279 -3.8128,-0.2044 -0.3234,-0.3896 -0.8912,-0.5787 -1.7378,-0.5787 -1.2336,0 -1.257,-0.0186 -1.257,-0.9868 0,-0.9542 -0.1902,-1.4703 -0.5771,-1.567 -0.1055,-0.0263 -0.6991,-0.0397 -1.5321,-0.0434 -0.3124,-0.0014 -0.6585,-0.0014 -1.025,0zM38.2068,19.0378l3.6466,0l0.1481,0.3253c0.0974,0.2137 0.1062,0.4192 0.0257,0.5996 -0.12,0.269 -0.197,0.2747 -3.8773,0.2947 -2.2268,0.0127 -3.8053,-0.03 -3.8789,-0.1035 -0.2058,-0.2058 -0.1466,-0.9488 0.0826,-1.0368 0.1135,-0.0435 1.8475,-0.0793 3.8531,-0.0793zM38.1263,21.237c2.2292,-0.0001 4.457,0.0283 5.4441,0.0849 1.1502,0.0659 1.5687,0.1381 1.6968,0.2926 0.2091,0.2525 0.2306,2.9821 0.0263,3.3367 -0.1272,0.2208 -0.5364,0.2395 -6.4566,0.298 -3.4765,0.0343 -6.6627,0.0829 -7.0804,0.1081 -0.718,0.0432 -0.7669,0.027 -0.8935,-0.298 -0.0737,-0.1891 -0.1337,-0.9481 -0.1333,-1.6866 0.001,-1.9337 0.0069,-1.9399 1.945,-2.0506 0.9921,-0.0566 3.2225,-0.085 5.4517,-0.0852zM27.7319,22.8228l0.0005,0c1.9148,0 2.081,0.0186 2.0409,0.2284 -0.0241,0.1256 -0.0483,0.2725 -0.0538,0.3263 -0.0055,0.0539 -0.821,0.0978 -1.8122,0.0978 -1.2366,0 -1.8719,0.0512 -2.0241,0.1632 -0.2067,0.1521 -0.2195,1.1344 -0.1881,14.455l0.026,11.0202c0.2475,-0.0745 0.5247,-0.122 0.8303,-0.1368 -0.0006,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0477 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0006,0.0127 -0.0035,0.0191 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0018,-0.0428 -0.0028,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.0025,-0.1046 0.0096,-0.171 0.0273,-0.2406l-0.0158,-6.3593c-0.0203,-8.1972 -0.0087,-11.7347 0.0566,-12.9742 -0.2525,0.0607 -0.2497,0.0453 -0.2911,-0.2044 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1329 0.0025,-0.0004 0.0048,-0.0014 0.0073,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0327,-0 0.0654,0 0.0981,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,-0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,-0 0.0489,0l0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0156,0.0294 0.023,0.0621 0.0275,0.0956 0.0489,0.0127 0.0856,0.0262 0.1004,0.041 0.0862,0.0862 0.1565,0.4131 0.1565,0.7265 0,0.6438 0.164,1.0364 0.57,1.3654 0.2561,0.2074 0.8506,0.226 7.4223,0.2284l7.1439,0.0028 0.4137,-0.3696c0.1767,-0.1579 0.282,-0.2773 0.35,-0.4392 -0.0001,-0.0204 -0.0003,-0.041 -0.0003,-0.0614 0.0004,-0.1083 0.0003,-0.2166 0.0003,-0.3248 -0,-0.0735 0,-0.147 0,-0.2205 0.0597,-0.0992 0.1046,-0.1809 0.1451,-0.257 0.0028,-0.0258 0.0058,-0.0517 0.0089,-0.0773 -0.0141,-0.0084 -0.0293,-0.0141 -0.0426,-0.0229 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.045,0.0014 -0.0906,0.0028 -0.1313,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.0173 0.0158,-0.007 0.0103,-0.0056 0.0237,-0.0127 0.09,-0.099 0.1828,-0.1936 0.3016,-0.2498 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0427 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0042 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1199,0.0988 0.2104,0.1789 0.2868,0.2516 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1274 0.0737,0.1912 0.0803,0.1399 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0504 -0.0152,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169 0,0.0099 0,0.0194 0,0.029 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0984c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1336 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0001,0.2318 -0.0008,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0432,0.158 0.0186,0.0681 0.0104,0.0376 0.0247,0.0745 0.0316,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0122,0.0458 0.0184,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0003,0.3222 0.0005,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.018 -0.0003,0.0267 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.2186,0.0897 0.4152,0.1984 0.5738,0.3255l0.0263,-8.1541c0.0314,-9.7327 0.0141,-10.8318 -0.1726,-11.0184 -0.1696,-0.1696 -0.5385,-0.2073 -2.0253,-0.2073 -1.5289,0 -1.8178,-0.0311 -1.8178,-0.1958 0,-0.1655 0.3096,-0.1958 2.0029,-0.1958 1.7745,0 2.0274,0.0272 2.2188,0.2386 0.1982,0.2191 0.2159,1.4288 0.2159,14.7285 0,2.9616 -0.0014,5.0561 -0.0044,6.9351 0.1711,0.1456 0.3356,0.2984 0.4894,0.4627 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.0169 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0462,0.3214 -0.1378,0.696 -0.2983,1.0182 -0.0276,2.6163 -0.0859,2.9521 -0.2136,3.0677 -0.2186,0.1978 -1.2833,0.2159 -12.6354,0.2159 -1.7311,0 -2.9087,-0.0028 -4.2049,-0.0042 -0.5231,0.0631 -1.0456,0.1332 -1.5741,0.1358 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -0.0385,-0.007 -0.0736,-0.0153 -0.1112,-0.0224 -0.8351,-0.0279 -1.1546,-0.0671 -1.2315,-0.1233 -0.0609,-0.0445 -0.1073,-0.1011 -0.1435,-0.3696 -1.4285,-0.8583 -1.3602,-2.3968 -0.1053,-3.0899 -0.027,-2.123 -0.044,-5.5463 -0.0699,-11.3218 -0.0562,-12.5629 -0.0391,-14.6284 0.1224,-14.829 0.1031,-0.1281 0.2676,-0.2023 0.3656,-0.1647 0.098,0.0376 0.1423,0.0099 0.0984,-0.0607 -0.0513,-0.0829 0.6644,-0.1289 2.0047,-0.1289zM48.5043,22.8382c0.3903,0 0.7807,0.0056 1.0768,0.0182 0.5922,0.0242 0.1077,0.0441 -1.0768,0.0441 -1.1845,0 -1.669,-0.02 -1.0768,-0.0441 0.2961,-0.0127 0.6864,-0.0182 1.0768,-0.0182zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8568c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9185c-0.03,-0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9393c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0206 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0642c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1019c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5118c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM24.9612,29.0757c0.0051,-0.0595 0.011,0.2868 0.0171,1.0888 0.0198,2.5663 0.0198,6.7658 0,9.3321 -0.0198,2.5663 -0.0359,0.4666 -0.0359,-4.6661 0,-3.5287 0.0076,-5.6239 0.0189,-5.7548zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7608c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8617c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.5305,44.4385c-0.0372,0.0342 -0.0766,0.0684 -0.119,0.1028 0.0399,0.029 0.0788,0.0593 0.1183,0.0887zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.5611,50.1969c-0.1134,0.1455 -0.177,0.2339 -0.1965,0.2666 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.382,1.2616 -0.4808,1.4754 -0.9695,1.7258 0.7337,-0.0443 1.0464,-0.099 1.0974,-0.1782 0.0551,-0.0854 0.0867,-1.0055 0.0783,-2.0965zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#7e8078\"/>\n  <path\n      android:pathData=\"m39.8342,17.9933c-0.4644,-0.0003 -1.0001,0.0014 -1.6177,0.0014 -4.0268,0.0014 -4.789,0.0452 -4.6531,0.2651 0.0363,0.0587 0.0062,0.1436 -0.0668,0.1886 -0.0729,0.045 -0.1325,0.4825 -0.1325,0.972 0,0.5541 -0.0616,0.9401 -0.1632,1.0227 -0.0897,0.0731 -0.6365,0.1535 -1.215,0.179 -0.9205,0.0405 -1.105,0.0912 -1.4783,0.4053l-0.4267,0.3589l-3.5878,0c-3.725,0 -3.9451,0.0286 -4.3836,0.57 -0.2109,0.2604 -0.2254,1.3262 -0.2284,16.4778 -0.0028,15.1974 0.0202,16.0007 0.2435,16.3426 0.0055,0.0005 0.2285,0.0793 0.1358,0.124 0.068,0.0113 0.1069,0.1604 0.2475,0.2655 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0245 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0467 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.2213,0.0148 0.4248,0.0279 0.6103,0.0805 0.03,-0.0003 0.0665,-0.0003 0.0964,-0.0005 0.0313,-0.0504 0.098,-0.0936 0.2042,-0.1104 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0601 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0169 0.0179,-0.0365 0.0194,-0.0369 0.039,-0.067 -0.003,0.0042 -0.0101,0.0179 -0.0084,0.0127 0.0025,-0.007 0.0076,-0.0141 0.011,-0.0204 0.001,-0.0014 0.0041,-0.0056 0.0025,-0.007 -0.0015,-0.0014 -0.0046,0.007 -0.0041,0.0056 0.0052,-0.0255 0.0142,-0.0501 0.0212,-0.0752 0.0082,-0.0294 0.0219,-0.0569 0.0311,-0.0859 0.0035,-0.0113 0.0086,-0.0384 0.0107,-0.0484 0.011,-0.0508 0.0038,-0.0127 0.0122,-0.0629 0.003,-0.0618 0.0104,-0.1239 0.0229,-0.1846 0.0035,-0.0169 0.0076,-0.0338 0.0115,-0.0507l0,-0.8451c-0.0024,-0.007 -0.0049,-0.0141 -0.0073,-0.0204 -0.0334,-0.0638 -0.0421,-0.1351 -0.0579,-0.2042 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2746 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0211,-0.0743 0.0267,-0.1225 0.0678,-0.1315 0.0065,-0.0014 0.0156,-0.0001 0.024,0.0005l0,-12.4404c0,-17.1395 0.0287,-16.4429 -0.6954,-16.9173 -0.2994,-0.1962 -0.7699,-0.2225 -3.9826,-0.2225 -3.136,0 -3.6666,-0.0283 -3.8116,-0.2029 -0.3229,-0.3891 -0.7195,-0.5154 -1.8214,-0.5802l-1.1094,-0.0653 -0.0336,-0.7831c-0.0706,-1.6449 0.1615,-1.7604 -3.0894,-1.7623zM34.2721,19.0377l3.8506,0 3.8506,0l0.0867,0.3446c0.0476,0.1895 0.0341,0.4898 -0.0301,0.6671l-0.1168,0.3225 -3.6545,0.0182c-3.6173,0.0179 -3.655,0.0208 -3.6941,0.2947 -0.028,0.1965 0.0476,0.3 0.261,0.3571 0.4766,0.1277 6.4145,0.0932 6.6226,-0.0384 0.1223,-0.0774 0.2802,-0.0542 0.4532,0.067 0.1882,0.1319 0.6579,0.1861 1.6129,0.1861 1.2736,0 1.3658,0.0184 1.6835,0.3362 0.3302,0.3302 0.3351,0.3627 0.2814,1.8678 -0.0439,1.2301 -0.1061,1.6203 -0.3166,1.9807l-0.2621,0.4487 -6.7146,0.0229c-3.6929,0.0127 -6.7891,-0.007 -6.8803,-0.0443 -0.7068,-0.287 -0.9242,-3.7177 -0.2766,-4.3653 0.2116,-0.2116 0.4479,-0.2468 1.6565,-0.2468 1.7759,0 2.0051,-0.1769 1.5114,-1.1665 -0.1651,-0.331 -0.18,-0.4923 -0.0681,-0.738zM27.7245,22.8225l0,0.0003l2.0766,0l-0.013,0.3589 -0.0132,0.3589 -1.9578,0.0653 -1.9578,0.0653 -0.0331,14.322 -0.0257,11.0992c0.2272,-0.0617 0.477,-0.1022 0.7507,-0.1154 -0.0008,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0004,0.0127 -0.0032,0.0191 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.0028,-0.147 -0.0018,-0.2205 0.002,-0.116 0.0008,-0.232 0.0003,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.0015,-0.0691 0.003,-0.1213 0.0107,-0.169 -0.0039,-1.9805 -0.0053,-4.1479 0.001,-6.5767l0.0316,-12.8223c-0.2273,0.0534 -0.2262,0.0307 -0.2661,-0.2106 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1863,-0.0127 0.3308,0.0412 0.4132,0.1326 0.0027,-0.0005 0.0051,-0.0014 0.0076,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0478,0.0277 0.0875,0.0534 0.1269,0.079 0.0325,-0 0.0651,0 0.0976,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0138,0.0262 0.0205,0.0549 0.0252,0.0843 0.0342,0.0113 0.0608,0.0239 0.0775,0.0377 0.1056,0.0877 0.1818,0.3951 0.1818,0.7347 0,0.673 0.2983,1.2574 0.7456,1.4612 0.2112,0.0961 2.5592,0.1417 7.3236,0.1417 6.2722,0 7.045,-0.0225 7.3167,-0.2129 0.2149,-0.1505 0.3942,-0.4252 0.5137,-0.75 0.0001,-0.0774 0.0003,-0.1549 0.0003,-0.2322 -0,-0.0735 0,-0.147 0,-0.2205 0.0607,-0.1011 0.1062,-0.184 0.1471,-0.261 0.0014,-0.0249 0.0027,-0.0498 0.0038,-0.0747 -0.0131,-0.007 -0.0272,-0.0127 -0.0395,-0.0211 -0.0024,-0.0014 -0.0035,-0.0028 -0.0059,-0.0042 -0.045,0.0014 -0.0905,0.0028 -0.131,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.017 0.0156,-0.007 0.0101,-0.0056 0.0235,-0.0113 0.0901,-0.0991 0.1829,-0.1941 0.3018,-0.2503 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0787 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1204,0.0992 0.2113,0.1797 0.2878,0.2526 0.0403,0.02 0.0803,0.0404 0.1494,0.0719 0.0228,0.0638 0.049,0.1267 0.0732,0.1902 0.0806,0.1402 0.0843,0.2687 0.0859,0.4782 0.0001,0.0191 0.0021,0.2104 -0.0028,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0831,0.057 -0.1667,0.113 -0.2503,0.1693 0,0.0099 0,0.0196 0,0.0293 -0,0.1305 0,0.261 0,0.3916l0,0.3316 0,0.3237 0,0.6429 0,0.0971c0.0083,0.0286 0.0167,0.0572 0.025,0.0856 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.0008,0.0056 -0.0008,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0062,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0003,0.0014 -0.0008,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0687 0,0.1373 0,0.206 0.1763,0.0724 0.3389,0.1564 0.4777,0.2536 0.0101,-1.6039 0.0027,-4.1467 -0.01,-8.2655l-0.0331,-10.8471 -1.9578,-0.0653c-1.6242,-0.0542 -1.9578,-0.0987 -1.9578,-0.261 0,-0.164 0.3264,-0.2017 2.009,-0.232 1.7322,-0.0311 2.0423,-0.0056 2.2515,0.1836 0.2336,0.2114 0.2424,0.7539 0.2424,14.7319 0,2.9715 -0.0014,5.0748 -0.0044,6.9583 0.171,0.1455 0.3352,0.2983 0.4889,0.4624 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0461,0.3212 -0.1375,0.6954 -0.2977,1.0174 -0.0276,2.6166 -0.086,2.9527 -0.2136,3.0682 -0.2186,0.1978 -1.2833,0.2159 -12.6354,0.2159 -1.7304,0 -2.9075,-0.0028 -4.2034,-0.0042 -0.5238,0.0632 -1.047,0.1335 -1.5762,0.1361 -1.64,0.0381 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -0.0396,-0.007 -0.0758,-0.0156 -0.1145,-0.0229 -0.8317,-0.0279 -1.1509,-0.067 -1.2277,-0.1232 -0.0608,-0.0445 -0.107,-0.1012 -0.1433,-0.3691 -1.4294,-0.8584 -1.3612,-2.3974 -0.1055,-3.0904 -0.027,-2.123 -0.044,-5.5463 -0.0699,-11.3218 -0.0562,-12.5629 -0.0391,-14.6284 0.1224,-14.829 0.1032,-0.1281 0.2676,-0.2023 0.3656,-0.1647 0.098,0.0376 0.1423,0.0099 0.0984,-0.0607 -0.0513,-0.0829 0.6617,-0.1289 1.9968,-0.1289zM48.5043,22.838c0.3903,0 0.7807,0.0056 1.0768,0.0182 0.5922,0.0242 0.1077,0.0441 -1.0768,0.0441 -1.1845,0 -1.669,-0.02 -1.0768,-0.0441 0.2961,-0.0127 0.6864,-0.0182 1.0768,-0.0182zM47.0569,23.8556c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8566c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8905c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9183c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9393c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0641c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1019c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5118c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0042 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM42.4419,25.3046c-3.7485,0.0028 -10.6902,0.2532 -10.8265,0.3895 -0.0872,0.0871 2.0932,0.1305 6.552,0.1305 3.6754,0 6.6825,-0.047 6.6825,-0.1046 0,-0.3498 -0.3926,-0.4175 -2.408,-0.4155zM24.9612,29.0757c0.0051,-0.0595 0.011,0.2868 0.0171,1.0888 0.0198,2.5663 0.0198,6.7658 0,9.3321 -0.0198,2.5663 -0.0359,0.4666 -0.0359,-4.6661 0,-3.5287 0.0076,-5.6239 0.0189,-5.7548zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4528l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7608c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8617c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8673c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.5104,50.2611c-0.0807,0.1059 -0.1288,0.1741 -0.1458,0.2024 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.3555,1.174 -0.4665,1.4407 -0.8739,1.6738 0.5198,-0.0251 0.7662,-0.067 0.8186,-0.1326 0.0681,-0.0852 0.1595,-0.9832 0.2108,-2.0258zM54.2873,51.1043c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2215c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0042 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#9b8257\"/>\n  <path\n      android:pathData=\"m39.8342,17.9933c-0.4644,-0.0003 -1.0001,0.0014 -1.6177,0.0014 -4.0268,0.0014 -4.789,0.0452 -4.6531,0.2651 0.0363,0.0587 0.0062,0.1436 -0.0668,0.1886 -0.0729,0.045 -0.1325,0.4825 -0.1325,0.972 0,0.5541 -0.0616,0.9401 -0.1632,1.0227 -0.0897,0.0731 -0.6365,0.1535 -1.215,0.179 -0.9205,0.0405 -1.105,0.0912 -1.4783,0.4053l-0.4267,0.3589l-3.5916,0c-3.899,0 -4.1485,0.0417 -4.4693,0.7456 -0.0994,0.2182 -0.1417,5.1144 -0.1417,16.4166l0.0217,15.9833 0.2439,0.3133c0.0954,-0.0434 0.0963,0.0297 0.1019,0.0305 0.068,0.0113 0.1216,0.1854 0.2621,0.2905 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,-0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,-0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.1989,0.0127 0.3833,0.0253 0.5534,0.0655 0.0406,-0.0001 0.1246,-0.0001 0.1647,-0.0003 0.0347,-0.0438 0.0973,-0.0805 0.1927,-0.0956 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0041,-0.007 0.0077,-0.0146 0.012,-0.022 0.0575,-0.35 0.0968,-0.9055 0.1249,-1.735 -0.0024,-0.0099 -0.0049,-0.0193 -0.0073,-0.0289 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.0321 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2746 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0204,-0.0717 0.027,-0.1181 0.0642,-0.1295 0.0319,-2.4138 0.0275,-6.2741 0.0275,-12.5576 0,-14.3021 -0.0195,-15.7859 -0.2134,-16.1943 -0.3792,-0.7991 -0.5461,-0.8298 -4.5282,-0.8298 -3.0787,0 -3.6033,-0.0284 -3.7481,-0.2029 -0.3229,-0.3891 -0.7195,-0.5154 -1.8214,-0.5802l-1.1094,-0.0653 -0.0336,-0.7831c-0.0706,-1.6449 0.1615,-1.7604 -3.0894,-1.7623zM34.2721,19.0377l3.8506,0 3.8506,0l0.0867,0.3446c0.0476,0.1895 0.0341,0.4898 -0.0301,0.6671l-0.1168,0.3225 -3.6545,0.0182c-3.6173,0.0179 -3.655,0.0208 -3.6941,0.2947 -0.028,0.1965 0.0476,0.3 0.261,0.3571 0.4766,0.1277 6.4145,0.0932 6.6226,-0.0384 0.1223,-0.0774 0.2802,-0.0542 0.4532,0.067 0.1882,0.1319 0.6579,0.1861 1.6129,0.1861 1.2736,0 1.3658,0.0184 1.6835,0.3362 0.3302,0.3302 0.3351,0.3627 0.2814,1.8678 -0.0439,1.2301 -0.1061,1.6203 -0.3166,1.9807l-0.2621,0.4487 -6.7146,0.0229c-3.6929,0.0127 -6.7891,-0.007 -6.8803,-0.0443 -0.7068,-0.287 -0.9242,-3.7177 -0.2766,-4.3653 0.2116,-0.2116 0.4479,-0.2468 1.6565,-0.2468 1.7759,0 2.0051,-0.1769 1.5114,-1.1665 -0.1651,-0.331 -0.18,-0.4923 -0.0681,-0.738zM27.7393,22.8225 L27.7395,22.8228l2.0618,0l-0.0132,0.3589 -0.0132,0.3589 -1.9578,0.0653 -1.9578,0.0653 -0.0331,14.322 -0.0257,11.0992c0.2272,-0.0617 0.477,-0.1022 0.7507,-0.1154 -0.0008,-0.0253 -0.0017,-0.0508 -0.002,-0.0762 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0215,-0.1767 0.0084,-0.3532 0.0247,-0.529 -0.001,0.007 -0.0004,0.0127 -0.0032,0.0191 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.0015,-0.0691 0.003,-0.1213 0.0107,-0.169 -0.0039,-1.9805 -0.0053,-4.148 0.001,-6.5767l0.0316,-12.8223c-0.2273,0.0534 -0.2262,0.0307 -0.2661,-0.2106 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1863,-0.0127 0.3308,0.0412 0.4132,0.1326 0.0027,-0.0005 0.0051,-0.0014 0.0076,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0478,0.0277 0.0875,0.0534 0.1269,0.079 0.0325,-0 0.0651,0 0.0976,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0138,0.0262 0.0205,0.0549 0.0252,0.0843 0.0343,0.0113 0.0611,0.0239 0.0777,0.0377 0.1056,0.0877 0.1815,0.3951 0.1815,0.7347 0,0.673 0.2983,1.2576 0.7456,1.4615 0.2112,0.0963 2.5592,0.1415 7.3236,0.1415 6.2722,0 7.045,-0.0225 7.3167,-0.2129 0.2149,-0.1505 0.3942,-0.4252 0.5137,-0.75 0.0001,-0.0774 0.0003,-0.1549 0.0003,-0.2322 -0,-0.0735 0,-0.147 0,-0.2205 0.0607,-0.1011 0.1062,-0.184 0.1471,-0.261 0.0014,-0.0249 0.0027,-0.0498 0.0038,-0.0747 -0.0131,-0.007 -0.0272,-0.0127 -0.0395,-0.0211 -0.0024,-0.0014 -0.0035,-0.0028 -0.0059,-0.0042 -0.045,0.0014 -0.0905,0.0028 -0.131,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.017 0.0156,-0.007 0.0101,-0.0056 0.0235,-0.0113 0.0901,-0.0991 0.1829,-0.1941 0.3018,-0.2503 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0787 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1204,0.0992 0.2113,0.1797 0.2878,0.2526 0.0403,0.02 0.0803,0.0404 0.1494,0.0719 0.0228,0.0638 0.049,0.1267 0.0732,0.1902 0.0806,0.1402 0.0843,0.2687 0.0859,0.4782 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.015,0.1007 -0.0232,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0831,0.057 -0.1667,0.113 -0.2503,0.1693 0,0.0099 0,0.0196 0,0.0293 -0,0.1305 0,0.261 0,0.3916l0,0.3316 0,0.3237 0,0.6429 0,0.0971c0.0083,0.0286 0.0167,0.0572 0.025,0.0856 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.0008,0.0056 -0.0008,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0424,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0432,0.158 0.0186,0.068 0.0104,0.0376 0.0246,0.0745 0.0316,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0003,0.0014 -0.0008,0.0028 0.0216,0.2091 0.0218,0.4138 -0.0408,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0686 0,0.1371 0,0.2057 0.1764,0.0724 0.3391,0.1566 0.478,0.2539 0.0103,-1.6039 0.0027,-4.1467 -0.01,-8.2655l-0.0331,-10.8471 -1.9578,-0.0653 -1.9578,-0.0653 -0.0061,-0.3589 -0.0059,-0.3589 2.0289,0.0014c1.116,0.001 2.1173,0.0588 2.2249,0.1285 0.1593,0.1029 0.1617,0.1273 0.0125,0.1287 -0.1459,0.0014 -0.1394,0.0505 0.0324,0.2404 0.1982,0.2191 0.2159,1.4288 0.2159,14.7286 0,2.9619 -0.0014,5.0568 -0.0044,6.9359 0.171,0.1455 0.3354,0.2983 0.4892,0.4624 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.0169 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0562 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0461,0.3213 -0.1376,0.6958 -0.298,1.0179 -0.0276,2.6161 -0.0859,2.9519 -0.2136,3.0675 -0.2186,0.1978 -1.2835,0.2159 -12.6356,0.2159 -1.7291,0 -2.9054,-0.0028 -4.2006,-0.0042 -0.5246,0.0633 -1.0485,0.1337 -1.5785,0.1364 -1.64,0.0381 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -0.0397,-0.007 -0.0759,-0.0158 -0.1147,-0.0232 -0.8312,-0.0277 -1.151,-0.0669 -1.2277,-0.1229 -0.0857,-0.0626 -0.1433,-0.141 -0.1869,-0.3979 -1.3497,-0.8433 -1.307,-2.3058 -0.1473,-3.0124 -0.0094,-0.3424 -0.0174,-0.5729 -0.0283,-0.9947 -0.06,-2.321 -0.4483,-12.8099 -0.4601,-18.5052l0.3082,-6.4248 0.3661,-0.3146c0.3537,-0.3041 0.4369,-0.3151 2.4279,-0.3151zM47.0569,23.8557c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8906c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0642c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM42.4419,25.3047c-3.7485,0.0028 -10.6902,0.2532 -10.8265,0.3895 -0.0872,0.0871 2.0932,0.1305 6.552,0.1305 3.6754,0 6.6825,-0.047 6.6825,-0.1046 0,-0.3498 -0.3926,-0.4175 -2.408,-0.4155zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.5104,50.2612c-0.0807,0.1059 -0.1288,0.1741 -0.1458,0.2024 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.3555,1.174 -0.4665,1.4407 -0.8739,1.6738 0.5198,-0.0251 0.7662,-0.067 0.8186,-0.1326 0.0681,-0.0852 0.1595,-0.9832 0.2108,-2.0258zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#7c7d74\"/>\n  <path\n      android:pathData=\"m38.0592,17.9989c-2.0406,0.0005 -4.0811,0.026 -4.2694,0.0764 -0.1607,0.0431 -0.2626,0.1264 -0.2264,0.1851 0.0363,0.0587 0.0062,0.1436 -0.0668,0.1886 -0.0729,0.045 -0.1325,0.4825 -0.1325,0.972 0,0.5541 -0.0616,0.9401 -0.1632,1.0227 -0.0897,0.0731 -0.6442,0.1539 -1.232,0.1797 -0.9548,0.0419 -1.1088,0.085 -1.4428,0.4051l-0.374,0.3584l-3.6273,0c-3.9378,0 -4.1835,0.0407 -4.5047,0.7456 -0.0994,0.2182 -0.1417,5.1144 -0.1417,16.4166l0.0217,15.9827 0.2246,0.2449c0.0056,0.0005 0.0138,0.0676 0.0193,0.0684 0.068,0.0113 0.2234,0.2165 0.364,0.3216 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0615 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0.0001 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.1989,0.0127 0.3833,0.0253 0.5534,0.0655 0.0406,-0.0001 0.1246,-0.0001 0.1647,-0.0003 0.0347,-0.0438 0.0973,-0.0805 0.1927,-0.0956 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0041,-0.007 0.0077,-0.0146 0.012,-0.022 0.0575,-0.35 0.0968,-0.9054 0.1249,-1.735 -0.0024,-0.0099 -0.0049,-0.0193 -0.0073,-0.0289 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0204,-0.0717 0.027,-0.1181 0.0642,-0.1295 0.0319,-2.4138 0.0275,-6.2741 0.0275,-12.5576 0,-14.3021 -0.0195,-15.7859 -0.2134,-16.1943 -0.3792,-0.799 -0.5464,-0.8298 -4.5251,-0.8298L46.0625,21.3901l-0.3459,-0.3204c-0.3747,-0.3473 -0.7673,-0.4457 -1.9764,-0.4956l-0.7831,-0.0324 -0.079,-0.9789c-0.0941,-1.1677 -0.1746,-1.3855 -0.5499,-1.4862 -0.1879,-0.0504 -2.2286,-0.0752 -4.2692,-0.0747zM35.8526,19.0536c0.5524,0.0003 1.2831,0.007 2.2522,0.0151l3.9388,0.0345 -0.0403,0.9664c-0.0289,0.6907 0.0084,0.9973 0.1305,1.0748 0.094,0.0595 0.7728,0.1094 1.5086,0.1104 1.8977,0.0028 1.906,0.0127 1.8359,2.2683 -0.0356,1.1458 -0.1167,1.8445 -0.2447,2.1061l-0.1917,0.3916 -6.9545,0.0339 -6.9547,0.0336 -0.2141,-0.4905c-0.1601,-0.3668 -0.216,-0.9135 -0.222,-2.1658 -0.0104,-2.1616 0.002,-2.1752 1.9481,-2.1752 1.6427,0 1.7616,-0.0822 1.5145,-1.0464 -0.0901,-0.3514 -0.1254,-0.7598 -0.0785,-0.9075 0.0629,-0.1982 0.1147,-0.2499 1.772,-0.2491zM38.0628,20.4735c-2.5234,0 -3.1325,0.0339 -3.1325,0.1741 0,0.0957 0.0391,0.2132 0.0869,0.261 0.0478,0.0479 1.4575,0.0869 3.1325,0.0869 2.9584,0 3.0455,-0.007 3.0455,-0.261 0,-0.2538 -0.087,-0.261 -3.1325,-0.261zM27.7311,22.8223 L27.7314,22.8228l2.0531,0l0.0298,0.3916 0.0298,0.3916 -1.8619,0.0014c-0.4826,0.0005 -0.9325,0.0113 -1.2909,0.0293 0.0477,0.0795 0.1093,0.1905 0.1782,0.3186 0.0049,-0.0028 0.0092,-0.0056 0.0143,-0.0084 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1862,-0.0127 0.3308,0.0411 0.4132,0.1323 0.0027,-0.0005 0.0051,-0.0014 0.0076,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0479,0.0279 0.0877,0.0536 0.1272,0.0793 0.0325,-0 0.0649,0 0.0974,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0076,0.0142 0.0127,0.0294 0.0171,0.0449l0.2022,0l0.0803,0.84c0.0694,0.7268 0.1356,0.8893 0.4915,1.2073l0.4112,0.3673 7.0134,-0.0005c6.7418,-0.0005 7.0296,-0.0099 7.434,-0.257 0.3065,-0.1869 0.4331,-0.3292 0.5346,-0.7612 0,-0.0588 0.0003,-0.1177 0.0003,-0.1764 -0,-0.0735 0,-0.147 0,-0.2205 0.0403,-0.067 0.0736,-0.126 0.1043,-0.1815 0.0083,-0.0556 0.0162,-0.1043 0.025,-0.1654 -0.0058,-0.0028 -0.0122,-0.0056 -0.0178,-0.0099 -0.0025,-0.0014 -0.0037,-0.0028 -0.0061,-0.0056 -0.0449,0.0014 -0.0903,0.0028 -0.1308,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.0169 0.0153,-0.007 0.0099,-0.0056 0.0229,-0.0113 0.0903,-0.0992 0.1832,-0.1945 0.3023,-0.2509 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0075,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1207,0.0995 0.212,0.1801 0.2886,0.2531 0.0402,0.02 0.0798,0.0403 0.1486,0.0717 0.0227,0.0636 0.0491,0.1263 0.0732,0.1894 0.0808,0.1403 0.0843,0.269 0.0859,0.4787 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0062,0.0505 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169 0,0.0099 0,0.0198 0,0.0298 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0969c0.0083,0.0286 0.0167,0.0572 0.025,0.0856 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1336 -0.011,0.2708 0.0018,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.0681 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0062,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0041,0.19 -0.0028,0.2842 -0.0004,0.0056 -0,-0.0172 -0.0015,-0.0166 0.0039,0.0966 0.0244,0.205 0.0156,0.2998 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.018 -0.0003,0.0267 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0687 0,0.1373 0,0.206 0.1743,0.0715 0.3354,0.1544 0.4731,0.2503 0.0209,-1.6077 0.0268,-4.1182 0.0268,-8.2436 0,-8.0842 -0.0373,-10.655 -0.1565,-10.7742 -0.1072,-0.1073 -0.7458,-0.1568 -2.0251,-0.1568l-1.8686,0l0.0298,-0.3916 0.0298,-0.3916 2.0279,0.0014c1.1153,0.001 2.116,0.0588 2.2237,0.1285 0.1593,0.1029 0.1617,0.127 0.0125,0.1285 -0.1459,0.0014 -0.1391,0.0508 0.0326,0.2406 0.1982,0.2191 0.2159,1.4288 0.2159,14.7286 0,2.9619 -0.0014,5.0568 -0.0044,6.9359 0.171,0.1455 0.3354,0.2983 0.4892,0.4624 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2498 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0461,0.3213 -0.1376,0.6958 -0.298,1.0179 -0.0276,2.6161 -0.0859,2.9519 -0.2136,3.0675 -0.2186,0.1978 -1.2833,0.2159 -12.6354,0.2159 -1.7291,0 -2.9056,-0.0028 -4.2008,-0.0042 -0.5246,0.0632 -1.0485,0.1337 -1.5785,0.1364 -1.64,0.038 -3.2754,0.1016 -4.9118,-0.067 -0.2074,-0.0214 -0.4111,-0.0691 -0.6167,-0.1037 -0.0398,-0.007 -0.0761,-0.0159 -0.115,-0.0232 -0.8308,-0.0277 -1.1507,-0.0669 -1.2274,-0.1229 -0.0857,-0.0626 -0.1433,-0.1411 -0.1869,-0.3979 -1.3497,-0.8433 -1.307,-2.3058 -0.1473,-3.0124 -0.0094,-0.3424 -0.0174,-0.5729 -0.0283,-0.9947 -0.0495,-1.9154 0.5262,-9.3149 0.7227,-15.0451 0.0012,1.4525 0.0017,2.8017 0.0061,4.6995l0.0257,11.0434c0.2137,-0.0541 0.4459,-0.0907 0.6995,-0.103 -0.0006,-0.0255 -0.0017,-0.051 -0.002,-0.0764 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0203,-0.1667 0.0051,-0.3331 0.0217,-0.4989 -0,-0.0042 0,-0.0084 0,-0.0141 -0.0003,0.001 -0.0001,0.0014 -0.0005,0.0028 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.0028,-0.147 -0.0018,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.001,-0.0377 -0.0001,-0.0697 0.0023,-0.0994 -0.001,-1.7637 0.0025,-3.9246 0.0092,-6.582l0.0316,-12.886c-0.2273,0.0534 -0.2262,0.0307 -0.2661,-0.2106 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0198,-0.1571 0.0222,-0.1387 0.1879,-0.311 -0.0342,-0.163 -0.0853,-0.3661 -0.1165,-0.5246 -0.2961,0.0183 -0.5126,0.041 -0.5522,0.066 -0.081,0.0514 -0.1218,1.2202 -0.1349,4.8167l-0.2072,-4.744 -0.3028,-0.6493c0.3535,-0.3041 0.4372,-0.3151 2.4195,-0.3151zM26.5088,23.648c0.0606,-0.0042 0.1142,-0.007 0.1825,-0.0113 -0.2277,-0.3794 -0.2337,-0.2489 -0.1825,0.0113zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.4591,50.3261c-0.0549,0.074 -0.0803,0.1135 -0.0946,0.1374 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.3232,1.0674 -0.4445,1.3843 -0.7699,1.6083 0.3918,-0.0239 0.6756,-0.0498 0.714,-0.0747 0.1094,-0.0708 0.1529,-0.6287 0.1601,-1.9532zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#c38525\"/>\n  <path\n      android:pathData=\"m38.0592,17.9989c-2.0406,0.0005 -4.0811,0.026 -4.2694,0.0764 -0.1607,0.0431 -0.2626,0.1264 -0.2264,0.1851 0.0363,0.0587 0.0062,0.1436 -0.0668,0.1886 -0.0729,0.045 -0.1325,0.4825 -0.1325,0.972 0,0.5541 -0.0616,0.9401 -0.1632,1.0227 -0.0897,0.0731 -0.6442,0.1539 -1.232,0.1797 -0.9548,0.0419 -1.1088,0.085 -1.4428,0.4051l-0.374,0.3584l-3.6273,0c-3.9378,0 -4.1835,0.0407 -4.5047,0.7456 -0.0994,0.2182 -0.1417,5.1144 -0.1417,16.4166l0.1608,15.9386 0.2233,0.2598c0.0056,0.0005 -0.124,0.0967 -0.1185,0.0977 0.068,0.0113 0.2234,0.2165 0.364,0.3216 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0615 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0.0001 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.1989,0.0127 0.3833,0.0253 0.5534,0.0655 0.0406,-0.0001 0.1246,-0.0001 0.1647,-0.0003 0.0347,-0.0438 0.0973,-0.0805 0.1927,-0.0956 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0041,-0.007 0.0077,-0.0146 0.012,-0.022 0.0575,-0.35 0.0968,-0.9054 0.1249,-1.735 -0.0024,-0.0099 -0.0049,-0.0193 -0.0073,-0.0289 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0204,-0.0717 0.027,-0.1181 0.0642,-0.1295 0.0319,-2.4138 0.0275,-6.2741 0.0275,-12.5576 0,-14.3021 -0.0195,-15.7859 -0.2134,-16.1943 -0.3792,-0.799 -0.5464,-0.8298 -4.5251,-0.8298L46.0625,21.3901l-0.3459,-0.3204c-0.3747,-0.3473 -0.7673,-0.4457 -1.9764,-0.4956l-0.7831,-0.0324 -0.079,-0.9789c-0.0941,-1.1677 -0.1746,-1.3855 -0.5499,-1.4862 -0.1879,-0.0504 -2.2286,-0.0752 -4.2692,-0.0747zM35.8526,19.0536c0.5524,0.0003 1.2831,0.007 2.2522,0.0151l3.9388,0.0345 -0.0403,0.9664c-0.0289,0.6907 0.0084,0.9973 0.1305,1.0748 0.094,0.0595 0.7728,0.1094 1.5086,0.1104 1.8977,0.0028 1.906,0.0127 1.8359,2.2683 -0.0356,1.1458 -0.1167,1.8445 -0.2447,2.1061l-0.1917,0.3916 -6.9545,0.0339 -6.9547,0.0336 -0.2141,-0.4905c-0.1601,-0.3668 -0.216,-0.9135 -0.222,-2.1658 -0.0104,-2.1616 0.002,-2.1752 1.9481,-2.1752 1.6427,0 1.7616,-0.0822 1.5145,-1.0464 -0.0901,-0.3514 -0.1254,-0.7598 -0.0785,-0.9075 0.0629,-0.1982 0.1147,-0.2499 1.772,-0.2491zM38.0628,20.4735c-2.5234,0 -3.1325,0.0339 -3.1325,0.1741 0,0.0957 0.0391,0.2132 0.0869,0.261 0.0478,0.0479 1.4575,0.0869 3.1325,0.0869 2.9584,0 3.0455,-0.007 3.0455,-0.261 0,-0.2538 -0.087,-0.261 -3.1325,-0.261zM26.407,22.3002 L26.4073,22.3007c2.8482,0 3.3022,0.0269 3.3022,0.1958 0,0.1653 -0.3039,0.1958 -1.9517,0.1958 -0.025,0.046 -0.0495,0.0881 -0.0742,0.1311 0.0236,0 0.0337,-0.0005 0.0579,-0.0005l2.0429,0l0.0298,0.3916 0.0298,0.3916 -1.8619,0.0014c-0.3303,0.0004 -0.6349,0.007 -0.918,0.0155 -0.4308,0.3657 -0.8005,0.2852 -1.0926,0.0862 -0.0035,0.0014 -0.0127,0.0028 -0.015,0.0042 -0.13,0.0825 -0.1569,3.0339 -0.1305,14.3225l0.0257,11.0434c0.2137,-0.0541 0.4459,-0.0907 0.6995,-0.103 -0.0006,-0.0255 -0.0017,-0.051 -0.002,-0.0764 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0203,-0.1667 0.0051,-0.3331 0.0217,-0.4989 -0,-0.0042 0,-0.0084 0,-0.0141 -0.0003,0.001 -0.0001,0.0014 -0.0005,0.0028 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.0028,-0.147 -0.0018,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.2179 0,-0.2246c-0.001,-0.0377 -0.0001,-0.0697 0.0023,-0.0994 -0.001,-1.7637 0.0025,-3.9246 0.0092,-6.5821l0.0316,-12.886c-0.2273,0.0534 -0.2262,0.0307 -0.2661,-0.2106 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1862,-0.0127 0.3308,0.0411 0.4132,0.1323 0.0027,-0.0005 0.0051,-0.0014 0.0076,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0479,0.0279 0.0877,0.0536 0.1272,0.0793 0.0325,-0 0.0649,0 0.0974,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0076,0.0142 0.0127,0.0294 0.0171,0.0449l0.2022,0l0.0803,0.84c0.0694,0.7268 0.1356,0.8893 0.4915,1.2073l0.4112,0.3673 7.0134,-0.0005c6.7418,-0.0005 7.0296,-0.0099 7.434,-0.2569 0.3065,-0.1869 0.4331,-0.3292 0.5346,-0.7612 0,-0.0588 0.0003,-0.1177 0.0003,-0.1764 -0,-0.0735 0,-0.147 0,-0.2205 0.0402,-0.067 0.0736,-0.126 0.1043,-0.1815 0.0083,-0.0556 0.0162,-0.1043 0.025,-0.1654 -0.0058,-0.0028 -0.0122,-0.0056 -0.0178,-0.0099 -0.0025,-0.0014 -0.0037,-0.0028 -0.0061,-0.0056 -0.0449,0.0014 -0.0903,0.0028 -0.1308,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.0168 0.0153,-0.007 0.0099,-0.0056 0.0229,-0.0113 0.0903,-0.0992 0.1832,-0.1945 0.3023,-0.2508 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1207,0.0995 0.212,0.1801 0.2886,0.2531 0.0402,0.02 0.0798,0.0403 0.1486,0.0717 0.0227,0.0636 0.0491,0.1263 0.0732,0.1894 0.0808,0.1403 0.0843,0.269 0.0859,0.4787 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0887 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169l0,0.0298c-0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0969c0.0083,0.0286 0.0167,0.0572 0.025,0.0856 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.011,0.2708 0.0018,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1978 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0041,0.19 -0.0028,0.2842 -0.0004,0.0056 -0,-0.0172 -0.0015,-0.0166 0.0039,0.0964 0.0244,0.205 0.0156,0.2998 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.018 -0.0003,0.0267 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0687 0,0.1373 0,0.206 0.1743,0.0715 0.3351,0.1545 0.4729,0.2503 0.0209,-1.6077 0.027,-4.1182 0.027,-8.2436 0,-8.0842 -0.0373,-10.655 -0.1565,-10.7742 -0.1072,-0.1073 -0.7458,-0.1568 -2.0251,-0.1568L46.4113,23.603l0.0298,-0.3916 0.0298,-0.3916 2.0279,0.0014c1.1153,0.001 2.116,0.0588 2.2237,0.1285 0.1593,0.1029 0.1617,0.127 0.0125,0.1285 -0.1459,0.0014 -0.1391,0.0508 0.0326,0.2406 0.1982,0.2191 0.2159,1.4288 0.2159,14.7285 0,2.9619 -0.0014,5.0568 -0.0044,6.9359 0.171,0.1455 0.3354,0.2983 0.4892,0.4624 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0461,0.3213 -0.1376,0.6958 -0.298,1.0179 -0.0276,2.6159 -0.0859,2.9519 -0.2136,3.0675 -0.2186,0.1978 -1.2833,0.2159 -12.6354,0.2159 -1.7292,0 -2.9056,-0.0028 -4.2008,-0.0042 -0.3241,0.0391 -0.6481,0.079 -0.9733,0.1059 4.7799,0.6234 11.3422,1.0735 19.8662,1.0735l0.1547,-0.2893c0.1185,-0.2213 0.1326,-3.8916 0.0597,-15.6261 -0.0524,-8.4354 -0.1236,-15.3828 -0.1581,-15.4385 -0.0344,-0.0557 -1.4687,-0.1012 -3.1873,-0.1012 -2.6906,0 -3.1248,-0.0273 -3.1248,-0.1958 0,-0.1687 0.4468,-0.1958 3.237,-0.1958 2.3435,0 3.2801,0.0431 3.3935,0.1565 0.1201,0.1201 0.1565,3.8479 0.1565,15.984 0,15.5145 -0.0049,15.8302 -0.2524,15.9626 -0.1698,0.0908 -5.1043,0.1351 -15.0658,0.1351 -13.2925,0 -14.8341,-0.021 -15.0181,-0.2049 -0.1842,-0.1842 -0.2052,-1.8117 -0.2052,-15.9626 0,-12.0817 0.0364,-15.7938 0.1565,-15.9139 0.1136,-0.1136 1.0645,-0.1568 3.4588,-0.1568zM32.9358,53.0735c-4.2564,-0.5552 -7.1005,-1.2477 -8.626,-1.8859 0.1162,0.4405 0.4309,0.8691 0.9626,1.2014 0.0432,0.2572 0.101,0.3351 0.1874,0.3982 0.0767,0.056 0.3965,0.095 1.2274,0.1229 0.0389,0.007 0.0751,0.0165 0.115,0.0232 0.2056,0.0346 0.4093,0.0824 0.6167,0.1037 1.6364,0.1687 3.2718,0.1052 4.9118,0.067 0.2025,-0.001 0.404,-0.0141 0.6052,-0.0305zM24.3098,51.1876c-0.1791,-0.6789 0.1147,-1.3862 0.8211,-1.814 -2.5438,0.1675 -2.9131,0.9387 -0.8211,1.814zM25.9716,23.7097c0.0866,-0.0369 0.5213,-0.0684 1.0926,-0.0862 0.1947,-0.1653 0.4018,-0.4223 0.6195,-0.8002 -1.8027,0.0005 -2.0184,0.0256 -2.329,0.27 0.1186,0.1143 0.3307,0.4213 0.6169,0.6164zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.4591,50.3261c-0.0549,0.074 -0.0803,0.1135 -0.0946,0.1374 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.3232,1.0674 -0.4445,1.3843 -0.7699,1.6083 0.3918,-0.0239 0.6756,-0.0498 0.714,-0.0747 0.1094,-0.0708 0.1529,-0.6287 0.1601,-1.9532zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#887c62\"/>\n  <path\n      android:pathData=\"m38.0592,17.9989c-2.0406,0.0005 -4.0811,0.026 -4.2694,0.0764 -0.1607,0.0431 -0.2626,0.1264 -0.2264,0.1851 0.0363,0.0587 0.0062,0.1436 -0.0668,0.1886 -0.0729,0.045 -0.1325,0.4825 -0.1325,0.972 0,0.5541 -0.0616,0.9401 -0.1632,1.0227 -0.0897,0.0731 -0.6442,0.1539 -1.232,0.1797 -0.9548,0.0419 -1.1088,0.085 -1.4428,0.4051l-0.374,0.3584l-3.6273,0c-3.9378,0 -4.1835,0.0407 -4.5047,0.7456 -0.0994,0.2182 -0.1417,5.1144 -0.1417,16.4166l0.1524,15.8175 0.1544,0.4372c0.1727,-0.1353 0.1783,0.0465 0.1839,0.0473 0.068,0.0113 -0.0016,0.2105 0.139,0.3156 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0615 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0.0001 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0122,0.0014 0.0236,0.0084 0.0359,0.0099 0.1072,0.0028 0.2144,-0.0141 0.3217,-0.0028 0.1989,0.0127 0.3833,0.0253 0.5534,0.0655 0.0406,-0.0001 0.1246,-0.0001 0.1647,-0.0003 0.0347,-0.0438 0.0973,-0.0805 0.1927,-0.0956 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0041,-0.007 0.0077,-0.0146 0.012,-0.022 0.0575,-0.35 0.0968,-0.9054 0.1249,-1.735 -0.0024,-0.0099 -0.0049,-0.0193 -0.0073,-0.0289 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0204,-0.0717 0.027,-0.1181 0.0642,-0.1295 0.0319,-2.4138 0.0275,-6.2741 0.0275,-12.5576 0,-14.3021 -0.0195,-15.7859 -0.2134,-16.1943 -0.3792,-0.799 -0.5464,-0.8298 -4.5251,-0.8298L46.0625,21.39l-0.3459,-0.3204c-0.3747,-0.3473 -0.7673,-0.4457 -1.9764,-0.4956l-0.7831,-0.0324 -0.079,-0.9789c-0.0941,-1.1677 -0.1746,-1.3855 -0.5499,-1.4862 -0.1879,-0.0504 -2.2286,-0.0752 -4.2692,-0.0747zM35.8526,19.0536c0.5524,0.0003 1.2831,0.007 2.2522,0.0151l3.9388,0.0345 -0.0403,0.9664c-0.0289,0.6907 0.0084,0.9973 0.1305,1.0748 0.094,0.0595 0.7728,0.1094 1.5086,0.1104 1.8977,0.0028 1.906,0.0127 1.8359,2.2683 -0.0356,1.1458 -0.1167,1.8445 -0.2447,2.1061l-0.1917,0.3916 -6.9545,0.0339 -6.9547,0.0336 -0.2141,-0.4905c-0.1601,-0.3668 -0.216,-0.9135 -0.222,-2.1658 -0.0104,-2.1616 0.002,-2.1752 1.9481,-2.1752 1.6427,0 1.7616,-0.0822 1.5145,-1.0464 -0.0901,-0.3514 -0.1254,-0.7598 -0.0785,-0.9075 0.0629,-0.1982 0.1147,-0.2499 1.772,-0.2491zM38.0628,20.4735c-2.5234,0 -3.1325,0.0339 -3.1325,0.1741 0,0.0957 0.0391,0.2132 0.0869,0.261 0.0478,0.0479 1.4575,0.0869 3.1325,0.0869 2.9584,0 3.0455,-0.007 3.0455,-0.261 0,-0.2538 -0.087,-0.261 -3.1325,-0.261zM26.4073,22.3002 L26.4076,22.3007c2.8482,0 3.302,0.0269 3.302,0.1958 0,0.1653 -0.3039,0.1958 -1.9517,0.1958 -0.0249,0.0459 -0.0488,0.0881 -0.0732,0.1311 0.0257,0 0.0366,-0.0005 0.063,-0.0005 1.1269,0 2.0798,0.0307 2.1174,0.0683 0.0375,0.0376 0.0404,0.2138 0.0063,0.3916l-0.0617,0.3232 -1.8449,0.0014c-0.2999,0.0003 -0.5801,0.0056 -0.8425,0.0127 -0.4552,0.4687 -0.7944,0.34 -1.1069,0.0757 -0.0263,0.0056 -0.0495,0.0113 -0.0589,0.0173 -0.13,0.0825 -0.1569,3.0339 -0.1305,14.3225l0.0257,11.0434c0.2136,-0.0541 0.4458,-0.0907 0.6992,-0.103 -0.0008,-0.0255 -0.0017,-0.051 -0.002,-0.0764 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0203,-0.1667 0.0051,-0.3331 0.0217,-0.4989 -0,-0.0042 0,-0.0084 0,-0.0141 -0.0003,0.001 -0.0001,0.0014 -0.0005,0.0028 -0.0015,0.0028 -0.0013,-0.0056 -0.0018,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.001,-0.0377 0.0003,-0.0697 0.0025,-0.0994 -0.001,-1.7637 0.0025,-3.9246 0.0092,-6.5821l0.0316,-12.886c-0.2273,0.0534 -0.2262,0.0307 -0.2661,-0.2106 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1862,-0.0127 0.3308,0.0411 0.4132,0.1323 0.0027,-0.0005 0.0051,-0.0014 0.0076,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0479,0.0279 0.0877,0.0536 0.1272,0.0793 0.0325,-0 0.0649,0 0.0974,0 0.0284,-0.0849 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0076,0.0142 0.0127,0.0294 0.0171,0.0449l0.2022,0l0.0803,0.84c0.0694,0.7268 0.1356,0.8893 0.4915,1.2073l0.4112,0.3673 7.0134,-0.0005c6.7418,-0.0005 7.0296,-0.0099 7.434,-0.257 0.3065,-0.1869 0.4331,-0.3292 0.5346,-0.7612 0,-0.0588 0.0003,-0.1177 0.0003,-0.1764 -0,-0.0735 0,-0.147 0,-0.2205 0.0403,-0.067 0.0736,-0.126 0.1043,-0.1815 0.0083,-0.0556 0.0162,-0.1043 0.025,-0.1654 -0.0058,-0.0028 -0.0122,-0.0056 -0.0178,-0.0099 -0.0025,-0.0014 -0.0037,-0.0028 -0.0061,-0.0056 -0.0449,0.0014 -0.0903,0.0028 -0.1308,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.0169 0.0153,-0.007 0.0099,-0.0056 0.0229,-0.0113 0.0903,-0.0992 0.1832,-0.1945 0.3023,-0.2509 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0003,-0.0001 0.0008,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0048,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0758,0.0425 -0.1498,0.0821 -0.2134,0.0981 -0.0114,0.0028 0.019,-0.0127 0.0288,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0787l0.0003,0c0.1187,-0.0667 0.2409,-0.1412 0.3286,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1205,0.0994 0.2115,0.1799 0.2881,0.2529 0.0403,0.02 0.0803,0.0404 0.1494,0.0719 0.0229,0.0639 0.0493,0.1268 0.0734,0.1904 0.0803,0.1401 0.0838,0.2684 0.0854,0.4777 0.0001,0.0191 0.0021,0.2104 -0.0028,0.2345 0.0124,0.0887 0.0248,0.1846 0.0148,0.2654 -0.0062,0.0505 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169 0,0.0099 0,0.0198 0,0.0298 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0978c0.0083,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1336 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.0008,0.0056 -0.0008,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.793 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.018 -0.0003,0.0267 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0687 0,0.1374 0,0.206 0.1743,0.0715 0.3351,0.1545 0.4729,0.2503 0.021,-1.6077 0.027,-4.1182 0.027,-8.2436 0,-8.0842 -0.0373,-10.6552 -0.1565,-10.7745 -0.1071,-0.1071 -0.7422,-0.1565 -2.008,-0.1565l-1.8512,0l-0.0617,-0.3233c-0.034,-0.1778 -0.031,-0.354 0.0066,-0.3916 0.1244,-0.1243 4.1322,-0.0669 4.3319,0.0622 0.1076,0.0695 0.1484,0.1275 0.0905,0.1285 -0.0579,0.001 -0.0138,0.1326 0.0979,0.2921 0.1809,0.2583 0.2032,1.868 0.2032,14.6893l0,7.0437c0.1222,0.1097 0.2418,0.2222 0.3543,0.3424 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0284,0.1977 -0.0745,0.4156 -0.1425,0.6291l0,3.162l-0.3204,0.3202 -0.3202,0.3204l-12.2833,0c-2.2997,0 -4.0746,-0.0014 -5.6134,-0.0056 -0.0821,0.0028 -0.1642,0.0056 -0.2465,0.007 -1.573,0.0365 -3.1419,0.0943 -4.7114,-0.0497 -1.8405,-0.0458 -2.1545,-0.1339 -2.3476,-0.3062 -0.0122,-0.0113 -0.012,-0.136 -0.0038,-0.3622 -1.4398,-0.9037 -1.2865,-2.5133 0.1216,-3.1467 0.087,-3.1061 0.0984,-8.0204 -0.4813,-13.7565 0,0 -3.0916,12.8942 -1.5683,18.5078 0.12,0.1199 3.5868,0.1565 14.8094,0.1565l14.6532,0l0.1547,-0.2893c0.1185,-0.2213 0.1326,-3.8916 0.0597,-15.6261 -0.0524,-8.4354 -0.1236,-15.3828 -0.158,-15.4385 -0.0344,-0.0557 -1.4687,-0.1012 -3.1873,-0.1012 -2.6906,0 -3.1248,-0.0272 -3.1248,-0.1958 0,-0.1687 0.4468,-0.1958 3.237,-0.1958 2.3435,0 3.2801,0.0431 3.3935,0.1565 0.1201,0.1201 0.1565,3.8478 0.1565,15.984 0,15.5145 -0.0049,15.8302 -0.2524,15.9626 -0.1698,0.0908 -5.1043,0.1351 -15.0658,0.1351 -13.2925,0 -14.8341,-0.021 -15.0181,-0.205 -0.1842,-0.1842 -0.2052,-1.8117 -0.2052,-15.9626 0,-12.0817 0.0367,-15.7938 0.1568,-15.9139 0.1136,-0.1136 1.0645,-0.1568 3.4588,-0.1568zM26.0155,23.6967c0.1512,-0.0329 0.5741,-0.0604 1.1069,-0.0757 0.1697,-0.1747 0.3559,-0.433 0.5624,-0.7976 -1.8029,0.0005 -2.0197,0.0259 -2.33,0.27 0.2282,0.1587 0.4378,0.4148 0.6608,0.6034zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.4591,50.3261c-0.0549,0.074 -0.0803,0.1135 -0.0946,0.1374 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.3232,1.0674 -0.4445,1.3843 -0.7699,1.6083 0.3918,-0.0239 0.6756,-0.0498 0.714,-0.0747 0.1094,-0.0708 0.1529,-0.6287 0.1601,-1.9532zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#937b57\"/>\n  <path\n      android:pathData=\"m38.0595,17.9989c-2.0406,0.0005 -4.0813,0.026 -4.2697,0.0764 -0.1607,0.0431 -0.2624,0.1264 -0.2261,0.1851 0.0363,0.0587 0.0062,0.1436 -0.0668,0.1886 -0.073,0.045 -0.1326,0.4825 -0.1326,0.972 0,0.5541 -0.0616,0.9401 -0.1631,1.0227 -0.0897,0.0731 -0.6442,0.1539 -1.232,0.1797 -0.9548,0.0419 -1.1088,0.085 -1.4428,0.4051l-0.374,0.3584l-3.693,0 -3.6933,0l-0.4436,0.4438 -0.4438,0.4438 0.0044,16.0995c0.0038,13.1994 -0.0125,15.8682 0.2853,16.563 0.0849,0.0415 0.2242,0.1435 0.3398,0.2299 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0256,0.0028 0.0511,0.007 0.0765,0.0113 0.3717,-0.0005 0.849,-0.0003 1.1877,-0.0014 0.0251,-0.0099 0.0525,-0.0176 0.0846,-0.0227 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.1881,0c1.2711,-0.0241 2.0646,-0.0645 2.5477,-0.1323 0.0189,-0.0099 0.0381,-0.0203 0.0574,-0.0267 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.041 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0709 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0169 0.0093,-0.0191 0.015,-0.0263 0.0204,-0.0352 0.031,-0.2731 0.0549,-0.7612 0.0732,-1.5754 -0.003,-0.0144 -0.0059,-0.0287 -0.0092,-0.0431 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0198,-0.0695 0.027,-0.1149 0.0614,-0.128 0.0237,-2.363 0.0306,-6.1789 0.0306,-12.4784 0,-17.1989 0.0365,-16.4041 -0.7744,-16.8831 -0.3285,-0.194 -0.8216,-0.2218 -3.9579,-0.2218L46.0625,21.3889l-0.3457,-0.3204c-0.3747,-0.3473 -0.7673,-0.4457 -1.9764,-0.4956l-0.7831,-0.0324 -0.079,-0.9789c-0.0941,-1.1677 -0.1748,-1.3855 -0.5501,-1.4862 -0.1879,-0.0504 -2.2283,-0.0752 -4.2689,-0.0747zM35.8526,19.0536c0.5524,0.0003 1.2831,0.007 2.2522,0.0151l3.9388,0.0345 -0.0403,0.9664c-0.0289,0.6907 0.0084,0.9973 0.1305,1.0748 0.094,0.0595 0.7728,0.1094 1.5086,0.1104 1.8977,0.0028 1.906,0.0127 1.8359,2.2683 -0.0356,1.1458 -0.1167,1.8445 -0.2447,2.1061l-0.1917,0.3916 -6.9545,0.0339 -6.9547,0.0336 -0.2141,-0.4905c-0.1601,-0.3668 -0.216,-0.9135 -0.222,-2.1658 -0.0104,-2.1616 0.002,-2.1752 1.9481,-2.1752 1.6427,0 1.7616,-0.0822 1.5145,-1.0464 -0.0901,-0.3514 -0.1254,-0.7598 -0.0785,-0.9075 0.0629,-0.1982 0.1147,-0.2499 1.772,-0.2491zM38.0628,20.4735c-2.5234,0 -3.1325,0.0339 -3.1325,0.1741 0,0.0957 0.0391,0.2132 0.0869,0.261 0.0478,0.0479 1.4575,0.0869 3.1325,0.0869 2.9584,0 3.0455,-0.007 3.0455,-0.261 0,-0.2538 -0.087,-0.261 -3.1325,-0.261zM26.4073,22.3002 L26.4076,22.3007c2.8482,0 3.302,0.0269 3.302,0.1958 0,0.1653 -0.3039,0.1958 -1.9517,0.1958 -0.1444,0.0555 -0.2948,0.0985 -0.4438,0.1342 0.1342,-0.0014 0.2737,-0.0028 0.4336,-0.0028 1.1269,0 2.0798,0.0307 2.1174,0.0683 0.0375,0.0376 0.0404,0.2138 0.0063,0.3916l-0.0617,0.3232 -1.8449,0.0014c-1.0146,0.001 -1.9182,0.0488 -2.0083,0.106 -0.13,0.0825 -0.1569,3.0339 -0.1305,14.3225l0.0257,11.0434c0.2136,-0.0541 0.4458,-0.0907 0.6992,-0.103 -0.0008,-0.0255 -0.0017,-0.051 -0.002,-0.0764 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0203,-0.1667 0.0051,-0.3331 0.0217,-0.4989 -0,-0.0042 0,-0.0084 0,-0.0141 -0.0003,0.001 -0.0001,0.0014 -0.0005,0.0028 -0.0015,0.0028 -0.0013,-0.0056 -0.0018,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0746 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.001,-0.0377 0.0003,-0.0697 0.0025,-0.0994 -0.001,-1.7637 0.0025,-3.9246 0.0092,-6.5821l0.0316,-12.886c-0.2273,0.0534 -0.2262,0.0307 -0.2661,-0.2106 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1862,-0.0127 0.3308,0.0411 0.4132,0.1323 0.0027,-0.0005 0.0051,-0.0014 0.0076,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0479,0.0279 0.0877,0.0536 0.1272,0.0793 0.0325,-0 0.0649,0 0.0974,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,-0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,-0 0.0489,0l0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0076,0.0142 0.0127,0.0294 0.0171,0.0449l0.2022,0l0.0803,0.84c0.0694,0.7268 0.1356,0.8893 0.4915,1.2073l0.4112,0.3673 7.0134,-0.0005c6.7418,-0.0005 7.0296,-0.0099 7.434,-0.257 0.3065,-0.1869 0.4331,-0.3292 0.5346,-0.7612 0,-0.0588 0.0003,-0.1177 0.0003,-0.1764 -0,-0.0735 0,-0.147 0,-0.2205 0.0403,-0.067 0.0736,-0.126 0.1043,-0.1815 0.0083,-0.0556 0.0162,-0.1043 0.025,-0.1654 -0.0058,-0.0028 -0.0122,-0.0056 -0.0178,-0.0099 -0.0025,-0.0014 -0.0037,-0.0028 -0.0061,-0.0056 -0.0449,0.0014 -0.0903,0.0028 -0.1308,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0521 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.0169 0.0153,-0.007 0.0099,-0.0056 0.0229,-0.0113 0.0903,-0.0992 0.1832,-0.1945 0.3023,-0.2508 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0003,-0.0001 0.0008,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0048,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0758,0.0425 -0.1498,0.0821 -0.2134,0.0981 -0.0114,0.0028 0.019,-0.0127 0.0288,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788l0.0003,0c0.1187,-0.0667 0.2409,-0.1412 0.3286,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0652 0.1205,0.0994 0.2115,0.1799 0.2881,0.2529 0.0403,0.02 0.0803,0.0404 0.1494,0.0719 0.0229,0.0639 0.0493,0.1268 0.0734,0.1904 0.0803,0.1401 0.0838,0.2685 0.0854,0.4777 0.0001,0.0191 0.0021,0.2104 -0.0028,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0062,0.0505 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169 0,0.0099 0,0.0198 0,0.0298 -0,0.1305 0,0.261 0,0.3916l0,0.3316 0,0.3238 0,0.6429 0,0.0978c0.0083,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.0107,0.2708 0.002,0.4045 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.0008,0.0056 -0.0008,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0746 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2068 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.018 -0.0003,0.0267 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0687 0,0.1374 0,0.206 0.1743,0.0715 0.3351,0.1545 0.4729,0.2503 0.021,-1.6077 0.027,-4.1182 0.027,-8.2436 0,-8.0842 -0.0373,-10.655 -0.1565,-10.7742 -0.1071,-0.1071 -0.7422,-0.1568 -2.008,-0.1568l-1.8512,0l-0.0617,-0.3232c-0.034,-0.1778 -0.031,-0.354 0.0066,-0.3916 0.1061,-0.106 4.1178,-0.0764 4.2796,0.0317 0.4359,0.2911 0.4438,0.5579 0.4438,15.1527l0,7.0315c0.1222,0.1097 0.2418,0.2222 0.3543,0.3424 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0284,0.1977 -0.0745,0.4156 -0.1425,0.6291l0,3.2617l-0.3447,0.2712c-0.3442,0.2708 -0.3624,0.2711 -12.8095,0.2378l-4.8578,-0.0127c-0.2565,0.0249 -0.5133,0.0449 -0.7719,0.0462 -1.6172,0.0376 -3.2301,0.099 -4.8437,-0.0612l-1.9914,-0.0056 -0.244,-0.3268c-0.0548,-0.0739 -0.0975,-0.1693 -0.132,-0.4231 -1.1482,-0.8232 -1.1117,-2.1302 -0.1129,-2.8324 -0.0323,-1.8918 -0.0576,-4.8966 -0.0953,-9.9274 -0.0414,-5.5217 0.5387,-11.4676 0.777,-13.3548l-2.6448,4.4206c-1.2657,2.1982 0.0754,2.9518 -0.2498,6.0918 0.0039,-10.5431 0.0416,-14.1155 0.155,-14.2289 0.1136,-0.1136 1.0645,-0.1568 3.4588,-0.1568zM25.8215,28.528c0.0567,-0.2199 -0.0391,-2.7641 -0.0336,-2.8996 0.0086,-0.2123 0.1004,2.3702 0.0336,2.8996zM27.3141,22.8263c-2.1334,0.0304 -1.1095,0.2649 0,0zM49.7833,22.3002c2.3435,0 3.2801,0.0431 3.3935,0.1565 0.1201,0.1201 0.1565,3.8478 0.1565,15.984 0,15.5145 -0.0049,15.8302 -0.2524,15.9626 -0.1698,0.0908 -5.1043,0.1351 -15.0658,0.1351 -13.2925,0 -14.8341,-0.021 -15.0181,-0.205 -0.093,-0.093 -0.1442,-0.5678 -0.1723,-2.8243 0.1359,0.9225 0.3054,1.7632 0.5152,2.4811 0.12,0.1199 3.5868,0.1565 14.8094,0.1565l14.6528,0l0.1547,-0.2893c0.1184,-0.2213 0.1326,-3.8916 0.0596,-15.6261 -0.0524,-8.4354 -0.1236,-15.3828 -0.158,-15.4385 -0.0344,-0.0557 -1.4687,-0.1012 -3.1873,-0.1012 -2.6906,0 -3.1248,-0.0272 -3.1248,-0.1958 0,-0.1687 0.4468,-0.1958 3.237,-0.1958zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.4591,50.3261c-0.0549,0.074 -0.0803,0.1135 -0.0946,0.1374 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.3232,1.0674 -0.4445,1.3843 -0.7699,1.6083 0.3918,-0.0239 0.6756,-0.0498 0.714,-0.0747 0.1094,-0.0708 0.1529,-0.6287 0.1601,-1.9532zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#bb8127\"/>\n  <path\n      android:pathData=\"m38.0595,17.9989c-2.0406,0.0005 -4.0813,0.026 -4.2697,0.0764 -0.1607,0.0431 -0.2624,0.1264 -0.2261,0.1851 0.0363,0.0587 0.0062,0.1436 -0.0668,0.1886 -0.073,0.045 -0.1326,0.4825 -0.1326,0.972 0,0.5541 -0.0616,0.9401 -0.1631,1.0227 -0.0897,0.0731 -0.6442,0.1539 -1.232,0.1797 -0.9548,0.0419 -1.1088,0.085 -1.4428,0.4051l-0.374,0.3584l-3.693,0 -3.6933,0l-0.4436,0.4438 -0.4438,0.4438 0.0044,16.0995c0.0038,13.1994 -0.0125,15.8682 0.2853,16.563 0.0849,0.0415 0.2242,0.1435 0.3398,0.2299 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0256,0.0028 0.0511,0.007 0.0765,0.0113 0.3717,-0.0005 0.849,-0.0003 1.1877,-0.0014 0.0251,-0.0099 0.0525,-0.0176 0.0846,-0.0227 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5881,0 0.1881,0c1.2711,-0.0241 2.0646,-0.0645 2.5477,-0.1323 0.0189,-0.0099 0.0381,-0.0203 0.0574,-0.0267 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.041 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0709 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0169 0.0093,-0.0191 0.015,-0.0263 0.0204,-0.0352 0.031,-0.2731 0.0549,-0.7612 0.0732,-1.5754 -0.003,-0.0144 -0.0059,-0.0287 -0.0092,-0.0431 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0198,-0.0695 0.027,-0.1149 0.0614,-0.128 0.0237,-2.363 0.0306,-6.1789 0.0306,-12.4784 0,-17.1989 0.0365,-16.4041 -0.7744,-16.8831 -0.3285,-0.194 -0.8216,-0.2218 -3.9579,-0.2218L46.0625,21.3889l-0.3457,-0.3204c-0.3747,-0.3473 -0.7673,-0.4457 -1.9764,-0.4956l-0.7831,-0.0324 -0.079,-0.9789c-0.0941,-1.1677 -0.1748,-1.3855 -0.5501,-1.4862 -0.1879,-0.0504 -2.2283,-0.0752 -4.2689,-0.0747zM35.8526,19.0536c0.5524,0.0003 1.2831,0.007 2.2522,0.0151l3.9388,0.0345 -0.0403,0.9664c-0.0289,0.6907 0.0084,0.9973 0.1305,1.0748 0.094,0.0595 0.7728,0.1094 1.5086,0.1104 1.8977,0.0028 1.906,0.0127 1.8359,2.2683 -0.0356,1.1458 -0.1167,1.8445 -0.2447,2.1061l-0.1917,0.3916 -6.9545,0.0339 -6.9547,0.0336 -0.2141,-0.4905c-0.1601,-0.3668 -0.216,-0.9135 -0.222,-2.1658 -0.0104,-2.1616 0.002,-2.1752 1.9481,-2.1752 1.6427,0 1.7616,-0.0822 1.5145,-1.0464 -0.0901,-0.3514 -0.1254,-0.7598 -0.0785,-0.9075 0.0629,-0.1982 0.1147,-0.2499 1.772,-0.2491zM38.0628,20.4735c-2.5234,0 -3.1325,0.0339 -3.1325,0.1741 0,0.0957 0.0391,0.2132 0.0869,0.261 0.0478,0.0479 1.4575,0.0869 3.1325,0.0869 2.9584,0 3.0455,-0.007 3.0455,-0.261 0,-0.2538 -0.087,-0.261 -3.1325,-0.261zM26.4575,22.3005 L26.4578,22.3007l3.3527,0l0.0484,0.3589c0.0266,0.1974 0.0291,0.4911 0.0056,0.6526 -0.042,0.2886 -0.0746,0.2937 -1.8935,0.2957 -1.018,0.001 -1.9243,0.0488 -2.0144,0.106 -0.13,0.0825 -0.1569,3.0339 -0.1305,14.3225l0.0257,11.0434c0.2137,-0.0541 0.4459,-0.0907 0.6995,-0.103 -0.0006,-0.0255 -0.0017,-0.051 -0.002,-0.0764 0.002,-0.181 0.0025,-0.362 0.002,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0203,-0.1667 0.0051,-0.3331 0.0217,-0.4989 -0,-0.0042 0,-0.0084 0,-0.0141 -0.0003,0.001 -0.0001,0.0014 -0.0005,0.0028 -0.0015,0.0028 -0.0015,-0.0056 -0.002,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.0028,-0.147 -0.0018,-0.2205 0.002,-0.116 0.0008,-0.232 0.0003,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.001,-0.0377 0.0003,-0.0697 0.0025,-0.0994 -0.001,-1.7637 0.0025,-3.9246 0.0089,-6.5821l0.0319,-12.8862c-0.2273,0.0534 -0.2262,0.0307 -0.2661,-0.2106 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329 0,0 0.0003,-0.0003 0.0003,-0.0003 0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1064 0.404,-0.1147 0.1863,-0.0127 0.3308,0.0412 0.4132,0.1326 0.0027,-0.0005 0.0051,-0.0014 0.0076,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0478,0.0277 0.0875,0.0534 0.1269,0.079 0.0325,-0 0.0651,0 0.0976,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,0 0.0489,0l0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0076,0.0144 0.013,0.0296 0.0173,0.0452l0.2019,0l0.0803,0.84c0.0694,0.7268 0.1356,0.8893 0.4915,1.2073l0.4112,0.3673 7.0134,-0.0005c6.7418,-0.0005 7.0296,-0.0099 7.434,-0.2569 0.3068,-0.187 0.4332,-0.3293 0.5346,-0.7622 0,-0.0584 0.0003,-0.117 0.0003,-0.1754 -0,-0.0735 0,-0.147 0,-0.2205 0.0401,-0.0667 0.0734,-0.1254 0.104,-0.1807 0.0084,-0.0559 0.0164,-0.105 0.0252,-0.1667 -0.0058,-0.0028 -0.0122,-0.0056 -0.0178,-0.0099 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.045,0.0014 -0.0906,0.0028 -0.1313,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.1321,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.017 0.0156,-0.007 0.0101,-0.0056 0.0235,-0.0113 0.0901,-0.0991 0.1829,-0.1941 0.3018,-0.2503 0.0471,-0.0222 0.0965,-0.0373 0.1473,-0.0472 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0084 0.0897,0.038 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1204,0.0992 0.2113,0.1797 0.2878,0.2526 0.0403,0.02 0.0803,0.0404 0.1494,0.0719 0.0228,0.0638 0.049,0.1267 0.0732,0.1902 0.0806,0.1402 0.0843,0.2687 0.0859,0.4782 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0887 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169 0,0.0099 0,0.0197 0,0.0296 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0971c0.0083,0.0286 0.0167,0.0572 0.025,0.0856 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1336 -0.011,0.2708 0.0018,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0062,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0041,0.19 -0.0028,0.2842 -0.0004,0.0056 -0,-0.0172 -0.0015,-0.0166 0.0039,0.0964 0.0244,0.205 0.0156,0.2998 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0176 -0.0003,0.0265 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0686 0,0.1371 0,0.2057 0.1743,0.0715 0.3351,0.1545 0.4729,0.2503 0.0209,-1.6077 0.027,-4.1182 0.027,-8.2434 0,-8.0842 -0.0373,-10.6552 -0.1565,-10.7745 -0.1071,-0.1071 -0.7422,-0.1565 -2.008,-0.1565l-1.8512,0l-0.063,-0.3294c-0.0346,-0.1812 -0.0097,-0.3623 0.0553,-0.4025 0.0651,-0.0403 0.0826,-0.1857 0.039,-0.3232 -0.0768,-0.242 0.0299,-0.2501 3.3262,-0.2501l3.4055,0l0.1275,0.3347c0.0801,0.2107 0.1272,6.0778 0.1272,15.8293 0,13.8123 -0.022,15.5256 -0.2032,15.7842l-0.2032,0.2901L38.0811,54.5371c-11.4741,0 -15.0129,-0.0365 -15.1328,-0.1565 -0.2275,-0.2274 -0.2275,-31.6975 0,-31.925 0.1137,-0.1137 1.0757,-0.1568 3.5092,-0.1568zM52.2647,22.7902c-0.2188,-0.0042 -0.4892,0.0056 -0.7584,0.0314 -0.5384,0.0514 -0.9149,0.1022 -0.8367,0.1126 0.4297,0.0579 0.4451,0.5839 0.4451,15.156l0,7.0162c0.1222,0.1097 0.2416,0.2223 0.3541,0.3424 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2498 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0881 -0.0284,0.1975 -0.0744,0.4151 -0.1422,0.6284l0,3.263l-0.3439,0.2705c-0.3424,0.2695 -0.3964,0.2705 -12.7815,0.2705l-5.6304,0c-0.0094,0.0001 -0.0188,0.0005 -0.0283,0.001 -1.3539,0.0314 -2.7047,0.0781 -4.0555,-0.001l-2.7233,0l-0.2547,-0.324c-0.0714,-0.0908 -0.1228,-0.1914 -0.1626,-0.4813 -1.1672,-0.8257 -1.131,-2.1474 -0.117,-2.8505 -0.0227,-1.2408 -0.044,-2.9203 -0.0727,-5.3804 -0.0847,-7.2895 -0.1518,-12.4189 -0.2144,-10.5935 -0.0174,0.5073 -0.1867,-0.0577 -0.6319,0.377 -0.3052,0.298 -0.5485,0.6739 -0.5873,0.9075 -0.1002,0.602 -0.3381,18.8188 -0.2488,19.0515 0.0658,0.1713 0.4775,0.2078 2.7078,0.2401 5.4903,0.0797 26.5681,0.094 26.7654,0.0183 0.1867,-0.0717 0.2065,-1.5668 0.2065,-15.5925 0,-14.6429 -0.0128,-15.5196 -0.2284,-15.6302 -0.0628,-0.0322 -0.2301,-0.051 -0.4489,-0.0553zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.4591,50.3261c-0.0549,0.074 -0.0803,0.1135 -0.0946,0.1374 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.3232,1.0674 -0.4445,1.3843 -0.7699,1.6083 0.3918,-0.0239 0.6756,-0.0498 0.714,-0.0747 0.1094,-0.0708 0.1529,-0.6287 0.1601,-1.9532zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#78786e\"/>\n  <path\n      android:pathData=\"m38.0595,17.9989c-2.0406,0.0005 -4.0813,0.026 -4.2697,0.0764 -0.1607,0.0431 -0.2624,0.1264 -0.2261,0.1851 0.0363,0.0587 0.0062,0.1436 -0.0668,0.1886 -0.073,0.045 -0.1326,0.4825 -0.1326,0.972 0,0.5541 -0.0616,0.9401 -0.1631,1.0227 -0.0897,0.0731 -0.6442,0.1539 -1.232,0.1797 -0.9548,0.0419 -1.1088,0.085 -1.4428,0.4051l-0.374,0.3584l-3.693,0 -3.6933,0l-0.4436,0.4438 -0.4438,0.4438 0.0044,16.0995c0.0038,13.1994 -0.0125,15.8682 0.2853,16.563 0.0849,0.0415 0.2242,0.1435 0.3398,0.2299 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0256,0.0028 0.0511,0.007 0.0765,0.0113 0.3717,-0.0005 0.849,-0.0003 1.1877,-0.0014 0.0251,-0.0099 0.0525,-0.0176 0.0846,-0.0227 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.1881,0c1.2711,-0.0241 2.0646,-0.0645 2.5477,-0.1323 0.0189,-0.0099 0.0381,-0.0203 0.0574,-0.0267 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.041 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0709 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0169 0.0093,-0.0191 0.015,-0.0263 0.0204,-0.0352 0.031,-0.2731 0.0549,-0.7612 0.0732,-1.5754 -0.003,-0.0144 -0.0059,-0.0287 -0.0092,-0.0431 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0198,-0.0695 0.027,-0.1149 0.0614,-0.128 0.0237,-2.363 0.0306,-6.1789 0.0306,-12.4784 0,-17.1989 0.0365,-16.4041 -0.7744,-16.8831 -0.329,-0.1943 -0.8264,-0.2218 -4.0377,-0.2218 -3.4526,0 -3.67,-0.0142 -3.794,-0.2457 -0.1899,-0.3548 -0.4618,-0.4384 -1.7049,-0.5251l-1.1094,-0.0774 -0.079,-0.9789c-0.0941,-1.1677 -0.1748,-1.3855 -0.5501,-1.4862 -0.1879,-0.0504 -2.2283,-0.0752 -4.2689,-0.0747zM38.0472,18.9072c2.836,0 3.6725,0.0381 3.8615,0.1764 0.2083,0.1523 0.2296,0.2891 0.1555,0.9983 -0.0561,0.5371 -0.0351,0.8829 0.0607,0.9983 0.1059,0.1275 0.5218,0.1764 1.501,0.1764 1.945,0 1.9244,-0.0279 1.8523,2.513 -0.0527,1.8549 -0.0717,1.9727 -0.3564,2.2033 -0.2849,0.2307 -0.6567,0.2434 -7.0486,0.2434 -5.1941,0 -6.7982,-0.0376 -6.967,-0.1632 -0.2978,-0.2215 -0.4831,-1.3109 -0.4831,-2.839 0,-1.087 0.0389,-1.3193 0.2705,-1.6137 0.2604,-0.331 0.3204,-0.3439 1.5989,-0.3459 0.7306,-0.001 1.4094,-0.0534 1.5084,-0.116 0.1388,-0.088 0.159,-0.307 0.0892,-0.959 -0.1424,-1.3296 -0.3205,-1.2723 3.9571,-1.2723zM26.4575,22.3005 L26.4578,22.3008l3.3527,0l0.0612,0.417c0.1309,0.8916 0.1387,0.8881 -1.9234,0.8881 -1.4087,0 -1.8972,0.0431 -2.0085,0.1772 -0.1076,0.1296 -0.138,3.9788 -0.1137,14.3245l0.0257,10.9723c0.2137,-0.0541 0.4459,-0.0907 0.6995,-0.103 -0.0006,-0.0255 -0.0017,-0.051 -0.002,-0.0764 0.002,-0.181 0.0023,-0.362 0.0018,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0203,-0.1667 0.0051,-0.3331 0.0217,-0.4989 -0,-0.0042 0,-0.0084 0,-0.0141 -0.0003,0.001 -0.0001,0.0014 -0.0005,0.0028 -0.0015,0.0028 -0.0013,-0.0056 -0.0018,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.001,-0.0377 0.0003,-0.0697 0.0025,-0.0994 -0.001,-1.7637 0.0025,-3.9246 0.0092,-6.5821l0.0316,-12.8862c-0.2273,0.0534 -0.2262,0.0307 -0.2661,-0.2106 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1064 0.404,-0.1147 0.1863,-0.0127 0.3308,0.0412 0.4132,0.1326 0.0027,-0.0005 0.0051,-0.0014 0.0076,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0478,0.0277 0.0875,0.0534 0.1269,0.079 0.0325,-0 0.0651,0 0.0976,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0076,0.0144 0.013,0.0296 0.0173,0.0452l0.2019,0l0.0803,0.84c0.0694,0.7268 0.1356,0.8893 0.4915,1.2073l0.4112,0.3673 7.0134,-0.0005c6.7418,-0.0005 7.0296,-0.0099 7.434,-0.2569 0.3067,-0.187 0.4332,-0.3293 0.5346,-0.7622 0,-0.0584 0.0003,-0.117 0.0003,-0.1754 -0,-0.0735 0,-0.147 0,-0.2205 0.0401,-0.0667 0.0734,-0.1254 0.104,-0.1807 0.0084,-0.0559 0.0164,-0.105 0.0252,-0.1667 -0.0058,-0.0028 -0.0122,-0.0056 -0.0178,-0.0099 -0.0024,-0.0014 -0.0035,-0.0028 -0.0059,-0.0042 -0.045,0.0014 -0.0905,0.0028 -0.131,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.0168 0.0153,-0.007 0.0099,-0.0056 0.0229,-0.0113 0.0903,-0.0992 0.1832,-0.1945 0.3023,-0.2508 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0427 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.037 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1154 0.7441,0.0652 0.1204,0.0992 0.2113,0.1797 0.2878,0.2526 0.0403,0.02 0.0803,0.0404 0.1494,0.0719 0.0228,0.0638 0.049,0.1267 0.0732,0.1902 0.0806,0.1402 0.0843,0.2687 0.0859,0.4782 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0504 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169 0,0.0099 0,0.0197 0,0.0296 -0,0.1305 0,0.261 0,0.3916l0,0.3316 0,0.3238 0,0.6429 0,0.0971c0.0083,0.0286 0.0167,0.0572 0.025,0.0856 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.011,0.2708 0.0018,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2556 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1978 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1198 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0227,0.1936 0.0143,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0176 -0.0003,0.0265 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0686 0,0.1371 0,0.2057 0.1743,0.0715 0.3351,0.1545 0.4729,0.2503 0.0209,-1.6077 0.027,-4.1181 0.027,-8.2434 0,-8.0842 -0.0373,-10.6552 -0.1565,-10.7745 -0.1071,-0.1071 -0.7422,-0.1565 -2.008,-0.1565 -1.797,0 -1.8534,-0.0084 -1.9262,-0.2868 -0.0413,-0.1578 -0.0178,-0.3559 0.0523,-0.4402 0.07,-0.0843 0.0907,-0.249 0.0459,-0.3658 -0.074,-0.1927 0.233,-0.2123 3.3293,-0.2123l3.4106,0l0.1275,0.3347c0.0801,0.2107 0.1272,6.0778 0.1272,15.8293 0,13.8123 -0.022,15.5256 -0.2032,15.7842l-0.2032,0.2901l-14.976,0c-11.4741,0 -15.0128,-0.0365 -15.1328,-0.1565 -0.2274,-0.2274 -0.2274,-31.6975 0,-31.925 0.1137,-0.1137 1.0757,-0.1568 3.5092,-0.1568zM52.2647,22.7902c-0.2188,-0.0042 -0.4892,0.0056 -0.7584,0.0314 -0.5384,0.0514 -0.9149,0.1022 -0.8366,0.1126 0.4297,0.0579 0.4451,0.5839 0.4451,15.156l0,7.0162c0.1222,0.1097 0.2416,0.2223 0.3541,0.3424 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2498 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0881 -0.0284,0.1975 -0.0744,0.4151 -0.1422,0.6284l0,3.263l-0.3439,0.2705c-0.3424,0.2695 -0.3964,0.2705 -12.7815,0.2705l-5.6304,0c-0.0094,0.0001 -0.0188,0.0005 -0.0283,0.001 -1.3539,0.0314 -2.7047,0.0781 -4.0555,-0.001l-2.7233,0l-0.2547,-0.324c-0.0714,-0.0908 -0.1228,-0.1914 -0.1626,-0.4813 0.7345,-2.3139 -1.131,-2.1474 -0.117,-2.8505 -0.0227,-1.2408 -0.044,-2.9203 -0.0727,-5.3804 -0.063,-5.4163 1.011,-10.2622 0.6712,-12.3687 -0.1285,0.5683 1.2386,-1.7837 0,0 -0.8346,1.0064 -2.3133,2.925 -2.3522,3.1587 -0.1002,0.602 -0.0909,18.7199 -0.0015,18.9526 0.0658,0.1713 0.4775,0.2078 2.7078,0.2401 5.4903,0.0797 26.5681,0.094 26.7654,0.0183 0.1867,-0.0717 0.2065,-1.5668 0.2065,-15.5925 0,-14.6429 -0.0128,-15.5196 -0.2284,-15.6302 -0.0628,-0.0322 -0.2301,-0.051 -0.4489,-0.0553zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8568c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9185c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.158,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0828,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0072,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM25.2026,31.6983c0.4134,-0 0.2319,-0.4442 0.4134,-0 -0.002,0.0559 -0.0694,5.0438 -0.0853,5.1142 0.0283,-0.5226 0.1201,-5.246 0.0853,-5.1142zM49.7736,36.5158c0.0101,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7608c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8617c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.4591,50.3261c-0.0549,0.074 -0.0803,0.1135 -0.0946,0.1374 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.3254,1.0747 -0.4463,1.3886 -0.7767,1.6129 0.3969,-0.0235 0.675,-0.0505 0.7209,-0.0801 0.1093,-0.0701 0.1528,-0.6275 0.1601,-1.9524zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#b97e23\"/>\n  <path\n      android:pathData=\"m38.0595,17.9989c-2.0406,0.0005 -4.0813,0.026 -4.2697,0.0764 -0.1607,0.0431 -0.2624,0.1264 -0.2261,0.1851 0.0363,0.0587 0.0062,0.1436 -0.0668,0.1886 -0.073,0.045 -0.1326,0.4825 -0.1326,0.972 0,0.5541 -0.0616,0.9401 -0.1631,1.0227 -0.0897,0.0731 -0.6442,0.1539 -1.232,0.1797 -0.9548,0.0419 -1.1088,0.085 -1.4428,0.4051l-0.374,0.3584l-3.693,0 -3.6933,0l-0.4436,0.4438 -0.4438,0.4438 0.0044,16.0995c0.0038,13.1994 -0.0125,15.8682 0.2853,16.563 0.0849,0.0415 0.2242,0.1435 0.3398,0.2299 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0256,0.0028 0.0511,0.007 0.0765,0.0113 0.3717,-0.0005 0.849,-0.0003 1.1877,-0.0014 0.0251,-0.0099 0.0525,-0.0176 0.0846,-0.0227 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5881,0 0.1881,0c1.2711,-0.0241 2.0646,-0.0645 2.5477,-0.1323 0.0189,-0.0099 0.0381,-0.0203 0.0574,-0.0267 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.041 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0709 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0169 0.0093,-0.0191 0.015,-0.0263 0.0204,-0.0352 0.031,-0.2731 0.0549,-0.7612 0.0732,-1.5754 -0.003,-0.0144 -0.0059,-0.0287 -0.0092,-0.0431 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0356 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0198,-0.0695 0.027,-0.1149 0.0614,-0.128 0.0237,-2.363 0.0306,-6.1789 0.0306,-12.4784 0,-17.1989 0.0365,-16.4041 -0.7744,-16.8831 -0.329,-0.1943 -0.8264,-0.2218 -4.0377,-0.2218 -3.4526,0 -3.67,-0.0142 -3.794,-0.2457 -0.1899,-0.3548 -0.4618,-0.4384 -1.7049,-0.5251l-1.1094,-0.0774 -0.079,-0.9789c-0.0941,-1.1677 -0.1748,-1.3855 -0.5501,-1.4862 -0.1879,-0.0504 -2.2283,-0.0752 -4.2689,-0.0747zM38.0472,18.9072c2.2639,0.3076 4.2356,-0.7238 4.017,1.1747 -0.0561,0.5371 -0.0351,0.8829 0.0607,0.9983 0.1059,0.1275 0.5218,0.1764 1.501,0.1764 1.945,0 1.9244,-0.0279 1.8523,2.513 -0.0527,1.8549 -0.0717,1.9727 -0.3564,2.2033 -0.2849,0.2307 -0.6567,0.2434 -7.0486,0.2434 -5.1941,0 -6.7982,-0.0376 -6.967,-0.1632 -0.2978,-0.2215 -0.4831,-1.3109 -0.4831,-2.839 0,-1.087 0.0389,-1.3193 0.2705,-1.6137 0.2604,-0.331 0.3204,-0.3439 1.5989,-0.3459 0.7306,-0.001 1.4094,-0.0534 1.5084,-0.116 0.1388,-0.088 0.159,-0.307 0.0892,-0.959 -0.1424,-1.3296 -0.3205,-1.2723 3.9571,-1.2723zM24.6285,22.1865 L24.6287,22.1868c0.4608,-0.0001 1.0604,0.0056 1.8474,0.0141l3.2882,0.0349 0.0885,0.4334c0.1897,0.9282 0.1715,0.9371 -1.9045,0.9371 -1.4087,0 -1.8972,0.0431 -2.0085,0.1772 -0.1076,0.1296 -0.138,3.9788 -0.1137,14.3245l0.0257,10.9723c0.2137,-0.0541 0.4459,-0.0907 0.6995,-0.103 -0.0006,-0.0255 -0.0017,-0.051 -0.002,-0.0764 0.002,-0.181 0.0023,-0.362 0.0018,-0.543 -0.0004,-0.1306 -0,-0.2614 0,-0.3921 -0.0051,-0.1354 0.0049,-0.2658 0.0194,-0.401 0.0134,-0.1232 0.0107,-0.0476 0.012,-0.1135 -0.0203,-0.1667 0.0051,-0.3331 0.0217,-0.4989 -0,-0.0042 0,-0.0084 0,-0.0141 -0.0003,0.001 -0.0001,0.0014 -0.0005,0.0028 -0.0015,0.0028 -0.0013,-0.0056 -0.0018,-0.0084 -0.0015,-0.0214 -0.0021,-0.0428 -0.0031,-0.0642 -0.0025,-0.0735 -0.003,-0.147 -0.002,-0.2205 0.002,-0.116 0.001,-0.232 0.0005,-0.348 -0,-0.0747 0,-0.1494 0,-0.2241 -0,-0.1101 -0,-0.2202 0,-0.3304l0,-0.193 0,-0.1886 0,-0.2855 0,-0.218 0,-0.2246c-0.001,-0.0377 0.0003,-0.0697 0.0025,-0.0994 -0.001,-1.7637 0.0025,-3.9246 0.0092,-6.5821l0.0316,-12.8862c-0.2273,0.0534 -0.2262,0.0307 -0.2661,-0.2106 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1064 0.404,-0.1147 0.1863,-0.0127 0.3308,0.0412 0.4132,0.1326 0.0027,-0.0005 0.0051,-0.0014 0.0076,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0478,0.0277 0.0875,0.0534 0.1269,0.079 0.0325,-0 0.0651,0 0.0976,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,0 0.0489,0l0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0076,0.0144 0.013,0.0296 0.0173,0.0452l0.2019,0l0.0803,0.84c0.0694,0.7268 0.1356,0.8893 0.4915,1.2073l0.4112,0.3673 7.0134,-0.0005c6.7418,-0.0005 7.0296,-0.0099 7.434,-0.2569 0.3067,-0.187 0.4332,-0.3293 0.5346,-0.7622 0,-0.0584 0.0003,-0.117 0.0003,-0.1754 -0,-0.0735 0,-0.147 0,-0.2205 0.0401,-0.0667 0.0734,-0.1254 0.104,-0.1807 0.0084,-0.0559 0.0164,-0.105 0.0252,-0.1667 -0.0058,-0.0028 -0.0122,-0.0056 -0.0178,-0.0099 -0.0023,-0.0014 -0.0034,-0.0028 -0.0056,-0.0042 -0.045,0.0014 -0.0906,0.0028 -0.1313,0.0028 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0581,-0.0532 0.1119,-0.1132 0.1657,-0.1728 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.017 0.0156,-0.007 0.0101,-0.0056 0.0235,-0.0113 0.0901,-0.0991 0.1829,-0.1941 0.3018,-0.2503 0.0471,-0.0222 0.0965,-0.0373 0.1473,-0.0472 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0196 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0787 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.0981,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.1204,0.0992 0.2113,0.1797 0.2878,0.2526 0.0403,0.02 0.0803,0.0404 0.1494,0.0719 0.0228,0.0638 0.049,0.1267 0.0732,0.1902 0.0806,0.1402 0.0843,0.2687 0.0859,0.4782 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0887 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169 0,0.0099 0,0.0197 0,0.0296 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0981c0.0083,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.032,0.2067 0.0423,0.3107 0.0132,0.1336 -0.011,0.2708 0.0018,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0227,0.1936 0.0143,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0176 -0.0003,0.0265 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0686 0,0.1371 0,0.2057 0.1743,0.0715 0.3351,0.1545 0.4729,0.2503 0.0209,-1.6077 0.027,-4.1182 0.027,-8.2434 0,-8.0842 -0.0373,-10.6552 -0.1565,-10.7745 -0.1071,-0.1071 -0.7422,-0.1565 -2.008,-0.1565 -1.7907,0 -1.8537,-0.0099 -1.9254,-0.2837 -0.0409,-0.156 -0.035,-0.3467 0.0128,-0.4239 0.0477,-0.0771 0.0891,-0.2578 0.0915,-0.4015 0.0049,-0.2531 0.1047,-0.2624 3.2329,-0.2962 3.088,-0.0334 3.2395,-0.0235 3.4914,0.2284 0.2613,0.2612 0.2633,0.3923 0.2633,15.9343 0,10.5527 -0.0441,15.7534 -0.1351,15.9233 -0.1324,0.2474 -0.4403,0.2524 -15.2741,0.2524L22.9159,54.5379l-0.1275,-0.3348c-0.0801,-0.2107 -0.1272,-6.1106 -0.1272,-15.9234 0,-15.4587 0.0025,-15.5909 0.2633,-15.852 0.189,-0.189 0.3215,-0.2417 1.7039,-0.2422zM51.2174,22.8488c-0.3759,0.0028 -0.6058,0.0297 -0.5465,0.0707 0.4361,0.3014 0.4438,0.5607 0.4438,15.156l0,7.031c0.1222,0.1097 0.2416,0.2223 0.3541,0.3424 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2746,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0303 -0.0199,0.0604 -0.0227,0.0883 -0.0284,0.1975 -0.0744,0.4151 -0.1422,0.6284l0,3.263l-0.3447,0.2712c-0.3442,0.2708 -0.3624,0.2711 -12.8095,0.2378l-4.8629,-0.0127c-0.2549,0.0246 -0.5101,0.0443 -0.7671,0.0456 -1.6152,0.0374 -3.226,0.0987 -4.8376,-0.0607l-1.9973,-0.0056 -0.234,-0.3263c-0.0551,-0.0767 -0.0977,-0.178 -0.1323,-0.4169 -1.1578,-0.8228 -1.1245,-2.1349 -0.1231,-2.8391 -0.0256,-1.3095 -0.0495,-3.1248 -0.0811,-5.815 -0.0564,-4.8096 -0.0842,-7.6141 -0.1221,-7.1689 -0.0617,0.7235 -0.1731,0.0991 -0.6325,0.5695l-0.5139,0.5262 -0.0959,1.5356c-0.0526,0.8446 -0.1388,4.4926 -0.1914,8.1067 -0.0811,5.5743 -0.0678,6.5889 0.0877,6.6876 0.1053,0.0667 6.4064,0.1032 14.8015,0.0854l14.6182,-0.0311 0.0308,-10.311c0.0169,-5.6711 -0.0124,-12.6604 -0.0653,-15.5318l-0.0961,-5.2208 -1.1094,-0.0339c-0.1525,-0.0042 -0.294,-0.007 -0.4193,-0.0056zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.158,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0828,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0072,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0101,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.9329,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.4591,50.3261c-0.0549,0.074 -0.0803,0.1135 -0.0946,0.1374 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.3254,1.0747 -0.4463,1.3886 -0.7767,1.6129 0.3969,-0.0235 0.675,-0.0505 0.7209,-0.0801 0.1093,-0.0701 0.1528,-0.6275 0.1601,-1.9524zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#727371\"/>\n  <path\n      android:pathData=\"m36.4667,18.1206l0,0.0003c-3.3648,-0.007 -2.9906,0.0498 -3.0945,1.439l-0.0732,0.9789 -1.1099,0.0763c-1.1213,0.0769 -1.5141,0.1995 -1.8209,0.5692 -0.1451,0.1748 -0.6826,0.2029 -3.8852,0.2029l-3.717,0l-0.4436,0.4438 -0.4438,0.4438 0.0044,16.0995c0.0038,13.1994 -0.0125,15.8682 0.2853,16.563 0.0849,0.0415 0.2242,0.1435 0.3398,0.2299 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0256,0.0028 0.0511,0.007 0.0765,0.0113 0.3717,-0.0005 0.849,-0.0003 1.1877,-0.0014 0.0251,-0.0099 0.0525,-0.0176 0.0846,-0.0227 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5881,0 0.1881,0c1.2711,-0.0241 2.0646,-0.0645 2.5477,-0.1323 0.0189,-0.0099 0.0381,-0.0203 0.0574,-0.0267 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0169 0.0093,-0.0191 0.015,-0.0265 0.0204,-0.0352 0.031,-0.2731 0.0549,-0.7612 0.0732,-1.5754 -0.003,-0.0144 -0.0059,-0.0287 -0.0092,-0.0431 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0358 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2746 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0198,-0.0695 0.027,-0.1149 0.0614,-0.128 0.0237,-2.363 0.0306,-6.1789 0.0306,-12.4784 0,-17.1989 0.0365,-16.4041 -0.7744,-16.8831 -0.3289,-0.1943 -0.8256,-0.2218 -4.0221,-0.2218 -3.4776,0 -3.6578,-0.0127 -3.8824,-0.2603 -0.3094,-0.3418 -0.9883,-0.5211 -1.9769,-0.5221 -0.937,-0.001 -0.9603,-0.031 -0.9603,-1.2438 0,-0.4565 -0.0599,-0.8668 -0.1328,-0.9118 -0.0729,-0.045 -0.0987,-0.1365 -0.0574,-0.2034 0.0458,-0.074 -1.6796,-0.1216 -4.4216,-0.1216 -0.6456,0 -1.2026,-0.0028 -1.6832,-0.0028zM39.7279,18.9312c1.4892,0.0042 2.2364,0.0407 2.3073,0.1122 0.0765,0.0771 0.1389,0.5626 0.1389,1.0788l0,0.9386l1.3705,0.0704c0.7537,0.0387 1.4703,0.1175 1.5925,0.1749 0.4152,0.1949 0.5388,0.9375 0.4456,2.68 -0.059,1.1042 -0.1508,1.731 -0.2827,1.9323l-0.1953,0.298 -6.9471,-0.0014c-3.8209,-0.001 -7.0168,-0.0462 -7.1021,-0.1004 -0.3195,-0.2032 -0.4323,-0.8784 -0.4323,-2.5874 0,-1.694 0.012,-1.7714 0.3204,-2.0799 0.2995,-0.2995 0.4097,-0.3204 1.6914,-0.3204l1.3712,0l0.0377,-1.0768 0.038,-1.0768 3.9072,-0.0346c0.6637,-0.0056 1.2424,-0.0084 1.7388,-0.007zM27.6342,22.1692 L27.6345,22.1695c2.3785,0.0014 2.2038,0.0611 2.2441,0.9259l0.0268,0.5759 -1.9578,0.0653 -1.9578,0.0653 -0.0331,14.194 -0.026,11.0646c0.179,-0.0404 0.3712,-0.0676 0.5769,-0.0798 -0.0835,-5.9078 -0.0853,-20.9501 0.0145,-23.8901 -0.1103,0.0113 -0.1161,-0.0429 -0.1476,-0.2327 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329 0,0 0.0003,-0.0003 0.0003,-0.0003 0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1327 0.0025,-0.0004 0.0046,-0.0014 0.0072,-0.0014 0.2194,-0.0335 0.4733,-0.0833 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0328,-0 0.0656,0 0.0984,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,0 0.0489,0l0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0069,0.0127 0.0115,0.027 0.0158,0.0411l0.2032,0.0042 0.0803,0.7464c0.1008,0.9378 0.3719,1.3572 0.996,1.54 0.3367,0.0988 2.6261,0.1309 7.3203,0.1032l6.8306,-0.0403 0.3806,-0.4262c0.2163,-0.2422 0.3162,-0.4172 0.3574,-0.6735 -0,-0.0099 0,-0.0201 0,-0.0301 -0,-0.0735 0,-0.147 0,-0.2205 0.009,-0.0151 0.015,-0.0263 0.0235,-0.0408 0.0005,-0.0266 0.002,-0.0508 0.002,-0.0793 0.0003,-0.0909 0.0013,-0.1654 0.0032,-0.2399 -0.0174,0 -0.0375,0.0014 -0.0543,0.001 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1269,-0.001 -0.1321,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0582,-0.0534 0.1121,-0.1133 0.166,-0.1731 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.017 0.0155,-0.007 0.01,-0.0056 0.0232,-0.0113 0.0901,-0.099 0.183,-0.1939 0.3018,-0.2501 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.0981,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.12,0.099 0.2107,0.1791 0.287,0.2519 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1275 0.0737,0.1914 0.08,0.1398 0.0835,0.2682 0.0851,0.4772 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0251,0.1846 0.015,0.2654 -0.0063,0.0504 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169 0,0.0099 0,0.0196 0,0.0293 -0,0.1305 0,0.261 0,0.3916l0,0.3316 0,0.3238 0,0.6429 0,0.0984c0.0082,0.0283 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3108 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3393 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0746 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2068 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0062,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.1194,0.049 0.2341,0.1022 0.3373,0.1629 0.0194,-1.6826 0.0147,-4.2414 0.0018,-8.1883l-0.0352,-10.7026 -1.9563,-0.0653c-1.754,-0.0586 -1.9608,-0.0897 -2.0014,-0.3013 -0.0249,-0.1298 -0.0025,-0.3354 0.0497,-0.4568 0.0523,-0.1215 0.1015,-0.3382 0.1091,-0.4818 0.0134,-0.2526 0.1177,-0.2622 3.2418,-0.296 3.088,-0.0334 3.2395,-0.0235 3.4914,0.2284 0.2613,0.2611 0.2633,0.3923 0.2633,15.9343 0,10.5528 -0.0441,15.7534 -0.1351,15.9234 -0.1324,0.2474 -0.4403,0.2524 -15.2741,0.2524L22.9159,54.5378l-0.1275,-0.3347c-0.0801,-0.2109 -0.1272,-6.1106 -0.1272,-15.9234 0,-15.4145 0.003,-15.5916 0.261,-15.8497 0.2471,-0.2471 0.4351,-0.261 3.5281,-0.261 0.4521,0 0.8441,-0.001 1.1839,-0.001zM51.2174,22.8488c-0.3759,0.0028 -0.6058,0.0297 -0.5465,0.0707 0.4361,0.3014 0.4438,0.5607 0.4438,15.156l0,7.031c0.1222,0.1097 0.2416,0.2223 0.3541,0.3424 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0303 -0.0199,0.0604 -0.0227,0.0883 -0.0284,0.1975 -0.0744,0.4151 -0.1422,0.6284l0,3.263l-0.3447,0.2712c-0.3442,0.2708 -0.3624,0.2711 -12.8095,0.2378l-4.8629,-0.0127c-0.2549,0.0246 -0.5101,0.0443 -0.7671,0.0456 -1.6152,0.0374 -3.226,0.0987 -4.8376,-0.0607l-1.9973,-0.0056 -0.234,-0.3263c-0.0551,-0.0767 -0.0977,-0.1781 -0.1323,-0.417 -1.1578,-0.8228 -1.1245,-2.1349 -0.1231,-2.8391 -0.0982,-13.973 1.1772,-10.5945 0.8611,-14.829l-2.7876,2.9408 0.0691,1.8158c-0.0526,0.8446 0.2731,4.2125 0.2205,7.8266 -0.0811,5.5743 -0.0678,6.5889 0.0877,6.6876 0.1053,0.0669 6.4064,0.1032 14.8015,0.0854l14.6182,-0.0311 0.0308,-10.311c0.0169,-5.6711 -0.0124,-12.6604 -0.0653,-15.5318l-0.0961,-5.2208 -1.1094,-0.0339c-0.1525,-0.0042 -0.294,-0.007 -0.4193,-0.0056zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.3982,50.4141c-0.009,0.0141 -0.0282,0.0404 -0.0337,0.0494 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.2888,0.954 -0.416,1.3085 -0.672,1.5331 0.1729,-0.0113 0.2997,-0.0229 0.3416,-0.0314 0.264,-0.0532 0.2857,-0.157 0.3714,-1.7867 0.0009,-0.0169 0.0014,-0.03 0.0023,-0.0467zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#b37b23\"/>\n  <path\n      android:pathData=\"m36.4667,18.1206l0,0.0003c-3.3648,-0.007 -2.9906,0.0498 -3.0945,1.439l-0.0732,0.9789 -1.1099,0.0763c-1.1213,0.0769 -1.5141,0.1995 -1.8209,0.5692 -0.1451,0.1748 -0.6826,0.2029 -3.8852,0.2029l-3.717,0l-0.4436,0.4438 -0.4438,0.4438 0.0044,16.0995c0.0038,13.1994 -0.0125,15.8682 0.2853,16.563 0.0849,0.0415 0.2242,0.1435 0.3398,0.2299 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0256,0.0028 0.0511,0.007 0.0765,0.0113 0.3717,-0.0005 0.849,-0.0003 1.1877,-0.0014 0.0251,-0.0099 0.0525,-0.0176 0.0846,-0.0227 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5881,0 0.1881,0c1.2711,-0.0241 2.0646,-0.0645 2.5477,-0.1323 0.0189,-0.0099 0.0381,-0.0203 0.0574,-0.0267 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0157,-0.028 0.0331,-0.0555 0.0467,-0.0846 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0169 0.0093,-0.0191 0.015,-0.0265 0.0204,-0.0352 0.031,-0.2731 0.0549,-0.7612 0.0732,-1.5754 -0.003,-0.0144 -0.0059,-0.0287 -0.0092,-0.0431 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0358 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2746 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0198,-0.0695 0.027,-0.1149 0.0614,-0.128 0.0237,-2.363 0.0306,-6.1789 0.0306,-12.4784 0,-17.1989 0.0365,-16.4041 -0.7744,-16.8831 -0.3289,-0.1943 -0.8256,-0.2218 -4.0221,-0.2218 -3.4776,0 -3.6578,-0.0127 -3.8824,-0.2603 -0.3094,-0.3418 -0.9883,-0.5211 -1.9769,-0.5221 -0.937,-0.001 -0.9603,-0.031 -0.9603,-1.2438 0,-0.4565 -0.0599,-0.8668 -0.1328,-0.9118 -0.0729,-0.045 -0.0987,-0.1365 -0.0574,-0.2034 0.0458,-0.074 -1.6796,-0.1216 -4.4216,-0.1216 -0.6456,0 -1.2026,-0.0028 -1.6832,-0.0028zM39.7279,18.9312c1.4892,0.0042 2.2364,0.0407 2.3073,0.1122 0.0765,0.0771 0.1389,0.5626 0.1389,1.0788l0,0.9386l1.3705,0.0704c0.7537,0.0387 1.4703,0.1175 1.5925,0.1749 0.4152,0.1949 0.5388,0.9375 0.4456,2.68 -0.059,1.1042 -0.1508,1.731 -0.2827,1.9323l-0.1953,0.298 -6.9471,-0.0014c-3.8209,-0.001 -7.0168,-0.0462 -7.1021,-0.1004 -0.3195,-0.2032 -0.4323,-0.8784 -0.4323,-2.5874 0,-1.694 0.012,-1.7714 0.3204,-2.0799 0.2995,-0.2995 0.4097,-0.3204 1.6914,-0.3204l1.3712,0l0.0377,-1.0768 0.038,-1.0768 3.9072,-0.0346c0.6637,-0.0056 1.2424,-0.0084 1.7388,-0.007zM26.4389,22.1702l0.0003,0 3.3672,0l0.0892,0.6717c0.049,0.3694 0.0406,0.7201 -0.0186,0.7793 -0.0592,0.0593 -0.9582,0.124 -1.9976,0.144l-1.8897,0.0365 -0.0331,14.194 -0.026,11.0646c0.179,-0.0404 0.3712,-0.0676 0.5769,-0.0798 -0.0835,-5.9078 -0.0853,-20.9501 0.0145,-23.8901 -0.1103,0.0113 -0.1162,-0.0429 -0.1476,-0.2327 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329 0,0 0.0003,-0.0003 0.0003,-0.0003 0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1064 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1327 0.0025,-0.0004 0.0046,-0.0014 0.0072,-0.0014 0.2194,-0.0335 0.4733,-0.0833 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0328,-0 0.0656,0 0.0984,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,0 0.0489,0l0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0069,0.0127 0.0115,0.027 0.0158,0.0411l0.2032,0.0042 0.0803,0.7464c0.1008,0.9378 0.3719,1.3572 0.996,1.54 0.3367,0.0988 2.6261,0.1309 7.3203,0.1032l6.8306,-0.0403 0.3806,-0.4262c0.2163,-0.2422 0.3162,-0.4172 0.3574,-0.6735 -0,-0.0099 0,-0.0201 0,-0.0301 -0,-0.0735 0,-0.147 0,-0.2205 0.009,-0.0151 0.015,-0.0265 0.0235,-0.0408 0.0005,-0.0266 0.002,-0.051 0.002,-0.0793 0.0003,-0.0909 0.0013,-0.1654 0.0032,-0.2399 -0.0174,0 -0.0375,0.0014 -0.0543,0.001 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0582,-0.0534 0.1121,-0.1133 0.166,-0.1731 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.017 0.0155,-0.007 0.01,-0.0056 0.0232,-0.0113 0.0901,-0.0991 0.183,-0.1939 0.3018,-0.2501 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.0981,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.12,0.099 0.2107,0.1791 0.287,0.2519 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1275 0.0737,0.1914 0.08,0.1398 0.0835,0.2682 0.0851,0.4772 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.015,0.1007 -0.0232,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.083,0.0569 -0.1666,0.1129 -0.2501,0.169 0,0.0099 0,0.0196 0,0.0293 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0984c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.0008,0.0056 -0.0008,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.793 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0746 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0854 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0939 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0176 -0.0003,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.1193,0.049 0.2339,0.1019 0.337,0.1626 0.0194,-1.6826 0.0147,-4.2413 0.0018,-8.188l-0.0349,-10.7026 -1.8897,-0.0365c-1.0394,-0.02 -1.9383,-0.0847 -1.9976,-0.144 -0.0592,-0.0593 -0.0676,-0.4099 -0.0186,-0.7793l0.0892,-0.6717l3.302,0c2.8279,0 3.3316,0.0296 3.5072,0.2052 0.1842,0.1842 0.2052,1.8179 0.2052,16.0335 0,15.1687 -0.0099,15.8377 -0.2386,16.0447 -0.2192,0.1984 -1.4553,0.2159 -15.1112,0.2159 -13.2495,0 -14.9043,-0.0221 -15.1627,-0.2032l-0.2898,-0.2032l0,-15.8413c0,-14.2272 0.021,-15.8623 0.2052,-16.0465 0.1759,-0.1758 0.6852,-0.2052 3.5725,-0.2052zM51.7439,22.9533c-0.8301,0 -0.9074,0.022 -0.8063,0.2284 0.0615,0.1256 0.127,6.7874 0.1456,14.8037l0.0166,7.1077c0.1275,0.1137 0.2521,0.2306 0.3691,0.3556 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0303 -0.0199,0.0604 -0.0227,0.0883 -0.029,0.2022 -0.077,0.4253 -0.1476,0.6434l0.0076,3.2461 -0.3452,0.2715c-0.3439,0.2704 -0.3845,0.2712 -12.7825,0.2712l-5.6304,0c-0.0094,0.0001 -0.0188,0.0005 -0.0283,0.001 -1.3539,0.0314 -2.7047,0.0781 -4.0555,-0.001l-2.7233,0l-0.2705,-0.3439c-0.0988,-0.1257 -0.1615,-0.2294 -0.2014,-0.5007 -1.1097,-0.8264 -1.0605,-2.1148 -0.0673,-2.8077 -0.0003,-0.237 -0.0018,-0.3745 -0.0018,-0.6521 0,-4.0344 -0.1142,-6.7898 -0.2812,-6.7898 -0.0608,0 -0.1104,0.1408 -0.1104,0.3128 0,0.1963 -0.177,0.468 -0.4752,0.7298l-0.4749,0.4173 -0.1063,2.0434c-0.0584,1.124 -0.1087,3.4671 -0.1122,5.207l-0.0061,3.1636 4.5355,0.0365l0,-0.0003c2.4946,0.0201 9.0874,0.0355 14.6508,0.0345l10.1152,-0.0014 0.0385,-14.8139c0.0211,-8.1477 0.002,-15.1223 -0.0421,-15.4992l-0.0805,-0.6852zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.3982,50.4141c-0.009,0.0141 -0.0282,0.0404 -0.0337,0.0494 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.2888,0.954 -0.416,1.3085 -0.672,1.5331 0.1729,-0.0113 0.2997,-0.0229 0.3416,-0.0314 0.264,-0.0532 0.2857,-0.157 0.3714,-1.7867 0.0009,-0.0169 0.0014,-0.03 0.0023,-0.0467zM54.2873,51.1044c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#917448\"/>\n  <path\n      android:pathData=\"m36.4665,18.1208c-3.3648,-0.007 -2.9906,0.0498 -3.0945,1.439l-0.0732,0.9789 -1.1099,0.0763c-1.1213,0.0769 -1.5141,0.1995 -1.8209,0.5692 -0.1449,0.1745 -0.6724,0.203 -3.7744,0.2034 -3.3737,0.0004 -3.6334,0.0172 -4.0334,0.261 -0.2351,0.1433 -0.4847,0.4114 -0.5547,0.5955 -0.0802,0.2108 -0.1272,6.2542 -0.1272,16.3279l0.0114,15.9113 0.4324,0.5255 0.0867,0.0867c0.0337,0.0246 0.0678,0.0481 0.0992,0.0717 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0006,0.0001 0.0012,0.0004 0.0018,0.0005 0.3686,-0.001 0.8452,-0.0005 1.1803,-0.0014 0.0255,-0.0099 0.0535,-0.018 0.0864,-0.0232 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4351,0 0.477,0 0.5871,0 0.5881,0 0.1726,0c1.2896,-0.0236 2.083,-0.0619 2.5505,-0.1261 0.0229,-0.0127 0.0465,-0.0249 0.0701,-0.0329 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.041 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0039,-0.007 0.007,-0.0155 0.0101,-0.0234 0.0938,-0.901 0.1252,-5.1796 0.1252,-16.2012L54.2469,22.3068l-0.3806,-0.4262 -0.3809,-0.4262 -3.7611,-0.0384c-3.5391,-0.0362 -3.7775,-0.0538 -4.0362,-0.2967 -0.3709,-0.3484 -1.0018,-0.5132 -1.9659,-0.5132 -0.9381,0 -0.9611,-0.0298 -0.9611,-1.243 0,-0.4565 -0.0596,-0.8668 -0.1326,-0.9118 -0.0729,-0.045 -0.0989,-0.1365 -0.0576,-0.2034 0.0458,-0.074 -1.6796,-0.1216 -4.4216,-0.1216 -0.6456,0 -1.2026,-0.0028 -1.6832,-0.0028zM39.7279,18.9312c1.4892,0.0042 2.2364,0.0407 2.3073,0.1122 0.0765,0.0771 0.1389,0.5626 0.1389,1.0788l0,0.9386l1.3705,0.0704c0.7537,0.0387 1.4703,0.1175 1.5925,0.1749 0.4152,0.1949 0.5388,0.9375 0.4456,2.68 -0.059,1.1042 -0.1508,1.731 -0.2827,1.9323l-0.1953,0.298 -6.9471,-0.0014c-3.8209,-0.001 -7.0168,-0.0462 -7.1021,-0.1004 -0.3195,-0.2032 -0.4323,-0.8784 -0.4323,-2.5874 0,-1.694 0.012,-1.7714 0.3204,-2.0799 0.2995,-0.2995 0.4097,-0.3204 1.6914,-0.3204l1.3712,0l0.0377,-1.0768 0.038,-1.0768 3.9072,-0.0346c0.6637,-0.0056 1.2424,-0.0084 1.7388,-0.007zM26.5274,22.1702l0.0003,0 3.456,0l-0.039,0.7505 -0.0393,0.7505 -1.9578,0.0652 -1.9578,0.0653 -0.0331,14.194 -0.026,11.0646c0.179,-0.0404 0.3712,-0.0676 0.5769,-0.0798 -0.0835,-5.9078 -0.0853,-20.9501 0.0145,-23.8901 -0.1103,0.0113 -0.1162,-0.0429 -0.1476,-0.2327 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1064 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1327 0.0025,-0.0004 0.0046,-0.0014 0.0072,-0.0014 0.2194,-0.0335 0.4733,-0.0833 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0328,-0 0.0656,0 0.0984,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0069,0.0127 0.0115,0.027 0.0158,0.0411l0.2032,0.0042 0.0803,0.7464c0.1008,0.9378 0.3719,1.3572 0.996,1.54 0.3367,0.0988 2.6261,0.1309 7.3203,0.1032l6.8306,-0.0403 0.3806,-0.4262c0.2163,-0.2422 0.3162,-0.4172 0.3574,-0.6735 -0,-0.0099 0,-0.0201 0,-0.0301 -0,-0.0735 0,-0.147 0,-0.2205 0.009,-0.0151 0.015,-0.0265 0.0235,-0.0408 0.0005,-0.0266 0.0018,-0.051 0.0018,-0.0793 0.0003,-0.0909 0.0015,-0.1654 0.0035,-0.2399 -0.0174,0 -0.0375,0.0014 -0.0543,0.001 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0582,-0.0534 0.1121,-0.1133 0.166,-0.1731 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.017 0.0155,-0.007 0.01,-0.0056 0.0232,-0.0113 0.0901,-0.0991 0.183,-0.1939 0.3018,-0.2501 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0075,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.12,0.099 0.2107,0.1791 0.287,0.2519 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1275 0.0737,0.1914 0.08,0.1398 0.0835,0.2682 0.0851,0.4772 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0829,0.0569 -0.1664,0.1129 -0.2498,0.169 0,0.0099 0,0.0196 0,0.0293 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0984c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0428,0.5067 0.0436,0.7627l0,0.793 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0746 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0854 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0939 0.0668,0.2209 0.1566,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0176 -0.0003,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.1193,0.049 0.2339,0.1019 0.337,0.1626 0.0194,-1.6826 0.015,-4.2413 0.002,-8.188l-0.0352,-10.7026 -1.9305,-0.0362c-2.1692,-0.0405 -2.1393,-0.0252 -1.9917,-1.0095l0.088,-0.5858l3.3109,0c2.8359,0 3.3402,0.0296 3.5159,0.2052 0.1842,0.1842 0.2052,1.8179 0.2052,16.0335 0,15.1687 -0.0099,15.8377 -0.2386,16.0447 -0.2192,0.1983 -1.4553,0.2159 -15.1112,0.2159 -13.2495,0 -14.9043,-0.0221 -15.1627,-0.2032l-0.3622,0.0084 0.0723,-16.0532c0.0641,-14.2271 0.021,-15.8623 0.2052,-16.0465 0.1761,-0.1761 0.6932,-0.2052 3.6609,-0.2052zM50.8476,22.9533 L51.0635,23.2827c0.199,0.3037 0.2083,1.4584 0.1193,14.7813 -0.0177,2.65 -0.0396,4.954 -0.0627,7.0473 0.1202,0.1081 0.2379,0.2192 0.3487,0.3375 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0303 -0.0199,0.0604 -0.0227,0.0883 -0.0349,0.2433 -0.0965,0.5169 -0.1935,0.7755 -0.0335,2.0017 -0.0675,3.2399 -0.0974,3.2964 -0.066,0.1247 -0.2505,0.2534 -0.4099,0.2858 -0.1594,0.0324 -5.819,0.0945 -12.5768,0.1382l-12.2869,0.0795 -0.3735,-0.3212 -0.3735,-0.3209 -0.0031,-0.4558c-0.9352,-0.8011 -0.8856,-1.9594 -0.0186,-2.6338l0.5506,-2.24c-0.023,-3.3004 -0.0187,-7.1089 -0.0974,-7.1089 -2.0809,3.4784 0,0 -2.0809,3.4784l-0.4117,2.1133c-0.0584,1.124 0.1944,3.2224 0.1909,4.9623l0.3691,3.1636 4.5355,0.0365l0,-0.0003c2.4946,0.0201 9.0874,0.0355 14.6508,0.0345l10.1152,-0.0014 0.0382,-14.8139c0.0211,-8.1476 0.0024,-15.1223 -0.0418,-15.4992l-0.0806,-0.6852l-0.9073,0zM47.0569,23.8557c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8906c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0642c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.3982,50.4141c-0.009,0.0141 -0.0282,0.0404 -0.0337,0.0494 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.2888,0.954 -0.416,1.3085 -0.672,1.5331 0.1729,-0.0113 0.2997,-0.0229 0.3416,-0.0314 0.264,-0.0532 0.2857,-0.157 0.3714,-1.7867 0.0009,-0.0169 0.0014,-0.03 0.0023,-0.0467zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#a87421\"/>\n  <path\n      android:pathData=\"m36.4665,18.1208c-3.3648,-0.007 -2.9906,0.0498 -3.0945,1.439l-0.0732,0.9789 -1.1099,0.0763c-1.1213,0.0769 -1.5141,0.1995 -1.8209,0.5692 -0.1449,0.1745 -0.6724,0.203 -3.7744,0.2034 -3.3737,0.0004 -3.6334,0.0172 -4.0334,0.261 -0.2351,0.1433 -0.4847,0.4114 -0.5547,0.5955 -0.0802,0.2108 -0.1272,6.2542 -0.1272,16.3279l0.0217,15.9599 0.4221,0.4769 0.0867,0.0867c0.0337,0.0246 0.0678,0.0481 0.0992,0.0717 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0268,0.0028 0.0537,0.007 0.0803,0.0113 0.0006,0.0001 0.0012,0.0004 0.0018,0.0005 0.3686,-0.001 0.8452,-0.0005 1.1803,-0.0014 0.0255,-0.0099 0.0535,-0.018 0.0864,-0.0232 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4351,0 0.477,0 0.5871,0 0.5881,0 0.1726,0c1.2896,-0.0236 2.083,-0.0619 2.5505,-0.1261 0.0229,-0.0127 0.0465,-0.0249 0.0701,-0.0329 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.041 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0039,-0.007 0.007,-0.0155 0.0101,-0.0234 0.0938,-0.901 0.1252,-5.1796 0.1252,-16.2012L54.2469,22.3068l-0.3806,-0.4262 -0.3809,-0.4262 -3.7611,-0.0384c-3.5391,-0.0362 -3.7775,-0.0538 -4.0362,-0.2967 -0.3709,-0.3484 -1.0018,-0.5132 -1.9659,-0.5132 -0.9381,0 -0.9611,-0.0298 -0.9611,-1.243 0,-0.4565 -0.0596,-0.8668 -0.1326,-0.9118 -0.0729,-0.045 -0.0989,-0.1365 -0.0576,-0.2034 0.0458,-0.074 -1.6796,-0.1216 -4.4216,-0.1216 -0.6456,0 -1.2026,-0.0028 -1.6832,-0.0028zM39.7279,18.9312c1.4892,0.0042 2.2364,0.0407 2.3073,0.1122 0.0765,0.0771 0.1389,0.5626 0.1389,1.0788l0,0.9386l1.3705,0.0704c0.7537,0.0387 1.4703,0.1175 1.5925,0.1749 0.4152,0.1949 0.5388,0.9375 0.4456,2.68 -0.059,1.1042 -0.1508,1.731 -0.2827,1.9323l-0.1953,0.298 -6.9471,-0.0014c-3.8209,-0.001 -7.0168,-0.0462 -7.1021,-0.1004 -0.3195,-0.2032 -0.4323,-0.8784 -0.4323,-2.5874 0,-1.694 0.012,-1.7714 0.3204,-2.0799 0.2995,-0.2995 0.4097,-0.3204 1.6914,-0.3204l1.3712,0l0.0377,-1.0768 0.038,-1.0768 3.9072,-0.0346c0.6637,-0.0056 1.2424,-0.0084 1.7388,-0.007zM26.4789,22.1702l0.0003,0 3.5044,0l-0.039,0.7505 -0.0393,0.7505 -1.9578,0.0652 -1.9578,0.0653 -0.0331,14.194 -0.026,11.0646c0.179,-0.0404 0.3712,-0.0676 0.5769,-0.0798 -0.0835,-5.9078 -0.0853,-20.9501 0.0145,-23.8901 -0.1103,0.0113 -0.1162,-0.0429 -0.1476,-0.2327 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.0021,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1064 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1327 0.0025,-0.0004 0.0046,-0.0014 0.0072,-0.0014 0.2194,-0.0335 0.4733,-0.0833 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0328,-0 0.0656,0 0.0984,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0069,0.0127 0.0115,0.027 0.0158,0.0411l0.2032,0.0042 0.0803,0.7464c0.1008,0.9378 0.3719,1.3572 0.996,1.54 0.3367,0.0988 2.6261,0.1309 7.3203,0.1032l6.8306,-0.0403 0.3806,-0.4262c0.2163,-0.2422 0.3162,-0.4172 0.3574,-0.6735 -0,-0.0099 0,-0.0201 0,-0.0301 -0,-0.0735 0,-0.147 0,-0.2205 0.009,-0.0151 0.015,-0.0265 0.0235,-0.0408 0.0005,-0.0266 0.0018,-0.051 0.0018,-0.0793 0.0003,-0.0909 0.0015,-0.1654 0.0035,-0.2399 -0.0174,0 -0.0375,0.0014 -0.0543,0.001 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0582,-0.0534 0.1121,-0.1133 0.166,-0.1731 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.017 0.0155,-0.007 0.01,-0.0056 0.0232,-0.0113 0.0901,-0.0991 0.183,-0.1939 0.3018,-0.2501 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0075,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.12,0.099 0.2107,0.1791 0.287,0.2519 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1275 0.0737,0.1914 0.08,0.1398 0.0835,0.2682 0.0851,0.4772 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0829,0.0569 -0.1664,0.1129 -0.2498,0.169 0,0.0099 0,0.0196 0,0.0293 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0984c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1335 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0428,0.5067 0.0436,0.7627l0,0.793 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0746 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0854 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0939 0.0668,0.2209 0.1566,0.4342 0.2348,0.6513 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0176 -0.0003,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.1193,0.049 0.2339,0.1019 0.337,0.1626 0.0194,-1.6826 0.015,-4.2413 0.002,-8.188l-0.0352,-10.7026 -1.9578,-0.0653 -1.9578,-0.0653l0,-0.7179 0,-0.7179l3.4549,0 3.4549,0l0.166,0.3263c0.1319,0.2596 0.1595,3.5348 0.1344,16.0317 -0.0309,15.3771 -0.037,15.7093 -0.285,15.8907 -0.2153,0.1574 -2.5017,0.1853 -15.2287,0.1853 -13.44,0 -14.9964,-0.0211 -15.1805,-0.2052 -0.1841,-0.1843 -0.205,-1.8233 -0.205,-16.0929 0,-12.1833 0.0364,-15.9246 0.1565,-16.0447 0.114,-0.114 1.1095,-0.1565 3.6612,-0.1565zM52.5298,23.0708 L51.9006,23.1101 51.2715,23.1491 51.1802,37.8325c-0.0172,2.7659 -0.0386,5.1364 -0.0612,7.278 0.1206,0.1084 0.2386,0.2196 0.3498,0.3383 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0428,0.0967 -0.0902,0.1913 -0.1428,0.2837 0.0874,0.1568 0.177,0.3116 0.2745,0.4596 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0348,0.2422 -0.096,0.5144 -0.1922,0.7719 -0.0329,2.0125 -0.0666,3.2425 -0.0974,3.2999 -0.0668,0.1249 -0.2517,0.2534 -0.4112,0.2858 -0.1594,0.0324 -5.819,0.0945 -12.5768,0.1381l-12.2869,0.0795 -0.3684,-0.3166c-0.2243,-0.1929 -0.3121,-0.2788 -0.3679,-0.7701 -0.9034,-0.7629 -0.9042,-1.8551 -0.1509,-2.5431 -0.0401,-0.5902 -0.0822,-1.0208 -0.1099,-1.0485 -0.042,-0.0419 -0.0765,0.0842 -0.0765,0.2804 0,0.3701 -0.6072,1.2964 -0.7798,1.1897 -0.1385,-0.0856 -0.2616,1.0894 -0.2631,2.5071l-0.001,1.2042 2.1209,0.0396 -0.0003,0.0003c1.1665,0.0218 7.73,0.0376 14.5855,0.0356l12.4646,-0.0042 -0.0263,-14.6834c-0.0144,-8.0759 -0.0631,-14.9946 -0.1081,-15.375zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.3982,50.4141c-0.009,0.0141 -0.0282,0.0404 -0.0337,0.0494 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.2888,0.954 -0.416,1.3085 -0.672,1.5331 0.1729,-0.0113 0.2997,-0.0229 0.3416,-0.0314 0.264,-0.0532 0.2857,-0.157 0.3714,-1.7867 0.0009,-0.0169 0.0014,-0.03 0.0023,-0.0467zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0087,0.007 0.0248,0.0184 0.0324,0.0239 -0.0227,-0.0201 -0.0304,-0.0273 -0.0326,-0.0289z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#876d42\"/>\n  <path\n      android:pathData=\"m36.4665,18.1208c-3.3648,-0.007 -2.9906,0.0498 -3.0945,1.439l-0.0732,0.9789 -0.6526,0.0321c-1.2591,0.0618 -1.7613,0.1798 -2.1487,0.5058l-0.3877,0.3265 -3.4305,-0.0028c-3.8861,-0.0028 -4.2139,0.0524 -4.5723,0.7696 -0.2135,0.4274 -0.2282,1.4777 -0.2284,16.2889 -0.0003,13.6169 -0.0269,15.9321 0.3477,16.5102 0.0835,0.0512 0.1899,0.1292 0.2817,0.1978 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0614 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0266,0.0028 0.0531,0.0056 0.0795,0.0084l7.1538,-0.0127c0.0564,-0.0113 0.113,-0.0208 0.169,-0.0334 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0025,-0.0042 0.0049,-0.0084 0.0073,-0.0127l0.0336,-15.8252c0.0307,-14.4117 0.0131,-16.0953 -0.1718,-16.4528 -0.3713,-0.7179 -0.7305,-0.778 -4.5814,-0.765 -3.3458,0.0113 -3.4802,0.0014 -3.7596,-0.2605 -0.3852,-0.3619 -1.0057,-0.5269 -1.9807,-0.5269 -0.9381,0 -0.9611,-0.0298 -0.9611,-1.243 0,-0.4565 -0.0596,-0.8668 -0.1326,-0.9118 -0.0729,-0.045 -0.0989,-0.1365 -0.0576,-0.2034 0.0458,-0.074 -1.6796,-0.1216 -4.4216,-0.1216 -0.6456,0 -1.2026,-0.0028 -1.6832,-0.0028zM39.7279,18.9312c1.4892,0.0042 2.2364,0.0407 2.3073,0.1122 0.0765,0.0771 0.1389,0.5626 0.1389,1.0788l0,0.9386l1.3705,0.0704c0.7537,0.0387 1.4703,0.1175 1.5925,0.1749 0.4152,0.1949 0.5388,0.9375 0.4456,2.68 -0.059,1.1042 -0.1508,1.731 -0.2827,1.9323l-0.1953,0.298 -6.9471,-0.0014c-3.8209,-0.001 -7.0168,-0.0462 -7.1021,-0.1004 -0.3195,-0.2032 -0.4323,-0.8784 -0.4323,-2.5874 0,-1.694 0.012,-1.7714 0.3204,-2.0799 0.2995,-0.2995 0.4097,-0.3204 1.6914,-0.3204l1.3712,0l0.0377,-1.0768 0.038,-1.0768 3.9072,-0.0346c0.6637,-0.0056 1.2424,-0.0084 1.7388,-0.007zM46.2345,22.0863 L46.2351,22.0866c0.0062,0.0028 0.0128,0.0084 0.0199,0.0183 0.0597,0.0812 1.3985,0.1305 3.5508,0.1305l3.4549,0l0.166,0.3263c0.1319,0.2596 0.1595,3.5348 0.1343,16.0317 -0.0309,15.3771 -0.0369,15.7094 -0.285,15.8907 -0.2153,0.1574 -2.5017,0.1853 -15.2287,0.1853 -13.44,0 -14.9964,-0.0211 -15.1805,-0.2052 -0.1842,-0.1842 -0.2052,-1.8243 -0.2052,-16.1031 0,-12.9829 0.0316,-15.9242 0.1718,-16.0406 0.2711,-0.2249 7.1921,-0.2212 7.2362,0.0042 0.0185,0.0946 -0.0124,0.4361 -0.0686,0.7591l-0.1022,0.5873 -1.9545,0.0653 -1.9545,0.0653 -0.0331,14.194 -0.026,11.0648c0.179,-0.0404 0.3712,-0.0676 0.5769,-0.0798 -0.0835,-5.9075 -0.0853,-20.9494 0.0145,-23.8901 -0.1103,0.0113 -0.1162,-0.0429 -0.1476,-0.2327 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1863,-0.0127 0.3308,0.0412 0.4132,0.1326 0.0025,-0.0004 0.0048,-0.0014 0.0073,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0478,0.0277 0.0875,0.0534 0.1269,0.079 0.0326,-0 0.0653,0 0.0979,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,-0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,-0 0.0489,0l0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0069,0.0127 0.0114,0.0269 0.0156,0.0408l0.2034,0.0042 0.0803,0.7464c0.1008,0.9378 0.3719,1.3572 0.996,1.54 0.3367,0.0988 2.6261,0.1309 7.3203,0.1032l6.8306,-0.04 0.3806,-0.4262c0.2163,-0.2422 0.3162,-0.4172 0.3574,-0.6735 -0,-0.0099 0,-0.0201 0,-0.0301 -0,-0.0735 0,-0.147 0,-0.2205 0.009,-0.0151 0.015,-0.0266 0.0235,-0.0411 0.0005,-0.0265 0.002,-0.0508 0.002,-0.0793 0.0003,-0.0909 0.0013,-0.1654 0.0032,-0.2399 -0.0174,0 -0.0375,0.0014 -0.0543,0.001 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0583,-0.0534 0.112,-0.1135 0.166,-0.1733 0.0008,-0.0056 0.0021,-0.0099 0.0028,-0.0166 0.0153,-0.007 0.0099,-0.0056 0.0229,-0.0113 0.0902,-0.0992 0.1831,-0.1943 0.3021,-0.2506 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0075,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.12,0.099 0.2107,0.1791 0.287,0.2519 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0642 0.0494,0.1275 0.0737,0.1914 0.08,0.1398 0.0835,0.2682 0.0851,0.4772 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0248,0.1846 0.0148,0.2654 -0.0063,0.0505 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.0829,0.0569 -0.1664,0.1129 -0.2498,0.169 0,0.0099 0,0.0196 0,0.0293 -0,0.1305 0,0.261 0,0.3916l0,0.3317 0,0.3237 0,0.6429 0,0.0984c0.0082,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1336 -0.0107,0.2708 0.002,0.4046 0.0015,0.151 0.0034,0.3018 -0.0031,0.4527 -0.0003,0.007 -0.0008,0.0056 -0.0008,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1978 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1198 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0686 0,0.1371 0,0.2057 0.1193,0.049 0.2339,0.1019 0.337,0.1626 0.0194,-1.6826 0.0147,-4.2416 0.0018,-8.1883l-0.0349,-10.7026 -1.9568,-0.0653 -1.9568,-0.0653 -0.0834,-0.3916c-0.1113,-0.5222 -0.1267,-1.2337 -0.0344,-1.193zM52.5298,23.0708 L51.8836,23.1101 51.2373,23.1491 51.2427,37.8486c0.0012,3.1629 0.0009,5.4198 -0.0015,7.3726 0.0778,0.0738 0.1542,0.1492 0.2276,0.2276 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0256,0.0579 -0.0536,0.1147 -0.0826,0.1711 -0.0001,0.076 -0.0004,0.1424 -0.0005,0.2167 0.0689,0.1209 0.1391,0.2406 0.2149,0.3556 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.007,0.049 -0.0163,0.1002 -0.0255,0.1514 -0.0221,3.621 -0.0817,3.8474 -0.2343,4.0359l-0.2511,0.3102 -12.5319,0.0345 -12.5319,0.0346 -0.3633,-0.3123c-0.2198,-0.189 0.5009,-0.1965 0.4453,-0.6845 0.019,-2.3549 -0.7307,-5.3573 0.0228,-6.0454 -0.4383,-0.1026 -2.759,2.8533 -3.148,2.9843 0.1482,0.0541 0.9381,2.2113 0.9369,3.3654l-0.001,1.2042 2.1209,0.0396 -0.0003,0.0003c1.1665,0.0218 7.73,0.0376 14.5855,0.0356l12.4646,-0.0042 -0.0263,-14.6834c-0.0145,-8.0757 -0.0631,-14.9947 -0.1081,-15.3751zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.3982,50.4141c-0.009,0.0141 -0.0282,0.0404 -0.0337,0.0494 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.2888,0.954 -0.416,1.3085 -0.672,1.5331 0.1729,-0.0113 0.2997,-0.0229 0.3416,-0.0314 0.264,-0.0532 0.2857,-0.157 0.3714,-1.7867 0.0009,-0.0169 0.0014,-0.03 0.0023,-0.0467zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0018,0.0014 0.0046,0.0028 0.0072,0.0056 0.0032,0.0014 0.0062,0.0028 0.0094,0.0056 -0.0053,-0.0056 -0.0157,-0.0141 -0.0168,-0.0148z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#a46f1e\"/>\n  <path\n      android:pathData=\"m36.4665,18.1208c-3.3648,-0.007 -2.9906,0.0498 -3.0945,1.439l-0.0732,0.9789 -0.6526,0.0321c-1.2591,0.0618 -1.7613,0.1798 -2.1487,0.5058l-0.3877,0.3265 -3.4305,-0.0028c-3.8861,-0.0028 -4.2139,0.0524 -4.5723,0.7696 -0.2135,0.4274 -0.2282,1.4777 -0.2284,16.2889 -0.0003,13.6169 -0.0269,15.9321 0.3477,16.5102 0.0835,0.0512 0.1899,0.1292 0.2817,0.1978 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0614 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0266,0.0028 0.0531,0.0056 0.0795,0.0084l7.1538,-0.0127c0.0564,-0.0113 0.113,-0.0208 0.169,-0.0334 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0025,-0.0042 0.0049,-0.0084 0.0073,-0.0127l0.0336,-15.8252c0.0307,-14.4117 0.0131,-16.0953 -0.1718,-16.4528 -0.3713,-0.7179 -0.7305,-0.778 -4.5814,-0.765 -3.3458,0.0113 -3.4802,0.0014 -3.7596,-0.2605 -0.3852,-0.3619 -1.0057,-0.5269 -1.9807,-0.5269 -0.9381,0 -0.9611,-0.0298 -0.9611,-1.243 0,-0.4565 -0.0596,-0.8668 -0.1326,-0.9118 -0.0729,-0.045 -0.0989,-0.1365 -0.0576,-0.2034 0.0458,-0.074 -1.6796,-0.1216 -4.4216,-0.1216 -0.6456,0 -1.2026,-0.0028 -1.6832,-0.0028zM39.7279,18.9312c1.4892,0.0042 2.2364,0.0407 2.3073,0.1122 0.0765,0.0771 0.1389,0.5626 0.1389,1.0788l0,0.9386l1.3705,0.0704c0.7537,0.0387 1.4703,0.1175 1.5925,0.1749 0.4152,0.1949 0.5388,0.9375 0.4456,2.68 -0.059,1.1042 -0.1508,1.731 -0.2827,1.9323l-0.1953,0.298 -6.9471,-0.0014c-3.8209,-0.001 -7.0168,-0.0462 -7.1021,-0.1004 -0.3195,-0.2032 -0.4323,-0.8784 -0.4323,-2.5874 0,-1.694 0.012,-1.7714 0.3204,-2.0799 0.2995,-0.2995 0.4097,-0.3204 1.6914,-0.3204l1.3712,0l0.0377,-1.0768 0.038,-1.0768 3.9072,-0.0346c0.6637,-0.0056 1.2424,-0.0084 1.7388,-0.007zM23.1792,22.0323 L26.5822,22.0435c2.6439,0.0099 3.4171,0.0493 3.4672,0.1797 0.0354,0.0923 0.0163,0.4557 -0.0426,0.8076l-0.1071,0.6398 -1.955,0.0653 -1.955,0.0653 -0.0331,14.194 -0.026,11.0646c0.179,-0.0404 0.3712,-0.0676 0.5769,-0.0798 -0.0835,-5.9077 -0.0856,-20.9501 0.0143,-23.8901 -0.1101,0.0113 -0.1159,-0.0431 -0.1473,-0.2327 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1143 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329 0,0 0.0003,-0.0003 0.0003,-0.0003 0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.1474,-0.0708 0.284,-0.1066 0.404,-0.1147 0.1865,-0.0127 0.3311,0.0414 0.4135,0.1327 0.0025,-0.0004 0.0046,-0.0014 0.0072,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0787 0.0328,-0 0.0656,0 0.0984,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,0 0.0489,0l0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.161,0.0418 0.2092,0.0593 0.3401,0.3064 0.0069,0.0127 0.0115,0.027 0.0158,0.0411l0.2032,0.0042 0.08,0.7464c0.1008,0.9378 0.3719,1.3572 0.996,1.54 0.3367,0.0988 2.6264,0.1309 7.3206,0.1032l6.8303,-0.0403 0.3808,-0.4262c0.2168,-0.2426 0.3164,-0.4179 0.3574,-0.675 -0,-0.0099 0,-0.019 0,-0.0286 -0,-0.0735 0,-0.147 0,-0.2205 0.009,-0.0149 0.0148,-0.0262 0.0232,-0.0405 0.0005,-0.0266 0.002,-0.0511 0.002,-0.0795 0.0003,-0.0909 0.0013,-0.1654 0.0032,-0.2399 -0.0173,0 -0.0373,0.0014 -0.054,0.001 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.2202,-0.0512 -0.2464,-0.1977 0.1244,-0.5374 0.0582,-0.0534 0.1121,-0.1133 0.166,-0.1731 0.0008,-0.0056 0.0021,-0.0113 0.0028,-0.017 0.0155,-0.007 0.01,-0.0056 0.0232,-0.0113 0.0901,-0.099 0.183,-0.1939 0.3018,-0.2501 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0427 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.1188,-0.0667 0.2411,-0.1412 0.3288,-0.1458 0.0371,-0.0014 0.0677,0.0099 0.0897,0.0383 0.0567,-0.0224 0.0804,0.0028 0.22,0.105 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.0981,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.2426,-0.022 0.5252,-0.1153 0.7441,0.0653 0.12,0.099 0.2107,0.1791 0.287,0.2519 0.0404,0.0201 0.0808,0.0407 0.1504,0.0724 0.023,0.0643 0.0494,0.1275 0.0737,0.1914 0.08,0.1398 0.0835,0.2682 0.0851,0.4772 0.0001,0.0191 0.0018,0.2104 -0.0031,0.2345 0.0124,0.0885 0.0251,0.1846 0.015,0.2654 -0.0062,0.0504 -0.0153,0.1007 -0.0235,0.1509 0.0474,0.1005 0.1093,0.2262 0.2116,0.4153 -0.1011,0.0693 0.0242,0.1551 -0.0775,0.2236l0.1004,0.2021c0.02,0.0757 -0.1907,0.1557 -0.273,0.2348l0,0.261 0,0.3237 0,0.6429 0,0.0984c0.0083,0.0282 0.0165,0.0564 0.0247,0.0846 0.0053,0.0704 -0.0073,-0.085 0.0291,0.1417 0.0165,0.1032 0.0317,0.2067 0.0421,0.3107 0.0132,0.1336 -0.011,0.2708 0.0018,0.4046 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0113 0.002,0.0084 0.0039,0.0127 0.0059,0.0217 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0062,0.0339 -0.0038,0.0456 -0.0015,0.0014 -0.0025,0.001 -0.0031,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.0174,-0.0339 -0.0771,-0.1304 -0.066,-0.0938 0.0668,0.2209 0.1565,0.4342 0.2348,0.6513 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0041,0.19 -0.0028,0.2842 -0.0004,0.0056 -0,-0.0172 -0.0015,-0.0166 0.0039,0.0964 0.0244,0.205 0.0155,0.2998 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 -0,0.137 0,0.2055 0.1193,0.049 0.2341,0.1019 0.3373,0.1626 0.0194,-1.6826 0.0147,-4.2414 0.0018,-8.188l-0.0352,-10.7026 -1.9565,-0.0653 -1.9568,-0.0652 -0.0836,-0.3916c-0.1303,-0.6116 -0.1292,-1.3349 0.0018,-1.128 0.0782,0.1235 0.1392,0.1333 0.2014,0.0327 0.0613,-0.0992 1.0898,-0.1445 3.2852,-0.1445l3.1962,0l0.3202,0.3204 0.3204,0.3204l0,15.7044c0,14.0025 -0.022,15.7358 -0.2032,15.9945l-0.2032,0.2901l-15.1164,0c-13.9018,0 -15.1313,-0.0172 -15.296,-0.2157 -0.1492,-0.1798 -0.1849,-2.8498 -0.2134,-15.9932l-0.0342,-15.777 0.3255,-0.3258zM51.2779,23.2144c-0.0538,-0 -0.0823,6.6 -0.0635,14.6669 0.0073,3.1444 0.0115,5.3811 0.013,7.3262 0.0828,0.078 0.1635,0.1582 0.2414,0.2414 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.029,0.0655 -0.0609,0.1296 -0.0943,0.1932 0,0.0593 -0.0003,0.1164 -0.0003,0.1746 0.0724,0.1278 0.1464,0.2542 0.2264,0.3755 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0303 -0.0199,0.0604 -0.0227,0.0883 -0.0089,0.0622 -0.0197,0.1266 -0.0321,0.192 -0.0152,3.5861 -0.0749,3.807 -0.2274,3.9954l-0.2511,0.3102 -12.5319,0.0346 -12.5319,0.0345 -0.3686,-0.3166c-0.2328,-0.2001 -0.319,-0.3007 -0.3712,-0.7722 -0.368,-0.3151 -0.5837,-0.6852 -0.6569,-1.06l-0.1422,0.1509c-0.4351,0.4613 -0.4601,0.538 -0.481,1.4688l-0.0222,0.9819 3.1325,0.0691l0,0.0003c1.7229,0.038 8.2717,0.0673 14.5529,0.0653l11.3404,-0.0273 0.0551,-12.7677c-0.0137,-7.035 -0.0642,-13.9215 -0.1122,-15.3034l-0.0872,-2.5125l-0.3954,0c-0.2236,0 -0.471,0.0992 -0.569,0.2284 -0.1651,0.2175 -0.1737,0.2175 -0.1777,0 -0.0025,-0.1256 -0.0481,-0.2284 -0.102,-0.2284zM47.0569,23.8558c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8567c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.8907c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM47.2708,23.9184c-0.03,0 -0.0556,0.0014 -0.0739,0.0042 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042l0.0003,-0.0005c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0365,-0.0014 -0.0709,-0.0028 -0.1009,-0.0028zM47.3962,23.9394c0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179l0,-0.0003c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141zM47.3937,24.0643c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.102c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.7736,36.5158c0.0103,0.0405 0.0158,0.0588 0.0331,0.1208 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.003,-0.0099 -0.0161,-0.0394 -0.0135,-0.029zM49.933,37.4529l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8206c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.3982,50.4141c-0.009,0.0141 -0.0282,0.0404 -0.0337,0.0494 0.0158,0.0954 0.0182,0.19 -0.0097,0.2822 -0.2888,0.954 -0.416,1.3085 -0.672,1.5331 0.1729,-0.0113 0.2997,-0.0229 0.3416,-0.0314 0.264,-0.0532 0.2857,-0.157 0.3714,-1.7867 0.0009,-0.0169 0.0014,-0.03 0.0023,-0.0467zM22.5892,55.2216c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0018,0.0014 0.0046,0.0028 0.0072,0.0056 0.0032,0.0014 0.0062,0.0028 0.0094,0.0056 -0.0053,-0.0056 -0.0157,-0.0141 -0.0168,-0.0148z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#666665\"/>\n  <path\n      android:pathData=\"m38.153,18.1343c-2.7736,-0.0056 -4.5141,0.037 -4.4682,0.1112 0.0413,0.0669 0.0153,0.1586 -0.0576,0.2037 -0.0729,0.045 -0.1326,0.4853 -0.1326,0.9784 0,1.2027 -0.1143,1.307 -1.4349,1.307 -0.909,0 -1.0696,0.0377 -1.4217,0.334l-0.3969,0.3339 -3.4962,-0.0014c-3.9566,-0.0028 -4.2788,0.0512 -4.6378,0.7699 -0.2135,0.4274 -0.2282,1.4777 -0.2284,16.2889 -0.0003,13.6169 -0.0269,15.9321 0.3477,16.5102 0.0835,0.0512 0.1899,0.1292 0.2817,0.1978 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0245 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0236 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0614 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0266,0.0028 0.0531,0.0056 0.0795,0.0084l7.1538,-0.0127c0.0564,-0.0113 0.113,-0.0208 0.169,-0.0334 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0025,-0.0042 0.0049,-0.0084 0.0073,-0.0127l0.0336,-15.8255c0.0307,-14.4117 0.0131,-16.095 -0.1718,-16.4526 -0.3718,-0.7191 -0.7251,-0.7776 -4.635,-0.7653l-3.5248,0.0113 -0.3969,-0.334c-0.3508,-0.2952 -0.5139,-0.3339 -1.4049,-0.3339 -0.61,0 -1.0699,-0.0619 -1.1647,-0.1568 -0.0908,-0.0908 -0.1565,-0.5257 -0.1565,-1.0347 0,-0.6043 -0.0609,-0.9391 -0.1948,-1.073 -0.1475,-0.1475 -0.1552,-0.2104 -0.0324,-0.26 0.0891,-0.036 -1.8825,-0.07 -4.3813,-0.0757zM38.0727,18.7762 L38.073,18.7767c3.3546,0 3.896,0.0275 4.0428,0.2042 0.1091,0.1315 0.1609,0.5264 0.1453,1.1094l-0.0245,0.9052l1.1675,0c1.2357,0 1.7736,0.1296 2.0508,0.4946 0.1215,0.1599 0.1709,0.7867 0.1739,2.2066 0.001,0.4767 0.0012,0.8391 -0.0038,1.1211 0.0422,-0.0747 0.1192,-0.1665 0.2455,-0.2822 0.0579,-0.0529 0.1114,-0.1126 0.1649,-0.1721 0.0009,-0.007 0.0023,-0.0113 0.0031,-0.0183 0.0168,-0.0084 0.0111,-0.0056 0.0252,-0.0127 0.0898,-0.0985 0.1822,-0.1931 0.3006,-0.2491 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0003,-0.0001 0.0008,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.0695,-0.039 0.1394,-0.079 0.2037,-0.1078 -0.2694,-0.0215 -0.4609,-0.048 -0.5152,-0.077 -0.1877,-0.1005 -0.2409,-0.2931 -0.2496,-0.9057l-0.011,-0.778 3.4521,-0.0113c3.3021,-0.0113 3.4635,-0.0001 3.7167,0.2531 0.2632,0.263 0.2646,0.3654 0.2646,16.1026l0,15.8377l-0.2748,0.2488c-0.2664,0.241 -0.7258,0.2493 -15.1074,0.2738 -8.3921,0.0142 -14.9567,-0.0251 -15.1181,-0.0905 -0.1568,-0.0636 -0.3477,-0.2326 -0.4242,-0.3755 -0.0961,-0.1798 -0.1392,-5.0937 -0.1392,-15.9366l0,-15.6769l0.3258,-0.3258 0.3258,-0.3258 3.4266,0.038 3.4266,0.038 0.0303,0.777c0.0195,0.5025 -0.0265,0.8129 -0.1305,0.879 -0.0884,0.0562 -0.998,0.103 -2.0213,0.104l-0.3678,0.0005c0.0636,0.0234 0.116,0.0576 0.1545,0.1002 0.0025,-0.0004 0.0048,-0.0014 0.0073,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0787 0.0327,-0 0.0654,0 0.0981,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,0 0.0489,0l0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.1373,0.0356 0.1951,0.0625 0.2896,0.2203 0.1588,0.0245 0.2715,0.0552 0.309,0.0925 0.0862,0.0862 0.1565,0.4723 0.1565,0.8583 0,0.6016 0.0545,0.7563 0.3816,1.0834l0.3814,0.3814l6.9994,0c7.7135,0 7.6034,0.0113 7.8857,-0.7987 0.0737,-0.2115 0.1328,-0.5755 0.1415,-0.8463 -0.0145,-0 -0.0315,0.0014 -0.0456,0.001 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.1321,-0.0519 -0.0566,-0.0127 -0.1002,-0.0328 -0.1277,-0.0601 -0.0227,0.6617 -0.0924,0.7917 -0.3,1.0332l-0.3151,0.3661 -6.7003,0.022c-3.6853,0.0127 -6.8474,-0.0191 -7.0269,-0.0691 -0.6081,-0.1695 -0.7826,-0.7901 -0.7829,-2.7842 -0.0001,-1.7009 0.015,-1.8051 0.3146,-2.1536 0.3024,-0.3518 0.3691,-0.3682 1.6968,-0.4109l1.3819,-0.0446l0,-0.9409c0,-0.5578 0.0637,-1.0048 0.1565,-1.0977 0.1148,-0.1147 1.1916,-0.1565 4.0303,-0.1565zM51.5251,22.4453 L51.0061,22.6298c-0.4802,0.0113 0.0879,0.4794 0.2526,0.7994 -0.012,0.1236 -0.0245,0.2329 -0.0372,0.3153 0.0173,-0.036 0.0393,-0.0733 0.0632,-0.1116l-0.0025,-0.1496c0.0217,0.0579 0.0275,0.1097 0.0025,0.1496l0.0515,3.148c-0.0238,-0.0552 -0.0531,5.4372 -0.0663,12.5054 -0.0048,2.5478 -0.0096,4.2818 -0.015,5.9486 0.0729,0.0695 0.1448,0.1402 0.2139,0.2139 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0222,0.0503 -0.0458,0.0998 -0.0706,0.1489 -0.0003,0.0679 -0.0005,0.1932 -0.0008,0.2595 0.0652,0.1139 0.1317,0.2265 0.2032,0.335 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0225 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0303 -0.0199,0.0604 -0.0227,0.0883 -0.0051,0.0355 -0.0121,0.0729 -0.0184,0.1097 -0.0287,3.5893 -0.0872,3.8874 -0.2429,4.0797l-0.2493,0.3082 -12.5069,0.0346 -12.5067,0.0345 -0.3936,-0.2835 -0.3936,-0.2837 0.3164,0.3469c0.1842,0.202 0.4569,0.348 0.6526,0.349 0.265,0.0014 -0.0641,0.3229 -0.2185,0.4227 -0.2362,0.1527 -0.139,0.1961 5.568,0.31 2.4048,0.048 8.2165,-0.2513 12.9005,-0.2376l8.5164,0.0249 -0.0003,-10.6047c-0.0001,-5.8326 0.4325,-11.7944 0.3852,-13.359l0.0176,-7.1748c-0.0562,-0.0028 -1.5063,-0.3041 -1.5583,-0.3026zM51.2215,23.7446c-0.1418,0.2956 -0.0737,0.4755 0,0zM47.2494,23.8358l0,0.0003c0.0001,0 0.0004,-0 0.0005,0 -0.0001,-0.0001 -0.0004,-0.0001 -0.0005,-0.0003zM47.2499,23.8361c0.029,0.0177 0.0615,0.0415 0.1219,0.0856 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.0981,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.0802,-0.007 0.1649,-0.022 0.2501,-0.0324l-0.2088,0c-0.4314,0 -0.8377,-0.0127 -1.1805,-0.0311zM47.4378,23.9256c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0828,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9397c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8561c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8571c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9688,23.891c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM27.1,23.8664 L26.0543,23.8674 26.0874,37.9961 26.1132,49.0258c0.125,-0.021 0.2553,-0.0365 0.3921,-0.0446l-0.0268,-10.8734c-0.0189,-7.6708 -0.0046,-11.5893 0.0533,-13.0185 -0.119,0.0146 -0.125,-0.0365 -0.1573,-0.2312 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329l0.0003,-0.0003c0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.0751,-0.036 0.1474,-0.0626 0.2162,-0.0816zM48.912,23.8674c0.0776,0.0141 0.1526,0.0412 0.221,0.0977 0.1194,0.0984 0.2124,0.18 0.2883,0.2524 0.0402,0.02 0.0802,0.0401 0.1491,0.0717 0.0124,0.0345 0.0262,0.0686 0.0398,0.1028 0.0184,0.0084 0.0341,0.0155 0.0426,0.0239 0.0156,0.0156 0.0299,0.0763 0.0428,0.1828 0.0304,0.1011 0.0325,0.2092 0.0337,0.3594 0,0.0084 0.0003,0.0576 0.0003,0.1063 0.003,0.0628 0.0058,0.1574 0.0087,0.2304 0.0045,0.0442 0.0072,0.0877 0.0051,0.1288 0.003,0.088 0.0066,0.1555 0.0094,0.257 0.0433,0.0894 0.0954,0.1934 0.1767,0.3436 -0.0553,0.0379 -0.1109,0.0752 -0.1665,0.1126 0.017,0.7952 0.0286,1.9117 0.0359,3.421 0.0315,0.2133 0.023,0.4276 0.0237,0.6434l0,0.793 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0005,0.001 -0.0008,0.0001 -0.0013,0 0,0.0014 0.0003,0.0028 0.0003,0.0056 0.0104,0.0374 0.0396,0.1444 0.0171,0.0622 0.0104,0.0376 0.0244,0.0746 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.0174,-0.0557 -0.0406,-0.1095 -0.066,-0.1621 0,0.0417 0.0003,0.0728 0.0003,0.1139 0.05,0.1374 0.1037,0.2734 0.1532,0.4109 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.0742,0.0304 0.148,0.0612 0.2162,0.0964 0.0174,-1.6211 0.0224,-4.091 0.0224,-8.0073l0,-10.7518zM47.3937,24.0647c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9795,24.1024c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.5119c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM49.9329,37.4531l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.9729,37.761c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8619c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.99,37.8675c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2253,49.8208c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.2914,50.9501c-0.2124,0.6904 -0.3402,1.037 -0.5254,1.2463 0.2284,-0.0203 0.3838,-0.0427 0.4079,-0.0669 0.0345,-0.0345 0.0786,-0.5411 0.1175,-1.1795zM22.5891,55.2218c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0018,0.0014 0.0046,0.0028 0.0072,0.0056 0.0032,0.0014 0.0062,0.0028 0.0094,0.0042 -0.0053,-0.0042 -0.0157,-0.0141 -0.0168,-0.0148z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#996b21\"/>\n  <path\n      android:pathData=\"m38.153,18.1343c-2.7736,-0.0056 -4.5141,0.037 -4.4682,0.1112 0.0413,0.0669 0.0153,0.1586 -0.0576,0.2037 -0.0729,0.045 -0.1326,0.4853 -0.1326,0.9784 0,1.2027 -0.1143,1.307 -1.4349,1.307 -0.909,0 -1.0696,0.0377 -1.4217,0.334l-0.3969,0.3339 -3.4962,-0.0014c-3.9566,-0.0028 -4.2788,0.0512 -4.6378,0.7699 -0.2135,0.4274 -0.2282,1.4777 -0.2284,16.2889 -0.0003,13.6169 -0.0269,15.9321 0.3477,16.5102 0.0835,0.0512 0.1899,0.1292 0.2817,0.1978 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0245 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0236 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0614 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0266,0.0028 0.0531,0.0056 0.0795,0.0084l7.1538,-0.0127c0.0564,-0.0113 0.113,-0.0208 0.169,-0.0334 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0025,-0.0042 0.0049,-0.0084 0.0073,-0.0127l0.0336,-15.8255c0.0307,-14.4117 0.0131,-16.095 -0.1718,-16.4526 -0.3718,-0.7191 -0.7251,-0.7776 -4.635,-0.7653l-3.5248,0.0113 -0.3969,-0.334c-0.3508,-0.2952 -0.5139,-0.3339 -1.4049,-0.3339 -0.61,0 -1.0699,-0.0619 -1.1647,-0.1568 -0.0908,-0.0908 -0.1565,-0.5257 -0.1565,-1.0347 0,-0.6043 -0.0609,-0.9391 -0.1948,-1.073 -0.1475,-0.1475 -0.1552,-0.2104 -0.0324,-0.26 0.0891,-0.036 -1.8825,-0.07 -4.3813,-0.0757zM38.0727,18.7762 L38.073,18.7767c3.3546,0 3.896,0.0275 4.0428,0.2042 0.1091,0.1315 0.1609,0.5264 0.1453,1.1094l-0.0245,0.9052l1.1675,0c1.2357,0 1.7736,0.1296 2.0508,0.4946 0.1215,0.1599 0.1709,0.7867 0.1739,2.2066 0.001,0.4767 0.0012,0.8391 -0.0038,1.1211 0.0422,-0.0747 0.1192,-0.1665 0.2455,-0.2822 0.0579,-0.0529 0.1114,-0.1126 0.1649,-0.1721 0.0009,-0.007 0.0023,-0.0113 0.0031,-0.0183 0.0168,-0.0084 0.0111,-0.0056 0.0252,-0.0127 0.0898,-0.0985 0.1822,-0.1931 0.3006,-0.2491 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0003,-0.0001 0.0008,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.0695,-0.039 0.1394,-0.079 0.2037,-0.1078 -0.2694,-0.0215 -0.4609,-0.048 -0.5152,-0.077 -0.1877,-0.1005 -0.2409,-0.2931 -0.2496,-0.9057l-0.011,-0.778 3.4521,-0.0113c0.8255,-0.0028 1.4547,-0.0042 1.9382,-0.001 1.4503,0.0099 1.5887,0.0641 1.7786,0.2539 0.2631,0.263 0.2645,0.3641 0.2626,16.0901 -0.001,8.704 -0.0451,15.89 -0.0979,15.969 -0.0527,0.079 -0.2241,0.2111 -0.3809,0.2937l0,0.0003c-0.2099,0.1105 -4.1872,0.1499 -15.09,0.1499 -14.7801,0 -14.8056,-0.0004 -15.1489,-0.2705l-0.3439,-0.2705l0,-15.8484c0,-15.7173 0.0025,-15.8508 0.2633,-16.1118 0.2526,-0.2526 0.4033,-0.2617 3.7524,-0.2284l3.4891,0.0346 0.0303,0.777c0.0195,0.5025 -0.0265,0.8129 -0.1305,0.879 -0.0884,0.0562 -0.998,0.103 -2.0213,0.104l-0.3678,0.0005c0.0636,0.0234 0.116,0.0576 0.1545,0.1002 0.0025,-0.0004 0.0048,-0.0014 0.0075,-0.0014 0.2194,-0.0335 0.4733,-0.0833 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0788 0.0327,-0 0.0654,0 0.0981,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,0 0.0489,0l0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.1373,0.0356 0.1951,0.0625 0.2896,0.2203 0.1588,0.0246 0.2715,0.0552 0.309,0.0925 0.0862,0.0862 0.1565,0.4723 0.1565,0.8583 0,0.6016 0.0545,0.7563 0.3816,1.0834l0.3814,0.3813l6.9994,0c7.7135,0 7.6034,0.0113 7.8857,-0.7987 0.0737,-0.2115 0.1328,-0.5755 0.1415,-0.8463 -0.0145,-0 -0.0315,0.0014 -0.0456,0.001 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.0566,-0.0127 -0.1002,-0.0328 -0.1277,-0.0601 -0.0227,0.6617 -0.0924,0.7917 -0.3,1.0332l-0.3151,0.3661 -6.7003,0.022c-3.6853,0.0127 -6.8474,-0.0191 -7.0269,-0.0691 -0.6081,-0.1694 -0.7826,-0.7901 -0.7829,-2.7842 -0.0001,-1.7009 0.015,-1.8051 0.3146,-2.1536 0.3024,-0.3518 0.3691,-0.3682 1.6968,-0.4109l1.3819,-0.0446l0,-0.9409c0,-0.5578 0.0637,-1.0048 0.1565,-1.0977 0.1148,-0.1147 1.1916,-0.1565 4.0303,-0.1565zM47.2494,23.8359l0,0.0003c0.0001,0 0.0004,-0 0.0005,0 -0.0001,-0.0001 -0.0004,-0.0001 -0.0005,-0.0003zM47.2499,23.8362c0.029,0.0177 0.0615,0.0415 0.1219,0.0856 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.0981,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.0802,-0.007 0.1649,-0.022 0.2501,-0.0324l-0.2088,0c-0.4314,0 -0.8377,-0.0127 -1.1805,-0.0311zM47.4378,23.9257c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0828,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9398c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8562c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8572c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9688,23.8911c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM27.1,23.8665 L26.0543,23.8675 26.0874,37.9962 26.1132,49.0258c0.125,-0.021 0.2553,-0.0365 0.3921,-0.0446l-0.0268,-10.8734c-0.0189,-7.6708 -0.0046,-11.5893 0.0533,-13.0185 -0.119,0.0146 -0.125,-0.0365 -0.1573,-0.2312 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329 0,0 0.0003,-0.0003 0.0003,-0.0003 0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.0751,-0.036 0.1474,-0.0626 0.2162,-0.0816zM48.912,23.8675c0.0776,0.0141 0.1526,0.0412 0.221,0.0977 0.1194,0.0984 0.2124,0.18 0.2883,0.2524 0.0402,0.02 0.0802,0.0401 0.1491,0.0717 0.0124,0.0345 0.0262,0.0686 0.0398,0.1028 0.0184,0.0084 0.0341,0.0155 0.0426,0.0239 0.0156,0.0156 0.0299,0.0763 0.0428,0.1828 0.0304,0.1011 0.0325,0.2092 0.0337,0.3594 0,0.0084 0.0003,0.0576 0.0003,0.1063 0.003,0.0628 0.0058,0.1574 0.0087,0.2304 0.0045,0.0442 0.0072,0.0877 0.0051,0.1288 0.003,0.088 0.0066,0.1555 0.0094,0.257 0.0433,0.0894 0.0954,0.1934 0.1767,0.3436 -0.0553,0.0379 -0.1109,0.0752 -0.1665,0.1126 0.017,0.7952 0.0286,1.9117 0.0359,3.421 0.0315,0.2133 0.023,0.4276 0.0237,0.6434l0,0.793 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.169 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0005,0.001 -0.0008,0.0001 -0.0013,0 0,0.0014 0.0003,0.0028 0.0003,0.0056 0.0104,0.0374 0.0396,0.1444 0.0171,0.0622 0.0104,0.0376 0.0244,0.0746 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.0174,-0.0557 -0.0406,-0.1095 -0.066,-0.1621 0,0.0417 0.0003,0.0728 0.0003,0.1139 0.05,0.1374 0.1037,0.2734 0.1532,0.4109 -0.0015,0.1104 -0.0015,0.1109 -0.0076,0.2067 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1573 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.0742,0.0304 0.148,0.0612 0.2162,0.0964 0.0174,-1.6211 0.0224,-4.091 0.0224,-8.0073l0,-10.7518zM47.3937,24.0648c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9795,24.1025c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4334,24.512c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0407 0.0084,-0.061zM52.4827,31.6048c-0.2031,-0.0262 -0.9403,0.6666 -1.0459,1.0069 -0.0557,0.1795 -0.1362,4.7733 -0.179,10.2085 -0.008,1.0164 -0.0141,1.5768 -0.0212,2.3963 0.0795,0.0752 0.1573,0.1522 0.2322,0.2322 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2499 -0.231,0.4155 -0.0292,0.0662 -0.0616,0.1309 -0.0953,0.195 -0.0005,0.0528 -0.001,0.117 -0.0015,0.1688 0.0732,0.1292 0.1479,0.257 0.2287,0.3796 0.0004,-0.021 0.0018,-0.0169 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4893 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0142,0.0988 -0.0342,0.2035 -0.0576,0.3095 -0.0446,3.3717 -0.0998,3.8025 -0.2524,3.9551 -0.218,0.218 -0.8716,0.2385 -10.1805,0.3174 -5.9784,0.0507 -10.1298,0.1365 -10.4043,0.2152 -0.2513,0.0719 -0.4275,0.1456 -0.3916,0.1639 0.0359,0.0183 5.1164,0.0683 11.2899,0.1112 6.1736,0.0428 11.2763,0.0383 11.3394,-0.0099 0.1316,-0.1016 0.0484,-21.9803 -0.0841,-22.1128 -0.0084,-0.0084 -0.0196,-0.0127 -0.0331,-0.0151zM49.933,37.453l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7609c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8618c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8674c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2254,49.8207c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.2914,50.95c-0.2124,0.6904 -0.3402,1.037 -0.5254,1.2463 0.2284,-0.0203 0.3838,-0.0427 0.4079,-0.0667 0.0345,-0.0345 0.0786,-0.5411 0.1175,-1.1795zM22.5892,55.2217c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0042 0.0018,0.0014 0.0046,0.0028 0.0072,0.0056 0.0032,0.0014 0.0062,0.0028 0.0094,0.0056 -0.0053,-0.0056 -0.0157,-0.0141 -0.0168,-0.0148z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#686157\"/>\n  <path\n      android:pathData=\"m38.153,18.1346c-2.7736,-0.0056 -4.5141,0.037 -4.4682,0.1112 0.0413,0.0669 0.0153,0.1583 -0.0576,0.2034 -0.0729,0.045 -0.1326,0.4855 -0.1326,0.9786 0,1.2027 -0.1143,1.3067 -1.4349,1.3067 -0.9152,0 -1.0685,0.0365 -1.4314,0.3418l-0.4063,0.3421 -3.4865,-0.0127c-3.9487,-0.0141 -4.2662,0.0387 -4.6283,0.7637 -0.2135,0.4274 -0.2282,1.4777 -0.2284,16.2889 -0.0003,13.6169 -0.0269,15.9321 0.3477,16.5102 0.0835,0.0512 0.1899,0.1292 0.2817,0.1978 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,0 0.3067,0 0.2376,0 0.4752,0.0001 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,-0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0266,0.0028 0.0531,0.0056 0.0795,0.0084l7.1538,-0.0127c0.0564,-0.0113 0.113,-0.0208 0.169,-0.0334 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0214 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0349 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0025,-0.0042 0.0049,-0.0084 0.0073,-0.0127l0.0336,-15.8252c0.0379,-17.8208 0.0941,-16.6539 -0.8219,-17.0366 -0.5506,-0.23 -7.0381,-0.275 -7.1773,-0.0497 -0.0497,0.0804 -0.2925,-0.0525 -0.5955,-0.3263 -0.4935,-0.4459 -0.5471,-0.4622 -1.529,-0.4622 -0.6175,0 -1.0794,-0.0615 -1.1744,-0.1565 -0.0908,-0.0908 -0.1565,-0.5259 -0.1565,-1.035 0,-0.6043 -0.0609,-0.9388 -0.1948,-1.0727 -0.1475,-0.1475 -0.1552,-0.2104 -0.0324,-0.26 0.0891,-0.0359 -1.8825,-0.0701 -4.3813,-0.0757zM38.0727,18.7762 L38.073,18.7767c3.3546,0 3.896,0.0275 4.0428,0.2042 0.1091,0.1315 0.1609,0.5264 0.1453,1.1094l-0.0245,0.9052l1.1675,0c1.2357,0 1.7736,0.1296 2.0508,0.4946 0.1215,0.1599 0.1709,0.7867 0.1739,2.2066 0.001,0.4767 0.0012,0.8391 -0.0038,1.1211 0.0422,-0.0747 0.1192,-0.1665 0.2455,-0.2822 0.0579,-0.0529 0.1114,-0.1126 0.1649,-0.1721 0.0009,-0.007 0.0023,-0.0113 0.0031,-0.0183 0.0168,-0.0084 0.0111,-0.0056 0.0252,-0.0127 0.0898,-0.0985 0.1822,-0.1931 0.3006,-0.2491 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0003,-0.0001 0.0008,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.0641,-0.0359 0.1288,-0.0732 0.1889,-0.1015 -0.3008,-0.0276 -0.4802,-0.0688 -0.5593,-0.1267 -0.1729,-0.1264 -0.25,-0.4069 -0.296,-1.0768 -0.0393,-0.5739 -0.0178,-0.8431 0.0586,-0.7344 0.0947,0.1347 0.8513,0.1687 3.5299,0.1591 3.2609,-0.0113 3.421,-0.0005 3.6744,0.2529 0.2634,0.2634 0.2648,0.3591 0.2628,16.0903 -0.001,8.704 -0.0451,15.89 -0.0979,15.969 -0.0527,0.079 -0.2241,0.2111 -0.3809,0.2937l0,0.0003c-0.2099,0.1105 -4.1872,0.1499 -15.09,0.1499 -14.7801,0 -14.8056,-0.0004 -15.1489,-0.2705l-0.3439,-0.2704l0,-15.8481c0,-15.6746 0.003,-15.8515 0.261,-16.1097 0.2479,-0.2479 0.4351,-0.261 3.7198,-0.261 3.2028,0 3.4596,-0.0169 3.47,-0.2284 0.0025,-0.0472 0.012,-0.0663 0.0265,-0.0604 0.0243,0.0099 0.0624,0.089 0.103,0.2236 0.1402,0.4648 0.0122,1.6562 -0.1925,1.7926 -0.0792,0.0528 -0.9812,0.0969 -2.0044,0.0978l-0.3679,0.0005c0.0636,0.0234 0.116,0.0576 0.1545,0.1002 0.0025,-0.0004 0.0048,-0.0014 0.0073,-0.0014 0.2194,-0.0335 0.4733,-0.0832 0.6651,0.0283 0.0476,0.0277 0.0871,0.0532 0.1264,0.0787 0.0327,-0 0.0654,0 0.0981,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,0 0.0489,0l0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.1373,0.0356 0.1951,0.0625 0.2896,0.2203 0.1588,0.0246 0.2715,0.0552 0.309,0.0925 0.0862,0.0862 0.1565,0.4723 0.1565,0.8583 0,0.6016 0.0545,0.7563 0.3816,1.0834l0.3814,0.3814l6.9994,0c7.7135,0 7.6034,0.0113 7.8857,-0.7987 0.0737,-0.2115 0.1328,-0.5755 0.1415,-0.8463 -0.0145,-0 -0.0315,0.0014 -0.0456,0.001 -0.0316,0.0127 -0.0641,0.0222 -0.0976,0.0305 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.0566,-0.0127 -0.1002,-0.0328 -0.1277,-0.0601 -0.0227,0.6617 -0.0924,0.7917 -0.3,1.0332l-0.3151,0.3661 -6.7003,0.0218c-3.6853,0.0127 -6.8474,-0.0191 -7.0269,-0.0691 -0.6081,-0.1695 -0.7826,-0.7901 -0.7829,-2.7842 -0.0001,-1.7009 0.015,-1.8051 0.3146,-2.1536 0.3024,-0.3518 0.3691,-0.3682 1.6967,-0.4109l1.3819,-0.0446l0,-0.9409c0,-0.5578 0.0637,-1.0048 0.1565,-1.0977 0.1148,-0.1147 1.1916,-0.1565 4.0303,-0.1565zM47.2644,23.8463c0.0277,0.0182 0.0542,0.0366 0.1073,0.0755 0.0219,0.001 0.0435,0.0028 0.066,0.0042 0.098,-0.0272 0.1847,-0.0372 0.3128,-0.0349 0.0973,0.0001 0.1944,0.0014 0.2916,-0.001 0.1148,-0.0005 0.2342,-0.0056 0.3469,0.0099 0.0802,-0.007 0.1649,-0.022 0.2501,-0.0324l-0.2404,0c-0.466,0 -0.8384,-0.007 -1.1344,-0.021zM47.4377,23.9256c-0.0128,0.0028 -0.0282,0.0099 -0.0416,0.0141 0.0025,0.0014 0.0049,0.0028 0.0076,0.0056 0.0039,0.0004 0.0082,0.0005 0.0122,0.001 -0.0034,0.0003 -0.0059,0.0004 -0.0097,0.001 0.0296,0.0211 0.0628,0.0431 0.1035,0.0698 0.0307,-0.0183 0.0546,-0.0342 0.0681,-0.0452 -0.0522,-0.0127 -0.1052,-0.0204 -0.1591,-0.0255 0.1485,-0.0113 0.2067,-0.0127 0.1591,0.0255l0,0.0003c0.0247,0.0056 0.0496,0.0113 0.0737,0.0196 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0829,-0.094 -0.0404,-0.0204 -0.0802,-0.0431 -0.1244,-0.0472 -0.0525,-0.0056 -0.1374,-0.0127 -0.2231,-0.0179zM47.3962,23.9397c-0.0084,-0.0056 -0.0168,-0.0127 -0.0245,-0.0176 -0.0729,-0.0042 -0.1382,-0.0056 -0.1749,0.001 0.0015,0.0042 0.0028,0.0084 0.0044,0.0127 0.0094,-0.0001 0.0187,-0.0014 0.028,-0.0014 0.051,0.0028 0.1024,0.0056 0.1537,0.0099 0.0045,-0.0014 0.0089,-0.0028 0.013,-0.0042zM47.0569,23.8561c-0.0294,0.0056 -0.0586,0.0099 -0.0874,0.0173 0.0004,0.0056 0.0004,0.0113 0.001,0.0173 0.0381,-0.0141 0.0612,-0.0238 0.0864,-0.0346zM46.9682,23.8571c0.0001,0.0056 0.0001,0.0099 0.0003,0.0163 -0.0082,0.0014 -0.0165,0.0028 -0.0245,0.0056 -0.0615,0.0196 -0.2414,0.0902 -0.179,0.0739 0.0863,-0.0224 0.1507,-0.0432 0.2037,-0.0622 -0.0001,-0.0056 0.0001,-0.0113 0,-0.0176 0.0001,-0 0.0004,0 0.0005,0 -0.0003,-0.0056 -0.0004,-0.0113 -0.001,-0.0163zM46.9687,23.891c0.0005,0.027 0.0001,0.0497 0.0005,0.076 0.0015,-0.0003 0.0031,-0.0005 0.0046,-0.001 -0.0015,-0.0251 -0.0021,-0.0501 -0.0035,-0.0752 -0.0004,0.0001 -0.001,0.0001 -0.0015,0.0003zM27.1,23.8662 L26.0543,23.8672 26.0874,37.9959 26.1134,49.0253c0.1249,-0.021 0.2552,-0.0362 0.3918,-0.0443l-0.0268,-10.8734c-0.0189,-7.6708 -0.0046,-11.5893 0.0533,-13.0185 -0.119,0.0146 -0.125,-0.0365 -0.1573,-0.2312 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.003,-0.0153 0.0072,-0.0305 0.0104,-0.0459 0.0124,-0.0573 0.026,-0.1144 0.039,-0.1716 0.002,-0.0042 0.0042,-0.0084 0.0061,-0.0127 0.0208,-0.165 0.0167,-0.1302 0.209,-0.3329 0,0 0.0003,-0.0003 0.0003,-0.0003 0.0656,-0.0822 0.1392,-0.1559 0.2371,-0.2029 0.0751,-0.036 0.1474,-0.0626 0.2162,-0.0816zM48.912,23.8672c0.0776,0.0141 0.1526,0.0412 0.221,0.0977 0.1195,0.0985 0.2129,0.18 0.2888,0.2524 0.0402,0.02 0.0798,0.0403 0.1486,0.0717 0.0122,0.0345 0.0262,0.0684 0.0398,0.1025 0.0185,0.007 0.0341,0.0155 0.0426,0.0239 0.0157,0.0156 0.0299,0.0764 0.0428,0.1833 0.0304,0.1009 0.0325,0.2091 0.0337,0.3592 0,0.0084 0.0003,0.0576 0.0003,0.106 0.003,0.0629 0.0058,0.1578 0.0087,0.231 0.0045,0.0442 0.0072,0.0874 0.0051,0.1285 0.003,0.088 0.0066,0.1555 0.0094,0.2569 0.0433,0.0894 0.0954,0.1934 0.1767,0.3436 -0.0553,0.0379 -0.1109,0.0752 -0.1665,0.1126 0.017,0.7953 0.0286,1.9118 0.0359,3.421 0.0315,0.2133 0.023,0.4276 0.0237,0.6434l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0005,0.001 -0.0008,0.0001 -0.0013,0 0,0.0014 0.0003,0.0028 0.0003,0.0056 0.0104,0.0374 0.0396,0.1444 0.0171,0.0622 0.0104,0.0376 0.0244,0.0746 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.0174,-0.0557 -0.0406,-0.1095 -0.066,-0.1621 0,0.0417 0.0003,0.0728 0.0003,0.1139 0.05,0.1374 0.1037,0.2735 0.1532,0.4109 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2068 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0063,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1198 0,0.2398 0,0.3597 -0.0001,0.1611 -0.0005,0.3222 0.0003,0.4833 0.0005,0.0945 0.0038,0.19 -0.0031,0.2842 0.0053,0.0923 0.0224,0.1936 0.0141,0.2832 -0.0141,0.15 -0.0282,0.2815 -0.0607,0.402 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.0008,-0.0084 -0.0023,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.0742,0.0304 0.148,0.0612 0.2162,0.0964 0.0174,-1.6211 0.0224,-4.091 0.0224,-8.0073l0,-10.7518zM47.3937,24.0645c0.0051,0.0028 0.0111,0.0056 0.0161,0.0084 0.0015,-0.001 0.0037,-0.0014 0.0051,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9794,24.1023c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM26.4333,24.5122c-0.0042,0.0204 -0.0086,0.0408 -0.0117,0.0614 -0.0004,0.0014 0.0003,0.0028 0,0.007 0.0004,-0.001 0.0009,-0.001 0.0013,-0.0014 0.0005,-0.0014 0.0015,-0.0028 0.002,-0.0056 0.0042,-0.0266 0.0058,-0.0408 0.0084,-0.061zM52.4826,31.6051c-0.2031,-0.0262 -0.9403,0.6666 -1.0459,1.0069 -0.0557,0.1795 -0.1362,4.7733 -0.179,10.2085 -0.008,1.0163 -0.0144,1.5766 -0.0214,2.396 0.0795,0.0753 0.1574,0.1524 0.2325,0.2325 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2498 -0.231,0.4155 -0.0293,0.0662 -0.0618,0.1311 -0.0956,0.1953 -0.0005,0.0525 -0.001,0.1166 -0.0015,0.1682 0.0732,0.1292 0.1481,0.2572 0.2289,0.3798 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4894 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0881 -0.0142,0.0991 -0.0342,0.204 -0.0576,0.3102 -0.0445,3.371 -0.0998,3.8018 -0.2524,3.9543 -0.218,0.218 -0.8716,0.2385 -10.1805,0.3174 -5.9784,0.0507 -10.1298,0.1365 -10.4043,0.2152 -0.2513,0.0719 -0.4275,0.1456 -0.3916,0.1639 0.0359,0.0183 5.1164,0.0683 11.2899,0.1112 6.1736,0.0429 11.2763,0.0383 11.3394,-0.0099 0.1316,-0.1016 0.0484,-21.9803 -0.0841,-22.1128 -0.0084,-0.0084 -0.0196,-0.0127 -0.0331,-0.0151zM49.9329,37.4532l0,0.0003c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0005,-0.0004 -0.0013,-0.0003 -0.0018,-0.001zM49.973,37.7612c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9893,37.8621c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9901,37.8677c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.2253,49.8209c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.2914,50.9502c-0.2124,0.6904 -0.3402,1.037 -0.5254,1.2463 0.2284,-0.0203 0.3838,-0.0427 0.4079,-0.0667 0.0345,-0.0345 0.0786,-0.5411 0.1175,-1.1795zM22.5892,55.2219c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0056 0.0018,0.0014 0.0046,0.0028 0.0072,0.0056 0.0032,0.0014 0.0062,0.0028 0.0094,0.0056 -0.0053,-0.0056 -0.0157,-0.0141 -0.0168,-0.0148z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#5e605e\"/>\n  <path\n      android:pathData=\"m38.1859,18.1409c-0.7807,-0 -1.5669,0.0056 -2.1704,0.016 -1.207,0.0215 -0.2564,0.0393 2.1125,0.0393 2.3689,0 3.3566,-0.0177 2.1946,-0.0393 -0.581,-0.0113 -1.3561,-0.016 -2.1367,-0.016zM36.2396,18.4089c-1.672,0.0042 -2.4705,0.0417 -2.5609,0.1167 -0.1094,0.0908 -0.1874,0.5128 -0.2113,1.1438l-0.038,0.9998 -1.1984,0.0667c-1.031,0.0573 -1.2443,0.1101 -1.5295,0.378l-0.3317,0.3113 -3.5605,-0.016c-4.0294,-0.0183 -4.3379,0.0315 -4.7023,0.7609 -0.2135,0.4274 -0.2282,1.4777 -0.2284,16.2889 -0.0003,13.6169 -0.0269,15.9321 0.3477,16.5102 0.0835,0.0512 0.1899,0.1292 0.2817,0.1978 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.018,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0.0001 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0362 0.084,0.0056 0.3222,0.0466 0.1393,0.0236 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,-0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0266,0.0028 0.0531,0.0056 0.0795,0.0084l7.1538,-0.0127c0.0564,-0.0113 0.113,-0.0208 0.169,-0.0334 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0215 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0351 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0025,-0.0042 0.0049,-0.0084 0.0073,-0.0127l0.0336,-15.8255c0.0379,-17.8208 0.0943,-16.6536 -0.8216,-17.0363 -0.5731,-0.2395 -6.9689,-0.2734 -7.2041,-0.0383 -0.1094,0.1094 -0.2277,0.0604 -0.4601,-0.1904 -0.4393,-0.474 -0.8824,-0.6095 -1.9925,-0.6095l-0.9644,0l-0.0714,-0.4242c-0.0392,-0.2333 -0.0858,-0.7472 -0.1037,-1.142l-0.0326,-0.7179 -4.3568,-0.0345c-0.7575,-0.0056 -1.4119,-0.0084 -1.9693,-0.007zM36.6029,18.6454c0.4358,-0.0005 0.9379,0.001 1.5165,0.001 3.0796,0 3.9887,0.0374 4.0978,0.169 0.0771,0.0929 0.1578,0.6069 0.1792,1.142l0.0388,0.973 1.399,0.0653c1.9075,0.089 1.8526,0.019 1.9066,2.4358 0.0113,0.501 0.0168,0.8946 0.0145,1.217 0.0332,-0.0351 0.0709,-0.0724 0.115,-0.1126 0.058,-0.0531 0.1116,-0.1128 0.1652,-0.1723 0.0009,-0.007 0.0023,-0.0113 0.0031,-0.0183 0.0166,-0.0084 0.011,-0.0056 0.025,-0.0127 0.0897,-0.0985 0.1822,-0.1928 0.3006,-0.2488 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.0069,-0.0042 0.0135,-0.007 0.0204,-0.0113 -0.1451,-0.018 -0.2573,-0.038 -0.3179,-0.0591l-0.4112,-0.1433 0.0018,-0.9988c0.001,-0.5835 0.0474,-0.9274 0.1109,-0.8272 0.0856,0.135 0.8338,0.1688 3.5179,0.1591 3.2609,-0.0113 3.421,-0.0005 3.6744,0.2529 0.2634,0.2634 0.2651,0.3592 0.2631,16.0903 -0.001,8.704 -0.0451,15.89 -0.0979,15.969 -0.0527,0.079 -0.2241,0.2111 -0.3809,0.2937 -0.2099,0.1105 -4.1872,0.1501 -15.09,0.1501 -14.7801,0 -14.8056,-0.0004 -15.1489,-0.2705l-0.3439,-0.2705l0,-15.8489c0,-15.6746 0.003,-15.8515 0.261,-16.1097 0.2479,-0.2479 0.4351,-0.261 3.7198,-0.261 3.2021,0 3.4597,-0.0169 3.4718,-0.2284 0.0055,-0.0949 0.0166,-0.1381 0.0313,-0.141 0.0638,-0.0127 0.1936,0.733 0.2037,1.3111 0.0107,0.6119 -0.0323,0.7702 -0.2394,0.881 -0.1169,0.0625 -0.7969,0.1128 -1.6088,0.1282 0.001,0.0005 0.0023,0.001 0.0032,0.0014 0.0475,0.0276 0.0869,0.0531 0.1262,0.0786 0.0327,-0 0.0654,0 0.0981,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.1355,0.0352 0.1936,0.0615 0.2858,0.2136l0.4043,0.0084 0.079,0.8267c0.063,0.6593 0.1494,0.897 0.427,1.1747l0.3482,0.348 6.6861,0.0384c3.6774,0.0213 6.8837,0.0028 7.125,-0.0412 0.7238,-0.1316 1.0623,-0.5812 1.0623,-1.411 0,-0.0486 0.001,-0.0949 0.0025,-0.1402 -0.0063,0.0014 -0.0122,0.0042 -0.0186,0.0056 -0.0887,0.0221 -0.1269,-0.001 -0.132,-0.0519 -0.0003,-0.0001 -0.0005,-0.0001 -0.001,-0.0003 -0.0305,0.6823 -0.1332,0.9036 -0.3765,1.1469 -0.2502,0.2502 -0.462,0.2577 -7.2831,0.2577l-7.0249,0l-0.3128,-0.3327c-0.3023,-0.3218 -0.3129,-0.4012 -0.3164,-2.4036 -0.0035,-2.0141 0.0049,-2.0797 0.3125,-2.408 0.3007,-0.3208 0.3832,-0.3375 1.6641,-0.3375l1.3478,0l0,-0.8846c0,-1.3595 -0.2031,-1.4613 2.8472,-1.4655zM46.829,23.9218c-0.0559,0.022 -0.0997,0.0404 -0.0637,0.0311 0.0389,-0.0099 0.0608,-0.0186 0.0918,-0.028 -0.0096,-0.001 -0.0187,-0.0014 -0.028,-0.0028zM46.9692,23.9366c0,0.0099 0.0001,0.0197 0.0003,0.0301 0.0015,-0.0003 0.0028,-0.0005 0.0044,-0.001 -0.0005,-0.0099 -0.0008,-0.0191 -0.0013,-0.0289 -0.0009,-0.0001 -0.0023,-0.0001 -0.0032,-0.0003zM47.4372,23.9677c0.0225,0.0153 0.0441,0.0301 0.0727,0.049 0.0286,-0.017 0.0494,-0.0313 0.0632,-0.0421 -0.0463,-0.0014 -0.0911,-0.0042 -0.1359,-0.007zM47.5938,23.9761c0.0194,0.0042 0.039,0.0084 0.0579,0.0155 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0828,-0.094 -0.0039,-0.0014 -0.0076,-0.0028 -0.0115,-0.0056 -0.0637,-0.0028 -0.1184,-0.007 -0.18,-0.0099zM26.8022,23.9978 L26.054,23.9981 26.0871,37.9963 26.1131,49.0259c0.0853,-0.0144 0.1731,-0.0256 0.2638,-0.0342 -0.077,-6.1194 -0.089,-21.6795 0.0008,-24.1142 -0.0012,-0.007 -0.002,-0.0113 -0.0032,-0.0194 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.001,-0.0056 0.0025,-0.0113 0.0038,-0.0166 0.0042,-0.0742 0.0086,-0.1312 0.0132,-0.1486 0.0137,-0.0508 0.027,-0.0926 0.0482,-0.1273 0.0155,-0.0918 0.0393,-0.1018 0.1993,-0.2705l0.0003,-0.0003c0.0459,-0.0576 0.0962,-0.1102 0.1555,-0.1532zM49.172,23.9981c0.1,0.0833 0.1764,0.1525 0.2432,0.2164 0.0409,0.0204 0.0834,0.0419 0.1552,0.0747 0.0038,0.0113 0.0082,0.0213 0.0122,0.0318l0.1616,0.0028 0.0044,1.3475c0.0438,0.0908 0.097,0.1974 0.1807,0.3523 -0.0596,0.0408 -0.1196,0.0809 -0.1795,0.1213l0.0072,2.2696c0.0059,0.0443 0.0139,0.0884 0.0184,0.1327 0.0132,0.1335 -0.011,0.2708 0.0018,0.4045 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0127 0.002,0.0084 0.0039,0.0127 0.0059,0.0214 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.002,-0.0028 -0.0068,-0.0113 -0.0094,-0.0155 0.0001,0.0328 0.0004,0.0631 0.0005,0.0956 0.0563,0.1602 0.1201,0.3179 0.1777,0.4777 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2068 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0062,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0,0.0127 0,0.0265 0,0.0398 0.0089,-1.5448 0.015,-3.3009 0.015,-5.4968l0,-10.6949zM47.3936,24.0648c0.0051,0.0028 0.0113,0.0056 0.0163,0.0084 0.0015,-0.001 0.0034,-0.0014 0.0048,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9796,24.1025c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM51.4369,32.6123c-0.0557,0.1795 -0.1362,4.7733 -0.179,10.2085 -0.008,1.0164 -0.0144,1.5768 -0.0214,2.3963 0.0795,0.0752 0.1573,0.1522 0.2322,0.2322 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2498 -0.231,0.4155 -0.0292,0.066 -0.0616,0.1306 -0.0953,0.1948 -0.0005,0.0529 -0.001,0.1173 -0.0015,0.1693 0.0731,0.1291 0.1479,0.2568 0.2287,0.3793 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4894 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0142,0.0987 -0.034,0.2032 -0.0574,0.3089 -0.0446,3.3722 -0.0998,3.803 -0.2524,3.9556 -0.218,0.218 -0.8716,0.2385 -10.1805,0.3174 -5.9784,0.0507 -9.9887,-0.1342 -10.2633,-0.0556 -0.2513,0.0719 -2.0688,1.3682 -2.0328,1.3865 0.0359,0.0183 5.4399,-0.2144 11.6135,-0.1716 6.1736,0.0429 12.4602,-0.6047 12.5232,-0.6534 0.1316,-0.1016 0.7335,-22.8718 0.6009,-23.0043 -0.0084,-0.0084 0.1671,-0.4246 -0.1111,0.293 -1.4688,0.8515 -1.8967,1.177 -2.0323,1.5311zM49.7858,36.5421c0,0.007 -0,0.0127 0,0.0183 0.0058,0.021 0.0101,0.0373 0.0212,0.0767 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.0004,-0.001 -0.001,-0.0014 -0.0015,-0.0028zM49.9332,37.4537c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0004,-0.0004 -0.0013,-0.0004 -0.0018,-0.001zM49.9732,37.7614c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9895,37.8623c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9903,37.8679c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.0469,41.3841 L50.0466,41.3843c-0,0.0005 0,0.001 0,0.0014 0.0001,-0.0005 0.0001,-0.001 0.0003,-0.0014zM50.0466,41.3855c-0.0099,0.076 -0.0218,0.1487 -0.04,0.2164 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.0237,0.0099 0.0485,0.0183 0.0716,0.0286 0.0045,-0.3735 0.0092,-0.7352 0.0128,-1.1726zM50.2253,49.8208c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.2503,51.0824c-0.1721,0.5503 -0.2915,0.8642 -0.4423,1.0605 0.0886,-0.0113 0.1607,-0.0228 0.1917,-0.0346 0.1047,-0.0401 0.1917,-0.4243 0.2506,-1.0258zM22.5891,55.2224c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0042 0.0018,0.0014 0.0046,0.0028 0.0072,0.0056 0.0032,0.0014 0.0062,0.0028 0.0094,0.0056 -0.0053,-0.0056 -0.0157,-0.0141 -0.0168,-0.0148z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#95661d\"/>\n  <path\n      android:pathData=\"m38.1859,18.1409c-0.7807,-0 -1.5669,0.0056 -2.1704,0.016 -1.207,0.0215 -0.2564,0.0393 2.1125,0.0393 2.3689,0 3.3566,-0.0177 2.1946,-0.0393 -0.581,-0.0113 -1.3561,-0.016 -2.1367,-0.016zM36.2396,18.4089c-1.672,0.0042 -2.4705,0.0417 -2.5609,0.1167 -0.1094,0.0908 -0.1874,0.5128 -0.2113,1.1438l-0.038,0.9998 -1.1984,0.0667c-1.031,0.0573 -1.2443,0.1101 -1.5295,0.378l-0.3317,0.3113 -3.5605,-0.016c-4.0294,-0.0183 -4.3379,0.0315 -4.7023,0.7609 -0.2135,0.4274 -0.2282,1.4777 -0.2284,16.2889 -0.0003,13.6169 -0.0269,15.9321 0.3477,16.5102 0.0835,0.0512 0.1899,0.1292 0.2817,0.1978 0.1378,0 0.2757,0 0.4135,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,-0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.018,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0 0.3067,0c0.2376,0 0.4752,0.0001 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0362 0.084,0.0056 0.3222,0.0466 0.1393,0.0236 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.0179 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,-0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0266,0.0028 0.0531,0.0056 0.0795,0.0084l7.1538,-0.0127c0.0564,-0.0113 0.113,-0.0208 0.169,-0.0334 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0423,-0.0283 0.0905,-0.0602 0.1389,-0.0764 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0215 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0216,-0.0351 0.0417,-0.0712 0.0655,-0.1047 0.0089,-0.0127 0.0172,-0.0252 0.0278,-0.0362 0.0161,-0.0165 0.0352,-0.0298 0.052,-0.0456 0.0049,-0.0042 -0.0107,0.0084 -0.0161,0.0127 -0.0062,-0.0099 -0.0005,0.0005 0.013,-0.0249 0.0096,-0.0182 0.0155,-0.0383 0.0247,-0.0566 0.0121,-0.0241 0.0258,-0.0472 0.0385,-0.0708 0.0025,-0.0042 0.0049,-0.0084 0.0073,-0.0127l0.0336,-15.8255c0.0379,-17.8208 0.0943,-16.6536 -0.8216,-17.0363 -0.5731,-0.2395 -6.9689,-0.2734 -7.2041,-0.0383 -0.1094,0.1094 -0.2277,0.0604 -0.4601,-0.1904 -0.4393,-0.474 -0.8824,-0.6095 -1.9925,-0.6095l-0.9644,0l-0.0714,-0.4242c-0.0392,-0.2333 -0.0858,-0.7472 -0.1037,-1.142l-0.0326,-0.7179 -4.3568,-0.0345c-0.7575,-0.0056 -1.4119,-0.0084 -1.9693,-0.007zM36.6029,18.6454c0.4358,-0.0005 0.9379,0.001 1.5165,0.001 3.0796,0 3.9887,0.0374 4.0978,0.169 0.0771,0.0929 0.1578,0.6069 0.1792,1.142l0.0388,0.973 1.399,0.0653c1.9075,0.089 1.8526,0.019 1.9066,2.4358 0.0113,0.501 0.0168,0.8946 0.0145,1.217 0.0332,-0.0351 0.0709,-0.0724 0.115,-0.1126 0.058,-0.0531 0.1116,-0.1128 0.1652,-0.1723 0.0009,-0.007 0.0023,-0.0113 0.0031,-0.0183 0.0166,-0.0084 0.011,-0.0056 0.025,-0.0127 0.0897,-0.0985 0.1822,-0.1928 0.3006,-0.2488 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.0069,-0.0042 0.0135,-0.007 0.0204,-0.0113 -0.1451,-0.018 -0.2573,-0.038 -0.3179,-0.0591l-0.4112,-0.1433 0.0018,-0.9988c0.001,-0.5829 0.0475,-0.9275 0.1109,-0.8275 0.0855,0.1349 0.8433,0.1688 3.5635,0.1593 3.3692,-0.0113 3.4612,-0.0056 3.7071,0.2666 0.2462,0.2721 0.2521,0.6494 0.2521,16.1344l0,15.8561l-0.3082,0.2493c-0.2994,0.2424 -0.7336,0.2496 -15.3034,0.2496 -14.8212,0 -14.9982,-0.0028 -15.2562,-0.261 -0.2581,-0.2582 -0.261,-0.4352 -0.261,-16.1643 0,-15.2409 0.0101,-15.9121 0.2389,-16.1191 0.2095,-0.1896 0.6637,-0.2159 3.7198,-0.2159 3.2234,0 3.4819,-0.0169 3.4939,-0.2284 0.0055,-0.0949 0.0166,-0.1381 0.0313,-0.141 0.0638,-0.0127 0.1936,0.733 0.2037,1.3111 0.0107,0.6119 -0.0323,0.7702 -0.2394,0.881 -0.1169,0.0625 -0.7969,0.1128 -1.6088,0.1282 0.001,0.0005 0.0023,0.001 0.0032,0.0014 0.0473,0.0276 0.0865,0.0529 0.1257,0.0783 0.0329,-0 0.0658,0 0.0987,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0l0.0959,0 0.0528,0 0.0489,0 0.0025,0l0,0.001c0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.1356,0.0352 0.1935,0.0617 0.2858,0.2139l0.4043,0.0084 0.079,0.8267c0.063,0.6593 0.1494,0.897 0.427,1.1747l0.3482,0.348 6.6861,0.0384c3.6774,0.0213 6.8837,0.0028 7.125,-0.0412 0.7238,-0.1318 1.0623,-0.5812 1.0623,-1.411 0,-0.0486 0.001,-0.0949 0.0025,-0.1402 -0.0063,0.0014 -0.0122,0.0042 -0.0186,0.0056 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.0003,-0.0001 -0.0005,-0.0001 -0.001,-0.0003 -0.0305,0.6823 -0.1332,0.9036 -0.3765,1.1469 -0.2502,0.2502 -0.462,0.2577 -7.2831,0.2577L31.0602,26.4762l-0.3128,-0.3326c-0.3024,-0.3219 -0.3129,-0.4013 -0.3164,-2.4036 -0.0035,-2.0141 0.0049,-2.0797 0.3125,-2.408 0.3007,-0.3208 0.3832,-0.3375 1.6641,-0.3375l1.3478,0l0,-0.8846c0,-1.3595 -0.2031,-1.4613 2.8472,-1.4655zM46.829,23.9218c-0.0559,0.022 -0.0997,0.0404 -0.0637,0.0311 0.0389,-0.0099 0.0608,-0.0186 0.0918,-0.028 -0.0096,-0.001 -0.0187,-0.0014 -0.028,-0.0028zM46.9692,23.9366c0,0.0099 0.0001,0.0197 0.0003,0.0301 0.0015,-0.0003 0.0028,-0.0005 0.0044,-0.001 -0.0005,-0.0099 -0.0008,-0.0191 -0.0013,-0.0289 -0.0009,-0.0001 -0.0023,-0.0001 -0.0032,-0.0003zM47.4372,23.9677c0.0225,0.0153 0.0441,0.0301 0.0727,0.049 0.0286,-0.017 0.0494,-0.0313 0.0632,-0.0421 -0.0463,-0.0014 -0.0911,-0.0042 -0.1359,-0.007zM47.5938,23.9761c0.0194,0.0042 0.039,0.0084 0.0579,0.0155 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0828,-0.094 -0.0039,-0.0014 -0.0076,-0.0028 -0.0115,-0.0056 -0.0637,-0.0028 -0.1184,-0.007 -0.18,-0.0099zM26.8022,23.9978 L26.054,23.9981 26.0871,37.9963 26.1131,49.0259c0.0853,-0.0144 0.1731,-0.0256 0.2638,-0.0342 -0.077,-6.1194 -0.089,-21.6795 0.0008,-24.1142 -0.0012,-0.007 -0.002,-0.0113 -0.0032,-0.0194 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.001,-0.0056 0.0025,-0.0113 0.0038,-0.0166 0.0042,-0.0742 0.0086,-0.1312 0.0132,-0.1486 0.0137,-0.0508 0.027,-0.0926 0.0482,-0.1273 0.0155,-0.0918 0.0393,-0.1018 0.1993,-0.2705l0.0003,-0.0003c0.0459,-0.0576 0.0962,-0.1102 0.1555,-0.1532zM49.172,23.9981c0.1,0.0833 0.1764,0.1525 0.2432,0.2164 0.0409,0.0204 0.0834,0.0419 0.1552,0.0747 0.0038,0.0113 0.0082,0.0213 0.0122,0.0318l0.1616,0.0028 0.0044,1.3475c0.0438,0.0908 0.097,0.1974 0.1807,0.3523 -0.0596,0.0408 -0.1196,0.0809 -0.1795,0.1213l0.0072,2.2696c0.0059,0.0443 0.0139,0.0884 0.0184,0.1327 0.0132,0.1335 -0.011,0.2708 0.0018,0.4045 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0127 0.002,0.0084 0.0039,0.0127 0.0059,0.0214 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.002,-0.0028 -0.0068,-0.0113 -0.0094,-0.0155 0.0001,0.0328 0.0004,0.0631 0.0005,0.0956 0.0563,0.1602 0.1201,0.3179 0.1777,0.4777 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2068 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0062,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0,0.0127 0,0.0265 0,0.0398 0.0089,-1.5448 0.015,-3.3009 0.015,-5.4968l0,-10.6949zM47.3936,24.0648c0.0051,0.0028 0.0113,0.0056 0.0163,0.0084 0.0015,-0.001 0.0034,-0.0014 0.0048,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9796,24.1025c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM51.3599,35.3218c-0.0374,0.0808 -0.0748,3.2841 -0.0877,7.8154 -0.0025,0.9005 -0.0056,1.3702 -0.0082,2.1074 0.0696,0.0667 0.1386,0.1342 0.2047,0.2047 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0788,0.0885 -0.0472,0.2498 -0.231,0.4155 -0.0198,0.0446 -0.0409,0.0888 -0.0627,0.1326 -0.0004,0.0928 -0.0006,0.199 -0.001,0.2891 0.0628,0.1092 0.1268,0.2175 0.1955,0.3217 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4894 0.015,0.0365 0.0252,0.7125 -0.0836,0.6122 -0.0226,0.3622 -0.2203,0.9718 -0.0046,0.0142 0.0049,-0.0217 0.0101,-0.0434 -0.01,0.0301 -0.0199,0.0604 -0.0227,0.0883 -0.0021,0.0148 -0.0053,0.0307 -0.0076,0.0456 -0.0259,3.6306 -0.0785,3.9277 -0.2534,4.1437l-0.2493,0.3082 -8.9197,0.0676c-3.7609,0.0284 -6.6502,0.0657 -7.4957,0.0936 0.0181,-0.0791 -0.9523,-0.3299 -1.0987,-0.2942 -0.0912,0.0222 0.4189,0.159 1.0449,0.2965 -0.1972,0.007 -0.2733,0.0144 -0.13,0.0198 0.0951,0.0028 0.1483,-0.001 0.1708,-0.0113 0.6273,0.1367 1.3591,0.2714 1.7217,0.2919 0.7179,0.0405 4.7411,0.0702 8.9406,0.0657l7.6354,-0.0084 -0.0222,-8.7886c-0.0121,-4.8338 -0.0572,-8.8237 -0.0999,-8.8664 -0.1082,-0.1081 -0.9813,0.7588 -1.0141,1.0069 -0.0148,0.1115 -0.2172,0.3407 -0.2595,-0.6283 -0.0052,-0.1212 -0.2932,-0.3934 0.1397,-1.0929zM49.7858,36.5421c0,0.007 -0,0.0127 0,0.0183 0.0058,0.021 0.0101,0.0373 0.0212,0.0767 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.0004,-0.001 -0.001,-0.0014 -0.0015,-0.0028zM49.9332,37.4537c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0004,-0.0004 -0.0013,-0.0004 -0.0018,-0.001zM49.9732,37.7614c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9895,37.8623c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9903,37.868c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.0469,41.3841 L50.0466,41.3844c-0,0.0005 0,0.001 0,0.0014 0.0001,-0.0005 0.0001,-0.001 0.0003,-0.0014zM50.0466,41.3855c-0.0099,0.076 -0.0218,0.1487 -0.04,0.2164 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.0237,0.0099 0.0485,0.0183 0.0716,0.0286 0.0045,-0.3735 0.0092,-0.7352 0.0128,-1.1726zM50.2253,49.8209c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.2503,51.0825c-0.1721,0.5503 -0.2915,0.8642 -0.4423,1.0605 0.0886,-0.0113 0.1607,-0.0228 0.1917,-0.0346 0.1047,-0.0401 0.1917,-0.4243 0.2506,-1.0258zM22.5891,55.2225c-0.002,-0.0014 0.0001,0.0014 0.0003,0.0042 0.0018,0.0014 0.0046,0.0028 0.0072,0.0056 0.0032,0.0014 0.0062,0.0028 0.0094,0.0056 -0.0053,-0.0056 -0.0157,-0.0141 -0.0168,-0.0148z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#886529\"/>\n  <path\n      android:pathData=\"m38.1859,18.1409c-0.7807,-0 -1.5669,0.0056 -2.1704,0.016 -1.207,0.0215 -0.2564,0.0393 2.1125,0.0393 2.3689,0 3.3566,-0.0177 2.1946,-0.0393 -0.581,-0.0113 -1.3561,-0.016 -2.1367,-0.016zM36.2396,18.4089c-1.672,0.0042 -2.4705,0.0417 -2.5609,0.1167 -0.1094,0.0908 -0.1874,0.5128 -0.2113,1.1438l-0.038,0.9998 -1.2002,0.0667c-1.0841,0.0602 -1.2366,0.1014 -1.5736,0.4242l-0.3729,0.3574l-3.6739,0c-2.334,0 -3.7859,0.0511 -3.9808,0.1399 -0.7215,0.3287 -0.6814,-0.5873 -0.7189,16.5173 -0.0274,12.4736 0.0001,15.9157 0.1305,16.3149 0.1062,0.3253 0.2546,0.5217 0.5183,0.6773 0.1212,0 0.2425,0 0.3638,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.018,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0142,0.0014 0.0284,0.0042 0.0426,0.0056 0.3886,-0.001 0.8877,-0.0014 1.2392,-0.0028 0.0205,-0.007 0.0424,-0.0127 0.067,-0.016 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5218,0c1.6475,-0.0214 2.4219,-0.0545 2.7562,-0.1053 0.0331,-0.0211 0.0677,-0.0418 0.1032,-0.0538 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0215 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0089,-0.0144 0.0183,-0.0286 0.027,-0.0431 0.2777,-0.9752 0.2506,-4.2279 0.2506,-16.3139 0,-11.4337 -0.0408,-15.953 -0.1458,-16.1584 -0.3782,-0.7395 -0.3858,-0.7406 -4.4815,-0.7334 -3.6325,0.007 -3.7867,-0.0028 -3.9808,-0.2552 -0.2995,-0.3883 -0.8241,-0.5364 -1.9012,-0.5364l-0.9644,0l-0.0714,-0.4242c-0.0392,-0.2333 -0.0858,-0.7472 -0.1037,-1.142l-0.0326,-0.7179 -4.3568,-0.0345c-0.7575,-0.0056 -1.4119,-0.0084 -1.9693,-0.007zM36.6029,18.6454c0.4358,-0.0005 0.9379,0.001 1.5165,0.001 3.0796,0 3.9887,0.0374 4.0978,0.169 0.0771,0.0929 0.1578,0.6069 0.1792,1.142l0.0388,0.973 1.399,0.0653c1.9075,0.089 1.8526,0.019 1.9066,2.4358 0.0113,0.501 0.0168,0.8946 0.0145,1.217 0.0332,-0.0351 0.0709,-0.0724 0.115,-0.1126 0.058,-0.0531 0.1116,-0.1128 0.1652,-0.1723 0.0009,-0.007 0.0023,-0.0113 0.0031,-0.0183 0.0166,-0.0084 0.011,-0.0056 0.025,-0.0127 0.0897,-0.0985 0.1822,-0.1928 0.3006,-0.2488 0.0471,-0.0222 0.0965,-0.037 0.1473,-0.0469 0.0003,-0.0001 0.0005,-0.0001 0.001,-0.0003 0.0725,-0.0334 0.1458,-0.0656 0.2136,-0.1068 0.0049,-0.0028 0.0059,-0.0042 0.0073,-0.0056 -0.0759,0.0425 -0.15,0.0821 -0.2136,0.0981 -0.0114,0.0028 0.0193,-0.0127 0.0291,-0.0194 0.0538,-0.0222 0.1999,-0.0915 0.1846,-0.0788 0.0068,-0.0042 0.0134,-0.007 0.0201,-0.0113 -0.1451,-0.018 -0.2574,-0.038 -0.3179,-0.0591l-0.4109,-0.1433 0.0101,-0.9988c0.0056,-0.5493 0.04,-0.9254 0.0762,-0.8356 0.0504,0.1249 0.8865,0.1632 3.5587,0.1632 3.3852,0 3.5008,0.0084 3.7417,0.2748 0.2416,0.267 0.2485,0.7215 0.2485,16.1306l0,15.8561l-0.3082,0.2493c-0.2994,0.2424 -0.7336,0.2496 -15.3034,0.2496 -14.8212,0 -14.9982,-0.0028 -15.2562,-0.261 -0.2581,-0.2582 -0.261,-0.4352 -0.261,-16.1643 0,-15.2409 0.0099,-15.9121 0.2386,-16.1191 0.2095,-0.1896 0.6637,-0.2159 3.7198,-0.2159 3.2234,0 3.4822,-0.0169 3.4942,-0.2284 0.0055,-0.0949 0.0166,-0.1381 0.0313,-0.141 0.0638,-0.0127 0.1936,0.733 0.2037,1.3111 0.0107,0.6119 -0.0323,0.7702 -0.2394,0.881 -0.1169,0.0625 -0.797,0.1128 -1.6088,0.1282 0.001,0.0005 0.0023,0.001 0.0032,0.0014 0.0473,0.0276 0.0865,0.0529 0.1257,0.0783 0.0329,-0 0.0658,0 0.0987,0 0.0284,-0.0847 0.1477,-0.0387 0.5631,0 0.032,0 0.0639,0 0.0959,0l0.0528,0c0.0252,0 0.0467,0 0.0489,0l0.0025,0c0,0.0003 0,0.0005 0,0.001 0.0092,0.0028 0.0137,0.0028 0.0222,0.0056 0.0025,0.0005 0.0052,0.001 0.0076,0.0014 0.0154,0.0042 0.0268,0.007 0.0405,0.0113 0.0106,0.0005 0.0175,0.0014 0.0283,0.0014 0.0079,0.0004 0.0127,0.0042 0.0161,0.0099 0.1356,0.0352 0.1935,0.0617 0.2858,0.2139l0.4043,0.0084 0.079,0.8267c0.063,0.6593 0.1494,0.897 0.427,1.1747l0.3482,0.348 6.6861,0.0384c3.6774,0.0213 6.8837,0.0028 7.125,-0.0412 0.7238,-0.1318 1.0623,-0.5812 1.0623,-1.411 0,-0.0486 0.001,-0.0949 0.0025,-0.1402 -0.0063,0.0014 -0.0122,0.0042 -0.0186,0.0056 -0.0887,0.0221 -0.1268,-0.001 -0.132,-0.0519 -0.0003,-0.0001 -0.0005,-0.0001 -0.001,-0.0003 -0.0305,0.6823 -0.1332,0.9036 -0.3765,1.1469 -0.2502,0.2502 -0.462,0.2577 -7.2831,0.2577L31.0601,26.4763l-0.3128,-0.3326c-0.3024,-0.3219 -0.3129,-0.4013 -0.3164,-2.4036 -0.0035,-2.0141 0.0049,-2.0797 0.3125,-2.408 0.3007,-0.3208 0.3832,-0.3375 1.6641,-0.3375l1.3478,0l0,-0.8846c0,-1.3595 -0.2031,-1.4613 2.8472,-1.4655zM46.829,23.9218c-0.0559,0.022 -0.0997,0.0404 -0.0637,0.0311 0.0389,-0.0099 0.0608,-0.0186 0.0918,-0.028 -0.0096,-0.001 -0.0187,-0.0014 -0.028,-0.0028zM46.9692,23.9366c0,0.0099 0.0001,0.0197 0.0003,0.0301 0.0015,-0.0003 0.0028,-0.0005 0.0044,-0.001 -0.0005,-0.0099 -0.0008,-0.0191 -0.0013,-0.0289 -0.0009,-0.0001 -0.0023,-0.0001 -0.0032,-0.0003zM47.4372,23.9677c0.0225,0.0153 0.0441,0.0301 0.0727,0.049 0.0286,-0.017 0.0494,-0.0313 0.0632,-0.0421 -0.0463,-0.0014 -0.0911,-0.0042 -0.1359,-0.007zM47.5938,23.9761c0.0194,0.0042 0.039,0.0084 0.0579,0.0155 0.092,0.0317 0.1579,0.0624 0.2164,0.0936 -0.0285,-0.0305 -0.0558,-0.0622 -0.0828,-0.094 -0.0039,-0.0014 -0.0076,-0.0028 -0.0115,-0.0056 -0.0637,-0.0028 -0.1184,-0.007 -0.18,-0.0099zM26.8022,23.9978 L26.0537,23.9981 26.0871,37.9963 26.1131,49.0259c0.0853,-0.0144 0.1731,-0.0256 0.2638,-0.0342 -0.077,-6.1194 -0.089,-21.6795 0.0008,-24.1142 -0.0012,-0.007 -0.002,-0.0113 -0.0032,-0.0194 0.0028,-0.0781 0.0018,-0.1098 0.0073,-0.144 0.001,-0.0056 0.0025,-0.0113 0.0038,-0.0166 0.0042,-0.0742 0.0086,-0.1312 0.0132,-0.1486 0.0137,-0.0508 0.027,-0.0926 0.0482,-0.1273 0.0155,-0.0918 0.0393,-0.1018 0.1993,-0.2705 0,0 0.0003,-0.0003 0.0003,-0.0003 0.0459,-0.0576 0.0962,-0.1102 0.1555,-0.1532zM49.172,23.9981c0.1,0.0833 0.1764,0.1525 0.2432,0.2164 0.0409,0.0204 0.0834,0.0419 0.1552,0.0747 0.0038,0.0113 0.0082,0.0213 0.0122,0.0318l0.1616,0.0028 0.0044,1.3475c0.0438,0.0908 0.097,0.1974 0.1807,0.3523 -0.0596,0.0408 -0.1196,0.0809 -0.1795,0.1213l0.0072,2.2696c0.0059,0.0443 0.0139,0.0884 0.0184,0.1327 0.0132,0.1335 -0.011,0.2708 0.0018,0.4045 0.0015,0.151 0.0037,0.3018 -0.0028,0.4527 -0.0003,0.007 -0.001,0.0056 -0.001,0.0127 0.002,0.0084 0.0039,0.0127 0.0059,0.0214 0.0585,0.2526 0.0425,0.5067 0.0433,0.7627l0,0.7931 0,0.6284 0,0.8813 0,0.4311 0,0.5677 0,0.4295 0,0.6258c0,0.1278 0,0.2555 0,0.3834 -0,0.1696 0,0.3392 0,0.5088 -0,0.0988 -0.0001,0.1979 0,0.2967 0.0003,0.116 -0.0004,0.2318 -0.001,0.3477 0.0015,0.1689 0.0097,0.3384 -0.0051,0.507 -0.0015,0.0152 0.0059,0.0339 -0.0041,0.0456 -0.0015,0.0014 -0.0023,0.001 -0.0028,0 0.0069,0.0246 0.0435,0.158 0.0189,0.068 0.0104,0.0376 0.0244,0.0745 0.0313,0.1129 0.0157,0.0864 0.0271,0.1736 0.0377,0.2608 -0.0053,-0.0372 -0.0128,-0.0856 -0.0201,-0.1091 -0.021,-0.0671 -0.0493,-0.1322 -0.0813,-0.1948 -0.002,-0.0028 -0.0068,-0.0113 -0.0094,-0.0155 0.0001,0.0328 0.0004,0.0631 0.0005,0.0956 0.0563,0.1602 0.1201,0.3179 0.1777,0.4777 -0.0015,0.1105 -0.0015,0.1109 -0.0076,0.2068 0.0198,0.08 0.0157,0.0891 0.0278,0.2559 0.0062,0.0228 0.0124,0.0458 0.0186,0.0686 0.0174,0.1112 0.0375,0.2219 0.052,0.3334 0.01,0.0769 -0.0077,0.1574 0,0.2345l0,0.4148 0,0.5239c0,0.1365 0,0.2731 0,0.4097 -0,0.1199 0,0.2398 0,0.3597 -0,0.0127 0,0.0265 0,0.0398 0.0089,-1.5448 0.015,-3.3009 0.015,-5.4968l0,-10.6949zM47.3936,24.0648c0.0051,0.0028 0.0113,0.0056 0.0163,0.0084 0.0015,-0.001 0.0034,-0.0014 0.0048,-0.0028 -0.0073,-0.0014 -0.0139,-0.0042 -0.0212,-0.0056zM46.9796,24.1025c-0.001,0.001 -0.0023,0.0028 -0.0032,0.0028 0.0012,-0 0.0024,0 0.0035,0 -0.0001,-0.001 -0.0001,-0.0028 -0.0003,-0.0028zM52.4655,36.0274c-0.0818,0.0084 -0.3057,0.1778 -0.5392,0.4112l-0.4851,0.4849 -0.0653,7.8908 -0.0044,0.5379c0.0322,0.0327 0.0658,0.0636 0.0971,0.0971 0.0369,-0.0377 0.074,-0.0752 0.1124,-0.1113 0.0595,-0.056 0.0065,0.0549 -0.053,0.1749 0.0086,0.0099 0.0177,0.0184 0.0263,0.028 0.0719,0.0808 -0.03,0.2229 -0.1876,0.3729l-0.0051,0.6355c0.0308,0.0504 0.0612,0.1014 0.0936,0.1504 0.0004,-0.021 0.0018,-0.017 0.0031,0.0056 -0.001,-0.0014 -0.002,-0.0028 -0.0031,-0.0056 -0.0003,0.0127 -0.0008,0.0227 0.001,0.0739 0.002,0.0563 0.0045,0.1126 0.0082,0.1688 0.0301,0.4894 0.015,0.0365 0.0252,0.7125 -0.0685,0.501 -0.0475,0.4473 -0.1433,0.7372l-0.0357,4.313 -0.2972,0.2973 -0.2972,0.2972 -7.9255,0.0708c-4.359,0.0389 -7.9548,0.0943 -7.9907,0.1232 -0.0359,0.0289 0.5221,0.0885 1.2399,0.1327 0.7179,0.0442 4.7411,0.0771 8.9406,0.0726l7.6354,-0.0084 -0.0222,-8.7886c-0.0122,-4.8338 -0.0582,-8.8249 -0.1022,-8.8689 -0.0055,-0.0056 -0.0143,-0.007 -0.026,-0.0056zM49.7858,36.5421c0,0.007 -0,0.0127 0,0.0183 0.0058,0.021 0.0101,0.0373 0.0212,0.0767 -0.0005,-0.007 -0.001,-0.0165 -0.002,-0.0214 -0.0058,-0.0234 -0.0104,-0.0473 -0.0176,-0.0704 -0.0004,-0.001 -0.001,-0.0014 -0.0015,-0.0028zM49.9332,37.4537c0.003,0.0113 0.003,0.0113 0.0053,0.0214 -0.001,-0.007 -0.0021,-0.0141 -0.0035,-0.0207 -0.0004,-0.0004 -0.0013,-0.0004 -0.0018,-0.001zM49.9732,37.7614c0.0025,0.0099 0.0051,0.0187 0.0076,0.028 -0.0005,-0.0042 -0.001,-0.0084 -0.0015,-0.0127 -0.001,-0.0056 -0.0075,-0.0213 -0.0061,-0.0158zM49.9895,37.8623c0.0003,0.0014 0.0005,0.0042 0.0008,0.0056 -0.0001,-0.0014 -0.0004,-0.0042 -0.0005,-0.0056zM49.9903,37.868c0.0027,0.0325 0.0037,0.0521 0.0082,0.1007 0.0015,0.0142 0.0005,-0.0286 -0.001,-0.0428 -0.0018,-0.0193 -0.0048,-0.0386 -0.0072,-0.0579zM50.0469,41.3841 L50.0466,41.3844c-0,0.0005 0,0.001 0,0.0014 0.0001,-0.0005 0.0001,-0.001 0.0003,-0.0014zM50.0466,41.3855c-0.0099,0.076 -0.0218,0.1487 -0.04,0.2164 -0.0003,0.0084 0.0001,0.0177 -0.0003,0.0265 -0.0005,-0.0056 -0.001,-0.0084 -0.0025,-0.0183 -0.0003,0.001 -0.0005,0.0014 -0.001,0.0028 0.0216,0.2091 0.0221,0.4138 -0.0405,0.6156 0,0.0317 -0,0.0632 0,0.0949 0,0.0684 0,0.137 0,0.2055 0.0237,0.0099 0.0485,0.0183 0.0716,0.0286 0.0045,-0.3735 0.0092,-0.7352 0.0128,-1.1726zM50.2253,49.8209c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0351 -0.007,-0.0708 -0.0153,-0.1053zM50.2503,51.0825c-0.1721,0.5503 -0.2915,0.8642 -0.4423,1.0605 0.0886,-0.0113 0.1607,-0.0228 0.1917,-0.0346 0.1047,-0.0401 0.1917,-0.4243 0.2506,-1.0258z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#5a5a5a\"/>\n  <path\n      android:pathData=\"m26.5057,21.5176c-2.2608,0 -3.6836,0.0514 -3.8779,0.1399 -0.7215,0.3287 -0.6814,-0.5873 -0.7189,16.5173 -0.0274,12.4736 0.0001,15.9157 0.1305,16.3149 0.1062,0.3253 0.2546,0.5217 0.5183,0.6773 0.1212,0 0.2425,0 0.3638,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0142,0.0014 0.0284,0.0042 0.0426,0.0056 0.3886,-0.001 0.8877,-0.0014 1.2392,-0.0028 0.0205,-0.007 0.0424,-0.0127 0.067,-0.016 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5216,0c1.648,-0.0214 2.4223,-0.0548 2.7565,-0.1056 0.033,-0.0211 0.0677,-0.0415 0.1032,-0.0535 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0215 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0087,-0.0142 0.0179,-0.028 0.0265,-0.0424 0.278,-0.9744 0.2511,-4.2258 0.2511,-16.3147 0,-11.4337 -0.0408,-15.953 -0.1458,-16.1584 -0.3734,-0.7302 -0.4065,-0.736 -4.3245,-0.739 -3.5359,-0.0028 -3.6219,0.0028 -3.6219,0.2582 0,0.2546 0.0871,0.261 3.5039,0.261 3.3963,0 3.5114,0.0084 3.7524,0.2748 0.2416,0.267 0.2485,0.7215 0.2485,16.1306l0,15.8561l-0.3082,0.2493c-0.2994,0.2425 -0.7336,0.2496 -15.3034,0.2496 -14.8212,0 -14.9982,-0.0028 -15.2562,-0.261 -0.2581,-0.2582 -0.261,-0.4352 -0.261,-16.1416 0,-14.5938 0.0174,-15.8998 0.2159,-16.1191 0.199,-0.2199 0.4908,-0.2386 3.7048,-0.2386 3.3788,0 3.4905,-0.0084 3.5567,-0.261 0.067,-0.2561 0.0039,-0.261 -3.5026,-0.261zM51.2202,36.4139 L50.8411,36.9807c0.1476,4.6903 0.3484,7.0128 0.2621,9.9576 -0.0473,1.6141 0.2254,3.5755 0.2073,5.7663l-0.2972,0.2972 -0.2972,0.2972 -8.5332,-0.173c-4.359,0.0389 -7.9073,-0.0793 -7.9907,0.1232 -0.0359,0.0289 -1.9171,1.1162 -1.1993,1.1605 0.7179,0.0442 7.5188,-0.103 11.7183,-0.1074l7.9046,-0.6116 0.7738,-13.8877c-0.13,-1.1232 1.6885,-7.6831 -0.3337,-4.8111z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#8c611e\"/>\n  <path\n      android:pathData=\"m26.5057,21.5176c-2.2608,0 -3.6836,0.0514 -3.8779,0.1399 -0.7215,0.3287 -0.6814,-0.5873 -0.7189,16.5173 -0.0274,12.4736 0.0001,15.9157 0.1305,16.3149 0.1062,0.3253 0.2546,0.5217 0.5183,0.6773 0.1212,0 0.2425,0 0.3638,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614l0.27,0 0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0 0.2842,0 0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0142,0.0014 0.0284,0.0042 0.0426,0.0056 0.3886,-0.001 0.8877,-0.0014 1.2392,-0.0028 0.0205,-0.007 0.0424,-0.0127 0.067,-0.016 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5216,0c1.648,-0.0214 2.4223,-0.0548 2.7565,-0.1056 0.033,-0.0211 0.0677,-0.0415 0.1032,-0.0535 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0215 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0087,-0.0142 0.0179,-0.028 0.0265,-0.0424 0.278,-0.9744 0.2511,-4.2258 0.2511,-16.3147 0,-11.4337 -0.0408,-15.953 -0.1458,-16.1584 -0.3734,-0.7302 -0.4065,-0.736 -4.3245,-0.739 -3.5359,-0.0028 -3.6219,0.0028 -3.6219,0.2582 0,0.2545 0.0877,0.2611 3.4914,0.2631 1.9203,0.001 3.5559,0.0452 3.6349,0.0978 0.437,0.2918 0.4438,0.5388 0.4438,16.3193 0,15.6384 -0.0003,15.6537 -0.2705,15.9973l-0.2705,0.3439L38.0962,54.8003c-13.8563,0 -15.1074,-0.0176 -15.3266,-0.2159 -0.2288,-0.207 -0.2386,-0.879 -0.2386,-16.1416 0,-14.6357 0.0174,-15.9449 0.2159,-16.1643 0.199,-0.2199 0.4908,-0.2386 3.7048,-0.2386 3.3788,0 3.4905,-0.0084 3.5567,-0.261 0.067,-0.2561 0.0039,-0.261 -3.5026,-0.261zM53.2367,41.2314c-0.4172,0.2981 -1.7749,1.3392 -2.1418,1.6871 -0.5318,0.5439 0.2511,-1.0092 -0.0425,0.2253 -0.7728,2.6336 0.1302,3.684 -0.0546,7.3563l-0.1207,2.7988 -5.3027,-0.0918c-2.9438,0.0359 1.2893,-0.4567 -2.2702,-0.0391 -0.0538,0.0539 -0.5044,0.5468 -1.7659,1.2344l11.6076,-0.198 0.1876,-12.7774c0.002,-0.1375 -0.0358,-0.1928 -0.0969,-0.1958z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#885d1b\"/>\n  <path\n      android:pathData=\"m26.5057,21.5176c-2.2608,0 -3.6836,0.0514 -3.8779,0.1399 -0.7215,0.3287 -0.6814,-0.5873 -0.7189,16.5173 -0.0274,12.4736 0.0001,15.9157 0.1305,16.3149 0.1062,0.3253 0.2546,0.5217 0.5183,0.6773 0.1212,0 0.2425,0 0.3638,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007 0.0862,0 0.1725,0 0.2587,0l0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0195,0.0028 0.0531,0.0113 0.0854,0.0204 0.0511,0.0028 0.0696,0.0028 0.1509,0.0127 0.0142,0.0014 0.0284,0.0042 0.0426,0.0056 0.3886,-0.001 0.8877,-0.0014 1.2392,-0.0028 0.0205,-0.007 0.0424,-0.0127 0.067,-0.016 0.1013,-0.0159 0.2043,-0.007 0.3064,-0.007 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0 0.1451,0 0.2901,0 0.4352,0l0.477,0 0.5871,0 0.5216,0c1.648,-0.0214 2.4223,-0.0548 2.7565,-0.1056 0.033,-0.0211 0.0677,-0.0415 0.1032,-0.0535 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0101,-0.0042 0.0211,-0.007 0.0313,-0.0113 0.0005,-0.0003 -0.0003,-0.0014 0.0003,-0.0014 0.0304,-0.016 0.0182,-0.0056 0.028,-0.0145 0.0204,-0.0099 0.0302,-0.0246 0.0449,-0.0411 0.0234,-0.0284 0.0508,-0.0491 0.0785,-0.0729 0.0005,-0.0005 -0.0025,0.0014 -0.0025,0.0003 -0.0001,-0.0014 0.001,-0.0028 0.002,-0.0028 0.0111,-0.0113 0.0226,-0.0215 0.0339,-0.0321 0.0072,-0.0056 0.0143,-0.0113 0.0214,-0.0176 0.008,-0.007 0.0142,-0.0146 0.0219,-0.0214 0.0334,-0.0294 0.0489,-0.0267 0.0319,-0.029 0.0209,-0.0229 0.049,-0.0396 0.0691,-0.0635 0.0101,-0.0127 -0.0021,-0.0028 0.0018,-0.0113 0.0049,-0.0099 0.011,-0.0198 0.0163,-0.0298 0.0087,-0.0142 0.0179,-0.028 0.0265,-0.0424 0.278,-0.9744 0.2511,-4.2258 0.2511,-16.3147 0,-11.4337 -0.0408,-15.953 -0.1458,-16.1584 -0.3734,-0.7302 -0.4065,-0.7358 -4.3245,-0.7388 -3.3635,-0.0028 -3.6218,0.0127 -3.6219,0.2253 -0.0001,0.2128 0.2367,0.2257 3.4585,0.1897l3.4588,-0.0387 0.3263,0.3265 0.3266,0.3266l0,15.8494c0,15.4109 -0.0069,15.858 -0.2496,16.1576l-0.2496,0.3082L38.0628,54.8657 22.8996,54.8657l-0.2496,-0.3086c-0.2426,-0.2997 -0.2496,-0.7467 -0.2496,-16.1577l0,-15.8494l0.3245,-0.3248 0.3245,-0.3245 3.4539,0.0369c3.1977,0.0341 3.4584,0.0197 3.5136,-0.1917 0.0557,-0.2134 -0.1747,-0.2284 -3.5113,-0.2284z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#7a5927\"/>\n  <path\n      android:pathData=\"m26.4527,21.5176c-2.3483,0 -3.4922,0.0479 -3.7295,0.156 -0.7504,0.3419 -0.7143,-0.5143 -0.7143,16.8803l0,16.0052l0.3816,0.3813 0.2264,0.2266c0.1016,0 0.2031,0 0.3046,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0.0001 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.1748,0.0491 -0.34,0.1962 0.261,-0.0614 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.0218 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0.0001 0.0015,-0 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0612 0.3224,-0.1122 0.4938,-0.1016l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,-0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,-0 0.3067,0 0.2376,0 0.4752,0 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0466 0.1393,0.0238 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0115,0.0014 0.0226,0.0056 0.0342,0.007 0.0659,0.0056 0.1323,0.0042 0.1981,0.0113 0.0532,0.0056 0.1046,0.0155 0.1558,0.0258l11.295,0c0.038,-0.0251 0.0793,-0.0511 0.1213,-0.0653 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0717,-0.0351 0.0336,-0.0193 0.0918,-0.0355l0.0005,-0.001c-0.0014,0.0003 -0.0028,0.001 -0.0023,0.0003 0.0012,-0.001 0.0018,-0.001 0.0041,-0.0014l0.0005,-0.001c-0.0115,0.0042 -0.0139,0.0056 0.01,-0.0099l0.2294,-0.2297 0.3816,-0.3814l0,-16.1219 0,-16.1219l-0.3148,-0.3661 -0.3148,-0.3663 -3.6135,-0.0384c-2.9139,-0.0311 -3.6028,-0.0056 -3.5574,0.1305 0.0434,0.1305 0.8387,0.1839 3.4832,0.2343 3.408,0.0649 3.4286,0.067 3.6765,0.3735 0.2423,0.2995 0.2493,0.751 0.2493,16.1372 0,15.3908 -0.0069,15.8376 -0.2496,16.1372l-0.2496,0.3082L38.0628,54.8668 22.8996,54.8668l-0.2496,-0.3083c-0.2426,-0.2996 -0.2496,-0.7467 -0.2496,-16.1576l0,-15.8494l0.3204,-0.3204 0.3202,-0.3204l3.3996,0c2.9347,0 3.3994,-0.0267 3.3994,-0.1958 0,-0.169 -0.4634,-0.1958 -3.3874,-0.1958z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#7a5116\"/>\n  <path\n      android:pathData=\"m49.5582,21.5087c-1.6814,0.0042 -3.2763,0.0631 -3.24,0.1721 0.0412,0.1239 0.8903,0.1714 3.5273,0.1978l3.4725,0.0349 0.2689,0.3416c0.2679,0.3408 0.2689,0.386 0.2689,16.1882l0,15.8466l-0.3204,0.3202 -0.3204,0.3204L38.0689,54.9305c-14.8273,0 -15.1516,-0.0056 -15.4,-0.2537 -0.1396,-0.1395 -0.2885,-0.4479 -0.3311,-0.6852 -0.0429,-0.2373 -0.0607,-7.4293 -0.04,-15.9822 0.0365,-15.0808 0.0451,-15.559 0.2863,-15.8255 0.241,-0.2663 0.3562,-0.2748 3.7524,-0.2748 3.0737,0 3.5039,-0.0246 3.5039,-0.2016 0,-0.178 -0.4165,-0.1972 -3.535,-0.1631l-3.535,0.0384 -0.3806,0.4262 -0.3806,0.4262 0.0044,15.954c0.0049,17.1322 -0.0253,16.3685 0.6623,16.7516l0.0005,0c0.0167,0.0099 0.0457,0.018 0.0862,0.0263 0.053,0 0.1061,0 0.1591,0 0.183,0 0.3659,0.0001 0.5488,0 0.2092,-0.0001 0.4184,-0.0004 0.6276,0.0005 0.1467,0 0.2934,-0.0056 0.44,0.0005 0.2029,0.0113 0.0607,0.0001 -0.0377,0.0277 -0.054,0.0152 -0.1042,0.0391 -0.1341,0.0581 0.0301,0.0005 0.0551,0.0014 0.0862,0.0014 0.0664,-0.0221 0.1645,-0.0597 0.309,-0.1216 0.09,0 0.1799,0 0.27,0l0.3286,0c0.1231,0 0.2462,-0 0.3694,0 0.1692,0 0.3383,0.0001 0.5075,0 0.0779,0.0003 0.1559,0.0014 0.2338,-0.001 0.1052,-0.0014 0.213,-0.0042 0.3169,0.0127 -0.0181,0.0056 -0.0725,0.022 -0.054,0.0176 0.0364,-0.0084 0.0711,-0.0244 0.1081,-0.0301 0.0774,-0.0113 0.1557,-0.017 0.2338,-0.0238 0.148,-0.0391 0.2264,-0.0442 0.3941,-0.0396 0.1519,-0.0003 0.304,-0.007 0.4558,0.0014 0.0127,-0.007 0.0139,-0.0056 0.0089,0.0003 0.0008,0 0.0015,-0.0001 0.0023,0 0.01,0.0005 0.0094,0.001 0.0176,0.0014 0.1576,-0.0614 0.3224,-0.1122 0.4938,-0.1018l0.2896,0 0.272,0 0.2004,0c0.0947,0 0.1895,0 0.2842,0l0.2498,0c0.0718,0 0.1438,0.0001 0.2157,0.0003 0.1182,0.0001 0.2364,-0.0004 0.3546,-0.0005 0.077,0.001 0.1541,0.0056 0.231,-0.0014 0.1096,-0.007 0.1754,-0.0146 0.2937,0.0056 0.0376,0.007 0.0744,0.0173 0.1109,0.0289l0.4224,0 0.365,0 0.3199,0 0.339,0c0.1022,0 0.2044,0 0.3067,0 0.2376,0 0.4752,0.0001 0.7128,0 0.1462,-0.0001 0.2925,0.0001 0.4387,0 0.279,-0.0003 0.558,-0.0014 0.8369,0.001 0.098,0.001 0.1962,0.0028 0.2942,-0.0003 0.0371,-0.001 0.1765,-0.0084 0.2027,-0.0084 0.0552,-0.0001 0.1103,0.0028 0.1654,0.0042 0.0499,0.007 0.1,0.0127 0.1496,0.0222 0.0232,0.0042 0.0816,0.0056 0.0864,0.0141 0.0225,0 0.0504,0 0.052,0 0.027,0 0.0541,-0 0.0811,0 0.184,0 0.368,-0.0014 0.5519,0 0.2709,0.0028 0.5419,0.0042 0.8127,-0.0084 0.1629,0.001 0.2031,-0.0014 0.3681,0.0127 0.0646,0.0056 0.1294,0.0113 0.1935,0.0217 0.0049,0.001 -0.0176,0.007 -0.0128,0.007 0.0749,0.0056 0.1502,-0.0042 0.2251,0 0.109,0 0.2179,0 0.3268,0 0.1921,0.0001 0.3843,0.0005 0.5764,-0.0003 0.1874,-0.0005 0.3747,0.0014 0.5621,0.0028 0.2407,-0.007 0.4835,-0.0287 0.7212,0.0245 0.3038,0.0363 0.084,0.0056 0.3222,0.0467 0.1393,0.0236 0.286,0.0363 0.4158,0.0966 0.043,0.0056 0.0864,0.0084 0.129,0.0166 0.0051,0.001 0.0063,0.0099 0.0115,0.0099 0.0254,0.0028 0.051,-0.0014 0.0765,-0.0028 0.1265,-0.0042 0.2532,-0.0084 0.3796,0.0028 0.0397,0.0042 0.0795,0.007 0.119,0.0127 0.0015,0.0003 0.0031,0.0005 0.0046,0.001l11.799,-0.0224c0.0003,-0.0001 0.0005,-0.0001 0.0008,-0.0003 0.0175,-0.0056 0.0357,-0.0099 0.0535,-0.0142 0.0005,-0.0001 0.001,-0.0001 0.0015,-0.0003 0.0087,-0.0056 0.0357,-0.0177 0.0408,-0.0239 0.0748,-0.0365 0.0254,-0.0165 0.0956,-0.0365 0.002,-0.0005 -0.007,0.0014 -0.0056,0.0005 0.003,-0.0028 0.0139,-0.0056 0.0101,-0.0042 -0.0149,0.0014 -0.0403,0.0222 0.0132,-0.0141 0.0357,-0.0218 0.0333,-0.0221 0.0762,-0.0405 0.0051,-0.0014 0.0103,-0.0042 0.0156,-0.0056l0.1958,-0.2277 0.3151,-0.3661l0,-15.9879c0,-17.5949 0.048,-16.579 -0.7987,-16.8742 -0.3106,-0.1083 -2.0789,-0.1582 -3.7603,-0.1537z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#714b14\"/>\n  <path\n      android:pathData=\"m26.6994,21.897c-0.6387,-0 -1.2829,0.0056 -1.7786,0.0166 -0.9913,0.0222 -0.2167,0.0404 1.7215,0.0405 1.9382,0.0001 2.7494,-0.018 1.8025,-0.0403 -0.4734,-0.0113 -1.1067,-0.0168 -1.7454,-0.0168zM51.2825,21.9012c-0.3707,0.001 -0.7927,0.0042 -1.2736,0.0084l-3.5276,0.0286 3.6326,0.0694c4.0264,0.0769 3.9054,0.048 4.0716,0.973 0.0397,0.2208 0.055,7.4401 0.0342,16.0427l-0.0372,15.4117c0.0135,-0.0234 0.0279,-0.0466 0.0393,-0.0711 0.0025,-0.0056 0.0038,-0.0113 0.0063,-0.0168 0.0179,-0.0365 0.0194,-0.0369 0.039,-0.067 -0.003,0.0042 -0.0101,0.0179 -0.0084,0.0127 0.0025,-0.007 0.0076,-0.0141 0.011,-0.0204 0.001,-0.0014 0.0041,-0.0056 0.0025,-0.007 -0.0015,-0.0014 -0.0046,0.007 -0.0041,0.0056 0.0052,-0.0255 0.0142,-0.0501 0.0212,-0.0752 0.0065,-0.0234 0.0163,-0.0455 0.0247,-0.0681l0.0031,-1.413c-0.0015,-0.007 -0.003,-0.0141 -0.0046,-0.0211 -0.0025,-0.0099 -0.0052,-0.0204 -0.0079,-0.0305 -0.0035,-0.0166 -0.0069,-0.0331 -0.0104,-0.0497 -0.0146,-0.0679 -0.0263,-0.1364 -0.0357,-0.2052 -0.0089,-0.032 -0.0079,-0.0649 -0.0101,-0.0977 -0.0001,-0.001 -0.0015,-0.0014 -0.0015,-0.0028 -0.0023,-0.0148 -0.0028,-0.0275 -0.0035,-0.0405 -0.0001,0.0014 0.0006,0.0056 -0.0013,0.0042 -0.01,-0.0084 -0.0101,-0.0234 -0.0132,-0.0358 -0.0061,-0.0235 -0.0115,-0.0473 -0.0145,-0.0714 -0.0061,-0.0505 0.0014,-0.1026 0.0048,-0.1532 0.0025,-0.0915 -0.0008,-0.1831 -0.0008,-0.2745 0.0005,-0.0567 0.0004,-0.1133 0.0003,-0.17 -0,-0.0476 0,-0.0952 0,-0.1428 -0.001,-0.0296 0.003,-0.0521 0.0094,-0.0786 -0.003,-0.0367 0.0154,-0.0869 0.0219,-0.1071 0.001,-0.007 0.0045,-0.017 0.0038,-0.0232 0.0179,-0.0733 0.0066,-0.0486 0.0275,-0.09 -0.001,0.0014 -0.0042,0.007 -0.0032,0.0056 0.0013,-0.0042 0.0031,-0.0084 0.0048,-0.0113 0.001,-0.007 0.0018,-0.0127 0.0032,-0.0191 -0.0001,0.0001 0,-0.0005 0,-0.0005 0.0132,-0.0465 0.0211,-0.0811 0.0347,-0.104l0.0265,-12.1021c0.0255,-11.7753 -0.0045,-15.9966 -0.116,-16.2662 -0.2659,-0.6438 -0.3537,-0.7335 -2.9487,-0.725zM22.9794,22.059c-0.0164,-0.001 -0.0441,0.0056 -0.0826,0.0207 -0.1184,0.0455 -0.3076,0.2779 -0.4209,0.5165 -0.1839,0.3875 -0.206,2.0993 -0.206,16.048 0,14.9146 -0.0103,16.1641 0.4351,16.5234 0.0721,0 0.1443,0 0.2164,0l0.1239,0l-0.194,-0.1942 -0.3204,-0.3202l0,-16.0187l0.3153,-15.7234 -0.1587,-0.2243 0.134,-0.3802c0.1561,-0.1661 0.2071,-0.2439 0.1578,-0.2475zM50.2253,49.8212c-0.003,-0.0113 -0.0023,0.0245 -0.0013,0.0367 0.003,0.0286 0.0089,0.0566 0.013,0.0849 0.0001,0.0014 0.0005,0.0028 0.0008,0.0042 0.001,-0.007 0.0017,-0.0141 0.0028,-0.0201 -0.0051,-0.0352 -0.007,-0.0708 -0.0153,-0.1053zM50.4083,50.3994c-0.0138,0.0208 -0.0368,0.0529 -0.0439,0.0648 0.0072,0.0436 0.011,0.0871 0.011,0.1302 0.0113,-0.0622 0.0223,-0.126 0.0329,-0.195zM50.3313,50.822c-0.0257,0.0843 -0.0478,0.1538 -0.0711,0.2287 0.0237,-0.0504 0.0476,-0.1278 0.0711,-0.2287zM54.2872,51.105c-0.0005,0.0014 -0.0008,0.0028 -0.0015,0.0056 0.0004,-0.001 0.0009,-0.0014 0.0013,-0.0028 0.0003,-0.001 0.0005,-0.0028 0.0003,-0.0028zM41.6765,55.2933c0.0189,0.001 0.0378,0.0014 0.0566,0.0028 0.1287,0.0127 0.2473,0.0528 0.3768,0.0597 -0.0482,0.018 -0.0245,0.007 0.0785,0.007l0.2587,0 0.1356,0c0.2822,0 0.5644,-0.0003 0.8466,-0.0003 0.2139,0 0.4277,0.0005 0.6416,0 0.2247,-0.0005 0.4493,0.001 0.674,0.0014 0.2927,-0.0028 0.5906,-0.03 0.8792,0.028 0.0301,0.0028 0.0601,0.0056 0.09,0.0099 0.0184,0.0028 0.05,0.0099 0.0805,0.0191l1.5124,0.0005c0.0988,-0.0142 0.1991,-0.007 0.2985,-0.0056 0.1277,-0.0003 0.2554,-0.0005 0.3831,-0.0005 0.1452,0.0001 0.2907,0 0.4359,0l0.4352,0 0.477,0 0.5871,0 0.5881,0 0.6388,0 0.5119,0 0.3039,0 0.3189,0 0.3169,0c0.0286,0 0.0571,0.0014 0.0857,0 0.0225,-0.001 0.0447,-0.0056 0.067,-0.007 0.0908,-0.0159 0.1815,-0.0336 0.2715,-0.0538 0.0402,-0.0084 0.081,-0.0099 0.1216,-0.0141 0.0083,-0.001 0.0167,-0.001 0.0247,-0.0028 0.01,-0.0028 -0.0065,-0.0042 -0.0066,-0.0042 0.0198,-0.0127 0.0409,-0.0272 0.0627,-0.04z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#6e4913\"/>\n  <path\n      android:pathData=\"m34.4529,17.5768c-0.6886,0 -1.2565,0.5677 -1.2565,1.2563l0,1.8635c0,0.0776 0.0096,0.1528 0.0234,0.2266 0.0423,0.0042 0.0773,0.007 0.1318,0.0127 0.0675,0.007 -0.0674,-0.007 0,0 0.1041,-0.0001 -0.0368,0.0876 0.6998,0.0597l0,0c0.0001,-0.1298 -0.096,-0.627 0.007,-0.7058l-0.0289,-1.4569c-0.0049,-0.2411 0.1821,-0.4229 0.4232,-0.4229L41.9071,18.4098c0.2411,0 0.4219,0.1818 0.4219,0.4229l0,1.8635c0,0.0824 -0.0222,0.1567 -0.0597,0.2203l0,0.1754c0.0717,-0 0.1434,0.0001 0.2152,0 0.1618,-0.0001 0.3238,0.001 0.4856,0.0014 0.0427,-0.0005 1.9523,0.04 1.9951,0.0391 0.7819,-0.4945 -1.8018,-0.6113 -1.8018,-0.436l0,-1.8635c0,-0.6886 -0.5677,-1.2563 -1.2563,-1.2563zM39.6573,21.8171c0.001,0.0198 0.0015,0.0412 0.002,0.0629 0.0049,-0.0211 0.0103,-0.0419 0.0153,-0.0629 -0.0058,0 -0.0115,0 -0.0173,0z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.8336795\"\n      android:fillColor=\"#5c5c5c\"\n      android:strokeColor=\"#00000000\"\n      android:fillType=\"nonZero\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m23.6891,21.1612c-0.5374,0 -1.0289,0.2046 -1.4005,0.5398 -0.4254,0.3838 -0.6937,0.9388 -0.6937,1.5541l0,30.2504c0,0.385 0.105,0.7464 0.2879,1.057 0.3646,0.6194 1.0386,1.0369 1.8063,1.0369L52.5109,55.5994c1.1526,0 2.0942,-0.9412 2.0942,-2.0939l0,-30.2504c0,-1.1527 -0.9416,-2.0939 -2.0942,-2.0939l-6.4653,0c0.1395,0.279 0.22,0.5921 0.22,0.9241l0,2.1543c0.0024,0 0.0059,-0.0003 0.0094,-0.0003l0,-2.1541c0,-0.0249 -0.0101,-0.0469 -0.011,-0.0717l6.2468,0c0.6952,0 1.1405,0.5214 1.1409,1.2166l0.0141,30.2753c0.0003,0.6952 -0.4597,1.2415 -1.1549,1.2415l-28.766,-0.0014c-0.6953,-0 -1.1074,-0.4967 -1.2417,-1.2415l-0.0558,-30.2483c-0.0013,-0.6952 0.5465,-1.2415 1.2417,-1.2415l5.9343,0c-0.001,0.0246 -0.011,0.0467 -0.011,0.0717l0,2.0636c0.0031,0.0014 0.0065,0.0028 0.0094,0.0056l0,-2.0684c0,-0.3319 0.0805,-0.6452 0.22,-0.9241zM45.3505,21.6382c0.0031,0.0258 0.0084,0.0512 0.0161,0.076 0.0364,0.117 0.0561,0.2409 0.0561,0.3712l0,3.3563c0,0.6923 -0.5438,1.2361 -1.2361,1.2361l-12.4852,0c-0.6923,0 -1.2364,-0.5439 -1.2364,-1.2361l0,-3.3563c0,-0.1304 0.0197,-0.2543 0.0561,-0.3712 0.0075,-0.0245 0.0128,-0.0497 0.0158,-0.0752 -0.0518,0.1382 -0.0813,0.2881 -0.0813,0.4464l0,3.3563c0,0.6972 0.5485,1.2455 1.2458,1.2455l12.4852,0c0.6973,0 1.2455,-0.5483 1.2455,-1.2455l0,-3.3563c0,-0.1585 -0.0296,-0.3087 -0.0816,-0.4471zM46.2656,25.1334l0,0.0084c0.0032,0.001 0.0061,0.0028 0.0094,0.0042l0,-0.0056c-0.0031,-0.0028 -0.0062,-0.0056 -0.0094,-0.007zM46.2656,25.168l0,0.2735c0,1.1447 -0.9345,2.0791 -2.0791,2.0791l-12.4852,0c-0.1627,0 -0.3205,-0.0207 -0.4726,-0.0563 0.0042,0.0042 0.0084,0.0084 0.0128,0.0127 0.0001,0.0003 0.0001,0.0005 0.0003,0.001 0.1482,0.0336 0.3016,0.0528 0.4596,0.0528l12.4852,0c1.1497,0 2.0886,-0.9389 2.0886,-2.0886l0,-0.2702c-0.0031,-0.001 -0.0063,-0.0014 -0.0094,-0.0028z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.8522618\"\n      android:fillColor=\"#704b15\"\n      android:strokeColor=\"#00000000\"\n      android:fillType=\"nonZero\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m26.0361,23.5672c-0.235,-0.0003 -0.0115,0.1953 -0.012,0.4302l-0.1846,28.5467c0.0005,0.2349 0.3757,-0.0884 0.6106,-0.0887l24.232,0.1414c0.2345,-0.0003 -0.1868,-0.3317 -0.1864,-0.5661l0,-28.0333c-0.0003,-0.2346 0.3864,-0.6381 0.1519,-0.6384l-4.7988,0.2134l0,0.8507l3.796,0l0,27.1817L26.8758,51.6049l0,-27.1817l3.1628,0l0,-0.8507z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.8509689\"\n      android:fillColor=\"#5c5c5c\"\n      android:strokeColor=\"#00000000\"\n      android:fillType=\"nonZero\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"M29.9721,40.0068l17.153,0l0,1.681l-17.153,0z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.79424924\"\n      android:fillColor=\"#c3c3c3\"\n      android:strokeColor=\"#00000000\"\n      android:fillAlpha=\"0.6784314\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m30.0183,30.2703l0,1.6809L46.9274,31.9512l0,-1.6809z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.78858197\"\n      android:fillColor=\"#c3c3c3\"\n      android:strokeColor=\"#00000000\"\n      android:fillAlpha=\"0.6784314\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m30.0183,33.5333l0,1.6809L46.2353,35.2142l0,-1.6809z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.77227277\"\n      android:fillColor=\"#c3c3c3\"\n      android:strokeColor=\"#00000000\"\n      android:fillAlpha=\"0.6784314\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m30.0183,36.7305l0,1.6812L44.5872,38.4117l0,-1.6812z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.73198014\"\n      android:fillColor=\"#c3c3c3\"\n      android:strokeColor=\"#00000000\"\n      android:fillAlpha=\"0.6784314\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m30.0183,43.2899l0,1.6809l10.5476,0l0,-1.6809z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.62282085\"\n      android:fillColor=\"#c3c3c3\"\n      android:strokeColor=\"#00000000\"\n      android:fillAlpha=\"0.6784314\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m21.9217,51.052c-0,0.0003 0,0.0005 0,0.001 0.0001,-0.0001 0.0001,-0.0004 0.0003,-0.0005zM21.9217,51.0529c-0.0052,0.007 -0.0104,0.0141 -0.0156,0.0204 0.0001,0.0144 0.0003,0.0298 0.0005,0.0428 0.0039,-0.0042 0.0083,-0.0084 0.0122,-0.0127 0.001,-0.016 0.0025,-0.0343 0.0031,-0.05zM25.8004,49.0925 L23.0357,51.8184c-0.4351,0.4613 0.1054,-0.1907 0.0844,0.7401l0.2197,1.4319 4.2556,0.2944c0.0371,-0.007 -1.1417,0.0056 -1.6366,-0.0228 -0.224,-0.1026 0.8023,-0.4669 1.8445,-1.1308l-2.121,0.1067 0.3502,-0.4984c-0.2329,-0.2001 -0.3928,-0.6315 -0.445,-1.1034 -0.3678,-0.315 0.2565,-2.7265 0.1832,-3.1011z\"\n      android:strokeWidth=\"0.13051933\"\n      android:fillColor=\"#a06e20\"/>\n  <path\n      android:pathData=\"m23.7169,21.8893c-0.6521,0 -1.19,0.5396 -1.19,1.194l0,30.5245c0,0.6544 0.5379,1.1946 1.19,1.1946L52.5068,54.8023c0.6521,0 1.1902,-0.5402 1.1902,-1.1946L53.6971,23.0833c0,-0.6544 -0.5382,-1.194 -1.1902,-1.194l-6.283,0c0.0055,0.0612 0.0094,0.1225 0.0094,0.1851l0,0.6432l6.2736,0c0.2092,0 0.3648,0.1558 0.3648,0.3658l0,30.5245c0,0.21 -0.1555,0.3661 -0.3648,0.3661L23.7169,53.9738c-0.2093,0 -0.3648,-0.156 -0.3648,-0.3661L23.3521,23.0833c0,-0.21 0.1555,-0.3658 0.3648,-0.3658l5.8989,0l0,-0.6432c0,-0.0625 0.0045,-0.1239 0.0097,-0.1851z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.82654744\"\n      android:fillColor=\"#c58727\"\n      android:strokeColor=\"#00000000\"\n      android:fillType=\"nonZero\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m43.1367,52.6547c4.3235,-2.1373 5.3839,-3.0835 5.4818,-3.1325 0.0979,-0.049 1.9392,-2.054 1.9392,-2.054l0.2419,2.4833 -0.1175,2.6455c0,0 -6.0771,0.4003 -6.2565,0.3514 -0.1795,-0.049 -1.2889,-0.2937 -1.2889,-0.2937z\"\n      android:strokeLineJoin=\"miter\"\n      android:strokeWidth=\"0.0927176\"\n      android:fillColor=\"#000000\"\n      android:strokeColor=\"#000000\"\n      android:fillAlpha=\"0.37151718\"\n      android:strokeLineCap=\"butt\"/>\n  <path\n      android:pathData=\"m25.61,22.7107a0.4284,0.426 90.0002,0 0,-0.4256 0.4278l0,29.731a0.4284,0.426 90.0002,0 0,0.4256 0.4278L50.922,53.2973a0.4284,0.426 90.0002,0 0,0.4263 -0.4278l0,-29.731a0.4284,0.426 90.0002,0 0,-0.4263 -0.4278l-4.7269,0l0,0.8566l4.3008,0l0,28.8734L26.0361,52.4407l0,-28.8734l3.8907,0l0,-0.8566z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.8543331\"\n      android:fillColor=\"#858984\"\n      android:strokeColor=\"#00000000\"\n      android:fillType=\"nonZero\"\n      android:strokeLineCap=\"butt\"/>\n  <path\n      android:pathData=\"m41.762,52.4966c0.2423,0 0.8076,0.1846 1.1767,-0.3461 0.3692,-0.5307 0.3297,-1.5075 0.3378,-2.935 0.0082,-1.4275 -0.0379,-2.2506 -0.0379,-2.2506l2.2727,-0.0113c0,0 2.8264,0.1326 3.9685,-0.4788 1.1421,-0.6114 1.1883,-1.4074 1.1883,-1.4074l0.3115,-0.0808c0,0 0.1615,2.9533 0.1269,2.9072 -0.0346,-0.0462 -0.4086,-0.5219 -0.4086,-0.5219l-0.1567,0.2678c0,0 -1.2575,2.0536 -3.1263,3.2765 -1.8689,1.2229 -2.776,1.595 -2.776,1.595z\"\n      android:strokeLineJoin=\"miter\"\n      android:strokeWidth=\"0.0927176\"\n      android:fillColor=\"#858984\"\n      android:strokeColor=\"#00000000\"\n      android:strokeLineCap=\"butt\"/>\n  <path\n      android:pathData=\"m43.9193,51.8851c0.2538,-0.6114 0.1154,-3.2648 0.1269,-3.657 0.0115,-0.3922 -0.0346,-0.5422 -0.0346,-0.5422 0,0 2.8841,0.1268 4.1646,-0.1384 1.2805,-0.2653 1.7535,-0.4499 1.7535,-0.4499 0,0 -1.1421,2.0535 -3.4378,3.4032 -2.2957,1.3498 -2.5726,1.3844 -2.5726,1.3844z\"\n      android:strokeLineJoin=\"miter\"\n      android:strokeWidth=\"0.0927176\"\n      android:fillColor=\"#a1a3a0\"\n      android:strokeColor=\"#00000000\"\n      android:strokeLineCap=\"butt\"/>\n  <path\n      android:pathData=\"m45.0383,48.5742c0.3692,-0.0231 1.9958,-0.0113 2.5265,-0.1615 0.5307,-0.15 0.5422,-0.1846 0.5422,-0.1846 0,0 -1.4651,1.2113 -2.2727,1.8112 -0.8075,0.5999 -0.9114,0.6345 -0.9114,0.5537 0,-0.0808 0.1154,-2.0189 0.1154,-2.0189z\"\n      android:strokeLineJoin=\"miter\"\n      android:strokeWidth=\"0.0927176\"\n      android:fillColor=\"#b3b3b3\"\n      android:strokeColor=\"#00000000\"\n      android:strokeLineCap=\"butt\"/>\n  <path\n      android:pathData=\"m31.7012,20.0062c-1.1447,0 -2.0794,0.9344 -2.0794,2.0791l0,3.3563c0,1.1447 0.9347,2.0791 2.0794,2.0791l12.4852,0c1.1447,0 2.0791,-0.9345 2.0791,-2.0791l0,-3.3563c0,-1.1447 -0.9345,-2.0791 -2.0791,-2.0791l-1.4095,0c-0.2302,0 -0.4167,0.1866 -0.4168,0.4168l0,0.2733c0,0.0512 -0.0099,0.0988 -0.025,0.1435l0.8293,0 0.5573,0 0.4647,0c0.6973,0 1.2455,0.5483 1.2455,1.2455l0,3.3563c0,0.6973 -0.5483,1.2455 -1.2455,1.2455l-12.4852,0c-0.6973,0 -1.2458,-0.5483 -1.2458,-1.2455l0,-3.3563c0,-0.6972 0.5485,-1.2455 1.2458,-1.2455l1.5553,0l0.7949,0.156c-0.0152,-0.0446 0.0052,-0.0113 0.0052,-0.0113l-0.0039,-0.8254c-0,-0.2302 -0.1784,-0.1531 -0.4086,-0.1531z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.8336793\"\n      android:fillColor=\"#5c5c5c\"\n      android:strokeColor=\"#00000000\"\n      android:fillType=\"nonZero\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"M31.4741,22.2162L44.6202,22.2162A0.6603,0.6603 135,0 1,45.2805 22.8765L45.2805,23.7248A0.6603,0.6603 45,0 1,44.6202 24.3851L31.4741,24.3851A0.6603,0.6603 45,0 1,30.8138 23.7248L30.8138,22.8765A0.6603,0.6603 135,0 1,31.4741 22.2162z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.8883065\"\n      android:fillColor=\"#b7b7b0\"\n      android:strokeColor=\"#00000000\"\n      android:strokeLineCap=\"butt\"/>\n  <path\n      android:pathData=\"M31.144,22.7238L44.9503,22.7238A0.3301,0.3301 45.0003,0 1,45.2805 23.0539L45.2805,23.4781A0.3301,0.3301 134.9996,0 1,44.9503 23.8082L31.144,23.8082A0.3301,0.3301 45.0003,0 1,30.8138 23.4781L30.8138,23.0539A0.3301,0.3301 134.9996,0 1,31.144 22.7238z\"\n      android:strokeLineJoin=\"round\"\n      android:strokeWidth=\"0.6281276\"\n      android:fillColor=\"#cacabf\"\n      android:strokeColor=\"#00000000\"\n      android:strokeLineCap=\"butt\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_stop_black_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"@android:color/black\">\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M6,6h12v12H6z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_visibility.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"@android:color/white\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/ic_volume_up_black_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"@android:color/black\"\n        android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/inset_divider_64dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<inset xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:insetLeft=\"64dp\">\n\n    <shape>\n        <size android:height=\"1dp\" />\n        <solid android:color=\"@color/color_on_surface_low_emphasis\" />\n    </shape>\n\n</inset>"
  },
  {
    "path": "collect_app/src/main/res/drawable/main_menu_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"@color/color_on_surface_low_emphasis\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <corners android:radius=\"100dp\" />\n            <solid android:color=\"?colorSurfaceContainerLow\" />\n        </shape>\n    </item>\n</ripple>"
  },
  {
    "path": "collect_app/src/main/res/drawable/odk_logo.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"440dp\"\n    android:height=\"240dp\"\n    android:viewportWidth=\"440\"\n    android:viewportHeight=\"240\">\n  <path\n      android:pathData=\"m300,200 l-0,-160\"\n      android:strokeWidth=\"38\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#fff\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m300,120 l100,-80\"\n      android:strokeWidth=\"38\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#fff\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m300,120 l100,80\"\n      android:strokeWidth=\"38\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#fff\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m300,200 l-0,-160\"\n      android:strokeWidth=\"30\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#0a711e\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m300,120 l100,-80\"\n      android:strokeWidth=\"30\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#0a711e\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m300,120 l100,80\"\n      android:strokeWidth=\"30\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#0a711e\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m200,200 l-0,-160\"\n      android:strokeWidth=\"38\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#fff\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m200,200 l100,-80\"\n      android:strokeWidth=\"38\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#fff\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m200,40 l100,80\"\n      android:strokeWidth=\"38\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#fff\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m200,200 l-0,-160\"\n      android:strokeWidth=\"30\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#904a22\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m200,200 l100,-80\"\n      android:strokeWidth=\"30\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#904a22\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m200,40 l100,80\"\n      android:strokeWidth=\"30\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#904a22\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m120,40 l80,80\"\n      android:strokeWidth=\"39\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#fff\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m40,120 l80,80\"\n      android:strokeWidth=\"39\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#fff\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m40,120 l80,-80\"\n      android:strokeWidth=\"39\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#fff\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m120,200 l80,-80\"\n      android:strokeWidth=\"39\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#fff\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m120,40 l80,80\"\n      android:strokeWidth=\"30\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#3e77b4\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m40,120 l80,80\"\n      android:strokeWidth=\"30\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#3e77b4\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m40,120 l80,-80\"\n      android:strokeWidth=\"30\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#3e77b4\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n  <path\n      android:pathData=\"m120,200 l80,-80\"\n      android:strokeWidth=\"30\"\n      android:fillColor=\"#3e77b4\"\n      android:strokeColor=\"#3e77b4\"\n      android:fillType=\"evenOdd\"\n      android:strokeLineCap=\"round\"/>\n</vector>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/pill_filled.xml",
    "content": "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <solid android:color=\"?colorPrimary\" />\n    <corners android:radius=\"2dp\" />\n\n</shape>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/pill_unfilled.xml",
    "content": "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <solid android:color=\"@color/color_on_surface_low_emphasis\" />\n    <corners android:radius=\"2dp\" />\n\n</shape>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/project_icon_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n    <solid android:color=\"@android:color/black\"/>\n    <size android:width=\"30dp\" android:height=\"30dp\"/>\n</shape>"
  },
  {
    "path": "collect_app/src/main/res/drawable/question_with_error_border.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\" >\n    <item\n        android:top=\"@dimen/margin_extra_extra_small\"\n        android:bottom=\"@dimen/margin_extra_extra_small\"\n        android:left=\"@dimen/margin_extra_small\"\n        android:right=\"@dimen/margin_extra_small\">\n        <shape\n            android:shape=\"rectangle\">\n            <stroke\n                android:width=\"2dp\"\n                android:color=\"?colorError\" />\n            <corners android:radius=\"10dp\" />\n        </shape>\n    </item>\n</layer-list>"
  },
  {
    "path": "collect_app/src/main/res/drawable/select_item_border.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2019 Nafundi\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <stroke\n        android:width=\"@dimen/margin_extra_extra_small\"\n        android:color=\"?colorSecondary\" />\n</shape>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable/start_new_form_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"@color/color_on_primary_low_emphasis\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <corners android:radius=\"100dp\" />\n            <solid android:color=\"?colorPrimary\" />\n        </shape>\n    </item>\n</ripple>"
  },
  {
    "path": "collect_app/src/main/res/drawable-ldrtl/counter_minus_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"@color/color_on_surface_low_emphasis\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <corners\n                android:topRightRadius=\"?dialogCornerRadius\"\n                android:bottomRightRadius=\"?dialogCornerRadius\" />\n            <solid android:color=\"?colorSurfaceContainerHighest\" />\n        </shape>\n    </item>\n</ripple>\n"
  },
  {
    "path": "collect_app/src/main/res/drawable-ldrtl/counter_plus_button_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"@color/color_on_surface_low_emphasis\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <corners\n                android:topLeftRadius=\"?dialogCornerRadius\"\n                android:bottomLeftRadius=\"?dialogCornerRadius\" />\n            <solid android:color=\"?colorSurfaceContainerHighest\" />\n        </shape>\n    </item>\n</ripple>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/about_item_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright 2018 Shobhit Agarwal\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?android:attr/selectableItemBackground\"\n    android:padding=\"@dimen/margin_standard\">\n\n    <ImageView\n        android:id=\"@+id/imageView\"\n        android:layout_width=\"24dp\"\n        android:layout_height=\"24dp\"\n        android:layout_alignParentTop=\"true\"\n        tools:src=\"@drawable/ic_outline_website_24\" />\n\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignTop=\"@id/imageView\"\n        android:layout_marginStart=\"@dimen/margin_extra_large\"\n        android:layout_toEndOf=\"@id/imageView\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/title\"\n            style=\"@style/TextAppearance.Collect.Subtitle1\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            tools:text=\"@string/odk_website\" />\n\n        <TextView\n            android:id=\"@+id/summary\"\n            style=\"@style/TextAppearance.Collect.Body2.MediumEmphasis\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n            tools:text=\"@string/odk_website_summary\" />\n\n    </LinearLayout>\n</RelativeLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/about_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\"/>\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recyclerView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/activity_blank_form_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/form_list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n\n    <org.odk.collect.lists.EmptyListView\n        android:id=\"@+id/empty_list_message\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:icon=\"@drawable/ic_baseline_note_72\"\n        app:title=\"@string/empty_list_of_blank_forms_title\"\n        app:subtitle=\"@string/empty_list_of_blank_forms_subtitle\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/activity_custom_scanner.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"org.odk.collect.android.activities.ScannerWithFlashlightActivity\">\n\n    <androidx.fragment.app.FragmentContainerView\n        android:name=\"org.odk.collect.android.fragments.BarcodeWidgetScannerFragment\"\n        android:id=\"@+id/fragmentContainerView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n</merge>"
  },
  {
    "path": "collect_app/src/main/res/layout/activity_preferences_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\"/>\n\n    <androidx.fragment.app.FragmentContainerView\n        android:name=\"org.odk.collect.android.preferences.screens.ProjectPreferencesFragment\"\n        android:id=\"@+id/preferences_fragment_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/add_repeat_dialog_layout.xml",
    "content": "<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"@dimen/margin_standard\">\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/message\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:textColor=\"?colorOnSurface\"\n        android:textAppearance=\"?textAppearanceHeadlineSmall\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        tools:text=\"Add person?\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/do_not_add_button\"\n        style=\"?materialButtonOutlinedIconStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/do_not_add_repeat\"\n        app:icon=\"@drawable/ic_close_24\"\n        app:iconGravity=\"textStart\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/add_button\"\n        style=\"?materialButtonIconStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/add_repeat\"\n        app:icon=\"@drawable/ic_add_white_24\"\n        app:iconGravity=\"textStart\" />\n\n    <androidx.constraintlayout.helper.widget.Flow\n        android:id=\"@+id/button_flow\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        app:constraint_referenced_ids=\"do_not_add_button,add_button\"\n        app:flow_wrapMode=\"chain\"\n        app:flow_horizontalGap=\"@dimen/margin_standard\"\n        app:flow_horizontalStyle=\"packed\"\n        app:flow_horizontalBias=\"1\"\n        app:layout_constraintTop_toBottomOf=\"@id/message\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"/>\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/admin_password_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:paddingTop=\"10dp\"\n    android:paddingBottom=\"10dp\"\n    android:paddingStart=\"20dp\"\n    android:paddingEnd=\"20dp\">\n\n    <EditText\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:inputType=\"textPassword\"\n        android:ems=\"10\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_alignParentEnd=\"true\"\n        android:id=\"@+id/editText\" />\n\n    <CheckBox\n        android:text=\"@string/show_password\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@+id/editText\"\n        android:layout_alignParentStart=\"true\"\n        android:id=\"@+id/checkBox\"\n        android:layout_marginBottom=\"10dp\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/annotate_widget.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/capture_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/capture_image\"\n        android:contentDescription=\"@string/capture_image\"\n        app:icon=\"@drawable/ic_baseline_photo_camera_white_24\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/choose_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/choose_image\"\n        android:contentDescription=\"@string/choose_image\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        app:icon=\"@drawable/ic_baseline_photo_library_white_24\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/capture_button\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/annotate_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/markup_image\"\n        android:contentDescription=\"@string/markup_image\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        app:icon=\"@drawable/ic_baseline_markup_white_24\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/choose_button\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/error_message\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        android:text=\"@string/selected_invalid_image\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/annotate_button\" />\n\n    <ImageView\n        android:id=\"@+id/image\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        android:tag=\"ImageView\"\n        android:adjustViewBounds=\"true\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/error_message\"\n        tools:src=\"@drawable/notes\"/>\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/audio_controller_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright 2018 Shobhit\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\"\n    android:paddingTop=\"@dimen/margin_large\">\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/play\"\n            style=\"@style/Widget.Material3.Button.IconButton.Filled\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_centerHorizontal=\"true\"\n            android:contentDescription=\"@string/play_audio\"\n            app:icon=\"@drawable/ic_play_arrow_24dp\" />\n\n        <TextView\n            android:id=\"@+id/currentDuration\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@id/seekBar\"\n            android:layout_alignParentStart=\"true\"\n            android:textColor=\"?colorOnSurface\"\n            android:textSize=\"@dimen/text_size_small\"\n            android:textStyle=\"bold\"\n            tools:text=\"00:00\" />\n\n        <TextView\n            android:id=\"@+id/totalDuration\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@id/seekBar\"\n            android:layout_alignParentEnd=\"true\"\n            android:textColor=\"?colorOnSurface\"\n            android:textSize=\"@dimen/text_size_small\"\n            android:textStyle=\"bold\"\n            tools:text=\"00:00\" />\n\n        <SeekBar\n            android:id=\"@+id/seekBar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@+id/play\"\n            android:minHeight=\"24dp\"\n            android:paddingVertical=\"@dimen/margin_standard\"\n            android:paddingStart=\"0dp\"\n            android:paddingEnd=\"0dp\"\n            android:thumbOffset=\"3dp\" />\n    </RelativeLayout>\n\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\">\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/remove\"\n            style=\"?materialButtonOutlinedIconStyle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/delete_answer_file\"\n            app:icon=\"@drawable/ic_baseline_delete_outline_24\" />\n\n    </LinearLayout>\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/audio_player_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <TextView\n        android:id=\"@+id/recording_duration\"\n        style=\"@style/TextAppearance.Collect.Headline5\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:textColor=\"?colorPrimary\"\n        tools:text=\"00:07\" />\n\n    <org.odk.collect.android.audio.Waveform\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:id=\"@+id/waveform\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"80dp\" />\n\n    <org.odk.collect.android.audio.AudioControllerView\n        android:id=\"@+id/audio_controller\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/audio_recording_controller_fragment.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:baselineAligned=\"false\"\n    android:orientation=\"horizontal\"\n    android:paddingVertical=\"@dimen/margin_extra_small\"\n    android:paddingStart=\"@dimen/margin_standard\"\n    android:paddingEnd=\"@dimen/margin_standard\"\n    tools:ignore=\"RtlSymmetry\">\n\n    <LinearLayout\n        android:id=\"@+id/recording_status\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center_vertical\"\n        android:orientation=\"horizontal\">\n\n        <ImageView\n            android:id=\"@+id/recording_icon\"\n            android:layout_width=\"24dp\"\n            android:layout_height=\"24dp\"\n            android:src=\"@drawable/ic_baseline_mic_24\" />\n\n        <TextView\n            android:id=\"@+id/time_code\"\n            style=\"@style/TextAppearance.Collect.Subtitle1\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"@dimen/margin_extra_small\"\n            android:text=\"@string/recording\"\n            android:textColor=\"?colorOnSurface\"/>\n\n        <org.odk.collect.android.audio.VolumeBar\n            android:id=\"@+id/volume_bar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"8dp\"\n            android:layout_marginStart=\"@dimen/margin_standard\" />\n    </LinearLayout>\n\n    <LinearLayout\n        android:id=\"@+id/controls\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:orientation=\"horizontal\">\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/pause_recording\"\n            style=\"?materialIconButtonOutlinedStyle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginEnd=\"@dimen/margin_standard\"\n            android:contentDescription=\"@string/pause_recording\"\n            app:icon=\"@drawable/ic_pause_24dp\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/stop_recording\"\n            style=\"@style/Widget.Material3.Button.IconButton.Filled\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:contentDescription=\"@string/stop_recording\"\n            app:icon=\"@drawable/ic_stop_black_24dp\" />\n    </LinearLayout>\n\n    <ImageView\n        android:id=\"@+id/help\"\n        android:layout_width=\"24dp\"\n        android:layout_height=\"24dp\"\n        android:layout_gravity=\"center_vertical\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:src=\"@drawable/ic_baseline_help_outline_24\"\n        android:visibility=\"gone\" />\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/audio_video_image_text_label.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/audio_video_image_text_label_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <LinearLayout\n        android:id=\"@+id/image_text_label_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentStart=\"true\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_toStartOf=\"@id/media_buttons\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:orientation=\"vertical\">\n\n        <FrameLayout\n            android:id=\"@+id/text_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <com.google.android.material.textview.MaterialTextView\n                android:id=\"@+id/text_label\"\n                android:textAppearance=\"?textAppearanceTitleLarge\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:orientation=\"vertical\"\n                tools:text=\"This is a question?\" />\n\n        </FrameLayout>\n\n        <ImageView\n            android:id=\"@+id/imageView\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_standard\"\n            android:scaleType=\"fitStart\"\n            android:adjustViewBounds=\"true\"\n            android:visibility=\"gone\"\n            tools:src=\"@drawable/ic_baseline_mic_24\"\n            tools:visibility=\"visible\"/>\n\n        <TextView\n            android:id=\"@+id/missingImage\"\n            style=\"@style/TextAppearance.Collect.Subtitle1.MediumEmphasis\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:visibility=\"gone\"\n            tools:text=\"media missing\"\n            tools:visibility=\"visible\" />\n    </LinearLayout>\n\n    <LinearLayout\n        android:id=\"@+id/media_buttons\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_alignParentEnd=\"true\"\n        android:gravity=\"end\"\n        android:orientation=\"vertical\"\n        android:visibility=\"gone\"\n        tools:visibility=\"visible\">\n\n        <org.odk.collect.android.audio.AudioButton\n            android:id=\"@+id/audioButton\"\n            style=\"?materialIconButtonOutlinedStyle\"\n            android:layout_width=\"@dimen/touch_target_size\"\n            android:layout_height=\"@dimen/touch_target_size\"\n            android:visibility=\"gone\"\n            tools:visibility=\"visible\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/videoButton\"\n            style=\"?materialIconButtonOutlinedStyle\"\n            android:layout_width=\"@dimen/touch_target_size\"\n            android:layout_height=\"@dimen/touch_target_size\"\n            android:visibility=\"gone\"\n            app:icon=\"@drawable/ic_ondemand_video_black_24dp\"\n            tools:visibility=\"visible\" />\n    </LinearLayout>\n</RelativeLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/audio_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/record_audio_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/capture_audio\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_mic_white_24\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/choose_audio_button\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/choose_sound\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_library_music_white_24\" />\n\n    <include\n        android:id=\"@+id/audio_player\"\n        layout=\"@layout/audio_player_layout\"/>\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/background_audio_help_fragment_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"@dimen/margin_standard\"\n        android:text=\"@string/background_audio_help\" />\n</ScrollView>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/bearing_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/bearing_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/get_bearing\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_explore_white_24\" />\n\n    <org.odk.collect.android.views.WidgetAnswerText\n        android:id=\"@+id/widget_answer_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"/>\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/blank_form_list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?android:attr/selectableItemBackground\">\n\n    <ImageView\n        android:id=\"@+id/image\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_form_state_blank\" />\n\n    <TextView\n        android:id=\"@+id/form_title\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:textAppearance=\"@style/TextAppearance.Collect.Subtitle1\"\n        app:layout_constraintEnd_toStartOf=\"@+id/trailing_view\"\n        app:layout_constraintStart_toEndOf=\"@+id/image\"\n        app:layout_constraintTop_toTopOf=\"@+id/image\"\n        tools:text=\"All widgets\" />\n\n    <TextView\n        android:id=\"@+id/form_version\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n        android:textAppearance=\"@style/TextAppearance.Collect.Body2.MediumEmphasis\"\n        app:layout_constraintEnd_toEndOf=\"@+id/form_title\"\n        app:layout_constraintStart_toStartOf=\"@+id/form_title\"\n        app:layout_constraintTop_toBottomOf=\"@+id/form_title\"\n        tools:text=\"Version: 2019051301\" />\n\n    <TextView\n        android:id=\"@+id/form_id\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"@style/TextAppearance.Collect.Body2.MediumEmphasis\"\n        app:layout_constraintEnd_toEndOf=\"@+id/form_title\"\n        app:layout_constraintStart_toStartOf=\"@+id/form_title\"\n        app:layout_constraintTop_toBottomOf=\"@+id/form_version\"\n        tools:text=\"ID: form-id\" />\n\n    <TextView\n        android:id=\"@+id/form_history\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:textAppearance=\"@style/TextAppearance.Collect.Body2.MediumEmphasis\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"@+id/form_title\"\n        app:layout_constraintStart_toStartOf=\"@+id/form_title\"\n        app:layout_constraintTop_toBottomOf=\"@+id/form_id\"\n        tools:text=\"Added on Thu, Jul 11, 2019 at 15:35\" />\n\n    <FrameLayout\n        android:id=\"@+id/trailing_view\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:minHeight=\"40dp\"\n        android:minWidth=\"40dp\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/bottom_sheet.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\nCopyright (C) 2017 Shobhit\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n\n-->\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\"\n    android:padding=\"16dp\"\n    app:layout_behavior=\"android.support.design.widget.BottomSheetBehavior\">\n\n    <TextView\n        android:id=\"@+id/label\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentTop=\"true\"\n        android:text=\"@string/sort_by\"\n        android:layout_marginBottom=\"8dp\"\n        android:textAppearance=\"@style/Base.TextAppearance.AppCompat.Medium\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recyclerView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@+id/label\" />\n</RelativeLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/captioned_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingVertical=\"@dimen/margin_small\"\n    android:paddingHorizontal=\"@dimen/margin_large\"\n    android:orientation=\"horizontal\"\n    android:background=\"?attr/selectableItemBackground\">\n\n    <RadioButton\n        android:id=\"@+id/button\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"-2dp\"\n        android:layout_marginEnd=\"@dimen/margin_small\"\n        android:saveEnabled=\"false\" />\n\n    <LinearLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/label\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"@android:style/TextAppearance.Medium\"\n            tools:text=\"Reykjavik\" />\n\n        <TextView\n            android:id=\"@+id/caption\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"@android:style/TextAppearance.Small\"\n            android:textColor=\"#888\"\n            tools:text=\"/sdcard/odk/layers/parent_directory/reykjavik/reykjavik.mbtiles\"/>\n\n    </LinearLayout>\n\n</LinearLayout>\n\n"
  },
  {
    "path": "collect_app/src/main/res/layout/captioned_list_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\"\n        android:layout_marginTop=\"16dp\"\n        android:layout_marginBottom=\"16dp\"\n        tools:ignore=\"UselessParent\">\n\n        <!--\n          Even though this contains RadioButtons, we're using a LinearLayout\n          instead of a RadioGroup here, because a RadioGroup forces all its\n          children to have layout_width=\"wrap_content\"; we want the items in\n          this list to be touchable across the entire width of the dialog.\n        -->\n\n        <LinearLayout\n            android:id=\"@+id/list\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"/>\n\n    </ScrollView>\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/changes_reason_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fillViewport=\"true\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center\"\n            android:orientation=\"vertical\"\n            android:padding=\"@dimen/margin_extra_large\">\n\n            <LinearLayout\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center_horizontal\"\n                android:orientation=\"vertical\">\n\n                <ImageView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    app:srcCompat=\"@drawable/ic_chat_bubble_24dp\" />\n\n                <TextView\n                    style=\"@style/TextAppearance.Collect.Headline6\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"@dimen/margin_standard\"\n                    android:text=\"@string/reason_for_changes\" />\n\n                <TextView\n                    style=\"@style/TextAppearance.Collect.Body1\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"@dimen/margin_extra_small\"\n                    android:text=\"@string/reason_for_changes_description\"\n                    android:textAlignment=\"center\" />\n\n            </LinearLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                style=\"?textInputFilledStyle\"\n                android:layout_width=\"280dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/margin_extra_large\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:id=\"@+id/reason\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:hint=\"@string/reason\"\n                    android:inputType=\"textMultiLine\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n        </LinearLayout>\n    </androidx.core.widget.NestedScrollView>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/checkbox.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.checkbox.MaterialCheckBox xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/checkbox\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\" />\n"
  },
  {
    "path": "collect_app/src/main/res/layout/circular_progress_indicator.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:indeterminate=\"true\"/>\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/counter_widget.xml",
    "content": "<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <ImageView\n        android:id=\"@+id/minus_button\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:src=\"@drawable/ic_baseline_remove_24\"\n        android:background=\"@drawable/counter_minus_button_background\"\n        android:paddingVertical=\"@dimen/margin_small\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toStartOf=\"@+id/value\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/value\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:gravity=\"center\"\n        android:background=\"@drawable/counter_value_background\"\n        android:textAppearance=\"?textAppearanceHeadlineSmall\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintStart_toEndOf=\"@id/minus_button\"\n        app:layout_constraintEnd_toStartOf=\"@id/plus_button\"\n        tools:text=\"145\" />\n\n    <ImageView\n        android:id=\"@+id/plus_button\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:src=\"@drawable/ic_baseline_add_24\"\n        android:background=\"@drawable/counter_plus_button_background\"\n        android:paddingVertical=\"@dimen/margin_small\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintStart_toEndOf=\"@id/value\"\n        app:layout_constraintEnd_toEndOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/current_project_menu_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<org.odk.collect.android.projects.ProjectIconView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:paddingHorizontal=\"@dimen/margin_standard\"\n    android:background=\"?attr/selectableItemBackgroundBorderless\"/>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/custom_date_picker_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2017 Nafundi\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center_horizontal\"\n        android:orientation=\"vertical\">\n\n        <LinearLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <NumberPicker\n                android:id=\"@+id/day_picker\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"match_parent\"\n                android:scrollbars=\"none\"\n                android:descendantFocusability=\"blocksDescendants\" />\n\n            <NumberPicker\n                android:id=\"@+id/month_picker\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"match_parent\"\n                android:layout_marginEnd=\"10dp\"\n                android:layout_marginLeft=\"10dp\"\n                android:layout_marginRight=\"10dp\"\n                android:layout_marginStart=\"10dp\"\n                android:scrollbars=\"none\"\n                android:descendantFocusability=\"blocksDescendants\" />\n\n            <NumberPicker\n                android:id=\"@+id/year_picker\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"match_parent\"\n                android:scrollbars=\"none\"\n                android:descendantFocusability=\"blocksDescendants\" />\n        </LinearLayout>\n\n        <TextView\n            android:id=\"@+id/date_gregorian\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"5dp\"\n            android:textColor=\"?colorOnSurface\" />\n\n    </LinearLayout>\n</ScrollView>"
  },
  {
    "path": "collect_app/src/main/res/layout/date_time_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\" android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <include\n        layout=\"@layout/date_widget_answer\"\n        android:id=\"@+id/date_widget\"/>\n\n    <include\n        layout=\"@layout/time_widget_answer\"\n        android:id=\"@+id/time_widget\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"/>\n\n</LinearLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/date_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/date_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/select_date\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_calendar_today_white_24\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/date_answer_text\"\n        style=\"@style/Widget.Collect.TextView.WidgetAnswer\"\n        android:text=\"@string/no_date_selected\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/delete_form_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/multi_select_list\"\n        android:name=\"org.odk.collect.lists.selects.MultiSelectListFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/delete_project_dialog_layout.xml",
    "content": "<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingHorizontal=\"@dimen/margin_standard\"\n        android:paddingBottom=\"@dimen/margin_standard\">\n\n        <ProgressBar\n            android:id=\"@+id/progress_bar\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_standard\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <com.google.android.material.textview.MaterialTextView\n            android:id=\"@+id/title\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:ellipsize=\"end\"\n            android:maxLines=\"1\"\n            android:text=\"@string/delete_project_dialog_title\"\n            android:textAppearance=\"?textAppearanceHeadlineSmall\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/progress_bar\"\n            app:layout_goneMarginTop=\"@dimen/margin_standard\"\n            tools:text=\"Delete X project\" />\n\n        <com.google.android.material.textview.MaterialTextView\n            android:id=\"@+id/message\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_standard\"\n            android:textAppearance=\"?textAppearanceBodyMedium\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/title\"\n            tools:text=\"Delete project message\" />\n\n        <com.google.android.material.textfield.TextInputLayout\n            android:id=\"@+id/confirmation_field\"\n            style=\"?textInputFilledStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_standard\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/message\">\n\n            <com.google.android.material.textfield.TextInputEditText\n                android:id=\"@+id/confirmation_field_input\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:inputType=\"text\"\n                android:lines=\"1\"\n                android:paddingVertical=\"@dimen/margin_standard\" />\n        </com.google.android.material.textfield.TextInputLayout>\n\n        <androidx.constraintlayout.widget.Barrier\n            android:id=\"@+id/barrierTopViews\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:barrierDirection=\"bottom\"\n            app:constraint_referenced_ids=\"progress_bar, confirmation_field\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/cancel_button\"\n            style=\"?materialButtonOutlinedStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_extra_large\"\n            android:text=\"@string/cancel\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/barrierTopViews\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/delete_button\"\n            style=\"?materialButtonIconStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:backgroundTint=\"@color/color_error_button_icon\"\n            android:enabled=\"false\"\n            android:text=\"@string/delete_project_confirm_button_text\"\n            android:textColor=\"@android:color/white\"\n            app:icon=\"@drawable/ic_delete\"\n            app:iconGravity=\"textStart\"\n            app:iconTint=\"@android:color/white\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/cancel_button\" />\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</ScrollView>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/draw_widget.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/draw_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/draw_image\"\n        android:contentDescription=\"@string/draw_image\"\n        app:icon=\"@drawable/ic_baseline_draw_white_24\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/error_message\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        android:text=\"@string/selected_invalid_image\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/draw_button\" />\n\n    <ImageView\n        android:id=\"@+id/image\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        android:tag=\"ImageView\"\n        android:adjustViewBounds=\"true\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/error_message\"\n        tools:src=\"@drawable/notes\"/>\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/ex_audio_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/launch_external_app_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/launch_app\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_open_in_new_white_24\" />\n\n    <include\n        android:id=\"@+id/audio_player\"\n        layout=\"@layout/audio_player_layout\"/>\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/ex_image_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/launch_external_app_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/launch_app\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_open_in_new_white_24\" />\n\n    <ImageView\n        android:id=\"@+id/image_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"/>\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/ex_string_question_type.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/launch_app_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/launch_app\"\n        android:contentDescription=\"@string/launch_app\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_open_in_new_white_24\" />\n\n    <org.odk.collect.android.views.WidgetAnswerText\n        android:id=\"@+id/widget_answer_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/first_launch_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fillViewport=\"true\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/center\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginHorizontal=\"@dimen/margin_large\"\n            android:paddingHorizontal=\"@dimen/margin_large\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:layout_constraintVertical_bias=\"0.5\">\n\n            <ImageView\n                android:id=\"@+id/logo\"\n                android:layout_width=\"100dp\"\n                android:layout_height=\"wrap_content\"\n                android:adjustViewBounds=\"true\"\n                android:contentDescription=\"@string/collect_app_name\"\n                android:src=\"@drawable/odk_logo\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <TextView\n                android:id=\"@+id/tagline\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/margin_extra_small\"\n                android:lines=\"2\"\n                android:text=\"@string/tagline\"\n                android:textAppearance=\"?textAppearanceHeadline4\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/logo\" />\n\n            <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n                android:id=\"@+id/configure_via_qr_button\"\n                style=\"?materialButtonIconStyle\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/configure_with_qr_code\"\n                android:layout_marginTop=\"@dimen/margin_standard\"\n                app:icon=\"@drawable/ic_baseline_qr_code_scanner_24\"\n                app:layout_constraintTop_toBottomOf=\"@id/tagline\"\n                app:layout_constraintBottom_toTopOf=\"@id/configure_manually_button\"\n                app:layout_constraintEnd_toEndOf=\"@id/barrierEnd\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintWidth_min=\"wrap\"/>\n\n            <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n                android:id=\"@+id/configure_manually_button\"\n                style=\"?materialButtonOutlinedIconStyle\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/margin_extra_small\"\n                android:text=\"@string/configure_manually\"\n                app:icon=\"@drawable/ic_outline_edit_24\"\n                app:layout_constraintEnd_toEndOf=\"@id/barrierEnd\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/configure_via_qr_button\"\n                app:layout_constraintWidth_min=\"wrap\"/>\n\n            <androidx.constraintlayout.widget.Barrier\n                android:id=\"@+id/barrierEnd\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"0dp\"\n                app:barrierDirection=\"end\"\n                app:constraint_referenced_ids=\"configure_via_qr_button,configure_manually_button\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <TextView\n            android:id=\"@+id/app_name\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_extra_large\"\n            android:layout_marginBottom=\"@dimen/margin_extra_extra_small\"\n            android:textAppearance=\"?textAppearanceBody1\"\n            android:textColor=\"@color/color_on_surface_medium_emphasis\"\n            app:layout_constraintBottom_toTopOf=\"@id/dont_have_server\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/center\"\n            app:layout_constraintVertical_bias=\"1\"\n            tools:text=\"ODK Collect v2022.3\" />\n\n        <TextView\n            android:id=\"@+id/dont_have_server\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"@dimen/margin_standard\"\n            android:paddingHorizontal=\"@dimen/margin_standard\"\n            android:textAppearance=\"?textAppearanceBody2\"\n            android:fontFamily=\"sans-serif-medium\"\n            android:textAlignment=\"center\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            tools:text=\"Don\\'t have a project yet? Try a demo\"/>\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</ScrollView>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/form_chooser_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\"/>\n\n    <ListView\n        android:id=\"@+id/scrollable_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:nestedScrollingEnabled=\"true\"\n        android:divider=\"@drawable/list_item_divider\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n\n    <org.odk.collect.lists.EmptyListView\n        android:id=\"@android:id/empty\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:icon=\"@drawable/ic_baseline_edit_72\"\n        app:title=\"@string/empty_list_of_drafts_title\"\n        app:subtitle=\"@string/empty_list_of_drafts_subtitle\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/form_chooser_list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:padding=\"@dimen/margin_standard\">\n\n    <!-- Material Design reference: https://material.io/design/components/lists.html#specs -->\n\n    <org.odk.collect.material.ErrorsPill\n        android:id=\"@+id/chip\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentStart=\"true\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_marginBottom=\"@dimen/margin_extra_extra_small\"\n        tools:visibility=\"visible\" />\n\n    <FrameLayout\n        android:id=\"@+id/imageView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/chip\">\n\n        <include layout=\"@layout/form_chooser_list_item_icon\" />\n    </FrameLayout>\n\n    <FrameLayout\n        android:id=\"@+id/text_view\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignTop=\"@id/imageView\"\n        android:layout_below=\"@id/chip\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:layout_marginLeft=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginRight=\"@dimen/margin_standard\"\n        android:layout_toStartOf=\"@id/map_view\"\n        android:layout_toEndOf=\"@id/imageView\">\n\n        <include layout=\"@layout/form_chooser_list_item_text\" />\n    </FrameLayout>\n\n    <FrameLayout\n        android:id=\"@+id/map_view\"\n        android:visibility=\"gone\"\n          tools:visibility=\"visible\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"0dp\"\n\n        android:layout_below=\"@id/chip\"\n        android:layout_alignBottom=\"@id/text_view\"\n        android:layout_alignParentEnd=\"true\"\n\n        android:layout_marginRight=\"-16dp\"\n        android:layout_marginStart=\"-16dp\"\n        android:layout_marginLeft=\"-16dp\"\n        android:layout_marginEnd=\"-16dp\"\n        android:layout_marginTop=\"-16dp\"\n        android:layout_marginBottom=\"-16dp\"\n\n        android:padding=\"@dimen/margin_standard\">\n\n        <include layout=\"@layout/form_chooser_list_item_map_button\" />\n    </FrameLayout>\n\n\n</RelativeLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/form_chooser_list_item_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <!-- Material Design reference: https://material.io/design/components/lists.html#specs -->\n\n    <ImageView\n        android:id=\"@+id/image\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        app:srcCompat=\"@drawable/ic_form_state_blank\" />\n\n</merge>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/form_chooser_list_item_map_button.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/map_button\"\n        style=\"?materialIconButtonOutlinedStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_map\" />\n</merge>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/form_chooser_list_item_multiple_choice.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright (C) 2009 University of Washington Licensed under the Apache\nLicense, Version 2.0 (the \"License\"); you may not use this file except in\ncompliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software distributed\nunder the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES\nOR CONDITIONS OF ANY KIND, either express or implied. See the License for\nthe specific language governing permissions and limitations under the License. -->\n<org.odk.collect.android.views.TwoItemMultipleChoiceView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <!-- Material Design reference: https://material.io/design/components/lists.html#specs -->\n\n    <FrameLayout\n        android:id=\"@+id/imageView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentTop=\"true\"\n        android:paddingVertical=\"@dimen/margin_standard\"\n        android:paddingHorizontal=\"@dimen/margin_standard\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <include layout=\"@layout/form_chooser_list_item_icon\" />\n\n    </FrameLayout>\n\n    <FrameLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignTop=\"@id/imageView\"\n        android:paddingVertical=\"@dimen/margin_standard\"\n        app:layout_constraintStart_toEndOf=\"@id/imageView\"\n        app:layout_constraintEnd_toStartOf=\"@id/selectView\"\n        app:layout_constraintTop_toTopOf=\"@id/imageView\">\n\n        <include layout=\"@layout/form_chooser_list_item_text\" />\n\n    </FrameLayout>\n\n    <FrameLayout\n        android:id=\"@+id/selectView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"0dp\"\n        android:paddingVertical=\"@dimen/margin_standard\"\n        android:paddingHorizontal=\"@dimen/margin_standard\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/imageView\">\n\n        <CheckBox\n            android:id=\"@+id/checkbox\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:clickable=\"false\"\n            android:focusable=\"false\"\n            android:padding=\"0dp\" />\n\n        <ImageView\n            android:id=\"@+id/close_box\"\n            android:layout_width=\"24dp\"\n            android:layout_height=\"24dp\"\n            android:visibility=\"gone\"\n            app:srcCompat=\"@drawable/ic_cancel\"\n            tools:visibility=\"visible\" />\n\n    </FrameLayout>\n</org.odk.collect.android.views.TwoItemMultipleChoiceView>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/form_chooser_list_item_text.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <!-- Material Design reference: https://material.io/design/components/lists.html#specs -->\n\n    <TextView\n        android:id=\"@+id/form_title\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"@style/TextAppearance.Collect.Subtitle1\"\n        tools:text=\"All widgets\" />\n\n    <TextView\n        android:id=\"@+id/form_subtitle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n        android:textAppearance=\"@style/TextAppearance.Collect.Body2.MediumEmphasis\"\n        tools:text=\"Version: 2019051301\" />\n\n    <TextView\n        android:id=\"@+id/form_subtitle2\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n        android:textAppearance=\"@style/TextAppearance.Collect.Body2.MediumEmphasis\"\n        android:visibility=\"gone\"\n        tools:text=\"Added on Thu, Jul 11, 2019 at 15:35\"\n        tools:visibility=\"visible\" />\n\n    <TextView\n        android:id=\"@+id/form_update_alert\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n        android:text=\"@string/newer_version_of_a_form_info\"\n        android:textAppearance=\"@style/TextAppearance.Collect.Body2\"\n        android:textColor=\"?attr/colorSecondary\"\n        android:visibility=\"gone\"\n        tools:visibility=\"visible\" />\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/form_download_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <LinearLayout\n            android:id=\"@+id/content\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:orientation=\"vertical\"\n            app:layout_constraintBottom_toTopOf=\"@id/buttonholder\"\n            app:layout_constraintTop_toTopOf=\"parent\">\n\n            <ListView\n                android:id=\"@+id/scrollable_container\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:nestedScrollingEnabled=\"true\"\n                android:divider=\"@drawable/list_item_divider\" />\n\n            <org.odk.collect.lists.EmptyListView\n                android:id=\"@android:id/empty\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                app:icon=\"@drawable/ic_baseline_download_72\"\n                app:title=\"@string/empty_list_of_forms_to_download_title\"\n                app:subtitle=\"@string/empty_list_of_forms_to_download_subtitle\"/>\n\n        </LinearLayout>\n\n        <androidx.appcompat.widget.LinearLayoutCompat\n            android:id=\"@+id/buttonholder\"\n            style=\"@style/Widget.AndroidShared.ButtonBar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\">\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/toggle_button\"\n                style=\"?materialButtonOutlinedStyle\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"match_parent\"\n                android:layout_marginStart=\"@dimen/margin_standard\"\n                android:layout_weight=\"1\"\n                android:text=\"@string/select_all\" />\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/refresh_button\"\n                style=\"?materialButtonOutlinedStyle\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"match_parent\"\n                android:layout_marginStart=\"@dimen/margin_standard\"\n                android:layout_weight=\"1\"\n                android:text=\"@string/refresh\" />\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/add_button\"\n                style=\"?materialButtonStyle\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"match_parent\"\n                android:layout_marginStart=\"@dimen/margin_standard\"\n                android:layout_marginEnd=\"@dimen/margin_standard\"\n                android:layout_weight=\"1\"\n                android:text=\"@string/download\" />\n\n        </androidx.appcompat.widget.LinearLayoutCompat>\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/form_entry.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/llParent\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <androidx.fragment.app.FragmentContainerView\n            android:id=\"@+id/audio_recording_controller\"\n            android:name=\"org.odk.collect.android.audio.AudioRecordingControllerFragment\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"/>\n\n        <FrameLayout\n            android:id=\"@+id/questionholder\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/audio_recording_controller\"\n            app:layout_constraintBottom_toTopOf=\"@id/buttonholder\"/>\n\n        <org.odk.collect.android.views.TransparentProgressScreen\n            android:id=\"@+id/loading_screen\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/audio_recording_controller\"\n            app:layout_constraintBottom_toTopOf=\"@id/buttonholder\"\n            android:visibility=\"gone\" />\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/buttonholder\"\n            style=\"@style/Widget.AndroidShared.ButtonBar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintBottom_toBottomOf=\"parent\">\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/form_back_button\"\n                style=\"?materialButtonOutlinedIconStyle\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"@dimen/margin_standard\"\n                android:text=\"@string/form_backward\"\n                app:icon=\"@drawable/ic_navigate_back\"\n                app:iconGravity=\"textStart\"\n                app:layout_constraintEnd_toStartOf=\"@id/form_forward_button\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/form_forward_button\"\n                style=\"?materialButtonOutlinedIconStyle\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"@dimen/margin_standard\"\n                android:layout_marginEnd=\"@dimen/margin_standard\"\n                android:text=\"@string/form_forward\"\n                android:theme=\"@style/ThemeOverlay.Material3.Button.Icon.End\"\n                app:icon=\"@drawable/ic_navigate_forward\"\n                app:iconGravity=\"textEnd\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintStart_toEndOf=\"@id/form_back_button\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/form_entry_end.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\nCopyright (C) 2009 University of Washington Licensed under the Apache\nLicense, Version 2.0 (the \"License\"); you may not use this file except in\ncompliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software distributed\nunder the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES\nOR CONDITIONS OF ANY KIND, either express or implied. See the License for\nthe specific language governing permissions and limitations under the License.\n-->\n\n<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/scroll_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fillViewport=\"true\"\n    android:orientation=\"vertical\">\n\n    <androidx.compose.ui.platform.ComposeView\n        android:id=\"@+id/compose_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"/>\n</androidx.core.widget.NestedScrollView>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/form_hierarchy_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ImageView\n        android:id=\"@+id/group_icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:visibility=\"gone\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:srcCompat=\"@drawable/ic_folder_open\"\n        tools:visibility=\"visible\" />\n\n    <TextView\n        android:id=\"@+id/pathtext\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_extra_small\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/group_icon\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toEndOf=\"@id/group_icon\"\n        app:layout_constraintTop_toTopOf=\"@id/group_icon\"\n        tools:text=\"Text widgets\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/list\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        app:layout_constraintBottom_toTopOf=\"@id/buttonholder\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/pathtext\"\n        app:layout_goneMarginTop=\"0dp\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/buttonholder\"\n        style=\"@style/Widget.AndroidShared.ButtonBar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentBottom=\"true\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\">\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/jumpBeginningButton\"\n            style=\"?materialButtonOutlinedStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginStart=\"@dimen/margin_standard\"\n            android:text=\"@string/jump_to_beginning\"\n            app:layout_constraintEnd_toStartOf=\"@+id/jumpEndButton\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/jumpEndButton\"\n            style=\"?materialButtonOutlinedStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginStart=\"@dimen/margin_standard\"\n            android:layout_marginEnd=\"@dimen/margin_standard\"\n            android:text=\"@string/jump_to_end\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toEndOf=\"@id/jumpBeginningButton\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/exitButton\"\n            style=\"?materialButtonOutlinedStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginStart=\"@dimen/margin_standard\"\n            android:layout_marginEnd=\"@dimen/margin_standard\"\n            android:text=\"@string/exit\"\n            android:visibility=\"gone\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n    <org.odk.collect.lists.EmptyListView\n        android:id=\"@android:id/empty\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/pathtext\"\n        app:title=\"@string/empty_list_of_questions\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/form_map_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/selection_map\"\n        android:name=\"org.odk.collect.geo.selection.SelectionMapFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n</merge>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/fragment_scan.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"org.odk.collect.android.fragments.BarCodeScannerFragment\">\n\n    <org.odk.collect.qrcode.BarcodeScannerViewContainer\n        android:id=\"@+id/barcode_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <androidx.compose.ui.platform.ComposeView\n        android:id=\"@+id/compose_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"/>\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/geopoint_question.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/simple_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/get_point\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_my_location_white_24\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/geo_answer_text\"\n        style=\"@style/Widget.Collect.TextView.WidgetAnswer\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/geoshape_question.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/simple_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/get_polygon\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_outline_polygon_white_24\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/geo_answer_text\"\n        style=\"@style/Widget.Collect.TextView.WidgetAnswer\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/geotrace_question.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/simple_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/get_line\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_outline_polyline_white_24\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/geo_answer_text\"\n        style=\"@style/Widget.Collect.TextView.WidgetAnswer\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/google_drive_deprecation_banner.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:visibility=\"gone\"\n    app:shapeAppearance=\"?shapeAppearanceLargeComponent\"\n    app:cardBackgroundColor=\"?colorSurfaceContainerHighest\"\n    tools:visibility=\"visible\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingTop=\"@dimen/margin_standard\"\n        android:paddingHorizontal=\"@dimen/margin_standard\">\n\n        <ImageView\n            android:id=\"@+id/instance_name_info_icon\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:src=\"@drawable/ic_outline_info_24\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:tint=\"?colorPrimary\" />\n\n        <TextView\n            android:id=\"@+id/message\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"@dimen/margin_standard\"\n            android:text=\"@string/google_drive_removed_message\"\n            android:textAppearance=\"?textAppearanceBody2\"\n            android:textColor=\"?colorOnSurface\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toEndOf=\"@id/instance_name_info_icon\"\n            app:layout_constraintTop_toTopOf=\"@id/instance_name_info_icon\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/learn_more_button\"\n            style=\"?borderlessButtonStyle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/learn_more_button_text\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/message\" />\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n</com.google.android.material.card.MaterialCardView>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/help_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/help_layout\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <RelativeLayout\n        android:id=\"@+id/text_layout\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\">\n\n        <ImageView\n            android:id=\"@+id/help_icon\"\n            android:layout_width=\"24dp\"\n            android:layout_height=\"24dp\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:layout_marginEnd=\"@dimen/margin_standard\"\n            android:visibility=\"gone\"\n            app:srcCompat=\"@drawable/ic_information_outline\"\n            tools:visibility=\"visible\" />\n\n        <TextView\n            android:id=\"@+id/help_text_view\"\n            style=\"@style/TextAppearance.Collect.Subtitle1.MediumEmphasis\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_centerVertical=\"true\"\n            android:layout_toEndOf=\"@id/help_icon\"\n            tools:text=\"Help text\" />\n    </RelativeLayout>\n\n    <LinearLayout\n        android:id=\"@+id/guidance_text_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:visibility=\"gone\"\n        tools:visibility=\"visible\">\n\n        <TextView\n            android:id=\"@+id/guidance_text_view\"\n            style=\"@style/TextAppearance.Collect.Subtitle1.MediumEmphasis\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            tools:text=\"Guidance text\" />\n    </LinearLayout>\n\n    <TextView\n        android:id=\"@+id/warning_text\"\n        style=\"@style/TextAppearance.Collect.Subtitle1\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textColor=\"?attr/colorError\"\n        android:visibility=\"gone\"\n        tools:text=\"A warning\"\n        tools:visibility=\"visible\" />\n</LinearLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/hierarchy_group_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingVertical=\"@dimen/margin_small\"\n    android:paddingHorizontal=\"@dimen/margin_standard\">\n\n    <ImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintBottom_toBottomOf=\"@id/primary_text\"\n        app:layout_constraintTop_toTopOf=\"@id/group_label\"\n        app:srcCompat=\"@drawable/ic_folder_open\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/group_label\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_extra_small\"\n        android:textAppearance=\"?textAppearanceBodyMedium\"\n        android:text=\"@string/group_label\"\n        app:layout_constraintStart_toEndOf=\"@id/icon\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/primary_text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"?textAppearanceTitleLarge\"\n        app:layout_constraintStart_toStartOf=\"@id/group_label\"\n        app:layout_constraintTop_toBottomOf=\"@id/group_label\"\n        tools:text=\"Household\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/hierarchy_host_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/nav_host_fragment\"\n        android:name=\"androidx.navigation.fragment.NavHostFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"/>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/hierarchy_question_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingVertical=\"@dimen/margin_small\">\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/primary_text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginHorizontal=\"@dimen/margin_standard\"\n        android:textAppearance=\"?textAppearanceTitleLarge\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:text=\"Name\" />\n\n    <androidx.compose.ui.platform.ComposeView\n        android:id=\"@+id/answer_view\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"@id/primary_text\"\n        app:layout_constraintTop_toBottomOf=\"@id/primary_text\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/hierarchy_repeatable_group_instance_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingVertical=\"@dimen/margin_small\"\n    android:paddingHorizontal=\"@dimen/margin_standard\">\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/primary_text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"?textAppearanceTitleLarge\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:text=\"Household > 1\" />\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/hierarchy_repeatable_group_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingVertical=\"@dimen/margin_small\"\n    android:paddingHorizontal=\"@dimen/margin_standard\">\n\n    <ImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintBottom_toBottomOf=\"@id/primary_text\"\n        app:layout_constraintTop_toTopOf=\"@id/group_label\"\n        app:srcCompat=\"@drawable/ic_repeat\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/group_label\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_extra_small\"\n        android:textAppearance=\"?textAppearanceBodyMedium\"\n        android:text=\"@string/repeatable_group_label\"\n        app:layout_constraintStart_toEndOf=\"@id/icon\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/primary_text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"?textAppearanceTitleLarge\"\n        app:layout_constraintStart_toStartOf=\"@id/group_label\"\n        app:layout_constraintTop_toBottomOf=\"@id/group_label\"\n        tools:text=\"Households\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/identify_user_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fillViewport=\"true\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center\"\n            android:orientation=\"vertical\"\n            android:padding=\"@dimen/margin_extra_large\">\n\n            <LinearLayout\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center_horizontal\"\n                android:orientation=\"vertical\">\n\n                <ImageView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    app:srcCompat=\"@drawable/ic_person_24dp\" />\n\n                <TextView\n                    style=\"@style/TextAppearance.Collect.Headline6\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"@dimen/margin_standard\"\n                    android:text=\"@string/enter_identity\" />\n\n                <TextView\n                    style=\"@style/TextAppearance.Collect.Body1\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"@dimen/margin_extra_small\"\n                    android:text=\"@string/enter_identity_description\" />\n\n            </LinearLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                style=\"?textInputFilledStyle\"\n                android:layout_width=\"280dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/margin_extra_large\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:id=\"@+id/identity\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:hint=\"@string/identity\"\n                    android:imeOptions=\"actionDone\"\n                    android:inputType=\"text\"\n                    android:lines=\"1\"\n                    android:saveEnabled=\"false\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n        </LinearLayout>\n    </androidx.core.widget.NestedScrollView>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/image_widget.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/capture_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/capture_image\"\n        android:contentDescription=\"@string/capture_image\"\n        app:icon=\"@drawable/ic_baseline_photo_camera_white_24\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/choose_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/choose_image\"\n        android:contentDescription=\"@string/choose_image\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        app:icon=\"@drawable/ic_baseline_photo_library_white_24\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/capture_button\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/error_message\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        android:text=\"@string/selected_invalid_image\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/choose_button\" />\n\n    <ImageView\n        android:id=\"@+id/image\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        android:tag=\"ImageView\"\n        android:adjustViewBounds=\"true\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/error_message\"\n        tools:src=\"@drawable/notes\"/>\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/instance_uploader_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <org.odk.collect.android.instancemanagement.send.ReadyToSendBanner\n            android:id=\"@+id/ready_to_send_banner\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginHorizontal=\"@dimen/margin_standard\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"/>\n\n        <ListView\n            android:id=\"@+id/scrollable_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:nestedScrollingEnabled=\"true\"\n            android:divider=\"@drawable/list_item_divider\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            app:layout_constraintBottom_toTopOf=\"@id/buttonholder\"\n            app:layout_constraintTop_toBottomOf=\"@id/ready_to_send_banner\" />\n\n        <org.odk.collect.lists.EmptyListView\n            android:id=\"@android:id/empty\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:icon=\"@drawable/ic_baseline_send_72\"\n            app:title=\"@string/empty_list_of_finalized_forms_title\"\n            app:subtitle=\"@string/empty_list_of_finalized_forms_subtitle\"/>\n\n        <LinearLayout\n            android:id=\"@+id/buttonholder\"\n            style=\"@style/Widget.AndroidShared.ButtonBar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/toggle_button\"\n                style=\"?materialButtonOutlinedStyle\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"match_parent\"\n                android:layout_marginStart=\"@dimen/margin_standard\"\n                android:layout_weight=\"1\"\n                android:text=\"@string/select_all\" />\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/upload_button\"\n                style=\"?materialButtonStyle\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"match_parent\"\n                android:layout_marginStart=\"@dimen/margin_standard\"\n                android:layout_marginEnd=\"@dimen/margin_standard\"\n                android:layout_weight=\"1\"\n                android:text=\"@string/send_selected_data\" />\n        </LinearLayout>\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/label_widget.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/question_widget_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"@dimen/margin_standard\">\n\n    <org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel\n        android:id=\"@+id/question_label\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentLeft=\"true\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_toLeftOf=\"@id/center\"\n        tools:ignore=\"RtlHardcoded\" />\n\n    <FrameLayout\n        android:id=\"@+id/help_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/question_label\"\n        android:orientation=\"vertical\">\n\n        <include layout=\"@layout/help_layout\" />\n\n    </FrameLayout>\n\n    <FrameLayout\n        android:id=\"@+id/center\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerInParent=\"true\" />\n\n    <LinearLayout\n        android:id=\"@+id/answer_container\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_toRightOf=\"@id/center\"\n        android:gravity=\"end\"\n        android:orientation=\"horizontal\"\n        tools:ignore=\"RtlHardcoded\">\n\n    </LinearLayout>\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/help_text\">\n\n        <include layout=\"@layout/question_error_layout\" />\n    </FrameLayout>\n</RelativeLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/launch_intent_button_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/launch_intent\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_margin=\"@dimen/margin_standard\"\n        app:icon=\"@drawable/ic_baseline_open_in_new_white_24\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/main_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\"/>\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:paddingBottom=\"@dimen/margin_standard\"\n            app:layout_constraintTop_toTopOf=\"parent\">\n\n            <androidx.fragment.app.FragmentContainerView\n                android:id=\"@+id/map_box_initialization_fragment\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"1dp\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <include\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:id=\"@+id/google_drive_deprecation_banner\"\n                layout=\"@layout/google_drive_deprecation_banner\"\n                android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n                android:layout_marginHorizontal=\"@dimen/margin_standard\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/map_box_initialization_fragment\" />\n\n            <org.odk.collect.android.mainmenu.StartNewFormButton\n                android:id=\"@+id/enter_data\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/margin_extra_small\"\n                app:layout_constraintWidth_max=\"@dimen/max_content_width\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/google_drive_deprecation_banner\" />\n\n            <org.odk.collect.android.mainmenu.MainMenuButton\n                android:id=\"@+id/review_data\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                app:icon=\"@drawable/ic_edit_24\"\n                app:layout_constraintEnd_toEndOf=\"@id/enter_data\"\n                app:layout_constraintStart_toStartOf=\"@id/enter_data\"\n                app:layout_constraintTop_toBottomOf=\"@id/enter_data\"\n                app:name=\"@string/review_data\" />\n\n            <org.odk.collect.android.mainmenu.MainMenuButton\n                android:id=\"@+id/send_data\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                app:icon=\"@drawable/ic_send_24\"\n                app:layout_constraintEnd_toEndOf=\"@id/enter_data\"\n                app:layout_constraintStart_toStartOf=\"@id/enter_data\"\n                app:layout_constraintTop_toBottomOf=\"@id/review_data\"\n                app:name=\"@string/send_data\"\n                app:highlightable=\"true\"/>\n\n            <org.odk.collect.android.mainmenu.MainMenuButton\n                android:id=\"@+id/view_sent_forms\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                app:icon=\"@drawable/ic_check_circle_24\"\n                app:layout_constraintEnd_toEndOf=\"@id/enter_data\"\n                app:layout_constraintStart_toStartOf=\"@id/enter_data\"\n                app:layout_constraintTop_toBottomOf=\"@id/send_data\"\n                app:name=\"@string/view_sent_forms\" />\n\n            <org.odk.collect.android.mainmenu.MainMenuButton\n                android:id=\"@+id/get_forms\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                app:icon=\"@drawable/ic_download_24\"\n                app:layout_constraintEnd_toEndOf=\"@id/enter_data\"\n                app:layout_constraintStart_toStartOf=\"@id/enter_data\"\n                app:layout_constraintTop_toBottomOf=\"@id/view_sent_forms\"\n                app:name=\"@string/get_forms\" />\n\n            <org.odk.collect.android.mainmenu.MainMenuButton\n                android:id=\"@+id/manage_forms\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                app:icon=\"@drawable/ic_delete_24\"\n                app:layout_constraintEnd_toEndOf=\"@id/enter_data\"\n                app:layout_constraintStart_toStartOf=\"@id/enter_data\"\n                app:layout_constraintTop_toBottomOf=\"@id/get_forms\"\n                app:name=\"@string/manage_files\" />\n\n            <TextView\n                android:id=\"@+id/app_name\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:textAppearance=\"?textAppearanceBody1\"\n                android:textColor=\"@color/color_on_surface_medium_emphasis\"\n                android:layout_marginTop=\"@dimen/margin_standard\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/manage_forms\"\n                tools:text=\"ODK Collect v2022.3\" />\n\n            <TextView\n                android:id=\"@+id/version_sha\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:textAppearance=\"?textAppearanceCaption\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@id/app_name\"\n                tools:text=\"commit sha\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n    </androidx.core.widget.NestedScrollView>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/main_menu_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/fragment_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:name=\"org.odk.collect.android.mainmenu.MainMenuFragment\"/>\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/main_menu_button.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginVertical=\"@dimen/margin_extra_small\"\n    android:layout_marginHorizontal=\"@dimen/margin_standard\"\n    android:elevation=\"1dp\"\n    android:background=\"@drawable/main_menu_button_background\">\n\n    <ImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_extra_large\"\n        android:layout_marginVertical=\"@dimen/margin_standard\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:src=\"@drawable/ic_delete\" />\n\n    <TextView\n        android:id=\"@+id/name\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginHorizontal=\"@dimen/margin_standard\"\n        android:ellipsize=\"end\"\n        android:maxLines=\"1\"\n        android:textAppearance=\"?textAppearanceLabelExtraLarge\"\n        app:layout_constraintBottom_toBottomOf=\"@id/icon\"\n        app:layout_constraintEnd_toStartOf=\"@+id/number\"\n        app:layout_constraintStart_toEndOf=\"@id/icon\"\n        app:layout_constraintTop_toTopOf=\"@id/icon\"\n        tools:text=\"Fill Blank Form\" />\n\n    <TextView\n        android:id=\"@+id/number\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_extra_large\"\n        android:textAppearance=\"?textAppearanceLabelExtraLarge\"\n        app:layout_constraintBottom_toBottomOf=\"@id/name\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/name\"\n        tools:text=\"10\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/manual_project_creator_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include\n        android:id=\"@+id/toolbar_layout\"\n        layout=\"@layout/app_bar_layout\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <androidx.core.widget.NestedScrollView\n            android:id=\"@+id/scrollable_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            app:layout_constraintBottom_toTopOf=\"@+id/bottom_container\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\">\n\n            <androidx.constraintlayout.widget.ConstraintLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:paddingBottom=\"@dimen/margin_standard\"\n                app:layout_constraintBottom_toTopOf=\"@+id/constraintLayout\">\n\n                <com.google.android.material.textfield.TextInputLayout\n                    android:id=\"@+id/url\"\n                    style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_margin=\"@dimen/margin_standard\"\n                    android:hint=\"@string/server_url\"\n                    app:layout_constraintTop_toTopOf=\"parent\">\n\n                    <com.google.android.material.textfield.TextInputEditText\n                        android:id=\"@+id/url_input_text\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:singleLine=\"true\" />\n\n                </com.google.android.material.textfield.TextInputLayout>\n\n                <com.google.android.material.textfield.TextInputLayout\n                    android:id=\"@+id/username\"\n                    style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_margin=\"@dimen/margin_standard\"\n                    android:hint=\"@string/username\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/url\">\n\n                    <com.google.android.material.textfield.TextInputEditText\n                        android:id=\"@+id/username_input_text\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:singleLine=\"true\" />\n\n                </com.google.android.material.textfield.TextInputLayout>\n\n                <com.google.android.material.textfield.TextInputLayout\n                    android:id=\"@+id/password\"\n                    style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_margin=\"@dimen/margin_standard\"\n                    android:hint=\"@string/password\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/username\"\n                    tools:layout_editor_absoluteX=\"16dp\">\n\n                    <com.google.android.material.textfield.TextInputEditText\n                        android:id=\"@+id/password_input_text\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:inputType=\"textPassword\"\n                        android:singleLine=\"true\" />\n\n                </com.google.android.material.textfield.TextInputLayout>\n\n                <androidx.constraintlayout.widget.ConstraintLayout\n                    android:id=\"@+id/config_tip\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_margin=\"@dimen/margin_standard\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/password\">\n\n                    <ImageView\n                        android:id=\"@+id/tip_icon\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:src=\"@drawable/ic_outline_info_24\"\n                        app:layout_constraintStart_toStartOf=\"parent\"\n                        app:layout_constraintTop_toTopOf=\"parent\" />\n\n                    <TextView\n                        android:id=\"@+id/tip_text\"\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"@dimen/margin_extra_small\"\n                        android:text=\"@string/project_config_tip\"\n                        android:textAppearance=\"?textAppearanceBody1\"\n                        app:layout_constraintEnd_toEndOf=\"parent\"\n                        app:layout_constraintStart_toEndOf=\"@+id/tip_icon\"\n                        app:layout_constraintTop_toTopOf=\"parent\" />\n                </androidx.constraintlayout.widget.ConstraintLayout>\n            </androidx.constraintlayout.widget.ConstraintLayout>\n        </androidx.core.widget.NestedScrollView>\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/bottom_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintBottom_toBottomOf=\"parent\">\n\n            <ImageView\n                android:id=\"@+id/shadow_up\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:src=\"@drawable/shadow_up\"\n                app:layout_constraintBottom_toTopOf=\"@+id/cancel_button\" />\n\n            <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n                android:id=\"@+id/cancel_button\"\n                style=\"?borderlessButtonStyle\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/cancel\"\n                app:layout_constraintEnd_toStartOf=\"@+id/add_button\"\n                app:layout_constraintTop_toTopOf=\"@+id/add_button\"\n                app:layout_constraintBottom_toBottomOf=\"@+id/add_button\" />\n\n            <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n                android:id=\"@+id/add_button\"\n                style=\"?borderlessButtonStyle\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"@dimen/margin_extra_extra_small\"\n                android:enabled=\"false\"\n                android:text=\"@string/add\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintBottom_toBottomOf=\"parent\" />\n        </androidx.constraintlayout.widget.ConstraintLayout>\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/map_button.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/map_button\"\n    style=\"?materialIconButtonOutlinedStyle\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:contentDescription=\"@string/open_form_map\"\n    app:icon=\"@drawable/ic_map\" />\n"
  },
  {
    "path": "collect_app/src/main/res/layout/no_buttons_item_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:paddingStart=\"@dimen/margin_extra_extra_small\"\n    android:paddingEnd=\"@dimen/margin_extra_extra_small\"\n    android:paddingTop=\"@dimen/margin_extra_extra_small\"\n    android:paddingBottom=\"@dimen/margin_extra_extra_small\">\n\n    <ImageView\n        android:id=\"@+id/imageView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:adjustViewBounds=\"true\"\n        android:visibility=\"gone\"/>\n\n    <TextView\n        android:id=\"@+id/label\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:visibility=\"gone\"/>\n</FrameLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/number_picker_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2017 Nafundi\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\nhttp://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\">\n    <org.odk.collect.android.views.CustomNumberPicker\n        android:id=\"@+id/number_picker\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:descendantFocusability=\"blocksDescendants\"\n        android:scrollbars=\"none\"\n        android:layoutDirection=\"ltr\"\n        android:layout_gravity=\"center_horizontal\">\n    </org.odk.collect.android.views.CustomNumberPicker>\n</FrameLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/odk_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright 2018 Nafundi\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/odk_view_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/group_text\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:layout_marginBottom=\"@dimen/margin_extra_small\"\n            android:layout_marginHorizontal=\"@dimen/margin_standard\"\n            android:visibility=\"gone\"\n            style=\"@style/TextAppearance.Collect.Subtitle1.MediumEmphasis\"\n            tools:text=\"Group text\"\n            tools:visibility=\"visible\" />\n\n        <LinearLayout\n            android:id=\"@+id/widgets\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:paddingVertical=\"@dimen/margin_extra_extra_small\"\n            android:gravity=\"top\"\n            android:orientation=\"vertical\">\n\n        </LinearLayout>\n\n    </LinearLayout>\n\n</androidx.core.widget.NestedScrollView>"
  },
  {
    "path": "collect_app/src/main/res/layout/osm_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\" android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/launch_open_map_kit_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/capture_osm\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/error_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:text=\"@string/invalid_osm_data\"\n        android:visibility=\"gone\"/>\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/osm_file_header_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:text=\"@string/edited_osm_file\"\n        android:textSize=\"20sp\"\n        android:textStyle=\"bold\"\n        android:textColor=\"?colorOnSurface\"\n        android:textAppearance=\"@style/TextAppearance.Collect.Body1\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/osm_file_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:paddingHorizontal=\"@dimen/margin_large\"\n        android:textSize=\"18sp\"\n        android:textStyle=\"italic\"\n        android:textColor=\"?colorOnSurface\"\n        android:textAppearance=\"@style/TextAppearance.Collect.Body1\"/>\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/password_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:paddingTop=\"@dimen/margin_standard\"\n    android:paddingLeft=\"@dimen/margin_large\"\n    android:paddingRight=\"@dimen/margin_large\">\n\n    <TextView\n        android:id=\"@+id/pwd_label\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentTop=\"true\"\n        android:text=\"@string/enter_new_password\"\n        android:textAppearance=\"?android:attr/textAppearanceMedium\"\n        android:textColor=\"@android:color/darker_gray\" />\n\n    <EditText\n        android:id=\"@+id/pwd_field\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@+id/pwd_label\"\n        android:layout_marginTop=\"5dp\"\n        android:inputType=\"textPassword\" />\n\n    <CheckBox\n        android:text=\"@string/show_password\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:id=\"@+id/checkBox2\"\n        android:layout_below=\"@+id/pwd_field\"\n        android:layout_alignStart=\"@+id/pwd_field\" />\n\n</RelativeLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/permissions_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:paddingHorizontal=\"@dimen/margin_large\"\n    android:paddingTop=\"@dimen/margin_standard\"\n    android:paddingBottom=\"@dimen/margin_large\">\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/permission_text\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/permission_dialog_text\"\n        android:textAppearance=\"?textAppearanceBodyMedium\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/notifications_icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_extra_large\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/permission_text\"\n        app:srcCompat=\"@drawable/ic_baseline_notifications_24\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/notifications_title\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin\"\n        android:text=\"@string/notifications\"\n        android:textAppearance=\"?textAppearanceBodyLarge\"\n        app:layout_constraintBottom_toTopOf=\"@id/notification_description\"\n        app:layout_constraintStart_toEndOf=\"@id/notifications_icon\"\n        app:layout_constraintTop_toTopOf=\"@id/notifications_icon\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/notification_description\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin\"\n        android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n        android:text=\"@string/required_for_notifications\"\n        android:textAppearance=\"?textAppearanceBodyMedium\"\n        android:textColor=\"@color/color_on_surface_medium_emphasis\"\n        app:layout_constraintBottom_toBottomOf=\"@id/notifications_icon\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toEndOf=\"@id/notifications_icon\"\n        app:layout_constraintTop_toBottomOf=\"@id/notifications_title\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/printer_widget.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/printer_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/print\"\n        app:icon=\"@drawable/ic_baseline_print_white_24\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/progress_bar_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:id=\"@+id/main_layout\"\n    android:layout_height=\"wrap_content\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n    <View\n        android:id=\"@+id/divider\"\n        android:background=\"?android:attr/listDivider\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"3dp\"/>\n    <View\n        android:id=\"@+id/progress_view\"\n        android:background=\"?colorPrimary\"\n        android:layout_width=\"0dp\"\n        tools:layout_width=\"match_parent\"\n        android:layout_height=\"3dp\"/>\n</FrameLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/project_icon_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/project_icon_text\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@drawable/project_icon_background\"\n    android:textAppearance=\"@style/TextAppearance.Collect.Subtitle1\"\n    android:layout_marginVertical=\"@dimen/margin_extra_small\"\n    android:textColor=\"@android:color/white\"\n    android:textAllCaps=\"true\"\n    android:gravity=\"center\" />"
  },
  {
    "path": "collect_app/src/main/res/layout/project_list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:paddingVertical=\"@dimen/margin_extra_extra_small\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <org.odk.collect.android.projects.ProjectIconView\n        android:id=\"@+id/project_icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:text=\"A\"/>\n\n    <LinearLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/project_icon\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toEndOf=\"@+id/project_icon\"\n        app:layout_constraintTop_toTopOf=\"@+id/project_icon\">\n\n        <TextView\n            android:id=\"@+id/project_name\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"@dimen/margin_extra_small\"\n            android:ellipsize=\"end\"\n            android:maxLines=\"1\"\n            android:textAppearance=\"?textAppearanceSubtitle1\"\n            tools:text=\"A project\"/>\n\n        <TextView\n            android:id=\"@+id/project_subtext\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"@dimen/margin_extra_small\"\n            android:ellipsize=\"end\"\n            android:maxLines=\"2\"\n            android:textAppearance=\"@style/TextAppearance.Collect.Body2.MediumEmphasis\"\n            tools:text=\"my.cool.project.com\"/>\n    </LinearLayout>\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/project_settings_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <androidx.constraintlayout.widget.Guideline\n        android:id=\"@+id/guideline_start\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        app:layout_constraintGuide_begin=\"@dimen/margin_standard\" />\n\n    <androidx.constraintlayout.widget.Guideline\n        android:id=\"@+id/guideline_end\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        app:layout_constraintGuide_end=\"@dimen/margin_standard\" />\n\n    <ImageView\n        android:id=\"@+id/close_icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:background=\"?attr/selectableItemBackgroundBorderless\"\n        android:src=\"@drawable/ic_close\"\n        app:layout_constraintEnd_toStartOf=\"@id/guideline_end\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <TextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/projects\"\n        app:layout_constraintBottom_toBottomOf=\"@id/close_icon\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/close_icon\" />\n\n    <org.odk.collect.android.projects.ProjectListItemView\n        android:id=\"@+id/current_project\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        app:layout_constraintEnd_toStartOf=\"@id/guideline_end\"\n        app:layout_constraintStart_toStartOf=\"@id/guideline_start\"\n        app:layout_constraintTop_toBottomOf=\"@id/title\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/general_settings_button\"\n        style=\"?materialButtonOutlinedIconStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"38dp\"\n        android:text=\"@string/settings\"\n        app:icon=\"@drawable/ic_baseline_settings_24\"\n        app:layout_constraintStart_toStartOf=\"@id/current_project\"\n        app:layout_constraintTop_toBottomOf=\"@id/current_project\" />\n\n    <View\n        android:id=\"@+id/top_divider\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:alpha=\"0.2\"\n        android:background=\"?colorOnSurface\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/general_settings_button\" />\n\n    <ScrollView\n        android:id=\"@+id/project_list_container\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constrainedHeight=\"true\"\n        app:layout_constraintBottom_toTopOf=\"@id/bottom_divider\"\n        app:layout_constraintEnd_toStartOf=\"@id/guideline_end\"\n        app:layout_constraintStart_toStartOf=\"@id/guideline_start\"\n        app:layout_constraintTop_toBottomOf=\"@id/top_divider\">\n\n        <LinearLayout\n            android:id=\"@+id/project_list\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\" />\n    </ScrollView>\n\n    <View\n        android:id=\"@+id/bottom_divider\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:alpha=\"0.2\"\n        android:background=\"?colorOnSurface\"\n        app:layout_constraintBottom_toTopOf=\"@id/add_project_button\"\n        app:layout_constraintStart_toStartOf=\"parent\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/add_project_button\"\n        style=\"?borderlessButtonIconStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_extra_small\"\n        android:text=\"@string/add_project\"\n        app:icon=\"@drawable/ic_baseline_qr_code_scanner_24\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toStartOf=\"@id/about_button\"\n        app:layout_constraintHorizontal_chainStyle=\"packed\"\n        app:layout_constraintStart_toStartOf=\"@id/guideline_start\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/about_button\"\n        style=\"?borderlessButtonIconStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:text=\"@string/about_preferences\"\n        app:icon=\"@drawable/ic_outline_info_24\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"@id/guideline_end\"\n        app:layout_constraintStart_toEndOf=\"@id/add_project_button\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/qr_code_project_creator_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include\n        android:id=\"@+id/toolbar_layout\"\n        layout=\"@layout/app_bar_layout\" />\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <androidx.core.widget.NestedScrollView\n            android:id=\"@+id/scrollable_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            app:layout_constraintBottom_toBottomOf=\"@+id/shadow_up\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\">\n\n            <androidx.constraintlayout.widget.ConstraintLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:paddingBottom=\"@dimen/margin_standard\"\n                app:layout_constraintBottom_toTopOf=\"@+id/constraintLayout\">\n\n                <com.google.android.material.textview.MaterialTextView\n                    android:id=\"@+id/title\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/scan_configuration_qr_code\"\n                    android:layout_marginTop=\"@dimen/margin_standard\"\n                    style=\"@style/TextAppearance.Collect.Subtitle1\"\n                    app:layout_constraintEnd_toEndOf=\"parent\"\n                    app:layout_constraintStart_toStartOf=\"parent\"\n                    app:layout_constraintTop_toTopOf=\"parent\" />\n\n                <org.odk.collect.qrcode.BarcodeScannerViewContainer\n                    android:id=\"@+id/barcode_view\"\n                    app:layout_constraintWidth_percent=\"0.75\"\n                    app:layout_constraintDimensionRatio=\"w,1:1\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"0dp\"\n                    android:layout_marginTop=\"@dimen/margin_extra_large\"\n                    app:layout_constraintEnd_toEndOf=\"parent\"\n                    app:layout_constraintStart_toStartOf=\"parent\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/title\" />\n\n                <com.google.android.material.textview.MaterialTextView\n                    android:id=\"@+id/or\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/or\"\n                    android:layout_marginTop=\"@dimen/margin_standard\"\n                    style=\"@style/TextAppearance.Collect.Subtitle1\"\n                    app:layout_constraintEnd_toEndOf=\"parent\"\n                    app:layout_constraintStart_toStartOf=\"parent\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/barcode_view\" />\n\n                <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n                    android:id=\"@+id/configure_manually_button\"\n                    style=\"?materialButtonOutlinedIconStyle\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"@dimen/margin_standard\"\n                    android:text=\"@string/configure_manually\"\n                    app:icon=\"@drawable/ic_outline_edit_24\"\n                    app:layout_constraintEnd_toEndOf=\"parent\"\n                    app:layout_constraintStart_toStartOf=\"parent\"\n                    app:layout_constraintTop_toBottomOf=\"@+id/or\" />\n            </androidx.constraintlayout.widget.ConstraintLayout>\n        </androidx.core.widget.NestedScrollView>\n\n        <ImageView\n            android:id=\"@+id/shadow_up\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:src=\"@drawable/shadow_up\"\n            app:layout_constraintBottom_toTopOf=\"@+id/cancel_button\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/cancel_button\"\n            style=\"?borderlessButtonStyle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/cancel\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/question_error_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/error_message_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"@dimen/margin_standard\"\n    android:visibility=\"gone\"\n    tools:visibility=\"visible\">\n\n    <ImageView\n        android:id=\"@+id/required_error_image\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_baseline_error_24\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/error_message\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_extra_small\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:textColor=\"?colorError\"\n        android:layout_gravity=\"center_vertical\"\n        android:textAppearance=\"?textAppearanceBodyLarge\"\n        app:layout_constraintStart_toEndOf=\"@id/required_error_image\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintBaseline_toBaselineOf=\"@id/required_error_image\"\n        tools:text=\"@string/required_answer_error\"/>\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/question_widget.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/question_widget_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:paddingHorizontal=\"@dimen/margin_standard\"\n    android:paddingVertical=\"@dimen/margin_extra_small\"\n    android:orientation=\"vertical\">\n\n    <org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel\n        android:id=\"@+id/question_label\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <FrameLayout\n        android:id=\"@+id/help_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginBottom=\"@dimen/margin_standard\">\n\n        <include layout=\"@layout/help_layout\" />\n\n    </FrameLayout>\n\n    <RelativeLayout\n        android:id=\"@+id/answer_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n    </RelativeLayout>\n\n    <include layout=\"@layout/question_error_layout\" />\n</LinearLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/quit_form_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"@dimen/margin_large\">\n\n        <com.google.android.material.textview.MaterialTextView\n            android:id=\"@+id/save_explanation\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/save_explanation\"\n            android:textAppearance=\"?textAppearanceBodyMedium\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/discard_changes\"\n            style=\"?materialButtonOutlinedIconStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginTop=\"@dimen/margin_standard\"\n            android:text=\"@string/discard_changes\"\n            app:icon=\"@drawable/ic_delete\"\n            app:iconGravity=\"textStart\"\n            app:iconSize=\"18dp\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/save_explanation\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/keep_editing_outlined\"\n            style=\"?materialButtonOutlinedIconStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:text=\"@string/keep_editing\"\n            app:icon=\"@drawable/ic_edit\"\n            app:iconGravity=\"textStart\"\n            app:iconSize=\"18dp\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/discard_changes\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/keep_editing_filled\"\n            style=\"?materialButtonStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:text=\"@string/keep_editing\"\n            android:visibility=\"gone\"\n            app:icon=\"@drawable/ic_edit\"\n            app:iconGravity=\"textStart\"\n            app:iconSize=\"18dp\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/discard_changes\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/save_changes\"\n            style=\"?materialButtonIconStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:text=\"@string/save_as_draft\"\n            app:icon=\"@drawable/ic_save\"\n            app:iconGravity=\"textStart\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"@id/discard_changes\"\n            app:layout_constraintStart_toStartOf=\"@id/discard_changes\"\n            app:layout_constraintTop_toBottomOf=\"@id/keep_editing_outlined\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n</androidx.core.widget.NestedScrollView>\n\n\n"
  },
  {
    "path": "collect_app/src/main/res/layout/range_picker_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/widget_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/select_value\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_format_list_numbered_white_24\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/widget_answer_text\"\n        style=\"@style/Widget.Collect.TextView.WidgetAnswer\"\n        android:text=\"@string/no_value_selected\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/range_widget_horizontal.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2017 Nafundi\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:clipChildren=\"false\"\n    android:clipToPadding=\"false\">\n\n    <TextView\n        android:id=\"@+id/current_value\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:textAppearance=\"?textAppearanceHeadline5\" />\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:clipChildren=\"false\"\n        android:clipToPadding=\"false\">\n\n        <org.odk.collect.android.views.TrackingTouchSlider\n            android:id=\"@+id/slider\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n\n    </FrameLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\">\n\n        <TextView\n            android:id=\"@+id/min_value\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layoutDirection=\"locale\"\n            android:textDirection=\"locale\"\n            android:textSize=\"20sp\" />\n\n        <View\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"/>\n\n        <TextView\n            android:id=\"@+id/max_value\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textSize=\"20sp\" />\n    </LinearLayout>\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/range_widget_vertical.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2017 Nafundi\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"horizontal\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_horizontal\"\n    android:clipChildren=\"false\"\n    android:clipToPadding=\"false\">\n\n    <TextView\n        android:id=\"@+id/current_value\"\n        android:minWidth=\"50dp\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:textAppearance=\"?textAppearanceHeadline5\" />\n\n    <FrameLayout\n        android:layout_width=\"50dp\"\n        android:layout_height=\"330dp\"\n        android:clipChildren=\"false\"\n        android:clipToPadding=\"false\"\n        android:layoutDirection=\"ltr\">\n\n        <org.odk.collect.android.views.TrackingTouchSlider\n            android:id=\"@+id/slider\"\n            android:layout_width=\"330dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:layerType=\"software\"\n            android:rotation=\"270\" />\n\n    </FrameLayout>\n\n    <RelativeLayout\n        android:minWidth=\"50dp\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/max_value\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentTop=\"true\"\n            android:textSize=\"20sp\" />\n\n        <TextView\n            android:id=\"@+id/min_value\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentBottom=\"true\"\n            android:textSize=\"20sp\" />\n\n    </RelativeLayout>\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/ranking_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2018 Nafundi\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"10dp\"\n    android:layout_marginTop=\"5dp\"\n    android:background=\"?colorSurfaceContainerHighest\">\n\n    <TextView\n        android:id=\"@+id/rank_item_text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textColor=\"?colorOnSurface\" />\n</FrameLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/ranking_widget.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/rank_items_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/rank_items\"\n        android:contentDescription=\"@string/rank_items\"\n        app:icon=\"@drawable/ic_baseline_format_list_bulleted_white_24\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/answer\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/rank_items_button\"\n        tools:text=\"1. A\\n2. B\\n3. C\\n4. D\"/>\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/rating_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"match_parent\">\n\n    <RatingBar\n        android:id=\"@+id/rating_bar1\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:saveEnabled=\"false\"\n        android:stepSize=\"1.0\" />\n\n    <RatingBar\n        android:id=\"@+id/rating_bar2\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:visibility=\"gone\"\n        android:saveEnabled=\"false\"\n        android:stepSize=\"1.0\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/ready_to_send_banner.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/banner\"\n    android:background=\"?colorSurface\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:visibility=\"gone\"\n    tools:visibility=\"visible\">\n\n    <com.google.android.material.card.MaterialCardView\n        style=\"@style/Widget.Material3.CardView.Filled\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:paddingVertical=\"@dimen/margin_standard\">\n\n            <ImageView\n                android:id=\"@+id/icon\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"@dimen/margin_standard\"\n                app:srcCompat=\"@drawable/ic_send_24\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\"\n                app:tint=\"?colorPrimary\"/>\n\n            <com.google.android.material.textview.MaterialTextView\n                android:id=\"@+id/title\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"@dimen/margin_standard\"\n                android:layout_marginEnd=\"@dimen/margin_standard\"\n                android:textAppearance=\"?textAppearanceTitleMedium\"\n                android:textColor=\"?colorPrimary\"\n                app:layout_constraintStart_toEndOf=\"@id/icon\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"@id/icon\"\n                tools:text=\"Last sent: 2 days ago\"/>\n\n            <com.google.android.material.textview.MaterialTextView\n                android:id=\"@+id/subtext\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:textAppearance=\"?textAppearanceBodyMedium\"\n                app:layout_constraintStart_toStartOf=\"@id/title\"\n                app:layout_constraintEnd_toEndOf=\"@id/title\"\n                app:layout_constraintTop_toBottomOf=\"@id/title\"\n                tools:text=\"4 forms ready to send\"/>\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n    </com.google.android.material.card.MaterialCardView>\n</FrameLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/reset_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2017 Nafundi\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_margin=\"5dp\"\n    android:fillViewport=\"true\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"10dp\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textColor=\"?colorError\"\n            android:textStyle=\"bold\"\n            android:textAlignment=\"center\"\n            android:padding=\"5dp\"\n            android:text=\"@string/reset_app_state_warning\" />\n\n        <androidx.appcompat.widget.AppCompatCheckBox\n            android:id=\"@+id/preferences\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/reset_settings\"\n            android:padding=\"5dp\"\n            android:button=\"@null\"\n            app:buttonCompat=\"@null\"\n            android:background=\"?android:attr/selectableItemBackground\"\n            android:drawableEnd=\"?android:attr/listChoiceIndicatorMultiple\" />\n\n        <androidx.appcompat.widget.AppCompatCheckBox\n            android:id=\"@+id/instances\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/reset_saved_forms\"\n            android:padding=\"5dp\"\n            android:button=\"@null\"\n            app:buttonCompat=\"@null\"\n            android:background=\"?android:attr/selectableItemBackground\"\n            android:drawableEnd=\"?android:attr/listChoiceIndicatorMultiple\" />\n\n        <androidx.appcompat.widget.AppCompatCheckBox\n            android:id=\"@+id/forms\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/reset_blank_forms\"\n            android:padding=\"5dp\"\n            android:button=\"@null\"\n            app:buttonCompat=\"@null\"\n            android:background=\"?android:attr/selectableItemBackground\"\n            android:drawableEnd=\"?android:attr/listChoiceIndicatorMultiple\" />\n\n        <androidx.appcompat.widget.AppCompatCheckBox\n            android:id=\"@+id/cache\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/reset_cache\"\n            android:padding=\"5dp\"\n            android:button=\"@null\"\n            app:buttonCompat=\"@null\"\n            android:background=\"?android:attr/selectableItemBackground\"\n            android:drawableEnd=\"?android:attr/listChoiceIndicatorMultiple\" />\n\n        <androidx.appcompat.widget.AppCompatCheckBox\n            android:id=\"@+id/layers\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/reset_layers\"\n            android:padding=\"5dp\"\n            android:button=\"@null\"\n            app:buttonCompat=\"@null\"\n            android:background=\"?android:attr/selectableItemBackground\"\n            android:drawableEnd=\"?android:attr/listChoiceIndicatorMultiple\" />\n\n    </LinearLayout>\n</ScrollView>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/select_image_map_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\"\n    android:paddingEnd=\"@dimen/margin_extra_large\">\n\n    <org.odk.collect.android.views.CustomWebView\n        android:id=\"@+id/image_map\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/selected_elements\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n        android:textAppearance=\"?textAppearanceBody2\"\n        tools:text=\"Selected image\" />\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/select_list_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.textfield.TextInputEditText\n        android:id=\"@+id/choices_search_box\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:saveEnabled=\"false\"\n        android:visibility=\"gone\" />\n\n    <org.odk.collect.android.views.ChoicesRecyclerView\n        android:id=\"@+id/choices_recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/select_minimal_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <org.odk.collect.android.views.ChoicesRecyclerView\n        android:id=\"@+id/choices_recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:paddingHorizontal=\"@dimen/margin_standard\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n    </org.odk.collect.android.views.ChoicesRecyclerView>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/select_minimal_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeTextInputEditText\n        android:id=\"@+id/answer\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingVertical=\"@dimen/margin_extra_small\"\n        android:paddingHorizontal=\"@dimen/margin_extra_small\"\n        android:text=\"@string/select_answer\"\n        android:focusable=\"false\"\n        android:drawableEnd=\"@drawable/ic_arrow_drop_down\"\n        android:cursorVisible=\"false\"\n        android:inputType=\"textMultiLine|textNoSuggestions\"\n        android:saveEnabled=\"false\"/>\n</FrameLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/select_multi_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.appcompat.widget.AppCompatCheckBox xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingVertical=\"@dimen/margin_extra_small\"\n    android:paddingStart=\"@dimen/margin_standard\"\n    tools:text=\"Option A\">\n\n</androidx.appcompat.widget.AppCompatCheckBox>"
  },
  {
    "path": "collect_app/src/main/res/layout/select_one_from_map_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/select_place\"\n        app:icon=\"@drawable/ic_baseline_location_on_white_24\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/answer\"\n        style=\"@style/Widget.Collect.TextView.WidgetAnswer\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/button\"\n        tools:text=\"Blah\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/select_one_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.appcompat.widget.AppCompatRadioButton xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingVertical=\"@dimen/margin_extra_small\"\n    android:paddingStart=\"@dimen/margin_standard\"\n    tools:text=\"Option A\">\n\n</androidx.appcompat.widget.AppCompatRadioButton>"
  },
  {
    "path": "collect_app/src/main/res/layout/server_auth_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright (C) 2008 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n  \n          http://www.apache.org/licenses/LICENSE-2.0\n  \n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <EditText\n        android:id=\"@+id/username_edit\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginLeft=\"20dip\"\n        android:layout_marginRight=\"20dip\"\n        android:gravity=\"fill_horizontal\"\n        android:hint=\"@string/username\"\n        android:inputType=\"textNoSuggestions\"\n        android:scrollHorizontally=\"true\"\n        android:textAppearance=\"?android:attr/textAppearanceMedium\" />\n\n    <EditText\n        android:id=\"@+id/password_edit\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginLeft=\"20dip\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginRight=\"20dip\"\n        android:gravity=\"fill_horizontal\"\n        android:hint=\"@string/password\"\n        android:inputType=\"textPassword\"\n        android:scrollHorizontally=\"true\"\n        android:textAppearance=\"?android:attr/textAppearanceMedium\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/show_qrcode_fragment.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n     Copyright (C) 2018 Shobhit\n\n     Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n     in compliance with the License. You may obtain a copy of the License at\n\n     http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software distributed under the License\n     is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n     or implied. See the License for the specific language governing permissions and limitations under\n     the License.\n-->\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_above=\"@+id/status\">\n\n        <ProgressBar\n            android:id=\"@+id/circularProgressBar\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:visibility=\"gone\" />\n\n        <ImageView\n            android:id=\"@+id/ivQRcode\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:contentDescription=\"@string/qr_code\" />\n    </FrameLayout>\n\n    <LinearLayout\n        android:id=\"@+id/status\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentBottom=\"true\"\n        android:background=\"?colorError\"\n        android:gravity=\"center\"\n        android:orientation=\"horizontal\"\n        android:paddingTop=\"5dp\"\n        android:paddingBottom=\"5dp\"\n        android:visibility=\"visible\">\n\n        <ImageView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:layout_margin=\"10dp\"\n            android:src=\"@android:drawable/ic_dialog_alert\"\n            app:tint=\"?colorOnError\" />\n\n        <TextView\n            android:id=\"@+id/tvPasswordWarning\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center_vertical\"\n            android:padding=\"5dp\"\n            android:textColor=\"?colorOnError\"\n            android:textSize=\"15sp\"\n            tools:text=\"@string/qrcode_with_both_passwords\" />\n\n    </LinearLayout>\n</RelativeLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/signature_widget.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/sign_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/sign_button\"\n        android:contentDescription=\"@string/sign_button\"\n        app:icon=\"@drawable/ic_baseline_signature_white_24\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/error_message\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        android:text=\"@string/selected_invalid_image\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/sign_button\" />\n\n    <ImageView\n        android:id=\"@+id/image\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        android:tag=\"ImageView\"\n        android:adjustViewBounds=\"true\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/error_message\"\n        tools:src=\"@drawable/notes\"/>\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/sort_item_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\nCopyright (C) 2018 Shobhit\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n\n-->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:foreground=\"?android:attr/selectableItemBackground\"\n    android:orientation=\"horizontal\"\n    android:paddingBottom=\"8dp\"\n    android:paddingTop=\"8dp\">\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        tools:src=\"@drawable/ic_sort\" />\n\n    <TextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_marginLeft=\"16dp\"\n        android:layout_marginRight=\"16dp\"\n        android:textAppearance=\"@style/Base.TextAppearance.AppCompat.Medium\"\n        tools:text=\"Sort by Name\" />\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/start_new_from_button.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginVertical=\"@dimen/margin_extra_small\"\n    android:layout_marginHorizontal=\"@dimen/margin_standard\"\n    android:elevation=\"1dp\"\n    android:background=\"@drawable/start_new_form_button_background\">\n\n    <ImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginVertical=\"@dimen/margin_standard\"\n        android:src=\"@drawable/ic_add_white_24\"\n        app:tint=\"?colorOnPrimary\"\n        app:layout_constraintHorizontal_chainStyle=\"packed\"\n        app:layout_constraintBottom_toBottomOf=\"@id/name\"\n        app:layout_constraintEnd_toStartOf=\"@id/name\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/name\" />\n\n    <TextView\n        android:id=\"@+id/name\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:ellipsize=\"end\"\n        android:textAppearance=\"?textAppearanceLabelExtraLarge\"\n        android:maxLines=\"1\"\n        android:textColor=\"?colorOnPrimary\"\n        android:text=\"@string/enter_data\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toEndOf=\"@id/icon\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/tabs_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <com.google.android.material.tabs.TabLayout\n            android:id=\"@+id/tabLayout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:tabGravity=\"fill\"\n            app:tabMaxWidth=\"0dp\"\n            app:tabIndicatorHeight=\"4dp\"\n            app:tabMode=\"fixed\"\n            android:layout_gravity=\"top\" />\n\n        <androidx.viewpager2.widget.ViewPager2\n            android:id=\"@+id/viewPager\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n        </androidx.viewpager2.widget.ViewPager2>\n    </LinearLayout>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/time_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/time_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/select_time\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_time_filled_white_24\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/time_answer_text\"\n        style=\"@style/Widget.Collect.TextView.WidgetAnswer\"\n        android:text=\"@string/no_time_selected\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/transparent_progress_screen.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:focusable=\"true\"\n    android:clickable=\"true\"\n    android:background=\"#1F000000\">\n\n    <com.google.android.material.progressindicator.LinearProgressIndicator\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        android:indeterminate=\"true\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/trigger_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <androidx.appcompat.widget.AppCompatCheckBox xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        android:id=\"@+id/check_box\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?selectableItemBackground\"\n        android:button=\"@null\"\n        android:drawableStart=\"?android:attr/listChoiceIndicatorMultiple\"\n        android:drawablePadding=\"@dimen/margin_extra_small\"\n        android:orientation=\"vertical\"\n        android:translationX=\"-5dp\"\n        android:text=\"@string/trigger\"\n        app:buttonCompat=\"@null\">\n    </androidx.appcompat.widget.AppCompatCheckBox>\n\n</FrameLayout>"
  },
  {
    "path": "collect_app/src/main/res/layout/url_widget_answer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/url_button\"\n        style=\"?widgetButtonIconQuestionWidgetStyle\"\n        android:text=\"@string/open_url\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:icon=\"@drawable/ic_baseline_open_in_new_white_24\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/waveform_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.visualizer.amplitude.AudioRecordView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    app:chunkAlignTo=\"bottom\"\n    app:chunkColor=\"?colorPrimary\"\n    app:chunkMinHeight=\"2dp\"\n    app:chunkRoundedCorners=\"true\"\n    app:chunkSoftTransition=\"true\"\n    app:chunkSpace=\"1dp\"\n    app:chunkWidth=\"5dp\" />\n\n"
  },
  {
    "path": "collect_app/src/main/res/layout/widget_answer_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/answer_fragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "collect_app/src/main/res/layout/widget_answer_text.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <com.google.android.material.textfield.TextInputLayout\n        style=\"?textInputFilledStyle\"\n        android:id=\"@+id/text_input_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:errorTextAppearance=\"?textAppearanceBodyLarge\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            android:id=\"@+id/edit_text\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:paddingVertical=\"@dimen/margin_standard\"/>\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/text_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textColor=\"?colorOnSurface\"/>\n</FrameLayout>"
  },
  {
    "path": "collect_app/src/main/res/menu/blank_form_list_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  Copyright (C) 2017 Shobhit Licensed under the Apache\n  License, Version 2.0 (the \"License\"); you may not use this file except in\n  compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n  Unless required by applicable law or agreed to in writing, software distributed\n  under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES\n  OR CONDITIONS OF ANY KIND, either express or implied. See the License for\n  the specific language governing permissions and limitations under the License.\n-->\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_refresh\"\n        android:icon=\"@drawable/ic_baseline_refresh_24\"\n        android:title=\"@string/refresh\"\n        android:visible=\"false\"\n        app:showAsAction=\"always\"/>\n\n    <item\n        android:id=\"@+id/menu_sort\"\n        android:icon=\"@drawable/ic_baseline_sort_24\"\n        android:title=\"@string/sort_the_list\"\n        app:showAsAction=\"always\"/>\n\n    <item\n        android:id=\"@+id/menu_filter\"\n        android:icon=\"@drawable/ic_baseline_search_24\"\n        android:title=\"@string/filter_the_list\"\n        app:actionViewClass=\"androidx.appcompat.widget.SearchView\"\n        app:showAsAction=\"always|collapseActionView\"/>\n\n</menu>\n"
  },
  {
    "path": "collect_app/src/main/res/menu/changes_reason_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:title=\"@string/save\"\n        app:showAsAction=\"always\" />\n</menu>"
  },
  {
    "path": "collect_app/src/main/res/menu/drafts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:id=\"@+id/bulk_finalize\"\n        android:showAsAction=\"never\"\n        android:icon=\"@drawable/ic_baseline_done_all_24\"\n        android:title=\"@string/finalize_all_drafts\">\n    </item>\n</menu>\n"
  },
  {
    "path": "collect_app/src/main/res/menu/form_hierarchy_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_edit\"\n        android:icon=\"@drawable/ic_edit_24\"\n        android:title=\"@string/edit_finalized_form\"\n        android:contentDescription=\"@string/edit_finalized_form\"\n        android:visible=\"false\"\n        app:showAsAction=\"always\" />\n\n    <item\n        android:id=\"@+id/menu_delete_child\"\n        android:icon=\"@drawable/ic_delete_24\"\n        android:title=\"@string/delete_repeat\"\n        android:visible=\"false\"\n        app:showAsAction=\"always\" />\n\n    <item\n        android:id=\"@+id/menu_add_repeat\"\n        android:icon=\"@drawable/ic_add_circle_24\"\n        android:title=\"@string/add_another_menu\"\n        android:visible=\"false\"\n        app:showAsAction=\"always\" />\n\n    <item\n        android:id=\"@+id/menu_go_up\"\n        android:icon=\"@drawable/ic_arrow_up\"\n        android:title=\"@string/jump_to_previous\"\n        android:visible=\"false\"\n        app:showAsAction=\"always\" />\n\n</menu>\n"
  },
  {
    "path": "collect_app/src/main/res/menu/form_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  Copyright (C) 2017 Shobhit Licensed under the Apache\n  License, Version 2.0 (the \"License\"); you may not use this file except in\n  compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n  Unless required by applicable law or agreed to in writing, software distributed\n  under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES\n  OR CONDITIONS OF ANY KIND, either express or implied. See the License for\n  the specific language governing permissions and limitations under the License.\n-->\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:ignore=\"AlwaysShowAction\">\n\n    <!-- tools:ignore=\"AlwaysShowAction\" due to #1154 on small screen action is more important\n    than title-->\n    <item\n        android:id=\"@+id/menu_add_repeat\"\n        android:icon=\"@drawable/ic_add_circle_24\"\n        android:title=\"@string/add_another_menu\"\n        app:showAsAction=\"always\" />\n\n    <item\n        android:id=\"@+id/menu_save\"\n        android:icon=\"@drawable/ic_save_menu_24\"\n        android:title=\"@string/save_all_answers\"\n        app:showAsAction=\"always\" />\n\n    <item\n        android:id=\"@+id/menu_goto\"\n        android:icon=\"@drawable/ic_baseline_list_24\"\n        android:title=\"@string/view_hierarchy\"\n        app:showAsAction=\"always\" />\n\n    <item\n        android:id=\"@+id/menu_validate\"\n        android:icon=\"@drawable/ic_baseline_rule_24\"\n        android:title=\"@string/validate\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/track_location\"\n        android:icon=\"@drawable/ic_baseline_location_on_24\"\n        android:title=\"@string/track_location_on\"\n        android:visible=\"false\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/menu_record_audio\"\n        android:icon=\"@drawable/ic_baseline_mic_24\"\n        android:title=\"@string/record_audio_on\"\n        android:visible=\"false\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/menu_languages\"\n        android:icon=\"@drawable/ic_baseline_language_24\"\n        android:title=\"@string/change_language\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/menu_preferences\"\n        android:icon=\"@drawable/ic_baseline_settings_24\"\n        android:title=\"@string/project_settings\"\n        app:showAsAction=\"never\" />\n\n</menu>\n"
  },
  {
    "path": "collect_app/src/main/res/menu/instance_uploader_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright 2018 Shobhit Agarwal\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_preferences\"\n        android:icon=\"@drawable/ic_baseline_settings_24\"\n        android:title=\"@string/project_settings\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/menu_change_view\"\n        android:icon=\"@drawable/ic_baseline_visibility_24\"\n        android:title=\"@string/change_view\"\n        app:showAsAction=\"never\" />\n\n</menu>"
  },
  {
    "path": "collect_app/src/main/res/menu/main_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright 2016 Nafundi\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/projects\"\n        android:title=\"@string/projects\"\n        app:actionLayout=\"@layout/current_project_menu_icon\"\n        app:showAsAction=\"always\" />\n</menu>\n"
  },
  {
    "path": "collect_app/src/main/res/menu/project_preferences_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_locked\"\n        android:icon=\"@drawable/ic_outline_lock_24\"\n        app:showAsAction=\"always\"\n        android:title=\"@string/unlock_settings\" />\n\n    <item\n        android:id=\"@+id/menu_unlocked\"\n        android:icon=\"@drawable/ic_outline_lock_open_24\"\n        app:showAsAction=\"always\"\n        android:title=\"@string/settings_unlocked\" />\n</menu>"
  },
  {
    "path": "collect_app/src/main/res/menu/qr_code_scan_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  Copyright (C) 2018 Shobhit Licensed under the Apache\n  License, Version 2.0 (the \"License\"); you may not use this file except in\n  compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n  Unless required by applicable law or agreed to in writing, software distributed\n  under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES\n  OR CONDITIONS OF ANY KIND, either express or implied. See the License for\n  the specific language governing permissions and limitations under the License.\n-->\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:id=\"@+id/menu_item_scan_sd_card\"\n        android:icon=\"@drawable/ic_baseline_qr_code_2_add_24\"\n        android:title=\"@string/import_qrcode_sd\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/menu_item_share\"\n        android:title=\"@string/share\"\n        android:icon=\"@drawable/ic_menu_share\"\n        app:showAsAction=\"always\" />\n</menu>"
  },
  {
    "path": "collect_app/src/main/res/menu/saved_form_list_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  Copyright (C) 2017 Shobhit Licensed under the Apache\n  License, Version 2.0 (the \"License\"); you may not use this file except in\n  compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n  Unless required by applicable law or agreed to in writing, software distributed\n  under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES\n  OR CONDITIONS OF ANY KIND, either express or implied. See the License for\n  the specific language governing permissions and limitations under the License.\n-->\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_sort\"\n        android:icon=\"@drawable/ic_baseline_sort_24\"\n        android:title=\"@string/sort_the_list\"\n        app:showAsAction=\"always\"/>\n\n    <item\n        android:id=\"@+id/menu_filter\"\n        android:icon=\"@drawable/ic_baseline_search_24\"\n        android:title=\"@string/filter_the_list\"\n        app:actionViewClass=\"androidx.appcompat.widget.SearchView\"\n        app:showAsAction=\"always|collapseActionView\"/>\n\n</menu>\n"
  },
  {
    "path": "collect_app/src/main/res/menu/select_minimal_dialog_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/menu_filter\"\n        android:icon=\"@drawable/ic_baseline_search_24\"\n        android:title=\"@string/filter_the_list\"\n        app:actionViewClass=\"androidx.appcompat.widget.SearchView\"\n        app:showAsAction=\"always\"\n        app:iconifiedByDefault=\"true\"\n        android:iconifiedByDefault=\"true\"/>\n</menu>"
  },
  {
    "path": "collect_app/src/main/res/navigation/form_entry.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/form_entry\"\n    app:startDestination=\"@id/hierarchy\">\n\n    <fragment\n        android:id=\"@+id/hierarchy\"\n        android:name=\"org.odk.collect.android.formhierarchy.FormHierarchyFragment\">\n        <argument\n            android:name=\"new_edit\"\n            app:argType=\"boolean\" />\n    </fragment>\n\n</navigation>\n"
  },
  {
    "path": "collect_app/src/main/res/raw/isrgrootx1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\nWhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\nZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\nMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\nh77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\nA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\nT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\nB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\nB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\nKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\nOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\njh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\nqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\nrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\nhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\nubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\nNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\nORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\nTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\njNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\noyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\nmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\nemyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "collect_app/src/main/res/raw/keep.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:keep=\"@string/mapbox_access_token\" />"
  },
  {
    "path": "collect_app/src/main/res/values/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright (C) 2009 University of Washington Licensed under the Apache\nLicense, Version 2.0 (the \"License\"); you may not use this file except in\ncompliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software distributed\nunder the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES\nOR CONDITIONS OF ANY KIND, either express or implied. See the License for\nthe specific language governing permissions and limitations under the License. -->\n<resources>\n    <!-- DO NOT alter font_size_entry_values - used internally in preferences activity -->\n    <string-array name=\"font_size_entry_values\" translatable=\"false\">\n        <item>29</item>\n        <item>25</item>\n        <item>21</item>\n        <item>17</item>\n        <item>13</item>\n    </string-array>\n    <string-array name=\"font_size_entries\">\n        <item>@string/font_size_extra_large</item>\n        <item>@string/font_size_large</item>\n        <item>@string/font_size_medium</item>\n        <item>@string/font_size_small</item>\n        <item>@string/font_size_extra_small</item>\n    </string-array>\n    <!-- DO NOT alter navigation_entry_values - used internally in preferences activity -->\n    <string-array name=\"navigation_entry_values\" translatable=\"false\">\n        <item>swipe</item>\n        <item>buttons</item>\n        <item>swipe_buttons</item>\n    </string-array>\n    <string-array name=\"navigation_entries\">\n        <item>@string/swipe_navigation</item>\n        <item>@string/buttons_navigation</item>\n        <item>@string/swipe_buttons_navigation</item>\n    </string-array>\n    <string-array name=\"constraint_behavior_entry_values\" translatable=\"false\">\n        <item>on_swipe</item>\n        <item>on_finalize</item>\n    </string-array>\n    <string-array name=\"constraint_behavior_entries\">\n        <item>@string/constraint_behavior_on_swipe</item>\n        <item>@string/constraint_behavior_on_finalize</item>\n    </string-array>\n    <string-array name=\"image_size_entry_values\" translatable=\"false\">\n        <item>original_image_size</item>\n        <item>very_small</item>\n        <item>small</item>\n        <item>medium</item>\n        <item>large</item>\n    </string-array>\n    <string-array name=\"image_size_entries\">\n        <item>@string/image_size_original</item>\n        <item>@string/image_size_very_small</item>\n        <item>@string/image_size_small</item>\n        <item>@string/image_size_medium</item>\n        <item>@string/image_size_large</item>\n    </string-array>\n    <string-array name=\"autosend_selector_entries\">\n        <item>@string/off</item>\n        <item>@string/wifi_autosend</item>\n        <item>@string/cellular_autosend</item>\n        <item>@string/wifi_cellular_autosend</item>\n    </string-array>\n    <string-array name=\"autosend_selector_entries_values\" translatable=\"false\">\n        <item>@string/auto_send_off</item>\n        <item>@string/auto_send_wifi_only</item>\n        <item>@string/auto_send_cellular_only</item>\n        <item>@string/auto_send_wifi_and_cellular</item>\n    </string-array>\n    <string-array name=\"guidance_hint_selector_entries\">\n        <item>@string/guidance_no</item>\n        <item>@string/guidance_yes</item>\n        <item>@string/guidance_yes_collapsed</item>\n    </string-array>\n    <string-array name=\"guidance_hint_selector_entries_values\" translatable=\"false\">\n        <item>@string/guidance_hint_no</item>\n        <item>@string/guidance_hint_yes</item>\n        <item>@string/guidance_hint_yes_collapsed</item>\n    </string-array>\n\n    <string-array name=\"ethiopian_months\">\n        <item>@string/ethiopian_month_1</item>\n        <item>@string/ethiopian_month_2</item>\n        <item>@string/ethiopian_month_3</item>\n        <item>@string/ethiopian_month_4</item>\n        <item>@string/ethiopian_month_5</item>\n        <item>@string/ethiopian_month_6</item>\n        <item>@string/ethiopian_month_7</item>\n        <item>@string/ethiopian_month_8</item>\n        <item>@string/ethiopian_month_9</item>\n        <item>@string/ethiopian_month_10</item>\n        <item>@string/ethiopian_month_11</item>\n        <item>@string/ethiopian_month_12</item>\n        <item>@string/ethiopian_month_13</item>\n    </string-array>\n    <string-array name=\"coptic_months\">\n        <item>@string/coptic_month_1</item>\n        <item>@string/coptic_month_2</item>\n        <item>@string/coptic_month_3</item>\n        <item>@string/coptic_month_4</item>\n        <item>@string/coptic_month_5</item>\n        <item>@string/coptic_month_6</item>\n        <item>@string/coptic_month_7</item>\n        <item>@string/coptic_month_8</item>\n        <item>@string/coptic_month_9</item>\n        <item>@string/coptic_month_10</item>\n        <item>@string/coptic_month_11</item>\n        <item>@string/coptic_month_12</item>\n        <item>@string/coptic_month_13</item>\n    </string-array>\n    <string-array name=\"islamic_months\">\n        <item>@string/islamic_month_1</item>\n        <item>@string/islamic_month_2</item>\n        <item>@string/islamic_month_3</item>\n        <item>@string/islamic_month_4</item>\n        <item>@string/islamic_month_5</item>\n        <item>@string/islamic_month_6</item>\n        <item>@string/islamic_month_7</item>\n        <item>@string/islamic_month_8</item>\n        <item>@string/islamic_month_9</item>\n        <item>@string/islamic_month_10</item>\n        <item>@string/islamic_month_11</item>\n        <item>@string/islamic_month_12</item>\n    </string-array>\n    <string-array name=\"persian_months\">\n        <item>@string/persian_month_1</item>\n        <item>@string/persian_month_2</item>\n        <item>@string/persian_month_3</item>\n        <item>@string/persian_month_4</item>\n        <item>@string/persian_month_5</item>\n        <item>@string/persian_month_6</item>\n        <item>@string/persian_month_7</item>\n        <item>@string/persian_month_8</item>\n        <item>@string/persian_month_9</item>\n        <item>@string/persian_month_10</item>\n        <item>@string/persian_month_11</item>\n        <item>@string/persian_month_12</item>\n    </string-array>\n    <string-array name=\"buddhist_months\">\n        <item>@string/buddhist_month_1</item>\n        <item>@string/buddhist_month_2</item>\n        <item>@string/buddhist_month_3</item>\n        <item>@string/buddhist_month_4</item>\n        <item>@string/buddhist_month_5</item>\n        <item>@string/buddhist_month_6</item>\n        <item>@string/buddhist_month_7</item>\n        <item>@string/buddhist_month_8</item>\n        <item>@string/buddhist_month_9</item>\n        <item>@string/buddhist_month_10</item>\n        <item>@string/buddhist_month_11</item>\n        <item>@string/buddhist_month_12</item>\n    </string-array>\n    <string-array name=\"periodic_form_updates_check_entry_values\">\n        <item>@string/every_fifteen_minutes_value</item>\n        <item>@string/every_one_hour_value</item>\n        <item>@string/every_six_hours_value</item>\n        <item>@string/every_24_hours_value</item>\n    </string-array>\n    <string-array name=\"periodic_form_updates_check_entries\">\n        <item>@string/every_fifteen_minutes</item>\n        <item>@string/every_one_hour</item>\n        <item>@string/every_six_hours</item>\n        <item>@string/every_24_hours</item>\n    </string-array>\n\n    <string-array name=\"form_update_mode_entries\">\n        <item>@string/manual</item>\n        <item>@string/previously_downloaded_only</item>\n        <item>@string/match_exactly</item>\n    </string-array>\n    <string-array name=\"form_update_mode_values\">\n        <item>@string/form_update_mode_manual</item>\n        <item>@string/form_update_mode_previously_downloaded</item>\n        <item>@string/form_update_mode_match_exactly</item>\n    </string-array>\n    \n    <string-array name=\"project_colors\">\n        <item>#EA4633</item>\n        <item>#E9527E</item>\n        <item>#9F50B0</item>\n        <item>#3F51B5</item>\n        <item>#2296F3</item>\n        <item>#53BDD4</item>\n        <item>#489789</item>\n        <item>#5DAF50</item>\n        <item>#8BC34A</item>\n        <item>#CDDC39</item>\n        <item>#FFEB3B</item>\n        <item>#F9C028</item>\n        <item>#F2972C</item>\n        <item>#795548</item>\n        <item>#9E9E9E</item>\n    </string-array>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <attr name=\"icon\" format=\"reference\" />\n\n    <declare-styleable name=\"Collect\">\n        <attr name=\"iconColor\" format=\"color\" />\n        <attr name=\"dividerCompat\" format=\"reference\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"Waveform\">\n        <attr name=\"mini\" format=\"boolean\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"MainMenuButton\">\n        <attr name=\"icon\" />\n        <attr name=\"name\" format=\"string\" />\n        <attr name=\"highlightable\" format=\"boolean\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"CollectCustomAttributes\">\n        <attr name=\"widgetButtonIconQuestionWidgetStyle\" format=\"reference\" />\n    </declare-styleable>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/buttons.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"Widget.Collect.Button.Icon.QuestionWidget\" parent=\"Widget.Material3.Button.Icon\">\n        <item name=\"android:paddingTop\">@dimen/margin_small</item>\n        <item name=\"android:paddingBottom\">@dimen/margin_small</item>\n        <item name=\"android:textAppearance\">?textAppearanceBodyLarge</item>\n        <item name=\"iconGravity\">textStart</item>\n        <item name=\"iconSize\">20dp</item>\n    </style>\n\n    <style name=\"Widget.Collect.Button\" parent=\"Widget.Material3.Button\">\n        <item name=\"android:paddingTop\">@dimen/margin_small</item>\n        <item name=\"android:paddingBottom\">@dimen/margin_small</item>\n        <item name=\"android:textAppearance\">?textAppearanceBodyMedium</item>\n    </style>\n\n    <style name=\"Widget.Collect.Button.Icon\" parent=\"Widget.Material3.Button.Icon\">\n        <item name=\"android:paddingTop\">@dimen/margin_small</item>\n        <item name=\"android:paddingBottom\">@dimen/margin_small</item>\n        <item name=\"android:textAppearance\">?textAppearanceBodyMedium</item>\n        <item name=\"iconSize\">24dp</item>\n    </style>\n\n    <style name=\"Widget.Collect.Button.OutlinedButton\" parent=\"Widget.Material3.Button.OutlinedButton\">\n        <item name=\"android:paddingTop\">@dimen/margin_small</item>\n        <item name=\"android:paddingBottom\">@dimen/margin_small</item>\n        <item name=\"android:textAppearance\">?textAppearanceBodyMedium</item>\n        <item name=\"strokeColor\">@color/color_on_surface_low_emphasis</item>\n    </style>\n\n    <style name=\"Widget.Collect.Button.OutlinedButton.Icon\" parent=\"Widget.Material3.Button.OutlinedButton.Icon\">\n        <item name=\"android:paddingTop\">@dimen/margin_small</item>\n        <item name=\"android:paddingBottom\">@dimen/margin_small</item>\n        <item name=\"android:textAppearance\">?textAppearanceBodyMedium</item>\n        <item name=\"strokeColor\">@color/color_on_surface_low_emphasis</item>\n        <item name=\"iconSize\">24dp</item>\n    </style>\n\n    <style name=\"Widget.Collect.Button.IconButton.Outlined\" parent=\"Widget.Material3.Button.IconButton.Outlined\">\n        <item name=\"strokeColor\">@color/color_on_surface_low_emphasis</item>\n    </style>\n\n    <style name=\"Widget.Collect.Button.TextButton\" parent=\"Widget.Material3.Button.TextButton\">\n        <item name=\"android:paddingTop\">@dimen/margin_small</item>\n        <item name=\"android:paddingBottom\">@dimen/margin_small</item>\n        <item name=\"android:textAppearance\">?textAppearanceBodyMedium</item>\n    </style>\n\n    <style name=\"Widget.Collect.Button.TextButton.Icon\" parent=\"Widget.Material3.Button.TextButton.Icon\">\n        <item name=\"android:paddingTop\">@dimen/margin_small</item>\n        <item name=\"android:paddingBottom\">@dimen/margin_small</item>\n        <item name=\"android:textAppearance\">?textAppearanceBodyMedium</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimaryLight\">#3e9fcc</color>\n    <color name=\"colorOnPrimaryLight\">#ffffff</color>\n    <color name=\"colorPrimaryContainerLight\">#bfe9ff</color>\n    <color name=\"colorOnPrimaryContainerLight\">#001f2a</color>\n    <color name=\"colorSurfaceLight\">#ffffff</color>\n    <color name=\"colorOnSurfaceLight\">#001f2a</color>\n    <color name=\"colorSurfaceContainerHighestLight\">#f2f2f2</color>\n\n    <color name=\"colorPrimaryDark\">#4cbcf0</color>\n    <color name=\"colorOnPrimaryDark\">#003547</color>\n    <color name=\"colorPrimaryContainerDark\">#004d65</color>\n    <color name=\"colorOnPrimaryContainerDark\">#bfe9ff</color>\n    <color name=\"colorSurfaceDark\">#001117</color>\n    <color name=\"colorOnSurfaceDark\">#d3e5ef</color>\n    <color name=\"colorSurfaceContainerHighestDark\">#162f3b</color>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/date_time_pickers.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!-- This file contains themes and styles needed to fix broken compatibility between Date/TimePickerDialog\nand newer themes. This probably has a lot to do with these components relying on platform dependencies\nrather than AndroidX/Library dependencies.\n\nThese dialog themes need to reference `colorPrimary` directly as they do not inherit from our app theme. -->\n\n<resources>\n\n    <!-- Dark styles -->\n\n    <style name=\"Theme.Collect.Dark.Dialog.Alert\" parent=\"Theme.AppCompat.Dialog.Alert\">\n        <item name=\"colorAccent\">@color/colorPrimary</item>\n        <item name=\"android:background\">?attr/colorSurface</item>\n        <item name=\"buttonBarNegativeButtonStyle\">@style/NegativeButtonStyle</item>\n        <item name=\"buttonBarPositiveButtonStyle\">@style/PositiveButtonStyle</item>\n        <item name=\"dialogCornerRadius\">8dp</item>\n        <item name=\"android:scrollbars\">none</item>\n    </style>\n\n    <style name=\"Theme.Collect.Dark.Calendar.DatePicker.Dialog\" parent=\"Theme.AppCompat.Dialog\">\n        <item name=\"colorAccent\">@color/colorPrimary</item>\n        <item name=\"android:background\">?attr/colorSurface</item>\n        <item name=\"dialogCornerRadius\">8dp</item>\n    </style>\n\n    <style name=\"Theme.Collect.Dark.Spinner.DatePicker.Dialog\" parent=\"Theme.Collect.Dark.Dialog.Alert\">\n        <item name=\"android:datePickerStyle\">@android:style/Widget.DatePicker</item>\n    </style>\n\n    <style name=\"Theme.Collect.Dark.Spinner.TimePicker.Dialog\" parent=\"Theme.Collect.Dark.Dialog.Alert\">\n        <item name=\"android:timePickerStyle\">@style/Theme.Collect.Dark.Spinner.TimePicker</item>\n    </style>\n\n    <style name=\"Theme.Collect.Dark.Spinner.TimePicker\" parent=\"android:Widget.Material.TimePicker\">\n        <item name=\"android:timePickerMode\">spinner</item>\n    </style>\n\n    <!-- Light styles -->\n\n    <style name=\"Theme.Collect.Light.Dialog.Alert\" parent=\"Theme.AppCompat.Light.Dialog.Alert\">\n        <item name=\"colorAccent\">@color/colorPrimary</item>\n        <item name=\"buttonBarNegativeButtonStyle\">@style/NegativeButtonStyle</item>\n        <item name=\"buttonBarPositiveButtonStyle\">@style/PositiveButtonStyle</item>\n        <item name=\"dialogCornerRadius\">8dp</item>\n        <item name=\"android:scrollbars\">none</item>\n    </style>\n\n    <style name=\"Theme.Collect.Light.Calendar.DatePicker.Dialog\" parent=\"Theme.AppCompat.Light.Dialog\">\n        <item name=\"colorAccent\">@color/colorPrimary</item>\n        <item name=\"dialogCornerRadius\">8dp</item>\n    </style>\n\n    <style name=\"Theme.Collect.Light.Spinner.DatePicker.Dialog\" parent=\"Theme.Collect.Light.Dialog.Alert\">\n        <item name=\"android:datePickerStyle\">@android:style/Widget.DatePicker</item>\n    </style>\n\n    <style name=\"Theme.Collect.Light.Spinner.TimePicker.Dialog\" parent=\"Theme.Collect.Light.Dialog.Alert\">\n        <item name=\"android:timePickerStyle\">@style/Theme.Collect.Light.Spinner.TimePicker</item>\n    </style>\n\n    <style name=\"Theme.Collect.Light.Spinner.TimePicker\" parent=\"android:Widget.Material.Light.TimePicker\">\n        <item name=\"android:timePickerMode\">spinner</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- These can likely all be replaced by Material resources -->\n<resources>\n    <!-- Custom font sizes -->\n    <dimen name=\"text_size_small\">16sp</dimen>\n\n    <!-- touch target -->\n    <dimen name=\"touch_target_size\">48dp</dimen>\n\n    <dimen name=\"mediumCornerSize\">8dp</dimen>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/form_entry_activity_theme.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <style name=\"Theme.Collect.FormEntry\" parent=\"Theme.Collect\">\n        <item name=\"android:textDirection\" >firstStrong</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/form_state_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!-- These colors are used to mark form items in different states -->\n\n<resources>\n    <color name=\"formStateIncomplete\">#2196F3</color>\n    <color name=\"formStateComplete\">#673AB7</color>\n    <color name=\"formStateSubmitted\">#4CAF50</color>\n    <color name=\"formStateSubmissionFailed\">#F44336</color>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/leak_canary_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- This is needed to enable LeakCanary according to https://square.github.io/leakcanary/changelog/#api-breaking-bettering-changes -->\n    <string name=\"leak_canary_test_class_name\" translatable=\"false\">androidx.test.ext.junit.runners.AndroidJUnit4</string>\n    <bool name=\"leak_canary_allow_in_non_debuggable_build\">true</bool>\n</resources>"
  },
  {
    "path": "collect_app/src/main/res/values/material_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">@color/colorPrimaryLight</color>\n    <color name=\"colorOnPrimary\">@color/colorOnPrimaryLight</color>\n    <color name=\"colorPrimaryContainer\">@color/colorPrimaryContainerLight</color>\n    <color name=\"colorOnPrimaryContainer\">@color/colorOnPrimaryContainerLight</color>\n    <color name=\"colorPrimaryInverse\">#6dd2ff</color>\n\n    <color name=\"colorError\">#ba1a1a</color>\n    <!-- <color name=\"colorOnError\">#ffffff</color> -->\n\n    <color name=\"colorSurface\">@color/colorSurfaceLight</color>\n    <color name=\"colorSurfaceVariant\">#dce3e9</color>\n    <color name=\"colorOnSurface\">@color/colorOnSurfaceLight</color>\n    <color name=\"colorOnSurfaceVariant\">#40484c</color>\n    <color name=\"colorSurfaceContainerLowest\">#ffffff</color>\n    <color name=\"colorSurfaceContainerLow\">#f7f7f7</color>\n    <color name=\"colorSurfaceContainer\">#f7f7f7</color>\n    <color name=\"colorSurfaceContainerHigh\">#f7f7f7</color>\n    <color name=\"colorSurfaceContainerHighest\">@color/colorSurfaceContainerHighestLight</color>\n    <color name=\"colorSurfaceInverse\">#003547</color>\n    <color name=\"colorOnSurfaceInverse\">#E1F4FF</color>\n\n    <bool name=\"lightStatusBar\">true</bool>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/screen_names.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"main_menu_screen\" translatable=\"false\">mainMenuScreen</string>\n    <string name=\"form_end_screen\" translatable=\"false\">formEndScreen</string>\n    <string name=\"form_entry_screen\" translatable=\"false\">formEntryScreen</string>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/settings_theme.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <style name=\"Theme.Collect.Settings\" parent=\"Theme.Collect\">\n        <item name=\"android:textColor\">?colorOnSurface</item>\n        <item name=\"android:textAppearance\">@style/TextAppearance.Collect.Subtitle1</item>\n        <item name=\"android:textDirection\" >locale</item>\n        <item name=\"android:listDivider\">@null</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/shape.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"ShapeAppearance.Collect.MediumComponent\" parent=\"ShapeAppearance.Material3.MediumComponent\">\n        <item name=\"cornerSize\">@dimen/mediumCornerSize</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"Widget.Collect.TextView.WidgetAnswer\" parent=\"Widget.AppCompat.TextView\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:gravity\">center</item>\n        <item name=\"android:paddingTop\">@dimen/margin_standard</item>\n        <item name=\"android:paddingBottom\">@dimen/margin_standard</item>\n        <item name=\"android:paddingStart\">@dimen/margin_extra_small</item>\n        <item name=\"android:paddingEnd\">@dimen/margin_extra_small</item>\n        <item name=\"android:textAppearance\">@style/TextAppearance.Collect.Body1</item>\n        <item name=\"android:textColor\">?colorOnSurface</item>\n    </style>\n\n    <style name=\"NegativeButtonStyle\" parent=\"Widget.AppCompat.Button.ButtonBar.AlertDialog\">\n        <item name=\"android:textColor\">?attr/colorSecondary</item>\n    </style>\n\n    <style name=\"PositiveButtonStyle\" parent=\"Widget.AppCompat.Button.ButtonBar.AlertDialog\">\n        <item name=\"android:textColor\">?attr/colorSecondary</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/theme.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <!-- Changes to this theme should also be made in ComposeTheme.kt-->\n    <style name=\"Theme.Collect\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n\n        <!-- Original AppCompat attributes. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorSecondary\">@color/colorPrimary</item>\n        <item name=\"android:colorBackground\">@color/colorSurface</item>\n        <item name=\"colorError\">@color/colorError</item>\n\n        <!-- MaterialComponents attributes (needed if parent=\"Theme.AppCompat\"). -->\n        <item name=\"colorPrimaryVariant\">@color/colorPrimary</item>\n        <item name=\"colorSecondaryVariant\">@color/colorPrimary</item>\n        <item name=\"colorSurface\">@color/colorSurface</item>\n        <item name=\"colorOnPrimary\">@color/colorOnPrimary</item>\n        <item name=\"colorOnSecondary\">@color/colorOnPrimary</item>\n        <item name=\"colorOnBackground\">@color/colorOnSurface</item>\n        <!--\n            colorOnError should refer to @color/colorOnError\n            This is a temporary fix and we can get rid of it when setting cursor error color is possible https://github.com/material-components/material-components-android/issues/3345\n        -->\n        <item name=\"colorOnError\">@color/colorPrimary</item>\n        <item name=\"colorOnSurface\">@color/colorOnSurface</item>\n        <item name=\"textAppearanceHeadline1\">@style/TextAppearance.MaterialComponents.Headline1\n        </item>\n        <item name=\"textAppearanceHeadline2\">@style/TextAppearance.MaterialComponents.Headline2\n        </item>\n        <item name=\"textAppearanceHeadline3\">@style/TextAppearance.MaterialComponents.Headline3\n        </item>\n        <item name=\"textAppearanceHeadline4\">@style/TextAppearance.Collect.Headline4</item>\n        <item name=\"textAppearanceHeadline5\">@style/TextAppearance.Collect.Headline5</item>\n        <item name=\"textAppearanceHeadline6\">@style/TextAppearance.Collect.Headline6</item>\n        <item name=\"textAppearanceSubtitle1\">@style/TextAppearance.Collect.Subtitle1</item>\n        <item name=\"textAppearanceSubtitle2\">@style/TextAppearance.MaterialComponents.Subtitle2\n        </item>\n        <item name=\"textAppearanceBody1\">@style/TextAppearance.Collect.Body1</item>\n        <item name=\"textAppearanceBody2\">@style/TextAppearance.Collect.Body2</item>\n        <item name=\"textAppearanceCaption\">@style/TextAppearance.MaterialComponents.Caption</item>\n        <item name=\"textAppearanceButton\">@style/TextAppearance.Collect.Button</item>\n        <item name=\"textAppearanceOverline\">@style/TextAppearance.MaterialComponents.Overline</item>\n\n        <!-- Material3 attributes (needed if parent=\"Theme.MaterialComponents\"). -->\n        <item name=\"colorPrimaryInverse\">@color/colorPrimaryInverse</item>\n        <item name=\"colorPrimaryContainer\">@color/colorPrimaryContainer</item>\n        <item name=\"colorOnPrimaryContainer\">@color/colorOnPrimaryContainer</item>\n        <item name=\"colorSecondaryContainer\">@color/colorPrimaryContainer</item>\n        <item name=\"colorOnSecondaryContainer\">@color/colorOnPrimaryContainer</item>\n        <item name=\"colorTertiary\">@color/colorPrimary</item>\n        <item name=\"colorOnTertiary\">@color/colorOnPrimary</item>\n        <item name=\"colorTertiaryContainer\">@color/colorPrimaryContainer</item>\n        <item name=\"colorOnTertiaryContainer\">@color/colorOnPrimaryContainer</item>\n        <item name=\"colorSurfaceVariant\">@color/colorSurfaceVariant</item>\n        <item name=\"colorOnSurfaceVariant\">@color/colorOnSurfaceVariant</item>\n        <item name=\"colorSurfaceInverse\">@color/colorSurfaceInverse</item>\n        <item name=\"colorOnSurfaceInverse\">@color/colorOnSurfaceInverse</item>\n        <item name=\"colorOutline\">@color/color_on_surface_medium_emphasis</item>\n        <item name=\"colorSurfaceContainerLowest\">@color/colorSurfaceContainerLowest</item>\n        <item name=\"colorSurfaceContainerLow\">@color/colorSurfaceContainerLow</item>\n        <item name=\"colorSurfaceContainer\">@color/colorSurfaceContainer</item>\n        <item name=\"colorSurfaceContainerHigh\">@color/colorSurfaceContainerHigh</item>\n        <item name=\"colorSurfaceContainerHighest\">@color/colorSurfaceContainerHighest</item>\n        <item name=\"textAppearanceDisplayLarge\">@style/TextAppearance.Material3.DisplayLarge</item>\n        <item name=\"textAppearanceDisplayMedium\">@style/TextAppearance.Material3.DisplayMedium\n        </item>\n        <item name=\"textAppearanceDisplaySmall\">@style/TextAppearance.Material3.DisplaySmall</item>\n        <item name=\"textAppearanceHeadlineLarge\">@style/TextAppearance.Material3.HeadlineLarge\n        </item>\n        <item name=\"textAppearanceHeadlineMedium\">@style/TextAppearance.Material3.HeadlineMedium\n        </item>\n        <item name=\"textAppearanceHeadlineSmall\">@style/TextAppearance.Material3.HeadlineSmall\n        </item>\n        <item name=\"textAppearanceTitleLarge\">@style/TextAppearance.Collect.TitleLarge</item>\n        <item name=\"textAppearanceTitleMedium\">@style/TextAppearance.Material3.TitleMedium</item>\n        <item name=\"textAppearanceTitleSmall\">@style/TextAppearance.Material3.TitleSmall</item>\n        <item name=\"textAppearanceBodyLarge\">?textAppearanceBody1</item>\n        <item name=\"textAppearanceBodyMedium\">@style/TextAppearance.Material3.BodyMedium</item>\n        <item name=\"textAppearanceBodySmall\">@style/TextAppearance.Material3.BodySmall</item>\n        <item name=\"textAppearanceLabelLarge\">@style/TextAppearance.Material3.LabelLarge</item>\n        <item name=\"textAppearanceLabelMedium\">@style/TextAppearance.Material3.LabelMedium</item>\n        <item name=\"textAppearanceLabelSmall\">@style/TextAppearance.Material3.LabelSmall</item>\n        <item name=\"shapeAppearanceSmallComponent\">@style/ShapeAppearance.Material3.SmallComponent\n        </item>\n        <item name=\"shapeAppearanceMediumComponent\">@style/ShapeAppearance.Collect.MediumComponent\n        </item>\n        <item name=\"shapeAppearanceLargeComponent\">@style/ShapeAppearance.Material3.LargeComponent\n        </item>\n        <item name=\"elevationOverlayEnabled\">true</item>\n\n        <!-- Material3 polyfill attributes (needed in Material Components yet). -->\n        <item name=\"materialButtonIconStyle\">@style/Widget.Collect.Button.Icon</item>\n        <item name=\"materialButtonOutlinedIconStyle\">@style/Widget.Collect.Button.OutlinedButton.Icon</item>\n        <item name=\"materialIconButtonOutlinedStyle\">@style/Widget.Collect.Button.IconButton.Outlined</item>\n        <item name=\"borderlessButtonIconStyle\">@style/Widget.Collect.Button.TextButton.Icon</item>\n        <item name=\"materialButtonStyle\">@style/Widget.Collect.Button</item>\n        <item name=\"materialButtonOutlinedStyle\">@style/Widget.Collect.Button.OutlinedButton</item>\n        <item name=\"borderlessButtonStyle\">@style/Widget.Collect.Button.TextButton</item>\n        <item name=\"materialAlertDialogTheme\">@style/Theme.Collect.Dialog.Alert</item>\n        <item name=\"bottomSheetDialogTheme\">@style/Theme.Collect.BottomSheet</item>\n\n        <!-- Custom style for buttons used in question widgets -->\n        <item name=\"widgetButtonIconQuestionWidgetStyle\">@style/Widget.Collect.Button.Icon.QuestionWidget</item>\n\n        <!-- Custom theme text appearance for larger buttons -->\n        <item name=\"textAppearanceLabelExtraLarge\">@style/TextAppearance.Collect.LabelExtraLarge</item>\n\n        <!-- Needed to make scrollbars always visible -->\n        <item name=\"android:scrollbars\">vertical</item>\n\n        <!-- Needed to customize the status bar color -->\n        <item name=\"android:statusBarColor\">?colorSurface</item>\n        <item name=\"android:windowLightStatusBar\" >@bool/lightStatusBar</item>\n\n        <!-- Used as divider in many places. Not sure why. -->\n        <item name=\"dividerCompat\">@android:drawable/divider_horizontal_bright</item>\n\n        <!-- Remove icon from SearchView -->\n        <item name=\"searchViewStyle\">@style/Widget.Collect.SearchView.NoIcon</item>\n\n        <!-- Always show scrollbars for a11y reasons -->\n        <item name=\"android:fadeScrollbars\">false</item>\n\n        <!-- Use locale for text direction in views -->\n        <item name=\"android:textDirection\">locale</item>\n\n        <!-- Sets up theme correction for anything using `AlertDialog.Builder` -->\n        <item name=\"alertDialogTheme\">@style/Theme.Collect.Dialog.Alert</item>\n        <item name=\"android:alertDialogTheme\">@style/Theme.Collect.Dialog.Alert</item>\n        <item name=\"dialogCornerRadius\">8dp</item>\n\n        <item name=\"linearProgressIndicatorStyle\">@style/Widget.Collect.LinearProgressIndicator</item>\n        <item name=\"sliderStyle\">@style/Widget.Collect.Slider</item>\n    </style>\n\n    <style name=\"Widget.Collect.LinearProgressIndicator\" parent=\"Widget.Material3.LinearProgressIndicator\">\n        <item name=\"trackStopIndicatorSize\">@null</item>\n        <item name=\"indicatorTrackGapSize\">@null</item>\n    </style>\n\n    <style name=\"Widget.Collect.Slider\" parent=\"Widget.Material3.Slider\">\n        <item name=\"labelBehavior\">gone</item>\n    </style>\n\n    <style name=\"Theme.Collect.SplashScreen\" parent=\"Theme.SplashScreen\">\n        <item name=\"windowSplashScreenBackground\">@color/colorSurface</item>\n        <item name=\"windowSplashScreenAnimatedIcon\">@drawable/ic_splash_screen_icon</item>\n        <item name=\"postSplashScreenTheme\">@style/Theme.Collect</item>\n    </style>\n\n    <style name=\"Theme.Collect.Dialog.Alert\" parent=\"ThemeOverlay.Material3.MaterialAlertDialog\">\n        <item name=\"buttonBarPositiveButtonStyle\">@style/Widget.Collect.Dialog.PositiveButton</item>\n    </style>\n\n    <style name=\"Widget.Collect.Dialog.PositiveButton\" parent=\"Widget.Material3.Button\">\n        <item name=\"android:layout_marginStart\">@dimen/margin_standard</item>\n    </style>\n\n    <style name=\"Widget.Collect.SearchView.NoIcon\" parent=\"Widget.AppCompat.SearchView\">\n        <item name=\"searchHintIcon\">@null</item>\n    </style>\n\n    <style name=\"Theme.Collect.BottomSheet\" parent=\"ThemeOverlay.Material3.BottomSheetDialog\">\n        <item name=\"materialButtonStyle\">@style/Widget.Collect.Button</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values/typography.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <style name=\"TextAppearance.Collect.Headline4\" parent=\"TextAppearance.MaterialComponents.Headline4\">\n        <item name=\"android:textColor\">@color/color_on_surface_high_emphasis</item>\n    </style>\n\n    <style name=\"TextAppearance.Collect.Headline5\" parent=\"TextAppearance.MaterialComponents.Headline5\">\n        <item name=\"android:textColor\">@color/color_on_surface_high_emphasis</item>\n    </style>\n\n    <style name=\"TextAppearance.Collect.Headline6\" parent=\"TextAppearance.MaterialComponents.Headline6\">\n        <item name=\"android:textColor\">@color/color_on_surface_high_emphasis</item>\n    </style>\n\n    <style name=\"TextAppearance.Collect.Subtitle1\" parent=\"TextAppearance.MaterialComponents.Subtitle1\">\n        <item name=\"android:textColor\">@color/color_on_surface_high_emphasis</item>\n    </style>\n\n    <style name=\"TextAppearance.Collect.Subtitle1.MediumEmphasis\" parent=\"TextAppearance.Collect.Subtitle1\">\n        <item name=\"android:textColor\">@color/color_on_surface_medium_emphasis</item>\n    </style>\n\n    <style name=\"TextAppearance.Collect.Body1\" parent=\"TextAppearance.MaterialComponents.Body1\">\n        <item name=\"android:textColor\">@color/color_on_surface_high_emphasis</item>\n    </style>\n\n    <style name=\"TextAppearance.Collect.Body2\" parent=\"TextAppearance.MaterialComponents.Body2\">\n        <item name=\"android:textColor\">@color/color_on_surface_high_emphasis</item>\n    </style>\n\n    <style name=\"TextAppearance.Collect.Body2.MediumEmphasis\" parent=\"TextAppearance.Collect.Body2\">\n        <item name=\"android:textColor\">@color/color_on_surface_medium_emphasis</item>\n    </style>\n\n    <style name=\"TextAppearance.Collect.TitleLarge\" parent=\"TextAppearance.Material3.TitleLarge\">\n        <item name=\"android:textSize\">20sp</item>\n        <item name=\"android:lineHeight\" tools:targetApi=\"p\">24sp</item>\n        <item name=\"lineHeight\">24sp</item>\n        <item name=\"fontFamily\">sans-serif-medium</item>\n    </style>\n\n    <style name=\"TextAppearance.Collect.Button\" parent=\"TextAppearance.MaterialComponents.Button\">\n        <item name=\"fontFamily\">sans-serif</item>\n        <item name=\"android:fontFamily\">sans-serif</item>\n        <item name=\"android:textStyle\">normal</item>\n        <item name=\"android:textAllCaps\">false</item>\n        <item name=\"android:letterSpacing\">0.009375</item>\n    </style>\n\n    <style name=\"TextAppearance.Collect.LabelExtraLarge\" parent=\"TextAppearance.Material3.TitleLarge\">\n        <item name=\"android:textSize\">18sp</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/values-night/material_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">@color/colorPrimaryDark</color>\n    <color name=\"colorOnPrimary\">@color/colorOnPrimaryDark</color>\n    <color name=\"colorPrimaryContainer\">@color/colorPrimaryContainerDark</color>\n    <color name=\"colorOnPrimaryContainer\">@color/colorOnPrimaryContainerDark</color>\n    <color name=\"colorPrimaryInverse\">#006689</color>\n\n    <color name=\"colorError\">#ba1a1a</color>\n    <!-- <color name=\"colorOnError\">#ffffff</color> -->\n\n    <color name=\"colorSurface\">@color/colorSurfaceDark</color>\n    <color name=\"colorSurfaceVariant\">#40484c</color>\n    <color name=\"colorOnSurface\">@color/colorOnSurfaceDark</color>\n    <color name=\"colorOnSurfaceVariant\">#c0c8cd</color>\n    <color name=\"colorSurfaceContainerLowest\">#001117</color>\n    <color name=\"colorSurfaceContainerLow\">#293134</color>\n    <color name=\"colorSurfaceContainer\">#293134</color>\n    <color name=\"colorSurfaceContainerHigh\">#293134</color>\n    <color name=\"colorSurfaceContainerHighest\">@color/colorSurfaceContainerHighestDark</color>\n    <color name=\"colorSurfaceInverse\">#BFE9FF</color>\n    <color name=\"colorOnSurfaceInverse\">#001F2A</color>\n\n    <bool name=\"lightStatusBar\">false</bool>\n</resources>\n"
  },
  {
    "path": "collect_app/src/main/res/xml/access_control_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:title=\"@string/access_control_section_title\">\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_menu_accent_24\"\n        android:key=\"main_menu\"\n        android:summary=\"@string/main_menu_settings_summary\"\n        android:title=\"@string/main_menu_settings\" />\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_settings_accent_24\"\n        android:key=\"user_settings\"\n        android:summary=\"@string/user_setting_summary\"\n        android:title=\"@string/user_settings\" />\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_assignment_accent_24\"\n        android:key=\"form_entry\"\n        android:summary=\"@string/form_entry_settings_summary\"\n        android:title=\"@string/form_entry_setting\" />\n</PreferenceScreen>"
  },
  {
    "path": "collect_app/src/main/res/xml/dev_tools_preferences.xml",
    "content": "<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/experimental\">\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_warning_accent_24\"\n        android:title=\"@string/crash_app\"\n        android:summary=\"@string/crash_app_summary\"\n        android:key=\"crash_app\"\n        app:persistent=\"false\"/>\n\n    <SwitchPreferenceCompat\n        android:key=\"experimental_debug_filters\"\n        android:title=\"@string/debug_filters\"\n        android:summary=\"@string/debug_filters_summary\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:key=\"zxing_scanning\"\n        android:title=\"@string/use_zxing_for_barcode_scanning\"\n        app:iconSpaceReserved=\"false\" />\n\n</PreferenceScreen>\n"
  },
  {
    "path": "collect_app/src/main/res/xml/experimental_preferences.xml",
    "content": "<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/experimental\">\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_settings_applications_accent_24\"\n        android:key=\"dev_tools\"\n        android:title=\"@string/dev_tools\"\n        app:persistent=\"false\" />\n\n    <PreferenceCategory\n        android:title=\"@string/entities_title\"\n        app:allowDividerAbove=\"false\"\n        app:allowDividerBelow=\"false\"\n        app:iconSpaceReserved=\"false\">\n\n        <Preference\n            android:key=\"entities\"\n            android:title=\"@string/entity_browser_button\"\n            app:iconSpaceReserved=\"false\"\n            app:persistent=\"false\" />\n\n    </PreferenceCategory>\n\n</PreferenceScreen>\n"
  },
  {
    "path": "collect_app/src/main/res/xml/form_entry_access_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/form_entry_setting\">\n\n    <CheckBoxPreference\n        android:key=\"moving_backwards\"\n        android:title=\"@string/moving_backwards_title\"\n        app:iconSpaceReserved=\"false\" />\n\n    <PreferenceCategory\n        android:title=\"@string/form_entry\"\n        app:allowDividerAbove=\"false\"\n        app:allowDividerBelow=\"false\"\n        app:iconSpaceReserved=\"false\">\n        <CheckBoxPreference\n            android:key=\"access_settings\"\n            android:title=\"@string/project_settings\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"change_language\"\n            android:title=\"@string/change_language\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"jump_to\"\n            android:title=\"@string/view_hierarchy\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"save_mid\"\n            android:summary=\"@string/save_mid_summary\"\n            android:title=\"@string/save_mid\"\n            app:iconSpaceReserved=\"false\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        android:title=\"@string/form_end\"\n        app:allowDividerAbove=\"false\"\n        app:allowDividerBelow=\"false\"\n        app:iconSpaceReserved=\"false\">\n        <CheckBoxPreference\n            android:key=\"save_as_draft\"\n            android:title=\"@string/save_as_draft\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"finalize_in_form_entry\"\n            android:title=\"@string/finalize\"\n            app:iconSpaceReserved=\"false\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        android:title=\"@string/uncheck_to_hide_from_drafts\"\n        app:allowDividerAbove=\"false\"\n        app:allowDividerBelow=\"false\"\n        app:iconSpaceReserved=\"false\">\n        <CheckBoxPreference\n            android:key=\"bulk_finalize\"\n            android:title=\"@string/finalize_all_drafts\"\n            app:iconSpaceReserved=\"false\" />\n    </PreferenceCategory>\n</PreferenceScreen>\n"
  },
  {
    "path": "collect_app/src/main/res/xml/form_management_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/form_management_preferences\">\n\n    <PreferenceCategory\n        android:key=\"form_update_category\"\n        android:title=\"@string/form_update_category\"\n        app:allowDividerAbove=\"false\"\n        app:allowDividerBelow=\"false\"\n        app:iconSpaceReserved=\"false\">\n\n        <ListPreference\n            android:dialogTitle=\"@string/form_update_mode_title\"\n            android:entries=\"@array/form_update_mode_entries\"\n            android:entryValues=\"@array/form_update_mode_values\"\n            android:key=\"form_update_mode\"\n            android:title=\"@string/form_update_mode_title\"\n            app:iconSpaceReserved=\"false\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <ListPreference\n            android:defaultValue=\"@string/never_value\"\n            android:dialogTitle=\"@string/form_update_frequency_title\"\n            android:entries=\"@array/periodic_form_updates_check_entries\"\n            android:entryValues=\"@array/periodic_form_updates_check_entry_values\"\n            android:key=\"periodic_form_updates_check\"\n            android:title=\"@string/form_update_frequency_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"automatic_update\"\n            android:summary=\"@string/automatic_download_summary\"\n            android:title=\"@string/automatic_download\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"hide_old_form_versions\"\n            android:summary=\"@string/hide_old_form_versions_setting_summary\"\n            android:title=\"@string/hide_old_form_versions_setting_title\"\n            app:iconSpaceReserved=\"false\" />\n    </PreferenceCategory>\n    <PreferenceCategory\n        android:key=\"form_submission\"\n        android:title=\"@string/form_submission_category\"\n        app:allowDividerAbove=\"false\"\n        app:allowDividerBelow=\"false\"\n        app:iconSpaceReserved=\"false\">\n        <ListPreference\n            android:dialogTitle=\"@string/autosend_selector_title\"\n            android:entries=\"@array/autosend_selector_entries\"\n            android:entryValues=\"@array/autosend_selector_entries_values\"\n            android:key=\"autosend\"\n            android:title=\"@string/autosend\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"delete_send\"\n            android:summary=\"@string/delete_after_send_summary\"\n            android:title=\"@string/delete_after_send\"\n            app:iconSpaceReserved=\"false\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        android:key=\"form_filling\"\n        android:title=\"@string/form_filling_category\"\n        app:allowDividerAbove=\"false\"\n        app:allowDividerBelow=\"false\"\n        app:iconSpaceReserved=\"false\">\n        <ListPreference\n            android:dialogTitle=\"@string/constraint_behavior\"\n            android:entries=\"@array/constraint_behavior_entries\"\n            android:entryValues=\"@array/constraint_behavior_entry_values\"\n            android:key=\"constraint_behavior\"\n            android:title=\"@string/constraint_behavior_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"high_resolution\"\n            android:summary=\"@string/high_resolution_summary\"\n            android:title=\"@string/high_resolution_title\"\n            app:iconSpaceReserved=\"false\" />\n        <ListPreference\n            android:dialogTitle=\"@string/image_size_dialog_title\"\n            android:entries=\"@array/image_size_entries\"\n            android:entryValues=\"@array/image_size_entry_values\"\n            android:key=\"image_size\"\n            android:title=\"@string/image_size_title\"\n            app:iconSpaceReserved=\"false\" />\n\n        <ListPreference\n            android:dialogTitle=\"@string/guidance_hint_title\"\n            android:entries=\"@array/guidance_hint_selector_entries\"\n            android:entryValues=\"@array/guidance_hint_selector_entries_values\"\n            android:key=\"guidance_hint\"\n            android:title=\"@string/guidance_hint_title\"\n            app:iconSpaceReserved=\"false\" />\n\n        <CheckBoxPreference\n            android:key=\"external_app_recording\"\n            android:title=\"@string/external_app_recording\"\n            app:iconSpaceReserved=\"false\" />\n\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        android:key=\"form_import\"\n        android:title=\"@string/form_import_category\"\n        app:allowDividerAbove=\"false\"\n        app:allowDividerBelow=\"false\"\n        app:iconSpaceReserved=\"false\">\n        <CheckBoxPreference\n            android:key=\"instance_sync\"\n            android:summary=\"@string/instance_sync_summary\"\n            android:title=\"@string/instance_sync\"\n            app:iconSpaceReserved=\"false\" />\n    </PreferenceCategory>\n</PreferenceScreen>\n"
  },
  {
    "path": "collect_app/src/main/res/xml/form_metadata_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/form_metadata_title\">\n\n    <PreferenceCategory\n        android:title=\"@string/user_defined\"\n        app:iconSpaceReserved=\"false\">\n\n        <EditTextPreference\n            android:inputType=\"textNoSuggestions\"\n            android:key=\"metadata_username\"\n            android:title=\"@string/username\"\n            app:iconSpaceReserved=\"false\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <EditTextPreference\n            android:key=\"metadata_phonenumber\"\n            android:title=\"@string/phone_number\"\n            android:inputType=\"\"\n            app:iconSpaceReserved=\"false\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <EditTextPreference\n            android:inputType=\"textEmailAddress\"\n            android:key=\"metadata_email\"\n            android:maxLength=\"254\"\n            android:title=\"@string/email\"\n            app:iconSpaceReserved=\"false\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        android:title=\"@string/device_defined\"\n        app:iconSpaceReserved=\"false\">\n\n        <EditTextPreference\n            android:key=\"deviceid\"\n            android:selectable=\"false\"\n            android:title=\"@string/device_id\"\n            app:iconSpaceReserved=\"false\"\n            app:persistent=\"false\" />\n    </PreferenceCategory>\n</PreferenceScreen>"
  },
  {
    "path": "collect_app/src/main/res/xml/identity_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/user_and_device_identity_title\">\n\n    <Preference\n        android:key=\"form_metadata\"\n        android:summary=\"@string/form_metadata_summary\"\n        android:title=\"@string/form_metadata\"\n        app:iconSpaceReserved=\"false\"\n        app:allowDividerAbove=\"false\"\n        app:allowDividerBelow=\"false\" />\n\n    <PreferenceCategory\n        android:key=\"usage_data\"\n        android:title=\"@string/analytics_preferences\"\n        app:iconSpaceReserved=\"false\">\n\n        <CheckBoxPreference\n            android:key=\"analytics\"\n            android:summary=\"@string/analytics_summary\"\n            android:title=\"@string/analytics\"\n            app:iconSpaceReserved=\"false\" />\n    </PreferenceCategory>\n</PreferenceScreen>"
  },
  {
    "path": "collect_app/src/main/res/xml/main_menu_access_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/main_menu_settings\">\n\n    <PreferenceCategory android:title=\"@string/found_in_main\"\n        app:iconSpaceReserved=\"false\">\n        <CheckBoxPreference\n            android:key=\"edit_saved\"\n            android:title=\"@string/review_data\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"send_finalized\"\n            android:title=\"@string/send_data\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"view_sent\"\n            android:title=\"@string/view_sent_forms\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"get_blank\"\n            android:title=\"@string/get_forms\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"delete_saved\"\n            android:title=\"@string/manage_files\"\n            app:iconSpaceReserved=\"false\" />\n    </PreferenceCategory>\n</PreferenceScreen>"
  },
  {
    "path": "collect_app/src/main/res/xml/maps_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/maps\">\n\n    <PreferenceCategory\n        android:key=\"category_basemap\"\n        android:title=\"@string/basemap\"\n        app:iconSpaceReserved=\"false\" />\n    <PreferenceCategory\n        android:key=\"category_reference_layer\"\n        android:title=\"@string/offline_layer\"\n        app:allowDividerAbove=\"false\"\n        app:allowDividerBelow=\"false\"\n        app:iconSpaceReserved=\"false\">\n        <Preference\n            android:key=\"reference_layer\"\n            android:title=\"@string/layer_data_file\"\n            android:dialogTitle=\"@string/offline_layer_dialog_title\"\n            android:negativeButtonText=\"@string/cancel\"\n            android:positiveButtonText=\"@string/ok\"\n            app:iconSpaceReserved=\"false\" />\n    </PreferenceCategory>\n</PreferenceScreen>\n"
  },
  {
    "path": "collect_app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\">\n        <trust-anchors>\n            <!-- Explicitly trust Let's Encrypt ISRG Root X1 which is not available to Android < 7.1.1 -->\n            <certificates src=\"@raw/isrgrootx1\"/>\n\n            <certificates src=\"system\"/>\n        </trust-anchors>\n    </base-config>\n</network-security-config>"
  },
  {
    "path": "collect_app/src/main/res/xml/odk_server_preferences.xml",
    "content": "<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        android:dialogTitle=\"@string/change_server_url\"\n        android:inputType=\"textNoSuggestions\"\n        android:key=\"server_url\"\n        android:title=\"@string/server_url\"\n        app:iconSpaceReserved=\"false\" />\n    <EditTextPreference\n        android:dialogTitle=\"@string/change_username\"\n        android:inputType=\"textNoSuggestions\"\n        android:key=\"username\"\n        android:title=\"@string/username\"\n        app:iconSpaceReserved=\"false\" />\n    <EditTextPreference\n        android:dialogTitle=\"@string/change_password\"\n        android:inputType=\"textPassword\"\n        android:key=\"password\"\n        android:title=\"@string/password\"\n        app:iconSpaceReserved=\"false\" />\n</PreferenceScreen>"
  },
  {
    "path": "collect_app/src/main/res/xml/project_display_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/project_display_title\">\n\n    <EditTextPreference\n        android:key=\"project_name\"\n        android:title=\"@string/project_name\"\n        app:iconSpaceReserved=\"false\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:persistent=\"false\"/>\n\n    <EditTextPreference\n        android:key=\"project_icon\"\n        android:title=\"@string/project_icon\"\n        app:iconSpaceReserved=\"false\"\n        app:persistent=\"false\"/>\n\n    <Preference\n        android:key=\"project_color\"\n        android:title=\"@string/project_color\"\n        app:iconSpaceReserved=\"false\"\n        app:persistent=\"false\"/>\n</PreferenceScreen>"
  },
  {
    "path": "collect_app/src/main/res/xml/project_management_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:title=\"@string/project_management_section_title\">\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_qr_code_scanner_accent_24\"\n        android:key=\"import_settings\"\n        android:title=\"@string/reconfigure_with_qr_code_settings_title\"\n        android:summary=\"@string/reconfigure_with_qr_code_settings_subtext\"/>\n\n    <org.odk.collect.android.preferences.dialogs.ResetDialogPreference\n        android:icon=\"@drawable/ic_outline_refresh_accent_24\"\n        android:dialogTitle=\"@string/reset_settings_dialog_title\"\n        android:key=\"reset_settings\"\n        android:negativeButtonText=\"@string/cancel\"\n        android:positiveButtonText=\"@string/reset_settings_button_reset\"\n        android:title=\"@string/reset_project_settings_title\"\n        android:summary=\"@string/reset_project_settings_subtext\" />\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_delete_accent_24\"\n        android:key=\"delete_project\"\n        android:title=\"@string/delete_project\" />\n</PreferenceScreen>"
  },
  {
    "path": "collect_app/src/main/res/xml/project_preferences.xml",
    "content": "<!-- Copyright 2018 Nafundi\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\nhttp://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/project_settings\">\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_cloud_accent_24\"\n        android:key=\"protocol\"\n        android:title=\"@string/server_settings_title\"\n        android:summary=\"@string/server_settings_subtext\" />\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_color_lens_accent_24\"\n        android:key=\"project_display\"\n        android:title=\"@string/project_display_title\"\n        android:summary=\"@string/project_display_subtext\" />\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_phonelink_setup_accent_24\"\n        android:key=\"user_interface\"\n        android:title=\"@string/client\"\n        android:summary=\"@string/user_interface_settings_subtext\" />\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_map_accent_24\"\n        android:key=\"maps\"\n        android:title=\"@string/maps\"\n        android:summary=\"@string/maps_settings_subtext\" />\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_assignment_accent_24\"\n        android:key=\"form_management\"\n        android:title=\"@string/form_management_preferences\"\n        android:summary=\"@string/form_management_settings_subtext\" />\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_face_accent_24\"\n        android:key=\"user_and_device_identity\"\n        android:title=\"@string/user_and_device_identity_title\"\n        android:summary=\"@string/user_and_device_identity_settings_subtext\" />\n\n    <Preference\n        android:icon=\"@drawable/ic_outline_warning_accent_24\"\n        android:key=\"experimental\"\n        android:title=\"@string/experimental\" />\n\n    <PreferenceCategory\n        android:title=\"@string/protected_settings\"\n        app:iconSpaceReserved=\"false\">\n\n        <Preference\n            android:icon=\"@drawable/ic_outline_lock_accent_24\"\n            android:key=\"unlock_protected_settings\"\n            android:title=\"@string/unlock_protected_settings_section_title\"\n            android:summary=\"@string/unlock_protected_settings_section_subtext\"/>\n\n        <Preference\n            android:icon=\"@drawable/ic_outline_vpn_key_accent_24\"\n            android:key=\"admin_password\"\n            android:title=\"@string/set_admin_password\" />\n\n        <Preference\n            android:icon=\"@drawable/ic_outline_settings_applications_accent_24\"\n            android:key=\"project_management\"\n            android:title=\"@string/project_management_section_title\"\n            android:summary=\"@string/project_management_section_subtext\"/>\n\n        <Preference\n            android:icon=\"@drawable/ic_outline_lock_accent_24\"\n            android:key=\"access_control\"\n            android:title=\"@string/access_control_section_title\"\n            android:summary=\"@string/access_control_section_subtext\"/>\n\n    </PreferenceCategory>\n</PreferenceScreen>\n"
  },
  {
    "path": "collect_app/src/main/res/xml/provider_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <external-path name=\"external_files\" path=\".\"/>\n</paths>"
  },
  {
    "path": "collect_app/src/main/res/xml/server_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:title=\"@string/server_preferences\">\n\n</PreferenceScreen>"
  },
  {
    "path": "collect_app/src/main/res/xml/user_interface_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/client\">\n\n    <ListPreference\n        android:dialogTitle=\"@string/language\"\n        android:key=\"app_language\"\n        android:title=\"@string/language\"\n        app:iconSpaceReserved=\"false\" />\n\n    <ListPreference\n        android:dialogTitle=\"@string/change_font_size\"\n        android:entries=\"@array/font_size_entries\"\n        android:entryValues=\"@array/font_size_entry_values\"\n        android:key=\"font_size\"\n        android:title=\"@string/font_size\"\n        app:iconSpaceReserved=\"false\" />\n\n    <ListPreference\n        android:dialogTitle=\"@string/navigation\"\n        android:entries=\"@array/navigation_entries\"\n        android:entryValues=\"@array/navigation_entry_values\"\n        android:key=\"navigation\"\n        android:title=\"@string/navigation\"\n        app:iconSpaceReserved=\"false\" />\n</PreferenceScreen>\n"
  },
  {
    "path": "collect_app/src/main/res/xml/user_settings_access_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:title=\"@string/user_settings\">\n\n    <PreferenceCategory\n        android:title=\"@string/found_in_settings\"\n        app:iconSpaceReserved=\"false\">\n        <CheckBoxPreference\n            android:key=\"change_server\"\n            android:title=\"@string/server_settings_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"change_project_display\"\n            android:title=\"@string/project_display_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"change_app_language\"\n            android:title=\"@string/language\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"change_font_size\"\n            android:title=\"@string/font_size\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"change_navigation\"\n            android:title=\"@string/navigation\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"maps\"\n            android:title=\"@string/maps\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"form_update_mode\"\n            android:title=\"@string/form_update_mode_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"periodic_form_updates_check\"\n            android:title=\"@string/form_update_frequency_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"automatic_update\"\n            android:title=\"@string/automatic_download\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"hide_old_form_versions\"\n            android:title=\"@string/hide_old_form_versions_setting_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"change_autosend\"\n            android:title=\"@string/autosend\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"delete_after_send\"\n            android:title=\"@string/delete_after_send\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"change_constraint_behavior\"\n            android:title=\"@string/constraint_behavior_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"high_resolution\"\n            android:title=\"@string/high_resolution_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"image_size\"\n            android:title=\"@string/image_size_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"guidance_hint\"\n            android:title=\"@string/guidance_hint_title\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"external_app_recording\"\n            android:title=\"@string/external_app_recording\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"instance_form_sync\"\n            android:title=\"@string/instance_sync\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"change_form_metadata\"\n            android:title=\"@string/form_metadata\"\n            app:iconSpaceReserved=\"false\" />\n        <CheckBoxPreference\n            android:key=\"analytics\"\n            android:title=\"@string/analytics\"\n            app:iconSpaceReserved=\"false\" />\n    </PreferenceCategory>\n</PreferenceScreen>\n"
  },
  {
    "path": "collect_app/src/release/google-services.json",
    "content": "{\n  \"project_info\": {\n    \"project_number\": \"640412404956\",\n    \"firebase_url\": \"https://getodk-forks.firebaseio.com\",\n    \"project_id\": \"getodk-forks\",\n    \"storage_bucket\": \"getodk-forks.appspot.com\"\n  },\n  \"client\": [\n    {\n      \"client_info\": {\n        \"mobilesdk_app_id\": \"1:640412404956:android:730483f389563845cb7819\",\n        \"android_client_info\": {\n          \"package_name\": \"org.odk.collect.android\"\n        }\n      },\n      \"oauth_client\": [\n        {\n          \"client_id\": \"640412404956-ag8do5nic02k70qr2k1a0mv3du8222hv.apps.googleusercontent.com\",\n          \"client_type\": 3\n        }\n      ],\n      \"api_key\": [\n        {\n          \"current_key\": \"AIzaSyA5tuKwVrlKEs-7iMSpdTmAy6qSH-w0lik\"\n        }\n      ],\n      \"services\": {\n        \"appinvite_service\": {\n          \"other_platform_oauth_client\": [\n            {\n              \"client_id\": \"640412404956-ag8do5nic02k70qr2k1a0mv3du8222hv.apps.googleusercontent.com\",\n              \"client_type\": 3\n            }\n          ]\n        }\n      }\n    }\n  ],\n  \"configuration_version\": \"1\"\n}"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/TestSettingsProvider.kt",
    "content": "package org.odk.collect.android\n\nimport androidx.test.core.app.ApplicationProvider\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.shared.settings.Settings\n\n// Use just for testing\nobject TestSettingsProvider {\n    @JvmStatic\n    fun getSettingsProvider(): SettingsProvider {\n        return DaggerUtils.getComponent(ApplicationProvider.getApplicationContext<Collect>()).settingsProvider()\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun getUnprotectedSettings(uuid: String? = null): Settings {\n        return getSettingsProvider().getUnprotectedSettings(uuid)\n    }\n\n    @JvmStatic\n    fun getProtectedSettings(): Settings {\n        return getSettingsProvider().getProtectedSettings()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/activities/CrashHandlerActivityTest.kt",
    "content": "package org.odk.collect.android.activities\n\nimport android.content.Context\nimport androidx.lifecycle.Lifecycle\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport org.odk.collect.crashhandler.CrashHandler\nimport org.odk.collect.crashhandler.MockCrashView\nimport org.odk.collect.testshared.ActivityExt.getContextView\n\n@RunWith(AndroidJUnit4::class)\nclass CrashHandlerActivityTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n    private val crashHandler = CrashHandler.getInstance(context)!!.also {\n        it.createMockViews = true\n    }\n\n    @get:Rule\n    val launcherRule = ActivityScenarioLauncherRule()\n\n    @Test\n    fun `pressing back dismisses crash view`() {\n        crashHandler.registerCrash(context, RuntimeException(\"BAM\"))\n\n        launcherRule.launch(CrashHandlerActivity::class.java).onActivity {\n            val crashView = it.getContextView<MockCrashView>()\n            assertThat(crashView.wasDismissed, equalTo(false))\n\n            it.onBackPressedDispatcher.onBackPressed()\n\n            assertThat(crashView.wasDismissed, equalTo(true))\n        }\n    }\n\n    /**\n     * This wouldn't ideally happen, but there are scenarios where the Activity is recreated after\n     * \"ok\" is created due to how Android's themed resources work - setting a theme that's different\n     * from the system (which happens in [MainMenuActivity]) can cause even finishing Activity\n     * objects to be recreated.\n     */\n    @Test\n    fun `finishes if crash view is null`() {\n        assertThat(launcherRule.launch(CrashHandlerActivity::class.java).state, equalTo(Lifecycle.State.DESTROYED))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/activities/FirstLaunchActivityTest.kt",
    "content": "package org.odk.collect.android.activities\n\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.scrollTo\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.containsString\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.CoreMatchers.notNullValue\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.Mockito.mock\nimport org.mockito.Mockito.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.projects.ManualProjectCreatorDialog\nimport org.odk.collect.android.projects.QrCodeProjectCreatorDialog\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport org.odk.collect.androidtest.RecordedIntentsRule\nimport org.odk.collect.material.MaterialProgressDialogFragment\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.localization.getLocalizedString\nimport org.odk.collect.testshared.FakeBarcodeScannerViewFactory\nimport org.odk.collect.testshared.RobolectricHelpers\n\n@RunWith(AndroidJUnit4::class)\nclass FirstLaunchActivityTest {\n\n    @get:Rule\n    val launcherRule = ActivityScenarioLauncherRule()\n\n    @get:Rule\n    val activityRule = RecordedIntentsRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesBarcodeScannerViewFactory(settingsProvider: SettingsProvider): BarcodeScannerViewContainer.Factory {\n                return FakeBarcodeScannerViewFactory()\n            }\n        })\n    }\n\n    @Test\n    fun `The QrCodeProjectCreatorDialog should be displayed after clicking on the 'Configure with QR code' button`() {\n        val scenario = launcherRule.launch(FirstLaunchActivity::class.java)\n        scenario.onActivity {\n            onView(withText(org.odk.collect.strings.R.string.configure_with_qr_code)).perform(click())\n            assertThat(\n                it.supportFragmentManager.findFragmentByTag(QrCodeProjectCreatorDialog::class.java.name),\n                `is`(notNullValue())\n            )\n        }\n    }\n\n    @Test\n    fun `The ManualProjectCreatorDialog should be displayed after clicking on the 'Configure manually' button`() {\n        val scenario = launcherRule.launch(FirstLaunchActivity::class.java)\n        scenario.onActivity {\n            onView(withText(org.odk.collect.strings.R.string.configure_manually)).perform(click())\n            assertThat(\n                it.supportFragmentManager.findFragmentByTag(ManualProjectCreatorDialog::class.java.name),\n                `is`(notNullValue())\n            )\n        }\n    }\n\n    @Test\n    fun `The ODK logo should be displayed`() {\n        val scenario = launcherRule.launch(FirstLaunchActivity::class.java)\n        scenario.onActivity {\n            onView(withId(R.id.logo)).check(matches(isDisplayed()))\n        }\n    }\n\n    @Test\n    fun `The app name with its version should be displayed`() {\n        val versionInformation = mock(VersionInformation::class.java)\n        whenever(versionInformation.versionToDisplay).thenReturn(\"vfake\")\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesVersionInformation(): VersionInformation {\n                return versionInformation\n            }\n        })\n\n        val scenario = launcherRule.launch(FirstLaunchActivity::class.java)\n        scenario.onActivity {\n            verify(versionInformation).versionToDisplay\n            onView(\n                withText(\n                    ApplicationProvider.getApplicationContext<Collect>().getLocalizedString(\n                        org.odk.collect.strings.R.string.collect_app_name\n                    ) + \" vfake\"\n                )\n            ).perform(scrollTo()).check(matches(isDisplayed()))\n        }\n    }\n\n    @Test\n    fun `Adding demo project displays a progress dialog`() {\n        val scenario = launcherRule.launch(FirstLaunchActivity::class.java)\n        scenario.onActivity {\n            val dialogClass = MaterialProgressDialogFragment::class.java\n            assertThat(RobolectricHelpers.getFragmentByClass(it.supportFragmentManager, dialogClass), nullValue())\n\n            onView(\n                withText(\n                    containsString(\n                        ApplicationProvider.getApplicationContext<Collect>().getLocalizedString(\n                            org.odk.collect.strings.R.string.try_demo\n                        )\n                    )\n                )\n            ).perform(scrollTo()).perform(click())\n\n            assertThat(RobolectricHelpers.getFragmentByClass(it.supportFragmentManager, dialogClass), notNullValue())\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/activities/FormFillingActivityTest.kt",
    "content": "package org.odk.collect.android.activities\n\nimport android.app.Activity\nimport android.app.Activity.RESULT_OK\nimport android.app.Application\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.fragment.app.DialogFragment\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasAction\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.withContentDescription\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.work.WorkManager\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.external.FormUriActivity\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.android.formhierarchy.FormHierarchyFragmentHostActivity\nimport org.odk.collect.android.formmanagement.FormFillingIntentFactory\nimport org.odk.collect.android.injection.config.AppDependencyComponent\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.storage.StorageSubdirectory\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.CollectHelpers.resetProcess\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidtest.ActivityScenarioExtensions.isFinishing\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport org.odk.collect.androidtest.RecordedIntentsRule\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.externalapp.ExternalAppUtils\nimport org.odk.collect.forms.Form\nimport org.odk.collect.formstest.FormFixtures.form\nimport org.odk.collect.strings.R\nimport org.odk.collect.testshared.ActivityControllerRule\nimport org.odk.collect.testshared.AssertIntentsHelper\nimport org.odk.collect.testshared.EspressoAssertions.assertVisible\nimport org.odk.collect.testshared.FakeScheduler\nimport org.odk.collect.testshared.EspressoInteractions\nimport org.odk.collect.testshared.RobolectricHelpers.recreateWithProcessRestore\nimport org.robolectric.Shadows.shadowOf\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass FormFillingActivityTest {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    @get:Rule\n    val recordedIntentsRule = RecordedIntentsRule()\n\n    @get:Rule\n    val activityControllerRule = ActivityControllerRule()\n\n    @get:Rule\n    val scenarioLauncherRule = ActivityScenarioLauncherRule()\n\n    private val assertIntentsHelper = AssertIntentsHelper()\n\n    private val scheduler = FakeScheduler()\n    private val dependencies = object : AppDependencyModule() {\n        override fun providesScheduler(workManager: WorkManager, application: Application): Scheduler {\n            return scheduler\n        }\n    }\n\n    private val application = ApplicationProvider.getApplicationContext<Application>()\n    private lateinit var component: AppDependencyComponent\n\n    @Before\n    fun setup() {\n        component = CollectHelpers.overrideAppDependencyModule(dependencies)\n    }\n\n    @Test\n    fun whenProcessIsKilledAndRestored_returnsToHierarchyAtQuestion() {\n        val projectId = CollectHelpers.setupDemoProject()\n\n        val form = setupForm(\"forms/two-question.xml\")\n        val intent = FormFillingIntentFactory.newFormIntent(\n            application,\n            FormsContract.getUri(projectId, form!!.dbId),\n            FormFillingActivity::class\n        )\n\n        // Start activity\n        val initial = activityControllerRule.build(FormFillingActivity::class.java, intent).setup()\n        scheduler.flush()\n        assertVisible(withText(\"Two Question\"))\n        assertVisible(withText(\"What is your name?\"))\n\n        EspressoInteractions.clickOn(withText(R.string.form_forward))\n        scheduler.flush()\n        assertVisible(withText(\"What is your age?\"))\n\n        // Recreate and assert we start FormHierarchyFragmentHostActivity\n        val recreated = activityControllerRule.add {\n            initial.recreateWithProcessRestore { resetProcess(dependencies) }\n        }\n\n        scheduler.flush()\n        assertIntentsHelper.assertNewIntent(FormHierarchyFragmentHostActivity::class)\n\n        // Return to FormFillingActivity from FormHierarchyFragmentHostActivity\n        val hierarchyIntent = shadowOf(recreated.get()).nextStartedActivityForResult.intent\n        shadowOf(recreated.get()).receiveResult(hierarchyIntent, Activity.RESULT_CANCELED, null)\n        scheduler.flush()\n\n        assertVisible(withText(\"Two Question\"))\n        assertVisible(withText(\"What is your age?\"))\n    }\n\n    @Test\n    fun whenProcessIsKilledAndRestored_andHierarchyIsOpen_returnsToHierarchyAtQuestion() {\n        val projectId = CollectHelpers.setupDemoProject()\n\n        val form = setupForm(\"forms/two-question.xml\")\n        val intent = FormFillingIntentFactory.newFormIntent(\n            application,\n            FormsContract.getUri(projectId, form!!.dbId),\n            FormFillingActivity::class\n        )\n\n        // Start activity\n        val initial = activityControllerRule.build(FormFillingActivity::class.java, intent).setup()\n        scheduler.flush()\n        assertVisible(withText(\"Two Question\"))\n        assertVisible(withText(\"What is your name?\"))\n\n        EspressoInteractions.clickOn(withText(R.string.form_forward))\n        scheduler.flush()\n        assertVisible(withText(\"What is your age?\"))\n\n        EspressoInteractions.clickOn(withContentDescription(R.string.view_hierarchy))\n        assertIntentsHelper.assertNewIntent(FormHierarchyFragmentHostActivity::class)\n\n        // Recreate and assert we start FormHierarchyFragmentHostActivity\n        val recreated = activityControllerRule.add {\n            initial.recreateWithProcessRestore { resetProcess(dependencies) }\n        }\n\n        scheduler.flush()\n        assertIntentsHelper.assertNewIntent(FormHierarchyFragmentHostActivity::class)\n\n        // Return to FormFillingActivity from FormHierarchyFragmentHostActivity\n        val hierarchyIntent = shadowOf(recreated.get()).nextStartedActivityForResult.intent\n        shadowOf(recreated.get()).receiveResult(hierarchyIntent, Activity.RESULT_CANCELED, null)\n        scheduler.flush()\n\n        assertVisible(withText(\"Two Question\"))\n        assertVisible(withText(\"What is your age?\"))\n    }\n\n    @Test\n    fun whenProcessIsKilledAndRestored_andThereADialogFragmentOpen_doesNotRestoreDialogFragment() {\n        val projectId = CollectHelpers.setupDemoProject()\n\n        val form = setupForm(\"forms/two-question.xml\")\n        val intent = FormFillingIntentFactory.newFormIntent(\n            application,\n            FormsContract.getUri(projectId, form!!.dbId),\n            FormFillingActivity::class\n        )\n\n        // Start activity\n        val initial = activityControllerRule.build(FormFillingActivity::class.java, intent).setup()\n        scheduler.flush()\n        assertVisible(withText(\"Two Question\"))\n        assertVisible(withText(\"What is your name?\"))\n\n        EspressoInteractions.clickOn(withText(R.string.form_forward))\n        scheduler.flush()\n        assertVisible(withText(\"What is your age?\"))\n\n        val initialFragmentManager = initial.get().supportFragmentManager\n        DialogFragmentUtils.showIfNotShowing(TestDialogFragment::class.java, initialFragmentManager)\n        assertThat(\n            initialFragmentManager.fragments.any { it::class == TestDialogFragment::class },\n            equalTo(true)\n        )\n\n        // Recreate and assert we start FormHierarchyFragmentHostActivity\n        val recreated = activityControllerRule.add {\n            initial.recreateWithProcessRestore { resetProcess(dependencies) }\n        }\n\n        scheduler.flush()\n        assertIntentsHelper.assertNewIntent(FormHierarchyFragmentHostActivity::class)\n\n        // Return to FormFillingActivity from FormHierarchyFragmentHostActivity\n        val hierarchyIntent = shadowOf(recreated.get()).nextStartedActivityForResult.intent\n        shadowOf(recreated.get()).receiveResult(hierarchyIntent, Activity.RESULT_CANCELED, null)\n        scheduler.flush()\n\n        assertVisible(withText(\"Two Question\"))\n        assertVisible(withText(\"What is your age?\"))\n    }\n\n    @Test\n    fun whenProcessIsKilledAndRestored_andIsWaitingForExternalData_dataCanStillBeReturned() {\n        val projectId = CollectHelpers.setupDemoProject()\n\n        val form = setupForm(\"forms/two-question-external.xml\")\n        val intent = FormFillingIntentFactory.newFormIntent(\n            application,\n            FormsContract.getUri(projectId, form!!.dbId),\n            FormFillingActivity::class\n        )\n\n        // Start activity\n        val initial = activityControllerRule.build(FormFillingActivity::class.java, intent).setup()\n        scheduler.flush()\n        assertVisible(withText(\"Two Question\"))\n        assertVisible(withText(\"What is your name?\"))\n\n        EspressoInteractions.clickOn(withText(R.string.form_forward))\n        scheduler.flush()\n        assertVisible(withText(\"What is your age?\"))\n\n        // Open external app\n        EspressoInteractions.clickOn(withContentDescription(R.string.launch_app))\n        assertIntentsHelper.assertNewIntent(hasAction(\"com.example.EXAMPLE\"))\n\n        // Recreate with result\n        val returnData = ExternalAppUtils.getReturnIntent(\"159\")\n        activityControllerRule.add {\n            initial.recreateWithProcessRestore(RESULT_OK, returnData) { resetProcess(dependencies) }\n        }\n\n        scheduler.flush()\n\n        assertIntentsHelper.assertNoNewIntent()\n        assertVisible(withText(\"Two Question\"))\n        assertVisible(withText(\"What is your age?\"))\n        assertVisible(withText(\"159\"))\n    }\n\n    /**\n     * This case will usually be protected by [FormUriActivity], but it could be possible when\n     * restoring the app/backstack.\n     */\n    @Test\n    fun whenFormDoesNotExist_showsFatalError() {\n        val projectId = CollectHelpers.setupDemoProject()\n\n        val intent = FormFillingIntentFactory.newFormIntent(\n            application,\n            FormsContract.getUri(projectId, 101),\n            FormFillingActivity::class\n        )\n\n        val scenario = scenarioLauncherRule.launch<FormFillingActivity>(intent)\n        scheduler.flush()\n        assertVisible(\n            withText(\"This form no longer exists, please email support@getodk.org with a description of what you were doing when this happened.\"),\n            root = isDialog()\n        )\n\n        EspressoInteractions.clickOn(withText(R.string.ok), root = isDialog())\n        assertThat(scenario.isFinishing, equalTo(true))\n    }\n\n    private fun setupForm(testFormPath: String): Form? {\n        val formsDir = component.storagePathProvider().getOdkDirPath(StorageSubdirectory.FORMS)\n        val formFile = FileUtils.copyFileFromResources(\n            testFormPath,\n            File(formsDir, \"two-question.xml\")\n        )\n\n        val formsRepository = component.formsRepositoryProvider().create()\n        val form = formsRepository.save(form(formFilePath = formFile.absolutePath))\n        return form\n    }\n\n    class TestDialogFragment : DialogFragment()\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/activities/FormHierarchyFragmentHostActivityTest.kt",
    "content": "package org.odk.collect.android.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.lifecycle.Lifecycle\nimport androidx.test.core.app.ActivityScenario\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.formhierarchy.FormHierarchyFragmentHostActivity\nimport org.odk.collect.android.support.CollectHelpers\n\n@RunWith(AndroidJUnit4::class)\nclass FormHierarchyFragmentHostActivityTest {\n\n    @Before\n    fun setup() {\n        CollectHelpers.setupDemoProject()\n    }\n\n    /**\n     * This can happen if the app process is restored after being kicked from memory.\n     */\n    @Test\n    fun whenFormHasNotLoadedYet_finishes() {\n        val context = ApplicationProvider.getApplicationContext<Context>()\n        val intent = Intent(context, FormHierarchyFragmentHostActivity::class.java).also {\n            it.putExtra(FormHierarchyFragmentHostActivity.EXTRA_SESSION_ID, \"blah\")\n        }\n\n        ActivityScenario.launch<FormHierarchyFragmentHostActivity>(intent).use { scenario ->\n            assertThat(scenario.state, equalTo(Lifecycle.State.DESTROYED))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/application/CollectSettingsChangeHandlerTest.kt",
    "content": "package org.odk.collect.android.application\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.clearInvocations\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoInteractions\nimport org.odk.collect.android.backgroundwork.FormUpdateScheduler\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.preferences.Defaults\nimport org.odk.collect.metadata.PropertyManager\nimport org.odk.collect.settings.keys.ProjectKeys\n\n@RunWith(AndroidJUnit4::class)\nclass CollectSettingsChangeHandlerTest {\n\n    private val propertyManager = mock<PropertyManager>()\n    private val formUpdateScheduler = mock<FormUpdateScheduler>()\n    private val formsDataService = mock<FormsDataService>()\n    private val handler = CollectSettingsChangeHandler(propertyManager, formUpdateScheduler, formsDataService)\n\n    @Test\n    fun `updates PropertyManager when a single setting is changed`() {\n        handler.onSettingChanged(\"projectId\", \"anything\", \"blah\")\n        verify(propertyManager).reload()\n    }\n\n    @Test\n    fun `when changed key is FORM_UPDATE_MODE schedules updates`() {\n        handler.onSettingChanged(\"projectId\", \"anything\", ProjectKeys.KEY_FORM_UPDATE_MODE)\n        verify(formUpdateScheduler).scheduleUpdates(\"projectId\")\n    }\n\n    @Test\n    fun `when changed key is PERIODIC_FORM_UPDATES_CHECK schedules updates`() {\n        handler.onSettingChanged(\n            \"projectId\",\n            \"anything\",\n            ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK\n        )\n        verify(formUpdateScheduler).scheduleUpdates(\"projectId\")\n    }\n\n    @Test\n    fun `when changed key is SERVER_URL clears forms data`() {\n        handler.onSettingChanged(\"projectId\", \"anything\", ProjectKeys.KEY_SERVER_URL)\n        verify(formsDataService).clear(\"projectId\")\n    }\n\n    @Test\n    fun `do not schedule updates if other single settings are changed`() {\n        handler.onSettingChanged(\"projectId\", \"anything\", \"blah\")\n        verifyNoInteractions(formUpdateScheduler)\n    }\n\n    @Test\n    fun `do not clear sync status if other single settings are changed`() {\n        handler.onSettingChanged(\"projectId\", \"anything\", \"blah\")\n        verifyNoInteractions(formsDataService)\n    }\n\n    @Test\n    fun `updates PropertyManager when multiple settings are changed`() {\n        handler.onSettingsChanged(\"projectId\", emptyList(), emptyList())\n        verify(propertyManager).reload()\n    }\n\n    @Test\n    fun `schedules updates when settings change and include form update-related ones`() {\n        val relevantKeys = setOf(ProjectKeys.KEY_FORM_UPDATE_MODE, ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)\n        val allUnprotectedKeys = Defaults.unprotected.keys\n        val allProtectedKeys = Defaults.protected.keys\n\n        allUnprotectedKeys.forEach { key ->\n            handler.onSettingsChanged(\"projectId\", listOf(key), emptyList())\n\n            if (key in relevantKeys) {\n                verify(formUpdateScheduler).scheduleUpdates(\"projectId\")\n            } else {\n                verify(formUpdateScheduler, never()).scheduleUpdates(\"projectId\")\n            }\n\n            clearInvocations(formUpdateScheduler)\n        }\n\n        allProtectedKeys.forEach { key ->\n            handler.onSettingsChanged(\"projectId\", emptyList(), listOf(key))\n            verify(formUpdateScheduler, never()).scheduleUpdates(\"projectId\")\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/application/RobolectricApplication.java",
    "content": "package org.odk.collect.android.application;\n\nimport static android.os.Environment.MEDIA_MOUNTED;\nimport static org.robolectric.Shadows.shadowOf;\n\nimport android.os.StrictMode;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.work.Configuration;\nimport androidx.work.WorkManager;\n\nimport org.odk.collect.db.sqlite.DatabaseConnection;\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard;\nimport org.odk.collect.crashhandler.CrashHandler;\nimport org.robolectric.shadows.ShadowApplication;\nimport org.robolectric.shadows.ShadowEnvironment;\n\n/**\n * @author James Knight\n */\n\npublic class RobolectricApplication extends Collect implements CollectComposeThemeProvider {\n\n    @Override\n    public void onCreate() {\n        // Make sure storage is accessible\n        ShadowEnvironment.setExternalStorageState(MEDIA_MOUNTED);\n\n        // Prevents OKHttp from exploding on initialization https://github.com/robolectric/robolectric/issues/5115\n        System.setProperty(\"javax.net.ssl.trustStore\", \"NONE\");\n\n        // We need this so WorkManager.getInstance doesn't explode\n        try {\n            WorkManager.initialize(ApplicationProvider.getApplicationContext(), new Configuration.Builder().build());\n        } catch (IllegalStateException e) {\n            // initialize() explodes if it's already been called\n        }\n\n        // We don't want to deal with permission checks in Robolectric\n        ShadowApplication shadowApplication = shadowOf(this);\n        shadowApplication.grantPermissions(\"android.permission.ACCESS_FINE_LOCATION\");\n        shadowApplication.grantPermissions(\"android.permission.ACCESS_COARSE_LOCATION\");\n        shadowApplication.grantPermissions(\"android.permission.READ_EXTERNAL_STORAGE\");\n        shadowApplication.grantPermissions(\"android.permission.CAMERA\");\n        shadowApplication.grantPermissions(\"android.permission.RECORD_AUDIO\");\n        shadowApplication.grantPermissions(\"android.permission.GET_ACCOUNTS\");\n\n        // These clear static state that can't persist from test to test\n        DatabaseConnection.closeAll();\n\n        // We don't want any clicks to be blocked\n        MultiClickGuard.test = true;\n\n        CrashHandler.uninstall(this);\n\n        super.onCreate();\n\n        // Don't enforce strict mode as we always use the \"main\" thread in Robolectric\n        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()\n                .permitAll()\n                .permitCustomSlowCalls()\n                .build()\n        );\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/application/initialization/AnalyticsInitializerTest.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.only\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\n\nclass AnalyticsInitializerTest {\n\n    private val analytics = mock<Analytics>()\n    private val settingsProvider = InMemSettingsProvider()\n    private val versionInformation = mock<VersionInformation> {\n        on { isBeta } doReturn false\n    }\n\n    @Test\n    fun whenBetaVersion_enablesAnalytics() {\n        whenever(versionInformation.isBeta).thenReturn(true)\n\n        val analyticsInitializer = AnalyticsInitializer(\n            analytics,\n            versionInformation,\n            settingsProvider\n        )\n        analyticsInitializer.initialize()\n        verify(analytics, only()).setAnalyticsCollectionEnabled(true)\n    }\n\n    @Test\n    fun whenBetaVersion_andAnalyticsDisabledInSettings_enablesAnalytics() {\n        whenever(versionInformation.isBeta).thenReturn(true)\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_ANALYTICS, false)\n\n        val analyticsInitializer = AnalyticsInitializer(\n            analytics,\n            versionInformation,\n            settingsProvider\n        )\n        analyticsInitializer.initialize()\n        verify(analytics, only()).setAnalyticsCollectionEnabled(true)\n    }\n\n    @Test\n    fun whenAnalyticsDisabledInSettings_disablesAnalytics() {\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_ANALYTICS, false)\n\n        val analyticsInitializer =\n            AnalyticsInitializer(analytics, versionInformation, settingsProvider)\n        analyticsInitializer.initialize()\n        verify(analytics, only()).setAnalyticsCollectionEnabled(false)\n    }\n\n    @Test\n    fun whenAnalyticsEnabledInSettings_enablesAnalytics() {\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_ANALYTICS, true)\n\n        val analyticsInitializer =\n            AnalyticsInitializer(analytics, versionInformation, settingsProvider)\n        analyticsInitializer.initialize()\n        verify(analytics, only()).setAnalyticsCollectionEnabled(true)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/application/initialization/CachedFormsCleanerTest.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.containsInAnyOrder\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.projects.Project\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass CachedFormsCleanerTest {\n    private val component = DaggerUtils.getComponent(ApplicationProvider.getApplicationContext<Context>() as Application)\n    private val projectsRepository = component.projectsRepository()\n    private val projectDependencyModuleFactory = component.projectDependencyModuleFactory()\n\n    private val cachedFormsCleaner = CachedFormsCleaner(projectsRepository, projectDependencyModuleFactory)\n\n    @Test\n    fun `cleaner should be run on every app upgrade`() {\n        assertThat(cachedFormsCleaner.key(), equalTo(null))\n    }\n\n    @Test\n    fun `cached forms should be removed on app upgrade from every project`() {\n        projectsRepository.save(Project.Saved(\"1\", \"Project 1\", \"1\", \"#000000\"))\n        projectsRepository.save(Project.Saved(\"2\", \"Project 2\", \"2\", \"#000000\"))\n\n        val project1DependencyModule = projectDependencyModuleFactory.create(\"1\").also {\n            File(it.cacheDir, \"file1.formdef\").createNewFile()\n            File(it.cacheDir, \"file2.formdef\").createNewFile()\n            File(it.cacheDir, \"file3.save\").createNewFile()\n            File(it.cacheDir, \"file4.txt\").createNewFile()\n        }\n        val project2DependencyModule = projectDependencyModuleFactory.create(\"2\").also {\n            File(it.cacheDir, \"file1.formdef\").createNewFile()\n            File(it.cacheDir, \"file2.formdef\").createNewFile()\n            File(it.cacheDir, \"file3.save\").createNewFile()\n            File(it.cacheDir, \"file4.txt\").createNewFile()\n        }\n\n        cachedFormsCleaner.run()\n\n        val cachedFilesProject1 = File(project1DependencyModule.cacheDir).listFiles()\n        val cachedFilesProject2 = File(project2DependencyModule.cacheDir).listFiles()\n\n        assertThat(cachedFilesProject1.size, equalTo(2))\n        assertThat(cachedFilesProject1.map { it.name }.toList(), containsInAnyOrder(\"file3.save\", \"file4.txt\"))\n\n        assertThat(cachedFilesProject2.size, equalTo(2))\n        assertThat(cachedFilesProject2.map { it.name }.toList(), containsInAnyOrder(\"file3.save\", \"file4.txt\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/application/initialization/ExistingSettingsMigratorTest.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.hamcrest.Matchers.nullValue\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.ODKAppSettingsMigrator\n\nclass ExistingSettingsMigratorTest {\n\n    @Test\n    fun `migrates unprotected and protected settings for each project`() {\n        val projectsRepository = InMemProjectsRepository()\n        val project1 = projectsRepository.save(Project.New(\"1\", \"1\", \"#ffffff\"))\n        val project2 = projectsRepository.save(Project.New(\"2\", \"2\", \"#ffffff\"))\n\n        val settingsProvider = InMemSettingsProvider()\n        val settingsMigrator = mock<ODKAppSettingsMigrator>()\n        val existingSettingsMigrator =\n            ExistingSettingsMigrator(projectsRepository, settingsProvider, settingsMigrator)\n\n        existingSettingsMigrator.run()\n        verify(settingsMigrator).migrate(\n            settingsProvider.getUnprotectedSettings(project1.uuid),\n            settingsProvider.getProtectedSettings(project1.uuid)\n        )\n        verify(settingsMigrator).migrate(\n            settingsProvider.getUnprotectedSettings(project2.uuid),\n            settingsProvider.getProtectedSettings(project2.uuid)\n        )\n    }\n\n    @Test\n    fun `has null key`() {\n        val existingSettingsMigrator = ExistingSettingsMigrator(mock(), mock(), mock())\n        assertThat(existingSettingsMigrator.key(), `is`(nullValue()))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/application/initialization/GoogleDriveProjectsDeleterTest.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoMoreInteractions\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.projects.DeleteProjectResult\nimport org.odk.collect.android.projects.ProjectDeleter\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\n\nclass GoogleDriveProjectsDeleterTest {\n    private val projectsRepository = InMemProjectsRepository()\n    private val settingsProvider = InMemSettingsProvider()\n    private val projectDeleter = mock<ProjectDeleter>()\n\n    private val googleDriveProjectsDeleter = GoogleDriveProjectsDeleter(projectsRepository, settingsProvider, projectDeleter)\n\n    @Test\n    fun `GoogleDriveProjectsDeleter should have null key`() {\n        assertThat(googleDriveProjectsDeleter.key(), equalTo(null))\n    }\n\n    @Test\n    fun `only GD projects should be considered for deletion`() {\n        projectsRepository.save(Project.Saved(\"1\", \"project\", \"Q\", \"#000000\"))\n        projectsRepository.save(Project.Saved(\"2\", \"project\", \"Q\", \"#000000\"))\n        projectsRepository.save(Project.Saved(\"3\", \"project\", \"Q\", \"#000000\"))\n        projectsRepository.save(Project.Saved(\"4\", \"project\", \"Q\", \"#000000\"))\n\n        settingsProvider.getUnprotectedSettings(\"1\").save(ProjectKeys.KEY_PROTOCOL, ProjectKeys.PROTOCOL_SERVER)\n        settingsProvider.getUnprotectedSettings(\"2\").save(ProjectKeys.KEY_PROTOCOL, ProjectKeys.PROTOCOL_GOOGLE_SHEETS)\n        settingsProvider.getUnprotectedSettings(\"3\").save(ProjectKeys.KEY_PROTOCOL, ProjectKeys.PROTOCOL_GOOGLE_SHEETS)\n        settingsProvider.getUnprotectedSettings(\"4\").save(ProjectKeys.KEY_PROTOCOL, ProjectKeys.PROTOCOL_SERVER)\n\n        googleDriveProjectsDeleter.run()\n\n        verify(projectDeleter).deleteProject(\"2\")\n        verify(projectDeleter).deleteProject(\"3\")\n        verifyNoMoreInteractions(projectDeleter)\n    }\n\n    @Test\n    fun `If GD project deletion results in 'DeletedSuccessfullyLastProject' there is no attempt to convert it to ODK protocol and mark it as an old GD project`() {\n        projectsRepository.save(Project.Saved(\"1\", \"project\", \"Q\", \"#000000\"))\n        settingsProvider.getUnprotectedSettings(\"1\").save(ProjectKeys.KEY_PROTOCOL, ProjectKeys.PROTOCOL_GOOGLE_SHEETS)\n        whenever(projectDeleter.deleteProject(\"1\")).thenReturn(DeleteProjectResult.DeletedSuccessfullyLastProject)\n\n        googleDriveProjectsDeleter.run()\n\n        assertThat(settingsProvider.getUnprotectedSettings(\"1\").getString(ProjectKeys.KEY_PROTOCOL), equalTo(ProjectKeys.PROTOCOL_GOOGLE_SHEETS))\n        assertThat(projectsRepository.get(\"1\")!!.isOldGoogleDriveProject, equalTo(false))\n    }\n\n    @Test\n    fun `If GD project deletion results in 'DeletedSuccessfullyInactiveProject' there is no attempt to convert it to ODK protocol and mark it as an old GD project`() {\n        projectsRepository.save(Project.Saved(\"1\", \"project\", \"Q\", \"#000000\"))\n        settingsProvider.getUnprotectedSettings(\"1\").save(ProjectKeys.KEY_PROTOCOL, ProjectKeys.PROTOCOL_GOOGLE_SHEETS)\n        whenever(projectDeleter.deleteProject(\"1\")).thenReturn(DeleteProjectResult.DeletedSuccessfullyInactiveProject)\n\n        googleDriveProjectsDeleter.run()\n\n        assertThat(settingsProvider.getUnprotectedSettings(\"1\").getString(ProjectKeys.KEY_PROTOCOL), equalTo(ProjectKeys.PROTOCOL_GOOGLE_SHEETS))\n        assertThat(projectsRepository.get(\"1\")!!.isOldGoogleDriveProject, equalTo(false))\n    }\n\n    @Test\n    fun `If GD project deletion results in 'DeletedSuccessfullyCurrentProject' there is no attempt to convert it to ODK protocol and mark it as an old GD project`() {\n        projectsRepository.save(Project.Saved(\"1\", \"project\", \"Q\", \"#000000\"))\n        settingsProvider.getUnprotectedSettings(\"1\").save(ProjectKeys.KEY_PROTOCOL, ProjectKeys.PROTOCOL_GOOGLE_SHEETS)\n        whenever(projectDeleter.deleteProject(\"1\")).thenReturn(DeleteProjectResult.DeletedSuccessfullyCurrentProject(mock()))\n\n        googleDriveProjectsDeleter.run()\n\n        assertThat(settingsProvider.getUnprotectedSettings(\"1\").getString(ProjectKeys.KEY_PROTOCOL), equalTo(ProjectKeys.PROTOCOL_GOOGLE_SHEETS))\n        assertThat(projectsRepository.get(\"1\")!!.isOldGoogleDriveProject, equalTo(false))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/application/initialization/SavepointsImporterTest.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.contains\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.storage.StorageSubdirectory\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.formstest.FormFixtures\nimport org.odk.collect.formstest.InstanceFixtures\nimport org.odk.collect.projects.Project\nimport org.odk.collect.shared.TimeInMs\nimport org.odk.collect.shared.strings.RandomString\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass SavepointsImporterTest {\n    private val component = DaggerUtils.getComponent(ApplicationProvider.getApplicationContext<Context>() as Application)\n\n    private val projectsRepository = component.projectsRepository()\n    private val projectDependencyModuleFactory = component.projectDependencyModuleFactory()\n\n    private val savepointsImporter =\n        SavepointsImporter(projectsRepository, projectDependencyModuleFactory)\n\n    private val project = projectsRepository.save(Project.DEMO_PROJECT)\n    private val projectDependencyModule = projectDependencyModuleFactory.create(project.uuid)\n    private val savepointsRepository = projectDependencyModule.savepointsRepository\n    private val storagePathProvider = component.storagePathProvider()\n    private val formsRepository = projectDependencyModule.formsRepository\n    private val instancesRepository = projectDependencyModule.instancesRepository\n\n    @Test\n    fun ifABlankFormHasNoSavepoint_nothingShouldBeImported() {\n        // create blank forms\n        createBlankForm(project, \"sampleForm\", \"1\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        assertThat(savepoints.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun ifABlankFormHasASavepointCreatedEarlierThanTheForm_nothingShouldBeImported() {\n        // create blank forms\n        createBlankForm(project, \"sampleForm\", \"1\", date = System.currentTimeMillis() + TimeInMs.ONE_HOUR)\n\n        // create savepoints\n        createFileInCache(project, \"sampleForm_2024-04-10_01-35-41.xml.save\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        assertThat(savepoints.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun ifABlankFormHasASavepointButTheFormIsSoftDeleted_nothingShouldBeImported() {\n        // create blank forms\n        createBlankForm(project, \"sampleForm\", \"1\", deleted = true)\n\n        // create savepoints\n        createFileInCache(project, \"sampleForm_2024-04-10_01-35-41.xml.save\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        assertThat(savepoints.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun ifAFileForABlankFormExistsWithMatchingName_butIncorrectSuffix_nothingShouldBeImported() {\n        val formName = \"sampleForm\"\n\n        // create blank forms\n        createBlankForm(project, formName, \"1\", \"1\")\n\n        // create savepoints\n        createFileInCache(project, \"${formName}_2024-04-10_01-35-41.xml\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        assertThat(savepoints.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun ifThereAreMultipleDifferentBlankFormsWithSavepoints_allSavepointsShouldBeImported() {\n        val form1Name = \"sampleForm1\"\n        val form2Name = \"sampleForm2\"\n\n        // create blank forms\n        val blankForm1 = createBlankForm(project, form1Name, \"1\")\n        val blankForm2 = createBlankForm(project, form2Name, \"2\")\n\n        // create savepoints\n        val savepointFile1 = createFileInCache(project, \"${form1Name}_2024-04-10_01-35-41.xml.save\")\n        val savepointFile2 = createFileInCache(project, \"${form2Name}_2024-04-10_01-35-42.xml.save\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        val expectedSavepoint1 =\n            Savepoint(blankForm1.dbId, null, savepointFile1.absolutePath, \"${storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, project.uuid)}/${form1Name}_2024-04-10_01-35-41/${form1Name}_2024-04-10_01-35-41.xml\")\n        val expectedSavepoint2 =\n            Savepoint(blankForm2.dbId, null, savepointFile2.absolutePath, \"${storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, project.uuid)}/${form2Name}_2024-04-10_01-35-42/${form2Name}_2024-04-10_01-35-42.xml\")\n        assertThat(savepoints, contains(expectedSavepoint1, expectedSavepoint2))\n    }\n\n    @Test\n    fun formFileNameShouldBeEscapedAndTreatedAsLiteralTextToAvoidPatternSyntaxException() {\n        val form1Name = \"sampleForm(\"\n\n        // create blank forms\n        val blankForm1 = createBlankForm(project, form1Name, \"1\")\n\n        // create savepoints\n        val savepointFile1 = createFileInCache(project, \"${form1Name}_2024-04-10_01-35-41.xml.save\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        val expectedSavepoint1 =\n            Savepoint(blankForm1.dbId, null, savepointFile1.absolutePath, \"${storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, project.uuid)}/${form1Name}_2024-04-10_01-35-41/${form1Name}_2024-04-10_01-35-41.xml\")\n\n        assertThat(savepoints, contains(expectedSavepoint1))\n    }\n\n    @Test\n    fun ifThereAreMultipleVersionsOfTheSameBlankFormWithSavepoints_allSavepointsShouldBeImported() {\n        val form1Name = \"sampleForm\"\n        val form2Name = \"sampleForm_1\"\n\n        // create blank forms\n        val blankForm1 = createBlankForm(project, form1Name, \"1\", \"1\", date = 1)\n        val blankForm2 = createBlankForm(project, form2Name, \"1\", \"2\", date = 2)\n\n        // create savepoints\n        val savepointFile1 = createFileInCache(project, \"${form1Name}_2024-04-10_01-35-41.xml.save\")\n        val savepointFile2 = createFileInCache(project, \"${form2Name}_2024-04-10_01-35-42.xml.save\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        val expectedSavepoint1 =\n            Savepoint(blankForm1.dbId, null, savepointFile1.absolutePath, \"${storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, project.uuid)}/${form1Name}_2024-04-10_01-35-41/${form1Name}_2024-04-10_01-35-41.xml\")\n        val expectedSavepoint2 =\n            Savepoint(blankForm2.dbId, null, savepointFile2.absolutePath, \"${storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, project.uuid)}/${form2Name}_2024-04-10_01-35-42/${form2Name}_2024-04-10_01-35-42.xml\")\n        assertThat(savepoints, contains(expectedSavepoint1, expectedSavepoint2))\n    }\n\n    @Test\n    fun ifASavedFormHasNoSavepoint_nothingShouldBeImported() {\n        // create blank forms\n        val form = createBlankForm(project, \"sampleForm\", \"1\")\n\n        // create saved forms\n        createSavedForm(\"sampleForm\", form)\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        assertThat(savepoints.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun ifASavedFormHasASavepointCreatedEarlierThanTheForm_nothingShouldBeImported() {\n        // create blank forms\n        val form = createBlankForm(project, \"sampleForm\", \"1\")\n\n        // create saved forms\n        val savedForm = createSavedForm(\"sampleForm\", form, lastStatusChangeDate = System.currentTimeMillis() + TimeInMs.ONE_HOUR)\n\n        // create savepoints\n        createFileInCache(project, \"${File(savedForm.instanceFilePath).name}.save\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        assertThat(savepoints.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun ifASavedFormHasASavepointButItsBlankFormIsSoftDeleted_nothingShouldBeImported() {\n        // create blank forms\n        val form = createBlankForm(project, \"sampleForm\", \"1\", deleted = true)\n\n        // create saved forms\n        val savedForm = createSavedForm(\"sampleForm\", form)\n\n        // create savepoints\n        createFileInCache(project, \"${File(savedForm.instanceFilePath).name}.save\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        assertThat(savepoints.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun ifASavedFormHasASavepointButItsBlankFormDoesNotExist_nothingShouldBeImported() {\n        // create saved forms\n        val savedForm = createSavedForm(\"sampleForm\", FormFixtures.form(\"1\"))\n\n        // create savepoints\n        createFileInCache(project, \"${File(savedForm.instanceFilePath).name}.save\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        assertThat(savepoints.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun ifASavedFormHasASavepointButTheFormIsSoftDeleted_nothingShouldBeImported() {\n        // create blank forms\n        val form = createBlankForm(project, \"sampleForm\", \"1\")\n\n        // create saved forms\n        val savedForm = createSavedForm(\"sampleForm\", form, deletedDate = System.currentTimeMillis())\n\n        // create savepoints\n        createFileInCache(project, \"${File(savedForm.instanceFilePath).name}.save\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        assertThat(savepoints.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun ifAFileForASavedFormExistsWithMatchingName_butIncorrectSuffix_nothingShouldBeImported() {\n        val formName = \"sampleForm\"\n\n        // create blank forms\n        val form = createBlankForm(project, formName, \"1\")\n\n        // create saved forms\n        val savedForm = createSavedForm(formName, form)\n\n        // create savepoints\n        createFileInCache(project, File(savedForm.instanceFilePath).name)\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        assertThat(savepoints.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun ifThereAreMultipleDifferentSavedFormsWithSavepoints_allSavepointsShouldBeImported() {\n        val form1Name = \"sampleForm1\"\n        val form2Name = \"sampleForm2\"\n\n        // create blank forms\n        val form1 = createBlankForm(project, form1Name, \"1\")\n        val form2 = createBlankForm(project, form2Name, \"2\")\n\n        // create saved forms\n        val savedForm1 = createSavedForm(form1Name, form1)\n        val savedForm2 = createSavedForm(form2Name, form2)\n\n        // create savepoints\n        val savepointFile1 = createFileInCache(project, \"${File(savedForm1.instanceFilePath).name}.save\")\n        val savepointFile2 = createFileInCache(project, \"${File(savedForm2.instanceFilePath).name}.save\")\n\n        // trigger importing\n        savepointsImporter.run()\n\n        // verify import\n        val savepoints = savepointsRepository.getAll()\n        val expectedSavepoint1 = Savepoint(1, 1, savepointFile1.absolutePath, savedForm1.instanceFilePath)\n        val expectedSavepoint2 = Savepoint(2, 2, savepointFile2.absolutePath, savedForm2.instanceFilePath)\n        assertThat(savepoints, contains(expectedSavepoint1, expectedSavepoint2))\n    }\n\n    private fun createBlankForm(project: Project.Saved, formName: String, formId: String, formVersion: String = \"1\", date: Long = 0, deleted: Boolean = false): Form {\n        val formFile = File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, project.uuid), \"$formName.xml\").also {\n            it.writeText(RandomString.randomString(10))\n        }\n        val blankForm = formsRepository.save(FormFixtures.form(formId, formVersion, formFile.absolutePath))\n        return formsRepository.save(Form.Builder(blankForm).date(date).deleted(deleted).build())\n    }\n\n    private fun createSavedForm(formName: String, form: Form, lastStatusChangeDate: Long = System.currentTimeMillis() - TimeInMs.ONE_HOUR, deletedDate: Long? = null): Instance {\n        return instancesRepository.save(InstanceFixtures.instance(displayName = formName, form = form, lastStatusChangeDate = lastStatusChangeDate, deletedDate = deletedDate))\n    }\n\n    private fun createFileInCache(project: Project.Saved, fileName: String): File {\n        val cacheDir = File(storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, project.uuid))\n        return File(cacheDir, fileName).also {\n            it.writeText(RandomString.randomString(10))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/application/initialization/ScheduledWorkUpgradeTest.kt",
    "content": "package org.odk.collect.android.application.initialization\n\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.backgroundwork.FormUpdateScheduler\nimport org.odk.collect.android.backgroundwork.InstanceSubmitScheduler\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\n\nclass ScheduledWorkUpgradeTest {\n\n    @Test\n    fun `cancels all existing background jobs`() {\n        val scheduler = mock<Scheduler>()\n        val scheduledWorkUpgrade = ScheduledWorkUpgrade(\n            scheduler,\n            InMemProjectsRepository(),\n            mock(),\n            mock()\n        )\n\n        scheduledWorkUpgrade.run()\n        verify(scheduler).cancelAllDeferred()\n    }\n\n    @Test\n    fun `schedules updates for all projects`() {\n        val projectsRepository = InMemProjectsRepository()\n        val project1 = projectsRepository.save(Project.New(\"1\", \"1\", \"#ffffff\"))\n        val project2 = projectsRepository.save(Project.New(\"2\", \"2\", \"#ffffff\"))\n\n        val formUpdateScheduler = mock<FormUpdateScheduler>()\n        val scheduledWorkUpgrade = ScheduledWorkUpgrade(\n            mock(),\n            projectsRepository,\n            formUpdateScheduler,\n            mock()\n        )\n\n        scheduledWorkUpgrade.run()\n        verify(formUpdateScheduler).scheduleUpdates(project1.uuid)\n        verify(formUpdateScheduler).scheduleUpdates(project2.uuid)\n    }\n\n    @Test\n    fun `schedules submits for all projects`() {\n        val projectsRepository = InMemProjectsRepository()\n        val project1 = projectsRepository.save(Project.New(\"1\", \"1\", \"#ffffff\"))\n        val project2 = projectsRepository.save(Project.New(\"2\", \"2\", \"#ffffff\"))\n\n        val instanceSubmitScheduler = mock<InstanceSubmitScheduler>()\n        val scheduledWorkUpgrade = ScheduledWorkUpgrade(\n            mock(),\n            projectsRepository,\n            mock(),\n            instanceSubmitScheduler\n        )\n\n        scheduledWorkUpgrade.run()\n        verify(instanceSubmitScheduler).scheduleAutoSend(project1.uuid)\n        verify(instanceSubmitScheduler).scheduleFormAutoSend(project1.uuid)\n        verify(instanceSubmitScheduler).scheduleAutoSend(project2.uuid)\n        verify(instanceSubmitScheduler).scheduleFormAutoSend(project2.uuid)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/application/initialization/upgrade/BeforeProjectsInstallDetectorTest.kt",
    "content": "package org.odk.collect.android.application.initialization.upgrade\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.preference.PreferenceManager\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass BeforeProjectsInstallDetectorTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n\n    @Test\n    fun `returns false if no legacy installation`() {\n        val detector = BeforeProjectsInstallDetector(context)\n        assertThat(detector.installDetected(), equalTo(false))\n    }\n\n    /**\n     * Account for installs of versions before `LAST_LAUNCHED` was added\n     */\n    @Test\n    fun `returns true if legacy general prefs is not empty`() {\n        PreferenceManager.getDefaultSharedPreferences(context).edit()\n            .putString(\"anything\", \"anything\")\n            .apply()\n\n        val detector = BeforeProjectsInstallDetector(context)\n        assertThat(detector.installDetected(), equalTo(true))\n    }\n\n    /**\n     * Account for installs of versions before `LAST_LAUNCHED` was added\n     */\n    @Test\n    fun `returns true if legacy admin prefs is not empty`() {\n        context.getSharedPreferences(\"admin_prefs\", Context.MODE_PRIVATE).edit()\n            .putString(\"anything\", \"anything\")\n            .apply()\n\n        val detector = BeforeProjectsInstallDetector(context)\n        assertThat(detector.installDetected(), equalTo(true))\n    }\n\n    /**\n     * Account for installs of versions before `LAST_LAUNCHED` was added\n     */\n    @Test\n    fun `returns true if legacy metadata dir is not empty`() {\n        val metadataDir = File(context.getExternalFilesDir(null), \"metadata\").also { it.mkdir() }\n        File(metadataDir, \"something\").createNewFile()\n\n        val detector = BeforeProjectsInstallDetector(context)\n        assertThat(detector.installDetected(), equalTo(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/audio/AudioButtonTest.java",
    "content": "package org.odk.collect.android.audio;\n\nimport android.app.Activity;\n\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.R;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.odk.collect.testshared.RobolectricHelpers.getCreatedFromResId;\n\n@RunWith(AndroidJUnit4.class)\npublic class AudioButtonTest {\n\n    private AudioButton button;\n\n    @Before\n    public void setup() {\n        Activity activity = RobolectricHelpers.createThemedActivity(FragmentActivity.class);\n        button = new AudioButton(activity);\n    }\n\n    @Test\n    public void isPlayingReturnsFalse_andShowsPlayIcon() {\n        assertThat(button.isPlaying(), equalTo(false));\n        assertThat(getCreatedFromResId(button.getIcon()), equalTo(R.drawable.ic_volume_up_black_24dp));\n    }\n\n    @Test\n    public void whenPlayingIsTrue_showsPlayingIcon() {\n        button.setPlaying(true);\n        assertThat(getCreatedFromResId(button.getIcon()), equalTo(R.drawable.ic_stop_black_24dp));\n    }\n\n    @Test\n    public void whenPlayingIsFalse_showsPlayIcon() {\n        button.setPlaying(false);\n        assertThat(getCreatedFromResId(button.getIcon()), equalTo(R.drawable.ic_volume_up_black_24dp));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/audio/AudioControllerViewTest.java",
    "content": " package org.odk.collect.android.audio;\n\nimport android.widget.SeekBar;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.support.SwipableParentActivity;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoInteractions;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.odk.collect.android.support.CollectHelpers.buildThemedActivity;\nimport static org.robolectric.Shadows.shadowOf;\nimport static org.robolectric.shadows.ShadowView.innerText;\n\n@RunWith(AndroidJUnit4.class)\npublic class AudioControllerViewTest {\n\n    private SwipableParentActivity activity;\n    private AudioControllerView view;\n\n    @Before\n    public void setup() {\n        activity = buildThemedActivity(SwipableParentActivity.class).get();\n        view = new AudioControllerView(activity);\n    }\n\n    @Test\n    public void setDuration_showsDurationInMinutesAndSeconds() {\n        view.setDuration(52000);\n        assertThat(view.binding.totalDuration.getText().toString(), equalTo(\"00:52\"));\n    }\n\n    @Test\n    public void setPosition_showsPositionInMinutesAndSeconds() {\n        view.setDuration(65000);\n\n        view.setPosition(64000);\n        assertThat(view.binding.currentDuration.getText().toString(), equalTo(\"01:04\"));\n    }\n\n    @Test\n    public void setPosition_changesSeekBarPosition() {\n        view.setDuration(65000);\n        assertThat(view.binding.seekBar.getProgress(), is(0));\n        assertThat(view.binding.seekBar.getMax(), is(65000));\n\n        view.setPosition(8000);\n        assertThat(view.binding.seekBar.getProgress(), is(8000));\n        assertThat(view.binding.seekBar.getMax(), is(65000));\n    }\n\n    @Test\n    public void swipingSeekBar_whenPaused_skipsToPositionOnceStopped() {\n        AudioControllerView.Listener listener = mock(AudioControllerView.Listener.class);\n\n        view.setListener(listener);\n        view.setDuration(12000);\n\n        SeekBar seekBar = view.binding.seekBar;\n        shadowOf(seekBar).getOnSeekBarChangeListener().onStartTrackingTouch(seekBar);\n\n        shadowOf(seekBar).getOnSeekBarChangeListener().onProgressChanged(seekBar, 7000, true);\n        assertThat(innerText(view.findViewById(R.id.currentDuration)), equalTo(\"00:07\"));\n        verifyNoInteractions(listener); // We don't change position yet\n\n        shadowOf(seekBar).getOnSeekBarChangeListener().onProgressChanged(seekBar, 5000, true);\n        assertThat(innerText(view.findViewById(R.id.currentDuration)), equalTo(\"00:05\"));\n        verifyNoInteractions(listener); // We don't change position yet\n\n        shadowOf(seekBar).getOnSeekBarChangeListener().onStopTrackingTouch(seekBar);\n        assertThat(innerText(view.findViewById(R.id.currentDuration)), equalTo(\"00:05\"));\n        verify(listener).onPositionChanged(5000);\n    }\n\n    @Test\n    public void swipingSeekBar_whenPlaying_pauses_andThenSkipsToPositionAndPlaysOnceStopped() {\n        AudioControllerView.Listener listener = mock(AudioControllerView.Listener.class);\n\n        view.setListener(listener);\n        view.setDuration(12000);\n        view.setPlaying(true);\n\n        SeekBar seekBar = view.binding.seekBar;\n        shadowOf(seekBar).getOnSeekBarChangeListener().onStartTrackingTouch(seekBar);\n        verify(listener).onPauseClicked();\n\n        shadowOf(seekBar).getOnSeekBarChangeListener().onProgressChanged(seekBar, 7000, true);\n        assertThat(innerText(view.findViewById(R.id.currentDuration)), equalTo(\"00:07\"));\n        verifyNoMoreInteractions(listener); // We don't change position yet\n\n        shadowOf(seekBar).getOnSeekBarChangeListener().onProgressChanged(seekBar, 5000, true);\n        assertThat(innerText(view.findViewById(R.id.currentDuration)), equalTo(\"00:05\"));\n        verifyNoMoreInteractions(listener); // We don't change position yet\n\n        shadowOf(seekBar).getOnSeekBarChangeListener().onStopTrackingTouch(seekBar);\n        assertThat(innerText(view.findViewById(R.id.currentDuration)), equalTo(\"00:05\"));\n        verify(listener).onPositionChanged(5000);\n        verify(listener).onPlayClicked();\n    }\n\n    @Test\n    public void whenSwiping_notifiesSwipeableParent() {\n        SeekBar seekBar = view.findViewById(R.id.seekBar);\n\n        shadowOf(seekBar).getOnSeekBarChangeListener().onStartTrackingTouch(seekBar);\n        assertThat(activity.isSwipingAllowed(), equalTo(false));\n\n        shadowOf(seekBar).getOnSeekBarChangeListener().onStopTrackingTouch(seekBar);\n        assertThat(activity.isSwipingAllowed(), equalTo(true));\n    }\n\n    @Test\n    public void whenNoListenerSet_clickingRemove_doesNotExplode() {\n        view.binding.remove.performClick();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/audio/AudioRecordingControllerFragmentTest.java",
    "content": "package org.odk.collect.android.audio;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.testshared.RobolectricHelpers.getFragmentByClass;\nimport static org.robolectric.Shadows.shadowOf;\n\nimport android.app.Application;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.fragment.app.testing.FragmentScenario;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.lifecycle.viewmodel.CreationExtras;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.BackgroundAudioViewModel;\nimport org.odk.collect.android.formentry.FormEntryViewModel;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData;\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder;\nimport org.odk.collect.audiorecorder.recorder.Output;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.testsupport.StubAudioRecorder;\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule;\nimport org.odk.collect.webpage.CustomTabsWebPageService;\n\nimport java.io.File;\nimport java.io.IOException;\n\n@RunWith(AndroidJUnit4.class)\npublic class AudioRecordingControllerFragmentTest {\n\n    public StubAudioRecorder audioRecorder;\n    private BackgroundAudioViewModel backgroundAudioViewModel;\n    private FormEntryViewModel formEntryViewModel;\n    private MutableNonNullLiveData<Boolean> hasBackgroundRecording;\n    private MutableNonNullLiveData<Boolean> isBackgroundRecordingEnabled;\n    private CustomTabsWebPageService customTabsWebPageService;\n\n    private ViewModelProvider.Factory viewModelFactory = new ViewModelProvider.Factory() {\n        @NonNull\n        @Override\n        public <T extends ViewModel> T create(@NonNull Class<T> modelClass, @NonNull CreationExtras extras) {\n            if (modelClass == BackgroundAudioViewModel.class) {\n                return (T) backgroundAudioViewModel;\n            } else if (modelClass == FormEntryViewModel.class) {\n                return (T) formEntryViewModel;\n            } else {\n                throw new IllegalArgumentException();\n            }\n        }\n    };\n\n    @Rule\n    public FragmentScenarioLauncherRule launcherRule = new FragmentScenarioLauncherRule(new FragmentFactoryBuilder()\n            .forClass(AudioRecordingControllerFragment.class, () -> new AudioRecordingControllerFragment(viewModelFactory))\n            .build());\n\n    @Before\n    public void setup() throws IOException {\n        File stubRecording = File.createTempFile(\"test\", \".m4a\");\n        stubRecording.deleteOnExit();\n\n        audioRecorder = new StubAudioRecorder(stubRecording.getAbsolutePath());\n        backgroundAudioViewModel = mock(BackgroundAudioViewModel.class);\n        formEntryViewModel = mock(FormEntryViewModel.class);\n\n        hasBackgroundRecording = new MutableNonNullLiveData<>(false);\n        when(formEntryViewModel.hasBackgroundRecording()).thenReturn(hasBackgroundRecording);\n        isBackgroundRecordingEnabled = new MutableNonNullLiveData<>(false);\n        when(backgroundAudioViewModel.isBackgroundRecordingEnabled()).thenReturn(isBackgroundRecordingEnabled);\n\n        customTabsWebPageService = mock(CustomTabsWebPageService.class);\n\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public AudioRecorder providesAudioRecorder(Application application) {\n                return audioRecorder;\n            }\n\n            @Override\n            public CustomTabsWebPageService providesWebPageService() {\n                return customTabsWebPageService;\n            }\n        });\n    }\n\n    @Test\n    public void updatesTimecode() {\n        audioRecorder.start(\"session\", Output.AAC);\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(fragment.binding.timeCode.getText().toString(), equalTo(\"00:00\"));\n\n            audioRecorder.setDuration(40000);\n            assertThat(fragment.binding.timeCode.getText().toString(), equalTo(\"00:40\"));\n        });\n    }\n\n    @Test\n    public void updatesWaveform() {\n        audioRecorder.start(\"session\", Output.AAC);\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(fragment.binding.volumeBar.getLatestAmplitude(), equalTo(0));\n\n            audioRecorder.setAmplitude(156);\n            assertThat(fragment.binding.volumeBar.getLatestAmplitude(), equalTo(156));\n        });\n    }\n\n    @Test\n    public void clickingPause_pausesRecording() {\n        audioRecorder.start(\"session\", Output.AAC);\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            fragment.binding.pauseRecording.performClick();\n            assertThat(audioRecorder.getCurrentSession().getValue().getPaused(), is(true));\n        });\n    }\n\n    @Test\n    public void whenRecordingPaused_clickingPause_resumesRecording() {\n        audioRecorder.start(\"session\", Output.AAC);\n        audioRecorder.pause();\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            fragment.binding.pauseRecording.performClick();\n            assertThat(audioRecorder.getCurrentSession().getValue().getPaused(), is(false));\n        });\n    }\n\n    @Test\n    public void whenRecordingPaused_pauseIconChangesToResume() {\n        audioRecorder.start(\"session\", Output.AAC);\n        audioRecorder.pause();\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(shadowOf(fragment.binding.pauseRecording.getIcon()).getCreatedFromResId(), is(org.odk.collect.icons.R.drawable.ic_baseline_mic_24));\n            assertThat(fragment.binding.pauseRecording.getContentDescription(), is(fragment.getString(org.odk.collect.strings.R.string.resume_recording)));\n        });\n    }\n\n    @Test\n    public void whenRecordingPaused_recordingStatusChangesToPaused() {\n        audioRecorder.start(\"session\", Output.AAC);\n        audioRecorder.pause();\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(shadowOf(fragment.binding.recordingIcon.getDrawable()).getCreatedFromResId(), is(R.drawable.ic_pause_24dp));\n        });\n    }\n\n    @Test\n    public void whenRecordingResumed_pauseIconChangesToPause() {\n        audioRecorder.start(\"session\", Output.AAC);\n        audioRecorder.pause();\n        audioRecorder.resume();\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(shadowOf(fragment.binding.pauseRecording.getIcon()).getCreatedFromResId(), is(R.drawable.ic_pause_24dp));\n            assertThat(fragment.binding.pauseRecording.getContentDescription(), is(fragment.getString(org.odk.collect.strings.R.string.pause_recording)));\n        });\n    }\n\n    @Test\n    public void whenRecordingResumed_recordingStatusChangesToRecording() {\n        audioRecorder.start(\"session\", Output.AAC);\n        audioRecorder.pause();\n        audioRecorder.resume();\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(shadowOf(fragment.binding.recordingIcon.getDrawable()).getCreatedFromResId(), is(org.odk.collect.icons.R.drawable.ic_baseline_mic_24));\n        });\n    }\n\n    @Test\n    public void whenFormHasBackgroundRecording_hidesControls() {\n        hasBackgroundRecording.setValue(true);\n        audioRecorder.start(\"session\", Output.AAC);\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(fragment.binding.controls.getVisibility(), is(View.GONE));\n        });\n    }\n\n    @Test\n    public void whenFormHasBackgroundRecording_clickingHelpButton_opensHelpDialog() {\n        hasBackgroundRecording.setValue(true);\n        audioRecorder.start(\"session\", Output.AAC);\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(fragment.binding.help.getVisibility(), is(View.VISIBLE));\n\n            fragment.binding.help.performClick();\n            BackgroundAudioHelpDialogFragment dialog = getFragmentByClass(fragment.getParentFragmentManager(), BackgroundAudioHelpDialogFragment.class);\n            assertThat(dialog, notNullValue());\n        });\n    }\n\n    @Test\n    public void whenFormDoesNotHaveBackgroundRecording_hidesHelpButton() {\n        hasBackgroundRecording.setValue(false);\n        audioRecorder.start(\"session\", Output.AAC);\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(fragment.binding.help.getVisibility(), is(View.GONE));\n        });\n    }\n\n    @Test\n    public void whenThereIsAnErrorStartingRecording_showsErrorDialog() {\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n\n        audioRecorder.failOnStart();\n        audioRecorder.start(\"blah\", Output.AAC);\n        scenario.onFragment(fragment -> {\n            AudioRecordingErrorDialogFragment dialog = getFragmentByClass(fragment.getParentFragmentManager(), AudioRecordingErrorDialogFragment.class);\n            assertThat(dialog, notNullValue());\n        });\n    }\n\n    @Test\n    public void whenFormHasBackgroundRecording_andBackgroundRecordingIsDisabled_showsThatRecordingIsDisabled() {\n        hasBackgroundRecording.setValue(true);\n        isBackgroundRecordingEnabled.setValue(false);\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(fragment.binding.getRoot().getVisibility(), is(View.VISIBLE));\n            assertThat(fragment.binding.timeCode.getText(), is(fragment.getString(org.odk.collect.strings.R.string.recording_disabled, \"⋮\")));\n            assertThat(fragment.binding.volumeBar.getVisibility(), is(View.GONE));\n            assertThat(fragment.binding.controls.getVisibility(), is(View.GONE));\n            assertThat(fragment.binding.help.getVisibility(), is(View.GONE));\n        });\n    }\n\n    @Test\n    public void whenFormDoesNotHaveBackgroundRecording_andBackgroundRecordingIsDisabled_doesNotShowRecordingIsDisabled() {\n        hasBackgroundRecording.setValue(false);\n        isBackgroundRecordingEnabled.setValue(false);\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n        scenario.onFragment(fragment -> {\n            assertThat(fragment.binding.getRoot().getVisibility(), is(View.GONE));\n        });\n    }\n\n    @Test\n    public void whenFormHasBackgroundRecording_andThereIsAnError_andSessionIsOver_showsThatThereIsAnError() {\n        hasBackgroundRecording.setValue(true);\n        isBackgroundRecordingEnabled.setValue(true);\n        audioRecorder.failOnStart();\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n\n        audioRecorder.start(\"blah\", Output.AAC_LOW);\n        audioRecorder.cleanUp();\n\n        scenario.onFragment(fragment -> {\n            assertThat(fragment.binding.getRoot().getVisibility(), is(View.VISIBLE));\n            assertThat(fragment.binding.timeCode.getText(), is(fragment.getString(org.odk.collect.strings.R.string.start_recording_failed)));\n            assertThat(fragment.binding.volumeBar.getVisibility(), is(View.GONE));\n            assertThat(fragment.binding.controls.getVisibility(), is(View.GONE));\n            assertThat(fragment.binding.help.getVisibility(), is(View.GONE));\n        });\n    }\n\n    @Test\n    public void whenFormDoesNotHaveBackgroundRecording_andThereIsAnError_andSessionIsOver_doesNotThatThereIsAnError() {\n        hasBackgroundRecording.setValue(false);\n        isBackgroundRecordingEnabled.setValue(true);\n        audioRecorder.failOnStart();\n\n        FragmentScenario<AudioRecordingControllerFragment> scenario = launcherRule.launchInContainer(AudioRecordingControllerFragment.class);\n\n        audioRecorder.start(\"blah\", Output.AAC_LOW);\n        audioRecorder.cleanUp();\n\n        scenario.onFragment(fragment -> {\n            assertThat(fragment.binding.getRoot().getVisibility(), is(View.GONE));\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/audio/AudioRecordingFormErrorDialogFragmentTest.java",
    "content": "package org.odk.collect.android.audio;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\n\nimport android.app.Application;\nimport android.content.DialogInterface;\nimport android.widget.Button;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.fragment.app.testing.FragmentScenario;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.testsupport.StubAudioRecorder;\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport java.io.File;\n\n@RunWith(AndroidJUnit4.class)\npublic class AudioRecordingFormErrorDialogFragmentTest {\n\n    private StubAudioRecorder audioRecorder;\n\n    @Rule\n    public FragmentScenarioLauncherRule launcherRule = new FragmentScenarioLauncherRule();\n\n    @Before\n    public void setup() throws Exception {\n        File stubRecording = File.createTempFile(\"test\", \".m4a\");\n        stubRecording.deleteOnExit();\n        audioRecorder = new StubAudioRecorder(stubRecording.getAbsolutePath());\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public AudioRecorder providesAudioRecorder(Application application) {\n                return audioRecorder;\n            }\n        });\n    }\n\n    @Test\n    public void clickingOK_dismissesDialog() {\n        FragmentScenario<AudioRecordingErrorDialogFragment> scenario = launcherRule.launch(AudioRecordingErrorDialogFragment.class);\n        scenario.onFragment(f -> {\n            AlertDialog dialog = (AlertDialog) f.getDialog();\n\n            Button button = dialog.getButton(DialogInterface.BUTTON_POSITIVE);\n            assertThat(button.getText(), is(f.getString(org.odk.collect.strings.R.string.ok)));\n\n            button.performClick();\n            RobolectricHelpers.runLooper();\n            assertThat(dialog.isShowing(), is(false));\n        });\n    }\n\n    @Test\n    public void onDismiss_consumesConsumable() {\n        FragmentScenario<AudioRecordingErrorDialogFragment> scenario = launcherRule.launch(AudioRecordingErrorDialogFragment.class);\n        scenario.onFragment(DialogFragment::dismiss);\n        assertThat(audioRecorder.failedToStart().getValue().isConsumed(), is(true));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/audio/BackgroundAudioHelpDialogFragmentTest.java",
    "content": "package org.odk.collect.android.audio;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\n\nimport android.content.DialogInterface;\nimport android.widget.Button;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.testing.FragmentScenario;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule;\n\n@RunWith(AndroidJUnit4.class)\npublic class BackgroundAudioHelpDialogFragmentTest {\n\n    @Rule\n    public FragmentScenarioLauncherRule launcherRule = new FragmentScenarioLauncherRule();\n\n    @Test\n    public void hasOkButton() {\n        FragmentScenario<BackgroundAudioHelpDialogFragment> scenario = launcherRule.launch(BackgroundAudioHelpDialogFragment.class);\n        scenario.onFragment(f -> {\n            AlertDialog dialog = (AlertDialog) f.getDialog();\n            Button okButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);\n            assertThat(okButton.isShown(), is(true));\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/backgroundwork/AutoUpdateTaskSpecTest.kt",
    "content": "package org.odk.collect.android.backgroundwork\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.injection.config.ProjectDependencyModuleFactory\nimport org.odk.collect.android.notifications.Notifier\nimport org.odk.collect.android.support.CollectHelpers\n\n@RunWith(AndroidJUnit4::class)\nclass AutoUpdateTaskSpecTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n    private val formUpdateChecker = mock<FormsDataService>()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesFormsUpdater(\n                application: Application,\n                notifier: Notifier,\n                projectDependencyModuleFactory: ProjectDependencyModuleFactory\n            ): FormsDataService {\n                return formUpdateChecker\n            }\n        })\n    }\n\n    @Test\n    fun `calls checkForUpdates with project from tag`() {\n        val autoUpdateTaskSpec = AutoUpdateTaskSpec()\n        val task = autoUpdateTaskSpec.getTask(context, mapOf(TaskData.DATA_PROJECT_ID to \"projectId\"), true) { false }\n\n        task.get()\n        verify(formUpdateChecker).downloadUpdates(\"projectId\")\n    }\n\n    @Test\n    fun `maxRetries should not be limited`() {\n        assertThat(AutoUpdateTaskSpec().maxRetries, `is`(nullValue()))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/backgroundwork/FormUpdateAndInstanceSubmitSchedulerTest.kt",
    "content": "package org.odk.collect.android.backgroundwork\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.eq\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoInteractions\nimport org.odk.collect.android.TestSettingsProvider\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.settings.enums.FormUpdateMode.MATCH_EXACTLY\nimport org.odk.collect.settings.enums.FormUpdateMode.PREVIOUSLY_DOWNLOADED_ONLY\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProjectKeys.KEY_FORM_UPDATE_MODE\nimport org.odk.collect.settings.keys.ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK\n\n@RunWith(AndroidJUnit4::class)\nclass FormUpdateAndInstanceSubmitSchedulerTest {\n\n    private val application by lazy { ApplicationProvider.getApplicationContext<Application>() }\n    private val settingsProvider by lazy { TestSettingsProvider.getSettingsProvider() }\n    private val scheduler = mock<Scheduler>()\n\n    @Test\n    fun `scheduleUpdates passes project id when scheduling previously downloaded only`() {\n        val generalSettings = settingsProvider.getUnprotectedSettings(\"myProject\")\n\n        generalSettings.save(KEY_FORM_UPDATE_MODE, PREVIOUSLY_DOWNLOADED_ONLY.getValue(application))\n        generalSettings.save(\n            KEY_PERIODIC_FORM_UPDATES_CHECK,\n            application.getString(org.odk.collect.strings.R.string.every_one_hour_value)\n        )\n\n        val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application)\n\n        manager.scheduleUpdates(\"myProject\")\n        verify(scheduler).networkDeferredRepeat(\n            eq(\"serverPollingJob:myProject\"),\n            any<AutoUpdateTaskSpec>(),\n            eq(3600000),\n            eq(mapOf(TaskData.DATA_PROJECT_ID to \"myProject\"))\n        )\n    }\n\n    @Test\n    fun `cancelUpdates cancels auto update for project`() {\n        val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application)\n\n        manager.cancelUpdates(\"myProject\")\n        verify(scheduler).cancelDeferred(\"serverPollingJob:myProject\")\n    }\n\n    @Test\n    fun `cancelUpdates cancels match exactly update for project`() {\n        val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application)\n\n        manager.cancelUpdates(\"myProject\")\n        verify(scheduler).cancelDeferred(\"match_exactly:myProject\")\n    }\n\n    @Test\n    fun `scheduleUpdates passes project id when scheduling match exactly`() {\n        val generalSettings = settingsProvider.getUnprotectedSettings(\"myProject\")\n\n        generalSettings.save(KEY_FORM_UPDATE_MODE, MATCH_EXACTLY.getValue(application))\n        generalSettings.save(\n            KEY_PERIODIC_FORM_UPDATES_CHECK,\n            application.getString(org.odk.collect.strings.R.string.every_one_hour_value)\n        )\n\n        val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application)\n\n        manager.scheduleUpdates(\"myProject\")\n        verify(scheduler).networkDeferredRepeat(\n            eq(\"match_exactly:myProject\"),\n            any<SyncFormsTaskSpec>(),\n            eq(3600000),\n            eq(mapOf(TaskData.DATA_PROJECT_ID to \"myProject\"))\n        )\n    }\n\n    @Test\n    fun `scheduleAutoSend passes current project ID`() {\n        settingsProvider.getUnprotectedSettings(\"myProject\")\n            .save(ProjectKeys.KEY_AUTOSEND, \"wifi_and_cellular\")\n        val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application)\n\n        manager.scheduleAutoSend(\"myProject\")\n        verify(scheduler).networkDeferred(\n            eq(\"AutoSendWorker:myProject\"),\n            any<SendFormsTaskSpec>(),\n            eq(mapOf(TaskData.DATA_PROJECT_ID to \"myProject\")),\n            eq(null)\n        )\n    }\n\n    @Test\n    fun `scheduleAutoSend uses wifi network type when set in settings`() {\n        settingsProvider.getUnprotectedSettings(\"myProject\")\n            .save(ProjectKeys.KEY_AUTOSEND, \"wifi_only\")\n        val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application)\n\n        manager.scheduleAutoSend(\"myProject\")\n        verify(scheduler).networkDeferred(\n            eq(\"AutoSendWorker:myProject\"),\n            any<SendFormsTaskSpec>(),\n            eq(mapOf(TaskData.DATA_PROJECT_ID to \"myProject\")),\n            eq(Scheduler.NetworkType.WIFI)\n        )\n    }\n\n    @Test\n    fun `scheduleAutoSend uses cellular network type when set in settings`() {\n        settingsProvider.getUnprotectedSettings(\"myProject\")\n            .save(ProjectKeys.KEY_AUTOSEND, \"cellular_only\")\n        val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application)\n\n        manager.scheduleAutoSend(\"myProject\")\n        verify(scheduler).networkDeferred(\n            eq(\"AutoSendWorker:myProject\"),\n            any<SendFormsTaskSpec>(),\n            eq(mapOf(TaskData.DATA_PROJECT_ID to \"myProject\")),\n            eq(Scheduler.NetworkType.CELLULAR)\n        )\n    }\n\n    @Test\n    fun `scheduleAutoSend does nothing if auto send is disabled`() {\n        settingsProvider.getUnprotectedSettings(\"myProject\")\n            .save(ProjectKeys.KEY_AUTOSEND, \"off\")\n        val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application)\n\n        manager.scheduleAutoSend(\"myProject\")\n        verifyNoInteractions(scheduler)\n    }\n\n    @Test\n    fun `cancelSubmit cancels auto send for current project`() {\n        val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application)\n\n        manager.cancelSubmit(\"myProject\")\n        verify(scheduler).cancelDeferred(\"AutoSendWorker:myProject\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpecTest.kt",
    "content": "package org.odk.collect.android.backgroundwork\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.injection.config.ProjectDependencyModuleFactory\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.notifications.Notifier\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.metadata.PropertyManager\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface\nimport org.odk.collect.testshared.RobolectricHelpers\n\n@RunWith(AndroidJUnit4::class)\nclass SendFormsTaskSpecTest {\n\n    private val instancesDataService = mock<InstancesDataService>()\n    private lateinit var projectId: String\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesInstancesDataService(\n                application: Application?,\n                projectsDataService: ProjectsDataService?,\n                instanceSubmitScheduler: InstanceSubmitScheduler?,\n                projectsDependencyProviderFactory: ProjectDependencyModuleFactory?,\n                notifier: Notifier?,\n                propertyManager: PropertyManager?,\n                httpInterface: OpenRosaHttpInterface\n            ): InstancesDataService {\n                return instancesDataService\n            }\n        })\n\n        RobolectricHelpers.mountExternalStorage()\n        projectId = CollectHelpers.setupDemoProject()\n    }\n\n    @Test\n    fun `returns false if sending instances fails`() {\n        whenever(instancesDataService.sendInstances(projectId)).doReturn(false)\n\n        val inputData = mapOf(TaskData.DATA_PROJECT_ID to projectId)\n        val spec = SendFormsTaskSpec()\n        val task = spec.getTask(ApplicationProvider.getApplicationContext(), inputData, true) { false }\n        assertThat(task.get(), equalTo(false))\n    }\n\n    @Test\n    fun `returns true if sending instances succeeds`() {\n        whenever(instancesDataService.sendInstances(projectId)).doReturn(true)\n\n        val inputData = mapOf(TaskData.DATA_PROJECT_ID to projectId)\n        val spec = SendFormsTaskSpec()\n        val task = spec.getTask(ApplicationProvider.getApplicationContext(), inputData, true) { false }\n        assertThat(task.get(), equalTo(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/backgroundwork/SyncFormsTaskSpecTest.kt",
    "content": "package org.odk.collect.android.backgroundwork\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.eq\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.injection.config.ProjectDependencyModuleFactory\nimport org.odk.collect.android.notifications.Notifier\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\n\n@RunWith(AndroidJUnit4::class)\nclass SyncFormsTaskSpecTest {\n    private val formsDataService = mock<FormsDataService>()\n    private val notifier = mock<Notifier>()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesFormsUpdater(\n                application: Application,\n                notifier: Notifier,\n                projectDependencyModuleFactory: ProjectDependencyModuleFactory\n            ): FormsDataService {\n                return formsDataService\n            }\n\n            override fun providesNotifier(\n                application: Application,\n                settingsProvider: SettingsProvider,\n                projectsRepository: ProjectsRepository,\n                uniqueIdGenerator: UniqueIdGenerator\n            ): Notifier {\n                return notifier\n            }\n        })\n    }\n\n    @Test\n    fun `#getTask calls synchronize with notify true when isLastUniqueExecution equals true`() {\n        val inputData = HashMap<String, String>().also {\n            it[TaskData.DATA_PROJECT_ID] = \"projectId\"\n        }\n        SyncFormsTaskSpec().getTask(ApplicationProvider.getApplicationContext(), inputData, true) { false }.get()\n        verify(formsDataService).matchFormsWithServer(eq(\"projectId\"), eq(true), any())\n    }\n\n    @Test\n    fun `#getTask calls synchronize with notify false when isLastUniqueExecution equals false`() {\n        val inputData = HashMap<String, String>().also {\n            it[TaskData.DATA_PROJECT_ID] = \"projectId\"\n        }\n        SyncFormsTaskSpec().getTask(ApplicationProvider.getApplicationContext(), inputData, false) { false }.get()\n        verify(formsDataService).matchFormsWithServer(eq(\"projectId\"), eq(false), any())\n    }\n\n    @Test\n    fun `#getTask calls synchronize and forwards isStopped to it`() {\n        val inputData = HashMap<String, String>().also {\n            it[TaskData.DATA_PROJECT_ID] = \"projectId\"\n        }\n        val isStopped = { false }\n        SyncFormsTaskSpec().getTask(ApplicationProvider.getApplicationContext(), inputData, false, isStopped).get()\n        verify(formsDataService).matchFormsWithServer(\"projectId\", false, isStopped)\n    }\n\n    @Test\n    fun `#getTask returns result from FormUpdater`() {\n        val inputData = HashMap<String, String>().also {\n            it[TaskData.DATA_PROJECT_ID] = \"projectId\"\n        }\n        whenever(formsDataService.matchFormsWithServer(eq(\"projectId\"), any(), any())).thenReturn(true)\n        var result = SyncFormsTaskSpec().getTask(ApplicationProvider.getApplicationContext(), inputData, true) { false }.get()\n        assertThat(result, `is`(true))\n\n        whenever(formsDataService.matchFormsWithServer(eq(\"projectId\"), any(), any())).thenReturn(false)\n        result = SyncFormsTaskSpec().getTask(ApplicationProvider.getApplicationContext(), inputData, false) { false }.get()\n        assertThat(result, `is`(false))\n    }\n\n    @Test\n    fun `maxRetries should be limited`() {\n        assertThat(SyncFormsTaskSpec().maxRetries, `is`(3))\n    }\n\n    @Test(expected = IllegalArgumentException::class)\n    fun `#getTask throws IllegalArgumentException when projectId is not set`() {\n        SyncFormsTaskSpec().getTask(ApplicationProvider.getApplicationContext(), HashMap(), false) { false }.get()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/configure/qr/QRCodeActivityResultDelegateTest.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.apache.commons.io.IOUtils\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.projects.Project.Saved\nimport org.odk.collect.projects.ProjectConfigurationResult\nimport org.odk.collect.qrcode.zxing.QRCodeDecoder\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.robolectric.Robolectric\nimport org.robolectric.Shadows\nimport org.robolectric.shadows.ShadowToast\nimport java.io.ByteArrayInputStream\nimport java.io.IOException\nimport java.io.InputStream\nimport java.lang.Exception\nimport java.lang.RuntimeException\nimport java.util.HashMap\n\n@RunWith(AndroidJUnit4::class)\nclass QRCodeActivityResultDelegateTest {\n    private val fakeQRDecoder = FakeQRDecoder()\n    private val settingsImporter = mock<ODKAppSettingsImporter>()\n    private val project = mock<Saved>()\n\n    private lateinit var context: Activity\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesQRCodeDecoder(): QRCodeDecoder {\n                return fakeQRDecoder\n            }\n        })\n        context = Robolectric.buildActivity(Activity::class.java).get()\n    }\n\n    @Test\n    fun forSelectPhoto_importsSettingsFromQRCode_showsSuccessToast() {\n        importSettingsFromQRCode_successfully()\n        assertThat(\n            ShadowToast.getTextOfLatestToast(),\n            equalTo(\n                context.getString(org.odk.collect.strings.R.string.successfully_imported_settings)\n            )\n        )\n    }\n\n    private fun importSettingsFromQRCode_successfully() {\n        val delegate = QRCodeActivityResultDelegate(context, settingsImporter, fakeQRDecoder, project)\n        val data = intentWithData(\"file://qr\", \"qr\")\n        fakeQRDecoder.register(\"qr\", \"data\")\n        whenever(settingsImporter.fromJSON(\"data\", project)).thenReturn(ProjectConfigurationResult.SUCCESS)\n        delegate.onActivityResult(QRCodeMenuProvider.SELECT_PHOTO, Activity.RESULT_OK, data)\n    }\n\n    @Test\n    fun forSelectPhoto_whenImportingFails_showsInvalidToast() {\n        importSettingsFromQRCode_withFailedImport()\n        assertThat(ShadowToast.getTextOfLatestToast(), equalTo(context.getString(org.odk.collect.strings.R.string.invalid_qrcode)))\n    }\n\n    private fun importSettingsFromQRCode_withFailedImport() {\n        val delegate = QRCodeActivityResultDelegate(context, settingsImporter, fakeQRDecoder, project)\n        val data = intentWithData(\"file://qr\", \"qr\")\n        fakeQRDecoder.register(\"qr\", \"data\")\n        whenever(settingsImporter.fromJSON(\"data\", project)).thenReturn(ProjectConfigurationResult.INVALID_SETTINGS)\n        delegate.onActivityResult(QRCodeMenuProvider.SELECT_PHOTO, Activity.RESULT_OK, data)\n    }\n\n    @Test\n    fun forSelectPhotoWithGoogleDriveProtocol_whenImporting_showsInvalidToast() {\n        val delegate = QRCodeActivityResultDelegate(context, settingsImporter, fakeQRDecoder, project)\n        val data = intentWithData(\"file://qr\", \"qr\")\n        fakeQRDecoder.register(\"qr\", \"data\")\n        whenever(settingsImporter.fromJSON(\"data\", project)).thenReturn(ProjectConfigurationResult.GD_PROJECT)\n        delegate.onActivityResult(QRCodeMenuProvider.SELECT_PHOTO, Activity.RESULT_OK, data)\n\n        assertThat(ShadowToast.getTextOfLatestToast(), equalTo(context.getString(org.odk.collect.strings.R.string.settings_with_gd_protocol)))\n    }\n\n    @Test\n    fun forSelectPhoto_whenQRCodeDecodeFailsWithInvalid_showsInvalidToast() {\n        importSettingsFromQrCode_withInvalidQrCode()\n        assertThat(ShadowToast.getTextOfLatestToast(), equalTo(context.getString(org.odk.collect.strings.R.string.invalid_qrcode)))\n    }\n\n    private fun importSettingsFromQrCode_withInvalidQrCode() {\n        val delegate = QRCodeActivityResultDelegate(context, settingsImporter, fakeQRDecoder, project)\n        val data = intentWithData(\"file://qr\", \"qr\")\n        fakeQRDecoder.failsWith(QRCodeDecoder.QRCodeInvalidException())\n        whenever(settingsImporter.fromJSON(\"data\", project)).thenReturn(ProjectConfigurationResult.INVALID_SETTINGS)\n        delegate.onActivityResult(QRCodeMenuProvider.SELECT_PHOTO, Activity.RESULT_OK, data)\n    }\n\n    @Test\n    fun forSelectPhoto_whenQRCodeDecodeFailsWithNotFound_showsNoQRToast() {\n        importSettingsFromImage_withoutQrCode()\n        assertThat(ShadowToast.getTextOfLatestToast(), equalTo(context.getString(org.odk.collect.strings.R.string.qr_code_not_found)))\n    }\n\n    private fun importSettingsFromImage_withoutQrCode() {\n        val delegate = QRCodeActivityResultDelegate(context, settingsImporter, fakeQRDecoder, project)\n        val data = intentWithData(\"file://qr\", \"qr\")\n        fakeQRDecoder.failsWith(QRCodeDecoder.QRCodeNotFoundException())\n        whenever(settingsImporter.fromJSON(\"data\", project)).thenReturn(ProjectConfigurationResult.INVALID_SETTINGS)\n        delegate.onActivityResult(QRCodeMenuProvider.SELECT_PHOTO, Activity.RESULT_OK, data)\n    }\n\n    @Test\n    fun forSelectPhoto_whenDataIsNull_doesNothing() {\n        val delegate = QRCodeActivityResultDelegate(context, settingsImporter, fakeQRDecoder, project)\n        delegate.onActivityResult(QRCodeMenuProvider.SELECT_PHOTO, Activity.RESULT_OK, null)\n    }\n\n    @Test\n    fun forSelectPhoto_whenResultCancelled_doesNothing() {\n        val delegate = QRCodeActivityResultDelegate(context, settingsImporter, fakeQRDecoder, project)\n        delegate.onActivityResult(\n            QRCodeMenuProvider.SELECT_PHOTO,\n            Activity.RESULT_CANCELED,\n            Intent()\n        )\n    }\n\n    private fun intentWithData(uri: String, streamContents: String): Intent {\n        val inputStream = ByteArrayInputStream(streamContents.toByteArray())\n        Shadows.shadowOf(ApplicationProvider.getApplicationContext<Context>().contentResolver)\n            .registerInputStream(\n                Uri.parse(\"file://qr\"),\n                inputStream\n            )\n        val data = Intent()\n        data.data = Uri.parse(uri)\n        return data\n    }\n\n    private class FakeQRDecoder : QRCodeDecoder {\n        private val data: MutableMap<String, String> = HashMap()\n        private var failsWith: Exception? = null\n\n        @Throws(QRCodeDecoder.QRCodeInvalidException::class, QRCodeDecoder.QRCodeNotFoundException::class)\n        override fun decode(inputStream: InputStream?): String {\n            if (failsWith != null) {\n                if (failsWith is QRCodeDecoder.QRCodeInvalidException) {\n                    throw (failsWith as QRCodeDecoder.QRCodeInvalidException?)!!\n                } else {\n                    throw (failsWith as QRCodeDecoder.QRCodeNotFoundException?)!!\n                }\n            }\n            return try {\n                val streamData = IOUtils.toString(inputStream)\n                val decoded = data[streamData]\n                    ?: throw RuntimeException(\"No decoded data specified for $streamData\")\n                decoded\n            } catch (e: IOException) {\n                throw RuntimeException(e)\n            }\n        }\n\n        fun register(streamData: String, decodedData: String) {\n            data[streamData] = decodedData\n        }\n\n        fun failsWith(exception: Exception?) {\n            failsWith = exception\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/configure/qr/QRCodeMenuProviderTest.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.fragment.app.FragmentActivity\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.notNullValue\nimport org.hamcrest.Matchers.nullValue\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.TestSettingsProvider.getSettingsProvider\nimport org.odk.collect.android.utilities.FileProvider\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidshared.system.IntentLauncherImpl\nimport org.odk.collect.testshared.ErrorIntentLauncher\nimport org.odk.collect.testshared.FakeScheduler\nimport org.robolectric.Robolectric\nimport org.robolectric.Shadows\nimport org.robolectric.fakes.RoboMenuItem\nimport org.robolectric.shadows.ShadowToast\n\n@RunWith(AndroidJUnit4::class)\nclass QRCodeMenuProviderTest {\n    private lateinit var activity: FragmentActivity\n    private lateinit var intentLauncher: IntentLauncher\n    private lateinit var menuProvider: QRCodeMenuProvider\n\n    private val qrCodeGenerator = mock<QRCodeGenerator>()\n    private val appConfigurationGenerator = mock<AppConfigurationGenerator>()\n    private val fileProvider = mock<FileProvider>()\n    private val fakeScheduler = FakeScheduler()\n\n    @Before\n    fun setup() {\n        activity = Robolectric.setupActivity(FragmentActivity::class.java)\n        intentLauncher = IntentLauncherImpl\n        setupMenuProvider()\n    }\n\n    private fun setupMenuProvider() {\n        menuProvider = QRCodeMenuProvider(\n            activity,\n            intentLauncher,\n            qrCodeGenerator,\n            appConfigurationGenerator,\n            fileProvider,\n            getSettingsProvider(),\n            fakeScheduler\n        )\n    }\n\n    @Test\n    fun clickingOnImportQRCode_startsExternalImagePickerIntent() {\n        menuProvider.onMenuItemSelected(RoboMenuItem(R.id.menu_item_scan_sd_card))\n        val intentForResult = Shadows.shadowOf(activity).nextStartedActivityForResult\n\n        assertThat(intentForResult, notNullValue())\n        assertThat(\n            intentForResult.requestCode,\n            equalTo(QRCodeMenuProvider.SELECT_PHOTO)\n        )\n        assertThat(\n            intentForResult.intent.action,\n            equalTo(Intent.ACTION_GET_CONTENT)\n        )\n        assertThat(intentForResult.intent.type, equalTo(\"image/*\"))\n    }\n\n    @Test\n    fun clickingOnImportQRCode_whenPickerActivityNotAvailable_showsToast() {\n        intentLauncher = ErrorIntentLauncher()\n        setupMenuProvider()\n        menuProvider.onMenuItemSelected(RoboMenuItem(R.id.menu_item_scan_sd_card))\n\n        assertThat(\n            Shadows.shadowOf(activity).nextStartedActivityForResult,\n            nullValue()\n        )\n        assertThat(ShadowToast.getLatestToast(), notNullValue())\n    }\n\n    @Test\n    fun clickingOnShare_beforeQRCodeIsGenerated_doesNothing() {\n        whenever(qrCodeGenerator.generateQRCode(any(), any())).thenReturn(\"qr.png\")\n        whenever(fileProvider.getURIForFile(\"qr.png\")).thenReturn(Uri.parse(\"uri\"))\n        menuProvider.onMenuItemSelected(RoboMenuItem(R.id.menu_item_share))\n\n        assertThat(Shadows.shadowOf(activity).nextStartedActivity, nullValue())\n    }\n\n    @Test\n    fun clickingOnShare_whenQRCodeIsGenerated_startsShareIntent() {\n        whenever(qrCodeGenerator.generateQRCode(any(), any())).thenReturn(\"qr.png\")\n        whenever(fileProvider.getURIForFile(\"qr.png\")).thenReturn(Uri.parse(\"uri\"))\n        fakeScheduler.flush()\n        menuProvider.onMenuItemSelected(RoboMenuItem(R.id.menu_item_share))\n        val intent = Shadows.shadowOf(activity).nextStartedActivity\n\n        assertThat(intent, notNullValue())\n        assertThat(intent.action, equalTo(Intent.ACTION_SEND))\n        assertThat(intent.type, equalTo(\"image/*\"))\n        assertThat(intent.extras!!.getParcelable(Intent.EXTRA_STREAM), equalTo(Uri.parse(\"uri\")))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/configure/qr/QRCodeScannerFragmentTest.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.intent.Intents\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.work.WorkManager\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.application.initialization.AnalyticsInitializer\nimport org.odk.collect.android.application.initialization.MapsInitializer\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.testshared.FakeBarcodeScannerViewFactory\nimport org.odk.collect.testshared.FakeScheduler\nimport org.robolectric.shadows.ShadowToast\n\n@RunWith(AndroidJUnit4::class)\nclass QRCodeScannerFragmentTest {\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n    private val barcodeScannerViewFactory = FakeBarcodeScannerViewFactory()\n    private val scheduler = FakeScheduler()\n\n    @Before\n    fun setup() {\n        Intents.init()\n\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesCurrentProjectProvider(\n                application: Application,\n                settingsProvider: SettingsProvider,\n                projectsRepository: ProjectsRepository,\n                analyticsInitializer: AnalyticsInitializer,\n                context: Context,\n                mapsInitializer: MapsInitializer\n            ): ProjectsDataService {\n                return mock<ProjectsDataService> {\n                    on { requireCurrentProject() } doReturn Project.DEMO_PROJECT\n                }\n            }\n\n            override fun providesBarcodeScannerViewFactory(settingsProvider: SettingsProvider): BarcodeScannerViewContainer.Factory {\n                return barcodeScannerViewFactory\n            }\n\n            override fun providesScheduler(workManager: WorkManager, application: Application): Scheduler {\n                return scheduler\n            }\n        })\n    }\n\n    @After\n    fun tearDown() {\n        Intents.release()\n    }\n\n    @Test\n    fun `When valid settings passed stop scanning and navigate to the main menu`() {\n        launcherRule.launch(QRCodeScannerFragment::class.java)\n        barcodeScannerViewFactory.scan(\n            \"\"\"\n                {\n                    \"general\": {},\n                    \"admin\": {}\n                }\n                \"\"\"\n        )\n        assertThat(barcodeScannerViewFactory.isScanning, equalTo(false))\n        assertThat(\n            ShadowToast.getTextOfLatestToast(),\n            equalTo(context.getString(org.odk.collect.strings.R.string.successfully_imported_settings))\n        )\n        assertThat(\n            Intents.getIntents()[0],\n            hasComponent(MainMenuActivity::class.java.name)\n        )\n    }\n\n    @Test\n    fun `When invalid settings passed display an error and keep scanning`() {\n        launcherRule.launch(QRCodeScannerFragment::class.java)\n        barcodeScannerViewFactory.scan(\"{}\")\n        scheduler.runForeground()\n        assertThat(barcodeScannerViewFactory.isScanning, equalTo(true))\n        assertThat(\n            ShadowToast.getTextOfLatestToast(),\n            equalTo(context.getString(org.odk.collect.strings.R.string.invalid_qrcode))\n        )\n        assertThat(Intents.getIntents().isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun `When settings with GD project passed display an error and keep scanning`() {\n        launcherRule.launch(QRCodeScannerFragment::class.java)\n        barcodeScannerViewFactory.scan(\n            \"\"\"\n                {\n                    \"general\": {\n                        \"protocol\": \"google_sheets\"\n                    },\n                    \"admin\": {}\n                }\n                \"\"\"\n        )\n        scheduler.runForeground()\n        assertThat(barcodeScannerViewFactory.isScanning, equalTo(true))\n        assertThat(\n            ShadowToast.getTextOfLatestToast(),\n            equalTo(context.getString(org.odk.collect.strings.R.string.settings_with_gd_protocol))\n        )\n        assertThat(Intents.getIntents().isEmpty(), equalTo(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/configure/qr/QRCodeViewModelTest.kt",
    "content": "package org.odk.collect.android.configure.qr\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.nullValue\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.TestSettingsProvider.getProtectedSettings\nimport org.odk.collect.android.TestSettingsProvider.getUnprotectedSettings\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.testshared.FakeScheduler\n\n@RunWith(AndroidJUnit4::class)\nclass QRCodeViewModelTest {\n    private val qrCodeGenerator = mock<QRCodeGenerator>()\n    private val appConfigurationGenerator = mock<AppConfigurationGenerator>()\n    private val fakeScheduler = FakeScheduler()\n    private val generalSettings = getUnprotectedSettings()\n    private val adminSettings = getProtectedSettings()\n\n    @Test\n    fun setIncludedKeys_generatesQRCodeWithKeys() {\n        val viewModel = createViewModel()\n        viewModel.setIncludedKeys(listOf(\"foo\", \"bar\"))\n        fakeScheduler.runBackground()\n\n        verify(qrCodeGenerator).generateQRCode(listOf(\"foo\", \"bar\"), appConfigurationGenerator)\n    }\n\n    @Test\n    fun warning_whenNeitherServerOrAdminPasswordSet_isNull() {\n        val viewModel = createViewModel()\n        assertThat(viewModel.warning.value, nullValue())\n    }\n\n    @Test\n    fun warning_whenServerAndAdminPasswordSet_isForBoth() {\n        generalSettings.save(ProjectKeys.KEY_PASSWORD, \"blah\")\n        adminSettings.save(ProtectedProjectKeys.KEY_ADMIN_PW, \"blah\")\n        val viewModel = createViewModel()\n\n        assertThat(viewModel.warning.value, equalTo(org.odk.collect.strings.R.string.qrcode_with_both_passwords))\n    }\n\n    private fun createViewModel(): QRCodeViewModel {\n        val viewModel = QRCodeViewModel(\n            qrCodeGenerator,\n            appConfigurationGenerator,\n            generalSettings,\n            adminSettings,\n            fakeScheduler\n        )\n\n        fakeScheduler.flush() // Run initial QR generation\n        return viewModel\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/database/DatabaseFormsRepositoryTest.java",
    "content": "package org.odk.collect.android.database;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.database.forms.DatabaseFormsRepository;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.formstest.FormsRepositoryTest;\nimport org.odk.collect.shared.TempFiles;\n\nimport java.io.File;\nimport java.util.function.Supplier;\n\n@RunWith(AndroidJUnit4.class)\npublic class DatabaseFormsRepositoryTest extends FormsRepositoryTest {\n\n    private final File dbDir = TempFiles.createTempDir();\n    private final File formsDir = TempFiles.createTempDir();\n    private final File cacheDir = TempFiles.createTempDir();\n\n    @Override\n    public FormsRepository buildSubject() {\n        return new DatabaseFormsRepository(ApplicationProvider.getApplicationContext(), dbDir.getAbsolutePath(), formsDir.getAbsolutePath(), cacheDir.getAbsolutePath(), System::currentTimeMillis, savepointsRepository);\n    }\n\n    @Override\n    public FormsRepository buildSubject(Supplier<Long> clock) {\n        return new DatabaseFormsRepository(ApplicationProvider.getApplicationContext(), dbDir.getAbsolutePath(), formsDir.getAbsolutePath(), cacheDir.getAbsolutePath(), clock, savepointsRepository);\n    }\n\n    @Override\n    public String getFormFilesPath() {\n        return formsDir.getAbsolutePath();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/database/DatabaseInstancesRepositoryTest.kt",
    "content": "package org.odk.collect.android.database\n\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.database.instances.DatabaseInstancesRepository\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.formstest.InstancesRepositoryTest\nimport org.odk.collect.shared.TempFiles.createTempDir\nimport java.io.File\nimport java.util.function.Supplier\n\n@RunWith(AndroidJUnit4::class)\nclass DatabaseInstancesRepositoryTest : InstancesRepositoryTest() {\n    private val dbDir = createTempDir()\n\n    override val instancesDir: File = createTempDir()\n\n    override fun buildSubject(): InstancesRepository {\n        return DatabaseInstancesRepository(\n            ApplicationProvider.getApplicationContext(),\n            dbDir.absolutePath,\n            instancesDir.absolutePath\n        ) { System.currentTimeMillis() }\n    }\n\n    override fun buildSubject(clock: Supplier<Long>): InstancesRepository {\n        return DatabaseInstancesRepository(\n            ApplicationProvider.getApplicationContext(),\n            dbDir.absolutePath,\n            instancesDir.absolutePath,\n            clock\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/database/DatabaseSavepointsRepositoryTest.kt",
    "content": "package org.odk.collect.android.database\n\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.database.savepoints.DatabaseSavepointsRepository\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport org.odk.collect.formstest.SavepointsRepositoryTest\nimport org.odk.collect.shared.TempFiles\n\n@RunWith(AndroidJUnit4::class)\nclass DatabaseSavepointsRepositoryTest : SavepointsRepositoryTest() {\n    override fun buildSubject(cacheDirPath: String, instancesDirPath: String): SavepointsRepository {\n        return DatabaseSavepointsRepository(\n            ApplicationProvider.getApplicationContext(),\n            TempFiles.createTempDir().absolutePath,\n            cacheDirPath,\n            instancesDirPath\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/database/FormDatabaseMigratorTest.java",
    "content": "package org.odk.collect.android.database;\n\nimport android.content.ContentValues;\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.database.forms.FormDatabaseMigrator;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.assertTrue;\nimport static org.odk.collect.android.database.DatabaseConstants.FORMS_TABLE_NAME;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_DELETE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.AUTO_SEND;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.BASE64_RSA_PUBLIC_KEY;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DELETED_DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DESCRIPTION;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DISPLAY_NAME;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.USES_ENTITIES;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_FILE_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_MEDIA_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.GEOMETRY_XPATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JRCACHE_FILE_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_FORM_ID;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_VERSION;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.LANGUAGE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.LAST_DETECTED_ATTACHMENTS_UPDATE_DATE;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.LAST_DETECTED_FORM_VERSION_HASH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.MD5_HASH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.SUBMISSION_URI;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns._ID;\n\n@RunWith(AndroidJUnit4.class)\npublic class FormDatabaseMigratorTest {\n\n    private SQLiteDatabase database;\n\n    @Before\n    public void setup() {\n        assertThat(\"Test expects different Forms DB version\", DatabaseConstants.FORMS_DATABASE_VERSION, is(14));\n        database = SQLiteDatabase.create(null);\n    }\n\n    @After\n    public void teardown() {\n        database.close();\n    }\n\n    @Test\n    public void databaseIdsShouldNotBeReused() {\n        FormDatabaseMigrator formDatabaseMigrator = new FormDatabaseMigrator();\n        formDatabaseMigrator.onCreate(database);\n\n        ContentValues contentValues = getContentValuesForFormV13();\n        database.insert(FORMS_TABLE_NAME, null, contentValues);\n        try (Cursor cursor = database.rawQuery(\"SELECT * FROM \" + FORMS_TABLE_NAME + \";\", new String[]{})) {\n            assertThat(cursor.getCount(), is(1));\n            cursor.moveToFirst();\n            assertThat(cursor.getInt(cursor.getColumnIndex(_ID)), is(1));\n        }\n\n        database.delete(FORMS_TABLE_NAME, null, null);\n        database.insert(FORMS_TABLE_NAME, null, contentValues);\n        try (Cursor cursor = database.rawQuery(\"SELECT * FROM \" + FORMS_TABLE_NAME + \";\", new String[]{})) {\n            assertThat(cursor.getCount(), is(1));\n            cursor.moveToFirst();\n            assertThat(cursor.getInt(cursor.getColumnIndex(_ID)), is(2));\n        }\n    }\n\n    @Test\n    public void onUpgrade_fromVersion13() {\n        int oldVersion = 13;\n        database.setVersion(oldVersion);\n        FormDatabaseMigrator formDatabaseMigrator = new FormDatabaseMigrator();\n\n        formDatabaseMigrator.createFormsTableV13(database);\n        ContentValues contentValues = getContentValuesForFormV13();\n        database.insert(FORMS_TABLE_NAME, null, contentValues);\n\n        formDatabaseMigrator.onUpgrade(database, oldVersion);\n\n        try (Cursor cursor = database.rawQuery(\"SELECT * FROM \" + FORMS_TABLE_NAME + \";\", new String[]{})) {\n            assertThat(cursor.getColumnCount(), is(19));\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToFirst();\n\n            assertThat(cursor.getInt(cursor.getColumnIndex(_ID)), is(1));\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(contentValues.getAsString(DISPLAY_NAME)));\n            assertThat(cursor.getString(cursor.getColumnIndex(DESCRIPTION)), is(contentValues.getAsString(DESCRIPTION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(contentValues.getAsString(JR_FORM_ID)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), is(contentValues.getAsString(JR_VERSION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(MD5_HASH)), is(contentValues.getAsString(MD5_HASH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DATE)), is(contentValues.getAsInteger(DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_MEDIA_PATH)), is(contentValues.getAsString(FORM_MEDIA_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_FILE_PATH)), is(contentValues.getAsString(FORM_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), is(contentValues.getAsString(LANGUAGE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(SUBMISSION_URI)), is(contentValues.getAsString(SUBMISSION_URI)));\n            assertThat(cursor.getString(cursor.getColumnIndex(BASE64_RSA_PUBLIC_KEY)), is(contentValues.getAsString(BASE64_RSA_PUBLIC_KEY)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JRCACHE_FILE_PATH)), is(contentValues.getAsString(JRCACHE_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_SEND)), is(contentValues.getAsString(AUTO_SEND)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_DELETE)), is(contentValues.getAsString(AUTO_DELETE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY_XPATH)), is(contentValues.getAsString(GEOMETRY_XPATH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DELETED_DATE)), is(contentValues.getAsInteger(DELETED_DATE)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(LAST_DETECTED_ATTACHMENTS_UPDATE_DATE)), is(contentValues.getAsInteger(LAST_DETECTED_ATTACHMENTS_UPDATE_DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(USES_ENTITIES)), is(nullValue()));\n        }\n    }\n\n    @Test\n    public void onUpgrade_fromVersion12() {\n        int oldVersion = 12;\n        database.setVersion(oldVersion);\n        FormDatabaseMigrator formDatabaseMigrator = new FormDatabaseMigrator();\n\n        formDatabaseMigrator.createFormsTableV12(database);\n        ContentValues contentValues = getContentValuesForFormV12();\n        database.insert(FORMS_TABLE_NAME, null, contentValues);\n\n        formDatabaseMigrator.onUpgrade(database, oldVersion);\n\n        try (Cursor cursor = database.rawQuery(\"SELECT * FROM \" + FORMS_TABLE_NAME + \";\", new String[]{})) {\n            assertThat(cursor.getColumnCount(), is(19));\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToFirst();\n\n            assertThat(cursor.getInt(cursor.getColumnIndex(_ID)), is(1));\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(contentValues.getAsString(DISPLAY_NAME)));\n            assertThat(cursor.getString(cursor.getColumnIndex(DESCRIPTION)), is(contentValues.getAsString(DESCRIPTION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(contentValues.getAsString(JR_FORM_ID)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), is(contentValues.getAsString(JR_VERSION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(MD5_HASH)), is(contentValues.getAsString(MD5_HASH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DATE)), is(contentValues.getAsInteger(DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_MEDIA_PATH)), is(contentValues.getAsString(FORM_MEDIA_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_FILE_PATH)), is(contentValues.getAsString(FORM_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), is(contentValues.getAsString(LANGUAGE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(SUBMISSION_URI)), is(contentValues.getAsString(SUBMISSION_URI)));\n            assertThat(cursor.getString(cursor.getColumnIndex(BASE64_RSA_PUBLIC_KEY)), is(contentValues.getAsString(BASE64_RSA_PUBLIC_KEY)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JRCACHE_FILE_PATH)), is(contentValues.getAsString(JRCACHE_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_SEND)), is(contentValues.getAsString(AUTO_SEND)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_DELETE)), is(contentValues.getAsString(AUTO_DELETE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY_XPATH)), is(contentValues.getAsString(GEOMETRY_XPATH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DELETED_DATE)), is(contentValues.getAsInteger(DELETED_DATE)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(LAST_DETECTED_ATTACHMENTS_UPDATE_DATE)), is(contentValues.getAsInteger(LAST_DETECTED_ATTACHMENTS_UPDATE_DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(USES_ENTITIES)), is(nullValue()));\n        }\n    }\n\n    @Test\n    public void onUpgrade_fromVersion11() {\n        int oldVersion = 11;\n        assertTrue(oldVersion < DatabaseConstants.FORMS_DATABASE_VERSION);\n        database.setVersion(oldVersion);\n\n        FormDatabaseMigrator formDatabaseMigrator = new FormDatabaseMigrator();\n        formDatabaseMigrator.createFormsTableV11(database);\n\n        ContentValues contentValues = createVersion11Form();\n        database.insert(FORMS_TABLE_NAME, null, contentValues);\n\n        formDatabaseMigrator.onUpgrade(database, oldVersion);\n\n        try (Cursor cursor = database.rawQuery(\"SELECT * FROM \" + FORMS_TABLE_NAME + \";\", new String[]{})) {\n            assertThat(cursor.getColumnCount(), is(19));\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToFirst();\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(contentValues.getAsString(DISPLAY_NAME)));\n            assertThat(cursor.getString(cursor.getColumnIndex(DESCRIPTION)), is(contentValues.getAsString(DESCRIPTION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(contentValues.getAsString(JR_FORM_ID)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), is(contentValues.getAsString(JR_VERSION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(MD5_HASH)), is(contentValues.getAsString(MD5_HASH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DATE)), is(contentValues.getAsInteger(DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_MEDIA_PATH)), is(contentValues.getAsString(FORM_MEDIA_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_FILE_PATH)), is(contentValues.getAsString(FORM_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), is(contentValues.getAsString(LANGUAGE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(SUBMISSION_URI)), is(contentValues.getAsString(SUBMISSION_URI)));\n            assertThat(cursor.getString(cursor.getColumnIndex(BASE64_RSA_PUBLIC_KEY)), is(contentValues.getAsString(BASE64_RSA_PUBLIC_KEY)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JRCACHE_FILE_PATH)), is(contentValues.getAsString(JRCACHE_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_SEND)), is(contentValues.getAsString(AUTO_SEND)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_DELETE)), is(contentValues.getAsString(AUTO_DELETE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY_XPATH)), is(contentValues.getAsString(GEOMETRY_XPATH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DELETED_DATE)), is(contentValues.getAsInteger(DELETED_DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(LAST_DETECTED_ATTACHMENTS_UPDATE_DATE)), is(nullValue()));\n            assertThat(cursor.getString(cursor.getColumnIndex(USES_ENTITIES)), is(nullValue()));\n        }\n    }\n\n    @Test\n    public void onUpgrade_fromVersion10() {\n        int oldVersion = 10;\n        assertTrue(oldVersion < DatabaseConstants.FORMS_DATABASE_VERSION);\n        database.setVersion(oldVersion);\n\n        FormDatabaseMigrator formDatabaseMigrator = new FormDatabaseMigrator();\n        formDatabaseMigrator.createFormsTableV10(database);\n\n        ContentValues contentValues = createVersion10Form();\n        database.insert(FORMS_TABLE_NAME, null, contentValues);\n\n        formDatabaseMigrator.onUpgrade(database, oldVersion);\n\n        try (Cursor cursor = database.rawQuery(\"SELECT * FROM \" + FORMS_TABLE_NAME + \";\", new String[]{})) {\n            assertThat(cursor.getColumnCount(), is(19));\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToFirst();\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(contentValues.getAsString(DISPLAY_NAME)));\n            assertThat(cursor.getString(cursor.getColumnIndex(DESCRIPTION)), is(contentValues.getAsString(DESCRIPTION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(contentValues.getAsString(JR_FORM_ID)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), is(contentValues.getAsString(JR_VERSION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(MD5_HASH)), is(contentValues.getAsString(MD5_HASH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DATE)), is(contentValues.getAsInteger(DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_MEDIA_PATH)), is(contentValues.getAsString(FORM_MEDIA_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_FILE_PATH)), is(contentValues.getAsString(FORM_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), is(contentValues.getAsString(LANGUAGE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(SUBMISSION_URI)), is(contentValues.getAsString(SUBMISSION_URI)));\n            assertThat(cursor.getString(cursor.getColumnIndex(BASE64_RSA_PUBLIC_KEY)), is(contentValues.getAsString(BASE64_RSA_PUBLIC_KEY)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JRCACHE_FILE_PATH)), is(contentValues.getAsString(JRCACHE_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_SEND)), is(contentValues.getAsString(AUTO_SEND)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_DELETE)), is(contentValues.getAsString(AUTO_DELETE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY_XPATH)), is(contentValues.getAsString(GEOMETRY_XPATH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DELETED_DATE)), is(contentValues.getAsInteger(DELETED_DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(LAST_DETECTED_ATTACHMENTS_UPDATE_DATE)), is(nullValue()));\n            assertThat(cursor.getString(cursor.getColumnIndex(USES_ENTITIES)), is(nullValue()));\n        }\n    }\n\n    @Test\n    public void onUpgrade_fromVersion9() {\n        int oldVersion = 9;\n        assertTrue(oldVersion < DatabaseConstants.FORMS_DATABASE_VERSION);\n        database.setVersion(oldVersion);\n\n        FormDatabaseMigrator formDatabaseMigrator = new FormDatabaseMigrator();\n        formDatabaseMigrator.createFormsTableV9(database);\n\n        ContentValues contentValues = createVersion9Form();\n        database.insert(FORMS_TABLE_NAME, null, contentValues);\n\n        formDatabaseMigrator.onUpgrade(database, oldVersion);\n\n        try (Cursor cursor = database.rawQuery(\"SELECT * FROM \" + FORMS_TABLE_NAME + \";\", new String[]{})) {\n            assertThat(cursor.getColumnCount(), is(19));\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToFirst();\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(contentValues.getAsString(DISPLAY_NAME)));\n            assertThat(cursor.getString(cursor.getColumnIndex(DESCRIPTION)), is(contentValues.getAsString(DESCRIPTION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(contentValues.getAsString(JR_FORM_ID)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), is(contentValues.getAsString(JR_VERSION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(MD5_HASH)), is(contentValues.getAsString(MD5_HASH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DATE)), is(contentValues.getAsInteger(DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_MEDIA_PATH)), is(contentValues.getAsString(FORM_MEDIA_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_FILE_PATH)), is(contentValues.getAsString(FORM_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), is(contentValues.getAsString(LANGUAGE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(SUBMISSION_URI)), is(contentValues.getAsString(SUBMISSION_URI)));\n            assertThat(cursor.getString(cursor.getColumnIndex(BASE64_RSA_PUBLIC_KEY)), is(contentValues.getAsString(BASE64_RSA_PUBLIC_KEY)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JRCACHE_FILE_PATH)), is(contentValues.getAsString(JRCACHE_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_SEND)), is(contentValues.getAsString(AUTO_SEND)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_DELETE)), is(contentValues.getAsString(AUTO_DELETE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY_XPATH)), is(contentValues.getAsString(GEOMETRY_XPATH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DELETED_DATE)), is(0));\n            assertThat(cursor.getColumnIndex(\"deleted\"), is(-1));\n            assertThat(cursor.getString(cursor.getColumnIndex(LAST_DETECTED_ATTACHMENTS_UPDATE_DATE)), is(nullValue()));\n            assertThat(cursor.getString(cursor.getColumnIndex(USES_ENTITIES)), is(nullValue()));\n        }\n    }\n\n    @Test\n    public void onUpgrade_fromVersion8() {\n        int oldVersion = 8;\n        assertTrue(oldVersion < DatabaseConstants.FORMS_DATABASE_VERSION);\n        database.setVersion(oldVersion);\n\n        FormDatabaseMigrator formDatabaseMigrator = new FormDatabaseMigrator();\n        formDatabaseMigrator.createFormsTableV8(database);\n\n        ContentValues contentValues = createVersion8Form();\n        database.insert(FORMS_TABLE_NAME, null, contentValues);\n\n        formDatabaseMigrator.onUpgrade(database, oldVersion);\n\n        try (Cursor cursor = database.rawQuery(\"SELECT * FROM \" + FORMS_TABLE_NAME + \";\", new String[]{})) {\n            assertThat(cursor.getColumnCount(), is(19));\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToFirst();\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(contentValues.getAsString(DISPLAY_NAME)));\n            assertThat(cursor.getString(cursor.getColumnIndex(DESCRIPTION)), is(contentValues.getAsString(DESCRIPTION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(contentValues.getAsString(JR_FORM_ID)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), is(contentValues.getAsString(JR_VERSION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(MD5_HASH)), is(contentValues.getAsString(MD5_HASH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DATE)), is(contentValues.getAsInteger(DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_MEDIA_PATH)), is(contentValues.getAsString(FORM_MEDIA_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_FILE_PATH)), is(contentValues.getAsString(FORM_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), is(contentValues.getAsString(LANGUAGE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(SUBMISSION_URI)), is(contentValues.getAsString(SUBMISSION_URI)));\n            assertThat(cursor.getString(cursor.getColumnIndex(BASE64_RSA_PUBLIC_KEY)), is(contentValues.getAsString(BASE64_RSA_PUBLIC_KEY)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JRCACHE_FILE_PATH)), is(contentValues.getAsString(JRCACHE_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_SEND)), is(contentValues.getAsString(AUTO_SEND)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_DELETE)), is(contentValues.getAsString(AUTO_DELETE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY_XPATH)), is(contentValues.getAsString(GEOMETRY_XPATH)));\n            assertThat(cursor.isNull(cursor.getColumnIndex(DELETED_DATE)), is(true));\n            assertThat(cursor.getString(cursor.getColumnIndex(LAST_DETECTED_ATTACHMENTS_UPDATE_DATE)), is(nullValue()));\n            assertThat(cursor.getString(cursor.getColumnIndex(USES_ENTITIES)), is(nullValue()));\n        }\n    }\n\n    @Test\n    public void onUpgrade_fromVersion7() {\n        int oldVersion = 7;\n        assertTrue(oldVersion < DatabaseConstants.FORMS_DATABASE_VERSION);\n        database.setVersion(oldVersion);\n\n        FormDatabaseMigrator formDatabaseMigrator = new FormDatabaseMigrator();\n        formDatabaseMigrator.createFormsTableV7(database);\n\n        ContentValues contentValues = createVersion7Form();\n        database.insert(FORMS_TABLE_NAME, null, contentValues);\n\n        formDatabaseMigrator.onUpgrade(database, oldVersion);\n\n        try (Cursor cursor = database.rawQuery(\"SELECT * FROM \" + FORMS_TABLE_NAME + \";\", new String[]{})) {\n            assertThat(cursor.getColumnCount(), is(19));\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToFirst();\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(contentValues.getAsString(DISPLAY_NAME)));\n            assertThat(cursor.getString(cursor.getColumnIndex(DESCRIPTION)), is(contentValues.getAsString(DESCRIPTION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(contentValues.getAsString(JR_FORM_ID)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), is(contentValues.getAsString(JR_VERSION)));\n            assertThat(cursor.getString(cursor.getColumnIndex(MD5_HASH)), is(contentValues.getAsString(MD5_HASH)));\n            assertThat(cursor.getInt(cursor.getColumnIndex(DATE)), is(contentValues.getAsInteger(DATE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_MEDIA_PATH)), is(contentValues.getAsString(FORM_MEDIA_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(FORM_FILE_PATH)), is(contentValues.getAsString(FORM_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), is(contentValues.getAsString(LANGUAGE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(SUBMISSION_URI)), is(contentValues.getAsString(SUBMISSION_URI)));\n            assertThat(cursor.getString(cursor.getColumnIndex(BASE64_RSA_PUBLIC_KEY)), is(contentValues.getAsString(BASE64_RSA_PUBLIC_KEY)));\n            assertThat(cursor.getString(cursor.getColumnIndex(JRCACHE_FILE_PATH)), is(contentValues.getAsString(JRCACHE_FILE_PATH)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_SEND)), is(contentValues.getAsString(AUTO_SEND)));\n            assertThat(cursor.getString(cursor.getColumnIndex(AUTO_DELETE)), is(contentValues.getAsString(AUTO_DELETE)));\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY_XPATH)), is(contentValues.getAsString(GEOMETRY_XPATH)));\n            assertThat(cursor.isNull(cursor.getColumnIndex(DELETED_DATE)), is(true));\n            assertThat(cursor.getString(cursor.getColumnIndex(LAST_DETECTED_ATTACHMENTS_UPDATE_DATE)), is(nullValue()));\n            assertThat(cursor.getString(cursor.getColumnIndex(USES_ENTITIES)), is(nullValue()));\n        }\n    }\n\n    private ContentValues createVersion8Form() {\n        ContentValues contentValues = new ContentValues();\n        contentValues.put(DISPLAY_NAME, \"DisplayName\");\n        contentValues.put(DESCRIPTION, \"Description\");\n        contentValues.put(JR_FORM_ID, \"FormId\");\n        contentValues.put(JR_VERSION, \"FormVersion\");\n        contentValues.put(MD5_HASH, \"Md5Hash\");\n        contentValues.put(DATE, 0);\n        contentValues.put(FORM_MEDIA_PATH, \"Form/Media/Path\");\n        contentValues.put(FORM_FILE_PATH, \"Form/File/Path\");\n        contentValues.put(LANGUAGE, \"Language\");\n        contentValues.put(SUBMISSION_URI, \"submission.uri\");\n        contentValues.put(BASE64_RSA_PUBLIC_KEY, \"Base64RsaPublicKey\");\n        contentValues.put(JRCACHE_FILE_PATH, \"Jr/Cache/File/Path\");\n        contentValues.put(AUTO_SEND, \"AutoSend\");\n        contentValues.put(AUTO_DELETE, \"AutoDelete\");\n        contentValues.put(LAST_DETECTED_FORM_VERSION_HASH, \"LastDetectedFormVersionHash\");\n        contentValues.put(GEOMETRY_XPATH, \"GeometryXPath\");\n        return contentValues;\n    }\n\n    private ContentValues createVersion7Form() {\n        ContentValues contentValues = new ContentValues();\n        contentValues.put(DISPLAY_NAME, \"DisplayName\");\n        contentValues.put(DESCRIPTION, \"Description\");\n        contentValues.put(JR_FORM_ID, \"FormId\");\n        contentValues.put(JR_VERSION, \"FormVersion\");\n        contentValues.put(MD5_HASH, \"Md5Hash\");\n        contentValues.put(DATE, 0);\n        contentValues.put(FORM_MEDIA_PATH, \"Form/Media/Path\");\n        contentValues.put(FORM_FILE_PATH, \"Form/File/Path\");\n        contentValues.put(LANGUAGE, \"Language\");\n        contentValues.put(SUBMISSION_URI, \"submission.uri\");\n        contentValues.put(BASE64_RSA_PUBLIC_KEY, \"Base64RsaPublicKey\");\n        contentValues.put(JRCACHE_FILE_PATH, \"Jr/Cache/File/Path\");\n        contentValues.put(AUTO_SEND, \"AutoSend\");\n        contentValues.put(AUTO_DELETE, \"AutoDelete\");\n        contentValues.put(LAST_DETECTED_FORM_VERSION_HASH, \"LastDetectedFormVersionHash\");\n        return contentValues;\n    }\n\n    private ContentValues createVersion9Form() {\n        ContentValues contentValues = new ContentValues();\n        contentValues.put(DISPLAY_NAME, \"DisplayName\");\n        contentValues.put(DESCRIPTION, \"Description\");\n        contentValues.put(JR_FORM_ID, \"FormId\");\n        contentValues.put(JR_VERSION, \"FormVersion\");\n        contentValues.put(MD5_HASH, \"Md5Hash\");\n        contentValues.put(DATE, 0);\n        contentValues.put(FORM_MEDIA_PATH, \"Form/Media/Path\");\n        contentValues.put(FORM_FILE_PATH, \"Form/File/Path\");\n        contentValues.put(LANGUAGE, \"Language\");\n        contentValues.put(SUBMISSION_URI, \"submission.uri\");\n        contentValues.put(BASE64_RSA_PUBLIC_KEY, \"Base64RsaPublicKey\");\n        contentValues.put(JRCACHE_FILE_PATH, \"Jr/Cache/File/Path\");\n        contentValues.put(AUTO_SEND, \"AutoSend\");\n        contentValues.put(AUTO_DELETE, \"AutoDelete\");\n        contentValues.put(GEOMETRY_XPATH, \"GeometryXPath\");\n        contentValues.put(\"deleted\", 0);\n        return contentValues;\n    }\n\n    private ContentValues createVersion10Form() {\n        ContentValues contentValues = new ContentValues();\n        contentValues.put(DISPLAY_NAME, \"DisplayName\");\n        contentValues.put(DESCRIPTION, \"Description\");\n        contentValues.put(JR_FORM_ID, \"FormId\");\n        contentValues.put(JR_VERSION, \"FormVersion\");\n        contentValues.put(MD5_HASH, \"Md5Hash\");\n        contentValues.put(DATE, 0);\n        contentValues.put(FORM_MEDIA_PATH, \"Form/Media/Path\");\n        contentValues.put(FORM_FILE_PATH, \"Form/File/Path\");\n        contentValues.put(LANGUAGE, \"Language\");\n        contentValues.put(SUBMISSION_URI, \"submission.uri\");\n        contentValues.put(BASE64_RSA_PUBLIC_KEY, \"Base64RsaPublicKey\");\n        contentValues.put(JRCACHE_FILE_PATH, \"Jr/Cache/File/Path\");\n        contentValues.put(AUTO_SEND, \"AutoSend\");\n        contentValues.put(AUTO_DELETE, \"AutoDelete\");\n        contentValues.put(GEOMETRY_XPATH, \"GeometryXPath\");\n        contentValues.put(DELETED_DATE, 0);\n        return contentValues;\n    }\n\n    private ContentValues createVersion11Form() {\n        ContentValues contentValues = new ContentValues();\n        contentValues.put(DISPLAY_NAME, \"DisplayName\");\n        contentValues.put(DESCRIPTION, \"Description\");\n        contentValues.put(JR_FORM_ID, \"FormId\");\n        contentValues.put(JR_VERSION, \"FormVersion\");\n        contentValues.put(MD5_HASH, \"Md5Hash\");\n        contentValues.put(DATE, 0);\n        contentValues.put(FORM_MEDIA_PATH, \"Form/Media/Path\");\n        contentValues.put(FORM_FILE_PATH, \"Form/File/Path\");\n        contentValues.put(LANGUAGE, \"Language\");\n        contentValues.put(SUBMISSION_URI, \"submission.uri\");\n        contentValues.put(BASE64_RSA_PUBLIC_KEY, \"Base64RsaPublicKey\");\n        contentValues.put(JRCACHE_FILE_PATH, \"Jr/Cache/File/Path\");\n        contentValues.put(AUTO_SEND, \"AutoSend\");\n        contentValues.put(AUTO_DELETE, \"AutoDelete\");\n        contentValues.put(GEOMETRY_XPATH, \"GeometryXPath\");\n        contentValues.put(DELETED_DATE, 0);\n        return contentValues;\n    }\n\n    private ContentValues getContentValuesForFormV12() {\n        ContentValues contentValues = new ContentValues();\n        contentValues.put(DISPLAY_NAME, \"DisplayName\");\n        contentValues.put(DESCRIPTION, \"Description\");\n        contentValues.put(JR_FORM_ID, \"FormId\");\n        contentValues.put(JR_VERSION, \"FormVersion\");\n        contentValues.put(MD5_HASH, \"Md5Hash\");\n        contentValues.put(DATE, 0);\n        contentValues.put(FORM_MEDIA_PATH, \"Form/Media/Path\");\n        contentValues.put(FORM_FILE_PATH, \"Form/File/Path\");\n        contentValues.put(LANGUAGE, \"Language\");\n        contentValues.put(SUBMISSION_URI, \"submission.uri\");\n        contentValues.put(BASE64_RSA_PUBLIC_KEY, \"Base64RsaPublicKey\");\n        contentValues.put(JRCACHE_FILE_PATH, \"Jr/Cache/File/Path\");\n        contentValues.put(AUTO_SEND, \"AutoSend\");\n        contentValues.put(AUTO_DELETE, \"AutoDelete\");\n        contentValues.put(GEOMETRY_XPATH, \"GeometryXPath\");\n        contentValues.put(DELETED_DATE, 0);\n        contentValues.put(LAST_DETECTED_ATTACHMENTS_UPDATE_DATE, 0);\n        return contentValues;\n    }\n\n    private ContentValues getContentValuesForFormV13() {\n        return getContentValuesForFormV12(); // there were no new columns added in v13\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/database/InstanceDatabaseMigratorTest.kt",
    "content": "package org.odk.collect.android.database\n\nimport android.content.ContentValues\nimport android.database.sqlite.SQLiteDatabase\nimport android.provider.BaseColumns._ID\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.CAN_DELETE_BEFORE_SEND\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.CAN_EDIT_WHEN_COMPLETE\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.DELETED_DATE\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.DISPLAY_NAME\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.EDIT_NUMBER\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.EDIT_OF\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.GEOMETRY\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.GEOMETRY_TYPE\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.INSTANCE_FILE_PATH\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.JR_FORM_ID\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.JR_VERSION\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.STATUS\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns.SUBMISSION_URI\nimport org.odk.collect.android.database.instances.InstanceDatabaseMigrator\nimport org.odk.collect.forms.instances.Instance\n\n@RunWith(AndroidJUnit4::class)\nclass InstanceDatabaseMigratorTest {\n\n    private var database = SQLiteDatabase.create(null)\n    private var instancesDatabaseMigrator = InstanceDatabaseMigrator()\n\n    @Before\n    fun setup() {\n        assertThat(\"Test expects different Instances DB version\", DatabaseConstants.INSTANCES_DATABASE_VERSION, equalTo(10))\n    }\n\n    @After\n    fun teardown() {\n        database.close()\n    }\n\n    @Test\n    fun databaseIdsShouldNotBeReused() {\n        instancesDatabaseMigrator.onCreate(database)\n        val contentValues = getContentValuesForInstanceV6()\n\n        database.insert(DatabaseConstants.INSTANCES_TABLE_NAME, null, contentValues)\n        database.rawQuery(\"SELECT * FROM \" + DatabaseConstants.INSTANCES_TABLE_NAME + \";\", arrayOf<String>()).use { cursor ->\n            assertThat(cursor.count, equalTo(1))\n            cursor.moveToFirst()\n            assertThat(cursor.getInt(cursor.getColumnIndex(_ID)), equalTo(1))\n        }\n\n        database.delete(DatabaseConstants.INSTANCES_TABLE_NAME, null, null)\n        database.insert(DatabaseConstants.INSTANCES_TABLE_NAME, null, contentValues)\n\n        database.rawQuery(\"SELECT * FROM \" + DatabaseConstants.INSTANCES_TABLE_NAME + \";\", arrayOf<String>()).use { cursor ->\n            assertThat(cursor.count, equalTo(1))\n            cursor.moveToFirst()\n            assertThat(cursor.getInt(cursor.getColumnIndex(_ID)), equalTo(2))\n        }\n    }\n\n    @Test\n    fun onUpgrade_fromVersion9_setsFinalizationDateToLastStatusChangedDate_forFinalizedStatuses() {\n        val finalizedStatuses = listOf(\n            Instance.STATUS_COMPLETE,\n            Instance.STATUS_SUBMITTED,\n            Instance.STATUS_SUBMISSION_FAILED\n        )\n\n        val oldVersion = 9\n\n        for (status in finalizedStatuses) {\n            database.version = oldVersion\n            instancesDatabaseMigrator.createInstancesTableV8(database)\n\n            val contentValues = ContentValues().apply {\n                put(DISPLAY_NAME, \"DisplayName\")\n                put(INSTANCE_FILE_PATH, \"InstanceFilePath\")\n                put(JR_FORM_ID, \"JrFormId\")\n                put(STATUS, status)\n                put(LAST_STATUS_CHANGE_DATE, 10)\n            }\n\n            database.insert(DatabaseConstants.INSTANCES_TABLE_NAME, null, contentValues)\n            instancesDatabaseMigrator.onUpgrade(database, oldVersion)\n\n            database.rawQuery(\"SELECT * FROM ${DatabaseConstants.INSTANCES_TABLE_NAME};\", null).use { cursor ->\n                cursor.moveToFirst()\n                val instance = DatabaseObjectMapper.getInstanceFromCurrentCursorPosition(cursor, \"\")!!\n                assertThat(instance.finalizationDate, equalTo(10))\n            }\n\n            database.execSQL(\"DROP TABLE IF EXISTS ${DatabaseConstants.INSTANCES_TABLE_NAME}\")\n        }\n    }\n\n    @Test\n    fun onUpgrade_fromVersion9_keepsFinalizationDateEmpty_forNotFinalizedStatuses() {\n        val finalizedStatuses = listOf(\n            Instance.STATUS_VALID,\n            Instance.STATUS_INVALID,\n            Instance.STATUS_NEW_EDIT\n        )\n\n        val oldVersion = 9\n\n        for (status in finalizedStatuses) {\n            database.version = oldVersion\n            instancesDatabaseMigrator.createInstancesTableV8(database)\n\n            val contentValues = ContentValues().apply {\n                put(DISPLAY_NAME, \"DisplayName\")\n                put(INSTANCE_FILE_PATH, \"InstanceFilePath\")\n                put(JR_FORM_ID, \"JrFormId\")\n                put(STATUS, status)\n                put(LAST_STATUS_CHANGE_DATE, 10)\n            }\n\n            database.insert(DatabaseConstants.INSTANCES_TABLE_NAME, null, contentValues)\n            instancesDatabaseMigrator.onUpgrade(database, oldVersion)\n\n            database.rawQuery(\"SELECT * FROM ${DatabaseConstants.INSTANCES_TABLE_NAME};\", null).use { cursor ->\n                cursor.moveToFirst()\n                val instance = DatabaseObjectMapper.getInstanceFromCurrentCursorPosition(cursor, \"\")!!\n                assertThat(instance.finalizationDate, equalTo(null))\n            }\n\n            database.execSQL(\"DROP TABLE IF EXISTS ${DatabaseConstants.INSTANCES_TABLE_NAME}\")\n        }\n    }\n\n    @Test\n    fun onUpgrade_fromVersion9() {\n        val oldVersion = 9\n        database.version = oldVersion\n        instancesDatabaseMigrator.createInstancesTableV9(database)\n\n        val contentValues = getContentValuesForInstanceV9()\n\n        database.insert(DatabaseConstants.INSTANCES_TABLE_NAME, null, contentValues)\n        instancesDatabaseMigrator.onUpgrade(database, oldVersion)\n        database.rawQuery(\"SELECT * FROM \" + DatabaseConstants.INSTANCES_TABLE_NAME + \";\", arrayOf<String>()).use { cursor ->\n            assertThat(cursor.columnCount, equalTo(16))\n            assertThat(cursor.count, equalTo(1))\n\n            cursor.moveToFirst()\n\n            val instance = DatabaseObjectMapper.getInstanceFromCurrentCursorPosition(cursor, \"\")!!\n\n            assertThat(instance.dbId, equalTo(1))\n            assertThat(instance.displayName, equalTo(contentValues.getAsString(DISPLAY_NAME)))\n            assertThat(instance.submissionUri, equalTo(contentValues.getAsString(SUBMISSION_URI)))\n            assertThat(instance.canEditWhenComplete(), equalTo(contentValues.getAsString(CAN_EDIT_WHEN_COMPLETE).toBoolean()))\n            assertThat(instance.instanceFilePath, equalTo(contentValues.getAsString(INSTANCE_FILE_PATH)))\n            assertThat(instance.formId, equalTo(contentValues.getAsString(JR_FORM_ID)))\n            assertThat(instance.formVersion, equalTo(contentValues.getAsString(JR_VERSION)))\n            assertThat(instance.status, equalTo(contentValues.getAsString(STATUS)))\n            assertThat(instance.lastStatusChangeDate, equalTo(contentValues.getAsLong(LAST_STATUS_CHANGE_DATE)))\n            assertThat(instance.deletedDate, equalTo(contentValues.getAsLong(DELETED_DATE)))\n            assertThat(instance.geometry, equalTo(contentValues.getAsString(GEOMETRY)))\n            assertThat(instance.geometryType, equalTo(contentValues.getAsString(GEOMETRY_TYPE)))\n            assertThat(instance.canDeleteBeforeSend(), equalTo(true))\n            assertThat(instance.editOf, equalTo(null))\n            assertThat(instance.editNumber, equalTo(null))\n            assertThat(instance.finalizationDate, equalTo(null))\n        }\n    }\n\n    @Test\n    fun onUpgrade_fromVersion8() {\n        val oldVersion = 8\n        database.version = oldVersion\n        instancesDatabaseMigrator.createInstancesTableV8(database)\n\n        val contentValues = getContentValuesForInstanceV8()\n\n        database.insert(DatabaseConstants.INSTANCES_TABLE_NAME, null, contentValues)\n        instancesDatabaseMigrator.onUpgrade(database, oldVersion)\n        database.rawQuery(\"SELECT * FROM \" + DatabaseConstants.INSTANCES_TABLE_NAME + \";\", arrayOf<String>()).use { cursor ->\n            assertThat(cursor.columnCount, equalTo(16))\n            assertThat(cursor.count, equalTo(1))\n\n            cursor.moveToFirst()\n\n            val instance = DatabaseObjectMapper.getInstanceFromCurrentCursorPosition(cursor, \"\")!!\n\n            assertThat(instance.dbId, equalTo(1))\n            assertThat(instance.displayName, equalTo(contentValues.getAsString(DISPLAY_NAME)))\n            assertThat(instance.submissionUri, equalTo(contentValues.getAsString(SUBMISSION_URI)))\n            assertThat(instance.canEditWhenComplete(), equalTo(contentValues.getAsString(CAN_EDIT_WHEN_COMPLETE).toBoolean()))\n            assertThat(instance.instanceFilePath, equalTo(contentValues.getAsString(INSTANCE_FILE_PATH)))\n            assertThat(instance.formId, equalTo(contentValues.getAsString(JR_FORM_ID)))\n            assertThat(instance.formVersion, equalTo(contentValues.getAsString(JR_VERSION)))\n            assertThat(instance.status, equalTo(contentValues.getAsString(STATUS)))\n            assertThat(instance.lastStatusChangeDate, equalTo(contentValues.getAsLong(LAST_STATUS_CHANGE_DATE)))\n            assertThat(instance.deletedDate, equalTo(contentValues.getAsLong(DELETED_DATE)))\n            assertThat(instance.geometry, equalTo(contentValues.getAsString(GEOMETRY)))\n            assertThat(instance.geometryType, equalTo(contentValues.getAsString(GEOMETRY_TYPE)))\n            assertThat(instance.canDeleteBeforeSend(), equalTo(true))\n            assertThat(instance.editOf, equalTo(null))\n            assertThat(instance.editNumber, equalTo(null))\n        }\n    }\n\n    @Test\n    fun onUpgrade_fromVersion7() {\n        val oldVersion = 7\n        database.version = oldVersion\n        instancesDatabaseMigrator.createInstancesTableV7(database)\n\n        val contentValues = getContentValuesForInstanceV7()\n\n        database.insert(DatabaseConstants.INSTANCES_TABLE_NAME, null, contentValues)\n        instancesDatabaseMigrator.onUpgrade(database, oldVersion)\n        database.rawQuery(\"SELECT * FROM \" + DatabaseConstants.INSTANCES_TABLE_NAME + \";\", arrayOf<String>()).use { cursor ->\n            assertThat(cursor.columnCount, equalTo(16))\n            assertThat(cursor.count, equalTo(1))\n\n            cursor.moveToFirst()\n\n            assertThat(cursor.getInt(cursor.getColumnIndex(_ID)), equalTo(1))\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), equalTo(contentValues.getAsString(DISPLAY_NAME)))\n            assertThat(cursor.getString(cursor.getColumnIndex(SUBMISSION_URI)), equalTo(contentValues.getAsString(SUBMISSION_URI)))\n            assertThat(cursor.getString(cursor.getColumnIndex(CAN_EDIT_WHEN_COMPLETE)), equalTo(contentValues.getAsString(CAN_EDIT_WHEN_COMPLETE)))\n            assertThat(cursor.getString(cursor.getColumnIndex(INSTANCE_FILE_PATH)), equalTo(contentValues.getAsString(INSTANCE_FILE_PATH)))\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), equalTo(contentValues.getAsString(JR_FORM_ID)))\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), equalTo(contentValues.getAsString(JR_VERSION)))\n            assertThat(cursor.getString(cursor.getColumnIndex(STATUS)), equalTo(contentValues.getAsString(STATUS)))\n            assertThat(cursor.getInt(cursor.getColumnIndex(LAST_STATUS_CHANGE_DATE)), equalTo(contentValues.getAsInteger(LAST_STATUS_CHANGE_DATE)))\n            assertThat(cursor.getInt(cursor.getColumnIndex(DELETED_DATE)), equalTo(contentValues.getAsInteger(DELETED_DATE)))\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY)), equalTo(contentValues.getAsString(GEOMETRY)))\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY_TYPE)), equalTo(contentValues.getAsString(GEOMETRY_TYPE)))\n            assertThat(cursor.getString(cursor.getColumnIndex(CAN_DELETE_BEFORE_SEND)), equalTo(\"true\"))\n        }\n    }\n\n    @Test\n    fun onUpgrade_fromVersion6() {\n        val oldVersion = 6\n        database.version = oldVersion\n        instancesDatabaseMigrator.createInstancesTableV6(database)\n\n        val contentValues = getContentValuesForInstanceV6()\n\n        database.insert(DatabaseConstants.INSTANCES_TABLE_NAME, null, contentValues)\n        instancesDatabaseMigrator.onUpgrade(database, oldVersion)\n        database.rawQuery(\"SELECT * FROM \" + DatabaseConstants.INSTANCES_TABLE_NAME + \";\", arrayOf<String>()).use { cursor ->\n            assertThat(cursor.columnCount, equalTo(16))\n            assertThat(cursor.count, equalTo(1))\n\n            cursor.moveToFirst()\n\n            assertThat(cursor.getInt(cursor.getColumnIndex(_ID)), equalTo(1))\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), equalTo(contentValues.getAsString(DISPLAY_NAME)))\n            assertThat(cursor.getString(cursor.getColumnIndex(SUBMISSION_URI)), equalTo(contentValues.getAsString(SUBMISSION_URI)))\n            assertThat(cursor.getString(cursor.getColumnIndex(CAN_EDIT_WHEN_COMPLETE)), equalTo(contentValues.getAsString(CAN_EDIT_WHEN_COMPLETE)))\n            assertThat(cursor.getString(cursor.getColumnIndex(INSTANCE_FILE_PATH)), equalTo(contentValues.getAsString(INSTANCE_FILE_PATH)))\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), equalTo(contentValues.getAsString(JR_FORM_ID)))\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), equalTo(contentValues.getAsString(JR_VERSION)))\n            assertThat(cursor.getString(cursor.getColumnIndex(STATUS)), equalTo(contentValues.getAsString(STATUS)))\n            assertThat(cursor.getInt(cursor.getColumnIndex(LAST_STATUS_CHANGE_DATE)), equalTo(contentValues.getAsInteger(LAST_STATUS_CHANGE_DATE)))\n            assertThat(cursor.getInt(cursor.getColumnIndex(DELETED_DATE)), equalTo(contentValues.getAsInteger(DELETED_DATE)))\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY)), equalTo(contentValues.getAsString(GEOMETRY)))\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY_TYPE)), equalTo(contentValues.getAsString(GEOMETRY_TYPE)))\n            assertThat(cursor.getString(cursor.getColumnIndex(CAN_DELETE_BEFORE_SEND)), equalTo(\"true\"))\n        }\n    }\n\n    private fun getContentValuesForInstanceV6(): ContentValues {\n        return ContentValues().apply {\n            put(DISPLAY_NAME, \"DisplayName\")\n            put(SUBMISSION_URI, \"SubmissionUri\")\n            put(CAN_EDIT_WHEN_COMPLETE, \"True\")\n            put(INSTANCE_FILE_PATH, \"InstanceFilePath\")\n            put(JR_FORM_ID, \"JrFormId\")\n            put(JR_VERSION, \"JrVersion\")\n            put(STATUS, \"Status\")\n            put(LAST_STATUS_CHANGE_DATE, 0)\n            put(DELETED_DATE, 0)\n            put(GEOMETRY, \"Geometry\")\n            put(GEOMETRY_TYPE, \"GeometryType\")\n        }\n    }\n\n    private fun getContentValuesForInstanceV7(): ContentValues {\n        return ContentValues().apply {\n            put(DISPLAY_NAME, \"DisplayName\")\n            put(SUBMISSION_URI, \"SubmissionUri\")\n            put(CAN_EDIT_WHEN_COMPLETE, \"True\")\n            put(INSTANCE_FILE_PATH, \"InstanceFilePath\")\n            put(JR_FORM_ID, \"JrFormId\")\n            put(JR_VERSION, \"JrVersion\")\n            put(STATUS, \"Status\")\n            put(LAST_STATUS_CHANGE_DATE, 0)\n            put(DELETED_DATE, 0)\n            put(GEOMETRY, \"Geometry\")\n            put(GEOMETRY_TYPE, \"GeometryType\")\n        }\n    }\n\n    private fun getContentValuesForInstanceV8(): ContentValues {\n        return ContentValues().apply {\n            put(DISPLAY_NAME, \"DisplayName\")\n            put(SUBMISSION_URI, \"SubmissionUri\")\n            put(CAN_EDIT_WHEN_COMPLETE, \"True\")\n            put(INSTANCE_FILE_PATH, \"InstanceFilePath\")\n            put(JR_FORM_ID, \"JrFormId\")\n            put(JR_VERSION, \"JrVersion\")\n            put(STATUS, \"Status\")\n            put(LAST_STATUS_CHANGE_DATE, 0)\n            put(DELETED_DATE, 0)\n            put(GEOMETRY, \"Geometry\")\n            put(GEOMETRY_TYPE, \"GeometryType\")\n            put(CAN_DELETE_BEFORE_SEND, \"true\")\n        }\n    }\n\n    private fun getContentValuesForInstanceV9(): ContentValues {\n        return ContentValues().apply {\n            put(DISPLAY_NAME, \"DisplayName\")\n            put(SUBMISSION_URI, \"SubmissionUri\")\n            put(CAN_EDIT_WHEN_COMPLETE, \"True\")\n            put(INSTANCE_FILE_PATH, \"InstanceFilePath\")\n            put(JR_FORM_ID, \"JrFormId\")\n            put(JR_VERSION, \"JrVersion\")\n            put(STATUS, \"Status\")\n            put(LAST_STATUS_CHANGE_DATE, 0)\n            put(DELETED_DATE, 0)\n            put(GEOMETRY, \"Geometry\")\n            put(GEOMETRY_TYPE, \"GeometryType\")\n            put(CAN_DELETE_BEFORE_SEND, \"true\")\n            putNull(EDIT_OF)\n            putNull(EDIT_NUMBER)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/database/entities/EntitiesDatabaseMigratorTest.kt",
    "content": "package org.odk.collect.android.database.entities\n\nimport android.content.ContentValues\nimport android.database.sqlite.SQLiteDatabase\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.db.sqlite.CursorExt.foldAndClose\nimport org.odk.collect.db.sqlite.CursorExt.rowToMap\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt.query\n\n@RunWith(AndroidJUnit4::class)\nclass EntitiesDatabaseMigratorTest {\n\n    /**\n     * Version 1 was only available in beta and upgrading from 1 to 2 originally just cleared\n     * all data. If there are any version 1s left in the wild, they will get a crash on launch.\n     */\n    @Test(expected = IllegalStateException::class)\n    fun `#onUpdate from version 1`() {\n        val db = SQLiteDatabase.create(null)\n        val migrator = EntitiesDatabaseMigrator(DATABASE_VERSION)\n        migrator.onUpgrade(db, 1)\n    }\n\n    @Test\n    fun `#onUpgrade from version 2`() {\n        val db = SQLiteDatabase.create(null)\n        val migrator = EntitiesDatabaseMigrator(DATABASE_VERSION)\n        migrator.createDbForVersion(db, 2)\n\n        val listContentValues = ContentValues().also {\n            it.put(\"name\", \"blah\")\n            it.put(\"hash\", \"somehash\")\n        }\n        db.insert(\"lists\", null, listContentValues)\n\n        migrator.onUpgrade(db, 2)\n        val lists = db.query(\"lists\").foldAndClose { it.rowToMap() }\n        assertThat(lists.size, equalTo(1))\n        assertThat(lists[0][\"name\"], equalTo(\"blah\"))\n        assertThat(lists[0][\"hash\"], equalTo(\"somehash\"))\n\n        assertThat(lists[0][\"needs_approval\"], equalTo(\"0\"))\n    }\n\n    @Test\n    fun `#onUpgrade from version 3`() {\n        val db = SQLiteDatabase.create(null)\n        val migrator = EntitiesDatabaseMigrator(DATABASE_VERSION)\n        migrator.createDbForVersion(db, 3)\n\n        val listContentValues = ContentValues().also {\n            it.put(\"name\", \"blah\")\n            it.put(\"hash\", \"somehash\")\n            it.put(\"needs_approval\", 0)\n        }\n        db.insert(\"lists\", null, listContentValues)\n\n        migrator.onUpgrade(db, 3)\n        val lists = db.query(\"lists\").foldAndClose { it.rowToMap() }\n        assertThat(lists.size, equalTo(1))\n        assertThat(lists[0][\"name\"], equalTo(\"blah\"))\n        assertThat(lists[0][\"hash\"], equalTo(\"somehash\"))\n        assertThat(lists[0][\"needs_approval\"], equalTo(\"0\"))\n\n        assertThat(lists[0].containsKey(\"last_updated\"), equalTo(true))\n        assertThat(lists[0][\"last_updated\"], equalTo(null))\n    }\n\n    companion object {\n        const val DATABASE_VERSION = 3\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/dependencies/BikramSambatTest.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.dependencies;\n\nimport org.junit.Test;\n\nimport bikramsambat.BikramSambatDate;\nimport bikramsambat.BsCalendar;\nimport bikramsambat.BsException;\nimport bikramsambat.BsGregorianDate;\n\nimport static org.junit.Assert.assertEquals;\n\n// Results confirmed with https://www.ashesh.com.np/nepali-date-converter.php\npublic class BikramSambatTest {\n\n    @Test\n    public void convertingGregorianToBikramSambatTest() throws BsException {\n        BikramSambatDate bikramSambatDate = BsCalendar.getInstance().toBik(1914, 1, 3);\n        assertEquals(1970, bikramSambatDate.year);\n        assertEquals(9, bikramSambatDate.month);\n        assertEquals(20, bikramSambatDate.day);\n\n        bikramSambatDate = BsCalendar.getInstance().toBik(1923, 2, 6);\n        assertEquals(1979, bikramSambatDate.year);\n        assertEquals(10, bikramSambatDate.month);\n        assertEquals(24, bikramSambatDate.day);\n\n        bikramSambatDate = BsCalendar.getInstance().toBik(1935, 3, 9);\n        assertEquals(1991, bikramSambatDate.year);\n        assertEquals(11, bikramSambatDate.month);\n        assertEquals(26, bikramSambatDate.day);\n\n        bikramSambatDate = BsCalendar.getInstance().toBik(1949, 4, 12);\n        assertEquals(2005, bikramSambatDate.year);\n        assertEquals(12, bikramSambatDate.month);\n        assertEquals(30, bikramSambatDate.day);\n\n        bikramSambatDate = BsCalendar.getInstance().toBik(1951, 5, 15);\n        assertEquals(2008, bikramSambatDate.year);\n        assertEquals(2, bikramSambatDate.month);\n        assertEquals(1, bikramSambatDate.day);\n\n        bikramSambatDate = BsCalendar.getInstance().toBik(1968, 6, 18);\n        assertEquals(2025, bikramSambatDate.year);\n        assertEquals(3, bikramSambatDate.month);\n        assertEquals(5, bikramSambatDate.day);\n\n        bikramSambatDate = BsCalendar.getInstance().toBik(1976, 7, 21);\n        assertEquals(2033, bikramSambatDate.year);\n        assertEquals(4, bikramSambatDate.month);\n        assertEquals(6, bikramSambatDate.day);\n\n        bikramSambatDate = BsCalendar.getInstance().toBik(1985, 8, 24);\n        assertEquals(2042, bikramSambatDate.year);\n        assertEquals(5, bikramSambatDate.month);\n        assertEquals(8, bikramSambatDate.day);\n\n        bikramSambatDate = BsCalendar.getInstance().toBik(1992, 9, 27);\n        assertEquals(2049, bikramSambatDate.year);\n        assertEquals(6, bikramSambatDate.month);\n        assertEquals(11, bikramSambatDate.day);\n\n        bikramSambatDate = BsCalendar.getInstance().toBik(2003, 10, 29);\n        assertEquals(2060, bikramSambatDate.year);\n        assertEquals(7, bikramSambatDate.month);\n        assertEquals(12, bikramSambatDate.day);\n\n        bikramSambatDate = BsCalendar.getInstance().toBik(2019, 11, 17);\n        assertEquals(2076, bikramSambatDate.year);\n        assertEquals(8, bikramSambatDate.month);\n        assertEquals(1, bikramSambatDate.day);\n    }\n\n    @Test\n    public void convertingBikramSambatToGregorian() throws BsException {\n        BsGregorianDate bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(1972, 1, 1));\n        assertEquals(1915, bsGregorianDate.year);\n        assertEquals(4, bsGregorianDate.month);\n        assertEquals(13, bsGregorianDate.day);\n\n        bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(1983, 2, 5));\n        assertEquals(1926, bsGregorianDate.year);\n        assertEquals(5, bsGregorianDate.month);\n        assertEquals(18, bsGregorianDate.day);\n\n        bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(1994, 3, 10));\n        assertEquals(1937, bsGregorianDate.year);\n        assertEquals(6, bsGregorianDate.month);\n        assertEquals(23, bsGregorianDate.day);\n\n        bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(2005, 4, 15));\n        assertEquals(1948, bsGregorianDate.year);\n        assertEquals(7, bsGregorianDate.month);\n        assertEquals(30, bsGregorianDate.day);\n\n        bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(2016, 5, 20));\n        assertEquals(1959, bsGregorianDate.year);\n        assertEquals(9, bsGregorianDate.month);\n        assertEquals(5, bsGregorianDate.day);\n\n        bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(2027, 6, 25));\n        assertEquals(1970, bsGregorianDate.year);\n        assertEquals(10, bsGregorianDate.month);\n        assertEquals(11, bsGregorianDate.day);\n\n        bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(2038, 7, 28));\n        assertEquals(1981, bsGregorianDate.year);\n        assertEquals(11, bsGregorianDate.month);\n        assertEquals(13, bsGregorianDate.day);\n\n        bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(2049, 8, 17));\n        assertEquals(1992, bsGregorianDate.year);\n        assertEquals(12, bsGregorianDate.month);\n        assertEquals(2, bsGregorianDate.day);\n\n        bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(2051, 9, 6));\n        assertEquals(1994, bsGregorianDate.year);\n        assertEquals(12, bsGregorianDate.month);\n        assertEquals(21, bsGregorianDate.day);\n\n        bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(2062, 10, 4));\n        assertEquals(2006, bsGregorianDate.year);\n        assertEquals(1, bsGregorianDate.month);\n        assertEquals(17, bsGregorianDate.day);\n\n        bsGregorianDate = BsCalendar.getInstance().toGreg(new BikramSambatDate(2073, 11, 14));\n        assertEquals(2017, bsGregorianDate.year);\n        assertEquals(2, bsGregorianDate.month);\n        assertEquals(25, bsGregorianDate.day);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/dependencies/PersianCalendarTest.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.dependencies;\n\nimport org.joda.time.DateTime;\nimport org.joda.time.chrono.GregorianChronology;\nimport org.joda.time.chrono.PersianChronologyKhayyamBorkowski;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * Results confirmed with:\n * http://www.iranchamber.com/calendar/converter/iranian_calendar_converter.php\n * and\n * https://calcuworld.com/calendar-calculators/persian-calendar-converter/\n */\npublic class PersianCalendarTest {\n\n    @Test\n    public void convertingGregorianToPersianTest() {\n        DateTime gregorianDateTime = new DateTime().withYear(1905).withMonthOfYear(2).withDayOfMonth(5);\n        assertDate(getPersianDateTime(gregorianDateTime), 1283, 11, 16);\n\n        gregorianDateTime = new DateTime().withYear(1918).withMonthOfYear(12).withDayOfMonth(1);\n        assertDate(getPersianDateTime(gregorianDateTime), 1297, 9, 9);\n\n        gregorianDateTime = new DateTime().withYear(1921).withMonthOfYear(1).withDayOfMonth(31);\n        assertDate(getPersianDateTime(gregorianDateTime), 1299, 11, 11);\n\n        gregorianDateTime = new DateTime().withYear(1935).withMonthOfYear(5).withDayOfMonth(18);\n        assertDate(getPersianDateTime(gregorianDateTime), 1314, 2, 27);\n\n        gregorianDateTime = new DateTime().withYear(1947).withMonthOfYear(9).withDayOfMonth(24);\n        assertDate(getPersianDateTime(gregorianDateTime), 1326, 7, 1);\n\n        gregorianDateTime = new DateTime().withYear(1952).withMonthOfYear(7).withDayOfMonth(17);\n        assertDate(getPersianDateTime(gregorianDateTime), 1331, 4, 26);\n\n        gregorianDateTime = new DateTime().withYear(1968).withMonthOfYear(10).withDayOfMonth(8);\n        assertDate(getPersianDateTime(gregorianDateTime), 1347, 7, 16);\n\n        gregorianDateTime = new DateTime().withYear(1970).withMonthOfYear(4).withDayOfMonth(30);\n        assertDate(getPersianDateTime(gregorianDateTime), 1349, 2, 10);\n\n        gregorianDateTime = new DateTime().withYear(1987).withMonthOfYear(6).withDayOfMonth(8);\n        assertDate(getPersianDateTime(gregorianDateTime), 1366, 3, 18);\n\n        gregorianDateTime = new DateTime().withYear(1991).withMonthOfYear(10).withDayOfMonth(20);\n        assertDate(getPersianDateTime(gregorianDateTime), 1370, 7, 28);\n\n        gregorianDateTime = new DateTime().withYear(2005).withMonthOfYear(11).withDayOfMonth(17);\n        assertDate(getPersianDateTime(gregorianDateTime), 1384, 8, 26);\n\n        gregorianDateTime = new DateTime().withYear(2019).withMonthOfYear(6).withDayOfMonth(5);\n        assertDate(getPersianDateTime(gregorianDateTime), 1398, 3, 15);\n\n        gregorianDateTime = new DateTime().withYear(2023).withMonthOfYear(2).withDayOfMonth(28);\n        assertDate(getPersianDateTime(gregorianDateTime), 1401, 12, 9);\n\n        gregorianDateTime = new DateTime().withYear(2034).withMonthOfYear(4).withDayOfMonth(1);\n        assertDate(getPersianDateTime(gregorianDateTime), 1413, 1, 12);\n\n        gregorianDateTime = new DateTime().withYear(2048).withMonthOfYear(1).withDayOfMonth(2);\n        assertDate(getPersianDateTime(gregorianDateTime), 1426, 10, 12);\n\n        gregorianDateTime = new DateTime().withYear(2056).withMonthOfYear(8).withDayOfMonth(22);\n        assertDate(getPersianDateTime(gregorianDateTime), 1435, 6, 1);\n\n        gregorianDateTime = new DateTime().withYear(2063).withMonthOfYear(10).withDayOfMonth(11);\n        assertDate(getPersianDateTime(gregorianDateTime), 1442, 7, 19);\n\n        gregorianDateTime = new DateTime().withYear(2075).withMonthOfYear(4).withDayOfMonth(18);\n        assertDate(getPersianDateTime(gregorianDateTime), 1454, 1, 29);\n\n        gregorianDateTime = new DateTime().withYear(2080).withMonthOfYear(12).withDayOfMonth(30);\n        assertDate(getPersianDateTime(gregorianDateTime), 1459, 10, 10);\n\n        gregorianDateTime = new DateTime().withYear(2099).withMonthOfYear(7).withDayOfMonth(14);\n        assertDate(getPersianDateTime(gregorianDateTime), 1478, 4, 24);\n    }\n\n    @Test\n    public void convertingPersianToGregorianTest() {\n        DateTime persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1281).withMonthOfYear(3).withDayOfMonth(17);\n        assertDate(getGregorianDateTime(persianDateTime), 1902, 6, 8);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1290).withMonthOfYear(1).withDayOfMonth(30);\n        assertDate(getGregorianDateTime(persianDateTime), 1911, 4, 20);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1307).withMonthOfYear(10).withDayOfMonth(1);\n        assertDate(getGregorianDateTime(persianDateTime), 1928, 12, 22);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1314).withMonthOfYear(7).withDayOfMonth(14);\n        assertDate(getGregorianDateTime(persianDateTime), 1935, 10, 7);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1326).withMonthOfYear(11).withDayOfMonth(21);\n        assertDate(getGregorianDateTime(persianDateTime), 1948, 2, 11);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1339).withMonthOfYear(3).withDayOfMonth(28);\n        assertDate(getGregorianDateTime(persianDateTime), 1960, 6, 18);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1348).withMonthOfYear(5).withDayOfMonth(9);\n        assertDate(getGregorianDateTime(persianDateTime), 1969, 7, 31);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1352).withMonthOfYear(8).withDayOfMonth(5);\n        assertDate(getGregorianDateTime(persianDateTime), 1973, 10, 27);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1367).withMonthOfYear(12).withDayOfMonth(15);\n        assertDate(getGregorianDateTime(persianDateTime), 1989, 3, 6);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1375).withMonthOfYear(2).withDayOfMonth(20);\n        assertDate(getGregorianDateTime(persianDateTime), 1996, 5, 9);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1381).withMonthOfYear(6).withDayOfMonth(11);\n        assertDate(getGregorianDateTime(persianDateTime), 2002, 9, 2);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1395).withMonthOfYear(9).withDayOfMonth(29);\n        assertDate(getGregorianDateTime(persianDateTime), 2016, 12, 19);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1400).withMonthOfYear(5).withDayOfMonth(10);\n        assertDate(getGregorianDateTime(persianDateTime), 2021, 8, 1);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1417).withMonthOfYear(10).withDayOfMonth(12);\n        assertDate(getGregorianDateTime(persianDateTime), 2039, 1, 2);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1424).withMonthOfYear(8).withDayOfMonth(20);\n        assertDate(getGregorianDateTime(persianDateTime), 2045, 11, 10);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1439).withMonthOfYear(11).withDayOfMonth(5);\n        assertDate(getGregorianDateTime(persianDateTime), 2061, 1, 24);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1443).withMonthOfYear(12).withDayOfMonth(17);\n        assertDate(getGregorianDateTime(persianDateTime), 2065, 3, 7);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1452).withMonthOfYear(3).withDayOfMonth(4);\n        assertDate(getGregorianDateTime(persianDateTime), 2073, 5, 24);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1467).withMonthOfYear(9).withDayOfMonth(22);\n        assertDate(getGregorianDateTime(persianDateTime), 2088, 12, 12);\n\n        persianDateTime = new DateTime(PersianChronologyKhayyamBorkowski.getInstance()).withYear(1474).withMonthOfYear(6).withDayOfMonth(12);\n        assertDate(getGregorianDateTime(persianDateTime), 2095, 9, 2);\n    }\n\n    private void assertDate(DateTime dateTime, int expectedYear, int expectedMonth, int expectedDay) {\n        assertEquals(expectedYear, dateTime.getYear());\n        assertEquals(expectedMonth, dateTime.getMonthOfYear());\n        assertEquals(expectedDay, dateTime.getDayOfMonth());\n    }\n\n    private DateTime getPersianDateTime(DateTime gregorianDateTime) {\n        return new DateTime(gregorianDateTime).withChronology(PersianChronologyKhayyamBorkowski.getInstance());\n    }\n\n    private DateTime getGregorianDateTime(DateTime persianDateTime) {\n        return new DateTime(persianDateTime).withChronology(GregorianChronology.getInstance());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/dynamicpreload/DynamicPreloadExtraTest.kt",
    "content": "package org.odk.collect.android.dynamicpreload\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.instanceOf\nimport org.javarosa.core.util.externalizable.ExtUtil\nimport org.junit.Test\n\nclass DynamicPreloadExtraTest {\n\n    @Test\n    fun `can be externalized`() {\n        val extra = DynamicPreloadExtra()\n\n        val external = ExtUtil.serialize(extra)\n        val deserialized =\n            ExtUtil.deserialize(external, DynamicPreloadExtra::class.java) as DynamicPreloadExtra\n\n        assertThat(deserialized, instanceOf(DynamicPreloadExtra::class.java))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/dynamicpreload/DynamicPreloadParseProcessorTest.kt",
    "content": "package org.odk.collect.android.dynamicpreload\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.instanceOf\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.QuestionDef\nimport org.javarosa.xpath.expr.XPathExpression\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\n\nclass DynamicPreloadParseProcessorTest {\n\n    private val processor = DynamicPreloadParseProcessor()\n\n    @Test\n    fun `DynamicPreloadExtra is null when xpath does not contain pulldata`() {\n        val formDef = FormDef()\n\n        processor.processXPath(createNonPullDataExpression())\n        processor.processFormDef(formDef)\n        assertThat(\n            formDef.extras.get(DynamicPreloadExtra::class.java),\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun `DynamicPreloadExtra is not null when xpath does contain pulldata`() {\n        val formDef = FormDef()\n\n        processor.processXPath(createPullDataExpression())\n        processor.processFormDef(formDef)\n        assertThat(\n            formDef.extras.get(DynamicPreloadExtra::class.java),\n            instanceOf(DynamicPreloadExtra::class.java)\n        )\n    }\n\n    @Test\n    fun `DynamicPreloadExtra is null when question appearance does not contain search`() {\n        val formDef = FormDef()\n\n        processor.processQuestion(createQuestion(appearance = \"minimal\"))\n        processor.processFormDef(formDef)\n        assertThat(\n            formDef.extras.get(DynamicPreloadExtra::class.java),\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun `DynamicPreloadExtra is not null when question appearance does contain search`() {\n        val formDef = FormDef()\n\n        processor.processQuestion(createQuestion(appearance = \"search('fruits')\"))\n        processor.processFormDef(formDef)\n        assertThat(\n            formDef.extras.get(DynamicPreloadExtra::class.java),\n            instanceOf(DynamicPreloadExtra::class.java)\n        )\n    }\n\n    private fun createNonPullDataExpression() = mock<XPathExpression> {\n        on { containsFunc(\"pulldata\") } doReturn false\n    }\n\n    private fun createPullDataExpression() = mock<XPathExpression> {\n        on { containsFunc(\"pulldata\") } doReturn true\n    }\n\n    private fun createQuestion(appearance: String): QuestionDef {\n        return mock {\n            on { appearanceAttr } doReturn appearance\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/dynamicpreload/ExternalDataReaderTest.java",
    "content": "package org.odk.collect.android.dynamicpreload;\n\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.apache.commons.io.IOUtils;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.db.sqlite.CustomSQLiteQueryBuilder;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.db.sqlite.SQLiteUtils;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.Writer;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.hamcrest.CoreMatchers.not;\nimport static org.hamcrest.CoreMatchers.notNullValue;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.odk.collect.android.dynamicpreload.ExternalDataUtil.COLUMN_DATASET_FILENAME;\nimport static org.odk.collect.android.dynamicpreload.ExternalDataUtil.COLUMN_MD5_HASH;\nimport static org.odk.collect.android.dynamicpreload.ExternalDataUtil.EXTERNAL_DATA_TABLE_NAME;\nimport static org.odk.collect.android.dynamicpreload.ExternalDataUtil.EXTERNAL_METADATA_TABLE_NAME;\n\n@RunWith(AndroidJUnit4.class)\npublic class ExternalDataReaderTest {\n    private static final String SIMPLE_SEARCH_EXTERNAL_CSV_FORM_FILENAME = \"simple-search-external-csv.xml\";\n    private static final String SIMPLE_SEARCH_EXTERNAL_CSV_NAME = \"simple-search-external-csv-fruits\";\n    private static final String SIMPLE_SEARCH_EXTERNAL_CSV_FILENAME = \"simple-search-external-csv-fruits.csv\";\n    private static final String SIMPLE_SEARCH_EXTERNAL_DB_FILENAME = \"simple-search-external-csv-fruits.db\";\n\n    private static final String SELECT_ALL_DATA_QUERY = \"SELECT * FROM \" + EXTERNAL_DATA_TABLE_NAME;\n\n    private static File csvFile;\n    private static File dbFile;\n\n    private static Map<String, File> formDefToCsvMedia;\n\n    @Before\n    public void setUp() throws IOException {\n        CollectHelpers.setupDemoProject();\n\n        File formFile = new File(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.FORMS) + File.separator + SIMPLE_SEARCH_EXTERNAL_CSV_FORM_FILENAME);\n        File mediaDir = FileUtils.getFormMediaDir(formFile);\n        mediaDir.mkdir();\n        csvFile = new File(mediaDir + File.separator + SIMPLE_SEARCH_EXTERNAL_CSV_FILENAME);\n        dbFile = new File(mediaDir + File.separator + SIMPLE_SEARCH_EXTERNAL_DB_FILENAME);\n        formDefToCsvMedia = makeExternalDataMap();\n\n        try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(\"forms\" + File.separator + SIMPLE_SEARCH_EXTERNAL_CSV_FORM_FILENAME);\n             OutputStream output = new FileOutputStream(formFile)) {\n            IOUtils.copy(input, output);\n        }\n\n        try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(\"media\" + File.separator + SIMPLE_SEARCH_EXTERNAL_CSV_FILENAME);\n             OutputStream output = new FileOutputStream(csvFile)) {\n            IOUtils.copy(input, output);\n        }\n    }\n\n    @Test\n    public void doImport_createsDataAndMetadataTables() {\n        ExternalDataReader externalDataReader = new ExternalDataReaderImpl(() -> false, mock());\n        externalDataReader.doImport(formDefToCsvMedia);\n\n        assertThat(dbFile.exists(), is(true));\n\n        SQLiteDatabase db = SQLiteDatabase.openDatabase(dbFile.getAbsolutePath(), null, SQLiteDatabase.OPEN_READWRITE);\n        assertThat(db, is(notNullValue()));\n        assertThat(SQLiteUtils.doesTableExist(db, EXTERNAL_DATA_TABLE_NAME), is(true));\n        assertThat(SQLiteUtils.doesTableExist(db, EXTERNAL_METADATA_TABLE_NAME), is(true));\n    }\n\n    /**\n     * There are multiple features that ingest CSV files so the original file should not be modified.\n     * https://github.com/getodk/collect/issues/3335\n     */\n    @Test\n    public void doImport_doesNotModifyOriginalCsv() {\n        ExternalDataReader externalDataReader = new ExternalDataReaderImpl(() -> false, mock());\n        externalDataReader.doImport(formDefToCsvMedia);\n\n        assertThat(dbFile.exists(), is(true));\n        assertThat(csvFile.exists(), is(true));\n    }\n\n    @Test\n    public void createAndPopulateMetadataTable_createsMetadataTableWithExpectedMd5Hash() {\n        final String testMetadataTable = \"testMetadataTable\";\n\n        SQLiteDatabase.OpenParams.Builder paramsBuilder = new SQLiteDatabase.OpenParams.Builder();\n        SQLiteDatabase db = SQLiteDatabase.createInMemory(paramsBuilder.build());\n        ExternalSQLiteOpenHelper.createAndPopulateMetadataTable(db, testMetadataTable, csvFile);\n\n        assertThat(SQLiteUtils.doesTableExist(db, testMetadataTable), is(true));\n\n        final String[] columnNames = {COLUMN_MD5_HASH};\n        final String selectCriteria = CustomSQLiteQueryBuilder.formatCompareEquals(\n                COLUMN_DATASET_FILENAME,\n                CustomSQLiteQueryBuilder.quoteStringLiteral(SIMPLE_SEARCH_EXTERNAL_CSV_FILENAME));\n        Cursor cursor = db.query(testMetadataTable, columnNames, selectCriteria, null, null, null, null);\n        cursor.moveToFirst();\n        String fileMd5 = cursor.getString(0);\n\n        assertThat(fileMd5, is(Md5.getMd5Hash(csvFile)));\n    }\n\n    @Test\n    public void doImport_reimportsCsvIfDatabaseFileIsDeleted() {\n        // Create the DB file with an initial import\n        ExternalDataReader externalDataReader = new ExternalDataReaderImpl(() -> false, mock());\n        externalDataReader.doImport(formDefToCsvMedia);\n        assertThat(dbFile.exists(), is(true));\n\n        dbFile.delete();\n\n        // Reimport\n        externalDataReader = new ExternalDataReaderImpl(() -> false, mock());\n        externalDataReader.doImport(formDefToCsvMedia);\n        assertThat(dbFile.exists(), is(true));\n    }\n\n    @Test\n    public void doImport_reimportsCsvIfMetadataTableIsMissing() {\n        // Create the DB file with an initial import\n        ExternalDataReader externalDataReader = new ExternalDataReaderImpl(() -> false, mock());\n        externalDataReader.doImport(formDefToCsvMedia);\n        assertThat(dbFile.exists(), is(true));\n\n        // Remove the metadata table (mimicking prior versions without the metadata table)\n        SQLiteDatabase db = SQLiteDatabase.openDatabase(dbFile.getAbsolutePath(), null, SQLiteDatabase.OPEN_READWRITE);\n        SQLiteUtils.dropTable(db, EXTERNAL_METADATA_TABLE_NAME);\n        db.close();\n\n        // Reimport\n        externalDataReader = new ExternalDataReaderImpl(() -> false, mock());\n        externalDataReader.doImport(formDefToCsvMedia);\n        assertThat(dbFile.exists(), is(true));\n        db = SQLiteDatabase.openDatabase(dbFile.getAbsolutePath(), null, SQLiteDatabase.OPEN_READWRITE);\n        assertThat(\"metadata table should be recreated\", SQLiteUtils.doesTableExist(db, EXTERNAL_METADATA_TABLE_NAME));\n        db.close();\n    }\n\n    @Test\n    public void doImport_reimportsCsvIfFileIsUpdated() throws IOException, InterruptedException {\n        // Create the DB file with an initial import\n        ExternalDataReader externalDataReader = new ExternalDataReaderImpl(() -> false, mock());\n        externalDataReader.doImport(formDefToCsvMedia);\n        assertThat(dbFile.exists(), is(true));\n\n        SQLiteDatabase db = SQLiteDatabase.openDatabase(dbFile.getAbsolutePath(), null, SQLiteDatabase.OPEN_READWRITE);\n        assertThat(db.rawQuery(SELECT_ALL_DATA_QUERY, null).getCount(), is(3));\n\n        String originalHash = Md5.getMd5Hash(csvFile);\n        String metadataTableHash = ExternalSQLiteOpenHelper.getLastMd5Hash(db, EXTERNAL_METADATA_TABLE_NAME, csvFile);\n        assertThat(metadataTableHash, is(originalHash));\n\n        try (Writer out = new BufferedWriter(new FileWriter(csvFile, true))) {\n            out.write(\"\\ncherimoya,Cherimoya\");\n        }\n\n        String newHash = Md5.getMd5Hash(csvFile);\n        assertThat(newHash, is(not(originalHash)));\n\n        // Reimport\n        externalDataReader = new ExternalDataReaderImpl(() -> false, mock());\n        externalDataReader.doImport(formDefToCsvMedia);\n\n        db = SQLiteDatabase.openDatabase(dbFile.getAbsolutePath(), null, SQLiteDatabase.OPEN_READONLY);\n        assertThat(db.rawQuery(SELECT_ALL_DATA_QUERY, null).getCount(), is(4));\n\n        // Check the metadata table import timestamp\n        metadataTableHash = ExternalSQLiteOpenHelper.getLastMd5Hash(db, EXTERNAL_METADATA_TABLE_NAME, csvFile);\n        assertThat(metadataTableHash, is(newHash));\n    }\n\n    @Test\n    public void doImport_skipsImportIfFileNotUpdated() {\n        // Create the DB file with an initial import\n        ExternalDataReader externalDataReader = new ExternalDataReaderImpl(() -> false, mock());\n        externalDataReader.doImport(formDefToCsvMedia);\n        assertThat(dbFile.exists(), is(true));\n        SQLiteDatabase db = SQLiteDatabase.openDatabase(dbFile.getAbsolutePath(), null, SQLiteDatabase.OPEN_READWRITE);\n        Cursor cursor = db.rawQuery(SELECT_ALL_DATA_QUERY, null);\n        assertThat(cursor.getCount(), is(3));\n\n        // Purge the contents of the data table before reimporting\n        db.delete(EXTERNAL_DATA_TABLE_NAME, null, null);\n        assertThat(SQLiteUtils.doesTableExist(db, EXTERNAL_DATA_TABLE_NAME), is(true));\n        cursor = db.rawQuery(SELECT_ALL_DATA_QUERY, null);\n        assertThat(\"expected zero rows of data after purging\", cursor.getCount(), is(0));\n        db.close();\n\n        // Reimport\n        externalDataReader = new ExternalDataReaderImpl(() -> false, mock());\n        externalDataReader.doImport(formDefToCsvMedia);\n        db = SQLiteDatabase.openDatabase(dbFile.getAbsolutePath(), null, SQLiteDatabase.OPEN_READWRITE);\n        cursor = db.rawQuery(SELECT_ALL_DATA_QUERY, null);\n        assertThat(\"expected zero rows of data after reimporting unchanged file\", cursor.getCount(), is(0));\n    }\n\n    private static Map<String, File> makeExternalDataMap() {\n        Map<String, File> externalDataMap = new HashMap<>();\n        externalDataMap.put(SIMPLE_SEARCH_EXTERNAL_CSV_NAME, csvFile);\n        return externalDataMap;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/dynamicpreload/ExternalDataUseCasesTest.kt",
    "content": "package org.odk.collect.android.dynamicpreload\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.FormDef\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass ExternalDataUseCasesTest {\n\n    @Test\n    fun `create() does nothing if the form doesn't use dynamic preload`() {\n        val form = FormDef()\n\n        val mediaDir = TempFiles.createTempDir().also {\n            File(it, \"items.csv\").writeText(\"name_key,name\\nmango,Mango\")\n        }\n\n        ExternalDataUseCases.create(form, mediaDir, { false }, {})\n        assertThat(mediaDir.listFiles().size, equalTo(1))\n    }\n\n    @Test\n    fun `create() does not create a db file if the FormDef does not have a DynamicPreloadExtra`() {\n        val form = FormDef()\n\n        val mediaDir = TempFiles.createTempDir().also {\n            File(it, \"items.csv\").writeText(\"name_key,name\\nmango,Mango\")\n        }\n\n        ExternalDataUseCases.create(form, mediaDir, { false }, {})\n        assertThat(mediaDir.listFiles().size, equalTo(1))\n    }\n\n    @Test\n    fun `create() creates a db file if the FormDef has a DynamicPreloadExtra`() {\n        val form = FormDef().also {\n            it.extras.put(DynamicPreloadExtra())\n        }\n\n        val mediaDir = TempFiles.createTempDir().also {\n            File(it, \"items.csv\").writeText(\"name_key,name\\nmango,Mango\")\n        }\n\n        ExternalDataUseCases.create(form, mediaDir, { false }, {})\n        assertThat(mediaDir.listFiles().size, equalTo(2))\n    }\n\n    @Test\n    fun `create() leaves original CSV in place`() {\n        val form = FormDef().also {\n            it.extras.put(DynamicPreloadExtra())\n        }\n\n        val mediaDir = TempFiles.createTempDir()\n        val csv = File(mediaDir, \"items.csv\").also { it.writeText(\"name_key,name\\nmango,Mango\") }\n\n        ExternalDataUseCases.create(form, mediaDir, { false }, {})\n        assertThat(csv.exists(), equalTo(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/dynamicpreload/ExternalDataUtilTest.java",
    "content": "package org.odk.collect.android.dynamicpreload;\n\nimport org.junit.Assert;\nimport org.junit.Test;\n\npublic class ExternalDataUtilTest {\n    @Test\n    public void testSafeColumnName() {\n        // This is likely bad behavior: the method does not check the input for null or empty strings.\n        Assert.assertEquals(\"c_\", ExternalDataUtil.toSafeColumnName(\"\"));\n\n        // casing\n        Assert.assertEquals(\"c_simplename\", ExternalDataUtil.toSafeColumnName(\"simplename\"));\n        Assert.assertEquals(\"c_camelcase\", ExternalDataUtil.toSafeColumnName(\"CamelCase\"));\n\n        // whitespace\n        Assert.assertEquals(\"c_trailingwhitespace\", ExternalDataUtil.toSafeColumnName(\"trailingwhitespace \"));\n        Assert.assertEquals(\"c_leadingwhitespace\", ExternalDataUtil.toSafeColumnName(\" leadingwhitespace\"));\n        Assert.assertEquals(\"c_middle_whitespace\", ExternalDataUtil.toSafeColumnName(\"middle whitespace\"));\n\n        // numbers\n        Assert.assertEquals(\"c_0123456789\", ExternalDataUtil.toSafeColumnName(\"0123456789\"));\n\n        // specials\n        Assert.assertEquals(\"c_a_b\", ExternalDataUtil.toSafeColumnName(\"a*b\"));\n        Assert.assertEquals(\"c_new_line\", ExternalDataUtil.toSafeColumnName(\"new\\nline\"));\n        Assert.assertEquals(\"c_double_quote\", ExternalDataUtil.toSafeColumnName(\"double\\\"quote\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/entities/DatabaseEntitiesRepositoryTest.kt",
    "content": "package org.odk.collect.android.entities\n\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.database.entities.DatabaseEntitiesRepository\nimport org.odk.collect.android.entities.support.EntitySameAsMatcher.Companion.sameEntityAs\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.shared.TempFiles\n\n@RunWith(AndroidJUnit4::class)\nclass DatabaseEntitiesRepositoryTest : EntitiesRepositoryTest() {\n    override fun buildSubject(clock: () -> Long): EntitiesRepository {\n        return DatabaseEntitiesRepository(\n            ApplicationProvider.getApplicationContext(),\n            TempFiles.createTempDir().absolutePath,\n            clock\n        )\n    }\n\n    @Test\n    fun `#save supports properties with db column names saving new entities and updating existing ones`() {\n        val repository = buildSubject()\n        val entity = Entity.New(\n            \"1\",\n            \"One\",\n            properties = listOf(Pair(\"_id\", \"value\"), Pair(\"version\", \"value\"))\n        )\n\n        repository.save(\"things\", entity)\n        val savedEntity = repository.query(\"things\")[0]\n        assertThat(savedEntity, sameEntityAs(entity))\n\n        repository.save(\"things\", savedEntity)\n        assertThat(repository.query(\"things\")[0], sameEntityAs(savedEntity))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/entities/EntitiesRepositoryTest.kt",
    "content": "package org.odk.collect.android.entities\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.contains\nimport org.hamcrest.Matchers.containsInAnyOrder\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Ignore\nimport org.junit.Test\nimport org.odk.collect.android.entities.support.EntitySameAsMatcher.Companion.sameEntityAs\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.entities.storage.EntityList\nimport org.odk.collect.entities.storage.QueryException\nimport org.odk.collect.entities.storage.getListNames\nimport org.odk.collect.shared.Query\n\nabstract class EntitiesRepositoryTest {\n\n    abstract fun buildSubject(clock: () -> Long): EntitiesRepository\n    fun buildSubject(): EntitiesRepository = buildSubject { 0 }\n\n    @Test\n    fun `#getLists returns lists for saved entities`() {\n        val repository = buildSubject { 123L }\n\n        val wine = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val whisky = Entity.New(\"2\", \"Lagavulin 16\")\n        repository.save(\"wines\", wine)\n        repository.save(\"whiskys\", whisky)\n\n        repository.updateList(\"wines\", \"blah\", true)\n\n        assertThat(\n            repository.getLists(),\n            containsInAnyOrder(\n                EntityList(\"wines\", \"blah\", true, 123L),\n                EntityList(\"whiskys\")\n            )\n        )\n    }\n\n    @Test\n    fun `#save updates existing entity with matching id`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            trunkVersion = 1\n        )\n        repository.save(\"wines\", wine)\n\n        val updatedWine = wine.copy(label = \"Léoville Barton 2009\", version = 2)\n        repository.save(\"wines\", updatedWine)\n\n        val wines = repository.query(\"wines\")\n        assertThat(wines, contains(sameEntityAs(updatedWine)))\n    }\n\n    @Test\n    fun `#save creates entity with matching id in different list`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\"1\", \"Léoville Barton 2008\", version = 1)\n        repository.save(\"wines\", wine)\n\n        val updatedWine = Entity.New(wine.id, \"Edradour 10\", version = 2)\n        repository.save(\"whisky\", updatedWine)\n\n        val wines = repository.query(\"wines\")\n        assertThat(wines, contains(sameEntityAs(wine)))\n        val whiskys = repository.query(\"whisky\")\n        assertThat(whiskys, contains(sameEntityAs(updatedWine)))\n    }\n\n    @Test\n    fun `#save updates existing entity with matching id and version`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\"1\", \"Léoville Barton 2008\", version = 1)\n        repository.save(\"wines\", wine)\n\n        val updatedWine = wine.copy(label = \"Léoville Barton 2009\")\n        repository.save(\"wines\", updatedWine)\n\n        val wines = repository.query(\"wines\")\n        assertThat(wines, contains(sameEntityAs(updatedWine)))\n    }\n\n    @Test\n    fun `#save updates state on existing entity when it is offline`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\"1\", \"Léoville Barton 2008\", state = Entity.State.OFFLINE)\n        repository.save(\"wines\", wine)\n\n        val updatedWine = wine.copy(state = Entity.State.ONLINE)\n        repository.save(\"wines\", updatedWine)\n\n        val wines = repository.query(\"wines\")\n        assertThat(wines, contains(sameEntityAs(updatedWine)))\n    }\n\n    @Test\n    fun `#save does not update state on existing entity when it is online`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\"1\", \"Léoville Barton 2008\", state = Entity.State.ONLINE)\n        repository.save(\"wines\", wine)\n\n        val updatedWine = wine.copy(state = Entity.State.OFFLINE)\n        repository.save(\"wines\", updatedWine)\n\n        val wines = repository.query(\"wines\")\n        assertThat(wines, contains(sameEntityAs(wine)))\n    }\n\n    @Test\n    fun `#save adds new properties and removes missing ones`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"window\" to \"2019-2038\", \"region\" to \"Bordeaux\"),\n            version = 1\n        )\n        repository.save(\"wines\", wine)\n\n        val updatedWine = Entity.New(\n            wine.id,\n            \"Léoville Barton 2008\",\n            properties = listOf(\"window\" to \"2019-2038\", \"score\" to \"92\"),\n            version = 2\n        )\n        repository.save(\"wines\", updatedWine)\n\n        val wines = repository.query(\"wines\")\n        assertThat(wines.size, equalTo(1))\n        assertThat(wines[0].properties, contains(\"window\" to \"2019-2038\", \"score\" to \"92\"))\n    }\n\n    @Test\n    fun `#save adds new properties and removes missing ones for lists with dashes`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"window\" to \"2019-2038\", \"region\" to \"Bordeaux\"),\n            version = 1\n        )\n        repository.save(\"favourite-wines\", wine)\n\n        val updatedWine = Entity.New(\n            wine.id,\n            \"Léoville Barton 2008\",\n            properties = listOf(\"window\" to \"2019-2038\", \"score\" to \"92\"),\n            version = 2\n        )\n        repository.save(\"favourite-wines\", updatedWine)\n\n        val wines = repository.query(\"favourite-wines\")\n        assertThat(wines.size, equalTo(1))\n        assertThat(wines[0].properties, contains(\"window\" to \"2019-2038\", \"score\" to \"92\"))\n    }\n\n    @Test\n    fun `#save adds new properties and removes missing ones to existing entities`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"window\" to \"2019-2038\", \"region\" to \"Bordeaux\"),\n            version = 1\n        )\n        repository.save(\"wines\", wine)\n\n        val otherWine = Entity.New(\n            \"2\",\n            \"Léoville Barton 2009\",\n            properties = listOf(\"window\" to \"2020-2039\", \"score\" to \"92\"),\n            version = 2\n        )\n        repository.save(\"wines\", otherWine)\n\n        val wines = repository.query(\"wines\")\n        assertThat(wines.size, equalTo(2))\n        assertThat(wines[0].properties, contains(\"window\" to \"2019-2038\", \"score\" to \"\"))\n        assertThat(wines[1].properties, contains(\"window\" to \"2020-2039\", \"score\" to \"92\"))\n    }\n\n    @Test\n    fun `#save updates existing properties`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"window\" to \"2019-2038\"),\n            version = 1\n        )\n        repository.save(\"wines\", wine)\n\n        val updatedWine = Entity.New(\n            wine.id,\n            \"Léoville Barton 2008\",\n            properties = listOf(\"window\" to \"2019-2042\"),\n            version = 2\n        )\n        repository.save(\"wines\", updatedWine)\n\n        val wines = repository.query(\"wines\")\n        assertThat(wines.size, equalTo(1))\n        assertThat(wines[0].properties, contains(\"window\" to \"2019-2042\"))\n    }\n\n    @Test\n    fun `#save does not update existing label if new one is null`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"window\" to \"2019-2038\"),\n            version = 1\n        )\n        repository.save(\"wines\", wine)\n\n        val updatedWine = Entity.New(\n            wine.id,\n            null,\n            properties = listOf(\"window\" to \"2019-2042\"),\n            version = 2\n        )\n        repository.save(\"wines\", updatedWine)\n\n        val wines = repository.query(\"wines\")\n        assertThat(wines.size, equalTo(1))\n        assertThat(wines[0].label, equalTo(wine.label))\n        assertThat(wines[0].properties, equalTo(updatedWine.properties))\n    }\n\n    @Test\n    fun `#save does not clear empty entity lists`() {\n        val repository = buildSubject()\n\n        repository.addList(\"wines\")\n        repository.addList(\"blah\")\n        assertThat(repository.getListNames(), containsInAnyOrder(\"wines\", \"blah\"))\n\n        repository.save(\"wines\", Entity.New(\"blah\", \"Blah\"))\n        assertThat(repository.getListNames(), containsInAnyOrder(\"wines\", \"blah\"))\n    }\n\n    @Test\n    fun `#save supports properties with dots and dashes when saving new entities and updating existing ones`() {\n        val repository = buildSubject()\n        val entity = Entity.New(\n            \"1\",\n            \"One\",\n            properties = listOf(Pair(\"a.property\", \"value\"), Pair(\"a-property\", \"value\"))\n        )\n\n        repository.save(\"things\", entity)\n        val savedEntity = repository.query(\"things\")[0]\n        assertThat(savedEntity, sameEntityAs(entity))\n\n        repository.save(\"things\", savedEntity)\n        assertThat(repository.query(\"things\")[0], sameEntityAs(savedEntity))\n    }\n\n    @Test\n    fun `#save does not create a list when no entities are provided`() {\n        val repository = buildSubject()\n        repository.save(\"blah\")\n        assertThat(repository.getLists(), equalTo(emptyList()))\n    }\n\n    @Test\n    fun `#save supports creating list names with with dots and dashes`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\"1\", \"Léoville Barton 2008\")\n\n        repository.save(\"favourite-wines\", wine)\n        assertThat(repository.query(\"favourite-wines\")[0], sameEntityAs(wine))\n\n        repository.save(\"favourite.wines\", wine)\n        assertThat(repository.query(\"favourite.wines\")[0], sameEntityAs(wine))\n    }\n\n    @Test\n    fun `#save can save multiple entities`() {\n        val repository = buildSubject()\n\n        val wine1 = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val wine2 = Entity.New(\"2\", \"Chateau Pontet Canet\")\n        repository.save(\"wines\", wine1, wine2)\n\n        assertThat(repository.query(\"wines\").size, equalTo(2))\n    }\n\n    @Test\n    fun `#save assigns an index to each entity in insert order when saving multiple entities`() {\n        /**\n         * first and second have alphabetically out of order IDs/names here so that any indexing on\n         * them is tested. We'd likely never see this fail if they were ordered.\n         */\n        val first = Entity.New(\"2\", \"B\")\n        val second = Entity.New(\"1\", \"A\")\n\n        val repository = buildSubject()\n        repository.save(\"wines\", first, second)\n\n        val entities = repository.query(\"wines\")\n        assertThat(entities[0].index, equalTo(0))\n        assertThat(entities[0].id, equalTo(first.id))\n        assertThat(entities[1].index, equalTo(1))\n        assertThat(entities[1].id, equalTo(second.id))\n    }\n\n    @Test\n    fun `#save assigns an index to each entity in insert order when saving single entities`() {\n        val first = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val second = Entity.New(\"2\", \"Pontet Canet 2014\")\n\n        val repository = buildSubject()\n        repository.save(\"wines\", first)\n        repository.save(\"wines\", second)\n\n        val entities = repository.query(\"wines\")\n        assertThat(entities[0].index, equalTo(0))\n        assertThat(entities[1].index, equalTo(1))\n    }\n\n    @Test\n    fun `#save does not change index when updating an existing entity`() {\n        val repository = buildSubject()\n\n        val first = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val second = Entity.New(\"2\", \"Pontet Canet 2014\")\n        repository.save(\"wines\", first, second)\n        assertThat(repository.query(\"wines\")[0].index, equalTo(0))\n\n        val updatedWine = first.copy(label = \"Léoville Barton 2009\")\n        repository.save(\"wines\", updatedWine)\n\n        assertThat(repository.query(\"wines\")[0].index, equalTo(0))\n    }\n\n    @Test\n    fun `#addList adds a list with no entities`() {\n        val repository = buildSubject()\n\n        repository.addList(\"wine\")\n        assertThat(repository.getListNames(), containsInAnyOrder(\"wine\"))\n        assertThat(repository.query(\"wine\").size, equalTo(0))\n    }\n\n    @Test\n    fun `#addList works if list already exists`() {\n        val repository = buildSubject()\n\n        repository.addList(\"wine\")\n        repository.addList(\"wine\")\n        assertThat(repository.getListNames(), containsInAnyOrder(\"wine\"))\n        assertThat(repository.query(\"wine\").size, equalTo(0))\n    }\n\n    @Test\n    fun `#delete removes an entity`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val canet = Entity.New(\"2\", \"Pontet-Canet 2014\")\n        repository.save(\"wines\", leoville, canet)\n\n        repository.delete(\"wines\", \"1\")\n\n        assertThat(\n            repository.query(\"wines\"),\n            containsInAnyOrder(sameEntityAs(canet))\n        )\n    }\n\n    @Test\n    fun `#delete supports list names with dots and dashes`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\"1\", \"Léoville Barton 2008\")\n\n        repository.save(\"wines.x\", leoville)\n        repository.save(\"wines-x\", leoville)\n\n        repository.delete(\"wines.x\", \"1\")\n        repository.delete(\"wines-x\", \"1\")\n\n        assertThat(\n            repository.query(\"wines.x\").isEmpty(),\n            equalTo(true)\n        )\n        assertThat(\n            repository.query(\"wines-x\").isEmpty(),\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `#delete updates index values so that they are always in sequence and start at 0`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val canet = Entity.New(\"2\", \"Pontet-Canet 2014\")\n        val gloria = Entity.New(\"3\", \"Chateau Gloria 2016\")\n        repository.save(\"wines\", leoville, canet, gloria)\n\n        repository.delete(\"wines\", \"1\")\n        var wines = repository.query(\"wines\")\n        assertThat(wines[0].index, equalTo(0))\n        assertThat(wines[1].index, equalTo(1))\n\n        repository.save(\"wines\", leoville)\n        wines = repository.query(\"wines\")\n        assertThat(wines[0].index, equalTo(0))\n        assertThat(wines[1].index, equalTo(1))\n        assertThat(wines[2].index, equalTo(2))\n    }\n\n    @Test\n    fun `#getCount returns 0 when a list is empty`() {\n        val repository = buildSubject()\n        repository.addList(\"wines\")\n\n        assertThat(repository.getCount(\"wines\"), equalTo(0))\n    }\n\n    @Test\n    fun `#getCount returns 0 when a list does not exist`() {\n        val repository = buildSubject()\n        assertThat(repository.getCount(\"wines\"), equalTo(0))\n    }\n\n    @Test\n    fun `#getCount returns number of entities in list`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val dows = Entity.New(\"2\", \"Dow's 1983\")\n        repository.save(\"wines\", leoville, dows)\n\n        val springbank = Entity.New(\"1\", \"Springbank 10\")\n        repository.save(\"whiskys\", springbank)\n\n        assertThat(repository.getCount(\"wines\"), equalTo(2))\n        assertThat(repository.getCount(\"whiskys\"), equalTo(1))\n    }\n\n    @Test\n    fun `#getCount supports list names with dots and dashes`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val dows = Entity.New(\"2\", \"Dow's 1983\")\n        repository.save(\"favourite-wines\", leoville, dows)\n\n        val springbank = Entity.New(\"1\", \"Springbank 10\")\n        repository.save(\"favourite.whiskys\", springbank)\n\n        assertThat(repository.getCount(\"favourite-wines\"), equalTo(2))\n        assertThat(repository.getCount(\"favourite.whiskys\"), equalTo(1))\n    }\n\n    @Test\n    fun `#getByIndex returns matching entity`() {\n        val repository = buildSubject()\n\n        val springbank = Entity.New(\"1\", \"Springbank 10\")\n        val aultmore = Entity.New(\"2\", \"Aultmore 12\")\n        repository.save(\"whiskys\", springbank, aultmore)\n\n        val aultmoreIndex = repository.query(\"whiskys\").first { it.id == aultmore.id }.index\n        assertThat(repository.getByIndex(\"whiskys\", aultmoreIndex), sameEntityAs(aultmore))\n    }\n\n    @Test\n    fun `#getByIndex returns null when the list does not exist`() {\n        val repository = buildSubject()\n        assertThat(repository.getByIndex(\"wine\", 0), equalTo(null))\n    }\n\n    @Test\n    fun `#getByIndex returns null when the list is empty`() {\n        val repository = buildSubject()\n        repository.addList(\"wine\")\n\n        assertThat(repository.getByIndex(\"wine\", 0), equalTo(null))\n    }\n\n    @Test\n    fun `#getByIndex supports list names with dots and dashes`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val canet = Entity.New(\"2\", \"Pontet-Canet 2014\")\n        repository.save(\"favourite-wines\", leoville)\n        repository.save(\"other.favourite.wines\", canet)\n\n        val leovilleIndex =\n            repository.query(\"favourite-wines\").first { it.id == leoville.id }.index\n        assertThat(repository.getByIndex(\"favourite-wines\", leovilleIndex), sameEntityAs(leoville))\n\n        val canetIndex =\n            repository.query(\"other.favourite.wines\").first { it.id == canet.id }.index\n        assertThat(repository.getByIndex(\"other.favourite.wines\", canetIndex), sameEntityAs(canet))\n    }\n\n    @Test\n    fun `#getList returns list`() {\n        val repository = buildSubject()\n\n        repository.addList(\"wine\")\n        repository.updateList(\"wine\", \"2024\", true)\n        assertThat(repository.getList(\"wine\"), equalTo(EntityList(\"wine\", \"2024\", true, 0L)))\n    }\n\n    @Test\n    fun `#getList returns list with null hash if list doesn't have hash`() {\n        val repository = buildSubject()\n\n        repository.addList(\"wine\")\n        assertThat(repository.getList(\"wine\"), equalTo(EntityList(\"wine\", null)))\n    }\n\n    @Test\n    fun `#getList returns null if list doesn't exist`() {\n        val repository = buildSubject()\n        assertThat(repository.getList(\"wine\"), equalTo(null))\n    }\n\n    @Test\n    fun `#save ignores case-insensitive duplicate new properties`() {\n        val repository = buildSubject()\n        val entity = Entity.New(\n            \"1\",\n            \"One\",\n            properties = listOf(Pair(\"prop\", \"value\"), Pair(\"Prop\", \"value\"))\n        )\n\n        repository.save(\"things\", entity)\n        val savedEntities = repository.query(\"things\")\n        assertThat(savedEntities[0].properties.size, equalTo(1))\n        assertThat(savedEntities[0].properties[0].first, equalTo(\"prop\"))\n    }\n\n    @Test\n    fun `#save ignores case-insensitive duplicate properties if one of them has already been saved`() {\n        val repository = buildSubject()\n        val entity = Entity.New(\n            \"1\",\n            \"One\",\n            properties = listOf(Pair(\"prop\", \"value\"))\n        )\n\n        repository.save(\"things\", entity)\n        var savedEntities = repository.query(\"things\")\n        assertThat(savedEntities[0].properties.size, equalTo(1))\n        assertThat(savedEntities[0].properties[0].first, equalTo(\"prop\"))\n\n        repository.save(\"things\", entity.copy(properties = listOf(Pair(\"Prop\", \"value\"))))\n        savedEntities = repository.query(\"things\")\n        assertThat(savedEntities[0].properties.size, equalTo(1))\n        assertThat(savedEntities[0].properties[0].first, equalTo(\"prop\"))\n    }\n\n    @Test\n    fun `#query returns matching entities`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            version = 1,\n            properties = listOf(\"vintage\" to \"2008\")\n        )\n\n        val canet = Entity.New(\n            \"2\",\n            \"Pontet-Canet 2014\",\n            version = 2,\n            properties = listOf(\"vintage\" to \"2009\")\n        )\n\n        repository.save(\"wines\", leoville, canet)\n\n        val wines = repository.query(\"wines\", Query.StringEq(\"name\", \"2\"))\n        assertThat(wines, containsInAnyOrder(sameEntityAs(canet)))\n    }\n\n    @Test\n    fun `#query returns empty list when there are no matches`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            version = 1,\n            properties = listOf(\"vintage\" to \"2008\")\n        )\n\n        repository.save(\"wines\", leoville)\n\n        val wines = repository.query(\"wines\", Query.StringEq(\"name\", \"3\"))\n        assertThat(wines, equalTo(emptyList()))\n    }\n\n    @Test\n    fun `#query returns empty list when there is a match in a different list`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val ardbeg = Entity.New(\"2\", \"Ardbeg 10\")\n\n        repository.save(\"wines\", leoville)\n        repository.save(\"whisky\", ardbeg)\n\n        assertThat(\n            repository.query(\"wines\", Query.StringEq(\"label\", \"Ardbeg 10\")),\n            equalTo(emptyList())\n        )\n    }\n\n    @Test\n    fun `#query returns empty list where there are no entities in the list`() {\n        val repository = buildSubject()\n        assertThat(\n            repository.query(\"wines\", Query.StringEq(\"label\", \"Léoville Barton 2008\")),\n            equalTo(emptyList())\n        )\n    }\n\n    @Test\n    fun `#query supports list names with dots and dashes`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\"1\", \"Léoville Barton 2008\")\n        val canet = Entity.New(\"2\", \"Pontet-Canet 2014\")\n        repository.save(\"favourite-wines\", leoville)\n        repository.save(\"other.favourite.wines\", canet)\n\n        val queriedLeoville =\n            repository.query(\"favourite-wines\", Query.StringEq(\"label\", \"Léoville Barton 2008\"))\n        assertThat(queriedLeoville, containsInAnyOrder(sameEntityAs(leoville)))\n\n        val queriedCanet =\n            repository.query(\"other.favourite.wines\", Query.StringEq(\"label\", \"Pontet-Canet 2014\"))\n        assertThat(queriedCanet, containsInAnyOrder(sameEntityAs(canet)))\n    }\n\n    @Test(expected = QueryException::class)\n    fun `#query throws an exception when not existing property is used`() {\n        val repository = buildSubject()\n        repository.save(\"wines\", Entity.New(\"1\", \"Léoville Barton 2008\"))\n\n        repository.query(\"wines\", Query.StringEq(\"score\", \"92\"))\n    }\n\n    @Test\n    fun `#query returns matching entities with numeric eq selection arguments for integer values`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"score\" to \"5\")\n        )\n\n        val dows = Entity.New(\n            \"3\",\n            \"Dow's 1983\",\n            properties = listOf(\"score\" to \"7\")\n        )\n\n        repository.save(\"wines\", leoville, dows)\n\n        val wines = repository.query(\"wines\", Query.NumericEq(\"score\", 5.0))\n        assertThat(wines, containsInAnyOrder(sameEntityAs(leoville)))\n    }\n\n    @Test\n    fun `#query returns matching entities with numeric eq selection arguments for decimal values`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"score\" to \"5.0\")\n        )\n\n        val dows = Entity.New(\n            \"3\",\n            \"Dow's 1983\",\n            properties = listOf(\"score\" to \"7.5\")\n        )\n\n        repository.save(\"wines\", leoville, dows)\n\n        val wines = repository.query(\"wines\", Query.NumericEq(\"score\", 5.0))\n        assertThat(wines, containsInAnyOrder(sameEntityAs(leoville)))\n    }\n\n    @Test\n    fun `#query returns matching entities with numeric not eq selection arguments for integer values`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"score\" to \"5\")\n        )\n\n        val dows = Entity.New(\n            \"3\",\n            \"Dow's 1983\",\n            properties = listOf(\"score\" to \"7\")\n        )\n\n        repository.save(\"wines\", leoville, dows)\n\n        val wines = repository.query(\"wines\", Query.NumericNotEq(\"score\", 5.0))\n        assertThat(wines, containsInAnyOrder(sameEntityAs(dows)))\n    }\n\n    @Test\n    fun `#query returns matching entities with numeric not eq selection arguments for decimal values`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"score\" to \"5.0\")\n        )\n\n        val dows = Entity.New(\n            \"3\",\n            \"Dow's 1983\",\n            properties = listOf(\"score\" to \"7.5\")\n        )\n\n        repository.save(\"wines\", leoville, dows)\n\n        val wines = repository.query(\"wines\", Query.NumericNotEq(\"score\", 5.0))\n        assertThat(wines, containsInAnyOrder(sameEntityAs(dows)))\n    }\n\n    @Test\n    fun `#query without query returns empty list when there are not entities`() {\n        val repository = buildSubject()\n        assertThat(repository.query(\"wines\").size, equalTo(0))\n    }\n\n    @Test\n    fun `#query without query returns entities for list`() {\n        val repository = buildSubject()\n\n        val wine = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            version = 2,\n            trunkVersion = 1\n        )\n\n        val whisky = Entity.New(\n            \"2\",\n            \"Lagavulin 16\",\n            version = 3,\n            trunkVersion = 1\n        )\n\n        repository.save(\"wines\", wine)\n        repository.save(\"whiskys\", whisky)\n\n        val wines = repository.query(\"wines\")\n        assertThat(wines.size, equalTo(1))\n        assertThat(wines[0], sameEntityAs(wine))\n\n        val whiskys = repository.query(\"whiskys\")\n        assertThat(whiskys.size, equalTo(1))\n        assertThat(whiskys[0], sameEntityAs(whisky))\n    }\n\n    @Test\n    @Ignore(\"https://github.com/getodk/collect/issues/6615\")\n    fun `#query returns entities when searching for empty string for property that doesn't exist`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"vintage\" to \"2008\")\n        )\n\n        repository.save(\"wines\", leoville)\n        assertThat(repository.query(\"wines\", Query.StringEq(\"score\", \"\")).size, equalTo(1))\n    }\n\n    @Test\n    @Ignore(\"https://github.com/getodk/collect/issues/6615\")\n    fun `#query returns empty list when searching for non empty string for property that doesn't exist`() {\n        val repository = buildSubject()\n\n        val leoville = Entity.New(\n            \"1\",\n            \"Léoville Barton 2008\",\n            properties = listOf(\"vintage\" to \"2008\")\n        )\n\n        repository.save(\"wines\", leoville)\n        assertThat(repository.query(\"wines\", Query.StringEq(\"score\", \"92\")).size, equalTo(0))\n    }\n\n    @Test\n    fun `#updateList creates list if doesn't exist`() {\n        val repository = buildSubject()\n        repository.updateList(\"blah\", \"abcd\", false)\n        assertThat(repository.getLists().size, equalTo(1))\n        assertThat(repository.query(\"blah\").size, equalTo(0))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/entities/InMemEntitiesRepositoryTest.kt",
    "content": "package org.odk.collect.android.entities\n\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.entities.storage.InMemEntitiesRepository\n\nclass InMemEntitiesRepositoryTest : EntitiesRepositoryTest() {\n\n    override fun buildSubject(clock: () -> Long): EntitiesRepository {\n        return InMemEntitiesRepository(clock)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/entities/support/EntitySameAsMatcher.kt",
    "content": "package org.odk.collect.android.entities.support\n\nimport org.hamcrest.Description\nimport org.hamcrest.TypeSafeMatcher\nimport org.odk.collect.entities.storage.Entity\n\nclass EntitySameAsMatcher(private val expected: Entity) : TypeSafeMatcher<Entity>() {\n    override fun describeTo(description: Description) {\n        description.appendText(\"is the same as $expected\")\n    }\n\n    override fun matchesSafely(item: Entity?): Boolean {\n        return if (item != null) {\n            expected.sameAs(item)\n        } else {\n            false\n        }\n    }\n\n    companion object {\n        fun sameEntityAs(entity: Entity): EntitySameAsMatcher {\n            return EntitySameAsMatcher(entity)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/external/FormUriActivityTest.kt",
    "content": "package org.odk.collect.android.external\n\nimport android.app.Activity\nimport android.app.Application\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.test.core.app.ActivityScenario\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.pressBack\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.intent.Intents\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasData\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.work.WorkManager\nimport com.google.gson.Gson\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.`is`\nimport org.hamcrest.Matchers.not\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.activities.FormFillingActivity\nimport org.odk.collect.android.application.initialization.AnalyticsInitializer\nimport org.odk.collect.android.application.initialization.MapsInitializer\nimport org.odk.collect.android.formentry.FormOpeningMode\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.utilities.ChangeLockProvider\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.utilities.SavepointsRepositoryProvider\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport org.odk.collect.androidtest.RecordedIntentsRule\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.formstest.FormUtils\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.formstest.InMemSavepointsRepository\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.shared.locks.BooleanChangeLock\nimport org.odk.collect.shared.strings.UUIDGenerator\nimport org.odk.collect.testshared.FakeScheduler\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.Locale\n\n@RunWith(AndroidJUnit4::class)\nclass FormUriActivityTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n    private val projectsRepository = InMemProjectsRepository()\n    private val projectsDataService = mock<ProjectsDataService>()\n    private val formsRepository = InMemFormsRepository()\n    private val instancesRepository = InMemInstancesRepository { 0 }\n    private val fakeScheduler = FakeScheduler()\n    private val settingsProvider = InMemSettingsProvider().apply {\n        getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, true)\n    }\n    private val savepointsRepository = InMemSavepointsRepository()\n    private val savepointsRepositoryProvider = mock<SavepointsRepositoryProvider>().apply {\n        whenever(create()).thenReturn(savepointsRepository)\n    }\n    private val changeLock = BooleanChangeLock()\n    private val changeLockProvider = ChangeLockProvider { changeLock }\n\n    @get:Rule\n    val activityRule = RecordedIntentsRule()\n\n    @get:Rule\n    val launcherRule = ActivityScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesProjectsRepository(\n                uuidGenerator: UUIDGenerator,\n                gson: Gson,\n                settingsProvider: SettingsProvider\n            ): ProjectsRepository {\n                return projectsRepository\n            }\n\n            override fun providesCurrentProjectProvider(\n                application: Application,\n                settingsProvider: SettingsProvider,\n                projectsRepository: ProjectsRepository,\n                analyticsInitializer: AnalyticsInitializer,\n                context: Context,\n                mapsInitializer: MapsInitializer\n            ): ProjectsDataService {\n                return projectsDataService\n            }\n\n            override fun providesFormsRepositoryProvider(application: Application): FormsRepositoryProvider {\n                return mock<FormsRepositoryProvider>().apply {\n                    whenever(create()).thenReturn(formsRepository)\n                }\n            }\n\n            override fun providesInstancesRepositoryProvider(\n                context: Context,\n                storagePathProvider: StoragePathProvider\n            ): InstancesRepositoryProvider {\n                return mock<InstancesRepositoryProvider>().apply {\n                    whenever(create()).thenReturn(instancesRepository)\n                }\n            }\n\n            override fun providesSettingsProvider(context: Context): SettingsProvider {\n                return settingsProvider\n            }\n\n            override fun providesScheduler(workManager: WorkManager?, application: Application): Scheduler {\n                return fakeScheduler\n            }\n\n            override fun providesSavepointsRepositoryProvider(context: Context?, storagePathProvider: StoragePathProvider?): SavepointsRepositoryProvider {\n                return savepointsRepositoryProvider\n            }\n\n            override fun providesChangeLockProvider(): ChangeLockProvider {\n                return changeLockProvider\n            }\n        })\n    }\n\n    @Test\n    fun `When there are no projects then display alert dialog`() {\n        val scenario = launcherRule.launchForResult(FormUriActivity::class.java)\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.app_not_configured)\n        )\n    }\n\n    @Test\n    fun `When there is project id specified in uri but it is not the currently used project then display alert dialog`() {\n        val firstProject = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        val secondProject = Project.Saved(\"345\", \"Second project\", \"A\", \"#cccccc\")\n        projectsRepository.save(firstProject)\n        projectsRepository.save(secondProject)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(firstProject)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        val scenario = launcherRule.launchForResult<FormUriActivity>(\n            getBlankFormIntent(\n                secondProject.uuid,\n                form.dbId\n            )\n        )\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.wrong_project_selected_for_form)\n        )\n    }\n\n    @Test\n    fun `When there is no project id specified in uri and first available project is not the currently used project then display alert dialog`() {\n        val firstProject = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        val secondProject = Project.Saved(\"345\", \"Second project\", \"A\", \"#cccccc\")\n        projectsRepository.save(firstProject)\n        projectsRepository.save(secondProject)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(secondProject)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        val scenario =\n            launcherRule.launchForResult<FormUriActivity>(getBlankFormIntent(null, form.dbId))\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.wrong_project_selected_for_form)\n        )\n    }\n\n    @Test\n    fun `When uri is null then display alert dialog`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        val scenario = launcherRule.launchForResult<FormUriActivity>(\n            getBlankFormIntent(project.uuid, form.dbId).apply {\n                data = null\n            }\n        )\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.unrecognized_uri)\n        )\n    }\n\n    @Test\n    fun `When uri is invalid then display alert dialog`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        val scenario = launcherRule.launchForResult<FormUriActivity>(\n            getBlankFormIntent(project.uuid, form.dbId).apply {\n                data = Uri.parse(\"blah\")\n            }\n        )\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.unrecognized_uri)\n        )\n    }\n\n    @Test\n    fun `When uri represents a blank form that does not exist then display alert dialog`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val scenario =\n            launcherRule.launchForResult<FormUriActivity>(getBlankFormIntent(project.uuid, 1))\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(scenario, context.getString(org.odk.collect.strings.R.string.bad_uri))\n    }\n\n    @Test\n    fun `When uri represents a blank form with non existing form file then display alert dialog`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        File(form.formFilePath).delete()\n        val scenario = launcherRule.launchForResult<FormUriActivity>(\n            getBlankFormIntent(\n                project.uuid,\n                form.dbId\n            )\n        )\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(scenario, context.getString(org.odk.collect.strings.R.string.bad_uri))\n    }\n\n    @Test\n    fun `When uri represents a saved form that does not exist then display alert dialog`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val scenario =\n            launcherRule.launchForResult<FormUriActivity>(getSavedIntent(project.uuid, 1))\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(scenario, context.getString(org.odk.collect.strings.R.string.bad_uri))\n    }\n\n    @Test\n    fun `When attempting to edit a form with non existing instance file then display alert dialog and remove the instance from the database`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_SUBMISSION_FAILED)\n                .build()\n        )\n\n        File(instance.instanceFilePath).delete()\n        val scenario = launcherRule.launchForResult<FormUriActivity>(\n            getSavedIntent(\n                project.uuid,\n                instance.dbId\n            )\n        )\n        fakeScheduler.flush()\n        fakeScheduler.flush()\n\n        assertThat(instancesRepository.get(instance.dbId), equalTo(null))\n        assertErrorDialogAndClickCancelButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.instance_deleted_message)\n        )\n    }\n\n    @Test\n    fun `When attempting to edit a form with zero form definitions then display alert dialog with formId if version does not exist`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"20\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        val scenario = launcherRule.launchForResult<FormUriActivity>(\n            getSavedIntent(\n                project.uuid,\n                instance.dbId\n            )\n        )\n        fakeScheduler.flush()\n\n        val expectedMessage = context.getString(\n            org.odk.collect.strings.R.string.parent_form_not_present,\n            instance.formId\n        )\n\n        assertErrorDialogAndClickCancelButton(scenario, expectedMessage)\n    }\n\n    @Test\n    fun `When attempting to edit a form with zero form definitions then display alert dialog with formId and version if both exist`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        val scenario = launcherRule.launchForResult<FormUriActivity>(\n            getSavedIntent(\n                project.uuid,\n                instance.dbId\n            )\n        )\n        fakeScheduler.flush()\n\n        val expectedMessage = context.getString(\n            org.odk.collect.strings.R.string.parent_form_not_present,\n            \"${instance.formId}\\n${\n                context.getString(org.odk.collect.strings.R.string.version)\n            } ${instance.formVersion}\"\n        )\n\n        assertErrorDialogAndClickCancelButton(scenario, expectedMessage)\n    }\n\n    @Test\n    fun `When attempting to edit a form with multiple form definitions then display alert dialog`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath,\n                FormUtils.createXFormBody(\"1\", \"1\", \"Form 1\")\n            ).build()\n        )\n        formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath,\n                FormUtils.createXFormBody(\"1\", \"1\", \"Form 2\")\n            ).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        val scenario = launcherRule.launchForResult<FormUriActivity>(\n            getSavedIntent(\n                project.uuid,\n                instance.dbId\n            )\n        )\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.survey_multiple_forms_error)\n        )\n    }\n\n    @Test\n    fun `When attempting to edit a form with only one non-deleted form definitions then start form`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form1 = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath,\n                FormUtils.createXFormBody(\"1\", \"1\", \"Form 1\")\n            ).build()\n        )\n        val form2 = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath,\n                FormUtils.createXFormBody(\"1\", \"1\", \"Form 2\")\n            ).build()\n        )\n\n        formsRepository.softDelete(form1.dbId)\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        launcherRule.launchForResult<FormUriActivity>(\n            getSavedIntent(\n                project.uuid,\n                instance.dbId\n            )\n        )\n        fakeScheduler.flush()\n\n        assertStartSavedFormIntent(project.uuid, instance.dbId, true)\n    }\n\n    @Test\n    fun `When attempting to edit an encrypted form then display alert dialog`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath,\n                FormUtils.createXFormBody(\"1\", \"1\", \"Form 1\")\n            ).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .canEditWhenComplete(false)\n                .build()\n        )\n\n        val scenario = launcherRule.launchForResult<FormUriActivity>(\n            getSavedIntent(\n                project.uuid,\n                instance.dbId\n            )\n        )\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.encrypted_form)\n        )\n    }\n\n    @Test\n    fun `When attempting to edit an incomplete form with disabled editing then start form for view only`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, false)\n\n        formsRepository.save(\n            FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        launcherRule.launchForResult<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertStartSavedFormIntent(project.uuid, instance.dbId, false)\n    }\n\n    @Test\n    fun `When attempting to start a new form then there should be no form mode passed with the intent even if editing saved forms is disabled`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, false)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        launcherRule.launchForResult<FormUriActivity>(getBlankFormIntent(project.uuid, form.dbId))\n        fakeScheduler.flush()\n\n        assertStartBlankFormIntent(project.uuid, form.dbId)\n    }\n\n    @Test\n    fun `When attempting to edit a finalized form then start form for view only`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n\n        launcherRule.launchForResult<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertStartSavedFormIntent(project.uuid, instance.dbId, false)\n    }\n\n    @Test\n    fun `When attempting to edit a submitted form then start form for view only`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_SUBMITTED)\n                .build()\n        )\n\n        launcherRule.launchForResult<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertStartSavedFormIntent(project.uuid, instance.dbId, false)\n    }\n\n    @Test\n    fun `When attempting to edit a form that failed to submit then start form for view only`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_SUBMISSION_FAILED)\n                .build()\n        )\n\n        launcherRule.launchForResult<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertStartSavedFormIntent(project.uuid, instance.dbId, false)\n    }\n\n    @Test\n    fun `Form filling should not be started again after recreating the activity`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        val scenario =\n            launcherRule.launch<FormUriActivity>(getBlankFormIntent(project.uuid, form.dbId))\n        fakeScheduler.flush()\n        scenario.recreate()\n        fakeScheduler.flush()\n\n        Intents.intended(hasComponent(FormFillingActivity::class.java.name), Intents.times(1))\n    }\n\n    @Test\n    fun `Form filling should not be started again after recreating the activity before starting`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        val scenario =\n            launcherRule.launch<FormUriActivity>(getBlankFormIntent(project.uuid, form.dbId))\n        scenario.recreate()\n        fakeScheduler.flush()\n\n        Intents.intended(hasComponent(FormFillingActivity::class.java.name), Intents.times(1))\n    }\n\n    @Test\n    fun `When there is project id specified in uri that represents a blank form and it matches current project id then start form filling`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        launcherRule.launch<FormUriActivity>(getBlankFormIntent(project.uuid, form.dbId))\n        fakeScheduler.flush()\n\n        assertStartBlankFormIntent(project.uuid, form.dbId)\n    }\n\n    @Test\n    fun `When there is project id specified in uri that represents an incomplete form and it matches current project id then start form filling`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        launcherRule.launch<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertStartSavedFormIntent(project.uuid, instance.dbId, true)\n    }\n\n    @Test\n    fun `When there is project id specified in uri that represents an invalid form and it matches current project id then start form filling`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INVALID)\n                .build()\n        )\n\n        launcherRule.launch<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertStartSavedFormIntent(project.uuid, instance.dbId, true)\n    }\n\n    @Test\n    fun `When there is no project id specified in uri that represents a blank form and first available project id matches current project id then start form filling`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        launcherRule.launch<FormUriActivity>(getBlankFormIntent(null, form.dbId))\n        fakeScheduler.flush()\n\n        assertStartBlankFormIntent(null, form.dbId)\n    }\n\n    @Test\n    fun `When there is no project id specified in uri that represents a saved form and first available project id matches current project id then start form filling`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        launcherRule.launch<FormUriActivity>(getSavedIntent(null, instance.dbId))\n        fakeScheduler.flush()\n\n        assertStartSavedFormIntent(null, instance.dbId, true)\n    }\n\n    @Test\n    fun `The activity should be finished after clicking the back button when an error dialog is displayed`() {\n        val scenario = launcherRule.launchForResult(FormUriActivity::class.java)\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickBackButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.app_not_configured)\n        )\n    }\n\n    @Test\n    fun `If there is a savepoint, display a recovery dialog before starting a blank form`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n        val savepointFile = TempFiles.createTempFile()\n        val savepoint = Savepoint(form.dbId, null, savepointFile.absolutePath, TempFiles.createTempFile().absolutePath)\n        savepointsRepository.save(savepoint)\n\n        launcherRule.launch<FormUriActivity>(getBlankFormIntent(project.uuid, form.dbId))\n        fakeScheduler.flush()\n\n        assertNonCancelableSavepointRecoveryDialog(savepointFile)\n    }\n\n    @Test\n    fun `If there is a savepoint, display a recovery dialog before starting a saved form`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        val savepointFile = TempFiles.createTempFile()\n        val savepoint = Savepoint(form.dbId, instance.dbId, savepointFile.absolutePath, TempFiles.createTempFile().absolutePath)\n        savepointsRepository.save(savepoint)\n\n        launcherRule.launch<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertNonCancelableSavepointRecoveryDialog(savepointFile)\n    }\n\n    @Test\n    fun `If there is a savepoint for older version of the blank form, display a recovery dialog and start the old version of the blank form if a user accepts`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val formV1 = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        val formV2 = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"2\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        val savepointFile = TempFiles.createTempFile()\n        val savepoint = Savepoint(formV1.dbId, null, savepointFile.absolutePath, TempFiles.createTempFile().absolutePath)\n        savepointsRepository.save(savepoint)\n\n        launcherRule.launch<FormUriActivity>(getBlankFormIntent(project.uuid, formV2.dbId))\n        fakeScheduler.flush()\n\n        assertNonCancelableSavepointRecoveryDialog(savepointFile)\n        onView(withText(org.odk.collect.strings.R.string.recover)).perform(click())\n        assertStartBlankFormIntent(project.uuid, formV1.dbId)\n    }\n\n    @Test\n    fun `If there is a savepoint for older version of the blank form, display a recovery dialog and start the new version of the blank form if a user declines`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val formV1 = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        val formV2 = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"2\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        val savepointFile = TempFiles.createTempFile()\n        val savepoint = Savepoint(formV1.dbId, null, savepointFile.absolutePath, TempFiles.createTempFile().absolutePath)\n        savepointsRepository.save(savepoint)\n\n        launcherRule.launch<FormUriActivity>(getBlankFormIntent(project.uuid, formV2.dbId))\n        fakeScheduler.flush()\n\n        assertNonCancelableSavepointRecoveryDialog(savepointFile)\n        onView(withText(org.odk.collect.strings.R.string.do_not_recover)).perform(click())\n        fakeScheduler.flush()\n        assertStartBlankFormIntent(project.uuid, formV2.dbId)\n    }\n\n    @Test\n    fun `An existing savepoint for a blank form should be removed when a user declines`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n        val savepointFile = TempFiles.createTempFile()\n        val instanceDirFile = TempFiles.createTempDir()\n        val instanceFile = TempFiles.createTempFile(instanceDirFile)\n        val savepoint = Savepoint(form.dbId, null, savepointFile.absolutePath, instanceFile.absolutePath)\n        savepointsRepository.save(savepoint)\n\n        launcherRule.launch<FormUriActivity>(getBlankFormIntent(project.uuid, form.dbId))\n        fakeScheduler.flush()\n\n        assertNonCancelableSavepointRecoveryDialog(savepointFile)\n        onView(withText(org.odk.collect.strings.R.string.do_not_recover)).perform(click())\n        fakeScheduler.flush()\n        assertThat(savepointsRepository.getAll().isEmpty(), equalTo(true))\n        assertThat(instanceDirFile.exists(), equalTo(false))\n        assertThat(instanceFile.exists(), equalTo(false))\n    }\n\n    @Test\n    fun `An existing savepoint for a saved form should be removed when a user declines`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        val savepointFile = TempFiles.createTempFile()\n        val instanceDirFile = TempFiles.createTempDir()\n        val instanceFile = TempFiles.createTempFile(instanceDirFile)\n        val savepoint = Savepoint(form.dbId, instance.dbId, savepointFile.absolutePath, instanceFile.absolutePath)\n        savepointsRepository.save(savepoint)\n\n        launcherRule.launch<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertNonCancelableSavepointRecoveryDialog(savepointFile)\n        onView(withText(org.odk.collect.strings.R.string.do_not_recover)).perform(click())\n        fakeScheduler.flush()\n        assertThat(savepointsRepository.getAll().isEmpty(), equalTo(true))\n        assertThat(instanceDirFile.exists(), equalTo(true))\n        assertThat(instanceFile.exists(), equalTo(true))\n    }\n\n    @Test\n    fun `When attempting to start a new form that does not use entities and the forms database is locked then start form filling`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        changeLock.lock(\"blah\")\n        launcherRule.launchForResult<FormUriActivity>(getBlankFormIntent(project.uuid, form.dbId))\n        fakeScheduler.flush()\n\n        assertStartBlankFormIntent(project.uuid, form.dbId)\n    }\n\n    @Test\n    fun `When attempting to start a new form that uses entities and the forms database is locked then display alert dialog`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        val form = formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath,\n                usesEntities = true\n            ).build()\n        )\n\n        changeLock.lock(\"blah\")\n        val scenario = launcherRule.launchForResult<FormUriActivity>(getBlankFormIntent(project.uuid, form.dbId))\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.cannot_open_form_because_of_forms_update)\n        )\n    }\n\n    @Test\n    fun `When attempting to edit a form that does not use entities and the forms database is locked then start form filling`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        changeLock.lock(\"blah\")\n        launcherRule.launchForResult<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertStartSavedFormIntent(project.uuid, instance.dbId, true)\n    }\n\n    @Test\n    fun `When attempting to edit a form that uses entities and the forms database is locked then display alert dialog`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath,\n                usesEntities = true\n            ).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        changeLock.lock(\"blah\")\n        val scenario = launcherRule.launchForResult<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertErrorDialogAndClickCancelButton(\n            scenario,\n            context.getString(org.odk.collect.strings.R.string.cannot_open_form_because_of_forms_update)\n        )\n    }\n\n    @Test\n    fun `When attempting to view a non-editable form that uses entities and the forms database is locked then start form for view only`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath,\n                usesEntities = true\n            ).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n\n        changeLock.lock(\"blah\")\n        launcherRule.launchForResult<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertStartSavedFormIntent(project.uuid, instance.dbId, false)\n    }\n\n    @Test\n    fun `When attempting to view a non-editable form that uses entities then do not lock the forms database`() {\n        val project = Project.Saved(\"123\", \"First project\", \"A\", \"#cccccc\")\n        projectsRepository.save(project)\n        whenever(projectsDataService.requireCurrentProject()).thenReturn(project)\n\n        formsRepository.save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath,\n                usesEntities = true\n            ).build()\n        )\n\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n\n        launcherRule.launchForResult<FormUriActivity>(getSavedIntent(project.uuid, instance.dbId))\n        fakeScheduler.flush()\n\n        assertThat(changeLock.tryLock(\"formEntryScreen\"), equalTo(true))\n    }\n\n    private fun getBlankFormIntent(projectId: String?, dbId: Long) =\n        Intent(context, FormUriActivity::class.java).apply {\n            data = if (projectId == null) {\n                getFormsUriInOldFormatWithNoProjectId(dbId)\n            } else {\n                FormsContract.getUri(projectId, dbId)\n            }\n            putExtra(\"KEY_1\", \"Text\")\n        }\n\n    private fun getSavedIntent(projectId: String?, dbId: Long) =\n        Intent(context, FormUriActivity::class.java).apply {\n            data = if (projectId == null) {\n                getInstancesUriInOldFormatWithNoProjectId(dbId)\n            } else {\n                InstancesContract.getUri(projectId, dbId)\n            }\n            putExtra(\"KEY_1\", \"Text\")\n        }\n\n    private fun getFormsUriInOldFormatWithNoProjectId(dbId: Long): Uri {\n        val uri = FormsContract.getUri(\"\", dbId)\n        return Uri.Builder()\n            .scheme(uri.scheme)\n            .authority(uri.authority)\n            .path(uri.path)\n            .query(null)\n            .build()\n    }\n\n    private fun getInstancesUriInOldFormatWithNoProjectId(dbId: Long): Uri {\n        val uri = InstancesContract.getUri(\"\", dbId)\n        return Uri.Builder()\n            .scheme(uri.scheme)\n            .authority(uri.authority)\n            .path(uri.path)\n            .query(null)\n            .build()\n    }\n\n    private fun assertErrorDialogAndClickCancelButton(scenario: ActivityScenario<FormUriActivity>, message: String) {\n        onView(withText(org.odk.collect.strings.R.string.form_cannot_be_used)).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(withText(message)).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(withId(android.R.id.button1)).perform(click())\n\n        assertThat(scenario.result.resultCode, `is`(Activity.RESULT_CANCELED))\n    }\n\n    private fun assertErrorDialogAndClickBackButton(scenario: ActivityScenario<FormUriActivity>, message: String) {\n        onView(withText(message)).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(isRoot()).perform(pressBack())\n\n        scenario.onActivity {\n            assertThat(scenario.result.resultCode, `is`(Activity.RESULT_CANCELED))\n        }\n    }\n\n    private fun assertNonCancelableSavepointRecoveryDialog(savepointFile: File) {\n        onView(isRoot()).perform(pressBack())\n        onView(withText(org.odk.collect.strings.R.string.savepoint_recovery_dialog_title)).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(withText(SimpleDateFormat(context.getString(org.odk.collect.strings.R.string.savepoint_recovery_dialog_message), Locale.getDefault()).format(savepointFile.lastModified()))).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(withText(org.odk.collect.strings.R.string.recover)).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(withText(org.odk.collect.strings.R.string.do_not_recover)).inRoot(isDialog()).check(matches(isDisplayed()))\n    }\n\n    private fun assertStartBlankFormIntent(projectId: String?, dbId: Long) {\n        Intents.intended(hasComponent(FormFillingActivity::class.java.name))\n        if (projectId == null) {\n            Intents.intended(hasData(getFormsUriInOldFormatWithNoProjectId(dbId)))\n        } else {\n            Intents.intended(hasData(FormsContract.getUri(projectId, dbId)))\n        }\n        Intents.intended(not(hasExtraWithKey(FormOpeningMode.FORM_MODE_KEY)))\n        Intents.intended(hasExtra(\"KEY_1\", \"Text\"))\n    }\n\n    private fun assertStartSavedFormIntent(projectId: String?, dbId: Long, canBeEdited: Boolean) {\n        Intents.intended(hasComponent(FormFillingActivity::class.java.name))\n        if (projectId == null) {\n            Intents.intended(hasData(getInstancesUriInOldFormatWithNoProjectId(dbId)))\n        } else {\n            Intents.intended(hasData(InstancesContract.getUri(projectId, dbId)))\n        }\n        if (canBeEdited) {\n            Intents.intended(not(hasExtraWithKey(FormOpeningMode.FORM_MODE_KEY)))\n        } else {\n            Intents.intended(\n                hasExtra(\n                    FormOpeningMode.FORM_MODE_KEY,\n                    FormOpeningMode.VIEW_SENT\n                )\n            )\n        }\n        Intents.intended(hasExtra(\"KEY_1\", \"Text\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/external/FormsProviderTest.java",
    "content": "package org.odk.collect.android.external;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.isOneOf;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.DISPLAY_NAME;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_FILE_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.FORM_MEDIA_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JRCACHE_FILE_PATH;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_FORM_ID;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.JR_VERSION;\nimport static org.odk.collect.android.database.forms.DatabaseFormColumns.LANGUAGE;\nimport static org.odk.collect.android.external.FormsContract.CONTENT_ITEM_TYPE;\nimport static org.odk.collect.android.external.FormsContract.CONTENT_TYPE;\nimport static org.odk.collect.android.external.FormsContract.getUri;\n\nimport android.content.ContentResolver;\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.net.Uri;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.injection.config.AppDependencyComponent;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.utilities.FileUtils;\nimport org.odk.collect.android.utilities.FormsRepositoryProvider;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.formstest.FormUtils;\nimport org.odk.collect.projects.Project;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.File;\nimport java.io.IOException;\n\n@RunWith(AndroidJUnit4.class)\npublic class FormsProviderTest {\n\n    private ContentResolver contentResolver;\n    private StoragePathProvider storagePathProvider;\n    private String firstProjectId;\n    private AppDependencyComponent component;\n\n    @Before\n    public void setup() {\n        Context context = ApplicationProvider.getApplicationContext();\n        component = DaggerUtils.getComponent(context);\n        storagePathProvider = component.storagePathProvider();\n\n        firstProjectId = CollectHelpers.createDemoProject();\n        contentResolver = context.getContentResolver();\n    }\n\n    @Test\n    public void insert_doesNotInsertForms_andReturnsNull() {\n        String formId = \"external_app_form\";\n        String formVersion = \"1\";\n        String formName = \"External app form\";\n        File formFile = addFormToFormsDir(firstProjectId, formId, formVersion, formName);\n\n        ContentValues values = getContentValues(formId, formVersion, formName, formFile);\n        Uri uri = contentResolver.insert(getUri(firstProjectId), values);\n        assertThat(uri, equalTo(null));\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, null)) {\n            assertThat(cursor.getCount(), is(0));\n        }\n    }\n\n    @Test\n    public void update_doesNotUpdateForms_andReturns0() {\n        Uri formUri = addFormsToDirAndDb(firstProjectId, \"external_app_form\", \"External app form\", \"1\");\n\n        ContentValues contentValues = new ContentValues();\n        contentValues.put(LANGUAGE, \"English\");\n\n        int updateCount = contentResolver.update(formUri, contentValues, null, null);\n        assertThat(updateCount, is(0));\n        try (Cursor cursor = contentResolver.query(formUri, null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(LANGUAGE)), equalTo(null));\n        }\n    }\n\n    @Test\n    public void delete_deletesForm() {\n        Uri formUri = addFormsToDirAndDb(firstProjectId, \"form1\", \"Matching form\", \"1\");\n        contentResolver.delete(formUri, null, null);\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null)) {\n            assertThat(cursor.getCount(), is(0));\n        }\n    }\n\n    @Test\n    public void delete_deletesFiles() {\n        Uri formUri = addFormsToDirAndDb(firstProjectId, \"form1\", \"Matching form\", \"1\");\n        try (Cursor cursor = contentResolver.query(formUri, null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            String formFileName = cursor.getString(cursor.getColumnIndex(FORM_FILE_PATH));\n            File formFile = new File(getFormsDirPath(firstProjectId) + formFileName);\n            assertThat(formFile.exists(), is(true));\n\n            String mediaDirName = cursor.getString(cursor.getColumnIndex(FORM_MEDIA_PATH));\n            File mediaDir = new File(getFormsDirPath(firstProjectId) + mediaDirName);\n            assertThat(mediaDir.exists(), is(true));\n\n            String cacheFileName = cursor.getString(cursor.getColumnIndex(JRCACHE_FILE_PATH));\n            File cacheFile = new File(storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, firstProjectId) + File.separator + cacheFileName);\n            assertThat(cacheFile.exists(), is(true));\n\n            contentResolver.delete(formUri, null, null);\n\n            assertThat(formFile.exists(), is(false));\n            assertThat(mediaDir.exists(), is(false));\n            assertThat(cacheFile.exists(), is(false));\n        }\n    }\n\n    @Test\n    public void delete_withSelection_onlyDeletesMatchingForms() {\n        addFormsToDirAndDb(firstProjectId, \"form1\", \"Matching form\", \"1\");\n        addFormsToDirAndDb(firstProjectId, \"form2\", \"Not matching form\", \"1\");\n        addFormsToDirAndDb(firstProjectId, \"form3\", \"Matching form\", \"1\");\n\n        contentResolver.delete(getUri(firstProjectId), DISPLAY_NAME + \"=?\", new String[]{\"Matching form\"});\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(\"form2\"));\n        }\n    }\n\n    @Test\n    public void query_returnsTheExpectedNumberColumns() {\n        Uri uri = addFormsToDirAndDb(firstProjectId, \"external_app_form\", \"External app form\", \"1\");\n\n        try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) {\n            assertThat(cursor.getColumnCount(), is(19));\n        }\n    }\n\n    @Test\n    public void query_withProjection_onlyReturnsSpecifiedColumns() {\n        addFormsToDirAndDb(firstProjectId, \"external_app_form\", \"External app form\", \"1\");\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), new String[]{JR_FORM_ID, JR_VERSION}, null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getColumnCount(), is(2));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(\"external_app_form\"));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), is(\"1\"));\n        }\n    }\n\n    @Test\n    public void query_withSelection_onlyReturnsMatchingRows() {\n        addFormsToDirAndDb(firstProjectId, \"form1\", \"Matching form\", \"1\");\n        addFormsToDirAndDb(firstProjectId, \"form2\", \"Not a matching form\", \"1\");\n        addFormsToDirAndDb(firstProjectId, \"form3\", \"Matching form\", \"1\");\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, DISPLAY_NAME + \"=?\", new String[]{\"Matching form\"}, null)) {\n            assertThat(cursor.getCount(), is(2));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(isOneOf(\"form1\", \"form3\")));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(isOneOf(\"form1\", \"form3\")));\n        }\n    }\n\n    @Test\n    public void query_withSortOrder_returnsSortedResults() {\n        addFormsToDirAndDb(firstProjectId, \"formB\", \"Form B\", \"1\");\n        addFormsToDirAndDb(firstProjectId, \"formC\", \"Form C\", \"1\");\n        addFormsToDirAndDb(firstProjectId, \"formA\", \"Form A\", \"1\");\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, DISPLAY_NAME + \" ASC\")) {\n            assertThat(cursor.getCount(), is(3));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(\"formA\"));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(\"formB\"));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(\"formC\"));\n        }\n    }\n\n    @Test\n    public void query_withoutProjectId_usesFirstProject() {\n        addFormsToDirAndDb(firstProjectId, \"formA\", \"Form A\", \"1\");\n        CollectHelpers.createProject(new Project.New(\"Another Project\", \"A\", \"#ffffff\"));\n\n        Uri uriWithProject = getUri(\"blah\");\n        Uri uriWithoutProject = new Uri.Builder()\n                .scheme(uriWithProject.getScheme())\n                .authority(uriWithProject.getAuthority())\n                .path(uriWithProject.getPath())\n                .query(null)\n                .build();\n\n        try (Cursor cursor = contentResolver.query(uriWithoutProject, null, null, null, DISPLAY_NAME + \" ASC\")) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(\"formA\"));\n        }\n    }\n\n    @Test\n    public void getType_returnsFormAndAllFormsTypes() {\n        assertThat(contentResolver.getType(getUri(firstProjectId)), is(CONTENT_TYPE));\n        assertThat(contentResolver.getType(Uri.withAppendedPath(getUri(firstProjectId), \"1\")), is(CONTENT_ITEM_TYPE));\n    }\n\n    private Uri addFormsToDirAndDb(String projectId, String id, String name, String version) {\n        File formFile = addFormToFormsDir(projectId, id, version, name);\n\n        FormsRepositoryProvider formsRepositoryProvider = component.formsRepositoryProvider();\n        FormsRepository formsRepository = formsRepositoryProvider.create(projectId);\n        Form form = formsRepository.save(\n                new Form.Builder()\n                        .formId(id)\n                        .displayName(name)\n                        .version(version)\n                        .formFilePath(formFile.getAbsolutePath())\n                        .build()\n        );\n\n        return FormsContract.getUri(projectId, form.getDbId());\n    }\n\n    @NotNull\n    private ContentValues getContentValues(String formId, String formVersion, String formName, File formFile) {\n        ContentValues values = new ContentValues();\n        values.put(DISPLAY_NAME, formName);\n        values.put(JR_FORM_ID, formId);\n        values.put(JR_VERSION, formVersion);\n        values.put(FORM_FILE_PATH, formFile.getAbsolutePath());\n        return values;\n    }\n\n    /**\n     * It seems like newer OS versions (10 or higher) won't let other apps actually do this as they won't be\n     * able to access our external files dir (according to\n     * https://developer.android.com/training/data-storage/app-specific#external anyway.\n     **/\n    private File addFormToFormsDir(String projectId, String formId, String formVersion, String formName) {\n        File formFile = createFormFileInFormsDir(projectId, formId, formVersion, formName);\n        String md5Hash = Md5.getMd5Hash(formFile);\n\n        createExtraFormFiles(projectId, formFile, md5Hash);\n        return formFile;\n    }\n\n    private File createFormFileInFormsDir(String projectId, String formId, String formVersion, String formName) {\n        String xformBody = FormUtils.createXFormBody(formId, formVersion, formName);\n        String fileName = formId + \"-\" + formVersion + \"-\" + Math.random();\n        File formFile = new File(getFormsDirPath(projectId) + fileName + \".xml\");\n        FileUtils.write(formFile, xformBody.getBytes());\n        return formFile;\n    }\n\n    private void createExtraFormFiles(String projectId, File formFile, String md5Hash) {\n        // Create a media directory (and file) so we can check deletion etc - wouldn't always be there\n        String mediaDirPath = getFormsDirPath(projectId) + formFile.getName().substring(0, formFile.getName().lastIndexOf('.')) + \"-media\";\n        new File(mediaDirPath).mkdir();\n        try {\n            new File(mediaDirPath, \"blah.test\").createNewFile();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        // Create a cache file so we can check deletion etc - wouldn't always be there\n        try {\n            new File(storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, projectId) + File.separator + md5Hash + \".formdef\").createNewFile();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @NotNull\n    private String getFormsDirPath(String projectId) {\n        return storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, projectId) + File.separator;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/external/InstanceProviderTest.java",
    "content": "package org.odk.collect.android.external;\n\nimport android.content.ContentResolver;\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.net.Uri;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.database.forms.DatabaseFormColumns;\nimport org.odk.collect.android.database.instances.DatabaseInstanceColumns;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.formstest.InstanceUtils;\nimport org.odk.collect.projects.Project;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.contains;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.fail;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.DELETED_DATE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.DISPLAY_NAME;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.GEOMETRY;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.GEOMETRY_TYPE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.INSTANCE_FILE_PATH;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.JR_FORM_ID;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.JR_VERSION;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.LAST_STATUS_CHANGE_DATE;\nimport static org.odk.collect.android.database.instances.DatabaseInstanceColumns.STATUS;\nimport static org.odk.collect.android.external.InstancesContract.CONTENT_ITEM_TYPE;\nimport static org.odk.collect.android.external.InstancesContract.CONTENT_TYPE;\nimport static org.odk.collect.android.external.InstancesContract.getUri;\nimport static org.odk.collect.forms.instances.Instance.STATUS_COMPLETE;\nimport static org.odk.collect.forms.instances.Instance.STATUS_INCOMPLETE;\nimport static org.odk.collect.forms.instances.Instance.STATUS_SUBMITTED;\n\n@RunWith(AndroidJUnit4.class)\npublic class InstanceProviderTest {\n\n    private ContentResolver contentResolver;\n    private String firstProjectId;\n    private StoragePathProvider storagePathProvider;\n\n    @Before\n    public void setup() {\n        Context context = ApplicationProvider.getApplicationContext();\n        storagePathProvider = DaggerUtils.getComponent(context).storagePathProvider();\n\n        firstProjectId = CollectHelpers.createDemoProject();\n        contentResolver = context.getContentResolver();\n    }\n\n    @Test\n    public void insert_addsInstance() {\n        ContentValues values = getContentValues(\"/blah\", \"External app form\", \"external_app_form\", \"1\");\n        contentResolver.insert(getUri(firstProjectId), values);\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(INSTANCE_FILE_PATH)), is(\"/blah\"));\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(\"External app form\"));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_FORM_ID)), is(\"external_app_form\"));\n            assertThat(cursor.getString(cursor.getColumnIndex(JR_VERSION)), is(\"1\"));\n\n            assertThat(cursor.getLong(cursor.getColumnIndex(LAST_STATUS_CHANGE_DATE)), is(notNullValue()));\n            assertThat(cursor.getString(cursor.getColumnIndex(STATUS)), is(STATUS_INCOMPLETE));\n        }\n    }\n\n    @Test\n    public void insert_withSubmissionUri_throwsException() {\n        ContentValues values = getContentValues(\"/blah\", \"External app form\", \"external_app_form\", \"1\");\n        values.put(DatabaseInstanceColumns.SUBMISSION_URI, \"https://blah.com/submission\");\n\n        try {\n            contentResolver.insert(getUri(firstProjectId), values);\n            fail();\n        } catch (SecurityException e) {\n            // Expected\n        }\n    }\n\n    @Test\n    public void insert_returnsInstanceUri() {\n        ContentValues values = getContentValues(\"/blah\", \"External app form\", \"external_app_form\", \"1\");\n        Uri uri = contentResolver.insert(getUri(firstProjectId), values);\n\n        try (Cursor cursor = contentResolver.query(uri, null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n        }\n    }\n\n    @Test\n    public void update_doesNotUpdateInstance_andReturns0() {\n        ContentValues values = getContentValues(\"/blah\", \"External app form\", \"external_app_form\", \"1\");\n\n        long originalStatusChangeDate = 0L;\n        values.put(LAST_STATUS_CHANGE_DATE, originalStatusChangeDate);\n        Uri instanceUri = contentResolver.insert(getUri(firstProjectId), values);\n\n        ContentValues updateValues = new ContentValues();\n        updateValues.put(STATUS, STATUS_COMPLETE);\n\n        int updatedCount = contentResolver.update(instanceUri, updateValues, null, null);\n        assertThat(updatedCount, is(0));\n        try (Cursor cursor = contentResolver.query(instanceUri, null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(STATUS)), is(STATUS_INCOMPLETE));\n            assertThat(cursor.getLong(cursor.getColumnIndex(LAST_STATUS_CHANGE_DATE)), is(originalStatusChangeDate));\n            assertThat(cursor.getLong(cursor.getColumnIndex(LAST_STATUS_CHANGE_DATE)), is(0L));\n        }\n    }\n\n    @Test\n    public void delete_deletesInstance() {\n        Uri uri = addInstanceToDb(firstProjectId, \"/blah1\", \"Instance 1\");\n        contentResolver.delete(uri, null, null);\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, null)) {\n            assertThat(cursor.getCount(), is(0));\n        }\n    }\n\n    @Test\n    public void delete_deletesInstanceDir() {\n        File instanceFile = createInstanceDirAndFile(firstProjectId);\n\n        Uri uri = addInstanceToDb(firstProjectId, instanceFile.getAbsolutePath(), \"Instance 1\");\n        contentResolver.delete(uri, null, null);\n        assertThat(instanceFile.getParentFile().exists(), is(false));\n    }\n\n    @Test\n    public void delete_whenStatusIsSubmitted_deletesFilesButSoftDeletesInstance() {\n        File instanceFile = createInstanceDirAndFile(firstProjectId);\n\n        ContentValues values = getContentValues(instanceFile.getAbsolutePath(), \"Instance 1\", \"external_app_form\", \"1\");\n        values.put(STATUS, STATUS_SUBMITTED);\n\n        Uri uri = contentResolver.insert(getUri(firstProjectId), values);\n\n        contentResolver.delete(uri, null, null);\n        assertThat(instanceFile.getParentFile().exists(), is(false));\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getLong(cursor.getColumnIndex(DELETED_DATE)), is(notNullValue()));\n        }\n    }\n\n    @Test\n    public void delete_whenStatusIsSubmitted_clearsGeometryFields() {\n        File instanceFile = createInstanceDirAndFile(firstProjectId);\n\n        ContentValues values = getContentValues(instanceFile.getAbsolutePath(), \"Instance 1\", \"external_app_form\", \"1\");\n        values.put(STATUS, STATUS_SUBMITTED);\n        values.put(GEOMETRY, \"something\");\n        values.put(GEOMETRY_TYPE, \"something else\");\n\n        Uri uri = contentResolver.insert(getUri(firstProjectId), values);\n\n        contentResolver.delete(uri, null, null);\n        assertThat(instanceFile.getParentFile().exists(), is(false));\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY)), is(nullValue()));\n            assertThat(cursor.getString(cursor.getColumnIndex(GEOMETRY_TYPE)), is(nullValue()));\n        }\n    }\n\n    /**\n     * It's not clear when this is used. A hypothetical might be updating an instance but wanting\n     * that to be a no-op if it has already been soft deleted.\n     */\n    @Test\n    public void delete_withInstanceUri_andSelection_doesNotDeleteInstanceThatDoesNotMatchSelection() {\n        Uri uri = addInstanceToDb(firstProjectId, \"/blah1\", \"Instance 1\");\n        addInstanceToDb(firstProjectId, \"/blah2\", \"Instance 2\");\n\n        contentResolver.delete(uri, DISPLAY_NAME + \"=?\", new String[]{\"Instance 2\"});\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, null)) {\n            assertThat(cursor.getCount(), is(2));\n        }\n    }\n\n    @Test\n    public void delete_withSelection_deletesMatchingInstances() {\n        addInstanceToDb(firstProjectId, \"/blah1\", \"Instance 1\");\n        addInstanceToDb(firstProjectId, \"/blah2\", \"Instance 2\");\n\n        contentResolver.delete(getUri(firstProjectId), DISPLAY_NAME + \"=?\", new String[]{\"Instance 2\"});\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(\"Instance 1\"));\n        }\n    }\n\n    @Test\n    public void query_returnsTheExpectedNumberColumns() {\n        Uri uri = addInstanceToDb(firstProjectId, \"/blah1\", \"Instance 1\");\n\n        try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) {\n            assertThat(cursor.getColumnCount(), is(16));\n        }\n    }\n\n    @Test\n    public void query_withProjection_onlyReturnsSpecifiedColumns() {\n        addInstanceToDb(firstProjectId, \"/blah1\", \"Instance 1\");\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), new String[]{INSTANCE_FILE_PATH, DISPLAY_NAME}, null, null, null)) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getColumnCount(), is(2));\n            assertThat(cursor.getString(cursor.getColumnIndex(INSTANCE_FILE_PATH)), is(\"/blah1\"));\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(\"Instance 1\"));\n        }\n    }\n\n    @Test\n    public void query_withSelection_onlyReturnsMatchingRows() {\n        addInstanceToDb(firstProjectId, \"/blah1\", \"Matching instance\");\n        addInstanceToDb(firstProjectId, \"/blah2\", \"Not a matching instance\");\n        addInstanceToDb(firstProjectId, \"/blah3\", \"Matching instance\");\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, DISPLAY_NAME + \"=?\", new String[]{\"Matching instance\"}, null)) {\n            assertThat(cursor.getCount(), is(2));\n\n            List<String> paths = new ArrayList<>();\n            while (cursor.moveToNext()) {\n                paths.add(cursor.getString(cursor.getColumnIndex(INSTANCE_FILE_PATH)));\n            }\n\n            assertThat(paths, contains(\"/blah1\", \"/blah3\"));\n        }\n    }\n\n    @Test\n    public void query_withSortOrder_returnsSortedResults() {\n        addInstanceToDb(firstProjectId, \"/blah3\", \"Instance C\");\n        addInstanceToDb(firstProjectId, \"/blah2\", \"Instance B\");\n        addInstanceToDb(firstProjectId, \"/blah1\", \"Instance A\");\n\n        try (Cursor cursor = contentResolver.query(getUri(firstProjectId), null, null, null, DISPLAY_NAME + \" ASC\")) {\n            assertThat(cursor.getCount(), is(3));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(\"Instance A\"));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(\"Instance B\"));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(\"Instance C\"));\n        }\n    }\n\n    @Test\n    public void query_withoutProjectId_usesFirstProject() {\n        addInstanceToDb(firstProjectId, \"/blah1\", \"Instance A\");\n        CollectHelpers.createProject(new Project.New(\"Another Project\", \"A\", \"#ffffff\"));\n\n        Uri uriWithProject = InstancesContract.getUri(\"blah\");\n        Uri uriWithoutProject = new Uri.Builder()\n                .scheme(uriWithProject.getScheme())\n                .authority(uriWithProject.getAuthority())\n                .path(uriWithProject.getPath())\n                .query(null)\n                .build();\n\n        try (Cursor cursor = contentResolver.query(uriWithoutProject, null, null, null, DatabaseFormColumns.DISPLAY_NAME + \" ASC\")) {\n            assertThat(cursor.getCount(), is(1));\n\n            cursor.moveToNext();\n            assertThat(cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), is(\"Instance A\"));\n        }\n    }\n\n    @Test\n    public void getType_returnsInstanceAndAllInstanceTypes() {\n        assertThat(contentResolver.getType(getUri(firstProjectId)), is(CONTENT_TYPE));\n        assertThat(contentResolver.getType(Uri.withAppendedPath(getUri(firstProjectId), \"1\")), is(CONTENT_ITEM_TYPE));\n    }\n\n    private File createInstanceDirAndFile(String projectId) {\n        return InstanceUtils.createInstanceDirAndFile(storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, projectId));\n    }\n\n    private Uri addInstanceToDb(String projectId, String instanceFilePath, String displayName) {\n        ContentValues values = getContentValues(instanceFilePath, displayName, \"external_app_form\", \"1\");\n        return contentResolver.insert(getUri(projectId), values);\n    }\n\n    private ContentValues getContentValues(String instanceFilePath, String displayName, String formId, String formVersion) {\n        ContentValues values = new ContentValues();\n        values.put(INSTANCE_FILE_PATH, instanceFilePath);\n        values.put(DISPLAY_NAME, displayName);\n        values.put(JR_FORM_ID, formId);\n        values.put(JR_VERSION, formVersion);\n        return values;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/fakes/FakePermissionsProvider.kt",
    "content": "package org.odk.collect.android.fakes\n\nimport android.app.Activity\nimport androidx.test.platform.app.InstrumentationRegistry\nimport org.odk.collect.permissions.ContextCompatPermissionChecker\nimport org.odk.collect.permissions.PermissionListener\nimport org.odk.collect.permissions.PermissionsProvider\n\n/**\n * Mocked implementation of [PermissionsProvider].\n * The runtime permissions can be stubbed for unit testing\n *\n * @author Shobhit Agarwal\n */\nclass FakePermissionsProvider :\n    PermissionsProvider(ContextCompatPermissionChecker(InstrumentationRegistry.getInstrumentation().targetContext)) {\n    private var isPermissionGranted = false\n\n    val requestedPermissions = mutableListOf<String>()\n\n    override fun requestPermissions(\n        activity: Activity,\n        listener: PermissionListener,\n        vararg permissions: String\n    ) {\n        requestedPermissions.addAll(permissions)\n\n        if (isPermissionGranted) {\n            listener.granted()\n        } else {\n            listener.denied()\n        }\n    }\n\n    fun setPermissionGranted(permissionGranted: Boolean) {\n        isPermissionGranted = permissionGranted\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/AppStateFormSessionRepositoryTest.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.runner.RunWith\n\n@RunWith(AndroidJUnit4::class)\nclass AppStateFormSessionRepositoryTest : FormSessionRepositoryTest() {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    private val appStateFormSessionRepository =\n        AppStateFormSessionRepository(ApplicationProvider.getApplicationContext())\n    override val formSessionRepository: FormSessionRepository\n        get() = appStateFormSessionRepository\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/AudioVideoImageTextLabelTest.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport static android.view.View.VISIBLE;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.not;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.support.CollectHelpers.createThemedActivity;\n\nimport android.view.View;\nimport android.widget.CheckBox;\nimport android.widget.RadioButton;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.testshared.FakeAudioPlayer;\nimport org.odk.collect.audioclips.Clip;\nimport org.odk.collect.imageloader.ImageLoader;\n\nimport java.io.File;\n\n@RunWith(AndroidJUnit4.class)\npublic class AudioVideoImageTextLabelTest {\n\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    private FakeAudioPlayer audioPlayer = new FakeAudioPlayer();\n\n    private WidgetTestActivity activity;\n\n    @Before\n    public void setup() {\n        activity = createThemedActivity(WidgetTestActivity.class);\n    }\n\n    @Test\n    public void withNullText_hidesTextLabel() {\n        AudioVideoImageTextLabel audioVideoImageTextLabel = new AudioVideoImageTextLabel(activity);\n        audioVideoImageTextLabel.setText(null, false, 16);\n\n        assertThat(audioVideoImageTextLabel.getLabelTextView().getVisibility(), equalTo(View.GONE));\n    }\n\n    @Test\n    public void withBlankText_hidesTextLabel() {\n        AudioVideoImageTextLabel audioVideoImageTextLabel = new AudioVideoImageTextLabel(activity);\n        audioVideoImageTextLabel.setText(\"\", false, 16);\n\n        assertThat(audioVideoImageTextLabel.getLabelTextView().getVisibility(), equalTo(View.GONE));\n    }\n\n    @Test\n    public void withText_andAudio_showsTextAndAudioButton()  {\n        AudioVideoImageTextLabel label = new AudioVideoImageTextLabel(activity);\n        label.setText(\"blah\", false, 16);\n        label.setAudio(\"file://audio.mp3\", audioPlayer);\n\n        assertThat(label.getLabelTextView().getVisibility(), equalTo(VISIBLE));\n        assertThat(label.getLabelTextView().getText().toString(), equalTo(\"blah\"));\n        assertThat(label.findViewById(R.id.audioButton).getVisibility(), equalTo(VISIBLE));\n    }\n\n    @Test\n    public void withText_andAudio_playingAudio_highlightsText() {\n        AudioVideoImageTextLabel audioVideoImageTextLabel = new AudioVideoImageTextLabel(activity);\n        audioVideoImageTextLabel.setTag(\"blah\");\n        audioVideoImageTextLabel.setText(\"blah\", false, 16);\n        audioVideoImageTextLabel.setAudio(\"file://audio.mp3\", audioPlayer);\n\n        int originalTextColor = audioVideoImageTextLabel.getLabelTextView().getCurrentTextColor();\n\n        audioPlayer.play(new Clip(\"blah\", \"file://audio.mp3\"));\n        int textColor = audioVideoImageTextLabel.getLabelTextView().getCurrentTextColor();\n        assertThat(textColor, not(equalTo(originalTextColor)));\n\n        audioPlayer.stop();\n        textColor = audioVideoImageTextLabel.getLabelTextView().getCurrentTextColor();\n        assertThat(textColor, equalTo(originalTextColor));\n    }\n\n    @Test\n    public void withAudio_pressingStopOnAudioButton_stopsAudio() {\n        AudioVideoImageTextLabel audioVideoImageTextLabel = new AudioVideoImageTextLabel(activity);\n        audioVideoImageTextLabel.setTag(\"blah\");\n        audioVideoImageTextLabel.setAudio(\"file://audio.mp3\", audioPlayer);\n\n        audioPlayer.play(new Clip(\"blah\", \"file://audio.mp3\"));\n        audioVideoImageTextLabel.findViewById(R.id.audioButton).performClick();\n        assertThat(audioPlayer.getCurrentClip(), equalTo(null));\n    }\n\n    @Test\n    public void bothClickingLabelAndImageView_shouldSelectOptionInSelectOneMode() {\n        File imageFile = mock(File.class);\n        when(imageFile.exists()).thenReturn(true);\n\n        AudioVideoImageTextLabel audioVideoImageTextLabel = new AudioVideoImageTextLabel(activity);\n        audioVideoImageTextLabel.setImage(imageFile, mock(ImageLoader.class));\n        audioVideoImageTextLabel.setTextView(new RadioButton(activity));\n\n        assertThat(((RadioButton) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(false));\n\n        // click on label\n        audioVideoImageTextLabel.getLabelTextView().performClick();\n        assertThat(((RadioButton) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(true));\n\n        // clear answer\n        ((RadioButton) audioVideoImageTextLabel.getLabelTextView()).setChecked(false);\n        assertThat(((RadioButton) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(false));\n\n        // click on image\n        audioVideoImageTextLabel.getImageView().performClick();\n        assertThat(((RadioButton) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(true));\n    }\n\n    @Test\n    public void bothClickingLabelAndImageView_shouldSelectOptionInSelectMultiMode() {\n        File imageFile = mock(File.class);\n        when(imageFile.exists()).thenReturn(true);\n\n        AudioVideoImageTextLabel audioVideoImageTextLabel = new AudioVideoImageTextLabel(activity);\n        audioVideoImageTextLabel.setImage(imageFile, mock(ImageLoader.class));\n        audioVideoImageTextLabel.setTextView(new CheckBox(activity));\n\n        assertThat(((CheckBox) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(false));\n\n        // click on label\n        audioVideoImageTextLabel.getLabelTextView().performClick();\n        assertThat(((CheckBox) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(true));\n\n        // click on image\n        audioVideoImageTextLabel.getImageView().performClick();\n        assertThat(((CheckBox) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(false));\n    }\n\n    @Test\n    public void whenClickOneElementTwiceInSelectOneMode_shouldThatElementRemainSelected() {\n        File imageFile = mock(File.class);\n        when(imageFile.exists()).thenReturn(true);\n\n        AudioVideoImageTextLabel audioVideoImageTextLabel = new AudioVideoImageTextLabel(activity);\n        audioVideoImageTextLabel.setImage(imageFile, mock(ImageLoader.class));\n        audioVideoImageTextLabel.setTextView(new RadioButton(activity));\n\n        assertThat(((RadioButton) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(false));\n\n        // click on label\n        audioVideoImageTextLabel.getLabelTextView().performClick();\n        assertThat(((RadioButton) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(true));\n\n        // click on label again\n        audioVideoImageTextLabel.getLabelTextView().performClick();\n        assertThat(((RadioButton) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(true));\n\n        // click on image\n        audioVideoImageTextLabel.getImageView().performClick();\n        assertThat(((RadioButton) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(true));\n\n        // click on image again\n        audioVideoImageTextLabel.getImageView().performClick();\n        assertThat(((RadioButton) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(true));\n    }\n\n    @Test\n    public void whenClickOneElementTwiceInSelectMultiMode_shouldThatElementBeUnSelected() {\n        File imageFile = mock(File.class);\n        when(imageFile.exists()).thenReturn(true);\n\n        AudioVideoImageTextLabel audioVideoImageTextLabel = new AudioVideoImageTextLabel(activity);\n        audioVideoImageTextLabel.setImage(imageFile, mock(ImageLoader.class));\n        audioVideoImageTextLabel.setTextView(new CheckBox(activity));\n\n        assertThat(((CheckBox) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(false));\n\n        // click on label\n        audioVideoImageTextLabel.getLabelTextView().performClick();\n        assertThat(((CheckBox) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(true));\n\n        // click on label again\n        audioVideoImageTextLabel.getLabelTextView().performClick();\n        assertThat(((CheckBox) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(false));\n\n        // click on image\n        audioVideoImageTextLabel.getImageView().performClick();\n        assertThat(((CheckBox) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(true));\n\n        // click on image again\n        audioVideoImageTextLabel.getImageView().performClick();\n        assertThat(((CheckBox) audioVideoImageTextLabel.getLabelTextView()).isChecked(), is(false));\n    }\n\n    @Test\n    public void whenImageFileDoesNotExist_ShouldAnAppropriateMessageBeDisplayed() {\n        File imageFile = new File(\"file://image.png\");\n\n        AudioVideoImageTextLabel audioVideoImageTextLabel = new AudioVideoImageTextLabel(activity);\n        audioVideoImageTextLabel.setImage(imageFile, mock(ImageLoader.class));\n\n        assertThat(audioVideoImageTextLabel.getMissingImage().getVisibility(), is(VISIBLE));\n        assertThat(audioVideoImageTextLabel.getMissingImage().getText().toString(), is(\"File: file:/image.png is missing.\"));\n    }\n\n    @Test\n    public void whenVideoFileClicked_ShouldMediaUtilsBeCalled() {\n        MediaUtils mediaUtils = mock(MediaUtils.class);\n\n        File videoFile = mock(File.class);\n        when(videoFile.exists()).thenReturn(true);\n\n        AudioVideoImageTextLabel audioVideoImageTextLabel = new AudioVideoImageTextLabel(activity);\n        audioVideoImageTextLabel.setVideo(videoFile);\n        audioVideoImageTextLabel.setMediaUtils(mediaUtils);\n        audioVideoImageTextLabel.getVideoButton().performClick();\n\n        verify(mediaUtils).openFile(activity, videoFile, \"video/*\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/AudioVideoImageTextLabelVisibilityTest.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.view.View\nimport android.widget.TextView\nimport androidx.test.core.app.ApplicationProvider\nimport org.javarosa.core.reference.InvalidReferenceException\nimport org.javarosa.core.reference.ReferenceManager\nimport org.junit.Assert.assertEquals\nimport org.junit.Assert.assertNotNull\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel\nimport org.odk.collect.android.logic.FileReference\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.WidgetTestActivity\nimport org.robolectric.ParameterizedRobolectricTestRunner\nimport java.io.File\nimport java.util.Random\n\n@RunWith(ParameterizedRobolectricTestRunner::class)\nclass AudioVideoImageTextLabelVisibilityTest(\n    private val imageURI: String?,\n    private val audioURI: String?,\n    private val videoURI: String?\n) {\n\n    private lateinit var referenceManager: ReferenceManager\n    private lateinit var reference: FileReference\n    private lateinit var audioVideoImageTextLabel: AudioVideoImageTextLabel\n    private lateinit var audioButton: View\n    private lateinit var videoButton: View\n    private lateinit var imageView: View\n    private lateinit var missingImage: View\n    private lateinit var textView: TextView\n    private var isReferenceManagerStubbed = false\n    private var imageFileExists = false\n\n    @Before\n    fun setUp() {\n        reference = mock()\n        referenceManager = mock()\n        textView = TextView(ApplicationProvider.getApplicationContext())\n        val activity = CollectHelpers.createThemedActivity(\n            WidgetTestActivity::class.java\n        )\n        audioVideoImageTextLabel = AudioVideoImageTextLabel(activity)\n        audioButton = audioVideoImageTextLabel.findViewById(R.id.audioButton)\n        videoButton = audioVideoImageTextLabel.findViewById(R.id.videoButton)\n        imageView = audioVideoImageTextLabel.findViewById(R.id.imageView)\n        missingImage = audioVideoImageTextLabel.findViewById(R.id.missingImage)\n\n        /*\n         * Stub reference manager randomly to account for both illegal URI and proper URI while\n         * attempting to load image view\n         */\n        if (Random().nextBoolean()) {\n            stubReferenceManager()\n            imageFileExists = true\n        } else {\n            whenever(referenceManager.deriveReference(RANDOM_URI)).thenThrow(\n                InvalidReferenceException::class.java\n            )\n        }\n    }\n\n    @Test\n    fun viewShouldBecomeVisibleIfUriPresent() {\n        assertNotNull(audioVideoImageTextLabel)\n        assertEquals(View.VISIBLE, audioVideoImageTextLabel.visibility)\n        assertVisibility(View.GONE, audioButton, videoButton, imageView, missingImage)\n        val imageFile: File\n        val videoFile: File\n        audioVideoImageTextLabel.setTextView(textView)\n        if (imageURI != null && isReferenceManagerStubbed) {\n            imageFile = mock()\n            whenever(imageFile.exists()).thenReturn(imageFileExists)\n            audioVideoImageTextLabel.setImage(imageFile, mock())\n        }\n        if (videoURI != null && isReferenceManagerStubbed) {\n            videoFile = mock()\n            audioVideoImageTextLabel.setVideo(videoFile)\n        }\n        if (audioURI != null && isReferenceManagerStubbed) {\n            audioVideoImageTextLabel.setAudio(audioURI, mock())\n        }\n\n        // we do not check for the validity of the URIs for the audio and video while loading MediaLayout\n        assertVisibility(\n            if (audioURI == null || !isReferenceManagerStubbed) View.GONE else View.VISIBLE,\n            audioButton\n        )\n        assertVisibility(\n            if (videoURI == null || !isReferenceManagerStubbed) View.GONE else View.VISIBLE,\n            videoButton\n        )\n        if (imageURI == null || !isReferenceManagerStubbed) {\n            // either the URI wasn't provided or it encountered InvalidReferenceException\n            assertVisibility(View.GONE, imageView, missingImage)\n        } else if (imageFileExists) {\n            assertVisibility(View.VISIBLE, imageView)\n            assertVisibility(View.GONE, missingImage)\n        } else {\n            assertVisibility(View.GONE, imageView)\n            assertVisibility(View.VISIBLE, missingImage)\n        }\n    }\n\n    /*\n     * Stubbing {@link ReferenceManager} to return random file name in order to prevent\n     * {@link InvalidReferenceException}\n     */\n    private fun stubReferenceManager() {\n        isReferenceManagerStubbed = true\n        whenever(referenceManager.deriveReference(RANDOM_URI)).doReturn(reference)\n        whenever(reference.localURI).doReturn(RANDOM_URI)\n    }\n\n    /**\n     * @param visibility Expected visibility\n     * @param views      Views whose actual visibility is to be asserted\n     */\n    private fun assertVisibility(visibility: Int, vararg views: View) {\n        for (view in views) {\n            assertEquals(visibility, view.visibility)\n        }\n    }\n\n    companion object {\n        private const val RANDOM_URI = \"randomMediaURI\"\n\n        @ParameterizedRobolectricTestRunner.Parameters\n        @JvmStatic\n        fun data() = listOf<Array<String?>>(\n            arrayOf(null, null, null),\n            arrayOf(RANDOM_URI, null, null),\n            arrayOf(null, RANDOM_URI, null),\n            arrayOf(null, null, RANDOM_URI),\n            arrayOf(RANDOM_URI, RANDOM_URI, null),\n            arrayOf(RANDOM_URI, null, RANDOM_URI),\n            arrayOf(null, RANDOM_URI, RANDOM_URI),\n            arrayOf(RANDOM_URI, RANDOM_URI, RANDOM_URI)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/BackgroundAudioPermissionDialogFragmentTest.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\n\nimport android.content.DialogInterface;\nimport android.widget.Button;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.fragment.app.testing.FragmentScenario;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.lifecycle.viewmodel.CreationExtras;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.fakes.FakePermissionsProvider;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder;\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule;\nimport org.odk.collect.permissions.PermissionsChecker;\nimport org.odk.collect.permissions.PermissionsProvider;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\n@RunWith(AndroidJUnit4.class)\npublic class BackgroundAudioPermissionDialogFragmentTest {\n\n    private BackgroundAudioViewModel backgroundAudioViewModel;\n    private FakePermissionsProvider fakePermissionsProvider;\n\n    private final ViewModelProvider.Factory viewModelFactory = new ViewModelProvider.Factory() {\n        @NonNull\n        @Override\n        public <T extends ViewModel> T create(@NonNull Class<T> modelClass, @NonNull CreationExtras extras) {\n            return (T) backgroundAudioViewModel;\n        }\n    };\n\n    @Rule\n    public FragmentScenarioLauncherRule launcherRule = new FragmentScenarioLauncherRule(\n            new FragmentFactoryBuilder()\n                    .forClass(BackgroundAudioPermissionDialogFragment.class, () -> new BackgroundAudioPermissionDialogFragment(viewModelFactory))\n                    .build()\n    );\n\n    @Before\n    public void setup() {\n        backgroundAudioViewModel = mock(BackgroundAudioViewModel.class);\n        fakePermissionsProvider = new FakePermissionsProvider();\n\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public PermissionsProvider providesPermissionsProvider(PermissionsChecker permissionsChecker) {\n                return fakePermissionsProvider;\n            }\n        });\n    }\n\n    @Test\n    public void isNotCancellable() {\n        FragmentScenario<BackgroundAudioPermissionDialogFragment> scenario = launcherRule.launch(BackgroundAudioPermissionDialogFragment.class);\n        scenario.onFragment(f -> {\n            assertThat(f.isCancelable(), is(false));\n        });\n    }\n\n    @Test\n    public void clickingOk_andGrantingPermissions_callsGrantPermission() {\n        FragmentScenario<BackgroundAudioPermissionDialogFragment> scenario = launcherRule.launch(BackgroundAudioPermissionDialogFragment.class);\n        scenario.onFragment(f -> {\n            AlertDialog dialog = (AlertDialog) f.getDialog();\n\n            Button button = dialog.getButton(DialogInterface.BUTTON_POSITIVE);\n            assertThat(button.getText(), is(f.getString(org.odk.collect.strings.R.string.ok)));\n\n            fakePermissionsProvider.setPermissionGranted(true);\n\n            button.performClick();\n            RobolectricHelpers.runLooper();\n            verify(backgroundAudioViewModel).grantAudioPermission();\n        });\n    }\n\n    @Test\n    public void clickingOk_andGrantingPermissions_whenGrantPermissionsThrowsIllegalStateException_finishesActivity() {\n        doThrow(IllegalStateException.class).when(backgroundAudioViewModel).grantAudioPermission();\n\n        FragmentScenario<BackgroundAudioPermissionDialogFragment> scenario = launcherRule.launch(BackgroundAudioPermissionDialogFragment.class);\n        scenario.onFragment(f -> {\n            FragmentActivity activity = f.getActivity(); // Need to grab this here as `getActivity()` will return null later\n\n            AlertDialog dialog = (AlertDialog) f.getDialog();\n            Button button = dialog.getButton(DialogInterface.BUTTON_POSITIVE);\n\n            fakePermissionsProvider.setPermissionGranted(true);\n\n            button.performClick();\n            RobolectricHelpers.runLooper();\n            assertThat(activity.isFinishing(), is(true));\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/BackgroundAudioViewModelTest.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport android.Manifest;\n\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.TestSettingsProvider;\nimport org.odk.collect.android.formentry.audit.AuditEvent;\nimport org.odk.collect.android.formentry.audit.AuditEventLogger;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.audiorecorder.recorder.Output;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.permissions.PermissionsChecker;\nimport org.odk.collect.shared.settings.Settings;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport java.util.HashSet;\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\n\n@RunWith(AndroidJUnit4.class)\n@SuppressWarnings(\"PMD.DoubleBraceInitialization\")\npublic class BackgroundAudioViewModelTest {\n\n    private final PermissionsChecker permissionsChecker = mock(PermissionsChecker.class);\n    private final FakeRecordAudioActionRegistry recordAudioActionRegistry = new FakeRecordAudioActionRegistry();\n    private final AudioRecorder audioRecorder = mock(AudioRecorder.class);\n    private final FormController formController = mock(FormController.class);\n    private final Form form = mock(Form.class);\n    private final AuditEventLogger auditEventLogger = mock(AuditEventLogger.class);\n\n    private BackgroundAudioViewModel viewModel;\n    private Supplier<Long> clock;\n    private final MutableLiveData<FormSession> formSession = new MutableLiveData<>(new FormSession(formController, form));\n\n    @Before\n    public void setup() {\n        clock = mock(Supplier.class);\n\n        Settings generalSettings = TestSettingsProvider.getUnprotectedSettings();\n        generalSettings.clear();\n\n        when(formController.getAuditEventLogger()).thenReturn(auditEventLogger);\n\n        viewModel = new BackgroundAudioViewModel(audioRecorder, generalSettings, recordAudioActionRegistry, permissionsChecker, clock, formSession);\n    }\n\n    @Test\n    public void whenRecordAudioActionIsTriggered_whenQualityIsVoiceOnly_startsAMRRecording() {\n        when(permissionsChecker.isPermissionGranted(Manifest.permission.RECORD_AUDIO)).thenReturn(true);\n\n        TreeReference treeReference = new TreeReference();\n        recordAudioActionRegistry.listener.accept(treeReference, \"voice-only\");\n        RobolectricHelpers.runLooper();\n\n        verify(audioRecorder).start(new HashSet<TreeReference>() {\n            {\n                add(treeReference);\n            }\n        }, Output.AMR);\n    }\n\n    @Test\n    public void whenRecordAudioActionIsTriggered_whenQualityIsLow_startsAACLowRecording() {\n        when(permissionsChecker.isPermissionGranted(Manifest.permission.RECORD_AUDIO)).thenReturn(true);\n\n        TreeReference treeReference = new TreeReference();\n        recordAudioActionRegistry.listener.accept(treeReference, \"low\");\n        RobolectricHelpers.runLooper();\n\n        verify(audioRecorder).start(new HashSet<TreeReference>() {\n            {\n                add(treeReference);\n            }\n        }, Output.AAC_LOW);\n    }\n\n    @Test\n    public void whenRecordAudioActionIsTriggered_whenQualityIsMissings_startsAMRRecording() {\n        when(permissionsChecker.isPermissionGranted(Manifest.permission.RECORD_AUDIO)).thenReturn(true);\n\n        TreeReference treeReference = new TreeReference();\n        recordAudioActionRegistry.listener.accept(treeReference, null);\n        RobolectricHelpers.runLooper();\n\n        verify(audioRecorder).start(new HashSet<TreeReference>() {\n            {\n                add(treeReference);\n            }\n        }, Output.AMR);\n    }\n\n    @Test\n    public void onCleared_unregistersRecordAudioActionListener() {\n        viewModel.onCleared();\n        assertThat(recordAudioActionRegistry.listener, is(nullValue()));\n    }\n\n    @Test\n    public void grantAudioPermission_startsBackgroundRecording() {\n        when(permissionsChecker.isPermissionGranted(Manifest.permission.RECORD_AUDIO)).thenReturn(false);\n\n        TreeReference treeReference1 = new TreeReference();\n        TreeReference treeReference2 = new TreeReference();\n        recordAudioActionRegistry.listener.accept(treeReference1, \"low\");\n        recordAudioActionRegistry.listener.accept(treeReference2, \"low\");\n        RobolectricHelpers.runLooper();\n\n        viewModel.grantAudioPermission();\n        verify(audioRecorder).start(new HashSet<TreeReference>() {\n            {\n                add(treeReference1);\n                add(treeReference2);\n            }\n        }, Output.AAC_LOW);\n    }\n\n    @Test\n    public void grantAudioPermission_whenActionsHaveDifferentQualities_usesFirstQuality() {\n        when(permissionsChecker.isPermissionGranted(Manifest.permission.RECORD_AUDIO)).thenReturn(false);\n\n        TreeReference treeReference1 = new TreeReference();\n        TreeReference treeReference2 = new TreeReference();\n        recordAudioActionRegistry.listener.accept(treeReference1, \"voice-only\");\n        recordAudioActionRegistry.listener.accept(treeReference2, \"low\");\n        RobolectricHelpers.runLooper();\n\n        viewModel.grantAudioPermission();\n        verify(audioRecorder).start(new HashSet<TreeReference>() {\n            {\n                add(treeReference1);\n                add(treeReference2);\n            }\n        }, Output.AMR);\n    }\n\n    @Test\n    public void grantAudioPermission_setsPermissionRequiredToFalse() {\n        when(permissionsChecker.isPermissionGranted(Manifest.permission.RECORD_AUDIO)).thenReturn(false);\n\n        TreeReference treeReference1 = new TreeReference();\n        recordAudioActionRegistry.listener.accept(treeReference1, \"low\");\n        RobolectricHelpers.runLooper();\n\n        viewModel.grantAudioPermission();\n        assertThat(viewModel.isPermissionRequired().getValue(), is(false));\n    }\n\n    @Test(expected = IllegalStateException.class)\n    public void grantAudioPermission_whenThereWasNoPermissionCheck_throwsIllegalStateException() {\n        viewModel.grantAudioPermission();\n    }\n\n    @Test\n    public void setBackgroundRecordingEnabled_whenFalse_logsEventToAuditLog() {\n        when(clock.get()).thenReturn(1234L);\n        viewModel.setBackgroundRecordingEnabled(false);\n        verify(auditEventLogger).logEvent(AuditEvent.AuditEventType.BACKGROUND_AUDIO_DISABLED, true, 1234L);\n    }\n\n    @Test\n    public void setBackgroundRecordingEnabled_whenTrue_logsEventToAuditLog() {\n        when(clock.get()).thenReturn(1234L);\n        viewModel.setBackgroundRecordingEnabled(true);\n        verify(auditEventLogger).logEvent(AuditEvent.AuditEventType.BACKGROUND_AUDIO_ENABLED, true, 1234L);\n    }\n\n    @Test\n    public void onCleared_stopsUpdatingAuditEventLogger() {\n        when(clock.get()).thenReturn(1234L);\n\n        viewModel.onCleared();\n        when(formController.getAuditEventLogger()).thenReturn(mock(AuditEventLogger.class));\n        formSession.setValue(new FormSession(formController, form));\n\n        viewModel.setBackgroundRecordingEnabled(false);\n        verify(auditEventLogger).logEvent(AuditEvent.AuditEventType.BACKGROUND_AUDIO_DISABLED, true, 1234L);\n    }\n\n    private static class FakeRecordAudioActionRegistry implements BackgroundAudioViewModel.RecordAudioActionRegistry {\n\n\n        private BiConsumer<TreeReference, String> listener;\n\n        @Override\n        public void register(BiConsumer<TreeReference, String> listener) {\n            this.listener = listener;\n        }\n\n        @Override\n        public void unregister() {\n            this.listener = null;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/FormEndTest.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.app.Application\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material.icons.filled.EditOff\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.assert\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.assertIsNotDisplayed\nimport androidx.compose.ui.test.hasContentDescription\nimport androidx.compose.ui.test.junit4.createComposeRule\nimport androidx.compose.ui.test.onNodeWithTag\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.strings.R.string\n\n@RunWith(AndroidJUnit4::class)\nclass FormEndTest {\n\n    @get:Rule\n    val composeTestRule = createComposeRule()\n\n    private val application = ApplicationProvider.getApplicationContext<Application>()\n\n    @Test\n    fun `form title is displayed correctly`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = true,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = true\n            )\n        }\n\n        composeTestRule\n            .onNodeWithText(application.getString(string.save_enter_data_description, \"blah\"))\n            .assertIsDisplayed()\n    }\n\n    @Test\n    fun `when saving drafts is enabled in settings should 'Save as draft' button be visible`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = false,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = true\n            )\n        }\n\n        composeTestRule\n            .onNodeWithText(application.getString(string.save_as_draft))\n            .assertIsDisplayed()\n    }\n\n    @Test\n    fun `when saving drafts is disabled in settings should 'Save as draft' button be hidden`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = false,\n                saveAsDraftEnabled = false,\n                finalizeEnabled = true\n            )\n        }\n\n        composeTestRule\n            .onNodeWithText(application.getString(string.save_as_draft))\n            .assertIsNotDisplayed()\n    }\n\n    @Test\n    fun `when 'Save as draft' button is clicked then onSaveClicked is called with false value`() {\n        val onSave = mock<(Boolean) -> Unit>()\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = false,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = true,\n                onSave = onSave\n            )\n        }\n\n        composeTestRule\n            .onNodeWithText(application.getString(string.save_as_draft))\n            .performClick()\n\n        verify(onSave).invoke(false)\n    }\n\n    @Test\n    fun `when finalizing forms is enabled in settings should 'Finalize' button be visible`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = false,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = true\n            )\n        }\n\n        composeTestRule\n            .onNodeWithText(application.getString(string.finalize))\n            .assertIsDisplayed()\n\n        composeTestRule\n            .onNodeWithText(application.getString(string.send))\n            .assertIsNotDisplayed()\n    }\n\n    @Test\n    fun `when finalizing forms is disabled in settings should 'Finalize' button be hidden`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = false,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = false\n            )\n        }\n\n        composeTestRule\n            .onNodeWithText(application.getString(string.finalize))\n            .assertIsNotDisplayed()\n    }\n\n    @Test\n    fun `when 'Finalize' button is clicked then onSaveClicked is called with true value`() {\n        val onSave = mock<(Boolean) -> Unit>()\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = false,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = true,\n                onSave = onSave\n            )\n        }\n\n        composeTestRule\n            .onNodeWithText(application.getString(string.finalize))\n            .performClick()\n\n        verify(onSave).invoke(true)\n    }\n\n    @Test\n    fun `when form should be sent automatically then 'Send' button should be displayed`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = true,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = true\n            )\n        }\n\n        composeTestRule\n            .onNodeWithText(application.getString(string.send))\n            .assertIsDisplayed()\n\n        composeTestRule\n            .onNodeWithText(application.getString(string.finalize))\n            .assertIsNotDisplayed()\n    }\n\n    @Test\n    fun `shows warning when form is not editable`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = false,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = true\n            )\n        }\n\n        composeTestRule\n            .onNodeWithTag(EditWarningSemantics.TAG)\n            .assert(\n                hasContentDescription(\n                    warningContentDescription(\n                        string.form_editing_disabled_after_finalizing,\n                        string.form_editing_disabled_hint\n                    )\n                )\n            )\n            .assert(hasIcon(Icons.Default.EditOff))\n            .assert(hasErrorBackground(true))\n    }\n\n    @Test\n    fun `does not show warning message when form is not editable and save as draft is disabled`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = false,\n                saveAsDraftEnabled = false,\n                finalizeEnabled = true\n            )\n        }\n\n        composeTestRule\n            .onNodeWithTag(EditWarningSemantics.TAG)\n            .assert(\n                hasContentDescription(application.getString(string.form_editing_disabled_after_finalizing))\n            )\n            .assert(hasIcon(Icons.Default.EditOff))\n            .assert(hasErrorBackground(true))\n    }\n\n    @Test\n    fun `shows warning when form is not editable after sending`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = false,\n                shouldBeSentAutomatically = true,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = true\n            )\n        }\n\n        composeTestRule\n            .onNodeWithTag(EditWarningSemantics.TAG)\n            .assert(\n                hasContentDescription(\n                    warningContentDescription(\n                        string.form_editing_disabled_after_sending,\n                        string.form_editing_disabled_hint\n                    )\n                )\n            )\n            .assert(hasIcon(Icons.Default.EditOff))\n            .assert(hasErrorBackground(true))\n    }\n\n    @Test\n    fun `shows warning when form is editable`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = true,\n                shouldBeSentAutomatically = false,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = true\n            )\n        }\n\n        composeTestRule\n            .onNodeWithTag(EditWarningSemantics.TAG)\n            .assert(\n                hasContentDescription(\n                    warningContentDescription(\n                        string.form_editing_enabled_after_finalizing,\n                        string.form_editing_enabled_after_finalizing_hint\n                    )\n                )\n            )\n            .assert(hasIcon(Icons.Default.Edit))\n            .assert(hasErrorBackground(false))\n    }\n\n    @Test\n    fun `shows warning when form is editable after sending`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = true,\n                shouldBeSentAutomatically = true,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = true\n            )\n        }\n\n        composeTestRule\n            .onNodeWithTag(EditWarningSemantics.TAG)\n            .assert(\n                hasContentDescription(\n                    warningContentDescription(\n                        string.form_editing_enabled_after_sending,\n                        string.form_editing_enabled_after_sending_hint\n                    )\n                )\n            )\n            .assert(hasIcon(Icons.Default.Edit))\n            .assert(hasErrorBackground(false))\n    }\n\n    @Test\n    fun `does not show warning if finalize is disabled`() {\n        composeTestRule.setContent {\n            FormEnd(\n                formTitle = \"blah\",\n                isEditableAfterFinalization = true,\n                shouldBeSentAutomatically = true,\n                saveAsDraftEnabled = true,\n                finalizeEnabled = false\n            )\n        }\n\n        composeTestRule\n            .onNodeWithTag(EditWarningSemantics.TAG)\n            .assertIsNotDisplayed()\n    }\n\n    private fun warningContentDescription(title: Int, message: Int): String {\n        return \"${application.getString(title)}: ${application.getString(message)}\"\n    }\n}\n\nprivate fun hasIcon(icon: ImageVector): SemanticsMatcher {\n    return SemanticsMatcher.expectValue(EditWarningSemantics.iconProperty, icon.name)\n}\n\nprivate fun hasErrorBackground(errorBackground: Boolean): SemanticsMatcher {\n    return SemanticsMatcher.expectValue(\n        EditWarningSemantics.errorBackgroundProperty,\n        errorBackground\n    )\n}"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/FormEndViewModelTest.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formentry.support.InMemFormSessionRepository\nimport org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider\nimport org.odk.collect.formstest.FormFixtures\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\n\n@RunWith(AndroidJUnit4::class)\nclass FormEndViewModelTest {\n    private val formSessionRepository = InMemFormSessionRepository()\n    private val sessionId = \"blah\"\n    private val settingsProvider = InMemSettingsProvider()\n    private val autoSendSettingsProvider = mock<AutoSendSettingsProvider>()\n    private val formEndViewModel = FormEndViewModel(formSessionRepository, sessionId, settingsProvider, autoSendSettingsProvider)\n\n    @Test\n    fun `when 'Save as draft' is enabled, isSaveDraftEnabled should return true`() {\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true)\n        assertThat(formEndViewModel.isSaveDraftEnabled(), equalTo(true))\n    }\n\n    @Test\n    fun `when 'Save as draft' is disabled, isSaveDraftEnabled should return false`() {\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false)\n        assertThat(formEndViewModel.isSaveDraftEnabled(), equalTo(false))\n    }\n\n    @Test\n    fun `when 'Finalize' is enabled, isFinalizeEnabled should return true`() {\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true)\n        assertThat(formEndViewModel.isFinalizeEnabled(), equalTo(true))\n    }\n\n    @Test\n    fun `when 'Finalize' is disabled, isFinalizeEnabled should return false`() {\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false)\n        assertThat(formEndViewModel.isFinalizeEnabled(), equalTo(false))\n    }\n\n    @Test\n    fun `when autoSend is enabled in settings, shouldFormBeSentAutomatically should return true`() {\n        whenever(autoSendSettingsProvider.isAutoSendEnabledInSettings()).thenReturn(true)\n        formSessionRepository.set(sessionId, mock(), FormFixtures.form())\n        assertThat(formEndViewModel.shouldFormBeSentAutomatically(), equalTo(true))\n    }\n\n    @Test\n    fun `when autoSend is disabled in settings, shouldFormBeSentAutomatically should return false`() {\n        whenever(autoSendSettingsProvider.isAutoSendEnabledInSettings()).thenReturn(false)\n        formSessionRepository.set(sessionId, mock(), FormFixtures.form())\n        assertThat(formEndViewModel.shouldFormBeSentAutomatically(), equalTo(false))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/FormEntryMenuProviderTest.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.fragment.app.FragmentActivity\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.notNullValue\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.core.model.data.IAnswerData\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.TestSettingsProvider.getSettingsProvider\nimport org.odk.collect.android.formentry.FormEntryMenuProvider.FormEntryMenuClickListener\nimport org.odk.collect.android.formentry.backgroundlocation.BackgroundLocationViewModel\nimport org.odk.collect.android.formentry.questions.AnswersProvider\nimport org.odk.collect.android.formhierarchy.FormHierarchyFragmentHostActivity\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.preferences.screens.ProjectPreferencesActivity\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.audiorecorder.recording.AudioRecorder\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.strings.localization.getLocalizedString\nimport org.odk.collect.testshared.RobolectricHelpers.createThemedActivity\nimport org.odk.collect.testshared.RobolectricHelpers.getFragmentByClass\nimport org.robolectric.Robolectric\nimport org.robolectric.Shadows\nimport org.robolectric.annotation.LooperMode\nimport org.robolectric.fakes.RoboMenu\n\n@RunWith(AndroidJUnit4::class)\n@LooperMode(LooperMode.Mode.PAUSED)\nclass FormEntryMenuProviderTest {\n    private val activity = createThemedActivity(AppCompatActivity::class.java)\n    private val formController = mock<FormController>()\n    private val formEntryViewModel = mock<FormEntryViewModel>().apply {\n        whenever(hasBackgroundRecording()).thenReturn(MutableNonNullLiveData(false))\n        whenever(sessionId).thenReturn(\"blah\")\n        whenever(formController).thenReturn(this@FormEntryMenuProviderTest.formController)\n    }\n    private val answersProvider = mock<AnswersProvider>()\n    private val audioRecorder = mock<AudioRecorder>().apply {\n        whenever(isRecording()).thenReturn(false)\n    }\n    private val backgroundAudioViewModel = mock<BackgroundAudioViewModel>().apply {\n        whenever(isBackgroundRecordingEnabled).thenReturn(MutableNonNullLiveData(true))\n    }\n    private val settingsProvider = getSettingsProvider()\n    private val formEntryMenuClickListener = mock<FormEntryMenuClickListener>()\n    private val backgroundLocationViewModel = mock<BackgroundLocationViewModel>()\n    private val formEntryMenuProvider = FormEntryMenuProvider(\n        activity,\n        answersProvider,\n        formEntryViewModel,\n        audioRecorder,\n        backgroundLocationViewModel,\n        backgroundAudioViewModel,\n        settingsProvider,\n        formEntryMenuClickListener\n    ) { true }\n\n    @Test\n    fun onPrepare_inRepeatQuestion_showsAddRepeat() {\n        whenever(formEntryViewModel.canAddRepeat()).thenReturn(true)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_add_repeat).isVisible, equalTo(true))\n    }\n\n    @Test\n    fun onPrepare_notInRepeatQuestion_hidesAddRepeat() {\n        whenever(formEntryViewModel.canAddRepeat()).thenReturn(false)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_add_repeat).isVisible, equalTo(false))\n    }\n\n    @Test\n    fun onPrepare_whenFormControllerIsNull_hidesAddRepeat() {\n        whenever(formEntryViewModel.formController).thenReturn(null)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_add_repeat).isVisible, equalTo(false))\n    }\n\n    @Test\n    fun onPrepare_whenFormHasBackgroundRecording_showsRecordAudio() {\n        whenever(formEntryViewModel.hasBackgroundRecording()).thenReturn(MutableNonNullLiveData(true))\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_record_audio).isVisible, equalTo(true))\n    }\n\n    @Test\n    fun onPrepare_whenFormDoesNotHaveBackgroundRecording_hidesRecordAudio() {\n        whenever(formEntryViewModel.hasBackgroundRecording()).thenReturn(MutableNonNullLiveData(false))\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_record_audio).isVisible, equalTo(false))\n    }\n\n    @Test\n    fun onPrepare_whenBackgroundRecodingEnabled_checksRecordAudio() {\n        whenever(backgroundAudioViewModel.isBackgroundRecordingEnabled).thenReturn(MutableNonNullLiveData(true))\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_record_audio).title, equalTo(activity.getLocalizedString(org.odk.collect.strings.R.string.record_audio_on)))\n    }\n\n    @Test\n    fun onPrepare_whenNotRecordingInBackground_unchecksRecordAudio() {\n        whenever(backgroundAudioViewModel.isBackgroundRecordingEnabled).thenReturn(MutableNonNullLiveData(false))\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_record_audio).title, equalTo(activity.getLocalizedString(org.odk.collect.strings.R.string.record_audio_off)))\n    }\n\n    @Test\n    fun onPrepare_whenSavingFormInTheMiddleEnabled_showsSave() {\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_SAVE_MID, true)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_save).isVisible, equalTo(true))\n    }\n\n    @Test\n    fun onPrepare_whenSavingFormInTheMiddleDisabled_hidesSave() {\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_SAVE_MID, false)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_save).isVisible, equalTo(false))\n    }\n\n    @Test\n    fun onPrepare_whenThereAreMoreThanOneDefinedLanguages_showsChangeLanguage() {\n        whenever(formController.getLanguages()).thenReturn(arrayOf(\"English\", \"Spanish\"))\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_languages).isVisible, equalTo(true))\n    }\n\n    @Test\n    fun onPrepare_whenThereIsJustOneDefinedLanguage_hidesChangeLanguage() {\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_languages).isVisible, equalTo(false))\n    }\n\n    @Test\n    fun onItemSelected_whenAddRepeat_callsPromptForNewRepeat() {\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_add_repeat))\n\n        verify(formEntryViewModel).promptForNewRepeat()\n    }\n\n    @Test\n    fun onItemSelected_whenAddRepeat_savesScreenAnswers() {\n        val answers: HashMap<FormIndex, IAnswerData> = HashMap()\n        whenever(answersProvider.answers).thenReturn(answers)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_add_repeat))\n\n        verify(formEntryViewModel).updateAnswersForScreen(answers, false)\n    }\n\n    @Test\n    fun onItemSelected_whenAddRepeat_whenRecording_showsWarning() {\n        whenever(audioRecorder.isRecording()).thenReturn(true)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_add_repeat))\n\n        verify(formEntryViewModel, never()).promptForNewRepeat()\n        val dialog = getFragmentByClass(activity.supportFragmentManager, RecordingWarningDialogFragment::class.java)\n        assertThat(dialog, notNullValue())\n        assertThat(dialog!!.dialog!!.isShowing, equalTo(true))\n    }\n\n    @Test\n    fun onItemSelected_whenAddRepeat_whenRecordingInTheBackground_doesNotShowWarning() {\n        whenever(audioRecorder.isRecording()).thenReturn(true)\n        whenever(backgroundAudioViewModel.isBackgroundRecording).thenReturn(true)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_add_repeat))\n\n        verify(formEntryViewModel).promptForNewRepeat()\n        val dialog = getFragmentByClass(activity.supportFragmentManager, RecordingWarningDialogFragment::class.java)\n        assertThat(dialog, nullValue())\n    }\n\n    @Test\n    fun onItemSelected_whenPreferences_startsPreferencesActivityWithChangeSettingsRequest() {\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_preferences))\n        val nextStartedActivity = Shadows.shadowOf(activity).nextStartedActivityForResult\n\n        assertThat(nextStartedActivity, notNullValue())\n        assertThat(nextStartedActivity.intent.component!!.className, equalTo(ProjectPreferencesActivity::class.java.name))\n        assertThat(nextStartedActivity.requestCode, equalTo(ApplicationConstants.RequestCodes.CHANGE_SETTINGS))\n    }\n\n    @Test\n    fun onItemSelected_whenPreferences_whenRecording_showsWarning() {\n        whenever(audioRecorder.isRecording()).thenReturn(true)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_preferences))\n\n        assertThat(Shadows.shadowOf(activity).nextStartedActivityForResult, nullValue())\n        val dialog = getFragmentByClass(activity.supportFragmentManager, RecordingWarningDialogFragment::class.java)\n        assertThat(dialog, notNullValue())\n        assertThat(dialog!!.dialog!!.isShowing, equalTo(true))\n    }\n\n    @Test\n    fun onItemSelected_whenHierarchy_startsHierarchyActivity() {\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_goto))\n        val nextStartedActivity = Shadows.shadowOf(activity).nextStartedActivityForResult\n\n        assertThat(nextStartedActivity, notNullValue())\n        assertThat(nextStartedActivity.intent.component!!.className, equalTo(FormHierarchyFragmentHostActivity::class.java.name))\n        assertThat(nextStartedActivity.requestCode, equalTo(ApplicationConstants.RequestCodes.HIERARCHY_ACTIVITY))\n    }\n\n    @Test\n    fun onItemSelected_whenHierarchy_savesScreenAnswers() {\n        val answers: HashMap<FormIndex, IAnswerData> = HashMap()\n        whenever(answersProvider.answers).thenReturn(answers)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_goto))\n\n        verify(formEntryViewModel).updateAnswersForScreen(answers, false)\n    }\n\n    @Test\n    fun onItemSelected_whenHierarchy_callsOpenHierarchy() {\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_goto))\n\n        verify(formEntryViewModel).openHierarchy()\n    }\n\n    @Test\n    fun onItemSelected_whenHierarchy_whenRecording_showsWarning() {\n        whenever(audioRecorder.isRecording()).thenReturn(true)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_goto))\n\n        assertThat(Shadows.shadowOf(activity).nextStartedActivity, nullValue())\n        val dialog = getFragmentByClass(activity.supportFragmentManager, RecordingWarningDialogFragment::class.java)\n        assertThat(dialog, notNullValue())\n        assertThat(dialog!!.dialog!!.isShowing, equalTo(true))\n    }\n\n    @Test\n    fun onItemSelected_whenHierarchy_whenRecordingInBackground_doesNotShowWarning() {\n        whenever(audioRecorder.isRecording()).thenReturn(true)\n        whenever(backgroundAudioViewModel.isBackgroundRecording).thenReturn(true)\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_goto))\n\n        assertThat(Shadows.shadowOf(activity).nextStartedActivity, notNullValue())\n        val dialog = getFragmentByClass(activity.supportFragmentManager, RecordingWarningDialogFragment::class.java)\n        assertThat(dialog, nullValue())\n    }\n\n    @Test\n    fun onItemSelected_whenRecordAudio_whenBackgroundRecordingDisabled_enablesBackgroundRecording_andShowsDialog() {\n        whenever(backgroundAudioViewModel.isBackgroundRecordingEnabled).thenReturn(MutableNonNullLiveData(false))\n\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_record_audio))\n\n        verify(backgroundAudioViewModel).setBackgroundRecordingEnabled(true)\n    }\n\n    @Test\n    fun onItemSelected_whenChangeLanguage_callsChangeLanguage() {\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_languages))\n\n        verify(formEntryMenuClickListener).changeLanguage()\n    }\n\n    @Test\n    fun onItemSelected_whenSave_callsSave() {\n        val menu = RoboMenu()\n        formEntryMenuProvider.onCreateMenu(menu, Robolectric.setupActivity(FragmentActivity::class.java).menuInflater)\n        formEntryMenuProvider.onPrepareMenu(menu)\n        formEntryMenuProvider.onMenuItemSelected(menu.findItem(R.id.menu_save))\n\n        verify(formEntryMenuClickListener).save()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/FormEntryUseCasesTest.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.data.IntegerData\nimport org.javarosa.form.api.FormEntryController\nimport org.javarosa.form.api.FormEntryModel\nimport org.javarosa.model.xform.XFormsModule\nimport org.javarosa.xform.parse.XFormParser\nimport org.junit.Before\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.entities.storage.InMemEntitiesRepository\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.FormFixtures\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\nclass FormEntryUseCasesTest {\n\n    private val projectRootDir = TempFiles.createTempDir()\n    private val formsRepository = InMemFormsRepository()\n    private val instancesRepository = InMemInstancesRepository()\n\n    @Before\n    fun setup() {\n        XFormsModule().registerModule()\n    }\n\n    @Test\n    fun loadFormDef_withInstance_loadsCorrectFormVersion() {\n        createForm(\n            copyTestForm(\"forms/one-question.xml\"),\n            formId = \"one-question\",\n            version = \"1\"\n        )\n\n        val (formUpdated, formDefUpdated) = createForm(\n            copyTestForm(\"forms/one-question-updated.xml\"),\n            formId = \"one-question\",\n            version = \"2\"\n        )\n\n        val instance = createDraft(formUpdated!!, formDefUpdated!!, instancesRepository)\n\n        val loadedFormDef =\n            FormEntryUseCases.loadFormDef(instance, formsRepository, projectRootDir, mock())!!.first\n        assertThat(loadedFormDef.title, equalTo(\"One Question Updated\"))\n    }\n\n    @Test\n    fun loadFormDef_withInstance_returnsNull_ifTheFormDoesNotExistInTheDatabase() {\n        val (form, formDef) = createForm(\n            copyTestForm(\"forms/one-question.xml\"),\n            formId = \"one-question\",\n            version = \"1\"\n        )\n\n        val instance = createDraft(form!!, formDef!!, instancesRepository)\n\n        formsRepository.deleteAll()\n\n        val formDefAndForm =\n            FormEntryUseCases.loadFormDef(instance, formsRepository, projectRootDir, mock())\n\n        assertThat(formDefAndForm, equalTo(null))\n    }\n\n    @Test\n    fun loadFormDef_withInstance_returnsNull_ifTheFormFileDoesNotExist() {\n        val testForm = copyTestForm(\"forms/one-question.xml\")\n        val (form, formDef) = createForm(\n            testForm,\n            formId = \"one-question\",\n            version = \"1\"\n        )\n\n        val instance = createDraft(form!!, formDef!!, instancesRepository)\n\n        testForm.delete()\n\n        val formDefAndForm =\n            FormEntryUseCases.loadFormDef(instance, formsRepository, projectRootDir, mock())\n\n        assertThat(formDefAndForm, equalTo(null))\n    }\n\n    @Test\n    fun loadDraft_returnsNull_ifTheInstanceFileDoesNotExist() {\n        val (form, formDef) = createForm(copyTestForm(\"forms/one-question-partial.xml\"))\n        val instance = createDraft(form!!, formDef!!, instancesRepository) {\n            it.stepToNextScreenEvent()\n            it.answerQuestion(it.getFormIndex(), IntegerData(64))\n        }\n\n        File(instance.instanceFilePath).delete()\n\n        val draftController = FormEntryUseCases.loadDraft(\n            form,\n            instance,\n            FormEntryController(FormEntryModel(formDef))\n        )\n\n        assertThat(draftController, equalTo(null))\n    }\n\n    @Test\n    fun finalizeDraft_whenValidationFails_marksInstanceAsHavingErrors() {\n        val (form, formDef) = createForm(copyTestForm(\"forms/two-question-required.xml\"))\n        val instance = createDraft(form!!, formDef!!, instancesRepository)\n\n        val draftController = FormEntryUseCases.loadDraft(\n            form,\n            instance,\n            FormEntryController(FormEntryModel(formDef))\n        )!!\n\n        FormEntryUseCases.finalizeDraft(\n            draftController,\n            instancesRepository,\n            InMemEntitiesRepository(),\n            mock()\n        )\n\n        assertThat(\n            instancesRepository.get(instance.dbId)!!.status,\n            equalTo(Instance.STATUS_INVALID)\n        )\n    }\n\n    @Test\n    fun finalizeDraft_canCreatePartialSubmissions() {\n        val (form, formDef) = createForm(copyTestForm(\"forms/one-question-partial.xml\"))\n        val instance = createDraft(form!!, formDef!!, instancesRepository) {\n            it.stepToNextScreenEvent()\n            it.answerQuestion(it.getFormIndex(), IntegerData(64))\n        }\n\n        val draftController = FormEntryUseCases.loadDraft(\n            form,\n            instance,\n            FormEntryController(FormEntryModel(formDef))\n        )!!\n\n        FormEntryUseCases.finalizeDraft(\n            draftController,\n            instancesRepository,\n            InMemEntitiesRepository(),\n            mock()\n        )\n\n        val updatedInstance = instancesRepository.get(instance.dbId)!!\n        assertThat(updatedInstance.canEditWhenComplete(), equalTo(false))\n\n        val root = XFormParser.getXMLDocument(File(updatedInstance.instanceFilePath).inputStream().reader()).rootElement\n        assertThat(root.name, equalTo(\"age\"))\n        assertThat(root.childCount, equalTo(1))\n        assertThat(root.getChild(0), equalTo(\"64\"))\n    }\n\n    @Test\n    fun finalizeDraft_updatesInstanceNameInRepository() {\n        val (form, formDef) = createForm(copyTestForm(\"forms/one-question-uuid-instance-name.xml\"))\n        val instance = createDraft(form!!, formDef!!, instancesRepository)\n\n        val draftController = FormEntryUseCases.loadDraft(\n            form,\n            instance,\n            FormEntryController(FormEntryModel(formDef))\n        )!!\n\n        FormEntryUseCases.finalizeDraft(\n            draftController,\n            instancesRepository,\n            InMemEntitiesRepository(),\n            mock()\n        )\n\n        val updatedInstance = instancesRepository.get(instance.dbId)!!\n        assertThat(\n            updatedInstance.displayName,\n            equalTo(draftController.getSubmissionMetadata()!!.instanceName)\n        )\n    }\n\n    private fun createForm(\n        xForm: File,\n        formId: String = \"formId\",\n        version: String = \"formVersion\"\n    ): Pair<Form?, FormDef?> {\n        val form = formsRepository.save(\n            FormFixtures.form(\n                formId = formId,\n                version = version,\n                formFilePath = xForm.absolutePath\n            )\n        )\n        val formDef = FormEntryUseCases.loadFormDef(form, projectRootDir, mock())\n        return Pair(form, formDef)\n    }\n\n    private fun createDraft(\n        form: Form,\n        formDef: FormDef,\n        instancesRepository: InMemInstancesRepository,\n        fillIn: (FormController) -> Any = {}\n    ): Instance {\n        val instanceFile = TempFiles.createTempFile(\"instance\", \".xml\")\n\n        val formController = FormEntryUseCases.loadBlankForm(\n            form,\n            FormEntryController(FormEntryModel(formDef)),\n            instanceFile\n        )\n\n        fillIn(formController)\n        val instance = FormEntryUseCases.saveDraft(form, formController, instancesRepository)\n        return instance\n    }\n\n    private fun copyTestForm(testForm: String): File {\n        return TempFiles.createTempFile(\".xml\").also {\n            FileUtils.copyFileFromResources(testForm, it.absolutePath)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/FormEntryViewModelTest.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.core.Is.is;\nimport static org.javarosa.core.model.Constants.CONTROL_SELECT_ONE;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.external.FormUriActivityKt.FORM_ENTRY_TOKEN;\nimport static org.odk.collect.androidtest.LiveDataTestUtilsKt.getOrAwaitValue;\nimport static java.util.Arrays.asList;\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.SubmissionProfile;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.javarosa.form.api.FormEntryController;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.javarosa.measure.Measure;\nimport org.javarosa.xpath.parser.XPathSyntaxException;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.exception.JavaRosaException;\nimport org.odk.collect.android.formentry.audit.AuditEventLogger;\nimport org.odk.collect.android.formentry.support.InMemFormSessionRepository;\nimport org.odk.collect.android.javarosawrapper.FailedValidationResult;\nimport org.odk.collect.android.javarosawrapper.FakeFormController;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.ChangeLocks;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.formstest.InMemFormsRepository;\nimport org.odk.collect.shared.locks.BooleanChangeLock;\nimport org.odk.collect.testshared.FakeScheduler;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.util.HashMap;\n\n@RunWith(AndroidJUnit4.class)\npublic class FormEntryViewModelTest {\n\n    private FormEntryViewModel viewModel;\n    private FakeFormController formController;\n    private FormIndex startingIndex;\n    private AuditEventLogger auditEventLogger;\n    private FakeScheduler scheduler;\n    private final Form form = new Form.Builder().formFilePath(\"blah\").build();\n    private final FormSessionRepository formSessionRepository = new InMemFormSessionRepository();\n    private final FormsRepository formsRepository = new InMemFormsRepository();\n    private final ChangeLocks changeLocks = new ChangeLocks(new BooleanChangeLock(), new BooleanChangeLock());\n\n    @Rule\n    public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();\n\n    @Before\n    public void setup() {\n        startingIndex = new FormIndex(null, 0, 0, new TreeReference());\n        auditEventLogger = mock(AuditEventLogger.class);\n        formController = new FakeFormController(startingIndex, auditEventLogger);\n\n        scheduler = new FakeScheduler();\n\n        formSessionRepository.set(\"blah\", formController, form);\n        viewModel = new FormEntryViewModel(() -> 0L, scheduler, formSessionRepository, \"blah\", formsRepository, changeLocks);\n    }\n\n    @Test\n    public void refresh_whenEventIsBeginningOfForm_stepsForwards() {\n        formController.setCurrentEvent(FormEntryController.EVENT_BEGINNING_OF_FORM);\n\n        viewModel.refresh();\n        scheduler.flush();\n        assertThat(formController.getStepPosition(), equalTo(1));\n    }\n\n    @Test\n    public void refresh_whenEventIsBeginningOfForm_andThereIsAnErrorSteppingForward_setsError() {\n        formController.setCurrentEvent(FormEntryController.EVENT_BEGINNING_OF_FORM);\n        formController.setNextStepError(new JavaRosaException(new IOException(\"OH NO\")));\n\n        viewModel.refresh();\n        scheduler.flush();\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"OH NO\")));\n    }\n\n    @Test\n    public void addRepeat_stepsToNextScreenEvent() throws Exception {\n        viewModel.addRepeat();\n        scheduler.flush();\n        assertThat(formController.getStepPosition(), equalTo(1));\n    }\n\n    @Test\n    public void addRepeat_whenThereIsAnErrorCreatingRepeat_setsErrorWithMessage() {\n        formController.setNewRepeatError(new RuntimeException(new IOException(\"OH NO\")));\n\n        viewModel.addRepeat();\n        scheduler.flush();\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"OH NO\")));\n    }\n\n    @Test\n    public void addRepeat_whenThereIsAnErrorCreatingRepeat_setsErrorWithoutCause() {\n        RuntimeException runtimeException = mock(RuntimeException.class);\n        when(runtimeException.getCause()).thenReturn(null);\n        when(runtimeException.getMessage()).thenReturn(\"Unknown issue occurred while adding a new group\");\n        formController.setNewRepeatError(runtimeException);\n\n        viewModel.addRepeat();\n        scheduler.flush();\n\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"Unknown issue occurred while adding a new group\")));\n    }\n\n    @Test\n    public void addRepeat_whenThereIsAnErrorSteppingToNextScreen_setsErrorWithMessage() throws Exception {\n        formController.setNextStepError(new JavaRosaException(new IOException(\"OH NO\")));\n\n        viewModel.addRepeat();\n        scheduler.flush();\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"OH NO\")));\n    }\n\n    @Test\n    public void addRepeat_whenThereIsAnErrorSteppingToNextScreen_setsErrorWithoutCause() throws Exception {\n        JavaRosaException javaRosaException = mock(JavaRosaException.class);\n        when(javaRosaException.getCause()).thenReturn(null);\n        when(javaRosaException.getMessage()).thenReturn(\"Unknown issue occurred while adding a new group\");\n        formController.setNextStepError(javaRosaException);\n\n        viewModel.addRepeat();\n        scheduler.flush();\n\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"Unknown issue occurred while adding a new group\")));\n    }\n\n    @Test\n    public void cancelRepeatPrompt_afterPromptForNewRepeatAndAddRepeat_stepsToNextRatherThanJumpingBack() {\n        FormIndex originalIndex = formController.getFormIndex();\n        formController.setNextRepeatPrompt(new FormIndex(null, originalIndex.getLocalIndex() + 1, 0, new TreeReference()));\n\n        viewModel.promptForNewRepeat();\n        scheduler.flush();\n\n        viewModel.addRepeat();\n        scheduler.flush();\n\n        FormIndex newIndex = new FormIndex(null, originalIndex.getLocalIndex() + 2, 0, new TreeReference());\n        formController.jumpToIndex(newIndex);\n        viewModel.cancelRepeatPrompt();\n        scheduler.flush();\n        assertThat(formController.getFormIndex(), equalTo(new FormIndex(null, newIndex.getLocalIndex() + 1, 0, new TreeReference())));\n    }\n\n    @Test\n    public void cancelRepeatPrompt_afterPromptForNewRepeatAndCancelRepeatPrompt_stepsToNextRatherThanJumpingBack() {\n        FormIndex originalIndex = formController.getFormIndex();\n        formController.setNextRepeatPrompt(new FormIndex(null, originalIndex.getLocalIndex() + 1, 0, new TreeReference()));\n\n        viewModel.promptForNewRepeat();\n        scheduler.flush();\n\n        viewModel.cancelRepeatPrompt();\n        scheduler.flush();\n\n        assertThat(formController.getFormIndex(), equalTo(originalIndex));\n\n        FormIndex newIndex = new FormIndex(null, originalIndex.getLocalIndex() + 2, 0, new TreeReference());\n        formController.jumpToIndex(newIndex);\n        viewModel.cancelRepeatPrompt();\n        scheduler.flush();\n        assertThat(formController.getFormIndex(), equalTo(new FormIndex(null, newIndex.getLocalIndex() + 1, 0, new TreeReference())));\n    }\n\n    @Test\n    public void cancelRepeatPrompt_whenThereIsAnErrorSteppingToNextScreen_setsErrorWithMessage() {\n        formController.setNextStepError(new JavaRosaException(new IOException(\"OH NO\")));\n\n        viewModel.cancelRepeatPrompt();\n        scheduler.flush();\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"OH NO\")));\n    }\n\n    @Test\n    public void getQuestionPrompt_returnsPromptForIndex() {\n        FormIndex formIndex = new FormIndex(null, 1, 1, new TreeReference());\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder().build();\n        formController.setPrompt(formIndex, prompt);\n\n        assertThat(viewModel.getQuestionPrompt(formIndex), is(prompt));\n    }\n\n    @Test\n    public void moveForward_whenThereIsAnErrorSteppingToNextEvent_setErrorWithMessage() {\n        formController.setNextStepError(new JavaRosaException(new IOException(\"OH NO\")));\n\n        viewModel.moveForward(new HashMap<>());\n        scheduler.flush();\n\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"OH NO\")));\n    }\n\n    @Test\n    public void moveForward_withEvaluateConstraints_whenThereIsAFailedConstraint_setsFailedConstraint() {\n        FailedValidationResult failedValidationResult =\n                new FailedValidationResult(startingIndex, 0, null, org.odk.collect.strings.R.string.invalid_answer_error);\n        formController.setFailedConstraint(failedValidationResult);\n\n        HashMap<FormIndex, IAnswerData> answers = new HashMap<>();\n        answers.put(startingIndex, new StringData(\"answer\"));\n        viewModel.moveForward(answers, true);\n        scheduler.flush();\n\n        assertThat(getOrAwaitValue(viewModel.getCurrentIndex()).getValidationResult(), equalTo(failedValidationResult));\n    }\n\n    /**\n     * We don't want to flush the log before answers are actually committed.\n     */\n    @Test\n    public void moveForward_withEvaluateConstraints_whenThereIsAFailedConstraint_doesNotFlushAuditLog() throws Exception {\n        FailedValidationResult failedValidationResult = new FailedValidationResult(startingIndex, 0, null, org.odk.collect.strings.R.string.invalid_answer_error);\n        formController.setFailedConstraint(failedValidationResult);\n\n        HashMap<FormIndex, IAnswerData> answers = new HashMap<>();\n        answers.put(startingIndex, new StringData(\"answer\"));\n        viewModel.moveForward(answers, true);\n        scheduler.flush();\n\n        verify(auditEventLogger, never()).flush();\n    }\n\n    @Test\n    public void moveForward_withEvaluateConstraints_whenThereIsAFailedConstraint_doesNotStepToNextEvent() throws Exception {\n        FailedValidationResult failedValidationResult = new FailedValidationResult(startingIndex, 0, null, org.odk.collect.strings.R.string.invalid_answer_error);\n        formController.setFailedConstraint(failedValidationResult);\n\n        HashMap<FormIndex, IAnswerData> answers = new HashMap<>();\n        answers.put(startingIndex, new StringData(\"answer\"));\n        viewModel.moveForward(answers, true);\n        scheduler.flush();\n\n        assertThat(formController.getStepPosition(), equalTo(0));\n    }\n\n    @Test\n    public void moveForward_whenThereIsAnErrorSaving_setsErrorWithMessage() {\n        formController.setSaveError(new JavaRosaException(new IOException(\"OH NO\")));\n\n        viewModel.moveForward(new HashMap<>());\n        scheduler.flush();\n\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"OH NO\")));\n    }\n\n    @Test\n    public void moveForward_whenThereIsAnErrorSaving_doesNotStepToNextEvent() {\n        formController.setSaveError(new JavaRosaException(new IOException(\"OH NO\")));\n\n        viewModel.moveForward(new HashMap<>());\n        scheduler.flush();\n\n        assertThat(formController.getStepPosition(), equalTo(0));\n    }\n\n    @Test\n    public void moveForward_setsLoadingToTrueWhileBackgroundWorkHappens() {\n        assertThat(getOrAwaitValue(viewModel.isLoading()), equalTo(false));\n\n        viewModel.moveForward(new HashMap<>());\n        assertThat(getOrAwaitValue(viewModel.isLoading()), equalTo(true));\n\n        scheduler.flush();\n        assertThat(getOrAwaitValue(viewModel.isLoading()), equalTo(false));\n    }\n\n    @Test\n    public void moveBackward_whenThereIsAnErrorSteppingToPreviousEvent_setErrorWithMessage() throws Exception {\n        formController.setPreviousStepError(new JavaRosaException(new IOException(\"OH NO\")));\n\n        viewModel.moveBackward(new HashMap<>());\n        scheduler.flush();\n\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"OH NO\")));\n    }\n\n    @Test\n    public void moveBackward_whenThereIsAnErrorSaving_setsErrorWithMessage() {\n        formController.setSaveError(new JavaRosaException(new IOException(\"OH NO\")));\n\n        viewModel.moveBackward(new HashMap<>());\n        scheduler.flush();\n\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"OH NO\")));\n    }\n\n    @Test\n    public void moveBackward_whenThereIsAnErrorSaving_doesNotStepToPreviousEvent() throws Exception {\n        formController.setPreviousStepError((new JavaRosaException(new IOException(\"OH NO\"))));\n\n        viewModel.moveBackward(new HashMap<>());\n        scheduler.flush();\n\n        assertThat(formController.getStepPosition(), equalTo(0));\n    }\n\n    /**\n     * We don't want to flush the log before answers are actually committed.\n     */\n    @Test\n    public void moveBackward_whenThereIsAnErrorSaving_doesNotFlushAuditLog() throws Exception {\n        formController.setSaveError(new JavaRosaException(new IOException(\"OH NO\")));\n\n        viewModel.moveBackward(new HashMap<>());\n        scheduler.flush();\n\n        verify(auditEventLogger, never()).flush();\n    }\n\n    @Test\n    public void moveBackward_setsLoadingToTrueWhileBackgroundWorkHappens() throws Exception {\n        assertThat(getOrAwaitValue(viewModel.isLoading()), equalTo(false));\n\n        viewModel.moveBackward(new HashMap<>());\n        assertThat(getOrAwaitValue(viewModel.isLoading()), equalTo(true));\n\n        scheduler.flush();\n        assertThat(getOrAwaitValue(viewModel.isLoading()), equalTo(false));\n    }\n\n    @Test\n    public void validate_setsLoadingToTrueWhileBackgroundWorkHappens() {\n        assertThat(getOrAwaitValue(viewModel.isLoading()), equalTo(false));\n\n        viewModel.validateForm();\n        assertThat(getOrAwaitValue(viewModel.isLoading()), equalTo(true));\n\n        scheduler.flush();\n        assertThat(getOrAwaitValue(viewModel.isLoading()), equalTo(false));\n    }\n\n    @Test\n    public void validate_whenThereIsAnErrorValidating_setsError() {\n        formController.setValidationError(new JavaRosaException(new IOException(\"OH NO\")));\n\n        viewModel.validateForm();\n        scheduler.flush();\n        assertThat(viewModel.getError().getValue(), equalTo(new FormError.NonFatal(\"OH NO\")));\n    }\n\n    @Test\n    public void refresh_whenThereIsASelectOnePrompt_preloadsSelectChoices() {\n        formController.setCurrentEvent(FormEntryController.EVENT_QUESTION);\n\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(CONTROL_SELECT_ONE)\n                .build();\n        formController.setQuestionPrompts(asList(prompt));\n\n        int loadCount = Measure.withMeasure(asList(\"LoadSelectChoices\"), () -> {\n            viewModel.refresh();\n            scheduler.runBackground();\n        });\n        assertThat(loadCount, equalTo(1));\n\n        loadCount = Measure.withMeasure(asList(\"LoadSelectChoices\"), () -> {\n            scheduler.runForeground();\n        });\n        assertThat(loadCount, equalTo(0));\n\n        loadCount = Measure.withMeasure(asList(\"LoadSelectChoices\"), () -> {\n            try {\n                viewModel.loadSelectChoices(prompt);\n            } catch (FileNotFoundException | XPathSyntaxException e) {\n                throw new RuntimeException(e);\n            }\n        });\n        assertThat(loadCount, equalTo(0));\n    }\n\n    @Test\n    public void isFormEditableAfterFinalization_returnsFalse_whenSubmissionProfileIsMissing() {\n        assertThat(viewModel.isFormEditableAfterFinalization(), equalTo(false));\n    }\n\n    @Test\n    public void isFormEditableAfterFinalization_returnsFalse_whenSubmissionProfileHasNoClientEditableAttribute() {\n        SubmissionProfile submissionProfile = new SubmissionProfile(null, null, null, null, new HashMap<>());\n\n        FormDef formDef = new FormDef();\n        formDef.setDefaultSubmission(submissionProfile);\n        formController.setFormDef(formDef);\n\n        assertThat(viewModel.isFormEditableAfterFinalization(), equalTo(false));\n    }\n\n    @Test\n    public void isFormEditableAfterFinalization_returnsFalse_whenClientEditableAttributeIsNotTrue() {\n        HashMap<String, String> attributeMap = new HashMap<>();\n        attributeMap.put(\"client-editable\", \"blah\");\n\n        SubmissionProfile submissionProfile = new SubmissionProfile(null, null, null, null, attributeMap);\n\n        FormDef formDef = new FormDef();\n        formDef.setDefaultSubmission(submissionProfile);\n        formController.setFormDef(formDef);\n\n        assertThat(viewModel.isFormEditableAfterFinalization(), equalTo(false));\n    }\n\n    @Test\n    public void isFormEditableAfterFinalization_returnsTrue_whenClientEditableAttributeIsTrue() {\n        HashMap<String, String> attributeMap = new HashMap<>();\n        attributeMap.put(\"client-editable\", \"true\");\n\n        SubmissionProfile submissionProfile = new SubmissionProfile(null, null, null, null, attributeMap);\n\n        FormDef formDef = new FormDef();\n        formDef.setDefaultSubmission(submissionProfile);\n        formController.setFormDef(formDef);\n\n        assertThat(viewModel.isFormEditableAfterFinalization(), equalTo(true));\n    }\n\n    @Test\n    public void exit_releasesFormsLock() {\n        ((BooleanChangeLock) changeLocks.getFormsLock()).lock(FORM_ENTRY_TOKEN);\n\n        viewModel.exit();\n        assertThat(changeLocks.getFormsLock().tryLock(FORM_ENTRY_TOKEN), equalTo(true));\n    }\n\n    @Test\n    public void answerQuestion_savesAnswerToFormController() {\n        TreeReference reference = new TreeReference();\n        reference.add(\"blah\", TreeReference.INDEX_UNBOUND);\n        FormIndex formIndex = new FormIndex(null, 1, 1, reference);\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder().build();\n        formController.setPrompt(formIndex, prompt);\n\n        viewModel.answerQuestion(formIndex, new StringData(\"answer\"));\n        scheduler.flush(true);\n        assertThat(formController.getAnswer(formIndex.getReference()).getValue(), equalTo(\"answer\"));\n    }\n\n    @Test\n    public void answerQuestion_whenQuestionIsAutoAdvance_movesForward() {\n        TreeReference reference = new TreeReference();\n        reference.add(\"blah\", TreeReference.INDEX_UNBOUND);\n        FormIndex formIndex = new FormIndex(null, 1, 1, reference);\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(CONTROL_SELECT_ONE)\n                .withAppearance(Appearances.QUICK)\n                .build();\n        formController.setPrompt(formIndex, prompt);\n\n        FormIndex originalIndex = formController.getFormIndex();\n        viewModel.answerQuestion(formIndex, new StringData(\"answer\"));\n        scheduler.flush(true);\n        assertThat(formController.getFormIndex(), equalTo(new FormIndex(null, originalIndex.getLocalIndex() + 1, 0, new TreeReference())));\n    }\n\n    @Test\n    public void answerQuestion_whenQuestionIsAutoAdvance_andAnswerViolatesConstraint_setsFailedConstraint() {\n        TreeReference reference = new TreeReference();\n        reference.add(\"blah\", TreeReference.INDEX_UNBOUND);\n        FormIndex formIndex = new FormIndex(null, 1, 1, reference);\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(CONTROL_SELECT_ONE)\n                .withAppearance(Appearances.QUICK)\n                .build();\n        formController.setPrompt(formIndex, prompt);\n\n        FailedValidationResult failedValidationResult = new FailedValidationResult(startingIndex, 0, null, org.odk.collect.strings.R.string.invalid_answer_error);\n        formController.setFailedConstraint(failedValidationResult);\n\n        FormIndex originalIndex = formController.getFormIndex();\n        viewModel.answerQuestion(formIndex, new StringData(\"answer\"));\n        scheduler.flush(true);\n        assertThat(getOrAwaitValue(viewModel.getCurrentIndex()).getValidationResult(), equalTo(failedValidationResult));\n        assertThat(formController.getFormIndex(), equalTo(new FormIndex(null, originalIndex.getLocalIndex(), 0, new TreeReference())));\n    }\n\n    @Test\n    public void answerQuestion_setsQuestionIndexToUpdatedQuestionIndex() {\n        TreeReference reference = new TreeReference();\n        reference.add(\"blah\", TreeReference.INDEX_UNBOUND);\n        FormIndex formIndex = new FormIndex(null, 1, 1, reference);\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .build();\n        formController.setPrompt(formIndex, prompt);\n\n        FormIndex originalIndex = formController.getFormIndex();\n        viewModel.answerQuestion(formIndex, new StringData(\"answer\"));\n        scheduler.flush(true);\n        assertThat(\n                getOrAwaitValue(viewModel.getCurrentIndex()).getScreenIndex(),\n                equalTo(originalIndex)\n        );\n        assertThat(\n                getOrAwaitValue(viewModel.getCurrentIndex()).getQuestionIndex(),\n                equalTo(formIndex)\n        );\n    }\n\n    @Test\n    public void answerQuestion_whenQuestionIsAutoAdvance_setsScreenIndexToNextScreenIndex() {\n        TreeReference reference = new TreeReference();\n        reference.add(\"blah\", TreeReference.INDEX_UNBOUND);\n        FormIndex formIndex = new FormIndex(null, 1, 1, reference);\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(CONTROL_SELECT_ONE)\n                .withAppearance(Appearances.QUICK)\n                .build();\n        formController.setPrompt(formIndex, prompt);\n\n        FormIndex originalIndex = formController.getFormIndex();\n        viewModel.answerQuestion(formIndex, new StringData(\"answer\"));\n        scheduler.flush(true);\n        assertThat(\n                getOrAwaitValue(viewModel.getCurrentIndex()).getScreenIndex(),\n                equalTo(new FormIndex(null, originalIndex.getLocalIndex() + 1, 0, new TreeReference()))\n        );\n        assertThat(\n                getOrAwaitValue(viewModel.getCurrentIndex()).getQuestionIndex(),\n                equalTo(null)\n        );\n    }\n\n    @Test\n    public void answerQuestion_whenQuestionIsAutoAdvance_andAnswerViolatesConstraint_setsScreenIndexToCurrentScreenIndex() {\n        TreeReference reference = new TreeReference();\n        reference.add(\"blah\", TreeReference.INDEX_UNBOUND);\n        FormIndex formIndex = new FormIndex(null, 1, 1, reference);\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(CONTROL_SELECT_ONE)\n                .withAppearance(Appearances.QUICK)\n                .build();\n        formController.setPrompt(formIndex, prompt);\n\n        FailedValidationResult failedValidationResult = new FailedValidationResult(startingIndex, 0, null, org.odk.collect.strings.R.string.invalid_answer_error);\n        formController.setFailedConstraint(failedValidationResult);\n\n        FormIndex originalIndex = formController.getFormIndex();\n        viewModel.answerQuestion(formIndex, new StringData(\"answer\"));\n        scheduler.flush(true);\n        assertThat(\n                getOrAwaitValue(viewModel.getCurrentIndex()).getScreenIndex(),\n                equalTo(originalIndex)\n        );\n        assertThat(\n                getOrAwaitValue(viewModel.getCurrentIndex()).getQuestionIndex(),\n                equalTo(null)\n        );\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/FormLoadingDialogFragmentTest.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\n\nimport androidx.fragment.app.testing.FragmentScenario;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule;\n\n@RunWith(AndroidJUnit4.class)\npublic class FormLoadingDialogFragmentTest {\n\n    @Rule\n    public FragmentScenarioLauncherRule launcherRule = new FragmentScenarioLauncherRule();\n\n    @Test\n    public void dialogIsNotCancellable() {\n        FragmentScenario<FormLoadingDialogFragment> fragmentScenario = launcherRule.launch(FormLoadingDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            assertThat(fragment.isCancelable(), equalTo(false));\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/FormSessionRepositoryTest.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.androidtest.getOrAwaitValue\nimport org.odk.collect.forms.Form\n\nabstract class FormSessionRepositoryTest {\n\n    abstract val formSessionRepository: FormSessionRepository\n\n    @Test\n    fun create_returnsNewUniqueId() {\n        val id1 = formSessionRepository.create()\n        val id2 = formSessionRepository.create()\n\n        assertThat(id1, not(equalTo(id2)))\n    }\n\n    @Test\n    fun get_beforeSet_returnsEmptyLiveData() {\n        val id = formSessionRepository.create()\n        assertThat(formSessionRepository.get(id).getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun set_setsSessionForId() {\n        val id1 = formSessionRepository.create()\n        val id2 = formSessionRepository.create()\n\n        val formController1 = mock<FormController>()\n        val form1 = mock<Form>()\n\n        val formController2 = mock<FormController>()\n        val form2 = mock<Form>()\n\n        formSessionRepository.set(id1, formController1, form1)\n        formSessionRepository.set(id2, formController2, form2)\n\n        assertThat(formSessionRepository.get(id1).getOrAwaitValue(), equalTo(FormSession(formController1, form1)))\n        assertThat(formSessionRepository.get(id2).getOrAwaitValue(), equalTo(FormSession(formController2, form2)))\n    }\n\n    @Test\n    fun clear_clearsLiveDataForId() {\n        val id = formSessionRepository.create()\n        val formController = mock<FormController>()\n        val form = mock<Form>()\n        formSessionRepository.set(id, formController, form)\n        val liveData = formSessionRepository.get(id)\n\n        formSessionRepository.clear(id)\n        val newLiveData = formSessionRepository.get(id)\n\n        assertThat(liveData.getOrAwaitValue(), equalTo(null))\n        assertThat(liveData != newLiveData, equalTo(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/InMemoryFormSessionRepositoryTest.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport org.junit.Rule\nimport org.odk.collect.android.formentry.support.InMemFormSessionRepository\n\nclass InMemoryFormSessionRepositoryTest : FormSessionRepositoryTest() {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    private val inMemFormSessionRepository = InMemFormSessionRepository()\n    override val formSessionRepository: FormSessionRepository\n        get() = inMemFormSessionRepository\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/PrinterWidgetViewModelTest.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.printer.HtmlPrinter\nimport org.odk.collect.qrcode.zxing.QRCodeCreator\nimport org.odk.collect.testshared.FakeScheduler\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass PrinterWidgetViewModelTest {\n    private val scheduler = FakeScheduler()\n    private val questionMediaManager = mock<QuestionMediaManager>()\n    private val qrCodeCreator = mock<QRCodeCreator>()\n    private val context = mock<Context>()\n    private val htmlPrinter = mock<HtmlPrinter>()\n\n    @Test\n    fun `printing an empty string does not crash`() {\n        val printerWidgetViewModel = PrinterWidgetViewModel(scheduler, qrCodeCreator, htmlPrinter)\n        val html = \"\"\n        printerWidgetViewModel.parseAndPrint(html, questionMediaManager, context)\n        scheduler.runBackground()\n        scheduler.runForeground()\n\n        verify(htmlPrinter).print(\n            context,\n            \"<html>\\n <head></head>\\n <body></body>\\n</html>\"\n        )\n    }\n\n    @Test\n    fun `parsing a broken HTML with unclosed tags does not crash`() {\n        val printerWidgetViewModel = PrinterWidgetViewModel(scheduler, qrCodeCreator, htmlPrinter)\n        val html = \"<h1>This is an <b>unclosed tag<p>Broken HTML example</h1>\"\n        printerWidgetViewModel.parseAndPrint(html, questionMediaManager, context)\n        scheduler.runBackground()\n        scheduler.runForeground()\n\n        verify(htmlPrinter).print(\n            context,\n            \"<html>\\n\" +\n                \" <head></head>\\n\" +\n                \" <body>\\n\" +\n                \"  <h1>This is an <b>unclosed tag\\n\" +\n                \"    <p>Broken HTML example</p></b></h1>\\n\" +\n                \" </body>\\n\" +\n                \"</html>\"\n        )\n    }\n\n    @Test\n    fun `printing a partial HTML without html, head and body tags adds it`() {\n        val printerWidgetViewModel = PrinterWidgetViewModel(scheduler, qrCodeCreator, htmlPrinter)\n        val html = \"blah\"\n        printerWidgetViewModel.parseAndPrint(html, questionMediaManager, context)\n        scheduler.runBackground()\n        scheduler.runForeground()\n\n        verify(htmlPrinter).print(\n            context,\n            \"<html>\\n\" +\n                \" <head></head>\\n\" +\n                \" <body>blah</body>\\n\" +\n                \"</html>\"\n        )\n    }\n\n    @Test\n    fun `printing an HTML with css keeps it`() {\n        val printerWidgetViewModel = PrinterWidgetViewModel(scheduler, qrCodeCreator, htmlPrinter)\n        val html = \"<html lang=\\\"en\\\"> <head> <meta charset=\\\"UTF-8\\\"> <meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\"> <title>Embedded CSS Example</title> <style> body { font-family: 'Arial', sans-serif; background-color: #f0f0f0; } h1 { color: blue; } p { font-size: 16px; line-height: 1.5; } </style> </head> <body> <h1>Hello, World!</h1> <p>This is a sample HTML document with embedded CSS.</p> </body> </html>\"\n        printerWidgetViewModel.parseAndPrint(html, questionMediaManager, context)\n        scheduler.runBackground()\n        scheduler.runForeground()\n\n        verify(htmlPrinter).print(\n            context,\n            \"<html lang=\\\"en\\\">\\n\" +\n                \" <head>\\n\" +\n                \"  <meta charset=\\\"UTF-8\\\">\\n\" +\n                \"  <meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\">\\n\" +\n                \"  <title>Embedded CSS Example</title>\\n\" +\n                \"  <style> body { font-family: 'Arial', sans-serif; background-color: #f0f0f0; } h1 { color: blue; } p { font-size: 16px; line-height: 1.5; } </style>\\n\" +\n                \" </head>\\n\" +\n                \" <body>\\n\" +\n                \"  <h1>Hello, World!</h1>\\n\" +\n                \"  <p>This is a sample HTML document with embedded CSS.</p>\\n\" +\n                \" </body>\\n\" +\n                \"</html>\"\n        )\n    }\n\n    @Test\n    fun `printing an HTML with images sets the src attribute to an absolute path of an image`() {\n        val tempFile = File.createTempFile(\"photo\", \".png\")\n        whenever(questionMediaManager.getAnswerFile(\"photo.png\")).thenReturn(tempFile)\n        val printerWidgetViewModel = PrinterWidgetViewModel(scheduler, qrCodeCreator, htmlPrinter)\n        val html = \"<img width=\\\"150\\\" height=\\\"150\\\" src=\\\"photo.png\\\">\"\n        printerWidgetViewModel.parseAndPrint(html, questionMediaManager, context)\n        scheduler.runBackground()\n        scheduler.runForeground()\n\n        verify(htmlPrinter).print(\n            context,\n            \"<html>\\n\" +\n                \" <head></head>\\n\" +\n                \" <body>\\n\" +\n                \"  <img width=\\\"150\\\" height=\\\"150\\\" src=\\\"file://${tempFile.absolutePath}\\\">\\n\" +\n                \" </body>\\n\" +\n                \"</html>\"\n        )\n    }\n\n    @Test\n    fun `printing an HTML with a non-existing image does not modify the src attribute`() {\n        whenever(questionMediaManager.getAnswerFile(\"photo.png\")).thenReturn(null)\n        val printerWidgetViewModel = PrinterWidgetViewModel(scheduler, qrCodeCreator, htmlPrinter)\n        val html = \"<img width=\\\"150\\\" height=\\\"150\\\" src=\\\"photo.png\\\">\"\n        printerWidgetViewModel.parseAndPrint(html, questionMediaManager, context)\n        scheduler.runBackground()\n        scheduler.runForeground()\n\n        verify(htmlPrinter).print(\n            context,\n            \"<html>\\n\" +\n                \" <head></head>\\n\" +\n                \" <body>\\n\" +\n                \"  <img width=\\\"150\\\" height=\\\"150\\\" src=\\\"photo.png\\\">\\n\" +\n                \" </body>\\n\" +\n                \"</html>\"\n        )\n    }\n\n    @Test\n    fun `printing an HTML converts qrcode tags to img ones`() {\n        whenever(qrCodeCreator.create(any())).thenReturn(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))\n        val printerWidgetViewModel = PrinterWidgetViewModel(scheduler, qrCodeCreator, htmlPrinter)\n        val html = \"<qrcode width=\\\"150\\\" height=\\\"150\\\">blah</qrcode>\"\n        printerWidgetViewModel.parseAndPrint(html, questionMediaManager, context)\n        scheduler.runBackground()\n        scheduler.runForeground()\n\n        verify(htmlPrinter).print(\n            context,\n            \"<html>\\n\" +\n                \" <head></head>\\n\" +\n                \" <body>\\n\" +\n                \"  <img width=\\\"150\\\" height=\\\"150\\\" src=\\\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR4AQEFAPr/AAAAAAAABQAB\\n\" +\n                \"ZHiVOAAAAABJRU5ErkJggg==\\n\" +\n                \"\\\">\\n\" +\n                \" </body>\\n\" +\n                \"</html>\"\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/QuitFormDialogTest.kt",
    "content": "package org.odk.collect.android.formentry\n\nimport android.app.Activity\nimport android.view.View\nimport android.widget.TextView\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formentry.saving.FormSaveViewModel\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys.KEY_SAVE_MID\nimport org.odk.collect.shadows.ShadowAndroidXAlertDialog\nimport org.robolectric.Robolectric\nimport org.robolectric.Shadows.shadowOf\nimport org.robolectric.annotation.Config\nimport org.robolectric.shadow.api.Shadow.extract\nimport java.text.SimpleDateFormat\nimport java.util.Locale\n\n@RunWith(AndroidJUnit4::class)\n@Config(shadows = [ShadowAndroidXAlertDialog::class])\nclass QuitFormDialogTest {\n\n    private val formSaveViewModel = mock<FormSaveViewModel>()\n    private val formEntryViewModel = mock<FormEntryViewModel>()\n    private val settingsProvider = InMemSettingsProvider()\n\n    @Test\n    fun isCancellable() {\n        val activity = Robolectric.buildActivity(Activity::class.java).get()\n        val dialog = showDialog(activity)\n\n        assertThat(shadowOf(dialog).isCancelable, equalTo(true))\n    }\n\n    @Test\n    fun clickingDiscardChanges_callsExitOnFormEntryViewModel() {\n        val activity = Robolectric.buildActivity(Activity::class.java).get()\n        val dialog = showDialog(activity)\n\n        val shadowDialog = extract<ShadowAndroidXAlertDialog>(dialog)\n        val view = shadowDialog.getView()\n        view.findViewById<View>(R.id.discard_changes).performClick()\n\n        verify(formEntryViewModel).exit()\n    }\n\n    @Test\n    fun clickingOutlinedKeepEditing_dismissesDialog() {\n        val activity = Robolectric.buildActivity(Activity::class.java).get()\n        val dialog = showDialog(activity)\n\n        val shadowDialog = extract<ShadowAndroidXAlertDialog>(dialog)\n        val view = shadowDialog.getView()\n        view.findViewById<View>(R.id.keep_editing_outlined).performClick()\n\n        assertThat(dialog.isShowing, equalTo(false))\n    }\n\n    @Test\n    fun clickingFilledKeepEditing_dismissesDialog() {\n        val activity = Robolectric.buildActivity(Activity::class.java).get()\n        val dialog = showDialog(activity)\n\n        val shadowDialog = extract<ShadowAndroidXAlertDialog>(dialog)\n        val view = shadowDialog.getView()\n        view.findViewById<View>(R.id.keep_editing_filled).performClick()\n\n        assertThat(dialog.isShowing, equalTo(false))\n    }\n\n    @Test\n    fun whenSaveAsDraftIsEnabled_andFormCanBeFullyDiscarded_showsSaveExplanation() {\n        settingsProvider.getProtectedSettings().save(KEY_SAVE_MID, true)\n        whenever(formSaveViewModel.canBeFullyDiscarded()).doReturn(true)\n        whenever(formSaveViewModel.lastSavedTime).doReturn(null)\n\n        val activity = Robolectric.buildActivity(Activity::class.java).get()\n        val dialog = showDialog(activity)\n\n        val shadowDialog = extract<ShadowAndroidXAlertDialog>(dialog)\n        assertThat(\n            shadowDialog.getView().findViewById<TextView>(R.id.save_explanation).text,\n            equalTo(activity.getString(org.odk.collect.strings.R.string.save_explanation))\n        )\n    }\n\n    @Test\n    fun whenSaveAsDraftIsEnabled_andFormCanNotBeFullyDiscarded_showsLastSavedTime() {\n        settingsProvider.getProtectedSettings().save(KEY_SAVE_MID, true)\n        whenever(formSaveViewModel.canBeFullyDiscarded()).doReturn(false)\n        whenever(formSaveViewModel.lastSavedTime).doReturn(456L)\n\n        val activity = Robolectric.buildActivity(Activity::class.java).get()\n        val dialog = showDialog(activity)\n\n        val shadowDialog = extract<ShadowAndroidXAlertDialog>(dialog)\n\n        assertThat(\n            shadowDialog.getView().findViewById<TextView>(R.id.save_explanation).text,\n            equalTo(\n                SimpleDateFormat(\n                    activity.getString(org.odk.collect.strings.R.string.save_explanation_with_last_saved),\n                    Locale.getDefault()\n                ).format(456L)\n            )\n        )\n    }\n\n    @Test\n    fun whenSaveAsDraftIsEnabled_showsOutlinedKeepEditing_andHidesFilledKeepEditing() {\n        settingsProvider.getProtectedSettings().save(KEY_SAVE_MID, true)\n        whenever(formSaveViewModel.canBeFullyDiscarded()).doReturn(true)\n        whenever(formSaveViewModel.lastSavedTime).doReturn(null)\n\n        val activity = Robolectric.buildActivity(Activity::class.java).get()\n        val dialog = showDialog(activity)\n\n        val shadowDialog = extract<ShadowAndroidXAlertDialog>(dialog)\n\n        assertThat(\n            shadowDialog.getView().findViewById<View>(R.id.keep_editing_outlined).visibility,\n            equalTo(View.VISIBLE)\n        )\n        assertThat(\n            shadowDialog.getView().findViewById<View>(R.id.keep_editing_filled).visibility,\n            equalTo(View.GONE)\n        )\n    }\n\n    @Test\n    fun whenSaveAsDraftIsDisabled_andFormCanBeFullyDiscarded_showsWarningTitleAndMessage_andHidesButton() {\n        settingsProvider.getProtectedSettings().save(KEY_SAVE_MID, false)\n        whenever(formSaveViewModel.canBeFullyDiscarded()).doReturn(true)\n        whenever(formSaveViewModel.lastSavedTime).doReturn(null)\n\n        val activity = Robolectric.buildActivity(Activity::class.java).get()\n        val dialog = showDialog(activity)\n\n        val shadowDialog = extract<ShadowAndroidXAlertDialog>(dialog)\n\n        assertThat(\n            shadowDialog.title,\n            equalTo(activity.getString(org.odk.collect.strings.R.string.quit_form_continue_title))\n        )\n        assertThat(\n            shadowDialog.getView().findViewById<TextView>(R.id.save_explanation).text,\n            equalTo(activity.getString(org.odk.collect.strings.R.string.discard_form_warning))\n        )\n        assertThat(\n            shadowDialog.getView().findViewById<View>(R.id.save_changes).visibility,\n            equalTo(View.GONE)\n        )\n    }\n\n    @Test\n    fun whenSaveAsDraftIsDisabled_andFormCanNotBeFullyDiscarded_showsWarningTitleAndMessage_andHidesButton() {\n        settingsProvider.getProtectedSettings().save(KEY_SAVE_MID, false)\n        whenever(formSaveViewModel.canBeFullyDiscarded()).doReturn(false)\n        whenever(formSaveViewModel.lastSavedTime).doReturn(456L)\n\n        val activity = Robolectric.buildActivity(Activity::class.java).get()\n        val dialog = showDialog(activity)\n\n        val shadowDialog = extract<ShadowAndroidXAlertDialog>(dialog)\n\n        assertThat(\n            shadowDialog.title,\n            equalTo(activity.getString(org.odk.collect.strings.R.string.quit_form_continue_title))\n        )\n        assertThat(\n            shadowDialog.getView().findViewById<TextView>(R.id.save_explanation).text,\n            equalTo(\n                SimpleDateFormat(\n                    activity.getString(org.odk.collect.strings.R.string.discard_changes_warning),\n                    Locale.getDefault()\n                ).format(456L)\n            )\n        )\n        assertThat(\n            shadowDialog.getView().findViewById<View>(R.id.save_changes).visibility,\n            equalTo(View.GONE)\n        )\n    }\n\n    @Test\n    fun whenSaveAsDraftIsDisabled_hidesOutlinedKeepEditing_andShowsFilledKeepEditing() {\n        settingsProvider.getProtectedSettings().save(KEY_SAVE_MID, false)\n        whenever(formSaveViewModel.canBeFullyDiscarded()).doReturn(true)\n        whenever(formSaveViewModel.lastSavedTime).doReturn(null)\n\n        val activity = Robolectric.buildActivity(Activity::class.java).get()\n        val dialog = showDialog(activity)\n\n        val shadowDialog = extract<ShadowAndroidXAlertDialog>(dialog)\n\n        assertThat(\n            shadowDialog.getView().findViewById<View>(R.id.keep_editing_outlined).visibility,\n            equalTo(View.GONE)\n        )\n        assertThat(\n            shadowDialog.getView().findViewById<View>(R.id.keep_editing_filled).visibility,\n            equalTo(View.VISIBLE)\n        )\n    }\n\n    private fun showDialog(activity: Activity) = QuitFormDialog.show(\n        activity,\n        formSaveViewModel,\n        formEntryViewModel,\n        settingsProvider,\n        null\n    )\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/RecordingHandlerTest.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.not;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoInteractions;\n\nimport androidx.annotation.Nullable;\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.odk.collect.android.audio.AudioFileAppender;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.utilities.StubFormController;\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager;\nimport org.odk.collect.androidtest.FakeLifecycleOwner;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.recording.RecordingSession;\n\nimport java.io.File;\nimport java.util.HashSet;\n\n@SuppressWarnings(\"PMD.DoubleBraceInitialization\")\npublic class RecordingHandlerTest {\n\n    @Rule\n    public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();\n\n    private final FakeQuestionMediaManager questionMediaManager = new FakeQuestionMediaManager();\n    private final FormController formController = new TestFormController();\n    private final FormDef formDef = mock(FormDef.class);\n    private final AudioFileAppender amrAppender = mock(AudioFileAppender.class);\n    private final AudioFileAppender m4aAppender = mock(AudioFileAppender.class);\n\n    private File existingRecording;\n    private RecordingHandler recordingHandler;\n\n    @Before\n    public void setup() {\n        recordingHandler = new RecordingHandler(questionMediaManager, new FakeLifecycleOwner(), mock(AudioRecorder.class), amrAppender, m4aAppender);\n    }\n\n    @Test\n    public void whenBackgroundRecordingM4A_andThereIsNoRecordingAlreadySavedForReference_savesNewAnswer() throws Exception {\n        TreeReference treeReference = new TreeReference();\n\n        File newRecording = File.createTempFile(\"new\", \".m4a\");\n        RecordingSession recordingSession = new RecordingSession(new HashSet<TreeReference>() {\n            {\n                add(treeReference);\n            }\n        }, newRecording, 0, 0, false);\n\n        recordingHandler.handle(formController, recordingSession, success -> {\n        });\n\n        verify(formDef).setValue(\n                new StringData(questionMediaManager.getAnswerFile(newRecording.getName()).getName()),\n                treeReference,\n                false\n        );\n\n        verifyNoInteractions(m4aAppender);\n    }\n\n    @Test\n    public void whenBackgroundRecordingM4A_andRecordingAlreadySavedForReference_appendsAudioFiles() throws Exception {\n        File recording = File.createTempFile(\"existing\", \".m4a\");\n        existingRecording = questionMediaManager.createAnswerFile(recording).getValue().getOrNull();\n        assertThat(existingRecording, not(nullValue()));\n\n        TreeReference treeReference = new TreeReference();\n\n        File newRecording = File.createTempFile(\"new\", \".m4a\");\n        RecordingSession recordingSession = new RecordingSession(new HashSet<TreeReference>() {\n            {\n                add(treeReference);\n            }\n        }, newRecording, 0, 0, false);\n\n        recordingHandler.handle(formController, recordingSession, success -> {\n        });\n\n        verify(m4aAppender).append(existingRecording, questionMediaManager.getAnswerFile(newRecording.getName()));\n        verifyNoInteractions(formDef);\n    }\n\n    @Test\n    public void whenBackgroundRecordingM4A_andRecordingAlreadySavedForReferenceButTheAudioFileDoesNotExist_savesNewAnswer() throws Exception {\n        File recording = File.createTempFile(\"existing\", \".m4a\");\n        existingRecording = questionMediaManager.createAnswerFile(recording).getValue().getOrNull();\n        assertThat(existingRecording, not(nullValue()));\n\n        existingRecording.delete();\n\n        TreeReference treeReference = new TreeReference();\n\n        File newRecording = File.createTempFile(\"new\", \".m4a\");\n        RecordingSession recordingSession = new RecordingSession(new HashSet<TreeReference>() {\n            {\n                add(treeReference);\n            }\n        }, newRecording, 0, 0, false);\n\n        recordingHandler.handle(formController, recordingSession, success -> {\n        });\n\n        verify(formDef).setValue(\n                new StringData(questionMediaManager.getAnswerFile(newRecording.getName()).getName()),\n                treeReference,\n                false\n        );\n\n        verifyNoInteractions(m4aAppender);\n    }\n\n    @Test\n    public void whenBackgroundRecordingAMR_andThereIsNoRecordingAlreadySavedForReference_savesNewAnswer() throws Exception {\n        TreeReference treeReference = new TreeReference();\n\n        File newRecording = File.createTempFile(\"new\", \".amr\");\n        RecordingSession recordingSession = new RecordingSession(new HashSet<TreeReference>() {\n            {\n                add(treeReference);\n            }\n        }, newRecording, 0, 0, false);\n\n        recordingHandler.handle(formController, recordingSession, success -> {\n        });\n\n        verify(formDef).setValue(\n                new StringData(questionMediaManager.getAnswerFile(newRecording.getName()).getName()),\n                treeReference,\n                false\n        );\n\n        verifyNoInteractions(amrAppender);\n    }\n\n    @Test\n    public void whenBackgroundRecordingAMR_andRecordingAlreadySavedForReference_appendsAudioFiles() throws Exception {\n        File recording = File.createTempFile(\"existing\", \".amr\");\n        existingRecording = questionMediaManager.createAnswerFile(recording).getValue().getOrNull();\n        assertThat(existingRecording, not(nullValue()));\n\n        TreeReference treeReference = new TreeReference();\n\n        File newRecording = File.createTempFile(\"new\", \".amr\");\n        RecordingSession recordingSession = new RecordingSession(new HashSet<TreeReference>() {\n            {\n                add(treeReference);\n            }\n        }, newRecording, 0, 0, false);\n\n        recordingHandler.handle(formController, recordingSession, success -> {\n        });\n\n        verify(amrAppender).append(existingRecording, questionMediaManager.getAnswerFile(newRecording.getName()));\n        verifyNoInteractions(formDef);\n    }\n\n    @Test\n    public void whenBackgroundRecordingAMR_andRecordingAlreadySavedForReferenceButTheAudioFileDoesNotExist_savesNewAnswer() throws Exception {\n        File recording = File.createTempFile(\"existing\", \".amr\");\n        existingRecording = questionMediaManager.createAnswerFile(recording).getValue().getOrNull();\n        assertThat(existingRecording, not(nullValue()));\n\n        existingRecording.delete();\n\n        TreeReference treeReference = new TreeReference();\n\n        File newRecording = File.createTempFile(\"new\", \".amr\");\n        RecordingSession recordingSession = new RecordingSession(new HashSet<TreeReference>() {\n            {\n                add(treeReference);\n            }\n        }, newRecording, 0, 0, false);\n\n        recordingHandler.handle(formController, recordingSession, success -> {\n        });\n\n        verify(formDef).setValue(\n                new StringData(questionMediaManager.getAnswerFile(newRecording.getName()).getName()),\n                treeReference,\n                false\n        );\n\n        verifyNoInteractions(amrAppender);\n    }\n\n    @Test\n    public void whenBackgroundRecording_andRecordingAlreadySavedForReference_deletesNewFile() throws Exception {\n        File recording = File.createTempFile(\"existing\", \".m4a\");\n        existingRecording = questionMediaManager.createAnswerFile(recording).getValue().getOrNull();\n        assertThat(existingRecording, not(nullValue()));\n\n        TreeReference treeReference = new TreeReference();\n\n        File newRecording = File.createTempFile(\"new\", \".m4a\");\n        RecordingSession recordingSession = new RecordingSession(new HashSet<TreeReference>() {\n            {\n                add(treeReference);\n            }\n        }, newRecording, 0, 0, false);\n\n        recordingHandler.handle(formController, recordingSession, success -> {\n        });\n\n        assertThat(questionMediaManager.getAnswerFile(newRecording.getName()).exists(), is(false));\n    }\n\n    private class TestFormController extends StubFormController {\n        @Override\n        public FormDef getFormDef() {\n            return formDef;\n        }\n\n        @Override\n        public IAnswerData getAnswer(@Nullable TreeReference treeReference) {\n            return existingRecording == null ? null : new StringData(existingRecording.getName());\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/RefreshFormListDialogFragmentTest.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\n\nimport android.content.DialogInterface;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.testing.FragmentScenario;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\n@RunWith(AndroidJUnit4.class)\npublic class RefreshFormListDialogFragmentTest {\n\n    @Rule\n    public FragmentScenarioLauncherRule launcherRule = new FragmentScenarioLauncherRule();\n\n    @Test\n    public void dialogIsNotCancellable() {\n        FragmentScenario<RefreshFormListDialogFragment> fragmentScenario = launcherRule.launch(RefreshFormListDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            assertThat(fragment.isCancelable(), equalTo(false));\n        });\n    }\n\n    @Test\n    public void clickingCancel_calls_onCancelFormLoading() {\n        FragmentScenario<RefreshFormListDialogFragment> fragmentScenario = launcherRule.launch(RefreshFormListDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            fragment.listener = mock(RefreshFormListDialogFragment.RefreshFormListDialogFragmentListener.class);\n\n            AlertDialog dialog = (AlertDialog) fragment.getDialog();\n            dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick();\n            RobolectricHelpers.runLooper();\n            verify(fragment.listener).onCancelFormLoading();\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/SaveFormProgressDialogFragmentTest.java",
    "content": "package org.odk.collect.android.formentry;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport androidx.annotation.NonNull;\nimport androidx.fragment.app.testing.FragmentScenario;\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.lifecycle.viewmodel.CreationExtras;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.saving.FormSaveViewModel;\nimport org.odk.collect.android.formentry.saving.SaveFormProgressDialogFragment;\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder;\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule;\n\n@RunWith(AndroidJUnit4.class)\npublic class SaveFormProgressDialogFragmentTest {\n\n    private final FormSaveViewModel formSaveViewModel = mock(FormSaveViewModel.class);\n    private final ViewModelProvider.Factory viewModelFactory = new ViewModelProvider.Factory() {\n        @NonNull\n        @Override\n        public <T extends ViewModel> T create(@NonNull Class<T> modelClass, @NonNull CreationExtras extras) {\n            return (T) formSaveViewModel;\n        }\n    };\n\n    @Rule\n    public FragmentScenarioLauncherRule launcherRule = new FragmentScenarioLauncherRule(\n            new FragmentFactoryBuilder()\n                    .forClass(SaveFormProgressDialogFragment.class, () -> new SaveFormProgressDialogFragment(viewModelFactory))\n                    .build()\n    );\n\n    @Before\n    public void setup() {\n        when(formSaveViewModel.getSaveResult()).thenReturn(new MutableLiveData<>());\n    }\n\n    @Test\n    public void dialogIsNotCancellable() {\n        FragmentScenario<SaveFormProgressDialogFragment> fragmentScenario = launcherRule.launch(SaveFormProgressDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            assertThat(fragment.isCancelable(), equalTo(false));\n        });\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/audit/AsyncTaskAuditEventWriterTest.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.formentry.audit;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.apache.commons.io.FileUtils;\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.LooperMode;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static java.util.Arrays.asList;\nimport static org.junit.Assert.assertEquals;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.CHANGE_REASON;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.END_OF_FORM;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_EXIT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_FINALIZE;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_RESUME;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_SAVE;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_START;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.HIERARCHY;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PERMISSIONS_GRANTED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PROVIDERS_ENABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_TRACKING_ENABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.PROMPT_NEW_REPEAT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.QUESTION;\n\n@RunWith(AndroidJUnit4.class)\n@LooperMode(LooperMode.Mode.LEGACY)\npublic class AsyncTaskAuditEventWriterTest {\n\n    private File auditFile;\n\n    @Before\n    public void setup() throws Exception {\n        auditFile = File.createTempFile(\"audit\", \".csv\");\n    }\n\n    @Test\n    public void saveAuditWithLocation() throws Exception {\n        AsyncTaskAuditEventWriter writer = new AsyncTaskAuditEventWriter(auditFile, true, false, false, false);\n        writer.writeEvents(getSampleAuditEventsWithLocations());\n\n        String expectedAuditContent = FileUtils.readFileToString(auditFile);\n        String expectedData = \"event,node,start,end,latitude,longitude,accuracy\\n\" +\n                \"form start,,1548106927319,,,,\\n\" +\n                \"location tracking enabled,,548108908250,,,,\\n\" +\n                \"location permissions granted,,548108908255,,,,\\n\" +\n                \"location providers enabled,,548108908259,,,,\\n\" +\n                \"question,/data/q1,1548106927323,1548106930112,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"add repeat,/data/g1[1],1548106930118,1548106931611,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"question,/data/g1[1]/q2,1548106931612,1548106937122,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"add repeat,/data/g1[2],1548106937123,1548106938276,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"question,/data/g1[2]/q2,1548106938277,1548106948127,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"add repeat,/data/g1[3],1548106948128,1548106949446,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"end screen,,1548106949448,1548106953601,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form save,,1548106953600,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form exit,,1548106953601,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form finalize,,1548106953601,,54.4112062,18.5896652,30.716999053955078\\n\";\n        assertEquals(expectedData, expectedAuditContent);\n    }\n\n    @Test\n    public void saveAuditWithLocationAndTrackingChanges() throws Exception {\n        AsyncTaskAuditEventWriter writer = new AsyncTaskAuditEventWriter(auditFile, true, true, false, false);\n        writer.writeEvents(getSampleAuditEventsWithLocationsAndTrackingChanges());\n\n        String expectedAuditContent = FileUtils.readFileToString(auditFile);\n        String expectedData = \"event,node,start,end,latitude,longitude,accuracy,old-value,new-value\\n\" +\n                \"form start,,1548106927319,,,,,,\\n\" +\n                \"location tracking enabled,,548108908250,,,,,,\\n\" +\n                \"location permissions granted,,548108908255,,,,,,\\n\" +\n                \"location providers enabled,,548108908259,,,,,,\\n\" +\n                \"question,/data/q1,1548106927323,1548106930112,54.4112062,18.5896652,30.716999053955078,Old value,New Value\\n\" +\n                \"add repeat,/data/g1[1],1548106930118,1548106931611,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"end screen,,1548106949448,1548106953601,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"form save,,1548106953600,,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"form exit,,1548106953601,,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"form finalize,,1548106953601,,54.4112062,18.5896652,30.716999053955078,,\\n\";\n        assertEquals(expectedData, expectedAuditContent);\n    }\n\n    @Test\n    public void saveAuditWithUser() throws Exception {\n        AsyncTaskAuditEventWriter writer = new AsyncTaskAuditEventWriter(auditFile, false, false, true, false);\n        writer.writeEvents(getSampleAuditEventsWithUser());\n\n        String expectedAuditContent = FileUtils.readFileToString(auditFile);\n        String expectedData = \"event,node,start,end,user\\n\" +\n                \"form start,,1548106927319,,User1\\n\" +\n                \"question,/data/q1,1548106927323,1548106930112,User1\\n\" +\n                \"add repeat,/data/g1[1],1548106930118,1548106931611,User1\\n\" +\n                \"question,/data/g1[1]/q2,1548106931612,1548106937122,User1\\n\" +\n                \"add repeat,/data/g1[2],1548106937123,1548106938276,User1\\n\" +\n                \"question,/data/g1[2]/q2,1548106938277,1548106948127,User1\\n\" +\n                \"add repeat,/data/g1[3],1548106948128,1548106949446,User1\\n\" +\n                \"end screen,,1548106949448,1548106953601,User1\\n\" +\n                \"form save,,1548106953600,,User1\\n\" +\n                \"form exit,,1548106953601,,User1\\n\" +\n                \"form finalize,,1548106953601,,User1\\n\";\n\n        assertEquals(expectedData, expectedAuditContent);\n    }\n\n    @Test\n    public void saveAuditWithChangeReason() throws Exception {\n        AsyncTaskAuditEventWriter writer = new AsyncTaskAuditEventWriter(auditFile, false, false, false, true);\n        writer.writeEvents(asList(\n                new AuditEvent(1548108900606L, FORM_RESUME, null, null, null, null),\n                new AuditEvent(1548108900606L, CHANGE_REASON, null, null, null, \"A good reason\")\n        ));\n\n        String auditContent = FileUtils.readFileToString(auditFile);\n        String expectedData = \"event,node,start,end,change-reason\\n\" +\n                \"form resume,,1548108900606,,\\n\" +\n                \"change reason,,1548108900606,,A good reason\\n\";\n        assertEquals(expectedData, auditContent);\n    }\n\n    @Test\n    public void whenChangeReasonHasCommaOrQuotes_escapesThem() throws Exception {\n        AsyncTaskAuditEventWriter writer = new AsyncTaskAuditEventWriter(auditFile, false, false, false, true);\n        writer.writeEvents(asList(\n                new AuditEvent(1548108900606L, FORM_RESUME, null, null, null, null),\n                new AuditEvent(1548108900606L, CHANGE_REASON, null, null, null, \"A \\\"good\\\", reason\")\n        ));\n\n        String auditContent = FileUtils.readFileToString(auditFile);\n        String expectedData = \"event,node,start,end,change-reason\\n\" +\n                \"form resume,,1548108900606,,\\n\" +\n                \"change reason,,1548108900606,,\\\"A \\\"\\\"good\\\"\\\", reason\\\"\\n\";\n        assertEquals(expectedData, auditContent);\n    }\n\n    @Test\n    public void whenUserHasCommaOrQuotes_escapesThem() throws Exception {\n        AsyncTaskAuditEventWriter writer = new AsyncTaskAuditEventWriter(auditFile, false, false, true, false);\n\n        List<AuditEvent> auditEvents = getSampleAuditEventsWithUser().subList(0, 1);\n        auditEvents.get(0).setUser(\"User,\\\"1\\\"\");\n        writer.writeEvents(auditEvents);\n\n        String expectedAuditContent = FileUtils.readFileToString(auditFile);\n        String expectedData = \"event,node,start,end,user\\n\" +\n                \"form start,,1548106927319,,\\\"User,\\\"\\\"1\\\"\\\"\\\"\\n\";\n        assertEquals(expectedData, expectedAuditContent);\n    }\n\n    /**\n     * A user could update the app and then resume form entry. In this case it would be possible\n     * for the form to have an audit config that wasn't supported by the old app. In this case\n     * the writer should update the header to account for the new data.\n     */\n    @Test\n    public void whenAppUpdatedBetweenInstances_updatesHeader() throws Exception {\n        // Use a form with enabled audit but without location\n        AsyncTaskAuditEventWriter writer = new AsyncTaskAuditEventWriter(auditFile, false, false, false, false);\n        writer.writeEvents(getSampleAuditEventsWithoutLocations());\n\n        String expectedAuditContent = FileUtils.readFileToString(auditFile);\n        String expectedData = \"event,node,start,end\\n\" +\n                \"form start,,1548106927319,\\n\" +\n                \"question,/data/q1,1548106927323,1548106930112\\n\" +\n                \"add repeat,/data/g1[1],1548106930118,1548106931611\\n\" +\n                \"question,/data/g1[1]/q2,1548106931612,1548106937122\\n\" +\n                \"add repeat,/data/g1[2],1548106937123,1548106938276\\n\" +\n                \"question,/data/g1[2]/q2,1548106938277,1548106948127\\n\" +\n                \"add repeat,/data/g1[3],1548106948128,1548106949446\\n\" +\n                \"end screen,,1548106949448,1548106953601\\n\" +\n                \"form save,,1548106953600,\\n\" +\n                \"form exit,,1548106953601,\\n\" +\n                \"form finalize,,1548106953601,\\n\";\n        assertEquals(expectedData, expectedAuditContent);\n\n        // Upgrade a form to use location\n        writer = new AsyncTaskAuditEventWriter(auditFile, true, false, false, false);\n        writer.writeEvents(getMoreSampleAuditEventsWithLocations());\n\n        expectedAuditContent = FileUtils.readFileToString(auditFile);\n        String expectedData2 = \"event,node,start,end,latitude,longitude,accuracy\\n\" +\n                \"form start,,1548106927319,\\n\" +\n                \"question,/data/q1,1548106927323,1548106930112\\n\" +\n                \"add repeat,/data/g1[1],1548106930118,1548106931611\\n\" +\n                \"question,/data/g1[1]/q2,1548106931612,1548106937122\\n\" +\n                \"add repeat,/data/g1[2],1548106937123,1548106938276\\n\" +\n                \"question,/data/g1[2]/q2,1548106938277,1548106948127\\n\" +\n                \"add repeat,/data/g1[3],1548106948128,1548106949446\\n\" +\n                \"end screen,,1548106949448,1548106953601\\n\" +\n                \"form save,,1548106953600,\\n\" +\n                \"form exit,,1548106953601,\\n\" +\n                \"form finalize,,1548106953601,\\n\" +\n                \"form resume,,1548108900606,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"jump,,1548108906276,1548108908206,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"location tracking enabled,,548108908250,,,,\\n\" +\n                \"location permissions granted,,548108908255,,,,\\n\" +\n                \"location providers enabled,,548108908259,,,,\\n\" +\n                \"end screen,,1548108908285,1548108909730,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form save,,1548108909730,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form exit,,1548108909730,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form finalize,,1548108909731,,54.4112062,18.5896652,30.716999053955078\\n\";\n        assertEquals(expectedData2, expectedAuditContent);\n\n        // Upgrade a form to use location and tracking changes\n        writer = new AsyncTaskAuditEventWriter(auditFile, true, true, false, false);\n        writer.writeEvents(getMoreSampleAuditEventsWithLocationsAndTrackingChanges());\n\n        expectedAuditContent = FileUtils.readFileToString(auditFile);\n        String expectedData3 = \"event,node,start,end,latitude,longitude,accuracy,old-value,new-value\\n\" +\n                \"form start,,1548106927319,\\n\" +\n                \"question,/data/q1,1548106927323,1548106930112\\n\" +\n                \"add repeat,/data/g1[1],1548106930118,1548106931611\\n\" +\n                \"question,/data/g1[1]/q2,1548106931612,1548106937122\\n\" +\n                \"add repeat,/data/g1[2],1548106937123,1548106938276\\n\" +\n                \"question,/data/g1[2]/q2,1548106938277,1548106948127\\n\" +\n                \"add repeat,/data/g1[3],1548106948128,1548106949446\\n\" +\n                \"end screen,,1548106949448,1548106953601\\n\" +\n                \"form save,,1548106953600,\\n\" +\n                \"form exit,,1548106953601,\\n\" +\n                \"form finalize,,1548106953601,\\n\" +\n                \"form resume,,1548108900606,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"jump,,1548108906276,1548108908206,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"location tracking enabled,,548108908250,,,,\\n\" +\n                \"location permissions granted,,548108908255,,,,\\n\" +\n                \"location providers enabled,,548108908259,,,,\\n\" +\n                \"end screen,,1548108908285,1548108909730,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form save,,1548108909730,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form exit,,1548108909730,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form finalize,,1548108909731,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form resume,,1548108900606,,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"question,,1548108900700,,54.4112062,18.5896652,30.716999053955078,Old value,New value\\n\" +\n                \"question,,1548108903100,,54.4112062,18.5896652,30.716999053955078,\\\"Old value, with comma\\\",New value\\n\" +\n                \"question,,1548108903101,,54.4112062,18.5896652,30.716999053955078,\\\"Old value \\n with linebreak\\\",\\\"New value \\n with linebreak and \\\"\\\"quotes\\\"\\\"\\\"\\n\" +\n                \"question,,1548108904200,,54.4112062,18.5896652,30.716999053955078,Old value,\\\"New value, with comma\\\"\\n\" +\n                \"form save,,1548108909730,,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"form exit,,1548108909730,,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"form finalize,,1548108909731,,54.4112062,18.5896652,30.716999053955078,,\\n\";\n        assertEquals(expectedData3, expectedAuditContent);\n\n        // Upgrade a form to use location and tracking changes and user\n        writer = new AsyncTaskAuditEventWriter(auditFile, true, true, true, false);\n        writer.writeEvents(getMoreSampleAuditEventsWithLocationsAndTrackingChangesAndUser());\n\n        expectedAuditContent = FileUtils.readFileToString(auditFile);\n        String expectedData4 = \"event,node,start,end,latitude,longitude,accuracy,old-value,new-value,user\\n\" +\n                \"form start,,1548106927319,\\n\" +\n                \"question,/data/q1,1548106927323,1548106930112\\n\" +\n                \"add repeat,/data/g1[1],1548106930118,1548106931611\\n\" +\n                \"question,/data/g1[1]/q2,1548106931612,1548106937122\\n\" +\n                \"add repeat,/data/g1[2],1548106937123,1548106938276\\n\" +\n                \"question,/data/g1[2]/q2,1548106938277,1548106948127\\n\" +\n                \"add repeat,/data/g1[3],1548106948128,1548106949446\\n\" +\n                \"end screen,,1548106949448,1548106953601\\n\" +\n                \"form save,,1548106953600,\\n\" +\n                \"form exit,,1548106953601,\\n\" +\n                \"form finalize,,1548106953601,\\n\" +\n                \"form resume,,1548108900606,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"jump,,1548108906276,1548108908206,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"location tracking enabled,,548108908250,,,,\\n\" +\n                \"location permissions granted,,548108908255,,,,\\n\" +\n                \"location providers enabled,,548108908259,,,,\\n\" +\n                \"end screen,,1548108908285,1548108909730,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form save,,1548108909730,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form exit,,1548108909730,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form finalize,,1548108909731,,54.4112062,18.5896652,30.716999053955078\\n\" +\n                \"form resume,,1548108900606,,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"question,,1548108900700,,54.4112062,18.5896652,30.716999053955078,Old value,New value\\n\" +\n                \"question,,1548108903100,,54.4112062,18.5896652,30.716999053955078,\\\"Old value, with comma\\\",New value\\n\" +\n                \"question,,1548108903101,,54.4112062,18.5896652,30.716999053955078,\\\"Old value \\n with linebreak\\\",\\\"New value \\n with linebreak and \\\"\\\"quotes\\\"\\\"\\\"\\n\" +\n                \"question,,1548108904200,,54.4112062,18.5896652,30.716999053955078,Old value,\\\"New value, with comma\\\"\\n\" +\n                \"form save,,1548108909730,,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"form exit,,1548108909730,,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"form finalize,,1548108909731,,54.4112062,18.5896652,30.716999053955078,,\\n\" +\n                \"form resume,,1548108900606,,54.4112062,18.5896652,30.716999053955078,,,User1\\n\" +\n                \"question,,1548108900700,,54.4112062,18.5896652,30.716999053955078,Old value,New value,User1\\n\" +\n                \"question,,1548108903100,,54.4112062,18.5896652,30.716999053955078,\\\"Old value, with comma\\\",New value,User1\\n\" +\n                \"question,,1548108903101,,54.4112062,18.5896652,30.716999053955078,\\\"Old value \\n with linebreak\\\",\\\"New value \\n with linebreak and \\\"\\\"quotes\\\"\\\"\\\",User1\\n\" +\n                \"question,,1548108904200,,54.4112062,18.5896652,30.716999053955078,Old value,\\\"New value, with comma\\\",User1\\n\" +\n                \"form save,,1548108909730,,54.4112062,18.5896652,30.716999053955078,,,User1\\n\" +\n                \"form exit,,1548108909730,,54.4112062,18.5896652,30.716999053955078,,,User1\\n\" +\n                \"form finalize,,1548108909731,,54.4112062,18.5896652,30.716999053955078,,,User1\\n\";\n        assertEquals(expectedData4, expectedAuditContent);\n    }\n\n    private List<AuditEvent> getSampleAuditEventsWithUser() {\n        List<AuditEvent> auditEvents = getSampleAuditEventsWithoutLocations();\n        for (AuditEvent event : auditEvents) {\n            event.setUser(\"User1\");\n        }\n\n        return auditEvents;\n    }\n\n    private List<AuditEvent> getMoreSampleAuditEventsWithLocationsAndTrackingChangesAndUser() {\n        List<AuditEvent> auditEvents = getMoreSampleAuditEventsWithLocationsAndTrackingChanges();\n        for (AuditEvent event : auditEvents) {\n            event.setUser(\"User1\");\n        }\n\n        return auditEvents;\n    }\n\n    private List<AuditEvent> getSampleAuditEventsWithoutLocations() {\n        AuditEvent event;\n        ArrayList<AuditEvent> auditEvents = new ArrayList<>();\n        auditEvents.add(new AuditEvent(1548106927319L, FORM_START));\n        event = new AuditEvent(1548106927323L, QUESTION, getTestFormIndex(\"/data/q1\"), \"\", null, null);\n        event.setEnd(1548106930112L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106930118L, PROMPT_NEW_REPEAT, getTestFormIndex(\"/data/g1[1]\"), \"\", null, null);\n        event.setEnd(1548106931611L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106931612L, QUESTION, getTestFormIndex(\"/data/g1[1]/q2\"), \"\", null, null);\n        event.setEnd(1548106937122L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106937123L, PROMPT_NEW_REPEAT, getTestFormIndex(\"/data/g1[2]\"), \"\", null, null);\n        event.setEnd(1548106938276L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106938277L, QUESTION, getTestFormIndex(\"/data/g1[2]/q2\"), \"\", null, null);\n        event.setEnd(1548106948127L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106948128L, PROMPT_NEW_REPEAT, getTestFormIndex(\"/data/g1[3]\"), \"\", null, null);\n        event.setEnd(1548106949446L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106949448L, END_OF_FORM);\n        event.setEnd(1548106953601L);\n        auditEvents.add(event);\n        auditEvents.add(new AuditEvent(1548106953600L, FORM_SAVE));\n        auditEvents.add(new AuditEvent(1548106953601L, FORM_EXIT));\n        auditEvents.add(new AuditEvent(1548106953601L, FORM_FINALIZE));\n        return auditEvents;\n    }\n\n    private ArrayList<AuditEvent> getMoreSampleAuditEventsWithLocations() {\n        AuditEvent event;\n        ArrayList<AuditEvent> auditEvents = new ArrayList<>();\n        event = new AuditEvent(1548108900606L, FORM_RESUME);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108906276L, HIERARCHY);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548108908206L);\n        auditEvents.add(event);\n        event = new AuditEvent(548108908250L, LOCATION_TRACKING_ENABLED);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(548108908255L, LOCATION_PERMISSIONS_GRANTED);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(548108908259L, LOCATION_PROVIDERS_ENABLED);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108908285L, END_OF_FORM);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548108909730L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548108909730L, FORM_SAVE);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108909730L, FORM_EXIT);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108909731L, FORM_FINALIZE);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        return auditEvents;\n    }\n\n    private ArrayList<AuditEvent> getMoreSampleAuditEventsWithLocationsAndTrackingChanges() {\n        AuditEvent event;\n        ArrayList<AuditEvent> auditEvents = new ArrayList<>();\n        event = new AuditEvent(1548108900606L, FORM_RESUME);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108900700L, QUESTION, null, \"Old value\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.recordValueChange(\"New value\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108903100L, QUESTION, null, \"Old value, with comma\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.recordValueChange(\"New value\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108903101L, QUESTION, null, \"Old value \\n with linebreak\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.recordValueChange(\"New value \\n with linebreak and \\\"quotes\\\"\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108904200L, QUESTION, null, \"Old value\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.recordValueChange(\"New value, with comma\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108909730L, FORM_SAVE);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108909730L, FORM_EXIT);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548108909731L, FORM_FINALIZE);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        return auditEvents;\n    }\n\n    private ArrayList<AuditEvent> getSampleAuditEventsWithLocations() {\n        ArrayList<AuditEvent> auditEvents = new ArrayList<>();\n        AuditEvent event;\n        event = new AuditEvent(1548106927319L, FORM_START);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(548108908250L, LOCATION_TRACKING_ENABLED);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(548108908255L, LOCATION_PERMISSIONS_GRANTED);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(548108908259L, LOCATION_PROVIDERS_ENABLED);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548106927323L, QUESTION, getTestFormIndex(\"/data/q1\"), \"\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548106930112L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106930118L, PROMPT_NEW_REPEAT, getTestFormIndex(\"/data/g1[1]\"), \"\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548106931611L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106931612L, QUESTION, getTestFormIndex(\"/data/g1[1]/q2\"), \"\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548106937122L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106937123L, PROMPT_NEW_REPEAT, getTestFormIndex(\"/data/g1[2]\"), \"\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548106938276L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106938277L, QUESTION, getTestFormIndex(\"/data/g1[2]/q2\"), \"\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548106948127L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106948128L, PROMPT_NEW_REPEAT, getTestFormIndex(\"/data/g1[3]\"), \"\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548106949446L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106949448L, END_OF_FORM);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548106953601L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106953600L, FORM_SAVE);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548106953601L, FORM_EXIT);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548106953601L, FORM_FINALIZE);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        return auditEvents;\n    }\n\n    private ArrayList<AuditEvent> getSampleAuditEventsWithLocationsAndTrackingChanges() {\n        ArrayList<AuditEvent> auditEvents = new ArrayList<>();\n        AuditEvent event;\n        event = new AuditEvent(1548106927319L, FORM_START);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(548108908250L, LOCATION_TRACKING_ENABLED);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(548108908255L, LOCATION_PERMISSIONS_GRANTED);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(548108908259L, LOCATION_PROVIDERS_ENABLED);\n        event.setLocationCoordinates(\"\", \"\", \"\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548106927323L, QUESTION, getTestFormIndex(\"/data/q1\"), \"Old value\", null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.recordValueChange(\"New Value\");\n        event.setEnd(1548106930112L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106930118L, PROMPT_NEW_REPEAT, getTestFormIndex(\"/data/g1[1]\"), null, null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548106931611L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106949448L, END_OF_FORM, null, null, null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        event.setEnd(1548106953601L);\n        auditEvents.add(event);\n        event = new AuditEvent(1548106953600L, FORM_SAVE, null, null, null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548106953601L, FORM_EXIT, null, null, null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        event = new AuditEvent(1548106953601L, FORM_FINALIZE, null, null, null, null);\n        event.setLocationCoordinates(\"54.4112062\", \"18.5896652\", \"30.716999053955078\");\n        auditEvents.add(event);\n        return auditEvents;\n    }\n\n    /**\n     * Given an XPath path, generate a corresponding {@link TreeReference} and a fake\n     * {@link FormIndex} that doesn't correspond to any real form definition. The only thing we care\n     * about for the {@link FormIndex} are the instance indexes at every level. Everything else can\n     * be faked.\n     * <p>\n     * TODO: once {@link AuditEvent}'s getXPathPath moves to FormIndex, just use a mock\n     */\n    private FormIndex getTestFormIndex(String xpathPath) {\n        String[] nodes = xpathPath.split(\"/\");\n        TreeReference treeReference = new TreeReference();\n        nodes = Arrays.copyOfRange(nodes, 1, nodes.length); // take care of leading /\n        ArrayList<Integer> positions = new ArrayList<>();\n\n        for (String node : nodes) {\n            String[] parts = node.split(\"\\\\[\");\n\n            String nodeName = parts[0];\n            int position = 0;\n            if (parts.length > 1) {\n                position = Integer.parseInt(parts[1].replace(\"]\", \"\")) - 1;\n                positions.add(position);\n            } else {\n                positions.add(-1);\n            }\n            treeReference.add(nodeName, position);\n        }\n\n        FormIndex formIndex = null;\n        for (int i = nodes.length - 1; i > 0; i--) { // exclude the root node\n            formIndex = new FormIndex(formIndex, -1, positions.get(i), treeReference);\n        }\n\n        return formIndex;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/audit/AuditConfigTest.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.formentry.audit;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.odk.collect.location.LocationClient.Priority.PRIORITY_BALANCED_POWER_ACCURACY;\nimport static org.odk.collect.location.LocationClient.Priority.PRIORITY_HIGH_ACCURACY;\nimport static org.odk.collect.location.LocationClient.Priority.PRIORITY_LOW_POWER;\nimport static org.odk.collect.location.LocationClient.Priority.PRIORITY_NO_POWER;\n\npublic class AuditConfigTest {\n\n    @Test\n    public void testParameters() {\n        AuditConfig auditConfig = new AuditConfig.Builder().setMode(\"high-accuracy\").setLocationMinInterval(\"10\").setLocationMaxAge(\"60\").setIsTrackingChangesEnabled(true).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n\n        assertTrue(auditConfig.isTrackingChangesEnabled());\n        assertTrue(auditConfig.isLocationEnabled());\n        assertEquals(PRIORITY_HIGH_ACCURACY, auditConfig.getLocationPriority());\n        assertEquals(10000, auditConfig.getLocationMinInterval().intValue());\n        assertEquals(60000, auditConfig.getLocationMaxAge().intValue());\n\n        auditConfig = new AuditConfig.Builder().setMode(\"high-accuracy\").setLocationMinInterval(\"0\").setLocationMaxAge(\"60\").setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n\n        assertFalse(auditConfig.isTrackingChangesEnabled());\n        assertTrue(auditConfig.isLocationEnabled());\n        assertEquals(PRIORITY_HIGH_ACCURACY, auditConfig.getLocationPriority());\n        assertEquals(1000, auditConfig.getLocationMinInterval().intValue());\n        assertEquals(60000, auditConfig.getLocationMaxAge().intValue());\n    }\n\n    @Test\n    public void logLocationCoordinatesOnlyIfAllParametersAreSet() {\n        AuditConfig auditConfig = new AuditConfig.Builder().setMode(\"high-accuracy\").setLocationMinInterval(\"10\").setLocationMaxAge(\"60\").setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertTrue(auditConfig.isLocationEnabled());\n        auditConfig = new AuditConfig.Builder().setMode(null).setLocationMinInterval(\"10\").setLocationMaxAge(\"60\").setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertFalse(auditConfig.isLocationEnabled());\n        auditConfig = new AuditConfig.Builder().setMode(null).setLocationMinInterval(null).setLocationMaxAge(\"60\").setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertFalse(auditConfig.isLocationEnabled());\n        auditConfig = new AuditConfig.Builder().setMode(null).setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertFalse(auditConfig.isLocationEnabled());\n        auditConfig = new AuditConfig.Builder().setMode(\"balanced\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertFalse(auditConfig.isLocationEnabled());\n        auditConfig = new AuditConfig.Builder().setMode(\"balanced\").setLocationMinInterval(\"10\").setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertFalse(auditConfig.isLocationEnabled());\n        auditConfig = new AuditConfig.Builder().setMode(\"balanced\").setLocationMinInterval(null).setLocationMaxAge(\"60\").setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertFalse(auditConfig.isLocationEnabled());\n        auditConfig = new AuditConfig.Builder().setMode(null).setLocationMinInterval(null).setLocationMaxAge(\"60\").setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertFalse(auditConfig.isLocationEnabled());\n    }\n\n    @Test\n    public void testPriorities() {\n        AuditConfig auditConfig = new AuditConfig.Builder().setMode(\"high_accuracy\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_HIGH_ACCURACY, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"high-accuracy\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_HIGH_ACCURACY, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"HIGH_ACCURACY\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_HIGH_ACCURACY, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"balanced\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_BALANCED_POWER_ACCURACY, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"BALANCED\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_BALANCED_POWER_ACCURACY, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"low_power\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_LOW_POWER, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"low-power\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_LOW_POWER, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"low_POWER\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_LOW_POWER, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"no_power\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_NO_POWER, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"no-power\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_NO_POWER, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"NO_power\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_NO_POWER, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"qwerty\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_HIGH_ACCURACY, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(\"\").setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertEquals(PRIORITY_HIGH_ACCURACY, auditConfig.getLocationPriority());\n        auditConfig = new AuditConfig.Builder().setMode(null).setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n        assertNull(auditConfig.getLocationPriority());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/audit/AuditEventCSVLineTest.java",
    "content": "package org.odk.collect.android.formentry.audit;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.junit.Test;\n\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.BEGINNING_OF_FORM;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.CONSTRAINT_ERROR;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.DELETE_REPEAT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.END_OF_FORM;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FINALIZE_ERROR;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_EXIT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_FINALIZE;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_RESUME;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_SAVE;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_START;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.GOOGLE_PLAY_SERVICES_NOT_AVAILABLE;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.GROUP;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.HIERARCHY;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PERMISSIONS_GRANTED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PERMISSIONS_NOT_GRANTED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PROVIDERS_DISABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PROVIDERS_ENABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_TRACKING_DISABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_TRACKING_ENABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.PROMPT_NEW_REPEAT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.QUESTION;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.REPEAT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.SAVE_ERROR;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.UNKNOWN_EVENT_TYPE;\nimport static org.odk.collect.android.formentry.audit.AuditEventCSVLine.toCSVLine;\n\npublic class AuditEventCSVLineTest {\n\n    private static final long START_TIME = 1545392727685L;\n    private static final long END_TIME = 1545392728527L;\n\n    //region CSV spec (https://tools.ietf.org/html/rfc4180)\n    @Test\n    public void commas_shouldBeSurroundedByQuotes() {\n        AuditEvent auditEvent = new AuditEvent(1L, QUESTION, getTestFormIndex(), \"a, b\", \"c, d\", \"e, f\");\n        auditEvent.recordValueChange(\"g, h\");\n        auditEvent.setEnd(2L);\n        String csvLine = toCSVLine(auditEvent, false, true, true);\n        assertThat(csvLine, is(\"question,/data/text1,1,2,\\\"a, b\\\",\\\"g, h\\\",\\\"c, d\\\",\\\"e, f\\\"\"));\n    }\n\n    @Test\n    public void newlines_shouldBeSurroundedByQuotes() {\n        AuditEvent auditEvent = new AuditEvent(1L, QUESTION, getTestFormIndex(), \"a\\nb\", \"c\\nd\", \"e\\nf\");\n        auditEvent.recordValueChange(\"g\\nh\");\n        auditEvent.setEnd(2L);\n        String csvLine = toCSVLine(auditEvent, false, true, true);\n        assertThat(csvLine, is(\"question,/data/text1,1,2,\\\"a\\nb\\\",\\\"g\\nh\\\",\\\"c\\nd\\\",\\\"e\\nf\\\"\"));\n    }\n\n    @Test\n    public void quotes_shouldBeEscaped_andSurroundedByQuotes() {\n        AuditEvent auditEvent = new AuditEvent(1L, QUESTION, getTestFormIndex(), \"a\\\"b\", \"c\\\"d\", \"e\\\"f\");\n        auditEvent.recordValueChange(\"g\\\"h\");\n        auditEvent.setEnd(2L);\n        String csvLine = toCSVLine(auditEvent, false, true, true);\n        assertThat(csvLine, is(\"question,/data/text1,1,2,\\\"a\\\"\\\"b\\\",\\\"g\\\"\\\"h\\\",\\\"c\\\"\\\"d\\\",\\\"e\\\"\\\"f\\\"\"));\n    }\n    //endregion\n\n    @Test\n    public void toString_() {\n        AuditEvent auditEvent = new AuditEvent(START_TIME, QUESTION, getTestFormIndex(), \"\", null, null);\n        assertNotNull(auditEvent);\n        assertTrue(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"question,/data/text1,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n        assertFalse(auditEvent.isEndTimeSet());\n        auditEvent.setEnd(END_TIME);\n        assertTrue(auditEvent.isEndTimeSet());\n        assertFalse(auditEvent.isLocationAlreadySet());\n        assertEquals(\"question,/data/text1,1545392727685,1545392728527\", toCSVLine(auditEvent, false, false, false));\n    }\n\n    @Test\n    public void toString_withLocationCoordinates() {\n        AuditEvent auditEvent = new AuditEvent(START_TIME, QUESTION, getTestFormIndex(), \"\", null, null);\n        assertNotNull(auditEvent);\n        auditEvent.setLocationCoordinates(\"54.35202520000001\", \"18.64663840000003\", \"10\");\n        assertTrue(auditEvent.isIntervalAuditEventType());\n        assertFalse(auditEvent.isEndTimeSet());\n        auditEvent.setEnd(END_TIME);\n        assertTrue(auditEvent.isEndTimeSet());\n        assertTrue(auditEvent.isLocationAlreadySet());\n        assertEquals(\"question,/data/text1,1545392727685,1545392728527,54.35202520000001,18.64663840000003,10\", toCSVLine(auditEvent, true, false, false));\n    }\n\n    @Test\n    public void toString_withTrackingChanges() {\n        AuditEvent auditEvent = new AuditEvent(START_TIME, QUESTION, getTestFormIndex(), \"First answer\", null, null);\n        assertNotNull(auditEvent);\n        assertTrue(auditEvent.isIntervalAuditEventType());\n        assertFalse(auditEvent.isEndTimeSet());\n        auditEvent.setEnd(END_TIME);\n        assertTrue(auditEvent.isEndTimeSet());\n        assertFalse(auditEvent.isLocationAlreadySet());\n        auditEvent.recordValueChange(\"Second answer\");\n        assertEquals(\"question,/data/text1,1545392727685,1545392728527,First answer,Second answer\", toCSVLine(auditEvent, false, true, false));\n    }\n\n    @Test\n    public void toString_withLocationCoordinates_andTrackingChanges() {\n        AuditEvent auditEvent = new AuditEvent(START_TIME, QUESTION, getTestFormIndex(), \"First answer\", null, null);\n        assertNotNull(auditEvent);\n        auditEvent.setLocationCoordinates(\"54.35202520000001\", \"18.64663840000003\", \"10\");\n        assertTrue(auditEvent.isIntervalAuditEventType());\n        assertFalse(auditEvent.isEndTimeSet());\n        auditEvent.setEnd(END_TIME);\n        assertTrue(auditEvent.isEndTimeSet());\n        assertTrue(auditEvent.isLocationAlreadySet());\n        auditEvent.recordValueChange(\"Second, answer\");\n        assertEquals(\"question,/data/text1,1545392727685,1545392728527,54.35202520000001,18.64663840000003,10,First answer,\\\"Second, answer\\\"\", toCSVLine(auditEvent, true, true, false));\n    }\n\n    @Test\n    public void toStringNullValues() {\n        AuditEvent auditEvent = new AuditEvent(START_TIME, QUESTION, getTestFormIndex(), \"Old value\", null, null);\n        assertNotNull(auditEvent);\n        auditEvent.setLocationCoordinates(\"\", \"\", \"\");\n        assertTrue(auditEvent.isIntervalAuditEventType());\n        assertFalse(auditEvent.isEndTimeSet());\n        auditEvent.setEnd(END_TIME);\n        assertTrue(auditEvent.isEndTimeSet());\n        assertFalse(auditEvent.isLocationAlreadySet());\n        auditEvent.recordValueChange(\"New value\");\n        assertEquals(\"question,/data/text1,1545392727685,1545392728527,,,,Old value,New value\", toCSVLine(auditEvent, true, true, false));\n    }\n\n    @Test\n    public void testEventTypes() {\n        AuditEvent auditEvent = new AuditEvent(START_TIME, QUESTION);\n        assertNotNull(auditEvent);\n        assertTrue(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"question,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, FORM_START);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"form start,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, END_OF_FORM);\n        assertNotNull(auditEvent);\n        assertTrue(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"end screen,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, REPEAT);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"repeat,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, PROMPT_NEW_REPEAT);\n        assertNotNull(auditEvent);\n        assertTrue(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"add repeat,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, GROUP);\n        assertNotNull(auditEvent);\n        assertTrue(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"group questions,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, BEGINNING_OF_FORM);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"beginning of form,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, FORM_EXIT);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"form exit,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, FORM_RESUME);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"form resume,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, FORM_SAVE);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"form save,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, FORM_FINALIZE);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"form finalize,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, HIERARCHY);\n        assertNotNull(auditEvent);\n        assertTrue(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"jump,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, SAVE_ERROR);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"save error,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, FINALIZE_ERROR);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"finalize error,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, CONSTRAINT_ERROR);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"constraint error,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, DELETE_REPEAT);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"delete repeat,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, GOOGLE_PLAY_SERVICES_NOT_AVAILABLE);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"google play services not available,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, LOCATION_PERMISSIONS_GRANTED);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"location permissions granted,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, LOCATION_PERMISSIONS_NOT_GRANTED);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"location permissions not granted,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, LOCATION_TRACKING_ENABLED);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"location tracking enabled,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, LOCATION_TRACKING_DISABLED);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"location tracking disabled,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, LOCATION_PROVIDERS_ENABLED);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"location providers enabled,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, LOCATION_PROVIDERS_DISABLED);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"location providers disabled,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n\n        auditEvent = new AuditEvent(START_TIME, UNKNOWN_EVENT_TYPE);\n        assertNotNull(auditEvent);\n        assertFalse(auditEvent.isIntervalAuditEventType());\n        assertEquals(\"Unknown AuditEvent Type,,1545392727685,\", toCSVLine(auditEvent, false, false, false));\n    }\n\n    private FormIndex getTestFormIndex() {\n        TreeReference treeReference = new TreeReference();\n        treeReference.add(\"data\", 0);\n        treeReference.add(\"text1\", 0);\n\n        return new FormIndex(0, treeReference);\n    }\n}"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/audit/AuditEventLoggerTest.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.formentry.audit;\n\nimport android.location.Location;\n\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.BEGINNING_OF_FORM;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.CHANGE_REASON;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.CONSTRAINT_ERROR;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.DELETE_REPEAT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.END_OF_FORM;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FINALIZE_ERROR;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_FINALIZE;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_RESUME;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_SAVE;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.FORM_START;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.GOOGLE_PLAY_SERVICES_NOT_AVAILABLE;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.GROUP;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.HIERARCHY;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PERMISSIONS_GRANTED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PERMISSIONS_NOT_GRANTED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PROVIDERS_DISABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_PROVIDERS_ENABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_TRACKING_DISABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.LOCATION_TRACKING_ENABLED;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.PROMPT_NEW_REPEAT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.QUESTION;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.REPEAT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.SAVE_ERROR;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.UNKNOWN_EVENT_TYPE;\n\npublic class AuditEventLoggerTest {\n\n    // All values are set so location coordinates should be collected\n    private final AuditConfig testAuditConfig = new AuditConfig.Builder().setMode(\"high-priority\").setLocationMinInterval(\"10\").setLocationMaxAge(\"60\").setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n    // At least one value is not set so location coordinates shouldn't be collected\n    private final AuditConfig testAuditConfigWithNullValues = new AuditConfig.Builder().setMode(\"high-priority\").setLocationMinInterval(\"10\").setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(false).createAuditConfig();\n\n    private final TestWriter testWriter = new TestWriter();\n    private final FormController formController = mock(FormController.class);\n\n    @Before\n    public void setup() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(\"The answer\")\n                .build();\n        when(formController.getQuestionPrompt(any())).thenReturn(prompt);\n    }\n\n    @Test\n    public void whenAuditConfigIsNull_doesntWriteEvents() {\n        AuditEventLogger auditEventLogger = new AuditEventLogger(null, testWriter, formController);\n\n        auditEventLogger.logEvent(END_OF_FORM, false, 0);\n        auditEventLogger.flush(); // Triggers event writing\n        assertEquals(0, testWriter.auditEvents.size());\n    }\n\n    @Test\n    public void usesMostAccurateLocationForEvents() {\n        final AuditEventLogger auditEventLogger = new AuditEventLogger(testAuditConfig, testWriter, formController);\n\n        Location location1 = mock(Location.class);\n        when(location1.getLatitude()).thenReturn(54.380746599999995);\n        when(location1.getLongitude()).thenReturn(18.606523);\n        when(location1.getAccuracy()).thenReturn(5f);\n        when(location1.getTime()).thenReturn(1548156641000L);\n        auditEventLogger.addLocation(location1);\n\n        Location location2 = mock(Location.class);\n        when(location2.getLatitude()).thenReturn(54.37971080550665);\n        when(location2.getLongitude()).thenReturn(18.612874470947304);\n        when(location2.getAccuracy()).thenReturn(10f);\n        when(location2.getTime()).thenReturn(1548156655000L);\n        auditEventLogger.addLocation(location2);\n\n        Location location3 = mock(Location.class);\n        when(location3.getLatitude()).thenReturn(54.3819102504987);\n        when(location3.getLongitude()).thenReturn(18.62025591015629);\n        when(location3.getAccuracy()).thenReturn(12f);\n        when(location3.getTime()).thenReturn(1548156665000L);\n        auditEventLogger.addLocation(location3);\n\n        Location location4 = mock(Location.class);\n        when(location4.getLatitude()).thenReturn(54.38620882544086);\n        when(location4.getLongitude()).thenReturn(18.62523409008793);\n        when(location4.getAccuracy()).thenReturn(7f);\n        when(location4.getTime()).thenReturn(1548156688000L);\n        auditEventLogger.addLocation(location4);\n\n        Location location5 = mock(Location.class);\n        when(location5.getLatitude()).thenReturn(54.39070685202294);\n        when(location5.getLongitude()).thenReturn(18.617166005371132);\n        when(location5.getAccuracy()).thenReturn(20f);\n        when(location5.getTime()).thenReturn(1548156710000L);\n        auditEventLogger.addLocation(location5);\n\n        auditEventLogger.logEvent(END_OF_FORM, false, 1548156712000L);\n        auditEventLogger.flush();\n\n        AuditEvent auditEvent = testWriter.auditEvents.get(0);\n        assertEquals(String.valueOf(location4.getLatitude()), auditEvent.getLatitude());\n        assertEquals(String.valueOf(location4.getLongitude()), auditEvent.getLongitude());\n        assertEquals(String.valueOf(location4.getAccuracy()), auditEvent.getAccuracy());\n    }\n\n    @Test\n    public void expiresLocationsOlderThan60Seconds() {\n        final AuditEventLogger auditEventLogger = new AuditEventLogger(testAuditConfig, testWriter, formController);\n\n        Location location1 = mock(Location.class);\n        when(location1.getLatitude()).thenReturn(54.380746599999995);\n        when(location1.getLongitude()).thenReturn(18.606523);\n        when(location1.getAccuracy()).thenReturn(2f);\n        when(location1.getTime()).thenReturn(0L);\n        auditEventLogger.addLocation(location1);\n\n        Location location2 = mock(Location.class);\n        when(location2.getLatitude()).thenReturn(54.37971080550665);\n        when(location2.getLongitude()).thenReturn(18.612874470947304);\n        when(location2.getAccuracy()).thenReturn(1f);\n        when(location2.getTime()).thenReturn(61 * 1000L);\n        auditEventLogger.addLocation(location2);\n\n        auditEventLogger.logEvent(END_OF_FORM, false, 120 * 1000L);\n        auditEventLogger.flush();\n\n        AuditEvent auditEvent = testWriter.auditEvents.get(0);\n        assertEquals(String.valueOf(location2.getLatitude()), auditEvent.getLatitude());\n        assertEquals(String.valueOf(location2.getLongitude()), auditEvent.getLongitude());\n        assertEquals(String.valueOf(location2.getAccuracy()), auditEvent.getAccuracy());\n    }\n\n    @Test\n    public void whenNoLocationSet_doesntAddedLocationToEvents() {\n        AuditEventLogger auditEventLogger = new AuditEventLogger(testAuditConfigWithNullValues, testWriter, formController);\n\n        auditEventLogger.logEvent(END_OF_FORM, false, 0);\n        auditEventLogger.flush(); // Triggers event writing\n\n        assertFalse(testWriter.auditEvents.get(0).isLocationAlreadySet());\n    }\n\n    @Test\n    public void isDuplicateOfLastAuditEventTest() {\n        AuditEventLogger auditEventLogger = new AuditEventLogger(testAuditConfig, testWriter, formController);\n        auditEventLogger.logEvent(LOCATION_PROVIDERS_ENABLED, false, 0);\n        assertTrue(auditEventLogger.isDuplicateOfLastLocationEvent(LOCATION_PROVIDERS_ENABLED));\n        auditEventLogger.logEvent(LOCATION_PROVIDERS_DISABLED, false, 0);\n        assertTrue(auditEventLogger.isDuplicateOfLastLocationEvent(LOCATION_PROVIDERS_DISABLED));\n        assertFalse(auditEventLogger.isDuplicateOfLastLocationEvent(LOCATION_PROVIDERS_ENABLED));\n\n        auditEventLogger.flush(); // Triggers event writing\n        assertEquals(2, testWriter.auditEvents.size());\n    }\n\n    @Test\n    public void withUserSet_addsUserToEvents() {\n        AuditEventLogger auditEventLogger = new AuditEventLogger(new AuditConfig.Builder().setMode(null).setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(true).setIsTrackChangesReasonEnabled(false).createAuditConfig(), testWriter, formController);\n        auditEventLogger.setUser(\"Riker\");\n\n        auditEventLogger.logEvent(END_OF_FORM, false, 0);\n        auditEventLogger.flush(); // Triggers event writing\n\n        assertEquals(\"Riker\", testWriter.auditEvents.get(0).getUser());\n    }\n\n    @Test\n    public void logEvent_WithChangeReason_addsChangeReasonToEvent() {\n        AuditEventLogger auditEventLogger = new AuditEventLogger(new AuditConfig.Builder().setMode(null).setLocationMinInterval(null).setLocationMaxAge(null).setIsTrackingChangesEnabled(false).setIsIdentifyUserEnabled(false).setIsTrackChangesReasonEnabled(true).createAuditConfig(), testWriter, formController);\n\n        auditEventLogger.logEvent(CHANGE_REASON, null, false, null, 123L, \"Blah\");\n        auditEventLogger.flush(); // Triggers event writing\n\n        assertEquals(\"Blah\", testWriter.auditEvents.get(0).getChangeReason());\n    }\n\n    @Test\n    public void testEventTypes() {\n        AuditEventLogger auditEventLogger = new AuditEventLogger(testAuditConfig, testWriter, formController);\n\n        auditEventLogger.logEvent(BEGINNING_OF_FORM, false, 0); //shouldn't be logged\n        auditEventLogger.logEvent(QUESTION, false, 0);\n        auditEventLogger.logEvent(GROUP, false, 0);\n        auditEventLogger.logEvent(PROMPT_NEW_REPEAT, false, 0);\n        auditEventLogger.logEvent(REPEAT, false, 0); //shouldn't be logged\n        auditEventLogger.logEvent(END_OF_FORM, false, 0);\n        auditEventLogger.logEvent(FORM_START, false, 0);\n        auditEventLogger.logEvent(FORM_RESUME, false, 0);\n        auditEventLogger.logEvent(FORM_SAVE, false, 0);\n        auditEventLogger.logEvent(FORM_FINALIZE, false, 0);\n        auditEventLogger.logEvent(HIERARCHY, false, 0);\n        auditEventLogger.logEvent(SAVE_ERROR, false, 0);\n        auditEventLogger.logEvent(FINALIZE_ERROR, false, 0);\n        auditEventLogger.logEvent(CONSTRAINT_ERROR, false, 0);\n        auditEventLogger.logEvent(DELETE_REPEAT, false, 0);\n        auditEventLogger.logEvent(GOOGLE_PLAY_SERVICES_NOT_AVAILABLE, false, 0);\n        auditEventLogger.logEvent(LOCATION_PERMISSIONS_GRANTED, false, 0);\n        auditEventLogger.logEvent(LOCATION_PERMISSIONS_NOT_GRANTED, false, 0);\n        auditEventLogger.logEvent(LOCATION_TRACKING_ENABLED, false, 0);\n        auditEventLogger.logEvent(LOCATION_TRACKING_DISABLED, false, 0);\n        auditEventLogger.logEvent(LOCATION_PROVIDERS_ENABLED, false, 0);\n        auditEventLogger.logEvent(LOCATION_PROVIDERS_DISABLED, false, 0);\n        auditEventLogger.logEvent(UNKNOWN_EVENT_TYPE, false, 0);\n\n        auditEventLogger.flush(); // Triggers event writing\n        assertEquals(21, testWriter.auditEvents.size());\n    }\n\n    private static class TestWriter implements AuditEventLogger.AuditEventWriter {\n\n        List<AuditEvent> auditEvents = new ArrayList<>();\n\n        @Override\n        public void writeEvents(List<AuditEvent> auditEvents) {\n            this.auditEvents.addAll(auditEvents);\n        }\n\n        @Override\n        public boolean isWriting() {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/audit/AuditEventTest.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.formentry.audit;\n\nimport org.javarosa.form.api.FormEntryController;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.BEGINNING_OF_FORM;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.END_OF_FORM;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.GROUP;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.PROMPT_NEW_REPEAT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.REPEAT;\nimport static org.odk.collect.android.formentry.audit.AuditEvent.AuditEventType.UNKNOWN_EVENT_TYPE;\n\npublic class AuditEventTest {\n\n    @Test\n    public void getAuditEventTypeFromFecTypeTest() {\n        assertEquals(BEGINNING_OF_FORM, AuditEvent.getAuditEventTypeFromFecType(FormEntryController.EVENT_BEGINNING_OF_FORM));\n        assertEquals(GROUP, AuditEvent.getAuditEventTypeFromFecType(FormEntryController.EVENT_GROUP));\n        assertEquals(REPEAT, AuditEvent.getAuditEventTypeFromFecType(FormEntryController.EVENT_REPEAT));\n        assertEquals(PROMPT_NEW_REPEAT, AuditEvent.getAuditEventTypeFromFecType(FormEntryController.EVENT_PROMPT_NEW_REPEAT));\n        assertEquals(END_OF_FORM, AuditEvent.getAuditEventTypeFromFecType(FormEntryController.EVENT_END_OF_FORM));\n        assertEquals(UNKNOWN_EVENT_TYPE, AuditEvent.getAuditEventTypeFromFecType(100));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java",
    "content": "package org.odk.collect.android.formentry.audit;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.formentry.saving.FormSaveViewModel.SaveResult.State.CHANGE_REASON_REQUIRED;\nimport static org.odk.collect.android.formentry.saving.FormSaveViewModel.SaveResult.State.CONSTRAINT_ERROR;\nimport static org.odk.collect.android.formentry.saving.FormSaveViewModel.SaveResult.State.FINALIZE_ERROR;\nimport static org.odk.collect.android.formentry.saving.FormSaveViewModel.SaveResult.State.SAVED;\nimport static org.odk.collect.android.formentry.saving.FormSaveViewModel.SaveResult.State.SAVE_ERROR;\nimport static org.odk.collect.android.formentry.saving.FormSaveViewModel.SaveResult.State.SAVING;\nimport static org.odk.collect.android.formentry.saving.FormSaveViewModel.SaveResult.State.WAITING_TO_SAVE;\nimport static org.odk.collect.androidshared.livedata.LiveDataUtils.liveDataOf;\n\nimport android.net.Uri;\n\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.lifecycle.SavedStateHandle;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport com.google.common.io.Files;\n\nimport org.javarosa.form.api.FormEntryController;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.InOrder;\nimport org.odk.collect.android.formentry.FormSession;\nimport org.odk.collect.android.formentry.saving.FormSaveViewModel;\nimport org.odk.collect.android.formentry.saving.FormSaver;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.projects.ProjectsDataService;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.tasks.SaveFormToDisk;\nimport org.odk.collect.android.tasks.SaveToDiskResult;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.entities.storage.EntitiesRepository;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.forms.savepoints.SavepointsRepository;\nimport org.odk.collect.formstest.InMemInstancesRepository;\nimport org.odk.collect.formstest.InMemSavepointsRepository;\nimport org.odk.collect.projects.Project;\nimport org.odk.collect.shared.DebugLogger;\nimport org.odk.collect.shared.TempFiles;\nimport org.odk.collect.testshared.FakeScheduler;\nimport org.odk.collect.utilities.Result;\nimport org.robolectric.Robolectric;\nimport org.robolectric.annotation.LooperMode;\n\nimport java.io.File;\nimport java.util.ArrayList;\n\n@RunWith(AndroidJUnit4.class)\n@LooperMode(LooperMode.Mode.LEGACY)\npublic class FormSaveViewModelTest {\n    private static final long CURRENT_TIME = 123L;\n\n    private final SavedStateHandle savedStateHandle = new SavedStateHandle();\n    private final FakeFormSaver formSaver = new FakeFormSaver();\n    private final FakeScheduler scheduler = new FakeScheduler();\n\n    private AuditEventLogger logger;\n    private FormSaveViewModel viewModel;\n    private MediaUtils mediaUtils;\n    private FormController formController;\n    private Form form;\n    private AudioRecorder audioRecorder;\n    private ProjectsDataService projectsDataService;\n\n    private final EntitiesRepository entitiesRepository = mock(EntitiesRepository.class);\n\n    private final InstancesRepository instancesRepository = new InMemInstancesRepository();\n    private final SavepointsRepository savepointsRepository = new InMemSavepointsRepository();\n    private MutableLiveData<FormSession> formSession;\n\n    @Before\n    public void setup() {\n        // Useful given some methods will execute AsyncTasks\n        Robolectric.getBackgroundThreadScheduler().pause();\n\n        formController = mock(FormController.class);\n        form = mock(Form.class);\n        logger = mock(AuditEventLogger.class);\n        mediaUtils = mock(MediaUtils.class);\n\n        File instanceFile = new File(TempFiles.getPathInTempDir());\n        when(formController.getInstanceFile()).thenReturn(instanceFile);\n        when(formController.getAuditEventLogger()).thenReturn(logger);\n        when(logger.isChangeReasonRequired()).thenReturn(false);\n\n        audioRecorder = mock(AudioRecorder.class);\n        projectsDataService = mock(ProjectsDataService.class);\n        when(projectsDataService.requireCurrentProject()).thenReturn(Project.Companion.getDEMO_PROJECT());\n\n        formSession = new MutableLiveData<>(new FormSession(formController, form));\n        viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, audioRecorder, projectsDataService, formSession, entitiesRepository, instancesRepository, savepointsRepository, mock(), mock());\n\n        CollectHelpers.createDemoProject(); // Needed to deal with `new StoragePathProvider()` calls in `FormSaveViewModel`\n    }\n\n    @Test\n    public void saveForm_returnsSaveResult_inSavingState() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), true, \"\", false);\n        FormSaveViewModel.SaveResult saveResult1 = viewModel.getSaveResult().getValue();\n        assertThat(saveResult1.getState(), equalTo(SAVING));\n    }\n\n    @Test\n    public void saveForm_wontRunMultipleSavesAtOnce() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), true, \"\", false);\n        viewModel.saveForm(Uri.parse(\"file://form\"), true, \"\", false);\n\n        whenFormSaverFinishes(SaveFormToDisk.SAVED);\n        assertThat(formSaver.numberOfTimesCalled, equalTo(1));\n\n        Robolectric.getBackgroundThreadScheduler().advanceToLastPostedRunnable(); // Run any other queued tasks\n\n        assertThat(formSaver.numberOfTimesCalled, equalTo(1));\n    }\n\n    @Test\n    public void saveForm_whenReasonRequiredToSave_returnsSaveResult_inChangeReasonRequiredState() {\n        whenReasonRequiredToSave();\n\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n        viewModel.saveForm(Uri.parse(\"file://form\"), true, \"\", false);\n        assertThat(saveResult.getValue().getState(), equalTo(CHANGE_REASON_REQUIRED));\n    }\n\n    @Test\n    public void saveForm_whenReasonRequiredToSave_andAudioIsRecording_andExiting_returnsSaveResult_inChangeReasonRequiredState() {\n        whenReasonRequiredToSave();\n        when(audioRecorder.isRecording()).thenReturn(true);\n\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n        viewModel.saveForm(Uri.parse(\"file://form\"), true, \"\", false);\n        assertThat(saveResult.getValue().getState(), equalTo(CHANGE_REASON_REQUIRED));\n    }\n\n    @Test\n    public void whenFormSaverFinishes_saved_setsSaveResultState_toSaved() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), true, \"\", false);\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n\n        whenFormSaverFinishes(SaveFormToDisk.SAVED);\n        assertThat(saveResult.getValue().getState(), equalTo(SAVED));\n    }\n\n    @Test\n    public void whenFormSaverFinishes_whenViewExiting_logsFormSaveAndFormExitAuditEventAfterFlush() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", true);\n\n        whenFormSaverFinishes(SaveFormToDisk.SAVED);\n\n        InOrder verifier = inOrder(logger);\n        verifier.verify(logger).flush();\n        verifier.verify(logger).logEvent(\n                AuditEvent.AuditEventType.FORM_SAVE,\n                false,\n                CURRENT_TIME\n        );\n        verifier.verify(logger).logEvent(AuditEvent.AuditEventType.FORM_EXIT, true, CURRENT_TIME);\n    }\n\n    @Test\n    public void whenFormSaverFinishes_whenFormComplete_andViewExiting_logsFormExitAndFinalizeAuditEventsAfterFlush() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), true, \"\", true);\n\n        whenFormSaverFinishes(SaveFormToDisk.SAVED);\n\n        InOrder verifier = inOrder(logger);\n        verifier.verify(logger).flush();\n        verifier.verify(logger).logEvent(\n                AuditEvent.AuditEventType.FORM_SAVE,\n                false,\n                CURRENT_TIME\n        );\n        verifier.verify(logger).logEvent(AuditEvent.AuditEventType.FORM_EXIT, false, CURRENT_TIME);\n        verifier.verify(logger).logEvent(AuditEvent.AuditEventType.FORM_FINALIZE, true, CURRENT_TIME);\n    }\n\n    @Test\n    public void whenFormSaverFinishes_savedAndExit_setsSaveResultState_toSaved() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n\n        whenFormSaverFinishes(SaveFormToDisk.SAVED_AND_EXIT);\n        assertThat(saveResult.getValue().getState(), equalTo(SAVED));\n    }\n\n    @Test\n    public void whenFormSaverFinishes_saveError_setSaveResultState_toSaveErrorWithMessage() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n\n        whenFormSaverFinishes(SaveFormToDisk.SAVE_ERROR, \"OH NO\");\n        assertThat(saveResult.getValue().getState(), equalTo(SAVE_ERROR));\n        assertThat(saveResult.getValue().getMessage(), equalTo(\"OH NO\"));\n    }\n\n    @Test\n    public void whenFormSaverFinishes_saveError_logsSaveErrorAuditEvenAfterFlush() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n\n        whenFormSaverFinishes(SaveFormToDisk.SAVE_ERROR);\n\n        InOrder verifier = inOrder(logger);\n        verifier.verify(logger).flush();\n        verifier.verify(logger).logEvent(AuditEvent.AuditEventType.SAVE_ERROR, true, CURRENT_TIME);\n    }\n\n    @Test\n    public void whenFormSaverFinishes_encryptionError_setSaveResultState_toFinalizeErrorWithMessage() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n\n        whenFormSaverFinishes(SaveFormToDisk.ENCRYPTION_ERROR, \"OH NO\");\n        assertThat(saveResult.getValue().getState(), equalTo(FINALIZE_ERROR));\n        assertThat(saveResult.getValue().getMessage(), equalTo(\"OH NO\"));\n    }\n\n    @Test\n    public void whenFormSaverFinishes_encryptionError_logsFinalizeErrorAuditEventAfterFlush() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n\n        whenFormSaverFinishes(SaveFormToDisk.ENCRYPTION_ERROR);\n\n        InOrder verifier = inOrder(logger);\n        verifier.verify(logger).flush();\n        verifier.verify(logger).logEvent(AuditEvent.AuditEventType.FINALIZE_ERROR, true, CURRENT_TIME);\n    }\n\n    @Test\n    public void whenFormSaverFinishes_answerConstraintViolated_setSaveResultState_toConstraintError() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n\n        whenFormSaverFinishes(FormEntryController.ANSWER_CONSTRAINT_VIOLATED);\n        assertThat(saveResult.getValue().getState(), equalTo(CONSTRAINT_ERROR));\n    }\n\n    @Test\n    public void whenFormSaverFinishes_answerConstraintViolated_finalizesAndLogsConstraintErrorAuditEvent() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n\n        whenFormSaverFinishes(FormEntryController.ANSWER_CONSTRAINT_VIOLATED);\n\n        InOrder verifier = inOrder(logger);\n        verifier.verify(logger).flush();\n        verifier.verify(logger).logEvent(AuditEvent.AuditEventType.CONSTRAINT_ERROR, true, CURRENT_TIME);\n    }\n\n    @Test\n    public void whenFormSaverFinishes_answerRequiredButEmpty_setSaveResultState_toConstraintError() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n\n        whenFormSaverFinishes(FormEntryController.ANSWER_REQUIRED_BUT_EMPTY);\n        assertThat(saveResult.getValue().getState(), equalTo(CONSTRAINT_ERROR));\n    }\n\n    @Test\n    public void whenFormSaverFinishes_isSaving_returnsFalse() {\n        assertThat(viewModel.isSaving(), equalTo(false));\n\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n        assertThat(viewModel.isSaving(), equalTo(true));\n\n        whenFormSaverFinishes(SaveFormToDisk.SAVED);\n        assertThat(viewModel.isSaving(), equalTo(false));\n    }\n\n    @Test\n    public void saveForm_savesCorrectFiles() {\n        viewModel.deleteAnswerFile(\"index\", \"blah\");\n        viewModel.replaceAnswerFile(\"index\", \"blah\");\n\n        viewModel.saveForm(Uri.parse(\"file://form\"), true, \"\", true);\n        whenFormSaverFinishes(SaveFormToDisk.SAVED);\n\n        assertThat(formSaver.tempFiles.contains(\"blah\"), equalTo(true));\n\n        viewModel.saveForm(Uri.parse(\"file://form\"), true, \"\", true);\n        whenFormSaverFinishes(SaveFormToDisk.SAVED);\n\n        assertThat(formSaver.tempFiles.isEmpty(), equalTo(true));\n    }\n\n    @Test\n    public void whenReasonRequiredToSave_resumeSave_setsSaveResultState_toSaving() {\n        whenReasonRequiredToSave();\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n\n        viewModel.setReason(\"blah\");\n        viewModel.resumeSave();\n        assertThat(saveResult.getValue().getState(), equalTo(SAVING));\n    }\n\n    @Test\n    public void whenReasonRequiredToSave_resumeSave_logsChangeReasonAuditEvent() {\n        whenReasonRequiredToSave();\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n\n        viewModel.setReason(\"Blah\");\n        viewModel.resumeSave();\n\n        verify(logger).logEvent(AuditEvent.AuditEventType.CHANGE_REASON, null, true, null, CURRENT_TIME, \"Blah\");\n    }\n\n    @Test\n    public void whenReasonRequiredToSave_resumeSave_whenReasonIsNotValid_doesNotSave() {\n        whenReasonRequiredToSave();\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n\n        viewModel.setReason(\"\");\n        viewModel.resumeSave();\n        assertThat(saveResult.getValue().getState(), equalTo(CHANGE_REASON_REQUIRED));\n\n        viewModel.setReason(\"  \");\n        viewModel.resumeSave();\n        assertThat(saveResult.getValue().getState(), equalTo(CHANGE_REASON_REQUIRED));\n    }\n\n    @Test\n    public void whenReasonRequiredToSave_andRecordingAudio_andExiting_resumeSave_savesRecording() {\n        whenReasonRequiredToSave();\n        when(audioRecorder.isRecording()).thenReturn(true);\n\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", true);\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n\n        viewModel.setReason(\"blah\");\n        viewModel.resumeSave();\n        assertThat(saveResult.getValue().getState(), equalTo(WAITING_TO_SAVE));\n    }\n\n    @Test\n    public void resumeFormEntry_clearsSaveResult() {\n        LiveData<FormSaveViewModel.SaveResult> saveResult = viewModel.getSaveResult();\n        viewModel.saveForm(Uri.parse(\"file://form\"), true, \"\", false);\n        viewModel.resumeFormEntry();\n        assertThat(saveResult.getValue(), equalTo(null));\n    }\n\n    @Test\n    public void ignoreChanges_whenThereAreUnsavedFiles_shouldDeleteThoseFiles() {\n        viewModel.replaceAnswerFile(\"index\", \"blah1\");\n        viewModel.ignoreChanges();\n\n        verify(mediaUtils).deleteMediaFile(\"blah1\");\n    }\n\n    @Test\n    public void ignoreChanges_whenAudioIsRecording_cleansUpAudioRecorder() {\n        when(audioRecorder.isRecording()).thenReturn(true);\n        viewModel.ignoreChanges();\n\n        verify(audioRecorder).cleanUp();\n    }\n\n    //region QuestionMediaManager implementation\n\n    /**\n     * Covers clearing an answer, adding a new answer and then clearing again - we'd never need\n     * to restore the new answer file in this case.\n     */\n    @Test\n    public void deleteAnswerFile_whenAnswerFileHasAlreadyBeenDeleted_actuallyDeletesNewFile() {\n        viewModel.deleteAnswerFile(\"index\", \"blah1\");\n        viewModel.deleteAnswerFile(\"index\", \"blah2\");\n\n        verify(mediaUtils).deleteMediaFile(\"blah2\");\n    }\n\n    @Test\n    public void deleteAnswerFile_whenAnswerFileHasAlreadyBeenDeleted_onRecreatingViewModel_actuallyDeletesNewFile() {\n        viewModel.deleteAnswerFile(\"index\", \"blah1\");\n\n        FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), projectsDataService, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, savepointsRepository, mock(), mock());\n        restoredViewModel.deleteAnswerFile(\"index\", \"blah2\");\n\n        verify(mediaUtils).deleteMediaFile(\"blah2\");\n    }\n\n    /**\n     * Covers replacing an answer, and then replacing an answer again - we'd never need\n     * to restore the first replacement in this case\n     */\n    @Test\n    public void replaceAnswerFile_whenAnswerFileHasAlreadyBeenReplaced_deletesPreviousReplacement() {\n        viewModel.replaceAnswerFile(\"index\", \"blah1\");\n        viewModel.replaceAnswerFile(\"index\", \"blah2\");\n\n        verify(mediaUtils).deleteMediaFile(\"blah1\");\n    }\n\n    @Test\n    public void replaceAnswerFile_whenAnswerFileHasAlreadyBeenReplaced_afterRecreatingViewModel_deletesPreviousReplacement() {\n        viewModel.replaceAnswerFile(\"index\", \"blah1\");\n\n        FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), projectsDataService, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, savepointsRepository, mock(), mock());\n        restoredViewModel.replaceAnswerFile(\"index\", \"blah2\");\n\n        verify(mediaUtils).deleteMediaFile(\"blah1\");\n    }\n\n    @Test\n    public void getAnswerFile_returnsFileFromInstance() {\n        File tempDir = Files.createTempDir();\n        when(formController.getInstanceFile()).thenReturn(new File(tempDir + File.separator + \"instance.xml\"));\n\n        File answerFile = viewModel.getAnswerFile(\"answer.file\");\n        assertThat(answerFile, is(new File(tempDir, \"answer.file\")));\n    }\n\n    @Test\n    public void createAnswerFile_copiesFileToInstanceFolder_andReturnsNewName() throws Exception {\n        File tempDir = Files.createTempDir();\n        when(formController.getInstanceFile()).thenReturn(new File(tempDir + File.separator + \"instance.xml\"));\n\n        File externalFile = File.createTempFile(\"external\", \".file\");\n        LiveData<Result<File>> answerFile = viewModel.createAnswerFile(externalFile);\n        scheduler.flush();\n\n        assertThat(tempDir.listFiles().length, is(1));\n        assertThat(answerFile.getValue().getOrNull().getName(), is(tempDir.listFiles()[0].getName()));\n    }\n\n    @Test\n    public void createAnswerFile_whenThereIsAnError_returnsNull_andSetsAnswerFileErrorToFilePath() throws Exception {\n        File tempDir = Files.createTempDir();\n        tempDir.setWritable(false);\n        when(formController.getInstanceFile()).thenReturn(new File(tempDir + File.separator + \"instance.xml\"));\n\n        File externalFile = File.createTempFile(\"external\", \".file\");\n        LiveData<Result<File>> answerFile = viewModel.createAnswerFile(externalFile);\n        scheduler.flush();\n\n        assertThat(answerFile.getValue().getOrNull(), nullValue());\n        assertThat(viewModel.getAnswerFileError().getValue(), equalTo(externalFile.getAbsolutePath()));\n    }\n\n    @Test\n    public void createAnswerFile_forSameFile_returnsSameName() throws Exception {\n        File tempDir = Files.createTempDir();\n        when(formController.getInstanceFile()).thenReturn(new File(tempDir + File.separator + \"instance.xml\"));\n\n        File externalFile = File.createTempFile(\"external\", \".file\");\n        LiveData<Result<File>> fileName1 = viewModel.createAnswerFile(externalFile);\n        scheduler.flush();\n        LiveData<Result<File>> fileName2 = viewModel.createAnswerFile(externalFile);\n        scheduler.flush();\n\n        assertThat(fileName1.getValue().getOrNull(), is(fileName2.getValue().getOrNull()));\n    }\n\n    //endregion\n\n    @Test\n    public void isSavingFileAnswerFile_isTrueWhenWhileIsSaving() throws Exception {\n        File tempDir = Files.createTempDir();\n        when(formController.getInstanceFile()).thenReturn(new File(tempDir + File.separator + \"instance.xml\"));\n\n        assertThat(viewModel.isSavingAnswerFile().getValue(), is(false));\n\n        viewModel.createAnswerFile(File.createTempFile(\"external\", \".file\"));\n        assertThat(viewModel.isSavingAnswerFile().getValue(), is(true));\n\n        scheduler.flush();\n        assertThat(viewModel.isSavingAnswerFile().getValue(), is(false));\n    }\n\n    @Test\n    public void ignoreChanges_whenFormControllerNotSet_doesNothing() {\n        FormSaveViewModel viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), projectsDataService, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, savepointsRepository, mock(), mock());\n        viewModel.ignoreChanges(); // Checks nothing explodes\n    }\n\n    @Test\n    public void getLastSavedTime_whenNewInstance_returnsNull() {\n        assertThat(viewModel.getLastSavedTime(), equalTo(null));\n    }\n\n    @Test\n    public void getLastSavedTime_whenInstanceNotSaved_returnsLastStatusChange() {\n        Instance instance = new Instance.Builder()\n                .lastStatusChangeDate(123L)\n                .build();\n\n        formSession.setValue(new FormSession(formController, form, instance));\n        assertThat(viewModel.getLastSavedTime(), equalTo(instance.getLastStatusChangeDate()));\n    }\n\n    @Test\n    public void getLastSavedTime_whenInstanceSaved_returnsLastStatusChange() {\n        viewModel.saveForm(Uri.parse(\"file://form\"), false, \"\", false);\n        whenFormSaverFinishes(SaveFormToDisk.SAVED);\n\n        assertThat(viewModel.getLastSavedTime(), equalTo(formSaver.instance.getLastStatusChangeDate()));\n    }\n\n    @Test\n    public void canBeFullyDiscarded_whenNewInstance_returnsTrue() {\n        assertThat(viewModel.canBeFullyDiscarded(), equalTo(true));\n    }\n\n    @Test\n    public void canBeFullyDiscarded_whenSavedInstance_returnsFalse() {\n        Instance instance = new Instance.Builder()\n                .lastStatusChangeDate(123L)\n                .status(Instance.STATUS_VALID)\n                .build();\n\n        formSession.setValue(new FormSession(formController, form, instance));\n        assertThat(viewModel.canBeFullyDiscarded(), equalTo(false));\n    }\n\n    @Test\n    public void canBeFullyDiscarded_whenNewlyEditedInstance_returnsTrue() {\n        Instance instance = new Instance.Builder()\n                .lastStatusChangeDate(123L)\n                .status(Instance.STATUS_NEW_EDIT)\n                .build();\n\n        formSession.setValue(new FormSession(formController, form, instance));\n        assertThat(viewModel.canBeFullyDiscarded(), equalTo(true));\n    }\n\n    private void whenReasonRequiredToSave() {\n        when(formController.isEditing()).thenReturn(true);\n        when(logger.isChangeReasonRequired()).thenReturn(true);\n    }\n\n    private void whenFormSaverFinishes(int result) {\n        whenFormSaverFinishes(result, null);\n    }\n\n    private void whenFormSaverFinishes(int result, String message) {\n        SaveToDiskResult saveToDiskResult = new SaveToDiskResult();\n        saveToDiskResult.setSaveResult(result, true);\n        saveToDiskResult.setSaveErrorMessage(message);\n\n        formSaver.saveToDiskResult = saveToDiskResult;\n        Robolectric.getBackgroundThreadScheduler().runOneTask();\n    }\n\n    public static class FakeFormSaver implements FormSaver {\n\n        public SaveToDiskResult saveToDiskResult;\n        public ArrayList<String> tempFiles;\n\n        public int numberOfTimesCalled;\n\n        public final Instance instance = new Instance.Builder()\n                .lastStatusChangeDate(123L)\n                .build();\n\n        @Override\n        public SaveToDiskResult save(Uri instanceContentURI, FormController formController, MediaUtils mediaUtils, boolean shouldFinalize,\n                                     boolean exitAfter, String updatedSaveName, ProgressListener progressListener, ArrayList<String> tempFiles, String currentProjectId, EntitiesRepository entitiesRepository, InstancesRepository instancesRepository, DebugLogger debugLogger) {\n            this.tempFiles = tempFiles;\n            numberOfTimesCalled++;\n\n            if (saveToDiskResult.getSaveResult() == SaveFormToDisk.SAVED) {\n                saveToDiskResult.setInstance(new Instance.Builder()\n                        .lastStatusChangeDate(123L)\n                        .build());\n            }\n\n            return saveToDiskResult;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/audit/IdentityPromptViewModelTest.java",
    "content": "package org.odk.collect.android.formentry.audit;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.javarosawrapper.FormController;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\n@RunWith(AndroidJUnit4.class)\npublic class IdentityPromptViewModelTest {\n\n    @Test\n    public void done_setsUserOnAuditEventLogger() {\n        FormController formController = mock(FormController.class);\n        AuditEventLogger auditEventLogger = mock(AuditEventLogger.class);\n        when(formController.getAuditEventLogger()).thenReturn(auditEventLogger);\n\n        IdentityPromptViewModel viewModel = new IdentityPromptViewModel();\n\n        viewModel.formLoaded(formController);\n        viewModel.setIdentity(\"Picard\");\n        viewModel.done();\n        verify(auditEventLogger).setUser(\"Picard\");\n    }\n}"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/backgroundlocation/BackgroundLocationManagerTest.java",
    "content": "package org.odk.collect.android.formentry.backgroundlocation;\n\nimport android.location.Location;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\nimport org.odk.collect.android.formentry.audit.AuditConfig;\nimport org.odk.collect.testshared.LocationTestUtils;\nimport org.odk.collect.android.location.client.FakeLocationClient;\nimport org.odk.collect.android.formentry.audit.AuditEvent;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\n@RunWith(AndroidJUnit4.class)\npublic class BackgroundLocationManagerTest {\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    private BackgroundLocationManager backgroundLocationManager;\n\n    private FakeLocationClient fakeLocationClient;\n    @Mock\n    private BackgroundLocationHelper locationHelper;\n\n    @Before\n    public void setUp() {\n        fakeLocationClient = new FakeLocationClient();\n        backgroundLocationManager = new BackgroundLocationManager(fakeLocationClient, locationHelper);\n    }\n\n    /**\n     * activityDisplayed is called from both onStart and loadingComplete in FormFillingActivity. This\n     * test confirms that it doesn't matter how many times activityDisplayed is called until the\n     * form has been loaded.\n     */\n    @Test\n    public void displayingActivityManyTimes_HasNoEffect_WhenFormNotLoaded() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n\n        assertThat(backgroundLocationManager.activityDisplayed(), is(nullValue()));\n        assertThat(backgroundLocationManager.isPendingPermissionCheck(), is(false));\n\n        assertThat(backgroundLocationManager.activityDisplayed(), is(nullValue()));\n        assertThat(backgroundLocationManager.isPendingPermissionCheck(), is(false));\n\n        verify(locationHelper, never()).logAuditEvent(Mockito.any());\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n\n        assertThat(backgroundLocationManager.isPendingPermissionCheck(), is(true));\n        verify(locationHelper, atLeastOnce()).logAuditEvent(Mockito.any());\n    }\n\n    @Test\n    public void locationPermissions_AreRequested_WhenFormRequestsBackgroundLocation_AndPreconditionsAreMet() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n\n        backgroundLocationManager.formFinishedLoading();\n\n        assertThat(backgroundLocationManager.activityDisplayed(), is(nullValue()));\n        verify(locationHelper, never()).logAuditEvent(AuditEvent.AuditEventType.LOCATION_TRACKING_DISABLED);\n        verify(locationHelper, never()).logAuditEvent(AuditEvent.AuditEventType.GOOGLE_PLAY_SERVICES_NOT_AVAILABLE);\n\n        assertThat(backgroundLocationManager.isPendingPermissionCheck(), is(true));\n    }\n\n    @Test\n    public void locationPermissionDenied_IsLogged_WhenFormAuditsLocation() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n\n        assertThat(backgroundLocationManager.isPendingPermissionCheck(), is(true));\n\n        backgroundLocationManager.locationPermissionDenied();\n\n        verify(locationHelper).logAuditEvent(AuditEvent.AuditEventType.LOCATION_PERMISSIONS_NOT_GRANTED);\n    }\n\n    @Test\n    public void locationPermissionGranted_IsLogged_WhenFormAuditsLocation() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n\n        assertThat(backgroundLocationManager.isPendingPermissionCheck(), is(true));\n\n        backgroundLocationManager.locationPermissionGranted();\n\n        verify(locationHelper).logAuditEvent(AuditEvent.AuditEventType.LOCATION_PERMISSIONS_GRANTED);\n    }\n\n    /**\n     * Simulates orientation change.\n     */\n    @Test\n    public void androidPermissionEvent_IsNotLogged_WhenActivityDisplayedMultipleTimes() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n        when(locationHelper.isAndroidLocationPermissionGranted()).thenReturn(true);\n        when(locationHelper.getCurrentFormAuditConfig()).thenReturn(new AuditConfig.Builder()\n                .setMode(\"foo\")\n                .setLocationMinInterval(\"2\")\n                .setLocationMaxAge(\"3\")\n                .setIsTrackingChangesEnabled(true)\n                .setIsIdentifyUserEnabled(false)\n                .setIsTrackChangesReasonEnabled(false)\n                .createAuditConfig());\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n        backgroundLocationManager.locationPermissionGranted();\n\n        // Activity displayed again without a permission change (e.g. orientation has changed)\n        backgroundLocationManager.activityHidden();\n        backgroundLocationManager.activityDisplayed();\n        verify(locationHelper, times(1)).logAuditEvent(AuditEvent.AuditEventType.LOCATION_PERMISSIONS_GRANTED);\n\n        Location location2 = LocationTestUtils.createLocation(\"GPS\", 7, 2, 3, 4);\n        fakeLocationClient.receiveFix(location2);\n        verify(locationHelper).provideLocationToAuditLogger(location2);\n    }\n\n    @Test\n    public void playServicesWarning_IsReturnedAndLogged_WhenFormRequestsBackgroundLocation_AndPlayServicesNotAvailable() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(false);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n\n        backgroundLocationManager.formFinishedLoading();\n        assertThat(backgroundLocationManager.activityDisplayed(), is(BackgroundLocationManager.BackgroundLocationMessage.PLAY_SERVICES_UNAVAILABLE));\n\n        verify(locationHelper).logAuditEvent(AuditEvent.AuditEventType.GOOGLE_PLAY_SERVICES_NOT_AVAILABLE);\n        verify(locationHelper, never()).logAuditEvent(AuditEvent.AuditEventType.LOCATION_TRACKING_DISABLED);\n    }\n\n    @Test\n    public void preferencesWarning_IsReturnedAndLogged_WhenFormRequestsBackgroundLocation_AndPreferenceDisabled() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(false);\n\n        backgroundLocationManager.formFinishedLoading();\n        assertThat(backgroundLocationManager.activityDisplayed(), is(BackgroundLocationManager.BackgroundLocationMessage.LOCATION_PREF_DISABLED));\n\n        verify(locationHelper).logAuditEvent(AuditEvent.AuditEventType.LOCATION_TRACKING_DISABLED);\n        verify(locationHelper, never()).logAuditEvent(AuditEvent.AuditEventType.GOOGLE_PLAY_SERVICES_NOT_AVAILABLE);\n    }\n\n    @Test\n    public void playServicesWarning_IsReturned_WhenFormRequestsBackgroundLocation_AndNoPreconditionMet() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(false);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(false);\n\n        backgroundLocationManager.formFinishedLoading();\n        assertThat(backgroundLocationManager.activityDisplayed(), is(BackgroundLocationManager.BackgroundLocationMessage.PLAY_SERVICES_UNAVAILABLE));\n\n        verify(locationHelper).logAuditEvent(AuditEvent.AuditEventType.LOCATION_TRACKING_DISABLED);\n        verify(locationHelper).logAuditEvent(AuditEvent.AuditEventType.GOOGLE_PLAY_SERVICES_NOT_AVAILABLE);\n    }\n\n    @Test\n    public void providersWarning_IsReturned_WhenFormRequestsLocationAudit_AndNoProviderAvailable() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n        when(locationHelper.isAndroidLocationPermissionGranted()).thenReturn(true);\n        when(locationHelper.getCurrentFormAuditConfig()).thenReturn(new AuditConfig.Builder()\n                .setMode(\"foo\")\n                .setLocationMinInterval(\"2\")\n                .setLocationMaxAge(\"3\")\n                .setIsTrackingChangesEnabled(true)\n                .setIsIdentifyUserEnabled(false)\n                .setIsTrackChangesReasonEnabled(false)\n                .createAuditConfig());\n\n        fakeLocationClient.setLocationAvailable(false);\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n        assertThat(backgroundLocationManager.locationPermissionGranted(), is(BackgroundLocationManager.BackgroundLocationMessage.PROVIDERS_DISABLED));\n    }\n\n    @Test\n    public void locationTrackingWarning_IsReturned_WhenFormRequestsBackgroundLocation_AndAllPreconditionsMet() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n\n        assertThat(backgroundLocationManager.locationPermissionGranted(), is(BackgroundLocationManager.BackgroundLocationMessage.COLLECTING_LOCATION));\n    }\n\n    @Test\n    public void locationChanges_AreSentToAuditLogger_WhenFormAuditsLocation_AndAllPreconditionsMet() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n        when(locationHelper.isAndroidLocationPermissionGranted()).thenReturn(true);\n        when(locationHelper.getCurrentFormAuditConfig()).thenReturn(new AuditConfig.Builder()\n                .setMode(\"foo\")\n                .setLocationMinInterval(\"2\")\n                .setLocationMaxAge(\"3\")\n                .setIsTrackingChangesEnabled(true)\n                .setIsIdentifyUserEnabled(false)\n                .setIsTrackChangesReasonEnabled(false)\n                .createAuditConfig());\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n        backgroundLocationManager.locationPermissionGranted();\n\n        Location location = LocationTestUtils.createLocation(\"GPS\", 1, 2, 3, 4);\n        fakeLocationClient.receiveFix(location);\n\n        verify(locationHelper).provideLocationToAuditLogger(location);\n    }\n\n    @Test\n    public void locationChanges_AreNotSentToAuditLogger_WhenFormHasSetGeopointAndNoAudit() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(false);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n        backgroundLocationManager.locationPermissionGranted();\n\n        Location location = LocationTestUtils.createLocation(\"GPS\", 1, 2, 3, 4);\n        fakeLocationClient.receiveFix(location);\n\n        verify(locationHelper, never()).provideLocationToAuditLogger(location);\n    }\n\n    @Test\n    public void revokingLocationPermission_ResultsInLocationUpdatesStopping_WhenFormAuditsLocation() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n        when(locationHelper.isAndroidLocationPermissionGranted()).thenReturn(true);\n        when(locationHelper.getCurrentFormAuditConfig()).thenReturn(new AuditConfig.Builder()\n                .setMode(\"foo\")\n                .setLocationMinInterval(\"2\")\n                .setLocationMaxAge(\"3\")\n                .setIsTrackingChangesEnabled(true)\n                .setIsIdentifyUserEnabled(false)\n                .setIsTrackChangesReasonEnabled(false)\n                .createAuditConfig());\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n        backgroundLocationManager.locationPermissionGranted();\n\n        Location location = LocationTestUtils.createLocation(\"GPS\", 1, 2, 3, 4);\n        fakeLocationClient.receiveFix(location);\n        verify(locationHelper).provideLocationToAuditLogger(location);\n\n        // User revokes permission in Android settings\n        backgroundLocationManager.activityHidden();\n        when(locationHelper.isAndroidLocationPermissionGranted()).thenReturn(false);\n        backgroundLocationManager.locationPermissionChanged();\n\n        verify(locationHelper).logAuditEvent(AuditEvent.AuditEventType.LOCATION_PERMISSIONS_NOT_GRANTED);\n\n        // No snackbar is shown on activity re-entry\n        assertThat(backgroundLocationManager.activityDisplayed(), is(nullValue()));\n\n        fakeLocationClient.receiveFix(location);\n\n        verify(locationHelper, times(1)).provideLocationToAuditLogger(location);\n    }\n\n    @Test\n    public void grantingLocationPermission_ResultsInLocationUpdatesStarting_WhenFormAuditsLocation() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n        when(locationHelper.isAndroidLocationPermissionGranted()).thenReturn(false);\n        when(locationHelper.getCurrentFormAuditConfig()).thenReturn(new AuditConfig.Builder()\n                .setMode(\"foo\")\n                .setLocationMinInterval(\"2\")\n                .setLocationMaxAge(\"3\")\n                .setIsTrackingChangesEnabled(true)\n                .setIsIdentifyUserEnabled(false)\n                .setIsTrackChangesReasonEnabled(false)\n                .createAuditConfig());\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n        backgroundLocationManager.locationPermissionDenied();\n\n        Location location = LocationTestUtils.createLocation(\"GPS\", 1, 2, 3, 4);\n        fakeLocationClient.receiveFix(location);\n        verify(locationHelper, never()).provideLocationToAuditLogger(location);\n\n        // User grants permission in Android settings\n        backgroundLocationManager.activityHidden();\n        when(locationHelper.isAndroidLocationPermissionGranted()).thenReturn(true);\n        // No snackbar is shown on activity re-entry\n        assertThat(backgroundLocationManager.activityDisplayed(), is(nullValue()));\n\n        fakeLocationClient.receiveFix(location);\n        verify(locationHelper).provideLocationToAuditLogger(location);\n    }\n\n    @Test\n    public void togglingBackgroundLocationPreference_TogglesLocationCapture() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n        when(locationHelper.isAndroidLocationPermissionGranted()).thenReturn(true);\n        when(locationHelper.getCurrentFormAuditConfig()).thenReturn(new AuditConfig.Builder()\n                .setMode(\"foo\")\n                .setLocationMinInterval(\"2\")\n                .setLocationMaxAge(\"3\")\n                .setIsTrackingChangesEnabled(true)\n                .setIsIdentifyUserEnabled(false)\n                .setIsTrackChangesReasonEnabled(false)\n                .createAuditConfig());\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n        backgroundLocationManager.locationPermissionGranted();\n\n        Location location = LocationTestUtils.createLocation(\"GPS\", 1, 2, 3, 4);\n        fakeLocationClient.receiveFix(location);\n\n        verify(locationHelper).provideLocationToAuditLogger(location);\n\n        // Toggle preference off\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(false);\n        backgroundLocationManager.backgroundLocationPreferenceToggled();\n\n        Location location2 = LocationTestUtils.createLocation(\"GPS\", 7, 2, 3, 4);\n        fakeLocationClient.receiveFix(location2);\n\n        verify(locationHelper, never()).provideLocationToAuditLogger(location2);\n\n        // Toggle preference on\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n        backgroundLocationManager.backgroundLocationPreferenceToggled();\n\n        fakeLocationClient.receiveFix(location2);\n        verify(locationHelper).provideLocationToAuditLogger(location2);\n    }\n\n    @Test\n    public void changesInLocationProviders_AreAudited_IfFormRequestsAudit_AndAllPreconditionsMet() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n        when(locationHelper.isAndroidLocationPermissionGranted()).thenReturn(true);\n        when(locationHelper.getCurrentFormAuditConfig()).thenReturn(new AuditConfig.Builder()\n                .setMode(\"foo\")\n                .setLocationMinInterval(\"2\")\n                .setLocationMaxAge(\"3\")\n                .setIsTrackingChangesEnabled(true)\n                .setIsIdentifyUserEnabled(false)\n                .setIsTrackChangesReasonEnabled(false)\n                .createAuditConfig());\n\n        backgroundLocationManager.formFinishedLoading();\n\n        backgroundLocationManager.activityDisplayed();\n        verify(locationHelper, never()).logAuditEvent(AuditEvent.AuditEventType.LOCATION_PROVIDERS_ENABLED);\n        verify(locationHelper, never()).logAuditEvent(AuditEvent.AuditEventType.LOCATION_PROVIDERS_DISABLED);\n\n        backgroundLocationManager.locationPermissionGranted();\n        verify(locationHelper).logAuditEvent(AuditEvent.AuditEventType.LOCATION_PROVIDERS_ENABLED);\n\n        // Providers off\n        fakeLocationClient.setLocationAvailable(false);\n        backgroundLocationManager.locationProvidersChanged();\n        verify(locationHelper).logAuditEvent(AuditEvent.AuditEventType.LOCATION_PROVIDERS_DISABLED);\n\n        // Providers back on\n        fakeLocationClient.setLocationAvailable(true);\n        backgroundLocationManager.locationProvidersChanged();\n        verify(locationHelper, times(2)).logAuditEvent(AuditEvent.AuditEventType.LOCATION_PROVIDERS_ENABLED);\n    }\n\n    @Test\n    public void locationEvents_IsNeverLogged_WhenFormDoesNotRequestLocation() {\n        when(locationHelper.currentFormCollectsBackgroundLocation()).thenReturn(false);\n\n        backgroundLocationManager.formFinishedLoading();\n        backgroundLocationManager.activityDisplayed();\n\n        backgroundLocationManager.activityHidden();\n        backgroundLocationManager.activityDisplayed();\n\n        // not possible through FormFillingActivity because menu doesn't show toggle\n        backgroundLocationManager.backgroundLocationPreferenceToggled();\n\n        // not possible through FormFillingActivity because provider not registered\n        backgroundLocationManager.locationProvidersChanged();\n\n        // not possible through FormFillingActivity because listener not set\n        backgroundLocationManager.onLocationChanged(LocationTestUtils.createLocation(\"GPS\", 1, 2, 3, 4));\n\n        // not possible through FormFillingActivity because location request never needed\n        backgroundLocationManager.locationPermissionGranted();\n        backgroundLocationManager.locationPermissionDenied();\n\n        verify(locationHelper, never()).logAuditEvent(Mockito.any());\n    }\n\n    /**\n     * Simulates the case where the system kills the activity. The form doesn't need to be loaded\n     * again because FormController is an Application-scoped singleton so if the form audits location,\n     * we should go straight to requesting location without showing anything to the user.\n     **/\n    @Test\n    public void locationRequests_Resume_WhenActivityIsDisplayed_AndFormThatAuditsLocationWasAlreadyLoaded() {\n        when(locationHelper.isCurrentFormSet()).thenReturn(true);\n        when(locationHelper.currentFormAuditsLocation()).thenReturn(true);\n\n        when(locationHelper.arePlayServicesAvailable()).thenReturn(true);\n        when(locationHelper.isBackgroundLocationPreferenceEnabled()).thenReturn(true);\n        when(locationHelper.isAndroidLocationPermissionGranted()).thenReturn(true);\n        when(locationHelper.getCurrentFormAuditConfig()).thenReturn(new AuditConfig.Builder()\n                .setMode(\"foo\")\n                .setLocationMinInterval(\"2\")\n                .setLocationMaxAge(\"3\")\n                .setIsTrackingChangesEnabled(true)\n                .setIsIdentifyUserEnabled(false)\n                .setIsTrackChangesReasonEnabled(false)\n                .createAuditConfig());\n\n        backgroundLocationManager.activityDisplayed();\n\n        Location location = LocationTestUtils.createLocation(\"GPS\", 1, 2, 3, 4);\n        fakeLocationClient.receiveFix(location);\n        verify(locationHelper).provideLocationToAuditLogger(location);\n    }\n\n    @Test\n    public void locationRequests_DoNotResume_WhenActivityIsDisplayed_AndFormThatDoesNotAuditLocationWasAlreadyLoaded() {\n        when(locationHelper.isCurrentFormSet()).thenReturn(true);\n        backgroundLocationManager.activityDisplayed();\n\n        Location location = LocationTestUtils.createLocation(\"GPS\", 1, 2, 3, 4);\n        fakeLocationClient.receiveFix(location);\n        verify(locationHelper, never()).provideLocationToAuditLogger(location);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/repeats/DeleteRepeatDialogFragmentTest.java",
    "content": "package org.odk.collect.android.formentry.repeats;\n\nimport static junit.framework.TestCase.assertTrue;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.junit.Assert.assertFalse;\nimport static org.mockito.Mockito.RETURNS_MOCKS;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.robolectric.Shadows.shadowOf;\n\nimport android.content.DialogInterface;\nimport android.os.Bundle;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.fragment.app.FragmentResultListener;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.lifecycle.viewmodel.CreationExtras;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.FormEntryViewModel;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.testshared.RobolectricHelpers;\nimport org.robolectric.shadows.ShadowDialog;\n\n@RunWith(AndroidJUnit4.class)\npublic class DeleteRepeatDialogFragmentTest {\n\n    private TestActivity activity;\n    private FragmentManager fragmentManager;\n    private DeleteRepeatDialogFragment dialogFragment;\n\n    private final FormController formController = mock(FormController.class, RETURNS_MOCKS);\n\n    @Before\n    public void setup() {\n        FormEntryViewModel formEntryViewModel = mock(FormEntryViewModel.class);\n\n        when(formEntryViewModel.getFormController()).thenReturn(formController);\n        when(formController.getLastRepeatedGroupName()).thenReturn(\"blah\");\n        when(formController.getLastRepeatedGroupRepeatCount()).thenReturn(0);\n\n        activity = CollectHelpers.createThemedActivity(TestActivity.class);\n        fragmentManager = activity.getSupportFragmentManager();\n        dialogFragment = new DeleteRepeatDialogFragment(new ViewModelProvider.Factory() {\n            @NonNull\n            @Override\n            public <T extends ViewModel> T create(@NonNull Class<T> modelClass, @NonNull CreationExtras extras) {\n                return (T) formEntryViewModel;\n            }\n        });\n    }\n\n    @Test\n    public void dialogIsNotCancellable() {\n        launchDialog();\n        assertThat(shadowOf(dialogFragment.getDialog()).isCancelable(), equalTo(false));\n    }\n\n    @Test\n    public void shouldShowCorrectMessage() {\n        AlertDialog dialog = launchDialog();\n        String message = ((TextView) dialog.findViewById(android.R.id.message)).getText().toString();\n\n        assertThat(message, equalTo(ApplicationProvider.getApplicationContext().getString(org.odk.collect.strings.R.string.delete_repeat_confirm, \"blah (1)\")));\n    }\n\n    @Test\n    public void clickingCancel_shouldDismissTheDialog() {\n        AlertDialog dialog = launchDialog();\n        assertTrue(dialog.isShowing());\n\n        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick();\n\n        RobolectricHelpers.runLooper();\n        assertFalse(dialog.isShowing());\n        assertTrue(shadowOf(dialog).hasBeenDismissed());\n    }\n\n    @Test\n    public void clickingRemoveGroup_shouldDismissTheDialog() {\n        AlertDialog dialog = launchDialog();\n        assertTrue(dialog.isShowing());\n\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();\n\n        RobolectricHelpers.runLooper();\n        assertFalse(dialog.isShowing());\n        assertTrue(shadowOf(dialog).hasBeenDismissed());\n    }\n\n    @Test\n    public void clickingRemoveGroup_setsResult() {\n        AlertDialog dialog = launchDialog();\n\n        FragmentResultCapturer fragmentResultCapturer = new FragmentResultCapturer();\n        activity.getSupportFragmentManager().setFragmentResultListener(DeleteRepeatDialogFragment.REQUEST_DELETE_REPEAT, activity, fragmentResultCapturer);\n\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();\n        RobolectricHelpers.runLooper();\n        assertThat(fragmentResultCapturer.result, notNullValue());\n    }\n\n    @Test\n    public void clickingRemoveGroup_callsDeleteRepeat() {\n        AlertDialog dialog = launchDialog();\n\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();\n        RobolectricHelpers.runLooper();\n        verify(formController).deleteRepeat();\n    }\n\n    private AlertDialog launchDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n        return (AlertDialog) ShadowDialog.getLatestDialog();\n    }\n\n    public static class TestActivity extends FragmentActivity {\n\n    }\n\n    private static class FragmentResultCapturer implements FragmentResultListener {\n\n        public Bundle result;\n\n        @Override\n        public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {\n            this.result = result;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formentry/support/InMemFormSessionRepository.kt",
    "content": "package org.odk.collect.android.formentry.support\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport org.odk.collect.android.formentry.FormSession\nimport org.odk.collect.android.formentry.FormSessionRepository\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.shared.strings.UUIDGenerator\n\nclass InMemFormSessionRepository : FormSessionRepository {\n\n    private val map = mutableMapOf<String, MutableLiveData<FormSession>>()\n\n    override fun create(): String {\n        return UUIDGenerator().generateUUID()\n    }\n\n    override fun get(id: String): LiveData<FormSession> {\n        return getLiveData(id)\n    }\n\n    override fun set(id: String, formController: FormController, form: Form, instance: Instance?) {\n        getLiveData(id).value = FormSession(formController, form, instance)\n    }\n\n    override fun update(id: String, instance: Instance?) {\n        val liveData = getLiveData(id)\n        liveData.value?.let {\n            liveData.value = it.copy(instance = instance)\n        }\n    }\n\n    override fun clear(id: String) {\n        getLiveData(id).value = null\n        map.remove(id)\n    }\n\n    private fun getLiveData(id: String): MutableLiveData<FormSession> {\n        return map.getOrPut(id) { MutableLiveData<FormSession>(null) }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formhierarchy/HierarchyListItemViewTest.kt",
    "content": "package org.odk.collect.android.formhierarchy\n\nimport android.app.Application\nimport android.view.View\nimport android.widget.ImageView\nimport androidx.compose.ui.platform.ComposeView\nimport androidx.compose.ui.test.junit4.createComposeRule\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.fromHtml\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport com.google.android.material.textview.MaterialTextView\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.R\nimport org.odk.collect.android.utilities.HtmlUtils\nimport org.odk.collect.android.widgets.MediaWidgetAnswerViewModel\n\n@RunWith(AndroidJUnit4::class)\nclass HierarchyListItemViewTest {\n    @get:Rule\n    val composeRule = createComposeRule()\n\n    private val context: Application =\n        ApplicationProvider.getApplicationContext<Application>().also {\n            it.setTheme(com.google.android.material.R.style.Theme_Material3_Light)\n        }\n\n    @Test\n    fun `Question item should contain only primary text and secondary text`() {\n        val view = HierarchyListItemView(context, R.layout.hierarchy_question_item)\n        composeRule.setContent {\n            AndroidView(factory = {\n                view.also {\n                    it.setElement(HierarchyItem.Question(mock<FormIndex>(), \"foo\", \"bar\", mock<FormEntryPrompt>()), mock<MediaWidgetAnswerViewModel>(), {})\n                }\n            })\n        }\n\n        assertThat(view.findViewById<MaterialTextView>(R.id.primary_text).visibility, equalTo(View.VISIBLE))\n        assertThat(view.findViewById<MaterialTextView>(R.id.primary_text).text.toString(), equalTo(\"foo\"))\n\n        assertThat(view.findViewById<ComposeView>(R.id.answer_view).visibility, equalTo(View.VISIBLE))\n\n        composeRule.onNodeWithText(\"bar\").assertExists()\n\n        assertThat(view.findViewById<ImageView>(R.id.icon), equalTo(null))\n    }\n\n    @Test\n    fun `When secondary text is html should be styled`() {\n        val view = HierarchyListItemView(context, R.layout.hierarchy_question_item)\n        composeRule.setContent {\n            AndroidView(factory = {\n                view.also {\n                    it.setElement(HierarchyItem.Question(mock<FormIndex>(), \"foo\", \"<h1>bar</h1>\", mock<FormEntryPrompt>()), mock<MediaWidgetAnswerViewModel>(), {})\n                }\n            })\n        }\n\n        assertThat(view.findViewById<MaterialTextView>(R.id.primary_text).visibility, equalTo(View.VISIBLE))\n        assertThat(view.findViewById<MaterialTextView>(R.id.primary_text).text.toString(), equalTo(\"foo\"))\n\n        val styledText = AnnotatedString.fromHtml(HtmlUtils.markdownToHtml(\"<h1>bar</h1>\"))\n        composeRule.onNodeWithText(styledText.text).assertExists()\n    }\n\n    @Test\n    fun `Group item should contain group label, primary text and icon`() {\n        val view = HierarchyListItemView(context, R.layout.hierarchy_group_item)\n\n        view.setElement(HierarchyItem.VisibleGroup(mock<FormIndex>(), \"foo\"), mock<MediaWidgetAnswerViewModel>(), {})\n\n        assertThat(view.findViewById<ImageView>(R.id.icon).visibility, equalTo(View.VISIBLE))\n\n        assertThat(view.findViewById<MaterialTextView>(R.id.group_label).visibility, equalTo(View.VISIBLE))\n        assertThat(view.findViewById<MaterialTextView>(R.id.group_label).text.toString(), equalTo(context.getString(org.odk.collect.strings.R.string.group_label)))\n\n        assertThat(view.findViewById<MaterialTextView>(R.id.primary_text).visibility, equalTo(View.VISIBLE))\n        assertThat(view.findViewById<MaterialTextView>(R.id.primary_text).text.toString(), equalTo(\"foo\"))\n    }\n\n    @Test\n    fun `Repeatable group item should contain group label, primary text and icon`() {\n        val view = HierarchyListItemView(context, R.layout.hierarchy_repeatable_group_item)\n\n        view.setElement(HierarchyItem.RepeatableGroup(mock<FormIndex>(), \"foo\"), mock<MediaWidgetAnswerViewModel>(), {})\n\n        assertThat(view.findViewById<ImageView>(R.id.icon).visibility, equalTo(View.VISIBLE))\n\n        assertThat(view.findViewById<MaterialTextView>(R.id.group_label).visibility, equalTo(View.VISIBLE))\n        assertThat(view.findViewById<MaterialTextView>(R.id.group_label).text.toString(), equalTo(context.getString(org.odk.collect.strings.R.string.repeatable_group_label)))\n\n        assertThat(view.findViewById<MaterialTextView>(R.id.primary_text).visibility, equalTo(View.VISIBLE))\n        assertThat(view.findViewById<MaterialTextView>(R.id.primary_text).text.toString(), equalTo(\"foo\"))\n    }\n\n    @Test\n    fun `Repeatable group instance item should contain only primary text`() {\n        val view = HierarchyListItemView(context, R.layout.hierarchy_repeatable_group_instance_item)\n\n        view.setElement(HierarchyItem.RepeatInstance(mock<FormIndex>(), \"foo\"), mock<MediaWidgetAnswerViewModel>(), {})\n\n        assertThat(view.findViewById<MaterialTextView>(R.id.primary_text).visibility, equalTo(View.VISIBLE))\n        assertThat(view.findViewById<MaterialTextView>(R.id.primary_text).text.toString(), equalTo(\"foo\"))\n\n        assertThat(view.findViewById<ImageView>(R.id.icon), equalTo(null))\n        assertThat(view.findViewById<ComposeView>(R.id.answer_view), equalTo(null))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formhierarchy/QuestionAnswerProcessorTest.kt",
    "content": "package org.odk.collect.android.formhierarchy\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.QuestionDef\nimport org.junit.Test\nimport org.mockito.Mockito.mock\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.android.utilities.Appearances\n\n// TODO: Add tests for other question/data types\nclass QuestionAnswerProcessorTest {\n\n    @Test\n    fun noAnswerShouldBeDisplayedForThePrinterWidget() {\n        val question = mock<QuestionDef>()\n        val prompt = MockFormEntryPromptBuilder()\n            .withQuestion(question)\n            .withAnswerDisplayText(\"<html>blah</html>>\")\n            .withAppearance(Appearances.PRINTER)\n            .build()\n\n        val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock())\n\n        assertThat(answer, equalTo(\"\"))\n    }\n\n    @Test\n    fun noAnswerShouldBeDisplayedIfItDoesNotExistAndMaskedAppearanceIsUsedForTextDataTypes() {\n        val question = mock<QuestionDef>()\n        val prompt = MockFormEntryPromptBuilder()\n            .withQuestion(question)\n            .withDataType(Constants.DATATYPE_TEXT)\n            .withControlType(Constants.CONTROL_INPUT)\n            .withAppearance(Appearances.MASKED)\n            .build()\n\n        val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock())\n\n        assertThat(answer, equalTo(\"\"))\n    }\n\n    @Test\n    fun noAnswerShouldBeDisplayedIfItIsEmptyAndMaskedAppearanceIsUsedForTextDataTypes() {\n        val question = mock<QuestionDef>()\n        val prompt = MockFormEntryPromptBuilder()\n            .withQuestion(question)\n            .withAnswerDisplayText(\"\")\n            .withAppearance(Appearances.MASKED)\n            .withControlType(Constants.CONTROL_INPUT)\n            .withDataType(Constants.DATATYPE_TEXT)\n            .build()\n\n        val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock())\n\n        assertThat(answer, equalTo(\"\"))\n    }\n\n    @Test\n    fun maskedAnswerShouldBeDisplayedIfItExistAndMaskedAppearanceIsUsedForTextDataTypes() {\n        val question = mock<QuestionDef>()\n        val prompt = MockFormEntryPromptBuilder()\n            .withQuestion(question)\n            .withAnswerDisplayText(\"blah\")\n            .withAppearance(Appearances.MASKED)\n            .withControlType(Constants.CONTROL_INPUT)\n            .withDataType(Constants.DATATYPE_TEXT)\n            .build()\n\n        val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock())\n\n        assertThat(answer, equalTo(\"••••••••••\"))\n    }\n\n    @Test\n    fun originalAnswerShouldBeDisplayedIfItExistAndMaskedAppearanceIsUsedForDataTypesOtherThanText() {\n        listOf(\n            Constants.DATATYPE_INTEGER,\n            Constants.DATATYPE_DECIMAL,\n            Constants.DATATYPE_DATE_TIME,\n            Constants.DATATYPE_DATE,\n            Constants.DATATYPE_TIME,\n            Constants.DATATYPE_GEOPOINT,\n            Constants.DATATYPE_GEOSHAPE,\n            Constants.DATATYPE_GEOSHAPE,\n            Constants.DATATYPE_GEOTRACE,\n            Constants.DATATYPE_BARCODE,\n            Constants.DATATYPE_BARCODE\n        ).forEach {\n            val question = mock<QuestionDef>()\n            val prompt = MockFormEntryPromptBuilder()\n                .withQuestion(question)\n                .withAnswerDisplayText(\"blah\")\n                .withAppearance(Appearances.MASKED)\n                .withControlType(Constants.CONTROL_INPUT)\n                .withDataType(it)\n                .build()\n\n            val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock())\n\n            assertThat(answer, equalTo(\"blah\"))\n        }\n    }\n\n    @Test\n    fun originalAnswerShouldBeDisplayedIfItExistAndMaskedAppearanceIsUsedForDataTypesAndControlTypeOtherThanInput() {\n        listOf(\n            Constants.CONTROL_RANGE,\n            Constants.CONTROL_RANK,\n            Constants.CONTROL_TRIGGER,\n            Constants.CONTROL_SELECT_MULTI,\n            Constants.CONTROL_SELECT_ONE,\n            Constants.CONTROL_VIDEO_CAPTURE,\n            Constants.CONTROL_AUDIO_CAPTURE,\n            Constants.CONTROL_OSM_CAPTURE,\n            Constants.CONTROL_IMAGE_CHOOSE,\n            Constants.CONTROL_FILE_CAPTURE\n        ).forEach {\n            val question = mock<QuestionDef>()\n            val prompt = MockFormEntryPromptBuilder()\n                .withQuestion(question)\n                .withAnswerDisplayText(\"blah\")\n                .withAppearance(Appearances.MASKED)\n                .withControlType(it)\n                .withDataType(Constants.DATATYPE_TEXT)\n                .build()\n\n            val answer = QuestionAnswerProcessor.getQuestionAnswer(prompt, mock(), mock())\n\n            assertThat(answer, equalTo(\"blah\"))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formlists/DeleteBlankFormFragmentTest.kt",
    "content": "package org.odk.collect.android.formlists\n\nimport android.app.Application\nimport android.net.Uri\nimport androidx.core.view.MenuHost\nimport androidx.core.view.MenuProvider\nimport androidx.fragment.app.testing.FragmentScenario\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewmodel.CreationExtras\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isChecked\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.instanceOf\nimport org.hamcrest.Matchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListItem\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListMenuProvider\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListViewModel\nimport org.odk.collect.android.formlists.blankformlist.DeleteBlankFormFragment\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView\nimport org.odk.collect.testshared.ViewActions.clickOnItemWith\nimport org.odk.collect.testshared.ViewMatchers.recyclerView\n\n@RunWith(AndroidJUnit4::class)\nclass DeleteBlankFormFragmentTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n    private val menuHost = RecordingMenuHost()\n\n    private val formsToDisplay = MutableLiveData<List<BlankFormListItem>>(emptyList())\n    private val blankFormListViewModel = mock<BlankFormListViewModel> {\n        on { formsToDisplay } doReturn formsToDisplay\n        on { isLoading } doReturn MutableLiveData()\n        on { isOutOfSyncWithServer() } doReturn MutableLiveData()\n    }\n\n    private val viewModelFactory = object : ViewModelProvider.Factory {\n        override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {\n            return when (modelClass) {\n                BlankFormListViewModel::class.java -> blankFormListViewModel\n                else -> throw IllegalArgumentException()\n            } as T\n        }\n    }\n\n    @get:Rule\n    val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule(\n        FragmentFactoryBuilder()\n            .forClass(DeleteBlankFormFragment::class) {\n                DeleteBlankFormFragment(viewModelFactory, menuHost)\n            }.build()\n    )\n\n    @Test\n    fun `clicking delete selected and then accepting deletes selected forms`() {\n        launchFragment()\n        formsToDisplay.value = listOf(\n            blankFormListItem(databaseId = 11, formName = \"Form 1\"),\n            blankFormListItem(databaseId = 12, formName = \"Form 2\")\n        )\n\n        onView(recyclerView()).perform(clickOnItemWith(withText(\"Form 1\")))\n        onView(recyclerView()).perform(clickOnItemWith(withText(\"Form 2\")))\n\n        onView(withText(org.odk.collect.strings.R.string.delete_file)).perform(click())\n        onView(withText(context.getString(org.odk.collect.strings.R.string.delete_confirm, 2)))\n            .inRoot(isDialog())\n            .check(matches(isDisplayed()))\n        onView(withText(org.odk.collect.strings.R.string.delete_yes)).inRoot(isDialog()).perform(click())\n\n        verify(blankFormListViewModel).deleteForms(11, 12)\n    }\n\n    @Test\n    fun `clicking delete selected and then cancelling does nothing`() {\n        launchFragment()\n        formsToDisplay.value = listOf(\n            blankFormListItem(databaseId = 11, formName = \"Form 1\"),\n            blankFormListItem(databaseId = 12, formName = \"Form 2\")\n        )\n\n        onView(recyclerView()).perform(clickOnItemWith(withText(\"Form 1\")))\n        onView(recyclerView()).perform(clickOnItemWith(withText(\"Form 2\")))\n\n        onView(withText(org.odk.collect.strings.R.string.delete_file)).perform(click())\n        onView(withText(context.getString(org.odk.collect.strings.R.string.delete_confirm, 2)))\n            .inRoot(isDialog())\n            .check(matches(isDisplayed()))\n        onView(withText(org.odk.collect.strings.R.string.delete_no)).inRoot(isDialog()).perform(click())\n\n        verify(blankFormListViewModel, never()).deleteForms(any())\n    }\n\n    @Test\n    fun `clicking delete selected unselects forms`() {\n        launchFragment()\n        formsToDisplay.value = listOf(\n            blankFormListItem(databaseId = 11, formName = \"Form 1\"),\n            blankFormListItem(databaseId = 12, formName = \"Form 2\")\n        )\n\n        onView(recyclerView()).perform(clickOnItemWith(withText(\"Form 1\")))\n\n        onView(withText(org.odk.collect.strings.R.string.delete_file)).perform(click())\n        onView(withText(context.getString(org.odk.collect.strings.R.string.delete_confirm, 1)))\n            .inRoot(isDialog())\n            .check(matches(isDisplayed()))\n        onView(withText(org.odk.collect.strings.R.string.delete_yes)).inRoot(isDialog()).perform(click())\n\n        onView(withRecyclerView(R.id.list).atPositionOnView(0, R.id.form_title)).check(matches(withText(\"Form 1\")))\n        onView(withRecyclerView(R.id.list).atPositionOnView(0, R.id.checkbox)).check(matches(not(isChecked())))\n    }\n\n    @Test\n    fun `provides blank form menu`() {\n        launchFragment()\n\n        val menuProviders = menuHost.getMenuProviders()\n        assertThat(menuProviders.size, equalTo(1))\n        assertThat(menuProviders[0].first, equalTo(Lifecycle.State.RESUMED))\n        assertThat(menuProviders[0].second, instanceOf(BlankFormListMenuProvider::class.java))\n    }\n\n    private fun launchFragment(): FragmentScenario<*> {\n        return fragmentScenarioLauncherRule.launchInContainer(DeleteBlankFormFragment::class.java)\n    }\n\n    private fun blankFormListItem(databaseId: Long = 1, formName: String = \"Form 1\") =\n        BlankFormListItem(\n            databaseId = databaseId,\n            formId = \"formId\",\n            formName = formName,\n            formVersion = \"formVersion\",\n            geometryPath = \"\",\n            dateOfCreation = 0,\n            dateOfLastUsage = 0,\n            dateOfLastDetectedAttachmentsUpdate = null,\n            Uri.parse(\"\")\n        )\n}\n\nprivate class RecordingMenuHost : MenuHost {\n\n    private val menuProviders = mutableListOf<Pair<Lifecycle.State?, MenuProvider>>()\n\n    override fun addMenuProvider(menuProvider: MenuProvider) {\n        menuProviders.add(Pair(null, menuProvider))\n    }\n\n    override fun addMenuProvider(menuProvider: MenuProvider, lifecycleOwner: LifecycleOwner) {\n        menuProviders.add(Pair(null, menuProvider))\n    }\n\n    override fun addMenuProvider(\n        menuProvider: MenuProvider,\n        lifecycleOwner: LifecycleOwner,\n        state: Lifecycle.State\n    ) {\n        menuProviders.add(Pair(state, menuProvider))\n    }\n\n    override fun removeMenuProvider(menuProvider: MenuProvider) {\n        TODO()\n    }\n\n    override fun invalidateMenu() { }\n\n    fun getMenuProviders(): List<Pair<Lifecycle.State?, MenuProvider>> {\n        return menuProviders\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItemTest.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.net.Uri\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.projects.Project\n\n@RunWith(AndroidJUnit4::class)\nclass BlankFormListItemTest {\n    private val instancesRepository = InMemInstancesRepository()\n\n    @Test\n    fun `Form should be properly converted to BlankFormListItem`() {\n        val form = Form.Builder()\n            .dbId(1)\n            .formId(\"1\")\n            .displayName(\"Sample form B\")\n            .version(\"1\")\n            .geometryXpath(\"blah\")\n            .date(1665742651522)\n            .build()\n\n        instancesRepository.save(\n            Instance.Builder()\n                .formId(form.formId)\n                .formVersion(form.version)\n                .lastStatusChangeDate(3)\n                .build()\n        )\n\n        val blankFormListItem = form.toBlankFormListItem(Project.DEMO_PROJECT_ID, instancesRepository)\n\n        assertThat(blankFormListItem.databaseId, `is`(form.dbId))\n        assertThat(blankFormListItem.formId, `is`(form.formId))\n        assertThat(blankFormListItem.formName, `is`(form.displayName))\n        assertThat(blankFormListItem.formVersion, `is`(form.version))\n        assertThat(blankFormListItem.geometryPath, `is`(form.geometryXpath))\n        assertThat(blankFormListItem.dateOfCreation, `is`(form.date))\n        assertThat(blankFormListItem.dateOfLastUsage, `is`(3L))\n        assertThat(blankFormListItem.contentUri, `is`(Uri.parse(\"content://org.odk.collect.android.provider.odk.forms/forms/1?projectId=DEMO\")))\n    }\n\n    @Test\n    fun `When there are no saved instances dateOfLastUsage in BlankFormListItem should be set to 0`() {\n        val form = Form.Builder()\n            .dbId(1)\n            .formId(\"1\")\n            .displayName(\"Sample form\")\n            .date(1665742651521)\n            .build()\n\n        val blankFormListItem = form.toBlankFormListItem(Project.DEMO_PROJECT_ID, instancesRepository)\n\n        assertThat(blankFormListItem.dateOfLastUsage, `is`(0L))\n    }\n\n    @Test\n    fun `When version in form is not set should be represented as an empty sting in BlankFormListItem`() {\n        val form = Form.Builder()\n            .dbId(1)\n            .formId(\"1\")\n            .displayName(\"Sample form\")\n            .date(1665742651521)\n            .build()\n\n        val blankFormListItem = form.toBlankFormListItem(Project.DEMO_PROJECT_ID, instancesRepository)\n\n        assertThat(blankFormListItem.formVersion, `is`(\"\"))\n    }\n\n    @Test\n    fun `When geometryXpath in form is not set should be represented as an empty sting in BlankFormListItem`() {\n        val form = Form.Builder()\n            .dbId(1)\n            .formId(\"1\")\n            .displayName(\"Sample form\")\n            .date(1665742651521)\n            .build()\n\n        val blankFormListItem = form.toBlankFormListItem(Project.DEMO_PROJECT_ID, instancesRepository)\n\n        assertThat(blankFormListItem.geometryPath, `is`(\"\"))\n    }\n\n    @Test\n    fun `dateOfLastUsage should be set taking into account forms saved not only for given formId but also formVersion`() {\n        val formV1 = Form.Builder()\n            .dbId(1)\n            .formId(\"1\")\n            .displayName(\"Sample form v1\")\n            .version(\"1\")\n            .geometryXpath(\"blah\")\n            .date(1665742651521)\n            .build()\n\n        val formV2 = Form.Builder(formV1)\n            .dbId(2)\n            .displayName(\"Sample form v2\")\n            .version(\"2\")\n            .date(1665742651522)\n            .build()\n\n        instancesRepository.save(\n            Instance.Builder()\n                .formId(formV1.formId)\n                .formVersion(formV1.version)\n                .lastStatusChangeDate(5)\n                .build()\n        )\n\n        instancesRepository.save(\n            Instance.Builder()\n                .formId(formV2.formId)\n                .formVersion(formV2.version)\n                .lastStatusChangeDate(3)\n                .build()\n        )\n\n        instancesRepository.save(\n            Instance.Builder()\n                .formId(formV2.formId)\n                .formVersion(formV2.version)\n                .lastStatusChangeDate(4)\n                .build()\n        )\n\n        val blankFormListItem = formV2.toBlankFormListItem(Project.DEMO_PROJECT_ID, instancesRepository)\n\n        assertThat(blankFormListItem.dateOfLastUsage, `is`(4L))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListItemViewTest.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.content.Context\nimport android.net.Uri\nimport android.view.View\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\n@RunWith(AndroidJUnit4::class)\nclass BlankFormListItemViewTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n\n    @Test\n    fun `displays form version`() {\n        val view = BlankFormListItemView(context)\n        view.setItem(blankFormListItem(formId = \"myId\", formVersion = \"myVersion\"))\n\n        assertThat(\n            view.binding.formVersion.text,\n            equalTo(context.getString(org.odk.collect.strings.R.string.version_number, \"myVersion\"))\n        )\n    }\n\n    @Test\n    fun `hides version when form version is blank`() {\n        val view = BlankFormListItemView(context)\n        view.setItem(blankFormListItem(formId = \"myId\", formVersion = \"\"))\n\n        assertThat(view.binding.formVersion.visibility, equalTo(View.GONE))\n    }\n\n    @Test\n    fun `displays form id`() {\n        val view = BlankFormListItemView(context)\n        view.setItem(blankFormListItem(formId = \"myId\"))\n\n        assertThat(\n            view.binding.formId.text,\n            equalTo(context.getString(org.odk.collect.strings.R.string.id_number, \"myId\"))\n        )\n    }\n}\n\nprivate fun blankFormListItem(\n    formId: String = \"formId\",\n    formVersion: String = \"formVersion\"\n): BlankFormListItem {\n    return BlankFormListItem(\n        databaseId = 0,\n        formId = formId,\n        formName = \"formName\",\n        formVersion = formVersion,\n        geometryPath = \"\",\n        dateOfCreation = 0,\n        dateOfLastUsage = 0,\n        dateOfLastDetectedAttachmentsUpdate = null,\n        contentUri = Uri.parse(\"http://example.com\")\n    )\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuProviderTest.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.view.Menu\nimport androidx.appcompat.view.SupportMenuInflater\nimport androidx.appcompat.view.menu.MenuBuilder\nimport androidx.appcompat.widget.SearchView\nimport androidx.core.internal.view.SupportMenu\nimport androidx.fragment.app.FragmentActivity\nimport androidx.lifecycle.MutableLiveData\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.instanceOf\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.async.network.NetworkStateProvider\nimport org.robolectric.Shadows\nimport org.robolectric.fakes.RoboMenuItem\nimport org.robolectric.shadows.ShadowDialog\nimport org.robolectric.shadows.ShadowToast\n\n@RunWith(AndroidJUnit4::class)\nclass BlankFormListMenuProviderTest {\n    private lateinit var activity: FragmentActivity\n\n    private val viewModel: BlankFormListViewModel = mock {\n        on { sortingOrder } doReturn BlankFormListViewModel.SortOrder.NAME_ASC\n    }\n\n    private val networkStateProvider: NetworkStateProvider = mock()\n\n    private val menuInflater: SupportMenuInflater\n        get() = SupportMenuInflater(activity)\n\n    @Before\n    fun setup() {\n        whenever(networkStateProvider.isDeviceOnline).thenReturn(true)\n        whenever(viewModel.isLoading).thenReturn(MutableLiveData(false))\n        whenever(viewModel.isOutOfSyncWithServer()).thenReturn(MutableLiveData(false))\n\n        activity = CollectHelpers.createThemedActivity(FragmentActivity::class.java)\n    }\n\n    @Test\n    fun `onPrepareMenu when not out of sync shows sync icon`() {\n        val menuProvider = createMenuProvider()\n        val menu = createdMenu()\n\n        menuProvider.onCreateMenu(menu, menuInflater)\n        menuProvider.onPrepareMenu(menu)\n\n        assertThat(\n            Shadows.shadowOf(menu.findItem(R.id.menu_refresh).icon).createdFromResId,\n            `is`(R.drawable.ic_baseline_refresh_24)\n        )\n    }\n\n    @Test\n    fun `onPrepareMenu when out of sync shows error sync icon`() {\n        whenever(viewModel.isOutOfSyncWithServer()).thenReturn(MutableLiveData(true))\n\n        val menuProvider = createMenuProvider()\n        val menu = createdMenu()\n\n        menuProvider.onCreateMenu(menu, menuInflater)\n        menuProvider.onPrepareMenu(menu)\n\n        assertThat(\n            Shadows.shadowOf(menu.findItem(R.id.menu_refresh).icon).createdFromResId,\n            `is`(R.drawable.ic_baseline_refresh_error_24)\n        )\n    }\n\n    @Test\n    fun `onPrepareMenu when loading disables refresh button`() {\n        whenever(viewModel.isLoading).thenReturn(MutableLiveData(true))\n\n        val menuProvider = createMenuProvider()\n        val menu = createdMenu()\n\n        menuProvider.onCreateMenu(menu, menuInflater)\n        menuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_refresh).isEnabled, `is`(false))\n    }\n\n    @Test\n    fun `onPrepareMenu when not loading enables refresh button`() {\n        val menuProvider = createMenuProvider()\n        val menu = createdMenu()\n\n        menuProvider.onCreateMenu(menu, menuInflater)\n        menuProvider.onPrepareMenu(menu)\n\n        assertThat(menu.findItem(R.id.menu_refresh).isEnabled, `is`(true))\n    }\n\n    @Test\n    fun `clicking refresh for sync when device is offline shows error toast and does not sync`() {\n        whenever(networkStateProvider.isDeviceOnline).thenReturn(false)\n\n        val menuProvider = createMenuProvider()\n        menuProvider.onMenuItemSelected(RoboMenuItem(R.id.menu_refresh))\n\n        assertThat(\n            ShadowToast.getTextOfLatestToast(),\n            `is`(activity.getString(org.odk.collect.strings.R.string.no_connection))\n        )\n        verify(viewModel, never()).syncWithServer()\n    }\n\n    @Test\n    fun `clicking sort displays sorting dialog`() {\n        val menuProvider = createMenuProvider()\n        menuProvider.onMenuItemSelected(RoboMenuItem(R.id.menu_sort))\n\n        assertThat(ShadowDialog.getLatestDialog(), instanceOf(FormListSortingBottomSheetDialog::class.java))\n    }\n\n    @Test\n    fun `changing search text should set filterText in viewModel`() {\n        val menu = createdMenu()\n        val menuProvider = createMenuProvider()\n\n        menuProvider.onCreateMenu(menu, menuInflater)\n        menuProvider.onPrepareMenu(menu)\n\n        val searchView = (menu.findItem(R.id.menu_filter).actionView as SearchView).findViewById<SearchView.SearchAutoComplete>(androidx.appcompat.R.id.search_src_text)\n        searchView.setText(\"abc\")\n\n        verify(viewModel).filterText = \"abc\"\n    }\n\n    @Test\n    fun `clicking search when matchExactly enabled hides refresh and sort and then collapsing search shows them again`() {\n        whenever(viewModel.isMatchExactlyEnabled()).thenReturn(true)\n\n        val menu = createdMenu()\n        val menuProvider = createMenuProvider()\n\n        menuProvider.onCreateMenu(menu, menuInflater)\n        menuProvider.onPrepareMenu(menu)\n\n        menu.findItem(R.id.menu_filter).expandActionView()\n\n        assertThat(menu.findItem(R.id.menu_refresh).isVisible, `is`(false))\n        assertThat(menu.findItem(R.id.menu_sort).isVisible, `is`(false))\n\n        menu.findItem(R.id.menu_filter).collapseActionView()\n\n        assertThat(menu.findItem(R.id.menu_refresh).isVisible, `is`(true))\n        assertThat(menu.findItem(R.id.menu_sort).isVisible, `is`(true))\n    }\n\n    @Test\n    fun `collapsing search when matchExactly not enabled does not show refresh`() {\n        whenever(viewModel.isMatchExactlyEnabled()).thenReturn(false)\n\n        val menu = createdMenu()\n        val menuProvider = createMenuProvider()\n\n        menuProvider.onCreateMenu(menu, menuInflater)\n        menuProvider.onPrepareMenu(menu)\n\n        menu.findItem(R.id.menu_filter).expandActionView()\n        menu.findItem(R.id.menu_filter).collapseActionView()\n\n        assertThat(menu.findItem(R.id.menu_refresh).isVisible, `is`(false))\n    }\n\n    /**\n     * SearchView is closed automatically after screen recreation. We think that filtering is not\n     * crucial enough to restore its state, so we do not reopen the view, and we clear filterText to\n     * display all available forms.\n     */\n    @Test\n    fun `filterText should be cleared when menu is created`() {\n        whenever(viewModel.isMatchExactlyEnabled()).thenReturn(false)\n\n        val menu = createdMenu()\n        val menuProvider = createMenuProvider()\n\n        menuProvider.onCreateMenu(menu, menuInflater)\n\n        verify(viewModel).filterText = \"\"\n    }\n\n    private fun createMenuProvider(): BlankFormListMenuProvider {\n        return BlankFormListMenuProvider(activity, viewModel, networkStateProvider)\n    }\n\n    private fun createdMenu(): Menu {\n        val menu: SupportMenu = MenuBuilder(activity)\n        menuInflater.inflate(R.menu.blank_form_list_menu, menu)\n        return menu\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListViewModelTest.kt",
    "content": "package org.odk.collect.android.formlists.blankformlist\n\nimport android.app.Application\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.lifecycle.MutableLiveData\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.utilities.ChangeLockProvider\nimport org.odk.collect.androidshared.utils.InMemUniqueIdGenerator\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.FormUtils\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.settings.enums.FormUpdateMode\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.locks.BooleanChangeLock\nimport org.odk.collect.shared.settings.InMemSettings\nimport org.odk.collect.testshared.FakeScheduler\nimport org.odk.collect.testshared.getOrAwaitValue\n\n@RunWith(AndroidJUnit4::class)\nclass BlankFormListViewModelTest {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    private val instancesRepository = InMemInstancesRepository()\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n    private val formsDataService: FormsDataService = mock {\n        on { getForms(any()) } doReturn MutableStateFlow(emptyList())\n    }\n\n    private val scheduler = FakeScheduler()\n    private val generalSettings = InMemSettings()\n    private val changeLockProvider: ChangeLockProvider = mock()\n    private val projectId = \"projectId\"\n\n    private val changeLock = BooleanChangeLock()\n    private lateinit var viewModel: BlankFormListViewModel\n\n    @Test\n    fun `updates forms when created`() {\n        createViewModel()\n        verify(formsDataService).refresh(projectId)\n    }\n\n    @Test\n    fun `isMatchExactlyEnabled returns correct value based on settings`() {\n        generalSettings.save(\n            ProjectKeys.KEY_FORM_UPDATE_MODE,\n            FormUpdateMode.MANUAL.getValue(context)\n        )\n\n        createViewModel()\n\n        assertThat(viewModel.isMatchExactlyEnabled(), `is`(false))\n\n        generalSettings.save(\n            ProjectKeys.KEY_FORM_UPDATE_MODE,\n            FormUpdateMode.MATCH_EXACTLY.getValue(context)\n        )\n\n        assertThat(viewModel.isMatchExactlyEnabled(), `is`(true))\n    }\n\n    @Test\n    fun `isOutOfSyncWithServer follows repository syncError`() {\n        createViewModel()\n\n        val liveData = MutableLiveData<FormSourceException?>(FormSourceException.FetchError())\n        whenever(formsDataService.getServerError(projectId)).thenReturn(liveData)\n\n        val outOfSync = viewModel.isOutOfSyncWithServer()\n        assertThat(outOfSync.getOrAwaitValue(scheduler), `is`(true))\n        liveData.value = null\n        assertThat(outOfSync.getOrAwaitValue(scheduler), `is`(false))\n    }\n\n    @Test\n    fun `isAuthenticationRequired follows repository syncError`() {\n        createViewModel()\n\n        val liveData = MutableLiveData<FormSourceException?>(FormSourceException.FetchError())\n        whenever(formsDataService.getServerError(projectId)).thenReturn(liveData)\n\n        val authenticationRequired = viewModel.isAuthenticationRequired()\n        assertThat(authenticationRequired.getOrAwaitValue(scheduler), `is`(false))\n        liveData.value = FormSourceException.AuthRequired()\n        assertThat(authenticationRequired.getOrAwaitValue(scheduler), `is`(true))\n        liveData.value = null\n        assertThat(authenticationRequired.getOrAwaitValue(scheduler), `is`(false))\n    }\n\n    @Test\n    fun `original list of forms should be fetched from database initially and sorted by name ASC`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\"),\n            form(dbId = 2, formId = \"2\")\n        )\n\n        createViewModel()\n\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 1, formId = \"1\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 2, formId = \"2\"))\n    }\n\n    @Test\n    fun `deleted forms should be ignored`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\"),\n            form(dbId = 2, formId = \"2\", deleted = true)\n        )\n\n        createViewModel()\n\n        val formsToDisplay = viewModel.formsToDisplay.getOrAwaitValue(scheduler)\n        assertThat(formsToDisplay.size, `is`(1))\n        assertFormItem(formsToDisplay[0], form(dbId = 1, formId = \"1\"))\n    }\n\n    @Test\n    fun `only the newest version of every form (by date) should be visible if hiding old form versions is enabled`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\", version = \"2\"),\n            form(dbId = 2, formId = \"1\", version = \"1\")\n        )\n\n        createViewModel(showAllVersions = false)\n\n        assertThat(viewModel.formsToDisplay.getOrAwaitValue(scheduler).size, `is`(1))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 2, formId = \"1\", version = \"1\"))\n    }\n\n    @Test\n    fun `all form versions should be visible if showAllVersions is true`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\", version = \"2\"),\n            form(dbId = 2, formId = \"1\", version = \"1\")\n        )\n\n        createViewModel(showAllVersions = true)\n\n        assertThat(viewModel.formsToDisplay.getOrAwaitValue(scheduler).size, `is`(2))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 1, formId = \"1\", version = \"2\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 2, formId = \"1\", version = \"1\"))\n    }\n\n    @Test\n    fun `when list of forms sorted 'by name ASC', saved should forms be ordered properly`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\", formName = \"1Form\"),\n            form(dbId = 2, formId = \"2\", formName = \"BForm\"),\n            form(dbId = 3, formId = \"3\", formName = \"aForm\"),\n            form(dbId = 4, formId = \"4\", formName = \"AForm\"),\n            form(dbId = 5, formId = \"5\", formName = \"2Form\")\n        )\n\n        createViewModel()\n\n        viewModel.sortingOrder = BlankFormListViewModel.SortOrder.NAME_ASC\n\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 1, formId = \"1\", formName = \"1Form\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 5, formId = \"5\", formName = \"2Form\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[2], form(dbId = 3, formId = \"3\", formName = \"aForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[3], form(dbId = 4, formId = \"4\", formName = \"AForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[4], form(dbId = 2, formId = \"2\", formName = \"BForm\"))\n    }\n\n    @Test\n    fun `when list of forms sorted 'by name DESC', saved should forms be ordered properly`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\", formName = \"1Form\"),\n            form(dbId = 2, formId = \"2\", formName = \"BForm\"),\n            form(dbId = 3, formId = \"3\", formName = \"aForm\"),\n            form(dbId = 4, formId = \"4\", formName = \"AForm\"),\n            form(dbId = 5, formId = \"5\", formName = \"2Form\")\n        )\n\n        createViewModel()\n\n        viewModel.sortingOrder = BlankFormListViewModel.SortOrder.NAME_DESC\n\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 2, formId = \"2\", formName = \"BForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 3, formId = \"3\", formName = \"aForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[2], form(dbId = 4, formId = \"4\", formName = \"AForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[3], form(dbId = 5, formId = \"5\", formName = \"2Form\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[4], form(dbId = 1, formId = \"1\", formName = \"1Form\"))\n    }\n\n    @Test\n    fun `when list of forms sorted 'by date newest first', saved should forms be ordered properly`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\", formName = \"1Form\"),\n            form(dbId = 2, formId = \"2\", formName = \"BForm\", lastDetectedAttachmentsUpdateDate = 6),\n            form(dbId = 3, formId = \"3\", formName = \"aForm\"),\n            form(dbId = 4, formId = \"4\", formName = \"AForm\", lastDetectedAttachmentsUpdateDate = 7),\n            form(dbId = 5, formId = \"5\", formName = \"2Form\")\n        )\n\n        createViewModel()\n\n        viewModel.sortingOrder = BlankFormListViewModel.SortOrder.DATE_DESC\n\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 4, formId = \"4\", formName = \"AForm\", lastDetectedAttachmentsUpdateDate = 7))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 2, formId = \"2\", formName = \"BForm\", lastDetectedAttachmentsUpdateDate = 6))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[2], form(dbId = 5, formId = \"5\", formName = \"2Form\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[3], form(dbId = 3, formId = \"3\", formName = \"aForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[4], form(dbId = 1, formId = \"1\", formName = \"1Form\"))\n    }\n\n    @Test\n    fun `when list of forms sorted 'by date oldest first', saved should forms be ordered properly`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\", formName = \"1Form\"),\n            form(dbId = 2, formId = \"2\", formName = \"BForm\", lastDetectedAttachmentsUpdateDate = 6),\n            form(dbId = 3, formId = \"3\", formName = \"aForm\"),\n            form(dbId = 4, formId = \"4\", formName = \"AForm\", lastDetectedAttachmentsUpdateDate = 7),\n            form(dbId = 5, formId = \"5\", formName = \"2Form\")\n        )\n\n        createViewModel()\n\n        viewModel.sortingOrder = BlankFormListViewModel.SortOrder.DATE_ASC\n\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 1, formId = \"1\", formName = \"1Form\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 3, formId = \"3\", formName = \"aForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[2], form(dbId = 5, formId = \"5\", formName = \"2Form\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[3], form(dbId = 2, formId = \"2\", formName = \"BForm\", lastDetectedAttachmentsUpdateDate = 6))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[4], form(dbId = 4, formId = \"4\", formName = \"AForm\", lastDetectedAttachmentsUpdateDate = 7))\n    }\n\n    @Test\n    fun `when list of forms sorted 'by last saved', should forms be ordered properly`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\", formName = \"1Form\"),\n            form(dbId = 2, formId = \"2\", formName = \"BForm\"),\n            form(dbId = 3, formId = \"3\", formName = \"aForm\"),\n            form(dbId = 4, formId = \"4\", formName = \"AForm\"),\n            form(dbId = 5, formId = \"5\", formName = \"2Form\")\n        )\n\n        saveInstances(\n            instance(formId = \"1\", lastStatusChangeDate = 1L),\n            instance(formId = \"3\", lastStatusChangeDate = 2L),\n            instance(formId = \"5\", lastStatusChangeDate = 3L),\n            instance(formId = \"4\", lastStatusChangeDate = 4L),\n            instance(formId = \"2\", lastStatusChangeDate = 5L)\n        )\n\n        createViewModel()\n\n        viewModel.sortingOrder = BlankFormListViewModel.SortOrder.LAST_SAVED\n\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 2, formId = \"2\", formName = \"BForm\"), 5L)\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 4, formId = \"4\", formName = \"AForm\"), 4L)\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[2], form(dbId = 5, formId = \"5\", formName = \"2Form\"), 3L)\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[3], form(dbId = 3, formId = \"3\", formName = \"aForm\"), 2L)\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[4], form(dbId = 1, formId = \"1\", formName = \"1Form\"), 1L)\n    }\n\n    @Test\n    fun `when list of forms sorted 'by last saved' and there are no saved instances, should the order from database be preserved`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\", formName = \"1Form\"),\n            form(dbId = 2, formId = \"2\", formName = \"BForm\"),\n            form(dbId = 3, formId = \"3\", formName = \"aForm\"),\n            form(dbId = 4, formId = \"4\", formName = \"AForm\"),\n            form(dbId = 5, formId = \"5\", formName = \"2Form\")\n        )\n\n        createViewModel()\n\n        viewModel.sortingOrder = BlankFormListViewModel.SortOrder.LAST_SAVED\n\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 1, formId = \"1\", formName = \"1Form\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 2, formId = \"2\", formName = \"BForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[2], form(dbId = 3, formId = \"3\", formName = \"aForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[3], form(dbId = 4, formId = \"4\", formName = \"AForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[4], form(dbId = 5, formId = \"5\", formName = \"2Form\"))\n    }\n\n    @Test\n    fun `when list of forms sorted 'by last saved', forms with no saved instances should be displayed at the bottom`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\", formName = \"1Form\"),\n            form(dbId = 2, formId = \"2\", formName = \"BForm\"),\n            form(dbId = 3, formId = \"3\", formName = \"aForm\"),\n            form(dbId = 4, formId = \"4\", formName = \"AForm\"),\n            form(dbId = 5, formId = \"5\", formName = \"2Form\")\n        )\n\n        saveInstances(\n            instance(formId = \"1\", lastStatusChangeDate = 1L),\n            instance(formId = \"3\", lastStatusChangeDate = 2L)\n        )\n\n        createViewModel()\n\n        viewModel.sortingOrder = BlankFormListViewModel.SortOrder.LAST_SAVED\n\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 3, formId = \"3\", formName = \"aForm\"), 2L)\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 1, formId = \"1\", formName = \"1Form\"), 1L)\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[2], form(dbId = 2, formId = \"2\", formName = \"BForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[3], form(dbId = 4, formId = \"4\", formName = \"AForm\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[4], form(dbId = 5, formId = \"5\", formName = \"2Form\"))\n    }\n\n    @Test\n    fun `when list of forms sorted 'by last saved', and there are different versions of the same form, sorting should distinguish different form versions`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\", formName = \"AForm v1\", version = \"1\"),\n            form(dbId = 2, formId = \"1\", formName = \"AForm v2\", version = \"2\"),\n            form(dbId = 3, formId = \"2\", formName = \"BForm\")\n        )\n\n        saveInstances(\n            instance(formId = \"1\", lastStatusChangeDate = 1L, version = \"1\"),\n            instance(formId = \"2\", lastStatusChangeDate = 2L),\n            instance(formId = \"1\", lastStatusChangeDate = 3L, version = \"2\")\n        )\n\n        createViewModel(showAllVersions = true)\n\n        viewModel.sortingOrder = BlankFormListViewModel.SortOrder.LAST_SAVED\n\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 2, formId = \"1\", formName = \"AForm v2\", version = \"2\"), 3L)\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 3, formId = \"2\", formName = \"BForm\"), 2L)\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[2], form(dbId = 1, formId = \"1\", formName = \"AForm v1\", version = \"1\"), 1L)\n    }\n\n    @Test\n    fun `list of forms should be filtered when filterText is changed`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\"),\n            form(dbId = 2, formId = \"2\", formName = \"Form 2\"),\n            form(dbId = 3, formId = \"3\", formName = \"Form 2x\")\n        )\n\n        createViewModel()\n\n        viewModel.filterText = \"2\"\n\n        assertThat(viewModel.formsToDisplay.getOrAwaitValue(scheduler).size, `is`(2))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 2, formId = \"2\"))\n        assertFormItem(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1],\n            form(dbId = 3, formId = \"3\", formName = \"Form 2x\")\n        )\n\n        viewModel.filterText = \"2x\"\n\n        assertThat(viewModel.formsToDisplay.getOrAwaitValue(scheduler).size, `is`(1))\n        assertFormItem(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0],\n            form(dbId = 3, formId = \"3\", formName = \"Form 2x\")\n        )\n\n        viewModel.filterText = \"\"\n\n        assertThat(viewModel.formsToDisplay.getOrAwaitValue(scheduler).size, `is`(3))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 1, formId = \"1\"))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 2, formId = \"2\"))\n        assertFormItem(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler)[2],\n            form(dbId = 3, formId = \"3\", formName = \"Form 2x\")\n        )\n    }\n\n    @Test\n    fun `filtering and sorting should work together`() {\n        saveForms(\n            form(dbId = 1, formId = \"1\"),\n            form(dbId = 2, formId = \"2\"),\n            form(dbId = 3, formId = \"3\", formName = \"Form 2x\")\n        )\n        createViewModel()\n\n        viewModel.filterText = \"2\"\n\n        assertThat(viewModel.formsToDisplay.getOrAwaitValue(scheduler).size, `is`(2))\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0], form(dbId = 2, formId = \"2\"))\n        assertFormItem(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1],\n            form(dbId = 3, formId = \"3\", formName = \"Form 2x\")\n        )\n\n        viewModel.sortingOrder = BlankFormListViewModel.SortOrder.NAME_DESC\n\n        assertThat(viewModel.formsToDisplay.getOrAwaitValue(scheduler).size, `is`(2))\n        assertFormItem(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler)[0],\n            form(dbId = 3, formId = \"3\", formName = \"Form 2x\")\n        )\n        assertFormItem(viewModel.formsToDisplay.getOrAwaitValue(scheduler)[1], form(dbId = 2, formId = \"2\"))\n    }\n\n    private fun saveForms(vararg forms: Form) {\n        whenever(formsDataService.getForms(any())).thenReturn(MutableStateFlow(forms.toList()))\n    }\n\n    private fun saveInstances(vararg instances: Instance) {\n        instancesRepository.deleteAll()\n\n        instances.forEach {\n            instancesRepository.save(it)\n        }\n    }\n\n    private fun createViewModel(\n        runAllBackgroundTasks: Boolean = true,\n        showAllVersions: Boolean = false\n    ) {\n        whenever(formsDataService.isSyncing(projectId)).thenReturn(MutableLiveData(false))\n        whenever(changeLockProvider.getFormLock(projectId)).thenReturn(changeLock)\n\n        viewModel = BlankFormListViewModel(\n            instancesRepository,\n            context,\n            formsDataService,\n            scheduler,\n            generalSettings,\n            projectId,\n            showAllVersions,\n            InMemUniqueIdGenerator()\n        )\n\n        if (runAllBackgroundTasks) {\n            scheduler.flush()\n        }\n    }\n\n    private fun assertFormItem(blankFormListItem: BlankFormListItem, form: Form, lastStatusChangeDate: Long = 0) {\n        assertThat(\n            blankFormListItem,\n            `is`(\n                form.toBlankFormListItem(projectId, instancesRepository)\n            )\n        )\n    }\n\n    private fun form(\n        dbId: Long,\n        formId: String = \"1\",\n        version: String? = null,\n        formName: String = \"Form $formId\",\n        deleted: Boolean = false,\n        lastDetectedAttachmentsUpdateDate: Long? = null\n    ) = Form.Builder()\n        .dbId(dbId)\n        .formId(formId)\n        .version(version)\n        .displayName(formName)\n        .date(dbId)\n        .deleted(deleted)\n        .formFilePath(FormUtils.createXFormFile(formId, version).absolutePath)\n        .lastDetectedAttachmentsUpdateDate(lastDetectedAttachmentsUpdateDate)\n        .build()\n\n    private fun instance(\n        formId: String,\n        lastStatusChangeDate: Long,\n        version: String? = null\n    ) = Instance.Builder()\n        .formId(formId)\n        .formVersion(version)\n        .lastStatusChangeDate(lastStatusChangeDate)\n        .build()\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formlists/savedformlist/DeleteSavedFormFragmentTest.kt",
    "content": "package org.odk.collect.android.formlists.savedformlist\n\nimport android.content.Context\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isChecked\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.Matchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.R\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.InstanceFixtures\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.testshared.EspressoAssertions\nimport org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView\nimport org.odk.collect.testshared.ViewActions.clickOnItemWith\nimport org.odk.collect.testshared.ViewMatchers.recyclerView\n\n@RunWith(AndroidJUnit4::class)\nclass DeleteSavedFormFragmentTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n\n    private val formsToDisplay = MutableLiveData<List<Instance>>(emptyList())\n    private val isDeleting = MutableLiveData(false)\n    private val savedFormListViewModel = mock<SavedFormListViewModel> {\n        on { formsToDisplay } doReturn formsToDisplay\n        on { isDeleting } doReturn isDeleting\n        on { deleteForms(any()) } doReturn MutableLiveData(null)\n    }\n\n    private val viewModelFactory = viewModelFactory {\n        addInitializer(SavedFormListViewModel::class) { savedFormListViewModel }\n    }\n\n    @get:Rule\n    val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule(\n        FragmentFactoryBuilder()\n            .forClass(DeleteSavedFormFragment::class) {\n                DeleteSavedFormFragment(viewModelFactory)\n            }.build()\n    )\n\n    @Test\n    fun `clicking delete selected and then cancelling does nothing`() {\n        fragmentScenarioLauncherRule.launchInContainer(DeleteSavedFormFragment::class.java)\n        formsToDisplay.value = listOf(\n            InstanceFixtures.instance(dbId = 1, displayName = \"Form 1\"),\n            InstanceFixtures.instance(dbId = 2, displayName = \"Form 2\")\n        )\n\n        onView(recyclerView()).perform(clickOnItemWith(withText(\"Form 1\")))\n        onView(recyclerView()).perform(clickOnItemWith(withText(\"Form 2\")))\n\n        onView(withText(string.delete_file)).perform(click())\n        onView(withText(context.getString(string.delete_confirm, 2)))\n            .inRoot(isDialog())\n            .check(matches(isDisplayed()))\n        onView(withText(string.delete_no))\n            .inRoot(isDialog())\n            .perform(click())\n\n        verify(savedFormListViewModel, never()).deleteForms(any())\n    }\n\n    @Test\n    fun `clicking delete selected unselects forms`() {\n        fragmentScenarioLauncherRule.launchInContainer(DeleteSavedFormFragment::class.java)\n        formsToDisplay.value = listOf(\n            InstanceFixtures.instance(dbId = 1, displayName = \"Form 1\"),\n            InstanceFixtures.instance(dbId = 2, displayName = \"Form 2\")\n        )\n\n        onView(recyclerView()).perform(clickOnItemWith(withText(\"Form 1\")))\n\n        onView(withText(string.delete_file)).perform(click())\n        onView(withText(context.getString(string.delete_confirm, 1)))\n            .inRoot(isDialog())\n            .check(matches(isDisplayed()))\n        onView(withText(string.delete_yes))\n            .inRoot(isDialog())\n            .perform(click())\n\n        onView(withRecyclerView(R.id.list).atPositionOnView(0, R.id.form_title))\n            .check(matches(withText(\"Form 1\")))\n        onView(withRecyclerView(R.id.list).atPositionOnView(0, R.id.checkbox))\n            .check(matches(not(isChecked())))\n    }\n\n    @Test\n    fun `shows progress while deleting forms`() {\n        fragmentScenarioLauncherRule.launchInContainer(DeleteSavedFormFragment::class.java)\n        formsToDisplay.value = listOf(InstanceFixtures.instance(dbId = 1))\n\n        isDeleting.value = true\n        onView(withText(string.form_delete_message)).inRoot(isDialog()).check(matches(isDisplayed()))\n\n        isDeleting.value = false\n        onView(withText(string.delete_file)).check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `displays only the latest edit when all edits are unsent`() {\n        fragmentScenarioLauncherRule.launchInContainer(DeleteSavedFormFragment::class.java)\n        formsToDisplay.value = listOf(\n            InstanceFixtures.instance(dbId = 1, displayName = \"Form\"),\n            InstanceFixtures.instance(dbId = 2, displayName = \"Form\", editOf = 1, editNumber = 1),\n            InstanceFixtures.instance(dbId = 3, displayName = \"Form\", editOf = 1, editNumber = 2),\n            InstanceFixtures.instance(dbId = 4, displayName = \"Form\", editOf = 1, editNumber = 3)\n        )\n\n        EspressoAssertions.assertNotVisible(withText(\"Form\"))\n        EspressoAssertions.assertNotVisible(withText(\"Form (Edit 1)\"))\n        EspressoAssertions.assertNotVisible(withText(\"Form (Edit 2)\"))\n        EspressoAssertions.assertVisible(withText(\"Form (Edit 3)\"))\n    }\n\n    @Test\n    fun `displays latest edit and submitted edits when not all are unsent`() {\n        fragmentScenarioLauncherRule.launchInContainer(DeleteSavedFormFragment::class.java)\n        formsToDisplay.value = listOf(\n            InstanceFixtures.instance(dbId = 1, displayName = \"Form\"),\n            InstanceFixtures.instance(dbId = 2, displayName = \"Form\", editOf = 1, editNumber = 1),\n            InstanceFixtures.instance(dbId = 3, displayName = \"Form\", editOf = 1, editNumber = 2, status = Instance.STATUS_SUBMITTED),\n            InstanceFixtures.instance(dbId = 4, displayName = \"Form\", editOf = 1, editNumber = 3)\n        )\n\n        EspressoAssertions.assertNotVisible(withText(\"Form\"))\n        EspressoAssertions.assertNotVisible(withText(\"Form (Edit 1)\"))\n        EspressoAssertions.assertVisible(withText(\"Form (Edit 2)\"))\n        EspressoAssertions.assertVisible(withText(\"Form (Edit 3)\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formlists/savedformlist/SavedFormListListMenuProviderTest.kt",
    "content": "package org.odk.collect.android.formlists.savedformlist\n\nimport androidx.appcompat.view.SupportMenuInflater\nimport androidx.appcompat.view.menu.MenuBuilder\nimport androidx.appcompat.widget.SearchView\nimport androidx.fragment.app.FragmentActivity\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.instanceOf\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.contains\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog\nimport org.odk.collect.android.formlists.sorting.FormListSortingOption\nimport org.odk.collect.android.support.CollectHelpers\nimport org.robolectric.fakes.RoboMenuItem\nimport org.robolectric.shadows.ShadowDialog\n\n@RunWith(AndroidJUnit4::class)\nclass SavedFormListListMenuProviderTest {\n\n    private val activity: FragmentActivity =\n        CollectHelpers.createThemedActivity(FragmentActivity::class.java)\n\n    private val viewModel: SavedFormListViewModel = mock()\n    private val menuInflater: SupportMenuInflater\n        get() = SupportMenuInflater(activity)\n\n    @Test\n    fun `changing search text should set filterText in viewModel`() {\n        val menu = MenuBuilder(activity)\n        val menuProvider = SavedFormListListMenuProvider(activity, viewModel)\n\n        menuProvider.onCreateMenu(menu, menuInflater)\n        menuProvider.onPrepareMenu(menu)\n\n        val searchView =\n            (menu.findItem(R.id.menu_filter).actionView as SearchView).findViewById<SearchView.SearchAutoComplete>(\n                androidx.appcompat.R.id.search_src_text\n            )\n        searchView.setText(\"abc\")\n        verify(viewModel).filterText = \"abc\"\n    }\n\n    @Test\n    fun `clicking search hides sort and hiding search shows it again`() {\n        val menu = MenuBuilder(activity)\n        val menuProvider = SavedFormListListMenuProvider(activity, viewModel)\n\n        menuProvider.onCreateMenu(menu, menuInflater)\n        menuProvider.onPrepareMenu(menu)\n\n        menu.findItem(R.id.menu_filter).expandActionView()\n        assertThat(menu.findItem(R.id.menu_sort).isVisible, equalTo(false))\n\n        menu.findItem(R.id.menu_filter).collapseActionView()\n        assertThat(menu.findItem(R.id.menu_sort).isVisible, equalTo(true))\n    }\n\n    @Test\n    fun `clicking sort displays sorting dialog`() {\n        whenever(viewModel.sortOrder).doReturn(SavedFormListViewModel.SortOrder.DATE_DESC)\n\n        val menuProvider = SavedFormListListMenuProvider(activity, viewModel)\n        menuProvider.onMenuItemSelected(RoboMenuItem(R.id.menu_sort))\n\n        val dialog = ShadowDialog.getLatestDialog()\n        assertThat(dialog, instanceOf(FormListSortingBottomSheetDialog::class.java))\n\n        val formListSortingBottomSheetDialog = dialog as FormListSortingBottomSheetDialog\n        assertThat(dialog.selectedOption, equalTo(2))\n        assertThat(\n            formListSortingBottomSheetDialog.options,\n            contains(\n                FormListSortingOption(\n                    R.drawable.ic_sort_by_alpha,\n                    org.odk.collect.strings.R.string.sort_by_name_asc\n                ),\n                FormListSortingOption(\n                    R.drawable.ic_sort_by_alpha,\n                    org.odk.collect.strings.R.string.sort_by_name_desc\n                ),\n                FormListSortingOption(\n                    R.drawable.ic_access_time,\n                    org.odk.collect.strings.R.string.sort_by_date_desc\n                ),\n                FormListSortingOption(\n                    R.drawable.ic_access_time,\n                    org.odk.collect.strings.R.string.sort_by_date_asc\n                )\n            )\n        )\n\n        formListSortingBottomSheetDialog.onSelectedOptionChanged.accept(3)\n        verify(viewModel).sortOrder = SavedFormListViewModel.SortOrder.DATE_ASC\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formlists/savedformlist/SavedFormListViewModelTest.kt",
    "content": "package org.odk.collect.android.formlists.savedformlist\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.contains\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formlists.savedformlist.SavedFormListViewModel.SortOrder\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.InstanceFixtures\nimport org.odk.collect.shared.settings.InMemSettings\nimport org.odk.collect.testshared.FakeScheduler\nimport org.odk.collect.testshared.getOrAwaitValue\n\n@RunWith(AndroidJUnit4::class)\nclass SavedFormListViewModelTest {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    private val scheduler = FakeScheduler()\n    private val settings = InMemSettings()\n\n    private val instancesDataService: InstancesDataService = mock {\n        on { getInstances(any()) } doReturn MutableStateFlow(emptyList())\n    }\n\n    @Test\n    fun `formsToDisplay should not include deleted forms`() {\n        val myForm = InstanceFixtures.instance(displayName = \"My form\", deletedDate = 1)\n        val yourForm = InstanceFixtures.instance(displayName = \"Your form\")\n        saveForms(\"projectId\", listOf(myForm, yourForm))\n\n        val viewModel = SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(yourForm)\n        )\n    }\n\n    @Test\n    fun `setting filterText filters forms on display name`() {\n        val myForm = InstanceFixtures.instance(displayName = \"My form\")\n        val yourForm = InstanceFixtures.instance(displayName = \"Your form\")\n        saveForms(\"projectId\", listOf(myForm, yourForm),)\n\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n\n        viewModel.filterText = \"Your\"\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(yourForm)\n        )\n\n        viewModel.filterText = \"form\"\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(myForm, yourForm)\n        )\n    }\n\n    @Test\n    fun `clearing filterText does not filter forms`() {\n        val myForm = InstanceFixtures.instance(displayName = \"My form\")\n        val yourForm = InstanceFixtures.instance(displayName = \"Your form\")\n        saveForms(\"projectId\", listOf(myForm, yourForm),)\n\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n\n        viewModel.filterText = \"blah\"\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            equalTo(emptyList())\n        )\n\n        viewModel.filterText = \"\"\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(myForm, yourForm)\n        )\n    }\n\n    @Test\n    fun `filtering forms is not case sensitive`() {\n        val myForm = InstanceFixtures.instance(displayName = \"My form\")\n        val yourForm = InstanceFixtures.instance(displayName = \"Your form\")\n        saveForms(\"projectId\", listOf(myForm, yourForm),)\n\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n\n        viewModel.filterText = \"my\"\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(myForm)\n        )\n    }\n\n    @Test\n    fun `can sort forms by ascending name`() {\n        val aa = InstanceFixtures.instance(displayName = \"Aa\")\n        val ab = InstanceFixtures.instance(displayName = \"ab\")\n        val b = InstanceFixtures.instance(displayName = \"B\")\n        saveForms(\"projectId\", listOf(b, aa, ab),)\n\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n\n        viewModel.sortOrder = SortOrder.NAME_ASC\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(aa, ab, b)\n        )\n    }\n\n    @Test\n    fun `can sort forms by descending name`() {\n        val aa = InstanceFixtures.instance(displayName = \"Aa\")\n        val ab = InstanceFixtures.instance(displayName = \"ab\")\n        val b = InstanceFixtures.instance(displayName = \"B\")\n        saveForms(\"projectId\", listOf(b, aa, ab),)\n\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n\n        viewModel.sortOrder = SortOrder.NAME_DESC\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(b, ab, aa)\n        )\n    }\n\n    @Test\n    fun `can sort forms by descending date`() {\n        val a = InstanceFixtures.instance(displayName = \"A\", lastStatusChangeDate = 0)\n        val b = InstanceFixtures.instance(displayName = \"B\", lastStatusChangeDate = 1)\n        saveForms(\"projectId\", listOf(a, b),)\n\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n\n        viewModel.sortOrder = SortOrder.DATE_DESC\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(b, a)\n        )\n    }\n\n    @Test\n    fun `can sort forms by ascending date`() {\n        val a = InstanceFixtures.instance(displayName = \"A\", lastStatusChangeDate = 0)\n        val b = InstanceFixtures.instance(displayName = \"B\", lastStatusChangeDate = 1)\n        saveForms(\"projectId\", listOf(b, a),)\n\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n\n        viewModel.sortOrder = SortOrder.DATE_ASC\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(a, b)\n        )\n    }\n\n    @Test\n    fun `sort order is retained between view models`() {\n        val a = InstanceFixtures.instance(displayName = \"A\", lastStatusChangeDate = 0)\n        val b = InstanceFixtures.instance(displayName = \"B\", lastStatusChangeDate = 1)\n        saveForms(\"projectId\", listOf(b, a),)\n\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n        viewModel.sortOrder = SortOrder.DATE_ASC\n\n        val newViewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n        assertThat(newViewModel.sortOrder, equalTo(SortOrder.DATE_ASC))\n        assertThat(\n            newViewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(a, b)\n        )\n    }\n\n    @Test\n    fun `isDeleting is true while deleting forms`() {\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n        assertThat(viewModel.isDeleting.getOrAwaitValue(), equalTo(false))\n\n        viewModel.deleteForms(longArrayOf(1))\n        assertThat(viewModel.isDeleting.getOrAwaitValue(), equalTo(true))\n\n        scheduler.flush()\n        assertThat(viewModel.isDeleting.getOrAwaitValue(), equalTo(false))\n    }\n\n    @Test\n    fun `deleteForms should return 0 if instances can not be deleted`() {\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n        whenever(instancesDataService.deleteInstances(any(), any())).thenReturn(false)\n\n        val result = viewModel.deleteForms(longArrayOf(1))\n        assertThat(result.getOrAwaitValue(scheduler)!!.value, equalTo(0))\n    }\n\n    @Test\n    fun `deleteForms should return the number of instances after deleting`() {\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n        whenever(instancesDataService.deleteInstances(any(), any())).thenReturn(true)\n\n        val result = viewModel.deleteForms(longArrayOf(1))\n        assertThat(result.getOrAwaitValue(scheduler)!!.value, equalTo(1))\n    }\n\n    @Test\n    fun `filtering takes into account edit numbers`() {\n        val originalInstance = InstanceFixtures.instance(displayName = \"My form\")\n        val editedInstance = InstanceFixtures.instance(displayName = \"My form\", editOf = 1, editNumber = 1)\n        saveForms(\"projectId\", listOf(originalInstance, editedInstance))\n\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n\n        viewModel.filterText = \"Edit 1\"\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(editedInstance)\n        )\n    }\n\n    @Test\n    fun `sorting takes into account edit numbers`() {\n        val instance1 = InstanceFixtures.instance(displayName = \"My form\", editOf = 1, editNumber = 1)\n        val instance2 = InstanceFixtures.instance(displayName = \"My form\", editOf = 1, editNumber = 2)\n        val instance3 = InstanceFixtures.instance(displayName = \"My form\", editOf = 1, editNumber = 3)\n        saveForms(\"projectId\", listOf(instance1, instance2, instance3),)\n\n        val viewModel =\n            SavedFormListViewModel(scheduler, settings, instancesDataService, \"projectId\")\n\n        viewModel.sortOrder = SortOrder.NAME_DESC\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(instance3, instance2, instance1)\n        )\n\n        viewModel.sortOrder = SortOrder.NAME_ASC\n        assertThat(\n            viewModel.formsToDisplay.getOrAwaitValue(scheduler),\n            contains(instance1, instance2, instance3)\n        )\n    }\n\n    private fun saveForms(projectId: String, instances: List<Instance>) {\n        whenever(instancesDataService.getInstances(projectId)).doReturn(MutableStateFlow(instances))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/DownloadMediaFilesServerFormUseCasesTest.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.mockito.kotlin.doAnswer\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.utilities.FileUtils\nimport org.odk.collect.entities.storage.InMemEntitiesRepository\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormSource\nimport org.odk.collect.forms.ManifestFile\nimport org.odk.collect.forms.MediaFile\nimport org.odk.collect.formstest.FormFixtures\nimport org.odk.collect.formstest.FormUtils\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.shared.strings.Md5.getMd5Hash\nimport java.io.File\n\nclass DownloadMediaFilesServerFormUseCasesTest {\n    @Test\n    fun `#downloadMediaFiles returns false when there is an existing copy of a media file and an older one`() {\n        var date: Long = 0\n        // Save forms\n        val formsRepository = InMemFormsRepository {\n            date += 1\n            date\n        }\n        val form1 = FormFixtures.form(\n            version = \"1\",\n            mediaFiles = listOf(Pair(\"file\", \"old\"))\n        )\n        formsRepository.save(form1)\n\n        val form2 = FormFixtures.form(\n            version = \"2\",\n            mediaFiles = listOf(Pair(\"file\", \"existing\"))\n        )\n        formsRepository.save(form2)\n\n        // Set up same media file on server\n        val existingMediaFileHash = File(form2.formMediaPath, \"file\").getMd5Hash()!!\n        val mediaFile = MediaFile(\"file\", existingMediaFileHash, \"downloadUrl\")\n        val manifestFile = ManifestFile(null, listOf(mediaFile))\n        val serverFormDetails =\n            ServerFormDetails(null, null, \"formId\", \"3\", null, false, true, manifestFile)\n        val formSource = mock<FormSource> {\n            on { fetchMediaFile(mediaFile.downloadUrl) } doReturn \"existing\".toByteArray()\n                .inputStream()\n        }\n\n        val result = ServerFormUseCases.downloadMediaFiles(\n            serverFormDetails,\n            formSource,\n            formsRepository,\n            File(TempFiles.createTempDir(), \"temp\").absolutePath,\n            TempFiles.createTempDir(),\n            InMemEntitiesRepository(),\n            mock(),\n            mock()\n        )\n\n        assertThat(result, equalTo(MediaFilesDownloadResult(false, false)))\n    }\n\n    @Test\n    fun `#downloadMediaFiles returns false when there is an existing copy of a media file and an older one and media file list hash doesn't match existing copy`() {\n        // Save forms\n        var date: Long = 0\n        val formsRepository = InMemFormsRepository {\n            date += 1\n            date\n        }\n        val form1 = FormFixtures.form(\n            version = \"1\",\n            mediaFiles = listOf(Pair(\"file\", \"old\"))\n        )\n        formsRepository.save(form1)\n\n        val form2 = FormFixtures.form(\n            version = \"2\",\n            mediaFiles = listOf(Pair(\"file\", \"existing\"))\n        )\n        formsRepository.save(form2)\n\n        // Set up same media file on server\n        val mediaFile = MediaFile(\"file\", \"somethingElse\", \"downloadUrl\")\n        val manifestFile = ManifestFile(null, listOf(mediaFile))\n        val serverFormDetails =\n            ServerFormDetails(null, null, \"formId\", \"3\", null, false, true, manifestFile)\n        val formSource = mock<FormSource> {\n            on { fetchMediaFile(mediaFile.downloadUrl) } doReturn \"existing\".toByteArray()\n                .inputStream()\n        }\n\n        val result = ServerFormUseCases.downloadMediaFiles(\n            serverFormDetails,\n            formSource,\n            formsRepository,\n            File(TempFiles.createTempDir(), \"temp\").absolutePath,\n            TempFiles.createTempDir(),\n            InMemEntitiesRepository(),\n            mock(),\n            mock()\n        )\n\n        assertThat(result, equalTo(MediaFilesDownloadResult(false, false)))\n    }\n\n    @Test\n    fun `#downloadMediaFiles does not download an entity list when it has already been downloaded for a different form`() {\n        val formsRepository = InMemFormsRepository()\n        val entitiesRepository = InMemEntitiesRepository()\n\n        val mediaFile = MediaFile(\"file\", \"hash\", \"downloadUrl\", type = MediaFile.Type.ENTITY_LIST)\n        val manifestFile = ManifestFile(null, listOf(mediaFile))\n        val form1 =\n            ServerFormDetails(null, null, \"1\", \"1\", null, true, false, manifestFile)\n        val form2 =\n            ServerFormDetails(null, null, \"2\", \"1\", null, true, false, manifestFile)\n        val formSource = mock<FormSource> {\n            on { fetchMediaFile(mediaFile.downloadUrl) } doAnswer {\n                \"name,label,__version\".toByteArray().inputStream()\n            }\n        }\n\n        ServerFormUseCases.downloadMediaFiles(\n            form1,\n            formSource,\n            formsRepository,\n            File(TempFiles.createTempDir(), \"temp\").absolutePath,\n            TempFiles.createTempDir(),\n            entitiesRepository,\n            mock(),\n            mock()\n        )\n\n        verify(formSource, times(1)).fetchMediaFile(mediaFile.downloadUrl)\n\n        ServerFormUseCases.downloadMediaFiles(\n            form2,\n            formSource,\n            formsRepository,\n            File(TempFiles.createTempDir(), \"temp\").absolutePath,\n            TempFiles.createTempDir(),\n            entitiesRepository,\n            mock(),\n            mock()\n        )\n\n        verify(formSource, times(1)).fetchMediaFile(mediaFile.downloadUrl)\n    }\n\n    @Test\n    fun `#copySavedFileFromPreviousFormVersionIfExists does not copy any file if there is no matching last-saved file`() {\n        val destinationMediaDirPath = TempFiles.createTempDir().absolutePath\n        ServerFormUseCases.copySavedFileFromPreviousFormVersionIfExists(\n            InMemFormsRepository(),\n            \"1\",\n            destinationMediaDirPath\n        )\n\n        val resultFile = File(destinationMediaDirPath, FileUtils.LAST_SAVED_FILENAME)\n        assertThat(resultFile.exists(), equalTo(false))\n    }\n\n    @Test\n    fun `#copySavedFileFromPreviousFormVersionIfExists copies the newest matching last-saved file for given formId`() {\n        val tempDir1 = TempFiles.createTempDir()\n        val file1 = TempFiles.createTempFile(tempDir1, \"last-saved\", \".xml\")\n        org.apache.commons.io.FileUtils.writeByteArrayToFile(file1, \"file1\".toByteArray())\n\n        val tempDir2 = TempFiles.createTempDir()\n        val file2 = TempFiles.createTempFile(tempDir2, \"last-saved\", \".xml\")\n        org.apache.commons.io.FileUtils.writeByteArrayToFile(file2, \"file2\".toByteArray())\n\n        val tempDir3 = TempFiles.createTempDir()\n        val file3 = TempFiles.createTempFile(tempDir3, \"last-saved\", \".xml\")\n        org.apache.commons.io.FileUtils.writeByteArrayToFile(file3, \"file3\".toByteArray())\n\n        val tempDir4 = TempFiles.createTempDir()\n        val file4 = TempFiles.createTempFile(tempDir4, \"last-saved\", \".xml\")\n        org.apache.commons.io.FileUtils.writeByteArrayToFile(file4, \"file4\".toByteArray())\n\n        val formsRepository = InMemFormsRepository().also {\n            it.save(\n                Form.Builder()\n                    .dbId(1)\n                    .formId(\"1\")\n                    .version(\"1\")\n                    .date(0)\n                    .formFilePath(FormUtils.createXFormFile(\"1\", \"1\").absolutePath)\n                    .formMediaPath(file1.parent)\n                    .build()\n            )\n\n            it.save(\n                Form.Builder()\n                    .dbId(2)\n                    .formId(\"1\")\n                    .version(\"2\")\n                    .date(2)\n                    .formFilePath(FormUtils.createXFormFile(\"1\", \"2\").absolutePath)\n                    .formMediaPath(file2.parent)\n                    .build()\n            )\n\n            it.save(\n                Form.Builder()\n                    .dbId(3)\n                    .formId(\"1\")\n                    .version(\"3\")\n                    .date(1)\n                    .formFilePath(FormUtils.createXFormFile(\"1\", \"3\").absolutePath)\n                    .formMediaPath(file3.parent)\n                    .build()\n            )\n\n            it.save(\n                Form.Builder()\n                    .dbId(4)\n                    .formId(\"2\")\n                    .version(\"1\")\n                    .date(3)\n                    .formFilePath(FormUtils.createXFormFile(\"2\", \"1\").absolutePath)\n                    .formMediaPath(file4.parent)\n                    .build()\n            )\n        }\n\n        val destinationMediaDirPath = TempFiles.createTempDir().absolutePath\n        ServerFormUseCases.copySavedFileFromPreviousFormVersionIfExists(\n            formsRepository,\n            \"1\",\n            destinationMediaDirPath\n        )\n\n        val resultFile = File(destinationMediaDirPath, FileUtils.LAST_SAVED_FILENAME)\n        assertThat(resultFile.readText(), equalTo(\"file2\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/DownloadUpdatesServerFormUseCasesTest.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.mockito.Mockito.any\nimport org.mockito.Mockito.doAnswer\nimport org.mockito.invocation.InvocationOnMock\nimport org.mockito.kotlin.mock\nimport org.mockito.stubbing.Answer\nimport org.odk.collect.android.formmanagement.download.FormDownloadException\nimport org.odk.collect.android.formmanagement.download.FormDownloader\nimport org.odk.collect.forms.ManifestFile\n\nclass DownloadUpdatesServerFormUseCasesTest {\n\n    @Test\n    fun `#downloadUpdates returns completed downloads when cancelled`() {\n        val formDownloader = mock<FormDownloader>()\n\n        val serverForms = listOf(\n            ServerFormDetails(\"\", \"\", \"\", \"\", \"\", false, true, ManifestFile(\"\", emptyList())),\n            ServerFormDetails(\"\", \"\", \"\", \"\", \"\", false, true, ManifestFile(\"\", emptyList()))\n        )\n\n        // Cancel form download after downloading one form\n        doAnswer(object : Answer<Unit> {\n            private var calledBefore = false\n\n            @Throws(Throwable::class)\n            override fun answer(invocation: InvocationOnMock) {\n                calledBefore = if (!calledBefore) {\n                    true\n                } else {\n                    throw FormDownloadException.DownloadingInterrupted()\n                }\n            }\n        }).`when`(formDownloader).downloadForm(any(), any(), any())\n\n        val results = ServerFormUseCases.downloadForms(\n            serverForms,\n            formDownloader\n        )\n\n        assertThat(results.size, equalTo(1))\n        assertThat(results[serverForms[0]], equalTo(null))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.contains\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.nullValue\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormListItem\nimport org.odk.collect.forms.FormSource\nimport org.odk.collect.forms.FormsRepository\nimport org.odk.collect.forms.ManifestFile\nimport org.odk.collect.forms.MediaFile\nimport org.odk.collect.formstest.FormUtils\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.shared.strings.Md5.getMd5Hash\nimport java.io.BufferedWriter\nimport java.io.ByteArrayInputStream\nimport java.io.File\nimport java.io.FileWriter\n\nclass FetchFormDetailsServerFormUseCasesTest {\n\n    private val formsRepository: FormsRepository = InMemFormsRepository()\n    private val formSource = mock<FormSource> {\n        on { fetchManifest(MANIFEST_URL) } doReturn ManifestFile(\n            \"manifest-hash\",\n            listOf(\n                MEDIA_FILE\n            )\n        )\n    }\n\n    @Test\n    fun whenFormHasManifestUrl_returnsMediaFilesInDetails() {\n        whenever(formSource.fetchFormList()).thenReturn(\n            listOf(FORM_WITHOUT_MANIFEST, FORM_WITH_MANIFEST)\n        )\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        assertThat(getFormFromList(serverFormDetails, \"form-1\").manifest, nullValue())\n        assertThat(\n            getFormFromList(serverFormDetails, \"form-2\").manifest!!.mediaFiles,\n            contains(MEDIA_FILE)\n        )\n    }\n\n    @Test\n    fun whenFormDoesNotExist_isNew() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST))\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-1\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.New))\n    }\n\n    @Test\n    fun whenAFormIsSoftDeleted_isNew() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST))\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-1\")\n                .version(\"server\")\n                .md5Hash(\"form-1-hash\")\n                .deleted(true)\n                .formFilePath(FormUtils.createXFormFile(\"form-1\", \"server\").absolutePath)\n                .build()\n        )\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-1\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.New))\n    }\n\n    @Test\n    fun whenAFormExists_andListContainsNewVersionWithDifferentHash_isUpdatedVersion() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST))\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-1\")\n                .md5Hash(\"form-1-hash-old\")\n                .formFilePath(FormUtils.createXFormFile(\"form-1\", null).absolutePath)\n                .build()\n        )\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-1\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.UpdatedVersion))\n    }\n\n    @Test\n    fun whenAFormExists_andListContainsSameVersionWithDifferentHash_isUpdatedHash() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST))\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-1\")\n                .version(\"1\")\n                .md5Hash(\"form-1-hash-old\")\n                .formFilePath(FormUtils.createXFormFile(\"form-1\", null).absolutePath)\n                .build()\n        )\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-1\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.UpdatedHash))\n    }\n\n    @Test\n    fun whenAFormExists_andHasNewMediaFileOnServer_isUpdatedMedia() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST))\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-2\")\n                .version(\"2\")\n                .md5Hash(\"form-2-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"form-2\", \"2\").absolutePath)\n                .formMediaPath(\"/does-not-exist\")\n                .build()\n        )\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-2\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.UpdatedMedia))\n    }\n\n    @Test\n    fun whenAFormExists_andHasUpdatedMediaFileOnServer_isUpdatedMedia() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST))\n\n        val mediaDir = TempFiles.createTempDir()\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-2\")\n                .version(\"2\")\n                .md5Hash(\"form-2-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"form-2\", \"2\").absolutePath)\n                .formMediaPath(mediaDir.absolutePath)\n                .build()\n        )\n        val oldMediaFile = TempFiles.createTempFile(mediaDir, \"blah\", \".csv\")\n        writeToFile(oldMediaFile, \"blah before\")\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-2\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.UpdatedMedia))\n    }\n\n    @Test\n    fun whenAFormExists_andItsNewerVersionWithUpdatedMediaFilesHasBeenAlreadyDownloaded_isOnDevice() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST))\n\n        val mediaDir1 = TempFiles.createTempDir()\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-2\")\n                .version(\"1\")\n                .md5Hash(\"form-2_v1-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"form-2\", \"1\").absolutePath)\n                .formMediaPath(mediaDir1.absolutePath)\n                .build()\n        )\n        val mediaFile1 = TempFiles.createTempFile(mediaDir1, MEDIA_FILE.filename)\n        writeToFile(mediaFile1, \"old blah\")\n\n        val mediaDir2 = TempFiles.createTempDir()\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-2\")\n                .version(\"2\")\n                .md5Hash(\"form-2-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"form-2\", \"2\").absolutePath)\n                .formMediaPath(mediaDir2.absolutePath)\n                .build()\n        )\n        val mediaFile2 = TempFiles.createTempFile(mediaDir2, MEDIA_FILE.filename)\n        writeToFile(mediaFile2, FILE_CONTENT)\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-2\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.OnDevice))\n    }\n\n    @Test\n    fun whenAFormExists_andItsNewerVersionHasBeenAlreadyDownloadedButThenSoftDeleted_isNew() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST))\n\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-1\")\n                .version(\"0\")\n                .md5Hash(\"form-1-hash0\")\n                .formFilePath(FormUtils.createXFormFile(\"form-1\", \"0\").absolutePath)\n                .build()\n        )\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-1\")\n                .version(\"1\")\n                .md5Hash(\"form-1-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"form-1\", \"1\").absolutePath)\n                .deleted(true)\n                .build()\n        )\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-1\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.New))\n    }\n\n    @Test\n    fun whenAFormExists_andItsNewerVersionWithMediaFilesHasBeenAlreadyDownloadedButThenSoftDeleted_isNew() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST))\n\n        val mediaDir1 = TempFiles.createTempDir()\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-2\")\n                .version(\"1\")\n                .md5Hash(\"form-2_v1-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"form-2\", \"1\").absolutePath)\n                .formMediaPath(mediaDir1.absolutePath)\n                .build()\n        )\n        val mediaFile1 = TempFiles.createTempFile(mediaDir1, MEDIA_FILE.filename)\n        writeToFile(mediaFile1, FILE_CONTENT)\n\n        val mediaDir2 = TempFiles.createTempDir()\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-2\")\n                .version(\"2\")\n                .md5Hash(\"form-2-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"form-2\", \"2\").absolutePath)\n                .formMediaPath(mediaDir2.absolutePath)\n                .deleted(true)\n                .build()\n        )\n        val mediaFile2 = TempFiles.createTempFile(mediaDir2, MEDIA_FILE.filename)\n        writeToFile(mediaFile2, FILE_CONTENT)\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-2\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.New))\n    }\n\n    @Test\n    fun whenAFormExists_andIsNotUpdatedOnServer_andDoesNotHaveAManifest_isOnDevice() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST))\n\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-1\")\n                .version(\"1\")\n                .md5Hash(\"form-1-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"form-1\", \"1\").absolutePath)\n                .build()\n        )\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-1\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.OnDevice))\n    }\n\n    @Test\n    fun whenFormExists_andMediaFilesExist_isOnDevice() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST))\n\n        val mediaDir = TempFiles.createTempDir()\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-2\")\n                .version(\"2\")\n                .md5Hash(\"form-2-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"form-2\", \"2\").absolutePath)\n                .formMediaPath(mediaDir.absolutePath)\n                .build()\n        )\n        val mediaFile = TempFiles.createTempFile(mediaDir, \"blah\", \".csv\")\n        writeToFile(mediaFile, FILE_CONTENT)\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-2\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.OnDevice))\n    }\n\n    @Test\n    fun whenAFormExists_andIsUpdatedOnServer_andDoesNotHaveNewMedia_isUpdatedVersion() {\n        whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST))\n\n        val mediaDir = TempFiles.createTempDir()\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-2\")\n                .md5Hash(\"form-2-hash-old\")\n                .formFilePath(FormUtils.createXFormFile(\"form-2\", \"2\").absolutePath)\n                .formMediaPath(mediaDir.absolutePath)\n                .build()\n        )\n        val localMediaFile = TempFiles.createTempFile(mediaDir, \"blah\", \".csv\")\n        writeToFile(localMediaFile, FILE_CONTENT)\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-2\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.UpdatedVersion))\n    }\n\n    @Test\n    fun whenAFormExists_andItsNewerVersionWithManifestIsAvailableButHasNullHash_isOnDevice() {\n        whenever(formSource.fetchFormList()).thenReturn(\n            listOf(FORM_WITH_MANIFEST.copy(hash = null))\n        )\n\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"form-2\")\n                .version(\"0\")\n                .md5Hash(\"form-2-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"form-2\", \"1\").absolutePath)\n                .build()\n        )\n\n        val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource)\n        val form = getFormFromList(serverFormDetails, \"form-2\")\n        assertThat(form.type, equalTo(ServerFormDetails.Type.OnDevice))\n    }\n\n    private fun writeToFile(mediaFile: File, blah: String) {\n        val bw = BufferedWriter(FileWriter(mediaFile))\n        bw.write(blah)\n        bw.close()\n    }\n\n    private fun getFormFromList(\n        serverFormDetails: List<ServerFormDetails>,\n        formId: String\n    ): ServerFormDetails {\n        return serverFormDetails.stream().filter { it.formId == formId }.findAny().get()\n    }\n}\n\nprivate const val MANIFEST_URL = \"http://example.com/form-3-manifest\"\nprivate const val FILE_CONTENT = \"blah\"\nprivate val MEDIA_FILE = MediaFile(\n    \"blah.txt\",\n    getMd5Hash(\n        ByteArrayInputStream(\n            FILE_CONTENT.toByteArray()\n        )\n    )!!,\n    \"http://example.com/media-file\"\n)\n\nprivate val FORM_WITHOUT_MANIFEST =\n    FormListItem(\"http://example.com/form-1\", \"form-1\", \"1\", \"form-1-hash\", \"Form 1\", null)\n\nprivate val FORM_WITH_MANIFEST = FormListItem(\n    \"http://example.com/form-2\",\n    \"form-2\",\n    \"2\",\n    \"form-2-hash\",\n    \"Form 2\",\n    MANIFEST_URL\n)\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/FilterFormsToAddTest.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport org.junit.Assert\nimport org.junit.Test\nimport org.odk.collect.android.formmanagement.LocalFormUseCases.filterFormsToAdd\nimport java.io.File\n\nclass FilterFormsToAddTest {\n\n    @Test\n    fun filterEmptyListOfForms() {\n        val formDefs = arrayOf<File>()\n        val files: List<File?> = filterFormsToAdd(formDefs, 0)\n        Assert.assertEquals(0, files.size.toLong())\n    }\n\n    @Test\n    fun filterNullListOfForms() {\n        val files: List<File?> = filterFormsToAdd(null, 0)\n        Assert.assertEquals(0, files.size.toLong())\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/FormFillingIntentFactoryTest.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport android.app.Activity\nimport android.content.Intent.ACTION_EDIT\nimport androidx.test.espresso.intent.Intents.intended\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasAction\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasData\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.Matchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.external.FormUriActivity\nimport org.odk.collect.android.external.InstancesContract\nimport org.odk.collect.android.formentry.FormOpeningMode.FORM_MODE_KEY\nimport org.odk.collect.android.formentry.FormOpeningMode.VIEW_SENT\nimport org.odk.collect.androidtest.RecordedIntentsRule\nimport org.robolectric.Robolectric\n\n@RunWith(AndroidJUnit4::class)\nclass FormFillingIntentFactoryTest {\n\n    @get:Rule\n    val recordedIntentsRule = RecordedIntentsRule()\n\n    private val activity = Robolectric.buildActivity(Activity::class.java).get()\n\n    @Test\n    fun `newInstance starts FormUriActivity with instance URI`() {\n        activity.startActivity(FormFillingIntentFactory.newFormIntent(activity, InstancesContract.getUri(\"projectId\", 101)))\n\n        intended(hasAction(ACTION_EDIT))\n        intended(hasComponent(FormUriActivity::class.java.name))\n        intended(hasData(InstancesContract.getUri(\"projectId\", 101)))\n        intended(not(hasExtra(FORM_MODE_KEY, VIEW_SENT)))\n    }\n\n    @Test\n    fun `editInstance starts FormUriActivity with instance URI`() {\n        activity.startActivity(FormFillingIntentFactory.editDraftFormIntent(activity, \"projectId\", 101))\n\n        intended(hasAction(ACTION_EDIT))\n        intended(hasComponent(FormUriActivity::class.java.name))\n        intended(hasData(InstancesContract.getUri(\"projectId\", 101)))\n        intended(not(hasExtra(FORM_MODE_KEY, VIEW_SENT)))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/FormSourceExceptionMapperTest.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.forms.FormSourceException\n\n@RunWith(AndroidJUnit4::class)\nclass FormSourceExceptionMapperTest {\n    private lateinit var context: Context\n    private lateinit var mapper: FormSourceExceptionMapper\n\n    @Before\n    fun setup() {\n        context = ApplicationProvider.getApplicationContext()\n        mapper = FormSourceExceptionMapper(context)\n    }\n\n    @Test\n    fun fetchError_returnsGenericMessage() {\n        val expectedString = context.getString(org.odk.collect.strings.R.string.report_to_project_lead)\n        assertThat(mapper.getMessage(FormSourceException.FetchError()), `is`(expectedString))\n    }\n\n    @Test\n    fun unreachable_returnsUnknownHostMessage() {\n        val expectedString = context.getString(\n            org.odk.collect.strings.R.string.unreachable_error,\n            \"http://unknown.com\"\n        ) + \" \" + context.getString(org.odk.collect.strings.R.string.report_to_project_lead)\n        assertThat(\n            mapper.getMessage(FormSourceException.Unreachable(\"http://unknown.com\")),\n            `is`(expectedString)\n        )\n    }\n\n    @Test\n    fun securityError_returnsSecurityMessage() {\n        val expectedString = context.getString(\n            org.odk.collect.strings.R.string.security_error,\n            \"http://unknown.com\"\n        ) + \" \" + context.getString(org.odk.collect.strings.R.string.report_to_project_lead)\n        assertThat(\n            mapper.getMessage(FormSourceException.SecurityError(\"http://unknown.com\")),\n            `is`(expectedString)\n        )\n    }\n\n    @Test\n    fun serverError_returnsServerErrorMessage() {\n        val expectedString = context.getString(\n            org.odk.collect.strings.R.string.server_error,\n            \"http://unknown.com\",\n            500\n        ) + \" \" + context.getString(org.odk.collect.strings.R.string.report_to_project_lead)\n        assertThat(\n            mapper.getMessage(\n                FormSourceException.ServerError(\n                    500,\n                    \"http://unknown.com\"\n                )\n            ),\n            `is`(expectedString)\n        )\n    }\n\n    @Test\n    fun parseError_returnsParserErrorMessage() {\n        val expectedString = context.getString(\n            org.odk.collect.strings.R.string.invalid_response,\n            \"http://unknown.com\"\n        ) + \" \" + context.getString(org.odk.collect.strings.R.string.report_to_project_lead)\n        assertThat(\n            mapper.getMessage(FormSourceException.ParseError(\"http://unknown.com\")),\n            `is`(expectedString)\n        )\n    }\n\n    @Test\n    fun serverNotOpenRosaError_returnsNotOpenRosaMessage() {\n        val expectedString =\n            \"This server does not correctly implement the OpenRosa formList API. \" + context.getString(\n                org.odk.collect.strings.R.string.report_to_project_lead\n            )\n        assertThat(\n            mapper.getMessage(FormSourceException.ServerNotOpenRosaError()),\n            `is`(expectedString)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/FormsDataServiceTest.kt",
    "content": "package org.odk.collect.android.formmanagement\n\nimport android.app.Application\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.Mockito.verifyNoInteractions\nimport org.mockito.Mockito.verifyNoMoreInteractions\nimport org.mockito.kotlin.doAnswer\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.injection.config.ProjectDependencyModuleFactory\nimport org.odk.collect.android.notifications.Notifier\nimport org.odk.collect.android.projects.ProjectDependencyModule\nimport org.odk.collect.android.storage.StorageSubdirectory\nimport org.odk.collect.android.utilities.ChangeLockProvider\nimport org.odk.collect.androidshared.data.AppState\nimport org.odk.collect.androidtest.getOrAwaitValue\nimport org.odk.collect.androidtest.recordValues\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.FormListItem\nimport org.odk.collect.forms.FormSource\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.formstest.FormUtils\nimport org.odk.collect.formstest.FormUtils.createXFormFile\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.locks.BooleanChangeLock\nimport org.odk.collect.shared.strings.Md5.getMd5Hash\n\n@RunWith(AndroidJUnit4::class)\nclass FormsDataServiceTest {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    private val application = ApplicationProvider.getApplicationContext<Application>()\n    private val component = DaggerUtils.getComponent(application)\n\n    private val formsRepositoryProvider = component.formsRepositoryProvider()\n    private val storagePathProvider = component.storagePathProvider()\n    private val settingsProvider = component.settingsProvider()\n    private val notifier = mock<Notifier>()\n    private val analytics = mock<Analytics>()\n\n    private val changeLockProvider = ChangeLockProvider { BooleanChangeLock() }\n\n    private val formSource = mock<FormSource> {\n        on { fetchFormList() } doReturn emptyList()\n    }\n\n    private lateinit var formsDataService: FormsDataService\n\n    private lateinit var project: Project.Saved\n\n    @Before\n    fun setup() {\n        project = setupProject()\n\n        val projectDependencyModule = ProjectDependencyModule(\n            project.uuid,\n            { settingsProvider.getUnprotectedSettings(project.uuid) },\n            formsRepositoryProvider,\n            mock(),\n            storagePathProvider,\n            changeLockProvider,\n            { formSource },\n            mock(),\n            mock(),\n            mock(),\n            mock()\n        )\n\n        val projectDependencyModuleFactory = mock<ProjectDependencyModuleFactory>()\n        whenever(projectDependencyModuleFactory.create(project.uuid)).thenReturn(projectDependencyModule)\n\n        formsDataService = FormsDataService(\n            appState = AppState(),\n            notifier = notifier,\n            projectDependencyModuleFactory = projectDependencyModuleFactory\n        ) { 0 }\n    }\n\n    @Test\n    fun getServerError_isNullAtFirst() {\n        assertThat(formsDataService.getServerError(project.uuid).value, equalTo(null))\n    }\n\n    @Test\n    fun `downloadUpdates() downloads updates when auto download is enabled`() {\n        addFormLocally(project, \"formId\", \"1\")\n\n        val updatedXForm = FormUtils.createXFormBody(\"formId\", \"2\")\n        addFormToServer(updatedXForm, \"formId\", \"2\")\n\n        settingsProvider.getUnprotectedSettings(project.uuid)\n            .save(ProjectKeys.KEY_AUTOMATIC_UPDATE, true)\n\n        formsDataService.downloadUpdates(project.uuid)\n        assertThat(\n            formsRepositoryProvider.create(project.uuid).getAllByFormIdAndVersion(\"formId\", \"2\").size,\n            equalTo(1)\n        )\n    }\n\n    @Test\n    fun `downloadUpdates() does nothing when change lock is locked`() {\n        val isSyncing = formsDataService.isSyncing(project.uuid)\n\n        val changeLock = changeLockProvider.create(project.uuid).formsLock as BooleanChangeLock\n        changeLock.lock(\"blah\")\n\n        isSyncing.recordValues { projectValues ->\n            formsDataService.downloadUpdates(project.uuid)\n            verifyNoInteractions(formSource)\n            verifyNoInteractions(notifier)\n            verifyNoInteractions(analytics)\n\n            assertThat(projectValues, equalTo(listOf(false)))\n        }\n    }\n\n    @Test\n    fun `matchFormsWithServer() does nothing when change lock is locked`() {\n        val isSyncing = formsDataService.isSyncing(project.uuid)\n\n        val changeLock = changeLockProvider.create(project.uuid).formsLock as BooleanChangeLock\n        changeLock.lock(\"blah\")\n\n        isSyncing.recordValues { projectValues ->\n            formsDataService.matchFormsWithServer(project.uuid)\n            verifyNoInteractions(formSource)\n            verifyNoInteractions(notifier)\n            verifyNoInteractions(analytics)\n\n            assertThat(projectValues, equalTo(listOf(false)))\n        }\n    }\n\n    /**\n     * We don't count calls where we can't acquire the lock as a \"success\". The front end should\n     * protect against this actually coming up by not letting the user sync while a sync is running.\n     */\n    @Test\n    fun `matchFormsWithServer() returns false when change lock is locked`() {\n        val changeLock = changeLockProvider.create(project.uuid).formsLock as BooleanChangeLock\n        changeLock.lock(\"blah\")\n\n        assertThat(formsDataService.matchFormsWithServer(project.uuid), equalTo(false))\n    }\n\n    @Test\n    fun `matchFormsWithServer() returns false when there is an error communicating with the server`() {\n        whenever(formSource.fetchFormList()).thenThrow(FormSourceException.FetchError())\n        assertThat(formsDataService.matchFormsWithServer(project.uuid), equalTo(false))\n    }\n\n    @Test\n    fun `matchFormsWithServer() updates project sync state`() {\n        val projectState = formsDataService.isSyncing(project.uuid)\n        val otherProjectState = formsDataService.isSyncing(\"other\")\n\n        projectState.recordValues { projectValues ->\n            otherProjectState.recordValues { otherProjectValues ->\n                formsDataService.matchFormsWithServer(project.uuid)\n\n                assertThat(projectValues, equalTo(listOf(false, true, false)))\n                assertThat(otherProjectValues, equalTo(listOf(false)))\n            }\n        }\n    }\n\n    @Test\n    fun `matchFormsWithServer() when there is an error updates project error state`() {\n        val error = FormSourceException.FetchError()\n        whenever(formSource.fetchFormList()).thenThrow(error)\n        formsDataService.matchFormsWithServer(project.uuid)\n\n        assertThat(formsDataService.getServerError(project.uuid).getOrAwaitValue(), equalTo(error))\n        assertThat(formsDataService.getServerError(\"other\").getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun `update() called after matchFormsWithServer() does not clear error state`() {\n        val error = FormSourceException.FetchError()\n        whenever(formSource.fetchFormList()).thenThrow(error)\n        formsDataService.matchFormsWithServer(project.uuid)\n\n        assertThat(formsDataService.getServerError(project.uuid).getOrAwaitValue(), equalTo(error))\n        formsDataService.refresh(project.uuid)\n        assertThat(formsDataService.getServerError(project.uuid).getOrAwaitValue(), equalTo(error))\n    }\n\n    @Test\n    fun `matchFormsWithServer() notifies on error when called with default notify value`() {\n        val error = FormSourceException.FetchError()\n        whenever(formSource.fetchFormList()).thenThrow(error)\n        formsDataService.matchFormsWithServer(project.uuid) { false }\n        verify(notifier).onSync(error, project.uuid)\n        verifyNoMoreInteractions(notifier)\n    }\n\n    @Test\n    fun `matchFormsWithServer() notifies on success when called with default notify value`() {\n        formsDataService.matchFormsWithServer(project.uuid) { false }\n        verify(notifier).onSync(null, project.uuid)\n        verifyNoMoreInteractions(notifier)\n    }\n\n    @Test\n    fun `matchFormsWithServer() notifies on error when called with notify true`() {\n        val error = FormSourceException.FetchError()\n        whenever(formSource.fetchFormList()).thenThrow(error)\n        formsDataService.matchFormsWithServer(project.uuid, true) { false }\n        verify(notifier).onSync(error, project.uuid)\n        verifyNoMoreInteractions(notifier)\n    }\n\n    @Test\n    fun `matchFormsWithServer() notifies on success when called with notify true`() {\n        formsDataService.matchFormsWithServer(project.uuid, true) { false }\n        verify(notifier).onSync(null, project.uuid)\n        verifyNoMoreInteractions(notifier)\n    }\n\n    @Test\n    fun `matchFormsWithServer() does not notify on error when called with default isStopped value`() {\n        val error = FormSourceException.FetchError()\n        whenever(formSource.fetchFormList()).thenThrow(error)\n        formsDataService.matchFormsWithServer(project.uuid, notify = false)\n        verifyNoInteractions(notifier)\n    }\n\n    @Test\n    fun `matchFormsWithServer() does not notify on success when called with default isStopped value`() {\n        formsDataService.matchFormsWithServer(project.uuid, notify = false)\n        verifyNoInteractions(notifier)\n    }\n\n    @Test\n    fun `matchFormsWithServer() does not notify on error when called with isStopped false`() {\n        val error = FormSourceException.FetchError()\n        whenever(formSource.fetchFormList()).thenThrow(error)\n        formsDataService.matchFormsWithServer(project.uuid, false) { false }\n        verifyNoInteractions(notifier)\n    }\n\n    @Test\n    fun `matchFormsWithServer() notifies on error when called with isStopped true`() {\n        val error = FormSourceException.FetchError()\n        whenever(formSource.fetchFormList()).thenThrow(error)\n        formsDataService.matchFormsWithServer(project.uuid, false) { true }\n        verify(notifier).onSyncStopped(project.uuid)\n        verifyNoMoreInteractions(notifier)\n    }\n\n    @Test\n    fun `clear() clears error state`() {\n        val error = FormSourceException.FetchError()\n        whenever(formSource.fetchFormList()).thenThrow(error)\n        formsDataService.matchFormsWithServer(project.uuid)\n\n        formsDataService.clear(project.uuid)\n        assertThat(formsDataService.getServerError(project.uuid).getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun `update() does nothing when change lock is locked`() {\n        val isSyncing = formsDataService.isSyncing(project.uuid)\n\n        val changeLock = changeLockProvider.create(project.uuid).formsLock as BooleanChangeLock\n        changeLock.lock(\"blah\")\n\n        isSyncing.recordValues { projectValues ->\n            formsDataService.refresh(project.uuid)\n            assertThat(projectValues, equalTo(listOf(false)))\n        }\n    }\n\n    @Test\n    fun `#getFormsCount ignores soft-deleted forms`() {\n        formsRepositoryProvider.create(project.uuid).apply {\n            save(\n                Form.Builder()\n                    .displayName(\"1\")\n                    .formId(\"1\")\n                    .version(\"1\")\n                    .deleted(false)\n                    .formFilePath(createXFormFile(\"1\", \"1\").absolutePath)\n                    .build()\n            )\n            save(\n                Form.Builder()\n                    .displayName(\"2\")\n                    .formId(\"2\")\n                    .version(\"1\")\n                    .deleted(true)\n                    .formFilePath(createXFormFile(\"2\", \"1\").absolutePath)\n                    .build()\n            )\n        }\n        formsDataService.update(project.uuid)\n        assertThat(formsDataService.getFormsCount(project.uuid).value, equalTo(1))\n    }\n\n    private fun addFormToServer(updatedXForm: String, formId: String, formVersion: String) {\n        whenever(formSource.fetchFormList()).doReturn(\n            listOf(\n                FormListItem(\n                    \"http://$formId\",\n                    formId,\n                    formVersion,\n                    updatedXForm.getMd5Hash(),\n                    \"blah\",\n                    null\n                )\n            )\n        )\n        whenever(formSource.fetchForm(\"http://$formId\")).doAnswer { updatedXForm.byteInputStream() }\n    }\n\n    private fun addFormLocally(project: Project.Saved, formId: String, formVersion: String) {\n        val formsDir = storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, project.uuid)\n        val formsRepository = formsRepositoryProvider.create(project.uuid)\n        formsRepository.save(\n            FormUtils.buildForm(formId, formVersion, formsDir).build()\n        )\n    }\n\n    private fun setupProject(): Project.Saved {\n        val projectsRepository = component.projectsRepository()\n        return projectsRepository.save(Project.New(\"blah\", \"B\", \"#ffffff\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/LocalFormUseCasesTest.java",
    "content": "package org.odk.collect.android.formmanagement;\n\nimport org.junit.Test;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.formstest.FormUtils;\nimport org.odk.collect.formstest.InMemFormsRepository;\nimport org.odk.collect.formstest.InMemInstancesRepository;\nimport org.odk.collect.shared.TempFiles;\n\nimport java.util.List;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.odk.collect.formstest.InstanceUtils.buildInstance;\n\npublic class LocalFormUseCasesTest {\n\n    private final InMemFormsRepository formsRepository = new InMemFormsRepository();\n    private final InMemInstancesRepository instancesRepository = new InMemInstancesRepository();\n\n    @Test\n    public void whenFormHasDeletedInstances_deletesForm() {\n        Form formToDelete = formsRepository.save(new Form.Builder()\n                .formId(\"id\")\n                .version(\"version\")\n                .formFilePath(FormUtils.createXFormFile(\"id\", \"version\").getAbsolutePath())\n                .build());\n\n        instancesRepository.save(new Instance.Builder()\n                .formId(\"id\")\n                .formVersion(\"version\")\n                .deletedDate(0L)\n                .build());\n\n        LocalFormUseCases.deleteForm(formsRepository, instancesRepository, formToDelete.getDbId());\n        assertThat(formsRepository.getAll().size(), is(0));\n    }\n\n    @Test\n    public void whenOtherVersionOfFormHasInstances_deletesForm() {\n        formsRepository.save(new Form.Builder()\n                .formId(\"1\")\n                .version(\"old\")\n                .formFilePath(FormUtils.createXFormFile(\"1\", \"old\").getAbsolutePath())\n                .build());\n\n        Form formToDelete = formsRepository.save(new Form.Builder()\n                .formId(\"1\")\n                .version(\"new\")\n                .formFilePath(FormUtils.createXFormFile(\"1\", \"new\").getAbsolutePath())\n                .build());\n\n        instancesRepository.save(new Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"old\")\n                .build());\n\n        LocalFormUseCases.deleteForm(formsRepository, instancesRepository, formToDelete.getDbId());\n        List<Form> forms = formsRepository.getAll();\n        assertThat(forms.size(), is(1));\n        assertThat(forms.get(0).getVersion(), is(\"old\"));\n    }\n\n    @Test\n    public void whenFormHasNullVersion_butAnotherVersionHasInstances_deletesForm() {\n        formsRepository.save(new Form.Builder()\n                .formId(\"1\")\n                .version(\"version\")\n                .formFilePath(FormUtils.createXFormFile(\"1\", \"version\").getAbsolutePath())\n                .build());\n\n        Form formToDelete = formsRepository.save(new Form.Builder()\n                .formId(\"1\")\n                .version(null)\n                .formFilePath(FormUtils.createXFormFile(\"1\", null).getAbsolutePath())\n                .build());\n\n        instancesRepository.save(new Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"version\")\n                .build());\n\n        LocalFormUseCases.deleteForm(formsRepository, instancesRepository, formToDelete.getDbId());\n        List<Form> forms = formsRepository.getAll();\n        assertThat(forms.size(), is(1));\n        assertThat(forms.get(0).getVersion(), is(\"version\"));\n    }\n\n    @Test\n    public void whenFormHasNullVersion_andInstancesWithNullVersion_softDeletesForm() {\n        Form formToDelete = formsRepository.save(new Form.Builder()\n                .formId(\"1\")\n                .version(null)\n                .formFilePath(FormUtils.createXFormFile(\"1\", null).getAbsolutePath())\n                .build());\n\n        instancesRepository.save(buildInstance(\"1\", null, TempFiles.createTempDir().getAbsolutePath()).build());\n\n        LocalFormUseCases.deleteForm(formsRepository, instancesRepository, formToDelete.getDbId());\n        List<Form> forms = formsRepository.getAll();\n        assertThat(forms.size(), is(1));\n        assertThat(forms.get(0).isDeleted(), is(true));\n    }\n\n    @Test\n    public void whenFormIdAndVersionCombinationIsNotUnique_andInstanceExists_hardDeletesForm() {\n        Form formToDelete = formsRepository.save(new Form.Builder()\n                .formId(\"id\")\n                .version(\"version\")\n                .formFilePath(FormUtils.createXFormFile(\"id\", \"version\", \"Form1\").getAbsolutePath())\n                .build());\n\n        instancesRepository.save(new Instance.Builder()\n                .formId(\"id\")\n                .formVersion(\"version\")\n                .build());\n\n        formsRepository.save(new Form.Builder()\n                .formId(\"id\")\n                .version(\"version\")\n                .formFilePath(FormUtils.createXFormFile(\"id\", \"version\", \"Form2\").getAbsolutePath())\n                .build());\n\n        LocalFormUseCases.deleteForm(formsRepository, instancesRepository, formToDelete.getDbId());\n        List<Form> forms = formsRepository.getAll();\n        assertThat(forms.size(), is(1));\n        assertThat(forms.get(0).getDbId(), is(2L));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsSynchronizerTest.java",
    "content": "package org.odk.collect.android.formmanagement;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.containsInAnyOrder;\nimport static org.hamcrest.Matchers.empty;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static java.util.Arrays.asList;\n\nimport org.junit.Test;\nimport org.odk.collect.android.formmanagement.download.FormDownloadException;\nimport org.odk.collect.android.formmanagement.download.FormDownloader;\nimport org.odk.collect.android.formmanagement.matchexactly.ServerFormsSynchronizer;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.FormSource;\nimport org.odk.collect.forms.FormSourceException;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.formstest.FormUtils;\nimport org.odk.collect.formstest.InMemFormsRepository;\nimport org.odk.collect.formstest.InMemInstancesRepository;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Supplier;\n\npublic class ServerFormsSynchronizerTest {\n\n    private final FormsRepository formsRepository = new InMemFormsRepository();\n    private final InstancesRepository instancesRepository = new InMemInstancesRepository();\n    private final RecordingFormDownloader formDownloader = new RecordingFormDownloader();\n    private final ServerFormsSynchronizer.ServerFormsDetailsFetcher serverFormDetailsFetcher = mock(ServerFormsSynchronizer.ServerFormsDetailsFetcher.class);\n    private final FormSource formSource = mock(FormSource.class);\n    private final ServerFormsSynchronizer synchronizer = new ServerFormsSynchronizer(serverFormDetailsFetcher, formsRepository, instancesRepository, formDownloader, formSource);\n\n    @Test\n    public void downloadsNewForms() throws Exception {\n        when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenReturn(asList(\n                new ServerFormDetails(\"form-1\", \"http://example.com/form-1\", \"form-1\", \"server\", \"form-1-hash\", true, false, null)\n        ));\n\n        synchronizer.synchronize();\n        assertThat(formDownloader.getDownloadedForms(), containsInAnyOrder(\"form-1\"));\n    }\n\n    @Test\n    public void downloadsUpdatedForms() throws Exception {\n        when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenReturn(asList(\n                new ServerFormDetails(\"form-1\", \"http://example.com/form-1\", \"form-1\", \"server\", \"form-1-hash\", false, true, null)\n        ));\n\n        synchronizer.synchronize();\n        assertThat(formDownloader.getDownloadedForms(), containsInAnyOrder(\"form-1\"));\n    }\n\n    @Test\n    public void deletesFormsNotInList() throws Exception {\n        formsRepository.save(new Form.Builder()\n                .dbId(3L)\n                .formId(\"form-3\")\n                .md5Hash(\"form-3-hash\")\n                .formFilePath(FormUtils.createXFormFile(\"1\", \"1\").getAbsolutePath())\n                .build());\n\n        when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenReturn(asList(\n                new ServerFormDetails(\"form-1\", \"http://example.com/form-1\", \"form-1\", \"server\", \"form-1-hash\", false, false, null)\n        ));\n\n        synchronizer.synchronize();\n        assertThat(formsRepository.getAll().isEmpty(), is(true));\n    }\n\n    @Test\n    public void doesNotDownloadExistingForms() throws Exception {\n        when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenReturn(asList(\n                new ServerFormDetails(\"form-1\", \"http://example.com/form-1\", \"form-1\", \"server\", \"form-1-hash\", false, false, null)\n        ));\n\n        synchronizer.synchronize();\n        assertThat(formDownloader.getDownloadedForms(), is(empty()));\n    }\n\n    @Test\n    public void whenFetchingFormDetailsThrowsAnError_throwsError() throws Exception {\n        FormSourceException exception = new FormSourceException.AuthRequired();\n        when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenThrow(exception);\n\n        try {\n            synchronizer.synchronize();\n        } catch (FormSourceException e) {\n            assertThat(e, is(exception));\n        }\n    }\n\n    @Test\n    public void whenDownloadingFormThrowsAnError_throwsErrorAndDownloadsOtherForms() throws Exception {\n        List<ServerFormDetails> serverForms = asList(\n                new ServerFormDetails(\"form-1\", \"http://example.com/form-1\", \"form-1\", \"server\", \"form-1-hash\", true, false, null),\n                new ServerFormDetails(\"form-2\", \"http://example.com/form-2\", \"form-2\", \"server\", \"form-2-hash\", true, false, null)\n        );\n\n        when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenReturn(serverForms);\n\n        FormDownloader formDownloader = mock(FormDownloader.class);\n        doThrow(new FormDownloadException.FormSourceError(new FormSourceException.FetchError())).when(formDownloader).downloadForm(serverForms.get(0), null, null);\n\n        ServerFormsSynchronizer synchronizer = new ServerFormsSynchronizer(serverFormDetailsFetcher, formsRepository, instancesRepository, formDownloader, formSource);\n\n        try {\n            synchronizer.synchronize();\n        } catch (FormSourceException.FetchError e) {\n            verify(formDownloader).downloadForm(serverForms.get(1), null, null);\n        }\n    }\n\n    private static class RecordingFormDownloader implements FormDownloader {\n\n        private final List<String> formsDownloaded = new ArrayList<>();\n\n        @Override\n        public void downloadForm(ServerFormDetails form, ProgressReporter progressReporter, Supplier<Boolean> isCancelled) {\n            formsDownloaded.add(form.getFormId());\n        }\n\n        public List<String> getDownloadedForms() {\n            return formsDownloaded;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/ShouldAddFormFileTest.java",
    "content": "package org.odk.collect.android.formmanagement;\n\nimport org.junit.Assert;\nimport org.junit.Test;\n\npublic class ShouldAddFormFileTest {\n\n    @Test\n    public void rejectIgnoredFiles() {\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\".ignored\"));\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\".ignored.xml\"));\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\".ignored.xhtml\"));\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\".xml\"));\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\".xhtml\"));\n    }\n\n    @Test\n    public void rejectNonFormFileTypes() {\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\"form\"));\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\"form.\"));\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\"form.html\"));\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\"form.js\"));\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\"form.xml.foo\"));\n        Assert.assertFalse(LocalFormUseCases.shouldAddFormFile(\"form.xml1\"));\n    }\n\n    @Test\n    public void acceptXmlForm() {\n        Assert.assertTrue(LocalFormUseCases.shouldAddFormFile(\"form.xml\"));\n    }\n\n    @Test\n    public void acceptXhtmlForm() {\n        Assert.assertTrue(LocalFormUseCases.shouldAddFormFile(\"form.xhtml\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/download/FormDownloadExceptionMapperTest.kt",
    "content": "package org.odk.collect.android.formmanagement.download\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\n@RunWith(AndroidJUnit4::class)\nclass FormDownloadExceptionMapperTest {\n    private lateinit var context: Context\n    private lateinit var mapper: FormDownloadExceptionMapper\n\n    @Before\n    fun setup() {\n        context = ApplicationProvider.getApplicationContext()\n        mapper = FormDownloadExceptionMapper(context)\n    }\n\n    @Test\n    fun formWithNoHashError_returnsFormWithNoHashErrorMessage() {\n        val expectedString = context.getString(\n            org.odk.collect.strings.R.string.form_with_no_hash_error\n        ) + \" \" + context.getString(org.odk.collect.strings.R.string.report_to_project_lead)\n        assertThat(\n            mapper.getMessage(FormDownloadException.FormWithNoHash()),\n            `is`(expectedString)\n        )\n    }\n\n    @Test\n    fun formParsingError_returnsFormParsingErrorMessage() {\n        val original = Exception(\"original message\")\n        original.stackTrace = arrayOf(\n            StackTraceElement(\"Class\", \"method\", \"File\", 1),\n            StackTraceElement(\"Class\", \"method\", \"File\", 2)\n        )\n\n        assertThat(\n            mapper.getMessage(FormDownloadException.FormParsingError(original)),\n            equalTo(\n                context.getString(org.odk.collect.strings.R.string.form_parsing_error) + \"\\n\\n\" +\n                    original.message + \"\\n\" +\n                    \"Class.method(File:1)\" + \"\\n\\n\" +\n                    context.getString(org.odk.collect.strings.R.string.report_to_project_lead)\n            )\n        )\n    }\n\n    @Test\n    fun formSaveError_returnsFormSaveErrorMessage() {\n        val expectedString = context.getString(\n            org.odk.collect.strings.R.string.form_save_disk_error\n        ) + \" \" + context.getString(org.odk.collect.strings.R.string.report_to_project_lead)\n        assertThat(\n            mapper.getMessage(FormDownloadException.DiskError()),\n            `is`(expectedString)\n        )\n    }\n\n    @Test\n    fun formWithInvalidSubmissionError_returnsFormInvalidSubmissionErrorMessage() {\n        val expectedString = context.getString(\n            org.odk.collect.strings.R.string.form_with_invalid_submission_error\n        ) + \" \" + context.getString(org.odk.collect.strings.R.string.report_to_project_lead)\n        assertThat(\n            mapper.getMessage(FormDownloadException.InvalidSubmission()),\n            `is`(expectedString)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/download/ServerFormDownloaderTest.java",
    "content": "package org.odk.collect.android.formmanagement.download;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.contains;\nimport static org.hamcrest.Matchers.empty;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.fail;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.utilities.FileUtils.read;\nimport static org.odk.collect.androidshared.utils.PathUtils.getAbsoluteFilePath;\nimport static org.odk.collect.formstest.FormUtils.buildForm;\nimport static org.odk.collect.formstest.FormUtils.createXFormBody;\nimport static java.util.Arrays.asList;\n\nimport com.google.common.io.Files;\n\nimport org.junit.Test;\nimport org.odk.collect.android.formmanagement.metadata.FormMetadataParser;\nimport org.odk.collect.android.formmanagement.ServerFormDetails;\nimport org.odk.collect.entities.server.EntitySource;\nimport org.odk.collect.entities.storage.EntitiesRepository;\nimport org.odk.collect.entities.storage.InMemEntitiesRepository;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.FormListItem;\nimport org.odk.collect.forms.FormSource;\nimport org.odk.collect.forms.FormSourceException;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.forms.ManifestFile;\nimport org.odk.collect.forms.MediaFile;\nimport org.odk.collect.formstest.FormUtils;\nimport org.odk.collect.formstest.InMemFormsRepository;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.function.Supplier;\n\npublic class ServerFormDownloaderTest {\n\n    private final FakeClock clock = new FakeClock(123L);\n    private final FormsRepository formsRepository = new InMemFormsRepository(clock::get);\n    private final File cacheDir = Files.createTempDir();\n    private final File formsDir = Files.createTempDir();\n\n    private final EntitiesRepository entitiesRepository = new InMemEntitiesRepository(clock::get);\n    private final EntitySource entitySource = mock();\n\n    @Test\n    public void downloadsAndSavesForm() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                null);\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        List<Form> allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n        Form form = allForms.get(0);\n        assertThat(form.getFormId(), is(\"id\"));\n\n        File formFile = new File(getAbsoluteFilePath(formsDir.getAbsolutePath(), form.getFormFilePath()));\n        assertThat(formFile.exists(), is(true));\n        assertThat(new String(read(formFile)), is(xform));\n    }\n\n    @Test\n    public void whenFormToDownloadIsUpdate_savesNewVersionAlongsideOldVersion() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                null);\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        String xformUpdate = createXFormBody(\"id\", \"updated\");\n        ServerFormDetails serverFormDetailsUpdated = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUpdatedUrl\",\n                \"id\",\n                \"updated\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xformUpdate.getBytes())),\n                true,\n                false,\n                null);\n\n        when(formSource.fetchForm(\"http://downloadUpdatedUrl\")).thenReturn(new ByteArrayInputStream(xformUpdate.getBytes()));\n        downloader.downloadForm(serverFormDetailsUpdated, null, null);\n\n        List<Form> allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(2));\n        allForms.forEach(f -> {\n            File formFile = new File(getAbsoluteFilePath(formsDir.getAbsolutePath(), f.getFormFilePath()));\n            assertThat(formFile.exists(), is(true));\n        });\n    }\n\n    @Test\n    public void whenFormToDownloadIsUpdate_withSameFormIdAndVersion_replacePreExistingForm() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", Collections.singletonList(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        List<Form> formsBeforeUpdate = formsRepository.getAllByFormIdAndVersion(\"id\", \"version\");\n\n        String xformUpdate = FormUtils.createXFormBody(\"id\", \"version\", \"A different title\");\n        ServerFormDetails serverFormDetailsUpdated = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xformUpdate.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", Collections.singletonList(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\")\n                )));\n\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xformUpdate.getBytes()));\n        downloader.downloadForm(serverFormDetailsUpdated, null, null);\n\n        List<Form> formsAfterUpdate = formsRepository.getAllByFormIdAndVersion(\"id\", \"version\");\n        assertThat(formsAfterUpdate.size(), is(1));\n\n        // Pre-existing forms should be deleted along with its files\n        File formFile = new File(getAbsoluteFilePath(formsDir.getAbsolutePath(), formsBeforeUpdate.get(0).getFormFilePath()));\n        assertThat(formFile.exists(), is(false));\n        File mediaFile = new File(getAbsoluteFilePath(formsDir.getAbsolutePath(), formsBeforeUpdate.get(0).getFormMediaPath()) + File.separator + \"file1\");\n        assertThat(mediaFile.exists(), is(false));\n\n        // New forms should be added\n        formFile = new File(getAbsoluteFilePath(formsDir.getAbsolutePath(), formsAfterUpdate.get(0).getFormFilePath()));\n        assertThat(formFile.exists(), is(true));\n        mediaFile = new File(getAbsoluteFilePath(formsDir.getAbsolutePath(), formsAfterUpdate.get(0).getFormMediaPath()) + File.separator + \"file1\");\n        assertThat(mediaFile.exists(), is(true));\n    }\n\n    @Test\n    public void whenFormListMissingHash_throwsError() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                null,\n                true,\n                false,\n                null);\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        try {\n            downloader.downloadForm(serverFormDetails, null, null);\n            fail(\"Expected exception because of missing form hash\");\n        } catch (FormDownloadException.FormWithNoHash e) {\n            // pass\n        }\n    }\n\n    @Test\n    public void whenFormHasMediaFiles_downloadsAndSavesFormAndMediaFiles() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", asList(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\"),\n                        new MediaFile(\"file2\", \"hash-2\", \"http://file2\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n        when(formSource.fetchMediaFile(\"http://file2\")).thenReturn(new ByteArrayInputStream(\"contents2\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        List<Form> allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n        Form form = allForms.get(0);\n        assertThat(form.getFormId(), is(\"id\"));\n\n        File formFile = new File(getAbsoluteFilePath(formsDir.getAbsolutePath(), form.getFormFilePath()));\n        assertThat(formFile.exists(), is(true));\n        assertThat(new String(read(formFile)), is(xform));\n\n        File mediaFile1 = new File(form.getFormMediaPath() + \"/file1\");\n        assertThat(mediaFile1.exists(), is(true));\n        assertThat(new String(read(mediaFile1)), is(\"contents1\"));\n\n        File mediaFile2 = new File(form.getFormMediaPath() + \"/file2\");\n        assertThat(mediaFile2.exists(), is(true));\n        assertThat(new String(read(mediaFile2)), is(\"contents2\"));\n    }\n\n    @Test\n    public void whenFormHasMediaFiles_andIsFormToDownloadIsUpdate_doesNotRedownloadMediaFiles() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", asList(\n                        new MediaFile(\"file1\", Md5.getMd5Hash(\"contents1\"), \"http://file1\"),\n                        new MediaFile(\"file2\", Md5.getMd5Hash(\"contents2\"), \"http://file2\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n        when(formSource.fetchMediaFile(\"http://file2\")).thenReturn(new ByteArrayInputStream(\"contents2\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        String xformUpdate = createXFormBody(\"id\", \"updated\");\n        ServerFormDetails serverFormDetailsUpdated = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUpdatedUrl\",\n                \"id\",\n                \"updated\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xformUpdate.getBytes())),\n                false,\n                true,\n                new ManifestFile(\"\", asList(\n                        new MediaFile(\"file1\", Md5.getMd5Hash(\"contents1\"), \"http://file1\"),\n                        new MediaFile(\"file2\", Md5.getMd5Hash(\"contents2\"), \"http://file2\")\n                )));\n\n        when(formSource.fetchForm(\"http://downloadUpdatedUrl\")).thenReturn(new ByteArrayInputStream(xformUpdate.getBytes()));\n        downloader.downloadForm(serverFormDetailsUpdated, null, null);\n\n        verify(formSource, times(1)).fetchMediaFile(\"http://file1\");\n        verify(formSource, times(1)).fetchMediaFile(\"http://file2\");\n    }\n\n    @Test\n    public void whenFormHasMediaFiles_andIsFormToDownloadIsUpdate_downloadsFilesWithChangedHash() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", asList(\n                        new MediaFile(\"file1\", Md5.getMd5Hash(\"contents1\"), \"http://file1\"),\n                        new MediaFile(\"file2\", Md5.getMd5Hash(\"contents2\"), \"http://file2\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n        when(formSource.fetchMediaFile(\"http://file2\")).thenReturn(new ByteArrayInputStream(\"contents2\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        String xformUpdate = createXFormBody(\"id\", \"updated\");\n        ServerFormDetails serverFormDetailsUpdated = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUpdatedUrl\",\n                \"id\",\n                \"updated\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xformUpdate.getBytes())),\n                false,\n                true,\n                new ManifestFile(\"\", asList(\n                        new MediaFile(\"file1\", Md5.getMd5Hash(\"contents1\"), \"http://file1\"),\n                        new MediaFile(\"file2\", Md5.getMd5Hash(\"contents3\"), \"http://file2\")\n                )));\n\n        when(formSource.fetchMediaFile(\"http://file2\")).thenReturn(new ByteArrayInputStream(\"contents3\".getBytes()));\n        when(formSource.fetchForm(\"http://downloadUpdatedUrl\")).thenReturn(new ByteArrayInputStream(xformUpdate.getBytes()));\n        downloader.downloadForm(serverFormDetailsUpdated, null, null);\n\n        Form form = formsRepository.getAllByFormIdAndVersion(\"id\", \"updated\").get(0);\n        File mediaFile2 = new File(form.getFormMediaPath() + \"/file2\");\n        assertThat(mediaFile2.exists(), is(true));\n        assertThat(new String(read(mediaFile2)), is(\"contents3\"));\n    }\n\n    @Test\n    public void whenFormHasMediaFiles_andFetchingMediaFileFails_throwsFetchErrorAndDoesNotSaveAnything() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenThrow(new FormSourceException.FetchError());\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n\n        try {\n            downloader.downloadForm(serverFormDetails, null, null);\n            fail(\"Expected exception\");\n        } catch (FormDownloadException.FormSourceError e) {\n            assertThat(formsRepository.getAll(), is(empty()));\n            assertThat(asList(new File(getCacheFilesPath()).listFiles()), is(empty()));\n            assertThat(asList(new File(getFormFilesPath()).listFiles()), is(empty()));\n        }\n    }\n\n    @Test\n    public void whenFormHasMediaFiles_andFileExistsInMediaDirPath_throwsDiskExceptionAndDoesNotSaveAnything() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n\n        // Create file where media dir would go\n        assertThat(new File(formsDir, \"Form-media\").createNewFile(), is(true));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n\n        try {\n            downloader.downloadForm(serverFormDetails, null, null);\n            fail(\"Expected exception\");\n        } catch (FormDownloadException.DiskError e) {\n            assertThat(formsRepository.getAll(), is(empty()));\n            assertThat(asList(new File(getCacheFilesPath()).listFiles()), is(empty()));\n            assertThat(asList(new File(getFormFilesPath()).listFiles()), is(empty()));\n        }\n    }\n\n    @Test\n    public void beforeDownloadingEachMediaFile_reportsProgress() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", asList(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\"),\n                        new MediaFile(\"file2\", \"hash-2\", \"http://file2\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents\".getBytes()));\n        when(formSource.fetchMediaFile(\"http://file2\")).thenReturn(new ByteArrayInputStream(\"contents\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        RecordingProgressReporter progressReporter = new RecordingProgressReporter();\n        downloader.downloadForm(serverFormDetails, progressReporter, null);\n\n        assertThat(progressReporter.reports, contains(1, 2));\n    }\n\n    //region Undelete on re-download\n    @Test\n    public void whenFormIsSoftDeleted_unDeletesForm() throws Exception {\n        String xform = createXFormBody(\"deleted-form\", \"version\");\n        Form form = buildForm(\"deleted-form\", \"version\", getFormFilesPath(), xform)\n                .deleted(true)\n                .build();\n        formsRepository.save(form);\n\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                form.getDisplayName(),\n                \"http://downloadUrl\",\n                form.getFormId(),\n                form.getVersion(),\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                null);\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n        assertThat(formsRepository.get(1L).isDeleted(), is(false));\n    }\n\n    @Test\n    public void whenMultipleFormsWithSameFormIdVersionDeleted_reDownloadUnDeletesFormWithSameHash() throws Exception {\n        String xform = FormUtils.createXFormBody(\"deleted-form\", \"version\", \"A title\");\n        Form form = buildForm(\"deleted-form\", \"version\", getFormFilesPath(), xform)\n                .deleted(true)\n                .build();\n        formsRepository.save(form);\n\n        String xform2 = FormUtils.createXFormBody(\"deleted-form\", \"version\", \"A different title\");\n        Form form2 = buildForm(\"deleted-form\", \"version\", getFormFilesPath(), xform2)\n                .deleted(true)\n                .build();\n        formsRepository.save(form2);\n\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                form2.getDisplayName(),\n                \"http://downloadUrl\",\n                form2.getFormId(),\n                form2.getVersion(),\n                Md5.getMd5Hash(new ByteArrayInputStream(xform2.getBytes())),\n                true,\n                false,\n                null);\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform2.getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n        assertThat(formsRepository.get(1L).isDeleted(), is(true));\n        assertThat(formsRepository.get(2L).isDeleted(), is(false));\n    }\n    //endregion\n\n    @Test\n    public void whenFormAlreadyDownloaded_formRemainsOnDevice() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                null);\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n\n        // Initial download\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        ServerFormDetails serverFormDetailsAlreadyOnDevice = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                false,\n                false,\n                null);\n\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        downloader.downloadForm(serverFormDetailsAlreadyOnDevice, null, null);\n\n        List<Form> allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n\n        Form form = allForms.get(0);\n        File formFile = new File(getAbsoluteFilePath(formsDir.getAbsolutePath(), form.getFormFilePath()));\n        assertThat(new String(read(formFile)), is(xform));\n    }\n\n    @Test\n    public void whenFormAlreadyDownloaded_andFormHasNewMediaFiles_updatesMediaFilesAndLastDetectedAttachmentsUpdateDateInForm() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", Md5.getMd5Hash(new ByteArrayInputStream(\"contents\".getBytes())), \"http://file1\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n\n        // Initial download\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        List<Form> allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n        Form form = allForms.get(0);\n        assertThat(form.getLastDetectedAttachmentsUpdateDate(), is(nullValue()));\n\n        ServerFormDetails serverFormDetailsUpdatedMediaFile = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                false,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", Md5.getMd5Hash(new ByteArrayInputStream(\"contents-updated\".getBytes())), \"http://file1\")\n                )));\n\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents-updated\".getBytes()));\n\n        // Second download\n        downloader.downloadForm(serverFormDetailsUpdatedMediaFile, null, null);\n\n        allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n        form = allForms.get(0);\n        assertThat(form.getFormId(), is(\"id\"));\n\n        File formFile = new File(getAbsoluteFilePath(formsDir.getAbsolutePath(), form.getFormFilePath()));\n        assertThat(formFile.exists(), is(true));\n        assertThat(new String(read(formFile)), is(xform));\n\n        File mediaFile1 = new File(form.getFormMediaPath() + \"/file1\");\n        assertThat(mediaFile1.exists(), is(true));\n        assertThat(new String(read(mediaFile1)), is(\"contents-updated\"));\n\n        assertThat(form.getLastDetectedAttachmentsUpdateDate(), is(123L));\n    }\n\n    @Test\n    public void whenFormAlreadyDownloaded_andFormHasNewMediaFiles_andMediaFetchFails_throwsFetchError() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", Md5.getMd5Hash(new ByteArrayInputStream(\"contents\".getBytes())), \"http://file1\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n\n        // Initial download\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        try {\n            ServerFormDetails serverFormDetailsUpdatedMediaFile = new ServerFormDetails(\n                    \"Form\",\n                    \"http://downloadUrl\",\n                    \"id\",\n                    \"version\",\n                    Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                    false,\n                    false,\n                    new ManifestFile(\"\", List.of(\n                            new MediaFile(\"file1\", Md5.getMd5Hash(new ByteArrayInputStream(\"contents-updated\".getBytes())), \"http://file1\")\n                    )));\n\n            when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n            when(formSource.fetchMediaFile(\"http://file1\")).thenThrow(new FormSourceException.FetchError());\n            downloader.downloadForm(serverFormDetailsUpdatedMediaFile, null, null);\n            fail(\"Expected exception\");\n        } catch (FormDownloadException.FormSourceError e) {\n            // Check form is still intact\n            List<Form> allForms = formsRepository.getAll();\n            assertThat(allForms.size(), is(1));\n            Form form = allForms.get(0);\n            assertThat(form.getFormId(), is(\"id\"));\n\n            File formFile = new File(getAbsoluteFilePath(formsDir.getAbsolutePath(), form.getFormFilePath()));\n            assertThat(formFile.exists(), is(true));\n            assertThat(new String(read(formFile)), is(xform));\n        }\n    }\n\n    @Test\n    public void afterDownloadingXForm_cancelling_throwsDownloadingInterruptedExceptionAndDoesNotSaveAnything() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                null);\n\n        CancelAfterFormDownloadFormSource formListApi = new CancelAfterFormDownloadFormSource(xform);\n        ServerFormDownloader downloader = new ServerFormDownloader(formListApi, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n\n        try {\n            downloader.downloadForm(serverFormDetails, null, formListApi);\n            fail(\"Expected exception\");\n        } catch (FormDownloadException.DownloadingInterrupted e) {\n            assertThat(formsRepository.getAll(), is(empty()));\n            assertThat(asList(new File(getCacheFilesPath()).listFiles()), is(empty()));\n            assertThat(asList(new File(getFormFilesPath()).listFiles()), is(empty()));\n        }\n    }\n\n    @Test\n    public void afterDownloadingMediaFile_cancelling_throwsDownloadingInterruptedExceptionAndDoesNotSaveAnything() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", asList(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\"),\n                        new MediaFile(\"file2\", \"hash-2\", \"http://file2\")\n                )));\n\n        CancelAfterMediaFileDownloadFormSource formListApi = new CancelAfterMediaFileDownloadFormSource(xform);\n        ServerFormDownloader downloader = new ServerFormDownloader(formListApi, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n\n        try {\n            downloader.downloadForm(serverFormDetails, null, formListApi);\n            fail(\"Excepted exception\");\n        } catch (FormDownloadException.DownloadingInterrupted e) {\n            assertThat(formsRepository.getAll(), is(empty()));\n            assertThat(asList(new File(getCacheFilesPath()).listFiles()), is(empty()));\n            assertThat(asList(new File(getFormFilesPath()).listFiles()), is(empty()));\n        }\n    }\n\n    @Test\n    public void whenFormHasEntityAttachmentsButNoEntityBlock_downloadsAndSavesFormAsAFormUsingEntities() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\", MediaFile.Type.ENTITY_LIST)\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        List<Form> allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n        Form form = allForms.get(0);\n        assertThat(form.usesEntities(), is(true));\n    }\n\n    @Test\n    public void whenFormHasEntityAttachmentsAndEntityBlock_downloadsAndSavesFormAsAFormUsingEntities() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\", \"Test Form\", \"2024.1.0\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\", MediaFile.Type.ENTITY_LIST)\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        List<Form> allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n        Form form = allForms.get(0);\n        assertThat(form.usesEntities(), is(true));\n    }\n\n    @Test\n    public void whenFormLacksEntityAttachmentsButHasEntityBlock_downloadsAndSavesFormAsAFormUsingEntities() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\", \"Test Form\", \"2024.1.0\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        List<Form> allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n        Form form = allForms.get(0);\n        assertThat(form.usesEntities(), is(true));\n    }\n\n    @Test\n    public void whenFormLacksEntityAttachmentsAndEntityBlock_downloadsAndSavesFormAsAFormNotUsingEntities() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        List<Form> allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n        Form form = allForms.get(0);\n        assertThat(form.usesEntities(), is(false));\n    }\n\n    @Test\n    public void whenFormAlreadyDownloadedDoesNotHaveEntities_andOneOfTheAttachmentsBecomesMarkedAsAnEntityList_downloadsAndMarkTheFormAsAFormThatUsesEntities() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", Md5.getMd5Hash(new ByteArrayInputStream(\"contents\".getBytes())), \"http://file1\")\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n\n        // Initial download\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        List<Form> allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n        Form form = allForms.get(0);\n        assertThat(form.usesEntities(), is(false));\n\n        ServerFormDetails serverFormDetailsUpdatedMediaFile = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                false,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", Md5.getMd5Hash(new ByteArrayInputStream(\"contents\".getBytes())), \"http://file1\", MediaFile.Type.ENTITY_LIST)\n                )));\n\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents\".getBytes()));\n\n        // Second download\n        downloader.downloadForm(serverFormDetailsUpdatedMediaFile, null, null);\n\n        allForms = formsRepository.getAll();\n        assertThat(allForms.size(), is(1));\n        form = allForms.get(0);\n        assertThat(form.usesEntities(), is(true));\n    }\n\n    @Test\n    public void whenAFormWithAnEntityListHasAlreadyBeenDownloading_redownloadingDoesNotUpdateLastDetectedAttachmentsUpdateDate() throws Exception {\n        String xform = createXFormBody(\"id\", \"version\", \"Test Form\", \"2024.1.0\");\n        ServerFormDetails serverFormDetails = new ServerFormDetails(\n                \"Form\",\n                \"http://downloadUrl\",\n                \"id\",\n                \"version\",\n                Md5.getMd5Hash(new ByteArrayInputStream(xform.getBytes())),\n                true,\n                false,\n                new ManifestFile(\"\", List.of(\n                        new MediaFile(\"file1\", \"hash-1\", \"http://file1\", MediaFile.Type.ENTITY_LIST)\n                )));\n\n        FormSource formSource = mock(FormSource.class);\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n\n        ServerFormDownloader downloader = new ServerFormDownloader(formSource, formsRepository, cacheDir, formsDir.getAbsolutePath(), FormMetadataParser.INSTANCE, clock::get, entitiesRepository, entitySource);\n\n        clock.set(1);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        when(formSource.fetchForm(\"http://downloadUrl\")).thenReturn(new ByteArrayInputStream(xform.getBytes()));\n        when(formSource.fetchMediaFile(\"http://file1\")).thenReturn(new ByteArrayInputStream(\"contents1\".getBytes()));\n\n        clock.set(2);\n        downloader.downloadForm(serverFormDetails, null, null);\n\n        assertThat(formsRepository.getAll().get(0).getLastDetectedAttachmentsUpdateDate(), equalTo(null));\n    }\n\n    private String getFormFilesPath() {\n        return formsDir.getAbsolutePath();\n    }\n\n    private String getCacheFilesPath() {\n        return cacheDir.getAbsolutePath();\n    }\n\n    public static class RecordingProgressReporter implements FormDownloader.ProgressReporter {\n\n        List<Integer> reports = new ArrayList<>();\n\n        @Override\n        public void onDownloadingMediaFile(int count) {\n            reports.add(count);\n        }\n    }\n\n    public static class CancelAfterFormDownloadFormSource implements FormSource, Supplier<Boolean> {\n\n        private final String xform;\n        private boolean isCancelled;\n\n        public CancelAfterFormDownloadFormSource(String xform) {\n            this.xform = xform;\n        }\n\n        @Override\n        public InputStream fetchForm(String formURL) {\n            isCancelled = true;\n            return new ByteArrayInputStream(xform.getBytes());\n        }\n\n        @Override\n        public List<FormListItem> fetchFormList() {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public ManifestFile fetchManifest(String manifestURL) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public InputStream fetchMediaFile(String mediaFileURL) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public Boolean get() {\n            return isCancelled;\n        }\n    }\n\n    public static class CancelAfterMediaFileDownloadFormSource implements FormSource, Supplier<Boolean> {\n\n        private final String xform;\n        private boolean isCancelled;\n\n        public CancelAfterMediaFileDownloadFormSource(String xform) {\n            this.xform = xform;\n        }\n\n        @Override\n        public InputStream fetchForm(String formURL) {\n            return new ByteArrayInputStream(xform.getBytes());\n        }\n\n        @Override\n        public InputStream fetchMediaFile(String mediaFileURL) {\n            isCancelled = true;\n            return new ByteArrayInputStream(\"contents\".getBytes());\n        }\n\n        @Override\n        public List<FormListItem> fetchFormList() {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public ManifestFile fetchManifest(String manifestURL) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public Boolean get() {\n            return isCancelled;\n        }\n    }\n\n    private static class FakeClock {\n        private long time;\n\n        FakeClock(long time) {\n            this.time = time;\n        }\n\n        void set(long time) {\n            this.time = time;\n        }\n\n        long get() {\n            return time;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt",
    "content": "package org.odk.collect.android.formmanagement.drafts\n\nimport androidx.appcompat.view.SupportMenuInflater\nimport androidx.appcompat.view.menu.MenuBuilder\nimport androidx.fragment.app.FragmentActivity\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.R\nimport org.odk.collect.android.support.CollectHelpers\n\n@RunWith(AndroidJUnit4::class)\nclass DraftsMenuProviderTest {\n\n    private val activity = CollectHelpers.createThemedActivity(FragmentActivity::class.java)\n    private val menuInflater = SupportMenuInflater(activity)\n    private val menu = MenuBuilder(activity)\n    private val draftsMenuProvider = DraftsMenuProvider(activity, mock()).also {\n        it.onCreateMenu(menu, menuInflater)\n    }\n\n    @Test\n    fun whenDraftCountHasNotLoaded_doesNotShowFinalizeAll() {\n        draftsMenuProvider.draftsCount = null\n        draftsMenuProvider.onPrepareMenu(menu)\n        assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(false))\n    }\n\n    @Test\n    fun whenDraftCountIsZero_doesNotShowFinalizeAll() {\n        draftsMenuProvider.draftsCount = 0\n        draftsMenuProvider.onPrepareMenu(menu)\n        assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(false))\n    }\n\n    @Test\n    fun whenDraftsCountIsNonZero_showsFinalizeAll() {\n        draftsMenuProvider.draftsCount = 1\n        draftsMenuProvider.onPrepareMenu(menu)\n        assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/formmap/FormMapViewModelTest.kt",
    "content": "package org.odk.collect.android.formmanagement.formmap\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.containsString\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.android.instancemanagement.userVisibleInstanceName\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.FormUtils\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.formstest.InstanceUtils\nimport org.odk.collect.geo.selection.IconifiedText\nimport org.odk.collect.geo.selection.MappableSelectItem\nimport org.odk.collect.geo.selection.Status\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.testshared.FakeScheduler\nimport java.text.SimpleDateFormat\nimport java.util.Locale\n\n@RunWith(AndroidJUnit4::class)\nclass FormMapViewModelTest {\n\n    private val formsRepository = InMemFormsRepository()\n    private val instancesRepository = InMemInstancesRepository()\n    private val settingsProvider = InMemSettingsProvider().also {\n        it.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, true)\n    }\n\n    private val application = ApplicationProvider.getApplicationContext<Application>()\n    private val scheduler = FakeScheduler()\n\n    @Test\n    fun `returns count based on form instances`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .build()\n        )\n        instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        assertThat(viewModel.getItemCount().value, equalTo(2))\n    }\n\n    @Test\n    fun `returns items based on form instances with point geometry`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n\n        instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"\")\n                .geometryType(\"Something else\")\n                .build()\n        )\n        instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .build()\n        )\n\n        val instanceWithPoint = instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(true)\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        assertThat(viewModel.getMappableItems().value!!.size, equalTo(1))\n\n        val expectedItem = MappableSelectItem.MappableSelectPoint(\n            instanceWithPoint.dbId,\n            instanceWithPoint.displayName,\n            point = MapPoint(2.0, 1.0),\n            smallIcon = R.drawable.ic_room_form_state_incomplete_24dp,\n            largeIcon = R.drawable.ic_room_form_state_incomplete_48dp,\n            action = IconifiedText(\n                org.odk.collect.icons.R.drawable.ic_edit,\n                application.getString(org.odk.collect.strings.R.string.edit_data)\n            ),\n            info = formatDate(\n                org.odk.collect.strings.R.string.saved_on_date_at_time,\n                instanceWithPoint.lastStatusChangeDate\n            ),\n            status = Status.ERRORS\n        )\n        assertThat(viewModel.getMappableItems().value!![0], equalTo(expectedItem))\n    }\n\n    @Test\n    fun `valid drafts with geometry have proper icons, actions and no info`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        val instance = instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(true)\n                .status(Instance.STATUS_VALID)\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        val expectedItem = MappableSelectItem.MappableSelectPoint(\n            instance.dbId,\n            instance.userVisibleInstanceName(),\n            point = MapPoint(2.0, 1.0),\n            smallIcon = R.drawable.ic_room_form_state_incomplete_24dp,\n            largeIcon = R.drawable.ic_room_form_state_incomplete_48dp,\n            action = IconifiedText(\n                org.odk.collect.icons.R.drawable.ic_edit,\n                application.getString(org.odk.collect.strings.R.string.edit_data)\n            ),\n            info = formatDate(\n                org.odk.collect.strings.R.string.saved_on_date_at_time,\n                instance.lastStatusChangeDate\n            ),\n            status = Status.NO_ERRORS\n        )\n        assertThat(viewModel.getMappableItems().value!![0], equalTo(expectedItem))\n    }\n\n    @Test\n    fun `new edits with geometry have proper icons, actions and no info`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        val instance = instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(true)\n                .status(Instance.STATUS_NEW_EDIT)\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        val expectedItem = MappableSelectItem.MappableSelectPoint(\n            instance.dbId,\n            instance.userVisibleInstanceName(),\n            point = MapPoint(2.0, 1.0),\n            smallIcon = R.drawable.ic_room_form_state_incomplete_24dp,\n            largeIcon = R.drawable.ic_room_form_state_incomplete_48dp,\n            action = IconifiedText(\n                org.odk.collect.icons.R.drawable.ic_edit,\n                application.getString(org.odk.collect.strings.R.string.edit_data)\n            ),\n            info = formatDate(\n                org.odk.collect.strings.R.string.saved_on_date_at_time,\n                instance.lastStatusChangeDate\n            ),\n            status = Status.NO_ERRORS\n        )\n        assertThat(viewModel.getMappableItems().value!![0], equalTo(expectedItem))\n    }\n\n    @Test\n    fun `invalid drafts with geometry have proper icons, actions, status and no info`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        val instance = instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(true)\n                .status(Instance.STATUS_INVALID)\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        val expectedItem = MappableSelectItem.MappableSelectPoint(\n            instance.dbId,\n            instance.userVisibleInstanceName(),\n            point = MapPoint(2.0, 1.0),\n            smallIcon = R.drawable.ic_room_form_state_incomplete_24dp,\n            largeIcon = R.drawable.ic_room_form_state_incomplete_48dp,\n            action = IconifiedText(\n                org.odk.collect.icons.R.drawable.ic_edit,\n                application.getString(org.odk.collect.strings.R.string.edit_data)\n            ),\n            info = formatDate(\n                org.odk.collect.strings.R.string.saved_on_date_at_time,\n                instance.lastStatusChangeDate\n            ),\n            status = Status.ERRORS\n        )\n        assertThat(viewModel.getMappableItems().value!![0], equalTo(expectedItem))\n    }\n\n    @Test\n    fun `finalized instances with geometry have view action and no info`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        val instance = instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(true)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        val expectedItem = MappableSelectItem.MappableSelectPoint(\n            instance.dbId,\n            instance.userVisibleInstanceName(),\n            point = MapPoint(2.0, 1.0),\n            smallIcon = R.drawable.ic_room_form_state_complete_24dp,\n            largeIcon = R.drawable.ic_room_form_state_complete_48dp,\n            action = IconifiedText(\n                R.drawable.ic_visibility,\n                application.getString(org.odk.collect.strings.R.string.view_data)\n            ),\n            info = formatDate(\n                org.odk.collect.strings.R.string.finalized_on_date_at_time,\n                instance.lastStatusChangeDate\n            )\n        )\n        assertThat(viewModel.getMappableItems().value!![0], equalTo(expectedItem))\n    }\n\n    @Test\n    fun `finalized instances with geometry when edit after save disabled have view action and no info`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        val instance = instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(true)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, false)\n\n        val viewModel = createAndLoadViewModel(form)\n        val expectedItem = MappableSelectItem.MappableSelectPoint(\n            instance.dbId,\n            instance.userVisibleInstanceName(),\n            point = MapPoint(2.0, 1.0),\n            smallIcon = R.drawable.ic_room_form_state_complete_24dp,\n            largeIcon = R.drawable.ic_room_form_state_complete_48dp,\n            action = IconifiedText(\n                R.drawable.ic_visibility,\n                application.getString(org.odk.collect.strings.R.string.view_data)\n            ),\n            info = formatDate(\n                org.odk.collect.strings.R.string.finalized_on_date_at_time,\n                instance.lastStatusChangeDate\n            )\n        )\n        assertThat(viewModel.getMappableItems().value!![0], equalTo(expectedItem))\n    }\n\n    @Test\n    fun `deleted instances with geometry have info and no action`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        val instance = instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(true)\n                .deletedDate(123L)\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        val expectedItem = MappableSelectItem.MappableSelectPoint(\n            instance.dbId,\n            instance.userVisibleInstanceName(),\n            point = MapPoint(2.0, 1.0),\n            smallIcon = R.drawable.ic_room_form_state_incomplete_24dp,\n            largeIcon = R.drawable.ic_room_form_state_incomplete_48dp,\n            info = formatDate(\n                org.odk.collect.strings.R.string.saved_on_date_at_time,\n                instance.lastStatusChangeDate\n            ) + \"\\n\" +\n                formatDate(\n                    org.odk.collect.strings.R.string.deleted_on_date_at_time,\n                    123L\n                )\n        )\n        assertThat(viewModel.getMappableItems().value!![0], equalTo(expectedItem))\n    }\n\n    @Test\n    fun `submitted instances with geometry have view action and no info`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        val instance = instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(true)\n                .status(Instance.STATUS_SUBMITTED)\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        val expectedItem = MappableSelectItem.MappableSelectPoint(\n            instance.dbId,\n            instance.userVisibleInstanceName(),\n            point = MapPoint(2.0, 1.0),\n            smallIcon = R.drawable.ic_room_form_state_submitted_24dp,\n            largeIcon = R.drawable.ic_room_form_state_submitted_48dp,\n            action = IconifiedText(\n                R.drawable.ic_visibility,\n                application.getString(org.odk.collect.strings.R.string.view_data)\n            ),\n            info = formatDate(\n                org.odk.collect.strings.R.string.sent_on_date_at_time,\n                instance.lastStatusChangeDate\n            )\n        )\n        assertThat(viewModel.getMappableItems().value!![0], equalTo(expectedItem))\n    }\n\n    @Test\n    fun `instances that failed to submit with geometry have view action and no info`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        val instance = instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(true)\n                .status(Instance.STATUS_SUBMISSION_FAILED)\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        val expectedItem = MappableSelectItem.MappableSelectPoint(\n            instance.dbId,\n            instance.userVisibleInstanceName(),\n            point = MapPoint(2.0, 1.0),\n            smallIcon = R.drawable.ic_room_form_state_submission_failed_24dp,\n            largeIcon = R.drawable.ic_room_form_state_submission_failed_48dp,\n            action = IconifiedText(\n                R.drawable.ic_visibility,\n                application.getString(org.odk.collect.strings.R.string.view_data)\n            ),\n            info = formatDate(\n                org.odk.collect.strings.R.string.sending_failed_on_date_at_time,\n                instance.lastStatusChangeDate\n            )\n        )\n        assertThat(viewModel.getMappableItems().value!![0], equalTo(expectedItem))\n    }\n\n    @Test\n    fun `instances that cannot be edited after completion with geometry have info and no action`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(false)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n        instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(false)\n                .status(Instance.STATUS_SUBMITTED)\n                .build()\n        )\n        instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .canEditWhenComplete(false)\n                .status(Instance.STATUS_SUBMISSION_FAILED)\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        val items = viewModel.getMappableItems().value\n\n        assertThat(\n            items!![0].info,\n            containsString(application.getString(org.odk.collect.strings.R.string.cannot_edit_completed_form))\n        )\n\n        assertThat(\n            items[1].info,\n            containsString(application.getString(org.odk.collect.strings.R.string.cannot_edit_completed_form))\n        )\n\n        assertThat(\n            items[2].info,\n            containsString(application.getString(org.odk.collect.strings.R.string.cannot_edit_completed_form))\n        )\n    }\n\n    @Test\n    fun `is loading is true and items is null while forms and instances are being fetched`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n\n        val viewModel = createViewModel(form)\n        assertThat(viewModel.isLoading().value, equalTo(false))\n        assertThat(viewModel.getMappableItems().value, equalTo(null))\n\n        viewModel.load()\n        assertThat(viewModel.isLoading().value, equalTo(true))\n        assertThat(viewModel.getMappableItems().value, equalTo(null))\n\n        scheduler.runBackground()\n        scheduler.runForeground()\n        assertThat(viewModel.isLoading().value, equalTo(false))\n    }\n\n    @Test\n    fun `edited finalized instances display name with edit number`() {\n        val form = formsRepository.save(\n            FormUtils.buildForm(\"id\", \"version\", TempFiles.createTempDir().absolutePath)\n                .build()\n        )\n        val originalInstance = instancesRepository.save(\n            InstanceUtils.buildInstance(\n                form.formId,\n                form.version,\n                TempFiles.createTempDir().absolutePath\n            )\n                .geometry(\"{ \\\"coordinates\\\": [1.0, 2.0] }\")\n                .geometryType(\"Point\")\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n        val editedInstance = instancesRepository.save(\n            Instance\n                .Builder(originalInstance)\n                .dbId(originalInstance.dbId + 1)\n                .editOf(originalInstance.dbId)\n                .editNumber(1)\n                .build()\n        )\n        val deletedEditedInstance = instancesRepository.save(\n            Instance\n                .Builder(originalInstance)\n                .dbId(originalInstance.dbId + 2)\n                .deletedDate(123L)\n                .editOf(originalInstance.dbId)\n                .editNumber(1)\n                .build()\n        )\n        val nonEditableEditedInstance = instancesRepository.save(\n            Instance\n                .Builder(originalInstance)\n                .dbId(originalInstance.dbId + 3)\n                .canEditWhenComplete(false)\n                .editOf(originalInstance.dbId)\n                .editNumber(1)\n                .build()\n        )\n\n        val viewModel = createAndLoadViewModel(form)\n        val items = viewModel.getMappableItems().value\n\n        assertThat(\n            items!![0].name,\n            equalTo(originalInstance.displayName)\n        )\n\n        assertThat(\n            items[1].name,\n            equalTo(editedInstance.userVisibleInstanceName())\n        )\n\n        assertThat(\n            items[2].name,\n            equalTo(deletedEditedInstance.userVisibleInstanceName())\n        )\n\n        assertThat(\n            items[3].name,\n            equalTo(nonEditableEditedInstance.userVisibleInstanceName())\n        )\n    }\n\n    private fun createAndLoadViewModel(form: Form): FormMapViewModel {\n        val viewModel = createViewModel(form)\n        viewModel.load()\n        scheduler.flush()\n        return viewModel\n    }\n\n    private fun createViewModel(form: Form): FormMapViewModel {\n        return FormMapViewModel(\n            application.resources,\n            form.dbId,\n            formsRepository,\n            instancesRepository,\n            settingsProvider,\n            scheduler\n        )\n    }\n\n    private fun formatDate(string: Int, date: Long): String {\n        return SimpleDateFormat(\n            application.getString(string),\n            Locale.getDefault()\n        ).format(date)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/formmanagement/metadata/FormMetadataParserTest.kt",
    "content": "package org.odk.collect.android.formmanagement.metadata\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.android.formmanagement.metadata.FormMetadataParser.readMetadata\n\nclass FormMetadataParserTest {\n\n    @Test\n    fun readMetadata_canParseWithAttributesOnMainInstance() {\n        val metadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns=\"http://www.w3.org/2002/xforms\"\n                        xmlns:custom=\"http://example.com/custom\">\n                    <h:head>\n                        <h:title>Form</h:title>\n                        <model>\n                            <instance custom:attribute=\"blah\">\n                                <data id=\"form\">\n                                </data>\n                            </instance>\n                        </model>\n                    </h:head>\n                    <h:body>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(metadata.id, equalTo(\"form\"))\n    }\n\n    @Test\n    fun readMetadata_canParseWithXformsNamespace() {\n        val metadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns:custom=\"http://www.w3.org/2002/xforms\">\n                    <h:head>\n                        <h:title>Form</h:title>\n                        <custom:model>\n                            <custom:instance>\n                                <custom:data custom:id=\"form\">\n                                </custom:data>\n                            </custom:instance>\n                        </custom:model>\n                    </h:head>\n                    <h:body>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(metadata.id, equalTo(\"form\"))\n    }\n\n    @Test\n    fun readMetadata_canParseFormsWithComments() {\n        readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <!-- Blah -->\n                <h:html xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns=\"http://www.w3.org/2002/xforms\">\n                    <!-- Blah -->\n                    <h:head>\n                        <!-- Blah -->\n                        <h:title>Form with comments</h:title>\n                        <!-- Blah -->\n                        <model>\n                            <!-- Blah -->\n                            <instance>\n                                <!-- Blah -->\n                                <data id=\"form-with-comments\">\n                                    <!-- Blah -->\n                                </data>\n                                <!-- Blah -->\n                            </instance>\n                            <!-- Blah -->\n                        </model>\n                    </h:head>\n                    <h:body>\n                        <!-- Blah -->\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n    }\n\n    @Test\n    fun readMetadata_returnsCorrectValuesForMandatoryElements() {\n        val formMetadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns=\"http://www.w3.org/2002/xforms\"\n                        xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns:orx=\"http://openrosa.org/xforms\">\n                    <h:head>\n                        <h:title>My Survey</h:title>\n                        <model>\n                            <instance>\n                                <data id=\"mysurvey\">\n                                </data>\n                            </instance>\n                        </model>\n                    </h:head>\n                    <h:body>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(formMetadata.title, equalTo(\"My Survey\"))\n        assertThat(formMetadata.id, equalTo(\"mysurvey\"))\n    }\n\n    @Test\n    fun readMetadata_withoutOptionalMetadata_returnsNullValuesForThoseElements() {\n        val formMetadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns=\"http://www.w3.org/2002/xforms\"\n                        xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns:orx=\"http://openrosa.org/xforms\">\n                    <h:head>\n                        <h:title>My Survey</h:title>\n                        <model>\n                            <instance>\n                                <data id=\"mysurvey\">\n                                </data>\n                            </instance>\n                        </model>\n                    </h:head>\n                    <h:body>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(formMetadata.version, equalTo(null))\n        assertThat(formMetadata.submissionUri, equalTo(null))\n        assertThat(formMetadata.autoSend, equalTo(null))\n        assertThat(formMetadata.autoDelete, equalTo(null))\n        assertThat(formMetadata.base64RsaPublicKey, equalTo(null))\n        assertThat(formMetadata.geometryXPath, equalTo(null))\n        assertThat(formMetadata.isEntityForm, equalTo(false))\n    }\n\n    @Test\n    fun readMetadata_witOptionalMetadata_returnsCorrectValuesForThoseElements() {\n        val formMetadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns=\"http://www.w3.org/2002/xforms\"\n                        xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns:orx=\"http://openrosa.org/xforms\"\n                        xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n                    <h:head>\n                        <h:title>My Survey</h:title>\n                        <model entities:entities-version=\"2024.1.0\">\n                            <instance>\n                                <data id=\"mysurvey\" orx:version=\"2014083101\">\n                                    <location1 />\n                                </data>\n                            </instance>\n                            <submission \n                                action=\"foo\" \n                                orx:auto-send=\"bar\" \n                                orx:auto-delete=\"baz\" \n                                base64RsaPublicKey=\"quux\" \n                            />\n                            <bind nodeset=\"/data/location1\" type=\"geopoint\" />\n                        </model>\n                    </h:head>\n                    <h:body>\n                        <input ref=\"/data/location1\">\n                            <label>Location</label>\n                        </input>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(formMetadata.version, equalTo(\"2014083101\"))\n        assertThat(formMetadata.submissionUri, equalTo(\"foo\"))\n        assertThat(formMetadata.autoSend, equalTo(\"bar\"))\n        assertThat(formMetadata.autoDelete, equalTo(\"baz\"))\n        assertThat(formMetadata.base64RsaPublicKey, equalTo(\"quux\"))\n        assertThat(formMetadata.geometryXPath, equalTo(\"/data/location1\"))\n        assertThat(formMetadata.isEntityForm, equalTo(true))\n    }\n\n    @Test\n    fun readMetadata_withEmptyFormVersion_returnsNullFormVersion() {\n        val formMetadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns=\"http://www.w3.org/2002/xforms\"\n                        xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns:orx=\"http://openrosa.org/xforms\">\n                    <h:head>\n                        <h:title>My Survey</h:title>\n                        <model>\n                            <instance>\n                                <data id=\"mysurvey\" orx:version=\"   \">\n                                </data>\n                            </instance>\n                        </model>\n                    </h:head>\n                    <h:body>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(formMetadata.version, equalTo(null))\n    }\n\n    @Test\n    fun readMetadata_withGeopointsAtTopLevel_returnsFirstGeopointXPath() {\n        val formMetadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns=\"http://www.w3.org/2002/xforms\">\n                    <h:head>\n                        <h:title>Two geopoints</h:title>\n                        <model>\n                            <instance>\n                                <data id=\"two-geopoints\">\n                                    <location1 />\n                                    <name />\n                                    <location2 />\n                                </data>\n                            </instance>\n                            <bind nodeset=\"/data/location1\" type=\"geopoint\" />\n                            <bind nodeset=\"/data/name\" type=\"string\" />\n                            <bind nodeset=\"/data/location2\" type=\"geopoint\" />\n                        </model>\n                    </h:head>\n                    <h:body>\n                        <input ref=\"/data/location1\">\n                            <label>Location</label>\n                        </input>\n                        <input ref=\"/data/name\">\n                            <label>Name</label>\n                        </input>\n                        <input ref=\"/data/location2\">\n                            <label>Location</label>\n                        </input>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(formMetadata.geometryXPath, equalTo(\"/data/location1\"))\n    }\n\n    @Test\n    fun readMetadata_withGeopointsAtTopLevel_returnsGeopointXPathThatBelongsToSetgeopointActionIfItIsTheFirstOne() {\n        val formMetadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns=\"http://www.w3.org/2002/xforms\"\n                        xmlns:odk=\"http://www.opendatakit.org/xforms\">\n                    <h:head>\n                        <h:title>Two geopoints</h:title>\n                        <model>\n                            <instance>\n                                <data id=\"two-geopoints\">\n                                    <location1 />\n                                    <name />\n                                    <location2 />\n                                </data>\n                            </instance>\n                            <bind nodeset=\"/data/location1\" type=\"geopoint\" />\n                            <bind nodeset=\"/data/name\" type=\"string\" />\n                            <bind nodeset=\"/data/location2\" type=\"geopoint\" />\n                            <odk:setgeopoint ref=\"/data/location1\" event=\"odk-instance-first-load\"/>\n                        </model>\n                    </h:head>\n                    <h:body>\n                        <input ref=\"/data/name\">\n                            <label>Name</label>\n                        </input>\n                        <input ref=\"/data/location2\">\n                            <label>Location</label>\n                        </input>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(formMetadata.geometryXPath, equalTo(\"/data/location1\"))\n    }\n\n    @Test\n    fun readMetadata_withGeopointInGroup_returnsFirstGeopointXPath() {\n        val formMetadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns=\"http://www.w3.org/2002/xforms\">\n                    <h:head>\n                        <h:title>Two geopoints in group</h:title>\n                        <model>\n                            <instance>\n                                <data id=\"two-geopoints-group\">\n                                    <my-group1>\n                                        <my-group2>\n                                            <location1 />\n                                        </my-group2>\n                                    </my-group1>\n                                    <location2 />\n                                </data>\n                            </instance>\n                            <bind nodeset=\"/data/my-group1/my-group2/location1\" type=\"geopoint\" />\n                            <bind nodeset=\"/data/location2\" type=\"geopoint\" />\n                        </model>\n                    </h:head>\n                    <h:body>\n                        <group ref=\"/data/my-group1/my-group2\">\n                            <input ref=\"/data/my-group1/my-group2/location1\">\n                                <label>Location</label>\n                            </input>\n                        </group>\n                        <input ref=\"/data/location2\">\n                            <label>Location</label>\n                        </input>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(formMetadata.geometryXPath, equalTo(\"/data/my-group1/my-group2/location1\"))\n    }\n\n    @Test\n    fun readMetadata_withGeopointInNonFirstGroup_returnsExpectedGeopointXPath() {\n        val formMetadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns=\"http://www.w3.org/2002/xforms\">\n                    <h:head>\n                        <h:title>Two geopoints in group</h:title>\n                        <model>\n                            <instance>\n                                <data id=\"two-geopoints-group\">\n                                    <my-group1>\n                                        <name />\n                                    </my-group1>\n                                    <my-group2>\n                                        <location />\n                                    </my-group2>\n                                </data>\n                            </instance>\n                            <bind nodeset=\"/data/my-group1/name\" type=\"string\" />\n                            <bind nodeset=\"/data/my-group2/location\" type=\"geopoint\" />\n                        </model>\n                    </h:head>\n                    <h:body>\n                        <group ref=\"/data/my-group1\">\n                            <input ref=\"/data/my-group1/name\">\n                                <label>Name</label>\n                            </input>\n                        </group>\n                        <group ref=\"/data/my-group2\">\n                            <input ref=\"/data/my-group2/location\">\n                                <label>Location</label>\n                            </input>\n                        </group>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(formMetadata.geometryXPath, equalTo(\"/data/my-group2/location\"))\n    }\n\n    @Test\n    fun readMetadata_withGeopointInRepeat_returnsFirstGeopointXPathThatIsNotInsideRepeat() {\n        val formMetadata = readMetadata(\n            \"\"\"\n                <?xml version=\"1.0\"?>\n                <h:html xmlns:h=\"http://www.w3.org/1999/xhtml\"\n                        xmlns=\"http://www.w3.org/2002/xforms\">\n                    <h:head>\n                        <h:title>Two geopoints repeat</h:title>\n                        <model>\n                            <instance>\n                                <data id=\"two-geopoints-repeat\">\n                                    <my-repeat>\n                                        <location1 />\n                                    </my-repeat>\n                                    <location2 />\n                                </data>\n                            </instance>\n                            <bind nodeset=\"/data/location2\" type=\"geopoint\" />\n                            <bind nodeset=\"/data/my-repeat/location1\" type=\"geopoint\" />\n                        </model>\n                    </h:head>\n                    <h:body>\n                        <repeat nodeset=\"/data/my-repeat\">\n                            <input ref=\"/data/my-repeat/location1\">\n                                <label>Location</label>\n                            </input>\n                        </repeat>\n                        <input ref=\"/data/location2\">\n                            <label>Location</label>\n                        </input>\n                    </h:body>\n                </h:html>\n            \"\"\".trimIndent().byteInputStream()\n        )\n\n        assertThat(formMetadata.geometryXPath, equalTo(\"/data/location2\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/fragments/dialogs/FormsDownloadResultDialogTest.kt",
    "content": "package org.odk.collect.android.fragments.dialogs\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.appcompat.app.AlertDialog\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.formmanagement.ServerFormDetails\nimport org.odk.collect.android.formmanagement.download.FormDownloadException\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.testshared.RobolectricHelpers\nimport org.robolectric.Shadows\n\n@RunWith(AndroidJUnit4::class)\nclass FormsDownloadResultDialogTest {\n    private val resultItem = ServerFormDetails(\"Form 1\", \"\", \"1\", \"1\", \"\", false, true, null)\n    val listener = mock<FormsDownloadResultDialog.FormDownloadResultDialogListener>()\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Test\n    fun `The dialog should be dismissed after clicking out of it's area or on device back button`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf<ServerFormDetails, FormDownloadException>()\n        )\n\n        val scenario =\n            launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        scenario.onFragment {\n            assertThat(it.isCancelable, `is`(true))\n        }\n    }\n\n    @Test\n    fun `The title of the 'POSITIVE BUTTON' should be 'OK'`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf<ServerFormDetails, FormDownloadException>()\n        )\n\n        val scenario =\n            launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        scenario.onFragment {\n            assertThat(\n                (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).text,\n                `is`(ApplicationProvider.getApplicationContext<Collect>().getString(org.odk.collect.strings.R.string.ok))\n            )\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on the 'POSITIVE BUTTON'`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf<ServerFormDetails, FormDownloadException>()\n        )\n\n        val scenario =\n            launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        scenario.onFragment {\n            it.listener = listener\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(nullValue()))\n        }\n    }\n\n    @Test\n    fun `onCloseDownloadingResult() should be called after clicking on the 'POSITIVE BUTTON'`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf<ServerFormDetails, FormDownloadException>()\n        )\n\n        val scenario =\n            launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        scenario.onFragment {\n            it.listener = listener\n            (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).performClick()\n            RobolectricHelpers.runLooper()\n            verify(listener).onCloseDownloadingResult()\n        }\n    }\n\n    @Test\n    fun `if there are errors onCloseDownloadingResult() should be called after clicking on the 'NEGATIVE BUTTON'`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf(resultItem to FormDownloadException.InvalidSubmission())\n        )\n\n        val scenario =\n            launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        scenario.onFragment {\n            it.listener = listener\n            (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_NEGATIVE).performClick()\n            RobolectricHelpers.runLooper()\n            verify(listener).onCloseDownloadingResult()\n        }\n    }\n\n    @Test\n    fun `If there are no errors an appropriate message should be displayed`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf<ServerFormDetails, String>()\n        )\n\n        launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        onView(withText(org.odk.collect.strings.R.string.all_downloads_succeeded)).inRoot(isDialog())\n            .check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `If there are no errors 'SHOW DETAILS' button should be hidden`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf<ServerFormDetails, String>()\n        )\n\n        val scenario =\n            launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        scenario.onFragment {\n            assertThat(\n                (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_NEGATIVE).visibility,\n                `is`(View.GONE)\n            )\n        }\n    }\n\n    @Test\n    fun `If there are errors an appropriate message should be displayed`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf(resultItem to FormDownloadException.InvalidSubmission())\n        )\n\n        launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        onView(\n            withText(\n                ApplicationProvider.getApplicationContext<Collect>()\n                    .getString(org.odk.collect.strings.R.string.some_downloads_failed, \"1\", \"1\")\n            )\n        ).inRoot(isDialog()).check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `If there are errors 'SHOW DETAILS' button should be displayed`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf(resultItem to FormDownloadException.InvalidSubmission())\n        )\n\n        val scenario =\n            launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        scenario.onFragment {\n            assertThat(\n                (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_NEGATIVE).visibility,\n                `is`(View.VISIBLE)\n            )\n            assertThat(\n                (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_NEGATIVE).text,\n                `is`(\n                    ApplicationProvider.getApplicationContext<Collect>()\n                        .getString(org.odk.collect.strings.R.string.show_details)\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on the 'NEGATIVE BUTTON'`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf<ServerFormDetails, String>()\n        )\n\n        val scenario =\n            launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        scenario.onFragment {\n            it.listener = listener\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_NEGATIVE).performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(nullValue()))\n        }\n    }\n\n    @Test\n    fun `Recreation should not change the state of dialog`() {\n        val args = Bundle()\n        args.putSerializable(\n            FormsDownloadResultDialog.ARG_RESULT,\n            hashMapOf(resultItem to FormDownloadException.InvalidSubmission())\n        )\n\n        val scenario =\n            launcherRule.launch(FormsDownloadResultDialog::class.java, args)\n        scenario.onFragment {\n            assertThat(Shadows.shadowOf(it.dialog).title, `is`(\"\"))\n            assertThat(\n                (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).text,\n                `is`(ApplicationProvider.getApplicationContext<Collect>().getString(org.odk.collect.strings.R.string.ok))\n            )\n            assertThat(\n                (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_NEGATIVE).text,\n                `is`(\n                    ApplicationProvider.getApplicationContext<Collect>()\n                        .getString(org.odk.collect.strings.R.string.show_details)\n                )\n            )\n        }\n\n        scenario.recreate()\n\n        scenario.onFragment {\n            assertThat(Shadows.shadowOf(it.dialog).title, `is`(\"\"))\n            assertThat(\n                (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).text,\n                `is`(ApplicationProvider.getApplicationContext<Collect>().getString(org.odk.collect.strings.R.string.ok))\n            )\n            assertThat(\n                (it.dialog as AlertDialog).getButton(AlertDialog.BUTTON_NEGATIVE).text,\n                `is`(\n                    ApplicationProvider.getApplicationContext<Collect>()\n                        .getString(org.odk.collect.strings.R.string.show_details)\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/fragments/dialogs/RangePickerDialogFragmentTest.kt",
    "content": "package org.odk.collect.android.fragments.dialogs\n\nimport androidx.core.os.bundleOf\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewmodel.CreationExtras\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.javarosa.core.model.data.DecimalData\nimport org.javarosa.core.model.data.IntegerData\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formentry.FormEntryViewModel\nimport org.odk.collect.android.fragments.dialogs.RangePickerDialogFragment.Companion.ARG_DECIMAL\nimport org.odk.collect.android.fragments.dialogs.RangePickerDialogFragment.Companion.ARG_FORM_INDEX\nimport org.odk.collect.android.fragments.dialogs.RangePickerDialogFragment.Companion.ARG_SELECTED\nimport org.odk.collect.android.fragments.dialogs.RangePickerDialogFragment.Companion.ARG_VALUES\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.testshared.ViewActions.setNumber\nimport org.odk.collect.testshared.ViewMatchers.hasPicked\n\n@RunWith(AndroidJUnit4::class)\nclass RangePickerDialogFragmentTest {\n\n    private var prompt = MockFormEntryPromptBuilder().build()\n\n    private val formEntryViewModel = mock<FormEntryViewModel> {\n        on { getQuestionPrompt(prompt.index) } doReturn prompt\n    }\n\n    private val viewModelFactory = object : ViewModelProvider.Factory {\n        override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {\n            return when (modelClass) {\n                FormEntryViewModel::class.java -> formEntryViewModel as T\n                else -> throw IllegalArgumentException()\n            }\n        }\n    }\n\n    @get:Rule\n    val launcherRule =\n        FragmentScenarioLauncherRule(\n            FragmentFactoryBuilder()\n                .forClass(RangePickerDialogFragment::class) {\n                    RangePickerDialogFragment(viewModelFactory)\n                }.build()\n        )\n\n    @Test\n    fun `answers question when number selected`() {\n        launcherRule.launch(\n            RangePickerDialogFragment::class.java,\n            bundleOf(\n                ARG_FORM_INDEX to prompt.index,\n                ARG_VALUES to arrayOf(\"1\", \"2\", \"3\"),\n                ARG_SELECTED to 0\n            )\n        )\n\n        onView(withId(R.id.number_picker)).inRoot(isDialog()).perform(setNumber(\"2\"))\n        onView(withText(string.ok)).perform(click())\n\n        verify(formEntryViewModel).answerQuestion(prompt.index, IntegerData(2))\n    }\n\n    @Test\n    fun `answers with DecimalData when DECIMAL arg is true`() {\n        launcherRule.launch(\n            RangePickerDialogFragment::class.java,\n            bundleOf(\n                ARG_FORM_INDEX to prompt.index,\n                ARG_VALUES to arrayOf(\"1.0\", \"2.0\", \"3.0\"),\n                ARG_SELECTED to 0,\n                ARG_DECIMAL to true\n            )\n        )\n\n        onView(withId(R.id.number_picker)).inRoot(isDialog()).perform(setNumber(\"3.0\"))\n        onView(withText(string.ok)).perform(click())\n\n        verify(formEntryViewModel).answerQuestion(prompt.index, DecimalData(3.0))\n    }\n\n    @Test\n    fun `selects value based on VALUE arg`() {\n        launcherRule.launch(\n            RangePickerDialogFragment::class.java,\n            bundleOf(\n                ARG_FORM_INDEX to prompt.index,\n                ARG_VALUES to arrayOf(\"1\", \"2\", \"3\"),\n                ARG_SELECTED to 1\n            )\n        )\n\n        onView(withId(R.id.number_picker)).inRoot(isDialog()).check(matches(hasPicked(\"2\")))\n    }\n}"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/fragments/dialogs/SelectMinimalDialogTest.java",
    "content": "package org.odk.collect.android.fragments.dialogs;\n\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.audioclips.AudioClipViewModel;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport java.util.List;\n\nimport static java.util.Arrays.asList;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.mockito.Mockito.mock;\n\n@RunWith(AndroidJUnit4.class)\npublic class SelectMinimalDialogTest {\n\n    protected FragmentManager fragmentManager;\n    protected SelectMinimalDialog dialogFragment;\n    protected FormEntryPrompt formEntryPrompt;\n\n    @Before\n    public void setup() {\n        FragmentActivity activity = CollectHelpers.createThemedActivity(WidgetTestActivity.class);\n\n        // Set up AudioClipViewModel used by the dialog\n        new ViewModelProvider(activity, new ViewModelProvider.Factory() {\n            @NonNull\n            @Override\n            public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {\n                if (modelClass == AudioClipViewModel.class) {\n                    return (T) mock(AudioClipViewModel.class);\n                } else {\n                    return ViewModelProvider.Factory.super.create(modelClass);\n                }\n            }\n        }).get(AudioClipViewModel.class);\n\n        fragmentManager = activity.getSupportFragmentManager();\n    }\n\n    @Test\n    public void whenClickBackButton_shouldDialogBeClosed() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"autocomplete\");\n        dialogFragment = new SelectOneMinimalDialog(null, false, true, ApplicationProvider.getApplicationContext(), items, formEntryPrompt, null, 0, 1, false, mock(MediaUtils.class));\n        SelectMinimalDialog.SelectMinimalDialogListener listener = mock(SelectMinimalDialog.SelectMinimalDialogListener.class);\n        dialogFragment.setListener(listener);\n\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        assertThat(isDialogVisible(), is(true));\n        dialogFragment.onBackPressed();\n        assertThat(isDialogVisible(), is(false));\n    }\n\n    @Test\n    public void whenClickBackArrowButton_shouldDialogBeClosed() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"autocomplete\");\n        dialogFragment = new SelectOneMinimalDialog(null, false, true, ApplicationProvider.getApplicationContext(), items, formEntryPrompt, null, 0, 1, false, mock(MediaUtils.class));\n        SelectMinimalDialog.SelectMinimalDialogListener listener = mock(SelectMinimalDialog.SelectMinimalDialogListener.class);\n        dialogFragment.setListener(listener);\n\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        assertThat(isDialogVisible(), is(true));\n        dialogFragment.getToolbar().getChildAt(0).performClick();\n        assertThat(isDialogVisible(), is(false));\n    }\n\n    @Test\n    public void whenAutoCompleteAppearanceUsed_shouldSearchBarBeVisible() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"autocomplete\");\n        dialogFragment = new SelectOneMinimalDialog(null, false, true, ApplicationProvider.getApplicationContext(), items, formEntryPrompt, null, 0, 1, false, mock(MediaUtils.class));\n        SelectMinimalDialog.SelectMinimalDialogListener listener = mock(SelectMinimalDialog.SelectMinimalDialogListener.class);\n        dialogFragment.setListener(listener);\n\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        assertThat(dialogFragment.getToolbar().findViewById(R.id.menu_filter).getVisibility(), equalTo(View.VISIBLE));\n    }\n\n    protected boolean isDialogVisible() {\n        return dialogFragment != null\n                && dialogFragment.getDialog() != null\n                && dialogFragment.getDialog().isShowing()\n                && !dialogFragment.isRemoving();\n    }\n\n    protected List<SelectChoice> getTestChoices() {\n        return asList(\n                new SelectChoice(\"AAA\", \"AAA\"),\n                new SelectChoice(\"BBB\", \"BBB\")\n        );\n    }\n\n    protected void setUpFormEntryPrompt(List<SelectChoice> items, String appearance) {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(items)\n                .withAppearance(appearance)\n                .build();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/fragments/dialogs/SelectMultiMinimalDialogTest.java",
    "content": "package org.odk.collect.android.fragments.dialogs;\n\nimport androidx.test.core.app.ApplicationProvider;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.junit.Test;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.mockito.ArgumentMatchers.anyList;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\npublic class SelectMultiMinimalDialogTest extends SelectMinimalDialogTest {\n    @Test\n    public void whenClickBackButton_shouldAnswerBeSavedOnlyIfChanged() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"autocomplete\");\n        List<Selection> selectedItems = new ArrayList<>();\n        selectedItems.add(items.get(0).selection());\n        dialogFragment = new SelectMultiMinimalDialog(selectedItems, false, true, ApplicationProvider.getApplicationContext(), items, formEntryPrompt, null, 0, 1, false, mock(MediaUtils.class));\n        SelectMinimalDialog.SelectMinimalDialogListener listener = mock(SelectMinimalDialog.SelectMinimalDialogListener.class);\n        dialogFragment.setListener(listener);\n\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        dialogFragment.onBackPressed();\n        verify(listener, times(0)).updateSelectedItems(anyList());\n        dialogFragment.show(fragmentManager, \"TAG\");\n        dialogFragment.adapter.clearAnswer();\n        dialogFragment.onBackPressed();\n        verify(listener, times(1)).updateSelectedItems(anyList());\n    }\n\n    @Test\n    public void whenClickBackArrowButton_shouldAnswerBeSavedOnlyIfChanged() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"autocomplete\");\n        List<Selection> selectedItems = new ArrayList<>();\n        selectedItems.add(items.get(0).selection());\n        dialogFragment = new SelectMultiMinimalDialog(selectedItems, false, true, ApplicationProvider.getApplicationContext(), items, formEntryPrompt, null, 0, 1, false, mock(MediaUtils.class));\n        SelectMinimalDialog.SelectMinimalDialogListener listener = mock(SelectMinimalDialog.SelectMinimalDialogListener.class);\n        dialogFragment.setListener(listener);\n\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n        dialogFragment.getToolbar().getChildAt(0).performClick();\n        verify(listener, times(0)).updateSelectedItems(anyList());\n        dialogFragment.show(fragmentManager, \"TAG\");\n        dialogFragment.adapter.clearAnswer();\n        dialogFragment.getToolbar().getChildAt(0).performClick();\n        verify(listener, times(1)).updateSelectedItems(anyList());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/fragments/dialogs/SelectOneMinimalDialogTest.java",
    "content": "package org.odk.collect.android.fragments.dialogs;\n\nimport androidx.test.core.app.ApplicationProvider;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.junit.Test;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport java.util.List;\n\nimport static org.mockito.ArgumentMatchers.anyList;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\npublic class SelectOneMinimalDialogTest extends SelectMinimalDialogTest {\n    @Test\n    public void whenClickBackButton_shouldAnswerBeSavedOnlyIfChanged() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"autocomplete\");\n        dialogFragment = new SelectOneMinimalDialog(\"AAA\", false, true, ApplicationProvider.getApplicationContext(), items, formEntryPrompt, null, 0, 1, false, mock(MediaUtils.class));\n        SelectMinimalDialog.SelectMinimalDialogListener listener = mock(SelectMinimalDialog.SelectMinimalDialogListener.class);\n        dialogFragment.setListener(listener);\n\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n        dialogFragment.onBackPressed();\n        verify(listener, times(0)).updateSelectedItems(anyList());\n        dialogFragment.show(fragmentManager, \"TAG\");\n        dialogFragment.adapter.clearAnswer();\n        dialogFragment.onBackPressed();\n        verify(listener, times(1)).updateSelectedItems(anyList());\n    }\n\n    @Test\n    public void whenClickBackArrowButton_shouldAnswerBeSavedOnlyIfChanged() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"autocomplete\");\n        dialogFragment = new SelectOneMinimalDialog(\"AAA\", false, true, ApplicationProvider.getApplicationContext(), items, formEntryPrompt, null, 0, 1, false, mock(MediaUtils.class));\n        SelectMinimalDialog.SelectMinimalDialogListener listener = mock(SelectMinimalDialog.SelectMinimalDialogListener.class);\n        dialogFragment.setListener(listener);\n\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        dialogFragment.getToolbar().getChildAt(0).performClick();\n        verify(listener, times(0)).updateSelectedItems(anyList());\n        dialogFragment.show(fragmentManager, \"TAG\");\n        dialogFragment.adapter.clearAnswer();\n        dialogFragment.getToolbar().getChildAt(0).performClick();\n        verify(listener, times(1)).updateSelectedItems(anyList());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/fragments/support/DialogFragmentHelpers.java",
    "content": "package org.odk.collect.android.fragments.support;\n\nimport static junit.framework.TestCase.assertTrue;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.robolectric.Shadows.shadowOf;\n\nimport android.content.DialogInterface;\nimport android.os.Bundle;\nimport android.widget.NumberPicker;\nimport android.widget.TextView;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.fragment.app.testing.FragmentScenario;\n\nimport org.joda.time.LocalDateTime;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.widgets.datetime.pickers.CustomDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.testshared.RobolectricHelpers;\nimport org.robolectric.Robolectric;\nimport org.robolectric.android.controller.ActivityController;\nimport org.robolectric.shadows.ShadowDialog;\n\npublic class DialogFragmentHelpers {\n\n    private DialogFragmentHelpers() {\n    }\n\n    public static DatePickerDetails setUpDatePickerDetails(DatePickerDetails.DatePickerType datePickerType) {\n        DatePickerDetails datePickerDetails = mock(DatePickerDetails.class);\n        when(datePickerDetails.getDatePickerType()).thenReturn(datePickerType);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(true);\n        when(datePickerDetails.isMonthYearMode()).thenReturn(false);\n        when(datePickerDetails.isYearMode()).thenReturn(false);\n\n        return datePickerDetails;\n    }\n\n    public static Bundle getDialogFragmentArguments(DatePickerDetails datePickerDetails) {\n        Bundle bundle = new Bundle();\n        bundle.putSerializable(DateTimeWidgetUtils.DATE, new LocalDateTime().withDate(2020, 5, 12));\n        bundle.putSerializable(DateTimeWidgetUtils.DATE_PICKER_DETAILS, datePickerDetails);\n        return bundle;\n    }\n\n    public static void assertDialogIsCancellable(boolean cancellable) {\n        assertThat(shadowOf(ShadowDialog.getLatestDialog()).isCancelable(), equalTo(cancellable));\n    }\n\n    public static void assertDialogShowsCorrectDate(int year, int month, int day, String date) {\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n\n        assertDatePickerValue(dialog, year, month, day);\n        assertThat(((TextView) dialog.findViewById(R.id.date_gregorian)).getText().toString(), equalTo(date));\n    }\n\n    public static void assertDialogShowsCorrectDateForYearMode(int year, String date) {\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n\n        assertDatePickerValue(dialog, year, 0, 1);\n        assertThat(((TextView) dialog.findViewById(R.id.date_gregorian)).getText().toString(), equalTo(date));\n    }\n\n    public static void assertDialogShowsCorrectDateForMonthMode(int year, int month, String date) {\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n\n        assertDatePickerValue(dialog, year, month, 1);\n        assertThat(((TextView) dialog.findViewById(R.id.date_gregorian)).getText().toString(), equalTo(date));\n    }\n\n    public static void assertDialogTextViewUpdatesDate(String date, int year, int month, int day) {\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n        setDatePickerValue(dialog, year, month, day);\n\n        assertThat(((TextView) dialog.findViewById(R.id.date_gregorian)).getText().toString(), equalTo(date));\n    }\n\n    public static void assertDateUpdateInActivity(DatePickerTestActivity activity, int year, int month, int day) {\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n        setDatePickerValue(dialog, year, month, day);\n\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();\n        RobolectricHelpers.runLooper();\n\n        assertThat(activity.selectedDate.getYear(), equalTo(2020));\n        assertThat(activity.selectedDate.getMonthOfYear(), equalTo(5));\n        assertThat(activity.selectedDate.getDayOfMonth(), equalTo(12));\n    }\n\n    public static void assertDialogIsDismissedOnButtonClick(int dialogButton) {\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n        dialog.getButton(dialogButton).performClick();\n        RobolectricHelpers.runLooper();\n        assertTrue(shadowOf(dialog).hasBeenDismissed());\n    }\n\n    /**\n     * @deprecated should use {@link FragmentScenario#recreate()} instead of Robolectric for this\n     */\n    @Deprecated\n    public static <T extends DialogFragment> void assertDialogRetainsDateOnScreenRotation(T dialogFragment, String date, int year, int month, int day) {\n        ActivityController<FragmentActivity> activityController = Robolectric.buildActivity(FragmentActivity.class);\n        activityController.setup();\n\n        dialogFragment.show(activityController.get().getSupportFragmentManager(), \"TAG\");\n        RobolectricHelpers.runLooper();\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n        setDatePickerValue(dialog, year, month, day);\n\n        activityController.recreate();\n\n        T restoredFragment = (T) activityController.get().getSupportFragmentManager().findFragmentByTag(\"TAG\");\n        AlertDialog restoredDialog = (AlertDialog) restoredFragment.getDialog();\n\n        assertDatePickerValue(restoredDialog, year, month, day);\n        assertThat(((TextView) restoredDialog.findViewById(R.id.date_gregorian)).getText().toString(), equalTo(date));\n    }\n\n    private static void setDatePickerValue(AlertDialog dialog, int year, int month, int day) {\n        ((NumberPicker) dialog.findViewById(R.id.year_picker)).setValue(year);\n        ((NumberPicker) dialog.findViewById(R.id.month_picker)).setValue(month);\n        ((NumberPicker) dialog.findViewById(R.id.day_picker)).setValue(day);\n    }\n\n    private static void assertDatePickerValue(AlertDialog dialog, int year, int month, int day) {\n        assertThat(((NumberPicker) dialog.findViewById(R.id.year_picker)).getValue(), equalTo(year));\n        assertThat(((NumberPicker) dialog.findViewById(R.id.month_picker)).getValue(), equalTo(month));\n        assertThat(((NumberPicker) dialog.findViewById(R.id.day_picker)).getValue(), equalTo(day));\n    }\n\n    public static class DatePickerTestActivity extends FragmentActivity implements CustomDatePickerDialog.DateChangeListener {\n        public LocalDateTime selectedDate;\n\n        @Override\n        public void onDateChanged(LocalDateTime selectedDate) {\n            this.selectedDate = selectedDate;\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/geo/MapFragmentFactoryImplTest.kt",
    "content": "package org.odk.collect.android.geo\n\nimport org.hamcrest.CoreMatchers.instanceOf\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.odk.collect.googlemaps.GoogleMapFragment\nimport org.odk.collect.osmdroid.OsmDroidMapFragment\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\n\nclass MapFragmentFactoryImplTest {\n\n    private val settingsProvider = InMemSettingsProvider()\n    private val mapFragmentFactoryImpl = MapFragmentFactoryImpl(settingsProvider)\n\n    @Test\n    fun `OsmDroidMapFragment should be return if any of OSM options selected in settings`() {\n        // BASEMAP_SOURCE_OSM\n        settingsProvider\n            .getUnprotectedSettings()\n            .save(ProjectKeys.KEY_BASEMAP_SOURCE, ProjectKeys.BASEMAP_SOURCE_OSM)\n\n        assertThat(\n            mapFragmentFactoryImpl.createMapFragment(),\n            instanceOf(OsmDroidMapFragment::class.java)\n        )\n\n        // BASEMAP_SOURCE_USGS\n        settingsProvider\n            .getUnprotectedSettings()\n            .save(ProjectKeys.KEY_BASEMAP_SOURCE, ProjectKeys.BASEMAP_SOURCE_USGS)\n\n        assertThat(\n            mapFragmentFactoryImpl.createMapFragment(),\n            instanceOf(OsmDroidMapFragment::class.java)\n        )\n\n        // BASEMAP_SOURCE_CARTO\n        settingsProvider\n            .getUnprotectedSettings()\n            .save(ProjectKeys.KEY_BASEMAP_SOURCE, ProjectKeys.BASEMAP_SOURCE_CARTO)\n\n        assertThat(\n            mapFragmentFactoryImpl.createMapFragment(),\n            instanceOf(OsmDroidMapFragment::class.java)\n        )\n    }\n\n    @Test\n    fun `GoogleMapFragment should be return if Google Maps selected in settings`() {\n        settingsProvider\n            .getUnprotectedSettings()\n            .save(ProjectKeys.KEY_BASEMAP_SOURCE, ProjectKeys.BASEMAP_SOURCE_GOOGLE)\n\n        assertThat(\n            mapFragmentFactoryImpl.createMapFragment(),\n            instanceOf(GoogleMapFragment::class.java)\n        )\n    }\n\n    @Test\n    fun `GoogleMapFragment should be return if corresponding value stored in settings is unsupported`() {\n        settingsProvider\n            .getUnprotectedSettings()\n            .save(ProjectKeys.KEY_BASEMAP_SOURCE, \"Blah\")\n\n        assertThat(\n            mapFragmentFactoryImpl.createMapFragment(),\n            instanceOf(GoogleMapFragment::class.java)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/instancemanagement/InstanceDeleterTest.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Assert.assertNotNull\nimport org.junit.Assert.assertTrue\nimport org.junit.Test\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.FormUtils.createXFormFile\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.formstest.InstanceUtils.buildInstance\nimport org.odk.collect.shared.TempFiles.createTempDir\n\nclass InstanceDeleterTest {\n    private val formsRepository = InMemFormsRepository()\n    private val instancesRepository = InMemInstancesRepository()\n    private val instanceDeleter = InstanceDeleter(instancesRepository, formsRepository)\n\n    @Test\n    fun `Soft-delete instance if it is submitted`() {\n        val instance = instancesRepository.save(\n            buildInstance(\"1\", \"version\", createTempDir().absolutePath)\n                .status(Instance.STATUS_SUBMITTED)\n                .build()\n        )\n\n        instanceDeleter.delete(instance.dbId)\n\n        assertNotNull(instancesRepository[instance.dbId]!!.deletedDate)\n    }\n\n    @Test\n    fun `Delete corresponding form when it is soft-deleted and there are no other instances`() {\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"1\")\n                .version(\"version\")\n                .deleted(true)\n                .formFilePath(createXFormFile(\"1\", \"version\").absolutePath)\n                .build()\n        )\n\n        val instanceToDelete = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"version\")\n                .instanceFilePath(createTempDir().absolutePath)\n                .build()\n        )\n\n        instanceDeleter.delete(instanceToDelete.dbId)\n\n        assertTrue(formsRepository.all.isEmpty())\n    }\n\n    @Test\n    fun `Do not delete corresponding form when it is soft-deleted and there and there are other instances`() {\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"1\")\n                .version(\"version\")\n                .deleted(true)\n                .formFilePath(createXFormFile(\"1\", \"version\").absolutePath)\n                .build()\n        )\n\n        instancesRepository.save(\n            buildInstance(\n                \"1\",\n                \"version\",\n                createTempDir().absolutePath\n            ).build()\n        )\n\n        instancesRepository.save(\n            buildInstance(\n                \"1\",\n                \"version\",\n                createTempDir().absolutePath\n            ).build()\n        )\n\n        val id = instancesRepository.all[0].dbId\n        instanceDeleter.delete(id)\n        assertThat(formsRepository.all.size, equalTo(1))\n    }\n\n    @Test\n    fun `Delete corresponding form when it is soft-deleted and there are no other instances with the same version`() {\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"1\")\n                .version(\"1\")\n                .deleted(true)\n                .formFilePath(createXFormFile(\"1\", \"1\").absolutePath)\n                .build()\n        )\n\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"1\")\n                .version(\"2\")\n                .formFilePath(createXFormFile(\"1\", \"2\").absolutePath)\n                .deleted(true)\n                .build()\n        )\n\n        val instanceToDelete = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(createTempDir().absolutePath)\n                .build()\n        )\n\n        instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"2\")\n                .instanceFilePath(createTempDir().absolutePath)\n                .build()\n        )\n\n        instanceDeleter.delete(instanceToDelete.dbId)\n\n        assertThat(formsRepository.all.size, equalTo(1))\n        assertThat(formsRepository.all[0].version, equalTo(\"2\"))\n    }\n\n    @Test\n    fun `Delete corresponding form when it is soft-deleted and there are another deleted instances`() {\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"1\")\n                .version(\"version\")\n                .deleted(true)\n                .formFilePath(createXFormFile(\"1\", \"version\").absolutePath)\n                .build()\n        )\n\n        instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .deletedDate(0L)\n                .formVersion(\"version\")\n                .build()\n        )\n\n        val instanceToDelete = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"version\")\n                .instanceFilePath(createTempDir().absolutePath)\n                .build()\n        )\n\n        instanceDeleter.delete(instanceToDelete.dbId)\n        assertThat(formsRepository.all.size, equalTo(0))\n    }\n\n    @Test\n    fun `Do not delete corresponding form when it is not soft-deleted`() {\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"1\")\n                .version(\"version\")\n                .deleted(false)\n                .formFilePath(createXFormFile(\"1\", \"version\").absolutePath)\n                .build()\n        )\n\n        val instanceToDelete = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"version\")\n                .instanceFilePath(createTempDir().absolutePath)\n                .build()\n        )\n\n        instanceDeleter.delete(instanceToDelete.dbId)\n        assertThat(formsRepository.all.size, equalTo(1))\n    }\n\n    @Test\n    fun `Do not delete corresponding form when its version is not soft-deleted`() {\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"1\")\n                .version(\"1\")\n                .deleted(true)\n                .formFilePath(createXFormFile(\"1\", \"1\").absolutePath)\n                .build()\n        )\n\n        formsRepository.save(\n            Form.Builder()\n                .formId(\"1\")\n                .version(\"2\")\n                .deleted(false)\n                .formFilePath(createXFormFile(\"1\", \"2\").absolutePath)\n                .build()\n        )\n\n        val instanceToDelete = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"2\")\n                .instanceFilePath(createTempDir().absolutePath)\n                .build()\n        )\n\n        instanceDeleter.delete(instanceToDelete.dbId)\n        assertThat(formsRepository.all.size, equalTo(2))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/instancemanagement/InstanceExtKtTest.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.InstanceFixtures\nimport org.odk.collect.strings.R\nimport java.text.SimpleDateFormat\nimport java.util.Locale\n\n@RunWith(AndroidJUnit4::class)\nclass InstanceExtKtTest {\n\n    private val resources = ApplicationProvider.getApplicationContext<Context>().resources\n\n    @Test\n    fun getStatusDescriptionTest() {\n        val incomplete = InstanceFixtures.instance(status = Instance.STATUS_INCOMPLETE)\n        assertDateFormat(\n            incomplete.getStatusDescription(resources),\n            R.string.saved_on_date_at_time\n        )\n\n        val invalid = InstanceFixtures.instance(status = Instance.STATUS_INVALID)\n        assertDateFormat(\n            invalid.getStatusDescription(resources),\n            R.string.saved_on_date_at_time\n        )\n\n        val valid = InstanceFixtures.instance(status = Instance.STATUS_VALID)\n        assertDateFormat(\n            valid.getStatusDescription(resources),\n            R.string.saved_on_date_at_time\n        )\n\n        val newEdit = InstanceFixtures.instance(status = Instance.STATUS_NEW_EDIT)\n        assertDateFormat(\n            newEdit.getStatusDescription(resources),\n            R.string.saved_on_date_at_time\n        )\n\n        val complete = InstanceFixtures.instance(status = Instance.STATUS_COMPLETE)\n        assertDateFormat(\n            complete.getStatusDescription(resources),\n            R.string.finalized_on_date_at_time\n        )\n\n        val submitted = InstanceFixtures.instance(status = Instance.STATUS_SUBMITTED)\n        assertDateFormat(\n            submitted.getStatusDescription(resources),\n            R.string.sent_on_date_at_time\n        )\n\n        val submissionFailed = InstanceFixtures.instance(status = Instance.STATUS_SUBMISSION_FAILED)\n        assertDateFormat(\n            submissionFailed.getStatusDescription(resources),\n            R.string.sending_failed_on_date_at_time\n        )\n    }\n\n    @Test\n    fun `isDeletable returns true if canDeleteBeforeSend is true no matter what the status is`() {\n        listOf(\n            Instance.STATUS_INCOMPLETE,\n            Instance.STATUS_INVALID,\n            Instance.STATUS_VALID,\n            Instance.STATUS_NEW_EDIT,\n            Instance.STATUS_COMPLETE,\n            Instance.STATUS_SUBMISSION_FAILED,\n            Instance.STATUS_SUBMITTED\n        ).forEach { status ->\n            val instance = Instance\n                .Builder()\n                .status(status)\n                .canDeleteBeforeSend(true)\n                .build()\n\n            assertThat(instance.isDeletable(), equalTo(true))\n        }\n    }\n\n    @Test\n    fun `isDeletable returns true if canDeleteBeforeSend is false but form is not finalized or finalized but sent`() {\n        listOf(\n            Instance.STATUS_INCOMPLETE,\n            Instance.STATUS_INVALID,\n            Instance.STATUS_VALID,\n            Instance.STATUS_NEW_EDIT,\n            Instance.STATUS_SUBMITTED\n        ).forEach { status ->\n            val instance = Instance\n                .Builder()\n                .status(status)\n                .canDeleteBeforeSend(false)\n                .build()\n\n            assertThat(instance.isDeletable(), equalTo(true))\n        }\n    }\n\n    @Test\n    fun `isDeletable returns false if canDeleteBeforeSend is false but form is finalized`() {\n        listOf(\n            Instance.STATUS_COMPLETE,\n            Instance.STATUS_SUBMISSION_FAILED\n        ).forEach { status ->\n            val instance = Instance\n                .Builder()\n                .status(status)\n                .canDeleteBeforeSend(false)\n                .build()\n\n            assertThat(instance.isDeletable(), equalTo(false))\n        }\n    }\n\n    private fun assertDateFormat(description: String, stringId: Int) {\n        assertThat(\n            description,\n            equalTo(\n                SimpleDateFormat(\n                    resources.getString(stringId),\n                    Locale.getDefault()\n                ).format(0)\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/instancemanagement/InstanceListItemViewTest.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.android.databinding.FormChooserListItemBinding\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.InstanceFixtures\n\n@RunWith(AndroidJUnit4::class)\nclass InstanceListItemViewTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n    private val layoutInflater = LayoutInflater.from(context)\n\n    @Before\n    fun setup() {\n        context.setTheme(R.style.Theme_Collect)\n    }\n\n    @Test\n    fun `show an error chip if the status is STATUS_INVALID`() {\n        val binding = FormChooserListItemBinding.inflate(layoutInflater)\n        val instance = InstanceFixtures.instance(status = Instance.STATUS_INVALID)\n\n        InstanceListItemView.setInstance(binding.root, instance, false)\n\n        assertThat(binding.chip.visibility, equalTo(View.VISIBLE))\n        assertThat(binding.chip.errors, equalTo(true))\n    }\n\n    @Test\n    fun `show a no-error chip if the status is STATUS_VALID`() {\n        val binding = FormChooserListItemBinding.inflate(layoutInflater)\n        val instance = InstanceFixtures.instance(status = Instance.STATUS_VALID)\n\n        InstanceListItemView.setInstance(binding.root, instance, false)\n\n        assertThat(binding.chip.visibility, equalTo(View.VISIBLE))\n        assertThat(binding.chip.errors, equalTo(false))\n    }\n\n    @Test\n    fun `show a no-error chip if the status is STATUS_NEW_EDIT`() {\n        val binding = FormChooserListItemBinding.inflate(layoutInflater)\n        val instance = InstanceFixtures.instance(status = Instance.STATUS_NEW_EDIT)\n\n        InstanceListItemView.setInstance(binding.root, instance, false)\n\n        assertThat(binding.chip.visibility, equalTo(View.VISIBLE))\n        assertThat(binding.chip.errors, equalTo(false))\n    }\n\n    @Test\n    fun `show an error chip if the status is STATUS_INCOMPLETE`() {\n        val binding = FormChooserListItemBinding.inflate(layoutInflater)\n        val instance = InstanceFixtures.instance(status = Instance.STATUS_INCOMPLETE)\n\n        InstanceListItemView.setInstance(binding.root, instance, false)\n\n        assertThat(binding.chip.visibility, equalTo(View.VISIBLE))\n        assertThat(binding.chip.errors, equalTo(true))\n    }\n\n    @Test\n    fun `do not show a chip if the status is STATUS_COMPLETE`() {\n        val binding = FormChooserListItemBinding.inflate(layoutInflater)\n        val instance = InstanceFixtures.instance(status = Instance.STATUS_COMPLETE)\n\n        InstanceListItemView.setInstance(binding.root, instance, false)\n\n        assertThat(binding.chip.visibility, equalTo(View.GONE))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/instancemanagement/InstancesDataServiceTest.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verifyNoInteractions\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.notifications.Notifier\nimport org.odk.collect.android.projects.ProjectDependencyModule\nimport org.odk.collect.android.utilities.ChangeLocks\nimport org.odk.collect.androidshared.data.AppState\nimport org.odk.collect.forms.instances.Instance.STATUS_COMPLETE\nimport org.odk.collect.forms.instances.Instance.STATUS_INCOMPLETE\nimport org.odk.collect.forms.instances.Instance.STATUS_INVALID\nimport org.odk.collect.forms.instances.Instance.STATUS_NEW_EDIT\nimport org.odk.collect.forms.instances.Instance.STATUS_SUBMISSION_FAILED\nimport org.odk.collect.forms.instances.Instance.STATUS_SUBMITTED\nimport org.odk.collect.forms.instances.Instance.STATUS_VALID\nimport org.odk.collect.formstest.FormFixtures\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.formstest.InstanceFixtures\nimport org.odk.collect.openrosa.http.HttpGetResult\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface\nimport org.odk.collect.projects.ProjectDependencyFactory\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.locks.BooleanChangeLock\nimport org.odk.collect.shared.settings.InMemSettings\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass InstancesDataServiceTest {\n    private val projectsDependencyModuleFactory = CachingProjectDependencyModuleFactory { projectId ->\n        ProjectDependencyModule(\n            projectId,\n            {\n                InMemSettings().also {\n                    it.save(ProjectKeys.KEY_SERVER_URL, \"http://example.com\")\n                }\n            },\n            { InMemFormsRepository() },\n            { InMemInstancesRepository() },\n            mock(),\n            { ChangeLocks(BooleanChangeLock(), BooleanChangeLock()) },\n            mock(),\n            mock(),\n            mock(),\n            mock(),\n            mock()\n        )\n    }\n\n    private val projectId = \"projectId\"\n    private val projectDependencyModule = projectsDependencyModuleFactory.create(projectId)\n    private val httpInterface = mock<OpenRosaHttpInterface>()\n    private val notifier = mock<Notifier>()\n\n    private val instancesDataService =\n        InstancesDataService(\n            AppState(),\n            mock(),\n            projectsDependencyModuleFactory,\n            notifier,\n            mock(),\n            httpInterface,\n            mock()\n        )\n\n    @Test\n    fun `instances should not be deleted if the instances database is locked`() {\n        (projectDependencyModule.instancesLock as BooleanChangeLock).lock(\"blah\")\n        val result = instancesDataService.deleteInstances(projectId, longArrayOf(1))\n        assertThat(result, equalTo(false))\n    }\n\n    @Test\n    fun `instances should be deleted if the instances database is not locked`() {\n        val result = instancesDataService.deleteInstances(projectId, longArrayOf(1))\n        assertThat(result, equalTo(true))\n    }\n\n    @Test\n    fun `sendInstances() returns true when there are no instances to send`() {\n        val result = instancesDataService.sendInstances(projectId)\n        assertThat(result, equalTo(true))\n    }\n\n    @Test\n    fun `sendInstances() does not notify when there are no instances to send`() {\n        instancesDataService.sendInstances(projectId)\n        verifyNoInteractions(notifier)\n    }\n\n    @Test\n    fun `sendInstances() returns false when an instance fails to send`() {\n        val formsRepository = projectDependencyModule.formsRepository\n        val form = formsRepository.save(FormFixtures.form())\n\n        val instancesRepository = projectDependencyModule.instancesRepository\n        instancesRepository.save(InstanceFixtures.instance(form = form, status = STATUS_COMPLETE))\n\n        whenever(httpInterface.executeGetRequest(any(), any(), any()))\n            .doReturn(\n                HttpGetResult(\n                    null,\n                    emptyMap(),\n                    \"\",\n                    500\n                )\n            )\n\n        val result = instancesDataService.sendInstances(projectId)\n        assertThat(result, equalTo(false))\n    }\n\n    @Test\n    fun `#reset does not reset instances that can't be deleted before sending`() {\n        val formsRepository = projectDependencyModule.formsRepository\n        val form = formsRepository.save(FormFixtures.form())\n\n        val instancesRepository = projectDependencyModule.instancesRepository\n        instancesRepository.save(\n            InstanceFixtures.instance(\n                form = form,\n                canDeleteBeforeSend = false,\n                status = STATUS_INCOMPLETE\n            )\n        )\n        instancesRepository.save(\n            InstanceFixtures.instance(\n                form = form,\n                canDeleteBeforeSend = false,\n                status = STATUS_COMPLETE\n            )\n        )\n        instancesRepository.save(\n            InstanceFixtures.instance(\n                form = form,\n                canDeleteBeforeSend = false,\n                status = STATUS_INVALID\n            )\n        )\n        instancesRepository.save(\n            InstanceFixtures.instance(\n                form = form,\n                canDeleteBeforeSend = false,\n                status = STATUS_VALID\n            )\n        )\n        instancesRepository.save(\n            InstanceFixtures.instance(\n                form = form,\n                canDeleteBeforeSend = false,\n                status = STATUS_NEW_EDIT\n            )\n        )\n        instancesRepository.save(\n            InstanceFixtures.instance(\n                form = form,\n                canDeleteBeforeSend = false,\n                status = STATUS_SUBMITTED\n            )\n        )\n        instancesRepository.save(\n            InstanceFixtures.instance(\n                form = form,\n                canDeleteBeforeSend = false,\n                status = STATUS_SUBMISSION_FAILED\n            )\n        )\n\n        instancesDataService.reset(projectDependencyModule.projectId)\n        val remainingInstances = instancesRepository.all\n        assertThat(remainingInstances.size, equalTo(2))\n        assertThat(remainingInstances.any { it.status == STATUS_COMPLETE }, equalTo(true))\n        assertThat(remainingInstances.any { it.status == STATUS_SUBMISSION_FAILED }, equalTo(true))\n        assertThat(File(remainingInstances[0].instanceFilePath).parentFile?.exists(), equalTo(true))\n        assertThat(File(remainingInstances[1].instanceFilePath).parentFile?.exists(), equalTo(true))\n    }\n\n    @Test\n    fun `#reset can delete forms with edits`() {\n        val formsRepository = projectDependencyModule.formsRepository\n        val form = formsRepository.save(FormFixtures.form())\n\n        val instancesRepository = projectDependencyModule.instancesRepository\n        val originalInstance = instancesRepository.save(\n            InstanceFixtures.instance(\n                form = form,\n                status = STATUS_COMPLETE,\n                lastStatusChangeDate = 1\n            )\n        )\n        instancesRepository.save(\n            InstanceFixtures.instance(\n                form = form,\n                status = STATUS_COMPLETE,\n                lastStatusChangeDate = 2,\n                editOf = originalInstance.dbId,\n                editNumber = 1\n            )\n        )\n        instancesRepository.save(\n            InstanceFixtures.instance(\n                form = form,\n                status = STATUS_VALID,\n                lastStatusChangeDate = 3,\n                editOf = originalInstance.dbId,\n                editNumber = 2\n            )\n        )\n\n        instancesDataService.reset(projectDependencyModule.projectId)\n        val remainingInstances = instancesRepository.all\n        assertThat(remainingInstances.size, equalTo(0))\n    }\n\n    @Test\n    fun `#update updates instances and counts`() {\n        val instancesRepository = projectDependencyModule.instancesRepository\n        instancesRepository.save(InstanceFixtures.instance(status = STATUS_COMPLETE))\n        instancesRepository.save(InstanceFixtures.instance(status = STATUS_SUBMITTED))\n        instancesRepository.save(InstanceFixtures.instance(status = STATUS_INCOMPLETE))\n        instancesRepository.save(InstanceFixtures.instance(status = STATUS_SUBMISSION_FAILED))\n\n        instancesDataService.update(projectId)\n        assertThat(\n            instancesDataService.getInstances(projectId).value,\n            equalTo(instancesRepository.all)\n        )\n        assertThat(instancesDataService.getSentCount(projectId).value, equalTo(2))\n        assertThat(instancesDataService.getSuccessfullySentCount(projectId).value, equalTo(1))\n        assertThat(instancesDataService.getEditableCount(projectId).value, equalTo(1))\n        assertThat(instancesDataService.getSendableCount(projectId).value, equalTo(2))\n        assertThat(instancesDataService.getInstances(\"otherProjectId\").value, equalTo(emptyList()))\n        assertThat(instancesDataService.getSentCount(\"otherProjectId\").value, equalTo(0))\n        assertThat(instancesDataService.getSuccessfullySentCount(\"otherProjectId\").value, equalTo(0))\n        assertThat(instancesDataService.getEditableCount(\"otherProjectId\").value, equalTo(0))\n        assertThat(instancesDataService.getSendableCount(\"otherProjectId\").value, equalTo(0))\n    }\n}\n\nclass CachingProjectDependencyModuleFactory(private val moduleFactory: (String) -> ProjectDependencyModule) :\n    ProjectDependencyFactory<ProjectDependencyModule> {\n\n    private val modules = mutableMapOf<String, ProjectDependencyModule>()\n\n    override fun create(projectId: String): ProjectDependencyModule {\n        return modules.getOrPut(projectId) {\n            moduleFactory(projectId)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/instancemanagement/LocalInstancesUseCasesTest.kt",
    "content": "package org.odk.collect.android.instancemanagement\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.junit.Test\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.FormFixtures\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.formstest.InstanceFixtures\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.shared.strings.Md5.getMd5Hash\nimport java.io.File\nimport java.util.TimeZone\nimport kotlin.random.Random\n\nclass LocalInstancesUseCasesTest {\n    @Test\n    fun `#createInstanceFile creates directory based on sanitized form name and current time in instances directory`() {\n        val instancesDirPath = TempFiles.createTempDir().absolutePath\n\n        LocalInstancesUseCases.createInstanceFile(\n            \"Cool form  name:\",\n            instancesDirPath,\n            TimeZone.getTimeZone(\"UTC\")\n        ) { 640915200000 }\n\n        val instanceDir = File(instancesDirPath + File.separator + \"Cool form name_1990-04-24_00-00-00\")\n        assertThat(instanceDir.exists(), equalTo(true))\n        assertThat(instanceDir.isDirectory, equalTo(true))\n    }\n\n    @Test\n    fun `#createInstanceFile returns instance file in instance directory`() {\n        val instancesDirPath = TempFiles.createTempDir().absolutePath\n\n        val instanceFile = LocalInstancesUseCases.createInstanceFile(\n            \"Cool form name\",\n            instancesDirPath,\n            TimeZone.getTimeZone(\"UTC\")\n        ) { 640915200000 }!!\n\n        val instanceDir = instancesDirPath + File.separator + \"Cool form name_1990-04-24_00-00-00\"\n        assertThat(\n            instanceFile.absolutePath,\n            equalTo(instanceDir + File.separator + \"Cool form name_1990-04-24_00-00-00.xml\")\n        )\n    }\n\n    @Test\n    fun `#editInstance makes a proper copy of the instance dir`() {\n        val formsRepository = InMemFormsRepository()\n        val instancesRepository = InMemInstancesRepository()\n        val instancesDir = TempFiles.createTempDir()\n\n        val form = formsRepository.save(FormFixtures.form())\n        val sourceInstance = instancesRepository.save(\n            InstanceFixtures.instance(\n                instancesDir = instancesDir,\n                formId = form.formId,\n                formVersion = form.version!!\n            )\n        )\n        val sourceInstanceFile = File(sourceInstance.instanceFilePath)\n        val sourceInstanceFileMd5Hash = sourceInstanceFile.getMd5Hash()\n\n        val sourceInstanceDir = File(sourceInstanceFile.parent!!)\n\n        val sourceMediaFile1 = TempFiles.createTempFile(sourceInstanceDir, \"foo\", \".jpg\").also {\n            it.writeText(\"foo\")\n        }\n        val sourceMediaFile1Md5Hash = sourceMediaFile1.getMd5Hash()\n\n        val sourceMediaFile2 = TempFiles.createTempFile(sourceInstanceDir, \"bar\", \".mp4\").also {\n            it.writeText(\"bar\")\n        }\n        val sourceMediaFile2Md5Hash = sourceMediaFile2.getMd5Hash()\n\n        val editedInstance = LocalInstancesUseCases.editInstance(\n            sourceInstanceFile.absolutePath,\n            instancesDir.absolutePath,\n            instancesRepository,\n            formsRepository\n        ) { Random.nextLong() }.instance\n\n        val editedInstanceFile = File(editedInstance.instanceFilePath)\n        val editedInstanceDir = File(editedInstanceFile.parent!!)\n\n        val sourceFiles = sourceInstanceDir.listFiles()!!\n        val editedFiles = editedInstanceDir.listFiles()!!\n\n        assertThat(sourceFiles.size, equalTo(3))\n        assertThat(editedFiles.size, equalTo(3))\n        assertThat(sourceInstanceDir.absolutePath, not(editedInstanceDir.absolutePath))\n        assertThat(sourceInstanceFile.name, not(editedInstanceFile.name))\n\n        assertThat(sourceInstanceFile.exists(), equalTo(true))\n        assertThat(editedInstanceFile.exists(), equalTo(true))\n        assertThat(sourceInstanceFileMd5Hash, equalTo(editedInstanceFile.getMd5Hash()))\n\n        val editedMediaFile1 = editedFiles.find { it.name == sourceMediaFile1.name }!!\n        assertThat(sourceMediaFile1.exists(), equalTo(true))\n        assertThat(editedMediaFile1.exists(), equalTo(true))\n        assertThat(sourceMediaFile1Md5Hash, equalTo(editedMediaFile1.getMd5Hash()))\n\n        val editedMediaFile2 = editedFiles.find { it.name == sourceMediaFile2.name }!!\n        assertThat(sourceMediaFile2.exists(), equalTo(true))\n        assertThat(editedMediaFile2.exists(), equalTo(true))\n        assertThat(sourceMediaFile2Md5Hash, equalTo(editedMediaFile2.getMd5Hash()))\n    }\n\n    @Test\n    fun `#editInstance makes a proper copy of the instance row in the database`() {\n        val formsRepository = InMemFormsRepository()\n        val instancesRepository = InMemInstancesRepository()\n        val instancesDir = TempFiles.createTempDir()\n\n        val form = formsRepository.save(FormFixtures.form())\n        val sourceInstance = instancesRepository.save(\n            InstanceFixtures.instance(\n                instancesDir = instancesDir,\n                status = Instance.STATUS_SUBMITTED,\n                formId = form.formId,\n                formVersion = form.version!!\n            )\n        )\n\n        // The first edit\n        val firstEditedInstance = LocalInstancesUseCases.editInstance(\n            sourceInstance.instanceFilePath,\n            instancesDir.absolutePath,\n            instancesRepository,\n            formsRepository\n        ) { Random.nextLong() }.instance\n\n        assertThat(instancesRepository.all.size, equalTo(2))\n        assertThat(sourceInstance, equalTo(instancesRepository.get(sourceInstance.dbId)))\n        assertThat(sourceInstance, not(firstEditedInstance))\n        assertThat(firstEditedInstance.status, equalTo(Instance.STATUS_NEW_EDIT))\n        assertThat(sourceInstance.instanceFilePath, not(firstEditedInstance.instanceFilePath))\n        assertThat(firstEditedInstance.editOf, equalTo(sourceInstance.dbId))\n        assertThat(firstEditedInstance.editNumber, equalTo(1))\n\n        // The second edit\n        val secondEditedInstance = LocalInstancesUseCases.editInstance(\n            firstEditedInstance.instanceFilePath,\n            instancesDir.absolutePath,\n            instancesRepository,\n            formsRepository\n        ) { Random.nextLong() }.instance\n\n        assertThat(instancesRepository.all.size, equalTo(3))\n        assertThat(secondEditedInstance.editOf, equalTo(sourceInstance.dbId))\n        assertThat(secondEditedInstance.editNumber, equalTo(2))\n    }\n\n    @Test\n    fun `#editInstance returns the newest edit instance when user attempts to edit an outdated one`() {\n        val formsRepository = InMemFormsRepository()\n        val instancesRepository = InMemInstancesRepository()\n        val instancesDir = TempFiles.createTempDir()\n\n        val form = formsRepository.save(FormFixtures.form())\n        val sourceInstance = instancesRepository.save(\n            InstanceFixtures.instance(\n                instancesDir = instancesDir,\n                formId = form.formId,\n                formVersion = form.version!!\n            )\n        )\n        val sourceInstanceFile = File(sourceInstance.instanceFilePath)\n        val editedInstance1 = instancesRepository.save(\n            InstanceFixtures.instance(\n                instancesDir = instancesDir,\n                formId = form.formId,\n                formVersion = form.version!!,\n                editOf = sourceInstance.dbId,\n                editNumber = 1\n            )\n        )\n        val editedInstance1File = File(editedInstance1.instanceFilePath)\n        val editedInstance2 = instancesRepository.save(\n            InstanceFixtures.instance(\n                instancesDir = instancesDir,\n                formId = form.formId,\n                formVersion = form.version!!,\n                editOf = sourceInstance.dbId,\n                editNumber = 2\n            )\n        )\n\n        var editResult = LocalInstancesUseCases.editInstance(\n            sourceInstanceFile.absolutePath,\n            instancesDir.absolutePath,\n            instancesRepository,\n            formsRepository\n        ) { Random.nextLong() }\n        assertThat(editResult.instance.dbId, equalTo(editedInstance2.dbId))\n\n        editResult = LocalInstancesUseCases.editInstance(\n            editedInstance1File.absolutePath,\n            instancesDir.absolutePath,\n            instancesRepository,\n            formsRepository\n        ) { Random.nextLong() }\n        assertThat(editResult.instance.dbId, equalTo(editedInstance2.dbId))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProviderTest.kt",
    "content": "package org.odk.collect.android.instancemanagement.autosend\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Assert.assertFalse\nimport org.junit.Assert.assertTrue\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.network.NetworkStateProvider\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.enums.AutoSend\nimport org.odk.collect.settings.keys.ProjectKeys\n\n@RunWith(AndroidJUnit4::class)\nclass AutoSendSettingsProviderTest {\n\n    private val networkStateProvider: NetworkStateProvider = mock()\n    private val settingsProvider = InMemSettingsProvider()\n    private val projectId = Project.DEMO_PROJECT_NAME\n    private val application = ApplicationProvider.getApplicationContext<Application>()\n\n    @Test\n    fun `return false when autosend is disabled in settings and network is not available`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.OFF.getValue(application),\n            networkType = null\n        )\n\n        assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return false when autosend is disabled in settings and network type is wifi`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.OFF.getValue(application),\n            networkType = Scheduler.NetworkType.WIFI\n        )\n\n        assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return false when autosend is disabled in settings and network type is cellular`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.OFF.getValue(application),\n            networkType = Scheduler.NetworkType.CELLULAR\n        )\n\n        assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return false when autosend is enabled for 'wifi_only' and network is not available`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.WIFI_ONLY.getValue(application),\n            networkType = null\n        )\n\n        assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return false when autosend is enabled for 'wifi_only' and network type is cellular`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.WIFI_ONLY.getValue(application),\n            networkType = Scheduler.NetworkType.CELLULAR\n        )\n\n        assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return true when autosend is enabled for 'wifi_only' and network type is wifi`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.WIFI_ONLY.getValue(application),\n            networkType = Scheduler.NetworkType.WIFI\n        )\n\n        assertTrue(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return false when autosend is enabled for 'cellular_only' and network is not available`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.CELLULAR_ONLY.getValue(application),\n            networkType = null\n        )\n\n        assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return false when autosend is enabled for 'cellular_only' and network type is wifi`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.CELLULAR_ONLY.getValue(application),\n            networkType = Scheduler.NetworkType.WIFI\n        )\n\n        assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return true when autosend is enabled for 'cellular_only' and network type is cellular`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.CELLULAR_ONLY.getValue(application),\n            networkType = Scheduler.NetworkType.CELLULAR\n        )\n\n        assertTrue(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return false when autosend is enabled for 'wifi_and_cellular' and network is not available`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.WIFI_AND_CELLULAR.getValue(application),\n            networkType = null\n        )\n\n        assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return true when autosend is enabled for 'wifi_and_cellular' and network type is wifi`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.WIFI_AND_CELLULAR.getValue(application),\n            networkType = Scheduler.NetworkType.WIFI\n        )\n\n        assertTrue(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    @Test\n    fun `return true when autosend is enabled for 'wifi_and_cellular' and network type is cellular`() {\n        val autoSendSettingsProvider = setupAutoSendSettingProvider(\n            autoSendOption = AutoSend.WIFI_AND_CELLULAR.getValue(application),\n            networkType = Scheduler.NetworkType.CELLULAR\n        )\n\n        assertTrue(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId))\n    }\n\n    private fun setupAutoSendSettingProvider(\n        autoSendOption: String? = null,\n        networkType: Scheduler.NetworkType? = null\n    ): AutoSendSettingsProvider {\n        whenever(networkStateProvider.currentNetwork).thenReturn(networkType)\n        settingsProvider.getUnprotectedSettings(projectId).save(ProjectKeys.KEY_AUTOSEND, autoSendOption)\n\n        return AutoSendSettingsProvider(application, networkStateProvider, settingsProvider)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/FormExtTest.kt",
    "content": "package org.odk.collect.android.instancemanagement.autosend\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.formstest.FormFixtures\n\nclass FormExtTest {\n    @Test\n    fun `#shouldFormBeSentAutomatically returns true when auto send is not set on a form level and enabled in settings`() {\n        val form = FormFixtures.form()\n        val result = form.shouldFormBeSentAutomatically(true)\n        assertThat(result, equalTo(true))\n    }\n\n    @Test\n    fun `#shouldFormBeSentAutomatically returns true when auto send is enabled on a form level and enabled in settings`() {\n        val form = FormFixtures.form(autoSend = \"true\")\n        val result = form.shouldFormBeSentAutomatically(true)\n        assertThat(result, equalTo(true))\n    }\n\n    @Test\n    fun `#shouldFormBeSentAutomatically returns true when auto send is enabled on a form level and disabled in settings`() {\n        val form = FormFixtures.form(autoSend = \"true\")\n        val result = form.shouldFormBeSentAutomatically(false)\n        assertThat(result, equalTo(true))\n    }\n\n    @Test\n    fun `#shouldFormBeSentAutomatically returns false when auto send is not set on a form level and disabled in settings`() {\n        val form = FormFixtures.form()\n        val result = form.shouldFormBeSentAutomatically(false)\n        assertThat(result, equalTo(false))\n    }\n\n    @Test\n    fun `#shouldFormBeSentAutomatically returns false when auto send is disabled on a form level and disabled in settings`() {\n        val form = FormFixtures.form(autoSend = \"false\")\n        val result = form.shouldFormBeSentAutomatically(false)\n        assertThat(result, equalTo(false))\n    }\n\n    @Test\n    fun `#shouldFormBeSentAutomatically returns false when auto send is disabled on a form level and enabled in settings`() {\n        val form = FormFixtures.form(autoSend = \"false\")\n        val result = form.shouldFormBeSentAutomatically(true)\n        assertThat(result, equalTo(false))\n    }\n\n    @Test\n    fun `#getAutoSendMode returns NEUTRAL when autoSend is unsupported`() {\n        val form = FormFixtures.form(autoSend = \"blah\")\n        assertThat(form.getAutoSendMode(), equalTo(FormAutoSendMode.NEUTRAL))\n    }\n\n    @Test\n    fun `#getAutoSendMode returns FORCED when autoSend is true but incorrectly cased`() {\n        val form = FormFixtures.form(autoSend = \"TRUE\")\n        assertThat(form.getAutoSendMode(), equalTo(FormAutoSendMode.FORCED))\n    }\n\n    @Test\n    fun `#getAutoSendMode returns FORCED when autoSend is true but with whitespace`() {\n        val form = FormFixtures.form(autoSend = \" true \")\n        assertThat(form.getAutoSendMode(), equalTo(FormAutoSendMode.FORCED))\n    }\n\n    @Test\n    fun `#getAutoSendMode returns OPT_OUT when autoSend is false but incorrectly cased`() {\n        val form = FormFixtures.form(autoSend = \"FALSE\")\n        assertThat(form.getAutoSendMode(), equalTo(FormAutoSendMode.OPT_OUT))\n    }\n\n    @Test\n    fun `#getAutoSendMode returns OPT_OUT when autoSend is false but with whitespace`() {\n        val form = FormFixtures.form(autoSend = \" false \")\n        assertThat(form.getAutoSendMode(), equalTo(FormAutoSendMode.OPT_OUT))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/InstanceAutoSendFetcherTest.kt",
    "content": "package org.odk.collect.android.instancemanagement.autosend\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.contains\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.FormUtils.buildForm\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.formstest.InstanceUtils.buildInstance\nimport org.odk.collect.shared.TempFiles.createTempDir\n\n@RunWith(AndroidJUnit4::class)\nclass InstanceAutoSendFetcherTest {\n\n    private val instancesRepository = InMemInstancesRepository()\n    private val formsRepository = InMemFormsRepository()\n\n    private val formWithEnabledAutoSend = buildForm(\"1\", \"1\", createTempDir().absolutePath, autosend = \"true\").build()\n\n    private val instanceOfFormWithEnabledAutoSendIncomplete = buildInstance(\"1\", \"1\", \"instance 1\", Instance.STATUS_INCOMPLETE, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithEnabledAutoSendComplete = buildInstance(\"1\", \"1\", \"instance 2\", Instance.STATUS_COMPLETE, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithEnabledAutoSendSubmissionFailed = buildInstance(\"1\", \"1\", \"instance 3\", Instance.STATUS_SUBMISSION_FAILED, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithEnabledAutoSendSubmitted = buildInstance(\"1\", \"1\", \"instance 4\", Instance.STATUS_SUBMITTED, null, createTempDir().absolutePath).build()\n\n    private val formWithoutSpecifiedAutoSend = buildForm(\"2\", \"1\", createTempDir().absolutePath).build()\n\n    private val instanceOfFormWithoutSpecifiedAutoSendIncomplete = buildInstance(\"2\", \"1\", \"instance 1\", Instance.STATUS_INCOMPLETE, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithoutSpecifiedAutoSendComplete = buildInstance(\"2\", \"1\", \"instance 2\", Instance.STATUS_COMPLETE, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithoutSpecifiedAutoSendSubmissionFailed = buildInstance(\"2\", \"1\", \"instance 3\", Instance.STATUS_SUBMISSION_FAILED, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithoutSpecifiedAutoSendSubmitted = buildInstance(\"2\", \"1\", \"instance 4\", Instance.STATUS_SUBMITTED, null, createTempDir().absolutePath).build()\n\n    private val formWithDisabledAutoSend = buildForm(\"3\", \"1\", createTempDir().absolutePath, autosend = \"false\").build()\n\n    private val instanceOfFormWithDisabledAutoSendIncomplete = buildInstance(\"3\", \"1\", \"instance 1\", Instance.STATUS_INCOMPLETE, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithDisabledAutoSendComplete = buildInstance(\"3\", \"1\", \"instance 2\", Instance.STATUS_COMPLETE, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithDisabledAutoSendSubmissionFailed = buildInstance(\"3\", \"1\", \"instance 3\", Instance.STATUS_SUBMISSION_FAILED, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithDisabledAutoSendSubmitted = buildInstance(\"3\", \"1\", \"instance 4\", Instance.STATUS_SUBMITTED, null, createTempDir().absolutePath).build()\n\n    private val formWithCustomAutoSend = buildForm(\"4\", \"1\", createTempDir().absolutePath, autosend = \"anything\").build()\n\n    private val instanceOfFormWithCustomAutoSendIncomplete = buildInstance(\"4\", \"1\", \"instance 1\", Instance.STATUS_INCOMPLETE, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithCustomAutoSendComplete = buildInstance(\"4\", \"1\", \"instance 2\", Instance.STATUS_COMPLETE, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithCustomAutoSendSubmissionFailed = buildInstance(\"4\", \"1\", \"instance 3\", Instance.STATUS_SUBMISSION_FAILED, null, createTempDir().absolutePath).build()\n    private val instanceOfFormWithCustomAutoSendSubmitted = buildInstance(\"4\", \"1\", \"instance 4\", Instance.STATUS_SUBMITTED, null, createTempDir().absolutePath).build()\n\n    @Test\n    fun `return all finalized instances of forms that do not have auto send on a form level`() {\n        formsRepository.save(formWithEnabledAutoSend)\n        formsRepository.save(formWithoutSpecifiedAutoSend)\n        formsRepository.save(formWithDisabledAutoSend)\n        formsRepository.save(formWithCustomAutoSend)\n\n        instancesRepository.apply {\n            save(instanceOfFormWithEnabledAutoSendIncomplete)\n            save(instanceOfFormWithEnabledAutoSendComplete)\n            save(instanceOfFormWithEnabledAutoSendSubmissionFailed)\n            save(instanceOfFormWithEnabledAutoSendSubmitted)\n\n            save(instanceOfFormWithoutSpecifiedAutoSendIncomplete)\n            save(instanceOfFormWithoutSpecifiedAutoSendComplete)\n            save(instanceOfFormWithoutSpecifiedAutoSendSubmissionFailed)\n            save(instanceOfFormWithoutSpecifiedAutoSendSubmitted)\n\n            save(instanceOfFormWithDisabledAutoSendIncomplete)\n            save(instanceOfFormWithDisabledAutoSendComplete)\n            save(instanceOfFormWithDisabledAutoSendSubmissionFailed)\n            save(instanceOfFormWithDisabledAutoSendSubmitted)\n\n            save(instanceOfFormWithCustomAutoSendIncomplete)\n            save(instanceOfFormWithCustomAutoSendComplete)\n            save(instanceOfFormWithCustomAutoSendSubmissionFailed)\n            save(instanceOfFormWithCustomAutoSendSubmitted)\n        }\n\n        val instancesToSend = InstanceAutoSendFetcher.getInstancesToAutoSend(\n            instancesRepository,\n            formsRepository\n        )\n\n        assertThat(\n            instancesToSend.map { it.instanceFilePath },\n            contains(\n                instanceOfFormWithoutSpecifiedAutoSendComplete.instanceFilePath,\n                instanceOfFormWithoutSpecifiedAutoSendSubmissionFailed.instanceFilePath,\n                instanceOfFormWithCustomAutoSendComplete.instanceFilePath,\n                instanceOfFormWithCustomAutoSendSubmissionFailed.instanceFilePath\n            )\n        )\n    }\n\n    @Test\n    fun `return all finalized forms with autosend when formAutoSend is true`() {\n        formsRepository.save(formWithEnabledAutoSend)\n        formsRepository.save(formWithoutSpecifiedAutoSend)\n        formsRepository.save(formWithDisabledAutoSend)\n        formsRepository.save(formWithCustomAutoSend)\n\n        instancesRepository.apply {\n            save(instanceOfFormWithEnabledAutoSendIncomplete)\n            save(instanceOfFormWithEnabledAutoSendComplete)\n            save(instanceOfFormWithEnabledAutoSendSubmissionFailed)\n            save(instanceOfFormWithEnabledAutoSendSubmitted)\n\n            save(instanceOfFormWithoutSpecifiedAutoSendIncomplete)\n            save(instanceOfFormWithoutSpecifiedAutoSendComplete)\n            save(instanceOfFormWithoutSpecifiedAutoSendSubmissionFailed)\n            save(instanceOfFormWithoutSpecifiedAutoSendSubmitted)\n\n            save(instanceOfFormWithDisabledAutoSendIncomplete)\n            save(instanceOfFormWithDisabledAutoSendComplete)\n            save(instanceOfFormWithDisabledAutoSendSubmissionFailed)\n            save(instanceOfFormWithDisabledAutoSendSubmitted)\n\n            save(instanceOfFormWithCustomAutoSendIncomplete)\n            save(instanceOfFormWithCustomAutoSendComplete)\n            save(instanceOfFormWithCustomAutoSendSubmissionFailed)\n            save(instanceOfFormWithCustomAutoSendSubmitted)\n        }\n\n        val instancesToSend = InstanceAutoSendFetcher.getInstancesToAutoSend(\n            instancesRepository,\n            formsRepository,\n            forcedOnly = true\n        )\n\n        assertThat(\n            instancesToSend.map { it.instanceFilePath },\n            contains(\n                instanceOfFormWithEnabledAutoSendComplete.instanceFilePath,\n                instanceOfFormWithEnabledAutoSendSubmissionFailed.instanceFilePath,\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendBannerTest.kt",
    "content": "package org.odk.collect.android.instancemanagement.send\n\nimport android.app.Application\nimport android.view.View\nimport android.widget.FrameLayout\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport com.google.android.material.textview.MaterialTextView\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.shared.TimeInMs\n\n@RunWith(AndroidJUnit4::class)\nclass ReadyToSendBannerTest {\n    private val context: Application =\n        ApplicationProvider.getApplicationContext<Application>().also {\n            it.setTheme(R.style.Theme_Collect)\n        }\n\n    @Test\n    fun `if there are no sent instances and no instances ready to send do not display the banner`() {\n        val data = ReadyToSendViewModel.Data(0, 0, 0)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(view.findViewById<FrameLayout>(R.id.banner).visibility, equalTo(View.GONE))\n    }\n\n    @Test\n    fun `if initially the banner is gone it should become visible when its data changes and there are both sent and ready to send instances`() {\n        var data = ReadyToSendViewModel.Data(0, 0, 0)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(view.findViewById<FrameLayout>(R.id.banner).visibility, equalTo(View.GONE))\n\n        data = ReadyToSendViewModel.Data(1, 1, 0)\n        view.setData(data)\n\n        assertThat(view.findViewById<FrameLayout>(R.id.banner).visibility, equalTo(View.VISIBLE))\n    }\n\n    @Test\n    fun `if initially the banner is visible it should become gone when its data changes and there are no both sent and ready to send instances`() {\n        var data = ReadyToSendViewModel.Data(1, 1, 0)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(view.findViewById<FrameLayout>(R.id.banner).visibility, equalTo(View.VISIBLE))\n\n        data = ReadyToSendViewModel.Data(0, 0, 0)\n        view.setData(data)\n\n        assertThat(view.findViewById<FrameLayout>(R.id.banner).visibility, equalTo(View.GONE))\n    }\n\n    @Test\n    fun `if there are sent instances but no instances ready to send do not display the banner`() {\n        val data = ReadyToSendViewModel.Data(0, 1, 0)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(view.findViewById<FrameLayout>(R.id.banner).visibility, equalTo(View.GONE))\n    }\n\n    @Test\n    fun `if there are instances ready to send but no sent instances do not display the banner`() {\n        val data = ReadyToSendViewModel.Data(1, 0, 0)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(view.findViewById<FrameLayout>(R.id.banner).visibility, equalTo(View.GONE))\n    }\n\n    @Test\n    fun `if there are both sent and ready to send instances display the banner`() {\n        val data = ReadyToSendViewModel.Data(1, 1, 0)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(\n            view.findViewById<FrameLayout>(R.id.banner).visibility,\n            equalTo(View.VISIBLE)\n        )\n    }\n\n    @Test\n    fun `the banner should display how long ago in seconds the last instance was sent if it was less than a minute ago`() {\n        val data = ReadyToSendViewModel.Data(1, 1, TimeInMs.ONE_SECOND * 5)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(\n            view.findViewById<MaterialTextView>(R.id.title).text,\n            equalTo(context.resources.getQuantityString(org.odk.collect.strings.R.plurals.last_form_sent_seconds_ago, 5, 5))\n        )\n    }\n\n    @Test\n    fun `the banner should display how long ago in minutes the last instance was sent if it was less than an hour ago`() {\n        val data = ReadyToSendViewModel.Data(1, 1, TimeInMs.ONE_MINUTE)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(\n            view.findViewById<MaterialTextView>(R.id.title).text,\n            equalTo(context.resources.getQuantityString(org.odk.collect.strings.R.plurals.last_form_sent_minutes_ago, 1, 1))\n        )\n    }\n\n    @Test\n    fun `the banner should display how long ago in hours the last instance was sent if it was less than a day ago`() {\n        val data = ReadyToSendViewModel.Data(1, 1, TimeInMs.ONE_HOUR * 2)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(\n            view.findViewById<MaterialTextView>(R.id.title).text,\n            equalTo(context.resources.getQuantityString(org.odk.collect.strings.R.plurals.last_form_sent_hours_ago, 2, 2))\n        )\n    }\n\n    @Test\n    fun `the banner should display how long ago in days the last instance was sent if it was more than 24 hours ago`() {\n        val data = ReadyToSendViewModel.Data(1, 1, TimeInMs.ONE_DAY * 34)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(\n            view.findViewById<MaterialTextView>(R.id.title).text,\n            equalTo(context.resources.getQuantityString(org.odk.collect.strings.R.plurals.last_form_sent_days_ago, 34, 34))\n        )\n    }\n\n    @Test\n    fun `the banner should display the number of instances ready to send`() {\n        val data = ReadyToSendViewModel.Data(3, 1, 0)\n        val view = ReadyToSendBanner(context).also {\n            it.setData(data)\n        }\n\n        assertThat(\n            view.findViewById<MaterialTextView>(R.id.subtext).text,\n            equalTo(context.resources.getQuantityString(org.odk.collect.strings.R.plurals.forms_ready_to_send, 3, 3))\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/instancemanagement/send/ReadyToSendViewModelTest.kt",
    "content": "package org.odk.collect.android.instancemanagement.send\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.shared.TimeInMs\nimport org.odk.collect.testshared.FakeScheduler\n\nclass ReadyToSendViewModelTest {\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    private var instancesRepository = InMemInstancesRepository().also {\n        it.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .status(Instance.STATUS_INCOMPLETE)\n                .lastStatusChangeDate(TimeInMs.ONE_SECOND * 1)\n                .build()\n        )\n\n        it.save(\n            Instance.Builder()\n                .formId(\"2\")\n                .status(Instance.STATUS_COMPLETE)\n                .lastStatusChangeDate(TimeInMs.ONE_SECOND * 5)\n                .build()\n        )\n\n        it.save(\n            Instance.Builder()\n                .formId(\"3\")\n                .status(Instance.STATUS_SUBMITTED)\n                .lastStatusChangeDate(TimeInMs.ONE_SECOND * 4)\n                .build()\n        )\n\n        it.save(\n            Instance.Builder()\n                .formId(\"4\")\n                .status(Instance.STATUS_SUBMITTED)\n                .lastStatusChangeDate(TimeInMs.ONE_SECOND * 6)\n                .build()\n        )\n\n        it.save(\n            Instance.Builder()\n                .formId(\"5\")\n                .status(Instance.STATUS_SUBMISSION_FAILED)\n                .lastStatusChangeDate(TimeInMs.ONE_SECOND * 7)\n                .build()\n        )\n\n        it.save(\n            Instance.Builder()\n                .formId(\"6\")\n                .status(Instance.STATUS_COMPLETE)\n                .lastStatusChangeDate(TimeInMs.ONE_SECOND * 3)\n                .build()\n        )\n\n        it.save(\n            Instance.Builder()\n                .formId(\"7\")\n                .status(Instance.STATUS_COMPLETE)\n                .lastStatusChangeDate(TimeInMs.ONE_SECOND * 10)\n                .build()\n        )\n    }\n    private val scheduler = FakeScheduler()\n\n    private val viewModel = ReadyToSendViewModel(instancesRepository, scheduler) { TimeInMs.ONE_SECOND * 10 }.apply {\n        update()\n    }\n\n    @Test\n    fun `numberOfSentInstances should represent the real number of instances with STATUS_SUBMITTED in the database`() {\n        scheduler.flush()\n        assertThat(viewModel.data.value!!.numberOfSentInstances, equalTo(2))\n    }\n\n    @Test\n    fun `numberOfInstancesReadyToSend should represent the real number of instances with STATUS_COMPLETE and STATUS_SUBMISSION_FAILED in the database`() {\n        scheduler.flush()\n        assertThat(viewModel.data.value!!.numberOfInstancesReadyToSend, equalTo(4))\n    }\n\n    @Test\n    fun `lastInstanceSentTimeMillis should be correctly calculate when the last instance has been sent`() {\n        scheduler.flush()\n        assertThat(viewModel.data.value!!.lastInstanceSentTimeMillis, equalTo(4000L))\n    }\n\n    @Test\n    fun `lastInstanceSentTimeMillis should be 0 if there are no sent instances`() {\n        instancesRepository.deleteAll()\n        scheduler.flush()\n        assertThat(viewModel.data.value!!.lastInstanceSentTimeMillis, equalTo(0L))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/javarosawrapper/FakeFormController.java",
    "content": "package org.odk.collect.android.javarosawrapper;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.instance.TreeReference;\nimport org.javarosa.form.api.FormEntryController;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.android.exception.JavaRosaException;\nimport org.odk.collect.android.formentry.audit.AuditEventLogger;\nimport org.odk.collect.android.utilities.StubFormController;\n\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\npublic class FakeFormController extends StubFormController {\n    private FormDef formDef;\n    private FormIndex index;\n    private final AuditEventLogger auditEventLogger;\n    private final LinkedList<Integer> nextEvents = new LinkedList<>();\n    private Integer currentEvent = FormEntryController.EVENT_END_OF_FORM;\n    private int step;\n    private RuntimeException newRepeatError;\n    private JavaRosaException nextStepError;\n    private JavaRosaException saveError;\n    private Map<FormIndex, FormEntryPrompt> prompts = new HashMap<>();\n    private FailedValidationResult failedConstraint;\n    private JavaRosaException validationError;\n    private JavaRosaException previousStepError;\n    private FormIndex nextRepeatPrompt;\n    private List<FormEntryPrompt> currentPrompts;\n    private Map<TreeReference, IAnswerData> answers = new HashMap<>();\n\n    public FakeFormController(FormIndex startingIndex, AuditEventLogger auditEventLogger) {\n        this.index = startingIndex;\n        this.auditEventLogger = auditEventLogger;\n    }\n\n    @Nullable\n    @Override\n    public FormIndex getFormIndex() {\n        return index;\n    }\n\n    @Nullable\n    @Override\n    public FormDef getFormDef() {\n        return formDef != null ? formDef : new FormDef();\n    }\n\n    @Override\n    public int getEvent() {\n        return currentEvent;\n    }\n\n    @Nullable\n    @Override\n    public AuditEventLogger getAuditEventLogger() {\n        return auditEventLogger;\n    }\n\n    @Override\n    public int stepToNextScreenEvent() throws JavaRosaException {\n        if (nextStepError != null) {\n            throw nextStepError;\n        }\n\n        step = step + 1;\n        index = new FormIndex(null, index.getLocalIndex() + 1, 0, new TreeReference());\n\n        if (!nextEvents.isEmpty()) {\n            currentEvent = nextEvents.pop();\n        } else {\n            currentEvent = FormEntryController.EVENT_END_OF_FORM;\n        }\n\n        return currentEvent;\n    }\n\n    @Override\n    public void newRepeat() {\n        if (newRepeatError != null) {\n            throw newRepeatError;\n        }\n    }\n\n    @Override\n    public @NotNull ValidationResult saveOneScreenAnswer(@Nullable FormIndex index, @Nullable IAnswerData data, boolean evaluateConstraints) {\n        if (failedConstraint != null && evaluateConstraints) {\n            return failedConstraint;\n        } else {\n            answers.put(index.getReference(), data);\n            return SuccessValidationResult.INSTANCE;\n        }\n    }\n\n    @NonNull\n    @Override\n    public ValidationResult saveAllScreenAnswers(@Nullable HashMap<FormIndex, IAnswerData> answers, boolean evaluateConstraints) throws JavaRosaException {\n        if (saveError != null) {\n            throw saveError;\n        } else {\n            for (Map.Entry<FormIndex, IAnswerData> entry : answers.entrySet()) {\n                ValidationResult validationResult = saveOneScreenAnswer(entry.getKey(), entry.getValue(), evaluateConstraints);\n                if (validationResult instanceof FailedValidationResult) {\n                    return validationResult;\n                }\n            }\n        }\n\n        return SuccessValidationResult.INSTANCE;\n    }\n\n    @Nullable\n    @Override\n    public FormEntryPrompt getQuestionPrompt(FormIndex index) {\n        return prompts.get(index);\n    }\n\n    @Nullable\n    @Override\n    public FormEntryPrompt getQuestionPrompt() {\n        return currentPrompts.get(0);\n    }\n\n    @Override\n    public @NotNull ValidationResult validateAnswerConstraint(@NotNull FormIndex index, @Nullable IAnswerData answer) {\n        if (failedConstraint != null) {\n            return failedConstraint;\n        } else {\n            return SuccessValidationResult.INSTANCE;\n        }\n    }\n\n    @NonNull\n    @Override\n    public ValidationResult validateAnswers(boolean moveToInvalidIndex) throws JavaRosaException {\n        if (validationError != null) {\n            throw validationError;\n        } else {\n            return SuccessValidationResult.INSTANCE;\n        }\n    }\n\n    @Override\n    public int stepToPreviousScreenEvent() throws JavaRosaException {\n        if (previousStepError != null) {\n            throw previousStepError;\n        }\n\n        step = step - 1;\n        index = new FormIndex(null, index.getLocalIndex() - 1, 0, new TreeReference());\n        return FormEntryController.EVENT_BEGINNING_OF_FORM;\n    }\n\n    @Override\n    public void jumpToNewRepeatPrompt() {\n        if (nextRepeatPrompt != null) {\n            index = nextRepeatPrompt;\n        } else {\n            throw new IllegalStateException(\"No repeat prompt index set!\");\n        }\n    }\n\n    @Override\n    public int jumpToIndex(@Nullable FormIndex index) {\n        this.index = index;\n        return FormEntryController.EVENT_END_OF_FORM;\n    }\n\n    @Override\n    public @Nullable IAnswerData getAnswer(@Nullable TreeReference treeReference) {\n        return answers.get(treeReference);\n    }\n\n    public void addNextEvents(List<Integer> events) {\n        nextEvents.addAll(events);\n    }\n\n    public int getStepPosition() {\n        return step;\n    }\n\n    public void setNewRepeatError(RuntimeException exception) {\n        this.newRepeatError = exception;\n    }\n\n    public void setNextStepError(JavaRosaException exception) {\n        this.nextStepError = exception;\n    }\n\n    public void setPreviousStepError(JavaRosaException exception) {\n        this.previousStepError = exception;\n    }\n\n    public void setSaveError(JavaRosaException exception) {\n        this.saveError = exception;\n    }\n\n    public void setPrompt(FormIndex index, FormEntryPrompt prompt) {\n        prompts.put(index, prompt);\n    }\n\n    public void setFailedConstraint(FailedValidationResult result) {\n        this.failedConstraint = result;\n    }\n\n    public void setValidationError(JavaRosaException exception) {\n        this.validationError = exception;\n    }\n\n    public void setNextRepeatPrompt(FormIndex nextRepeatPrompt) {\n        this.nextRepeatPrompt = nextRepeatPrompt;\n    }\n\n    public void setCurrentEvent(int event) {\n        this.currentEvent = event;\n    }\n\n    public void setQuestionPrompts(List<FormEntryPrompt> prompts) {\n        this.currentPrompts = prompts;\n    }\n\n    public void setFormDef(FormDef formDef) {\n        this.formDef = formDef;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/javarosawrapper/FormControllerTest.java",
    "content": "package org.odk.collect.android.javarosawrapper;\n\nimport com.google.common.io.Files;\n\nimport org.javarosa.form.api.FormEntryController;\nimport org.javarosa.form.api.FormEntryModel;\nimport org.javarosa.xform.parse.XFormParser;\nimport org.javarosa.xform.util.XFormUtils;\nimport org.jetbrains.annotations.NotNull;\nimport org.junit.Test;\nimport org.odk.collect.android.utilities.FileUtils;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.hamcrest.Matchers.nullValue;\n\npublic class FormControllerTest {\n    @Test\n    public void validateAnswers_shouldNotChangeFormIndexToTheIndexOfInvalidQuestionIfAskedNotToDoThat() throws Exception {\n        String form = new String(\n                FileUtils.getResourceAsStream(\"forms/two-question-required.xml\").readAllBytes(),\n                StandardCharsets.UTF_8\n        );\n\n        FormController formController = createFormController(form);\n        formController.stepToNextScreenEvent();\n\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0, \"));\n        formController.validateAnswers(false);\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0, \"));\n    }\n\n    @Test\n    public void validateAnswers_shouldChangeFormIndexToTheIndexOfInvalidQuestionIfAskedToDoThat() throws Exception {\n        String form = new String(\n                FileUtils.getResourceAsStream(\"forms/two-question-required.xml\").readAllBytes(),\n                StandardCharsets.UTF_8\n        );\n\n        FormController formController = createFormController(form);\n        formController.stepToNextScreenEvent();\n\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0, \"));\n        formController.validateAnswers(true);\n        assertThat(formController.getFormIndex().toString(), equalTo(\"1, \"));\n    }\n\n    @Test\n    public void jumpToNewRepeatPrompt_whenIndexIsInRepeat_jumpsToRepeatPrompt() throws Exception {\n        FormController formController = createFormController(ONE_QUESTION_REPEAT);\n\n        formController.stepToNextScreenEvent();\n        assertThat(formController.getEvent(), equalTo(FormEntryController.EVENT_QUESTION));\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0_0, 0, \"));\n\n        formController.jumpToNewRepeatPrompt();\n\n        assertThat(formController.getEvent(), equalTo(FormEntryController.EVENT_PROMPT_NEW_REPEAT));\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0_1, \"));\n    }\n\n    @Test\n    public void jumpToNewRepeatPrompt_whenIndexIsRepeatInGroup_jumpsToRepeatPrompt() throws Exception {\n        FormController formController = createFormController(ONE_QUESTION_GROUP_REPEAT);\n\n        formController.stepToNextScreenEvent();\n        assertThat(formController.getEvent(), equalTo(FormEntryController.EVENT_QUESTION));\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0_0, 0, 0, \"));\n\n        formController.jumpToNewRepeatPrompt();\n\n        assertThat(formController.getEvent(), equalTo(FormEntryController.EVENT_PROMPT_NEW_REPEAT));\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0_1, \"));\n    }\n\n    @Test\n    public void jumpToNewRepeatPrompt_whenIndexIsOuterOfNestedRepeat_jumpsToOuterRepeatPrompt() throws Exception {\n        FormController formController = createFormController(ONE_QUESTION_NESTED_REPEAT);\n\n        formController.stepToNextScreenEvent();\n        assertThat(formController.getEvent(), equalTo(FormEntryController.EVENT_QUESTION));\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0_0, 0, \"));\n\n        formController.jumpToNewRepeatPrompt();\n\n        assertThat(formController.getEvent(), equalTo(FormEntryController.EVENT_PROMPT_NEW_REPEAT));\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0_1, \"));\n    }\n\n    @Test\n    public void jumpToNewRepeatPrompt_whenIndexIsInNestedRepeat_jumpsToNestedRepeatPrompt() throws Exception {\n        FormController formController = createFormController(ONE_QUESTION_NESTED_REPEAT);\n\n        formController.stepToNextScreenEvent();\n        formController.stepToNextScreenEvent();\n        assertThat(formController.getEvent(), equalTo(FormEntryController.EVENT_QUESTION));\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0_0, 1_0, 0, \"));\n\n        formController.jumpToNewRepeatPrompt();\n\n        assertThat(formController.getEvent(), equalTo(FormEntryController.EVENT_PROMPT_NEW_REPEAT));\n        assertThat(formController.getFormIndex().toString(), equalTo(\"0_0, 1_1, \"));\n    }\n\n    @Test\n    public void whenInstanceFileAndAuditConfigNull_getAuditEventLogger_isNotNull() throws Exception {\n        ByteArrayInputStream inputStream = new ByteArrayInputStream(ONE_QUESTION_NESTED_REPEAT.getBytes());\n        final FormEntryModel fem = new FormEntryModel(XFormUtils.getFormFromInputStream(inputStream));\n        final FormEntryController formEntryController = new FormEntryController(fem);\n        FormController formController = new JavaRosaFormController(Files.createTempDir(), formEntryController, null);\n        assertThat(formController.getSubmissionMetadata().auditConfig, is(nullValue()));\n        assertThat(formController.getInstanceFile(), is(nullValue()));\n\n        assertThat(formController.getAuditEventLogger(), notNullValue());\n    }\n\n\n    //region indexIsInFieldList\n    @Test\n    public void questionInGroupWithoutFieldListAppearance_isNotInFieldList() throws IOException, XFormParser.ParseException {\n        FormController formController = createFormController(GROUP);\n\n        formController.stepToNextEvent(true);\n        formController.stepToNextEvent(true);\n        assertThat(formController.getEvent(), equalTo(FormEntryController.EVENT_QUESTION));\n        assertThat(formController.indexIsInFieldList(), is(false));\n    }\n\n    @Test\n    public void questionInGroupWithoutFieldListAppearance_isInFieldList() throws IOException, XFormParser.ParseException {\n        FormController formController = createFormController(FIELD_LIST);\n\n        formController.stepToNextEvent(true);\n        formController.stepToNextEvent(true);\n        assertThat(formController.getEvent(), equalTo(FormEntryController.EVENT_QUESTION));\n        assertThat(formController.indexIsInFieldList(), is(true));\n    }\n    //endregion\n\n    @NotNull\n    private FormController createFormController(String xform) throws IOException, XFormParser.ParseException {\n        ByteArrayInputStream inputStream = new ByteArrayInputStream(xform.getBytes());\n        final FormEntryModel fem = new FormEntryModel(XFormUtils.getFormFromInputStream(inputStream));\n        final FormEntryController formEntryController = new FormEntryController(fem);\n        return new JavaRosaFormController(Files.createTempDir(), formEntryController, File.createTempFile(\"instance\", \"\"));\n    }\n\n    private static final String ONE_QUESTION_REPEAT = \"<?xml version=\\\"1.0\\\"?>\\n\" +\n            \"<h:html xmlns=\\\"http://www.w3.org/2002/xforms\\\" xmlns:ev=\\\"http://www.w3.org/2001/xml-events\\\" xmlns:h=\\\"http://www.w3.org/1999/xhtml\\\" xmlns:jr=\\\"http://openrosa.org/javarosa\\\" xmlns:orx=\\\"http://openrosa.org/xforms\\\" xmlns:xsd=\\\"http://www.w3.org/2001/XMLSchema\\\">\\n\" +\n            \"    <h:head>\\n\" +\n            \"        <h:title>One Question Repeat</h:title>\\n\" +\n            \"        <model>\\n\" +\n            \"            <instance>\\n\" +\n            \"                <data id=\\\"one_question_repeat\\\">\\n\" +\n            \"                    <person>\\n\" +\n            \"                        <age/>\\n\" +\n            \"                    </person>\\n\" +\n            \"                </data>\\n\" +\n            \"            </instance>\\n\" +\n            \"            <bind nodeset=\\\"age\\\" type=\\\"int\\\"/>\\n\" +\n            \"        </model>\\n\" +\n            \"    </h:head>\\n\" +\n            \"    <h:body>\\n\" +\n            \"        <group ref=\\\"/data/person\\\">\\n\" +\n            \"            <label>Person</label>\\n\" +\n            \"            <repeat nodeset=\\\"/data/person\\\">\\n\" +\n            \"                <input ref=\\\"/data/person/age\\\">\\n\" +\n            \"                    <label>What is their age?</label>\\n\" +\n            \"                </input>\\n\" +\n            \"            </repeat>\\n\" +\n            \"        </group>\\n\" +\n            \"    </h:body>\\n\" +\n            \"</h:html>\\n\";\n\n    private static final String ONE_QUESTION_GROUP_REPEAT = \"<?xml version=\\\"1.0\\\"?>\\n\" +\n            \"<h:html xmlns=\\\"http://www.w3.org/2002/xforms\\\" xmlns:ev=\\\"http://www.w3.org/2001/xml-events\\\" xmlns:h=\\\"http://www.w3.org/1999/xhtml\\\" xmlns:jr=\\\"http://openrosa.org/javarosa\\\" xmlns:orx=\\\"http://openrosa.org/xforms\\\" xmlns:xsd=\\\"http://www.w3.org/2001/XMLSchema\\\">\\n\" +\n            \"    <h:head>\\n\" +\n            \"        <h:title>One Question Repeat</h:title>\\n\" +\n            \"        <model>\\n\" +\n            \"            <instance>\\n\" +\n            \"                <data id=\\\"one_question_repeat\\\">\\n\" +\n            \"                    <person>\\n\" +\n            \"                        <questions>\\n\" +\n            \"                            <age/>\\n\" +\n            \"                        </questions>\\n\" +\n            \"                    </person>\\n\" +\n            \"                </data>\\n\" +\n            \"            </instance>\\n\" +\n            \"            <bind nodeset=\\\"age\\\" type=\\\"int\\\"/>\\n\" +\n            \"        </model>\\n\" +\n            \"    </h:head>\\n\" +\n            \"    <h:body>\\n\" +\n            \"        <group ref=\\\"/data/person\\\">\\n\" +\n            \"            <label>Person</label>\\n\" +\n            \"            <repeat nodeset=\\\"/data/person\\\">\\n\" +\n            \"                <group ref=\\\"/data/person/questions\\\">\\n\" +\n            \"                    <input ref=\\\"/data/person/questions/age\\\">\\n\" +\n            \"                        <label>What is their age?</label>\\n\" +\n            \"                    </input>\\n\" +\n            \"                </group>\\n\" +\n            \"            </repeat>\\n\" +\n            \"        </group>\\n\" +\n            \"    </h:body>\\n\" +\n            \"</h:html>\\n\";\n\n    private static final String ONE_QUESTION_NESTED_REPEAT = \"<?xml version=\\\"1.0\\\"?>\\n\" +\n            \"<h:html xmlns=\\\"http://www.w3.org/2002/xforms\\\" xmlns:ev=\\\"http://www.w3.org/2001/xml-events\\\" xmlns:h=\\\"http://www.w3.org/1999/xhtml\\\" xmlns:jr=\\\"http://openrosa.org/javarosa\\\" xmlns:orx=\\\"http://openrosa.org/xforms\\\" xmlns:xsd=\\\"http://www.w3.org/2001/XMLSchema\\\">\\n\" +\n            \"    <h:head>\\n\" +\n            \"        <h:title>One Question Repeat</h:title>\\n\" +\n            \"        <model>\\n\" +\n            \"            <instance>\\n\" +\n            \"                <data id=\\\"one_question_repeat\\\">\\n\" +\n            \"                    <person>\\n\" +\n            \"                        <age/>\\n\" +\n            \"                        <tattoo>\\n\" +\n            \"                            <description />\\n\" +\n            \"                        </tattoo>\\n\" +\n            \"                    </person>\\n\" +\n            \"                </data>\\n\" +\n            \"            </instance>\\n\" +\n            \"            <bind nodeset=\\\"age\\\" type=\\\"int\\\"/>\\n\" +\n            \"            <bind nodeset=\\\"description\\\" type=\\\"string\\\"/>\\n\" +\n            \"        </model>\\n\" +\n            \"    </h:head>\\n\" +\n            \"    <h:body>\\n\" +\n            \"        <group ref=\\\"/data/person\\\">\\n\" +\n            \"            <label>Person</label>\\n\" +\n            \"            <repeat nodeset=\\\"/data/person\\\">\\n\" +\n            \"                <input ref=\\\"/data/person/age\\\">\\n\" +\n            \"                    <label>What is their age?</label>\\n\" +\n            \"                </input>\\n\" +\n            \"                <repeat nodeset=\\\"/data/person/tattoo\\\">\\n\" +\n            \"                    <input ref=\\\"/data/person/tattoo/description\\\">\\n\" +\n            \"                        <label>What is the tattoo of?</label>\\n\" +\n            \"                    </input>\\n\" +\n            \"                </repeat>\\n\" +\n            \"            </repeat>\\n\" +\n            \"        </group>\\n\" +\n            \"    </h:body>\\n\" +\n            \"</h:html>\\n\";\n\n    private static final String GROUP = \"<?xml version=\\\"1.0\\\"?>\\n\" +\n            \"<h:html xmlns=\\\"http://www.w3.org/2002/xforms\\\" xmlns:h=\\\"http://www.w3.org/1999/xhtml\\\">\\n\" +\n            \"    <h:head>\\n\" +\n            \"        <h:title>Group</h:title>\\n\" +\n            \"        <model>\\n\" +\n            \"            <instance>\\n\" +\n            \"                <data id=\\\"group\\\">\\n\" +\n            \"                    <group>\\n\" +\n            \"                        <question/>\\n\" +\n            \"                    </group>\\n\" +\n            \"                </data>\\n\" +\n            \"            </instance>\\n\" +\n            \"            <bind nodeset=\\\"/data/group/question\\\" type=\\\"int\\\"/>\\n\" +\n            \"        </model>\\n\" +\n            \"    </h:head>\\n\" +\n            \"    <h:body>\\n\" +\n            \"        <group ref=\\\"/data/group\\\">\\n\" +\n            \"          <input ref=\\\"/data/group/question\\\">\\n\" +\n            \"            <label>Question</label>\\n\" +\n            \"          </input>\\n\" +\n            \"        </group>\\n\" +\n            \"    </h:body>\\n\" +\n            \"</h:html>\\n\";\n\n    private static final String FIELD_LIST = \"<?xml version=\\\"1.0\\\"?>\\n\" +\n            \"<h:html xmlns=\\\"http://www.w3.org/2002/xforms\\\" xmlns:h=\\\"http://www.w3.org/1999/xhtml\\\">\\n\" +\n            \"    <h:head>\\n\" +\n            \"        <h:title>Field list</h:title>\\n\" +\n            \"        <model>\\n\" +\n            \"            <instance>\\n\" +\n            \"                <data id=\\\"field-list\\\">\\n\" +\n            \"                    <group>\\n\" +\n            \"                        <question/>\\n\" +\n            \"                    </group>\\n\" +\n            \"                </data>\\n\" +\n            \"            </instance>\\n\" +\n            \"            <bind nodeset=\\\"/data/group/question\\\" type=\\\"int\\\"/>\\n\" +\n            \"        </model>\\n\" +\n            \"    </h:head>\\n\" +\n            \"    <h:body>\\n\" +\n            \"        <group ref=\\\"/data/group\\\" appearance=\\\"fake fieLd-list fake\\\">\\n\" +\n            \"          <input ref=\\\"/data/group/question\\\">\\n\" +\n            \"            <label>Question</label>\\n\" +\n            \"          </input>\\n\" +\n            \"        </group>\\n\" +\n            \"    </h:body>\\n\" +\n            \"</h:html>\\n\";\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/location/client/FakeLocationClient.java",
    "content": "package org.odk.collect.android.location.client;\n\nimport android.location.Location;\n\nimport androidx.annotation.Nullable;\n\nimport com.google.android.gms.location.LocationListener;\n\nimport org.odk.collect.location.LocationClient;\n\npublic class FakeLocationClient implements LocationClient {\n    private boolean failOnStart;\n    private boolean failOnRequest;\n    private LocationClientListener listener;\n    private LocationListener locationListener;\n    private boolean running;\n    private boolean locationAvailable = true;\n    private Priority priority = Priority.PRIORITY_HIGH_ACCURACY;\n    private Location lastLocation;\n\n    // Instrumentation for testing.\n\n    public void setLocationAvailable(boolean available) {\n        locationAvailable = available;\n    }\n\n    public void setFailOnStart(boolean fail) {\n        failOnStart = fail;\n    }\n\n    public void setFailOnRequest(boolean fail) {\n        failOnRequest = fail;\n    }\n\n    public void receiveFix(Location location) {\n        lastLocation = location;\n        if (locationListener != null) {\n            locationListener.onLocationChanged(location);\n        }\n    }\n\n    public boolean isRunning() {\n        return running;\n    }\n\n    // Implementation of the LocationClient interface.\n\n    public void start(LocationClientListener listener) {\n        setListener(listener);\n        running = true;\n        if (getListener() != null) {\n            if (failOnStart) {\n                getListener().onClientStartFailure();\n            } else {\n                getListener().onClientStart();\n            }\n        }\n    }\n\n    public void stop() {\n        running = false;\n        stopLocationUpdates();\n        if (getListener() != null) {\n            getListener().onClientStop();\n        }\n        setListener(null);\n    }\n\n    public boolean isLocationAvailable() {\n        return locationAvailable;\n    }\n\n    public void requestLocationUpdates(LocationListener locationListener) {\n        if (failOnRequest) {\n            throw new SecurityException();\n        }\n\n        this.locationListener = locationListener;\n    }\n\n    public void stopLocationUpdates() {\n        this.locationListener = null;\n    }\n\n    @Override\n    public void setListener(@Nullable LocationClientListener locationClientListener) {\n        this.listener = locationClientListener;\n    }\n\n    protected LocationClientListener getListener() {\n        return listener;\n    }\n\n    public Location getLastLocation() {\n        return lastLocation;\n    }\n\n    public boolean isMonitoringLocation() {\n        return locationListener != null;\n    }\n\n    public Priority getPriority() {\n        return priority;\n    }\n\n    public void setPriority(Priority priority) {\n        this.priority = priority;\n    }\n\n    @Override\n    public void setRetainMockAccuracy(boolean retainMockAccuracy) {\n        throw new UnsupportedOperationException();\n    }\n\n    public boolean canSetUpdateIntervals() {\n        return false;\n    }\n\n    public void setUpdateInterval(long updateInterval) { }\n\n    public void resetUpdateIntervals() { }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/location/client/MaxAccuracyWithinTimeoutLocationClientWrapperTest.java",
    "content": "package org.odk.collect.android.location.client;\n\nimport android.location.Location;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport com.google.android.gms.location.LocationListener;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport java.util.concurrent.TimeUnit;\n\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.odk.collect.testshared.LocationTestUtils.createLocation;\nimport static org.robolectric.shadows.ShadowSystemClock.advanceBy;\n\n@RunWith(AndroidJUnit4.class)\npublic class MaxAccuracyWithinTimeoutLocationClientWrapperTest {\n    private FakeLocationClient fakeLocationClient;\n    private LocationListener locationListener;\n\n    private MaxAccuracyWithinTimeoutLocationClientWrapper maxAccuracyLocationClient;\n\n    @Before\n    public void setUp() {\n        fakeLocationClient = new FakeLocationClient();\n        locationListener = mock(LocationListener.class);\n\n        maxAccuracyLocationClient = new MaxAccuracyWithinTimeoutLocationClientWrapper(fakeLocationClient, locationListener);\n    }\n\n    @Test\n    public void requestingLocationUpdates_ShouldResultInUpdate() {\n        maxAccuracyLocationClient.requestLocationUpdates(5);\n        RobolectricHelpers.runLooper();\n\n        Location location = createLocation(\"GPS\", 1, 1, 1, 100);\n        fakeLocationClient.receiveFix(location);\n\n        verify(locationListener).onLocationChanged(location);\n    }\n\n    @Test\n    public void denyingLocationPermissions_ShouldResultInNoUpdates() {\n        fakeLocationClient.setFailOnRequest(true);\n\n        maxAccuracyLocationClient.requestLocationUpdates(5);\n\n        Location location = createLocation(\"GPS\", 1, 1, 1, 100);\n        fakeLocationClient.receiveFix(location);\n\n        verify(locationListener, never()).onLocationChanged(any());\n    }\n\n    @Test\n    public void onlyMoreAccurateFixes_ShouldResultInUpdates() {\n        maxAccuracyLocationClient.requestLocationUpdates(5);\n        RobolectricHelpers.runLooper();\n\n        Location location = createLocation(\"GPS\", 1, 1, 1, 100);\n        fakeLocationClient.receiveFix(location);\n        verify(locationListener).onLocationChanged(location);\n\n        Location moreAccurateLocation = createLocation(\"GPS\", 1, 1, 1, 2);\n        fakeLocationClient.receiveFix(moreAccurateLocation);\n        verify(locationListener).onLocationChanged(moreAccurateLocation);\n\n        Location lessAccurateLocation = createLocation(\"GPS\", 1, 1, 1, 10);\n        fakeLocationClient.receiveFix(lessAccurateLocation);\n        verify(locationListener).onLocationChanged(moreAccurateLocation);\n    }\n\n    @Test\n    public void fixWithAccuracyAfterAFixWithout_ShouldResultInUpdate() {\n        maxAccuracyLocationClient.requestLocationUpdates(5);\n        RobolectricHelpers.runLooper();\n\n        Location locationWithoutAccuracy = createLocation(\"GPS\", 1, 1, 1);\n        fakeLocationClient.receiveFix(locationWithoutAccuracy);\n        verify(locationListener).onLocationChanged(locationWithoutAccuracy);\n\n        Location locationWithAccuracy = createLocation(\"GPS\", 5, 5, 5, 100);\n        fakeLocationClient.receiveFix(locationWithAccuracy);\n        verify(locationListener).onLocationChanged(locationWithAccuracy);\n    }\n\n    @Test\n    public void fixWithoutAccuracyAfterAFixWith_ShouldNotResultInUpdate() {\n        maxAccuracyLocationClient.requestLocationUpdates(5);\n        RobolectricHelpers.runLooper();\n\n        Location locationWithAccuracy = createLocation(\"GPS\", 5, 5, 5, 100);\n        fakeLocationClient.receiveFix(locationWithAccuracy);\n        verify(locationListener).onLocationChanged(locationWithAccuracy);\n\n        Location locationWithoutAccuracy = createLocation(\"GPS\", 1, 1, 1);\n        fakeLocationClient.receiveFix(locationWithoutAccuracy);\n        verify(locationListener, never()).onLocationChanged(locationWithoutAccuracy);\n    }\n\n    @Test\n    public void requestingLocationUpdatesAgain_ShouldResetHighestAccuracy() {\n        maxAccuracyLocationClient.requestLocationUpdates(5);\n        RobolectricHelpers.runLooper();\n\n        Location location = createLocation(\"GPS\", 1, 1, 1, 2);\n        fakeLocationClient.receiveFix(location);\n        verify(locationListener).onLocationChanged(location);\n\n        maxAccuracyLocationClient.requestLocationUpdates(5);\n\n        Location lessAccurateLocation = createLocation(\"GPS\", 1, 1, 1, 10);\n        fakeLocationClient.receiveFix(lessAccurateLocation);\n        verify(locationListener).onLocationChanged(lessAccurateLocation);\n    }\n\n    @Test\n    public void timeoutSecondsPassing_ShouldStopUpdates() {\n        int timeoutSeconds = 5;\n        maxAccuracyLocationClient.requestLocationUpdates(timeoutSeconds);\n        RobolectricHelpers.runLooper();\n        advanceBy(timeoutSeconds, TimeUnit.SECONDS);\n        RobolectricHelpers.runLooper();\n\n        // verify that stop() was called so resources are released\n        assertFalse(fakeLocationClient.isRunning());\n\n        // verify that if the location client is stopped, location updates aren't sent\n        Location location = createLocation(\"GPS\", 1, 1, 1, 100);\n        fakeLocationClient.receiveFix(location);\n        verify(locationListener, never()).onLocationChanged(any());\n    }\n\n    @Test\n    public void requestingLocationUpdatesAgain_ShouldResetTimeout() {\n        int timeoutSeconds = 5;\n        maxAccuracyLocationClient.requestLocationUpdates(timeoutSeconds);\n        RobolectricHelpers.runLooper();\n\n        // advance time but not enough for updates to stop\n        advanceBy(timeoutSeconds - 1, TimeUnit.SECONDS);\n        assertTrue(fakeLocationClient.isRunning());\n\n        // initiate a new request\n        maxAccuracyLocationClient.requestLocationUpdates(timeoutSeconds);\n\n        // verify that the new request reset the timer\n        advanceBy(timeoutSeconds - 1, TimeUnit.SECONDS);\n        assertTrue(fakeLocationClient.isRunning());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/mainmenu/CurrentProjectViewModelTest.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.mockito.Mockito.verify\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.androidtest.MainDispatcherRule\nimport org.odk.collect.projects.Project\n\nclass CurrentProjectViewModelTest {\n\n    @get:Rule\n    var instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    @get:Rule\n    val mainDispatcherRule = MainDispatcherRule()\n\n    private val projectsDataService = mock<ProjectsDataService> {\n        on { getCurrentProject() } doReturn MutableStateFlow(\n            Project.Saved(\n                \"123\",\n                \"Project X\",\n                \"X\",\n                \"#cccccc\"\n            )\n        )\n    }\n\n    @Test\n    fun `Initial current project should be set`() {\n        val viewModel = CurrentProjectViewModel(projectsDataService)\n        assertThat(viewModel.hasCurrentProject(), equalTo(true))\n        assertThat(\n            viewModel.currentProject.value,\n            equalTo(Project.Saved(\"123\", \"Project X\", \"X\", \"#cccccc\"))\n        )\n    }\n\n    @Test\n    fun `setCurrentProject() sets current project`() {\n        val project = Project.Saved(\"456\", \"Project Y\", \"Y\", \"#ffffff\")\n\n        val viewModel = CurrentProjectViewModel(projectsDataService)\n        viewModel.setCurrentProject(project)\n        verify(projectsDataService).setCurrentProject(\"456\")\n    }\n\n    @Test\n    fun `hasCurrentProject returns false when there is no current project`() {\n        whenever(projectsDataService.getCurrentProject()).thenReturn(MutableStateFlow(null))\n        val currentProjectViewModel = CurrentProjectViewModel(projectsDataService)\n\n        assertThat(currentProjectViewModel.hasCurrentProject(), equalTo(false))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/mainmenu/MainMenuActivityTest.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.app.Application\nimport android.content.RestrictionsManager\nimport android.graphics.Color\nimport android.graphics.drawable.GradientDrawable\nimport android.view.View\nimport android.widget.TextView\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.intent.Intents\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.CoreMatchers.notNullValue\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.activities.CrashHandlerActivity\nimport org.odk.collect.android.activities.DeleteFormsActivity\nimport org.odk.collect.android.activities.FormDownloadListActivity\nimport org.odk.collect.android.activities.InstanceChooserList\nimport org.odk.collect.android.application.initialization.AnalyticsInitializer\nimport org.odk.collect.android.fakes.FakePermissionsProvider\nimport org.odk.collect.android.formentry.FormOpeningMode\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListActivity\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider\nimport org.odk.collect.android.instancemanagement.send.InstanceUploaderListActivity\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.androidshared.system.BroadcastReceiverRegister\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.crashhandler.CrashHandler\nimport org.odk.collect.mobiledevicemanagement.MDMConfigObserver\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.permissions.PermissionsProvider\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectCreator\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.odk.collect.settings.SettingsProvider\nimport org.robolectric.Robolectric\nimport org.robolectric.Shadows.shadowOf\n\n@RunWith(AndroidJUnit4::class)\nclass MainMenuActivityTest {\n\n    private val project = Project.Saved(\"123\", \"Project\", \"P\", \"#f5f5f5\")\n\n    private val mainMenuViewModel = mock<MainMenuViewModel> {\n        on { sendableInstancesCount } doReturn MutableLiveData(0)\n        on { sentInstancesCount } doReturn MutableLiveData(0)\n        on { editableInstancesCount } doReturn MutableLiveData(0)\n        on { savedForm } doReturn MutableLiveData()\n    }\n\n    private val currentProjectViewModel = mock<CurrentProjectViewModel> {\n        on { hasCurrentProject() } doReturn true\n        on { currentProject } doReturn MutableLiveData(project)\n    }\n\n    private val permissionsViewModel = mock<RequestPermissionsViewModel> {\n        on { shouldAskForPermissions() } doReturn false\n    }\n\n    private val permissionsProvider = FakePermissionsProvider()\n\n    private val application = ApplicationProvider.getApplicationContext<Application>()\n\n    @get:Rule\n    val launcherRule = ActivityScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesMainMenuViewModelFactory(\n                versionInformation: VersionInformation,\n                application: Application,\n                settingsProvider: SettingsProvider,\n                instancesDataService: InstancesDataService,\n                scheduler: Scheduler,\n                projectsDataService: ProjectsDataService,\n                analyticsInitializer: AnalyticsInitializer,\n                permissionChecker: PermissionsChecker,\n                formsRepositoryProvider: FormsRepositoryProvider,\n                instancesRepositoryProvider: InstancesRepositoryProvider,\n                autoSendSettingsProvider: AutoSendSettingsProvider\n            ): MainMenuViewModelFactory {\n                return object : MainMenuViewModelFactory(\n                    versionInformation,\n                    application,\n                    settingsProvider,\n                    instancesDataService,\n                    scheduler,\n                    projectsDataService,\n                    permissionChecker,\n                    formsRepositoryProvider,\n                    instancesRepositoryProvider,\n                    autoSendSettingsProvider\n                ) {\n                    override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                        return when (modelClass) {\n                            MainMenuViewModel::class.java -> mainMenuViewModel\n                            CurrentProjectViewModel::class.java -> currentProjectViewModel\n                            RequestPermissionsViewModel::class.java -> permissionsViewModel\n                            else -> throw IllegalArgumentException()\n                        } as T\n                    }\n                }\n            }\n\n            override fun providesPermissionsProvider(permissionsChecker: PermissionsChecker?): PermissionsProvider {\n                return permissionsProvider\n            }\n\n            override fun providesMDMConfigObserver(\n                scheduler: Scheduler,\n                settingsProvider: SettingsProvider,\n                projectsRepository: ProjectsRepository,\n                projectCreator: ProjectCreator,\n                settingsImporter: ODKAppSettingsImporter,\n                broadcastReceiverRegister: BroadcastReceiverRegister,\n                restrictionsManager: RestrictionsManager\n            ): MDMConfigObserver {\n                return mock<MDMConfigObserver>()\n            }\n        })\n\n        CrashHandler.install(application)\n    }\n\n    @After\n    fun teardown() {\n        CrashHandler.uninstall(application)\n    }\n\n    @Test\n    fun `Activity title is current project name`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity {\n            assertThat(it.title, `is`(\"Project\"))\n        }\n    }\n\n    @Test\n    fun `Project icon for current project should be displayed`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val projectIcon = activity.findViewById<TextView>(R.id.project_icon_text)\n\n            assertThat(projectIcon.visibility, `is`(View.VISIBLE))\n            assertThat(projectIcon.text, `is`(project.icon))\n\n            val background = projectIcon.background as GradientDrawable\n            assertThat(background.color!!.defaultColor, equalTo(Color.parseColor(project.color)))\n        }\n    }\n\n    @Test\n    fun `Fill Blank Form button should have proper text`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val button = activity.findViewById<StartNewFormButton>(R.id.enter_data)\n            assertThat(button.text, `is`(activity.getString(org.odk.collect.strings.R.string.enter_data)))\n        }\n    }\n\n    @Test\n    fun `Fill Blank Form button should start list of blank forms`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            Intents.init()\n\n            val button = activity.findViewById<StartNewFormButton>(R.id.enter_data)\n            button.performClick()\n            assertThat(\n                Intents.getIntents()[0],\n                hasComponent(BlankFormListActivity::class.java.name)\n            )\n\n            Intents.release()\n        }\n    }\n\n    @Test\n    fun `Edit Saved Form button should have proper text`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val button = activity.findViewById<MainMenuButton>(R.id.review_data)\n            assertThat(button.text, `is`(activity.getString(org.odk.collect.strings.R.string.review_data)))\n        }\n    }\n\n    @Test\n    fun `Edit Saved Form button should start list of saved forms`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            Intents.init()\n\n            val button = activity.findViewById<MainMenuButton>(R.id.review_data)\n            button.performClick()\n            assertThat(Intents.getIntents()[0], hasComponent(InstanceChooserList::class.java.name))\n            assertThat(\n                Intents.getIntents()[0].extras!!.get(FormOpeningMode.FORM_MODE_KEY),\n                `is`(FormOpeningMode.EDIT_SAVED)\n            )\n\n            Intents.release()\n        }\n    }\n\n    @Test\n    fun `Send Finalized Form button should have proper text`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val button = activity.findViewById<MainMenuButton>(R.id.send_data)\n            assertThat(button.text, `is`(activity.getString(org.odk.collect.strings.R.string.send_data)))\n        }\n    }\n\n    @Test\n    fun `Send Finalized Form button should start list of finalized forms`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            Intents.init()\n\n            val button = activity.findViewById<MainMenuButton>(R.id.send_data)\n            button.performClick()\n            assertThat(\n                Intents.getIntents()[0],\n                hasComponent(InstanceUploaderListActivity::class.java.name)\n            )\n\n            Intents.release()\n        }\n    }\n\n    @Test\n    fun `View Sent Form button should have proper text`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val button = activity.findViewById<MainMenuButton>(R.id.view_sent_forms)\n            assertThat(button.text, `is`(activity.getString(org.odk.collect.strings.R.string.view_sent_forms)))\n        }\n    }\n\n    @Test\n    fun `View Sent Form button should start list of sent forms`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            Intents.init()\n\n            val button = activity.findViewById<MainMenuButton>(R.id.view_sent_forms)\n            button.performClick()\n            assertThat(Intents.getIntents()[0], hasComponent(InstanceChooserList::class.java.name))\n            assertThat(\n                Intents.getIntents()[0].extras!!.get(FormOpeningMode.FORM_MODE_KEY),\n                `is`(FormOpeningMode.VIEW_SENT)\n            )\n\n            Intents.release()\n        }\n    }\n\n    @Test\n    fun `Get Blank Form button should have proper text`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val button = activity.findViewById<MainMenuButton>(R.id.get_forms)\n            assertThat(button.text, `is`(activity.getString(org.odk.collect.strings.R.string.get_forms)))\n        }\n    }\n\n    @Test\n    fun `Get Blank Form button should start list of forms to download`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            Intents.init()\n\n            val button = activity.findViewById<MainMenuButton>(R.id.get_forms)\n            button.performClick()\n            assertThat(\n                Intents.getIntents()[0],\n                hasComponent(FormDownloadListActivity::class.java.name)\n            )\n\n            Intents.release()\n        }\n    }\n\n    @Test\n    fun `Delete Saved Form button should have proper text`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val button = activity.findViewById<MainMenuButton>(R.id.manage_forms)\n            assertThat(button.text, `is`(activity.getString(org.odk.collect.strings.R.string.manage_files)))\n        }\n    }\n\n    @Test\n    fun `Delete Saved Form button should start list of forms to delete`() {\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            Intents.init()\n\n            val button = activity.findViewById<MainMenuButton>(R.id.manage_forms)\n            button.performClick()\n            assertThat(\n                Intents.getIntents()[0],\n                hasComponent(DeleteFormsActivity::class.java.name)\n            )\n\n            Intents.release()\n        }\n    }\n\n    @Test\n    fun `When editSavedFormButton is enabled in settings, should be visible`() {\n        whenever(mainMenuViewModel.shouldEditSavedFormButtonBeVisible()).thenReturn(true)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val editSavedFormButton = activity.findViewById<MainMenuButton>(R.id.review_data)\n            assertThat(editSavedFormButton.visibility, equalTo(View.VISIBLE))\n        }\n    }\n\n    @Test\n    fun `When editSavedFormButton is disabled in settings, should be gone`() {\n        whenever(mainMenuViewModel.shouldEditSavedFormButtonBeVisible()).thenReturn(false)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val editSavedFormButton = activity.findViewById<MainMenuButton>(R.id.review_data)\n            assertThat(editSavedFormButton.visibility, equalTo(View.GONE))\n        }\n    }\n\n    @Test\n    fun `When sendFinalizedFormButton is enabled in settings, should be visible`() {\n        whenever(mainMenuViewModel.shouldSendFinalizedFormButtonBeVisible()).thenReturn(true)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val editSavedFormButton = activity.findViewById<MainMenuButton>(R.id.send_data)\n            assertThat(editSavedFormButton.visibility, equalTo(View.VISIBLE))\n        }\n    }\n\n    @Test\n    fun `When sendFinalizedFormButton is disabled in settings, should be gone`() {\n        whenever(mainMenuViewModel.shouldSendFinalizedFormButtonBeVisible()).thenReturn(false)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val editSavedFormButton = activity.findViewById<MainMenuButton>(R.id.send_data)\n            assertThat(editSavedFormButton.visibility, equalTo(View.GONE))\n        }\n    }\n\n    @Test\n    fun `When viewSentFormButton is enabled in settings, should be visible`() {\n        whenever(mainMenuViewModel.shouldViewSentFormButtonBeVisible()).thenReturn(true)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val editSavedFormButton = activity.findViewById<MainMenuButton>(R.id.view_sent_forms)\n            assertThat(editSavedFormButton.visibility, equalTo(View.VISIBLE))\n        }\n    }\n\n    @Test\n    fun `When viewSentFormButton is disabled in settings, should be gone`() {\n        whenever(mainMenuViewModel.shouldViewSentFormButtonBeVisible()).thenReturn(false)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val editSavedFormButton = activity.findViewById<MainMenuButton>(R.id.view_sent_forms)\n            assertThat(editSavedFormButton.visibility, equalTo(View.GONE))\n        }\n    }\n\n    @Test\n    fun `When getBlankFormButton is enabled in settings, should be visible`() {\n        whenever(mainMenuViewModel.shouldGetBlankFormButtonBeVisible()).thenReturn(true)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val editSavedFormButton = activity.findViewById<MainMenuButton>(R.id.get_forms)\n            assertThat(editSavedFormButton.visibility, equalTo(View.VISIBLE))\n        }\n    }\n\n    @Test\n    fun `When getBlankFormButton is disabled in settings, should be gone`() {\n        whenever(mainMenuViewModel.shouldGetBlankFormButtonBeVisible()).thenReturn(false)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val editSavedFormButton = activity.findViewById<MainMenuButton>(R.id.get_forms)\n            assertThat(editSavedFormButton.visibility, equalTo(View.GONE))\n        }\n    }\n\n    @Test\n    fun `When deleteSavedFormButton is enabled in settings, should be visible`() {\n        whenever(mainMenuViewModel.shouldDeleteSavedFormButtonBeVisible()).thenReturn(true)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val editSavedFormButton = activity.findViewById<MainMenuButton>(R.id.manage_forms)\n            assertThat(editSavedFormButton.visibility, equalTo(View.VISIBLE))\n        }\n    }\n\n    @Test\n    fun `When deleteSavedFormButton is disabled in settings, should be gone`() {\n        whenever(mainMenuViewModel.shouldDeleteSavedFormButtonBeVisible()).thenReturn(false)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity { activity: MainMenuActivity ->\n            val editSavedFormButton = activity.findViewById<MainMenuButton>(R.id.manage_forms)\n            assertThat(editSavedFormButton.visibility, equalTo(View.GONE))\n        }\n    }\n\n    @Test\n    fun `when shouldAskForPermissions is true, shows permissions dialog`() {\n        whenever(permissionsViewModel.shouldAskForPermissions()).doReturn(true)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity {\n            val dialog =\n                it.supportFragmentManager.findFragmentByTag(PermissionsDialogFragment::class.java.name)\n            assertThat(dialog, notNullValue())\n        }\n    }\n\n    @Test\n    fun `when shouldAskForPermissions is false, does not show permissions dialog`() {\n        whenever(permissionsViewModel.shouldAskForPermissions()).doReturn(false)\n\n        val scenario = launcherRule.launch(MainMenuActivity::class.java)\n        scenario.onActivity {\n            val dialog =\n                it.supportFragmentManager.findFragmentByTag(PermissionsDialogFragment::class.java.name)\n            assertThat(dialog, equalTo(null))\n        }\n    }\n\n    @Test\n    fun `when there has been a crash, opens CrashHandlerActivity and finishes`() {\n        CrashHandler.getInstance(application)!!.registerCrash(application, IllegalStateException())\n\n        Robolectric.buildActivity(MainMenuActivity::class.java).use {\n            it.setup()\n\n            val startedActivityName = shadowOf(it.get()).nextStartedActivity.component?.className\n            assertThat(startedActivityName, equalTo(CrashHandlerActivity::class.qualifiedName))\n            assertThat(it.get().isFinishing, equalTo(true))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/mainmenu/MainMenuButtonTest.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.app.Application\nimport android.graphics.Typeface\nimport android.graphics.drawable.VectorDrawable\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.robolectric.Robolectric\n\n@RunWith(AndroidJUnit4::class)\nclass MainMenuButtonTest {\n    private val context: Application =\n        ApplicationProvider.getApplicationContext<Application>().also {\n            it.setTheme(com.google.android.material.R.style.Theme_Material3_Light)\n        }\n\n    @Test\n    fun `when icon attribute is not used then it is null`() {\n        val mainMenuButton = MainMenuButton(context)\n\n        assertThat(mainMenuButton.findViewById<ImageView>(R.id.icon).drawable, equalTo(null))\n    }\n\n    @Test\n    fun `when icon attribute is used then it is set correctly`() {\n        val attrs = Robolectric.buildAttributeSet().addAttribute(R.attr.icon, \"@drawable/ic_edit_24\").build()\n        val mainMenuButton = MainMenuButton(context, attrs)\n\n        assertThat(\n            (mainMenuButton.findViewById<ImageView>(R.id.icon).drawable as VectorDrawable).toBitmap().sameAs((context.getDrawable(R.drawable.ic_edit_24) as VectorDrawable).toBitmap()),\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `when name attribute is not used then it is empty`() {\n        val mainMenuButton = MainMenuButton(context)\n\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.name).text, equalTo(\"\"))\n    }\n\n    @Test\n    fun `when name attribute is used then it is set correctly`() {\n        val attrs = Robolectric.buildAttributeSet().addAttribute(R.attr.name, \"blah\").build()\n        val mainMenuButton = MainMenuButton(context, attrs)\n\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.name).text, equalTo(\"blah\"))\n    }\n\n    @Test\n    fun `setNumberOfForms sets number correctly`() {\n        val mainMenuButton = MainMenuButton(context)\n\n        mainMenuButton.setNumberOfForms(10)\n\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.number).text, equalTo(\"10\"))\n    }\n\n    @Test\n    fun `setNumberOfForms makes 'name' and 'number' bold if highlightable attr is true`() {\n        val attrs = Robolectric.buildAttributeSet().addAttribute(R.attr.highlightable, \"true\").build()\n        val mainMenuButton = MainMenuButton(context, attrs)\n\n        mainMenuButton.setNumberOfForms(10)\n\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.name).typeface.style, equalTo(Typeface.BOLD))\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.number).typeface.style, equalTo(Typeface.BOLD))\n    }\n\n    @Test\n    fun `setNumberOfForms does not 'make' name and 'number' bold if highlightable attr is false`() {\n        val attrs = Robolectric.buildAttributeSet().addAttribute(R.attr.highlightable, \"false\").build()\n        val mainMenuButton = MainMenuButton(context, attrs)\n\n        mainMenuButton.setNumberOfForms(10)\n\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.name).typeface, equalTo(null))\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.number).typeface, equalTo(null))\n    }\n\n    @Test\n    fun `setNumberOfForms does not make 'name' and 'number' bold if highlightable attr is not set`() {\n        val mainMenuButton = MainMenuButton(context)\n\n        mainMenuButton.setNumberOfForms(10)\n\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.name).typeface, equalTo(null))\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.number).typeface, equalTo(null))\n    }\n\n    @Test\n    fun `setNumberOfForms sets an empty string when number is less than 1`() {\n        val mainMenuButton = MainMenuButton(context)\n\n        mainMenuButton.setNumberOfForms(0)\n\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.number).text, equalTo(\"\"))\n    }\n\n    @Test\n    fun `setNumberOfForms removes bold typeface form 'name' and 'number' when number is less than 1`() {\n        val attrs = Robolectric.buildAttributeSet().addAttribute(R.attr.highlightable, \"true\").build()\n        val mainMenuButton = MainMenuButton(context, attrs)\n\n        mainMenuButton.setNumberOfForms(10)\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.name).typeface.style, equalTo(Typeface.BOLD))\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.number).typeface.style, equalTo(Typeface.BOLD))\n\n        mainMenuButton.setNumberOfForms(0)\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.name).typeface.style, equalTo(Typeface.NORMAL))\n        assertThat(mainMenuButton.findViewById<TextView>(R.id.number).typeface.style, equalTo(Typeface.NORMAL))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/mainmenu/MainMenuViewModelTest.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.external.InstancesContract\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.androidtest.getOrAwaitValue\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.FormUtils\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.testshared.FakeScheduler\nimport java.util.concurrent.TimeoutException\n\n@RunWith(AndroidJUnit4::class)\nclass MainMenuViewModelTest {\n    private val formsRepository = InMemFormsRepository()\n    private val formsRepositoryProvider = mock<FormsRepositoryProvider>().apply {\n        whenever(create()).thenReturn(formsRepository)\n    }\n    private val instancesRepository = InMemInstancesRepository()\n    private val instancesRepositoryProvider = mock<InstancesRepositoryProvider>().apply {\n        whenever(create()).thenReturn(instancesRepository)\n    }\n    private val autoSendSettingsProvider = mock<AutoSendSettingsProvider>()\n    private val settingsProvider = InMemSettingsProvider()\n\n    private val projectsDataService = mock<ProjectsDataService> {\n        on { requireCurrentProject() } doReturn Project.DEMO_PROJECT\n        on { getCurrentProject() } doReturn MutableStateFlow(Project.DEMO_PROJECT)\n    }\n\n    private val instancesDataService = mock<InstancesDataService> {\n        on { getSendableCount(any()) } doReturn MutableStateFlow(0)\n        on { getEditableCount(any()) } doReturn MutableStateFlow(0)\n        on { getSentCount(any()) } doReturn MutableStateFlow(0)\n    }\n\n    private val scheduler = FakeScheduler()\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    @Test\n    fun `version when beta release returns semantic version with prefix and beta version`() {\n        val viewModel = createViewModelWithVersion(\"v1.23.0-beta.1\")\n        assertThat(viewModel.version, equalTo(\"v1.23.0 Beta 1\"))\n    }\n\n    @Test\n    fun `version when dirty beta release returns semantic version with prefix and beta version`() {\n        val viewModel = createViewModelWithVersion(\"v1.23.0-beta.1-dirty\")\n        assertThat(viewModel.version, equalTo(\"v1.23.0 Beta 1\"))\n    }\n\n    @Test\n    fun `version when beta tag returns semantic version with prefix and beta version`() {\n        val viewModel = createViewModelWithVersion(\"v1.23.0-beta.1-181-ge51d004d4\")\n        assertThat(viewModel.version, equalTo(\"v1.23.0 Beta 1\"))\n    }\n\n    @Test\n    fun `versionCommitDescription when release returns null`() {\n        val viewModel = createViewModelWithVersion(\"v1.1.7\")\n        assertThat(viewModel.versionCommitDescription, equalTo(null))\n    }\n\n    @Test\n    fun `versionCommitDescription when dirty release returns dirty`() {\n        val viewModel = createViewModelWithVersion(\"v1.1.7-dirty\")\n        assertThat(viewModel.versionCommitDescription, equalTo(\"dirty\"))\n    }\n\n    @Test\n    fun `versionCommitDescription when beta release returns null`() {\n        val viewModel = createViewModelWithVersion(\"v1.1.7-beta.7\")\n        assertThat(viewModel.versionCommitDescription, equalTo(null))\n    }\n\n    @Test\n    fun `versionCommitDescription when dirty beta release returns null`() {\n        val viewModel = createViewModelWithVersion(\"v1.1.7-beta.7-dirty\")\n        assertThat(viewModel.versionCommitDescription, equalTo(\"dirty\"))\n    }\n\n    @Test\n    fun `versionCommitDescription when beta tag returns commit count and SHA`() {\n        val viewModel = createViewModelWithVersion(\"v1.23.0-beta.1-181-ge51d004d4\")\n        assertThat(viewModel.versionCommitDescription, equalTo(\"181-ge51d004d4\"))\n    }\n\n    @Test\n    fun `versionCommitDescription when release tag returns commit count and SHA`() {\n        val viewModel = createViewModelWithVersion(\"v1.23.0-181-ge51d004d4\")\n        assertThat(viewModel.versionCommitDescription, equalTo(\"181-ge51d004d4\"))\n    }\n\n    @Test\n    fun `versionCommitDescription when dirty commit returns commit count and SHA and dirty tag`() {\n        val viewModel = createViewModelWithVersion(\"v1.14.0-181-ge51d004d4-dirty\")\n        assertThat(viewModel.versionCommitDescription, equalTo(\"181-ge51d004d4-dirty\"))\n    }\n\n    @Test\n    fun `setSavedForm should set proper message and action when the corresponding instance is saved as draft, editing drafts is enabled and encryption is disabled`() {\n        val viewModel = createViewModelWithVersion(\"\")\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, true)\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build())\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        val uri = InstancesContract.getUri(Project.DEMO_PROJECT_ID, instance.dbId)\n        viewModel.setSavedForm(uri)\n        scheduler.flush()\n\n        val formSavedSnackbarType = viewModel.savedForm.getOrAwaitValue().value!!\n        assertThat(formSavedSnackbarType.message, equalTo(org.odk.collect.strings.R.string.form_saved_as_draft))\n        assertThat(formSavedSnackbarType.action, equalTo(org.odk.collect.strings.R.string.edit_form))\n    }\n\n    @Test\n    fun `setSavedForm should set proper message and action when the corresponding instance is saved as draft, editing drafts is enabled and encryption is enabled`() {\n        val viewModel = createViewModelWithVersion(\"\")\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, true)\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build())\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .canEditWhenComplete(false)\n                .build()\n        )\n\n        val uri = InstancesContract.getUri(Project.DEMO_PROJECT_ID, instance.dbId)\n\n        viewModel.setSavedForm(uri)\n        scheduler.flush()\n\n        val formSavedSnackbarType = viewModel.savedForm.getOrAwaitValue().value!!\n        assertThat(formSavedSnackbarType.message, equalTo(org.odk.collect.strings.R.string.form_saved_as_draft))\n        assertThat(formSavedSnackbarType.action, equalTo(org.odk.collect.strings.R.string.edit_form))\n    }\n\n    @Test\n    fun `setSavedForm should set proper message and action when the corresponding instance is saved as draft, editing drafts is disabled and encryption is disabled`() {\n        val viewModel = createViewModelWithVersion(\"\")\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, false)\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build())\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        val uri = InstancesContract.getUri(Project.DEMO_PROJECT_ID, instance.dbId)\n\n        viewModel.setSavedForm(uri)\n        scheduler.flush()\n\n        val formSavedSnackbarType = viewModel.savedForm.getOrAwaitValue().value!!\n        assertThat(formSavedSnackbarType.message, equalTo(org.odk.collect.strings.R.string.form_saved_as_draft))\n        assertThat(formSavedSnackbarType.action, equalTo(org.odk.collect.strings.R.string.view_form))\n    }\n\n    @Test\n    fun `setSavedForm should set proper message and action when the corresponding instance is saved as draft, editing drafts is disabled and encryption is enabled`() {\n        val viewModel = createViewModelWithVersion(\"\")\n        settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, false)\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build())\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .canEditWhenComplete(false)\n                .build()\n        )\n\n        val uri = InstancesContract.getUri(Project.DEMO_PROJECT_ID, instance.dbId)\n\n        viewModel.setSavedForm(uri)\n        scheduler.flush()\n\n        val formSavedSnackbarType = viewModel.savedForm.getOrAwaitValue().value!!\n        assertThat(formSavedSnackbarType.message, equalTo(org.odk.collect.strings.R.string.form_saved_as_draft))\n        assertThat(formSavedSnackbarType.action, equalTo(org.odk.collect.strings.R.string.view_form))\n    }\n\n    @Test\n    fun `setSavedForm should set proper message and action when the corresponding instance is finalized, auto send is disabled and encryption is disabled`() {\n        val viewModel = createViewModelWithVersion(\"\")\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build())\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n        whenever(autoSendSettingsProvider.isAutoSendEnabledInSettings()).thenReturn(false)\n\n        val uri = InstancesContract.getUri(Project.DEMO_PROJECT_ID, instance.dbId)\n\n        viewModel.setSavedForm(uri)\n        scheduler.flush()\n\n        val formSavedSnackbarType = viewModel.savedForm.getOrAwaitValue().value!!\n        assertThat(formSavedSnackbarType.message, equalTo(org.odk.collect.strings.R.string.form_saved))\n        assertThat(formSavedSnackbarType.action, equalTo(org.odk.collect.strings.R.string.view_form))\n    }\n\n    @Test\n    fun `setSavedForm should set proper message and action when the corresponding instance is finalized, auto send is disabled and encryption is enabled`() {\n        val viewModel = createViewModelWithVersion(\"\")\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build())\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .canEditWhenComplete(false)\n                .build()\n        )\n        whenever(autoSendSettingsProvider.isAutoSendEnabledInSettings()).thenReturn(false)\n\n        val uri = InstancesContract.getUri(Project.DEMO_PROJECT_ID, instance.dbId)\n\n        viewModel.setSavedForm(uri)\n        scheduler.flush()\n\n        val formSavedSnackbarType = viewModel.savedForm.getOrAwaitValue().value!!\n        assertThat(formSavedSnackbarType.message, equalTo(org.odk.collect.strings.R.string.form_saved))\n        assertThat(formSavedSnackbarType.action, equalTo(null))\n    }\n\n    @Test\n    fun `setSavedForm should set proper message and action when the corresponding instance is finalized, auto send is enabled and encryption is disabled`() {\n        val viewModel = createViewModelWithVersion(\"\")\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build())\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n\n        whenever(autoSendSettingsProvider.isAutoSendEnabledInSettings()).thenReturn(true)\n\n        val uri = InstancesContract.getUri(Project.DEMO_PROJECT_ID, instance.dbId)\n\n        viewModel.setSavedForm(uri)\n        scheduler.flush()\n\n        val formSavedSnackbarType = viewModel.savedForm.getOrAwaitValue().value!!\n        assertThat(formSavedSnackbarType.message, equalTo(org.odk.collect.strings.R.string.form_sending))\n        assertThat(formSavedSnackbarType.action, equalTo(org.odk.collect.strings.R.string.view_form))\n    }\n\n    @Test\n    fun `setSavedForm should set proper message and action when the corresponding instance is finalized, auto send is enabled and encryption is enabled`() {\n        val viewModel = createViewModelWithVersion(\"\")\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build())\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .canEditWhenComplete(false)\n                .build()\n        )\n\n        whenever(autoSendSettingsProvider.isAutoSendEnabledInSettings()).thenReturn(true)\n\n        val uri = InstancesContract.getUri(Project.DEMO_PROJECT_ID, instance.dbId)\n\n        viewModel.setSavedForm(uri)\n        scheduler.flush()\n\n        val formSavedSnackbarType = viewModel.savedForm.getOrAwaitValue().value!!\n        assertThat(formSavedSnackbarType.message, equalTo(org.odk.collect.strings.R.string.form_sending))\n        assertThat(formSavedSnackbarType.action, equalTo(null))\n    }\n\n    @Test(expected = TimeoutException::class)\n    fun `setSavedForm should not set when the corresponding instance is already sent`() {\n        val viewModel = createViewModelWithVersion(\"\")\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build())\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_SUBMITTED)\n                .build()\n        )\n\n        whenever(autoSendSettingsProvider.isAutoSendEnabledInSettings()).thenReturn(true)\n\n        val uri = InstancesContract.getUri(Project.DEMO_PROJECT_ID, instance.dbId)\n        viewModel.setSavedForm(uri)\n        scheduler.flush()\n\n        viewModel.savedForm.getOrAwaitValue()\n    }\n\n    @Test\n    fun `setSavedForm should set proper message and action when the corresponding instance failed to sent`() {\n        val viewModel = createViewModelWithVersion(\"\")\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", TempFiles.createTempDir().absolutePath).build())\n        val instance = instancesRepository.save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"1\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_SUBMISSION_FAILED)\n                .build()\n        )\n\n        whenever(autoSendSettingsProvider.isAutoSendEnabledInSettings()).thenReturn(true)\n\n        val uri = InstancesContract.getUri(Project.DEMO_PROJECT_ID, instance.dbId)\n\n        viewModel.setSavedForm(uri)\n        scheduler.flush()\n\n        val formSavedSnackbarType = viewModel.savedForm.getOrAwaitValue().value!!\n        assertThat(formSavedSnackbarType.message, equalTo(org.odk.collect.strings.R.string.form_sending))\n        assertThat(formSavedSnackbarType.action, equalTo(org.odk.collect.strings.R.string.view_form))\n    }\n\n    private fun createViewModelWithVersion(version: String): MainMenuViewModel {\n        return MainMenuViewModel(mock(), VersionInformation { version }, settingsProvider, instancesDataService, scheduler, formsRepositoryProvider, instancesRepositoryProvider, autoSendSettingsProvider, projectsDataService)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/mainmenu/PermissionsDialogFragmentTest.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.R\nimport org.odk.collect.android.fakes.FakePermissionsProvider\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\n\n@RunWith(AndroidJUnit4::class)\nclass PermissionsDialogFragmentTest {\n\n    private val permissionsProvider = FakePermissionsProvider()\n    private val requestPermissionsViewModel = mock<RequestPermissionsViewModel> {\n        on { permissions } doReturn arrayOf(\"blah\")\n        on { shouldAskForPermissions() } doReturn true\n    }\n\n    private val fragmentFactory = FragmentFactoryBuilder()\n        .forClass(PermissionsDialogFragment::class) {\n            PermissionsDialogFragment(permissionsProvider, requestPermissionsViewModel)\n        }\n        .build()\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule(fragmentFactory)\n\n    @Test\n    fun clickingOK_asksForPermissions() {\n        launcherRule.launch(PermissionsDialogFragment::class.java)\n\n        onView(withText(org.odk.collect.strings.R.string.ok)).inRoot(isDialog()).perform(click())\n        assertThat(\n            permissionsProvider.requestedPermissions,\n            equalTo(listOf(\"blah\"))\n        )\n    }\n\n    @Test\n    fun clickingOK_callsPermissionRequested() {\n        launcherRule.launch(PermissionsDialogFragment::class.java)\n\n        onView(withText(org.odk.collect.strings.R.string.ok)).inRoot(isDialog()).perform(click())\n        verify(requestPermissionsViewModel).permissionsRequested()\n    }\n\n    @Test\n    fun recreating_doesNotCallPermissionsRequested() {\n        launcherRule.launch(PermissionsDialogFragment::class.java).recreate()\n        verify(requestPermissionsViewModel, never()).permissionsRequested()\n    }\n\n    @Test\n    fun `The dialog should not be dismissed after clicking out of its area or on device back button`() {\n        val scenario = launcherRule.launch(PermissionsDialogFragment::class.java)\n        scenario.onFragment {\n            assertThat(it.isCancelable, equalTo(false))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/mainmenu/RequestPermissionsViewModelTest.kt",
    "content": "package org.odk.collect.android.mainmenu\n\nimport android.Manifest.permission.POST_NOTIFICATIONS\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.MetaKeys\nimport org.robolectric.annotation.Config\n\n@RunWith(AndroidJUnit4::class)\nclass RequestPermissionsViewModelTest {\n\n    private val permissionChecker = mock<PermissionsChecker> {\n        on { isPermissionGranted(any()) } doReturn false\n    }\n\n    private val settingsProvider = InMemSettingsProvider()\n\n    @Test\n    fun shouldAskForPermissions_returnsTrue() {\n        val viewModel = RequestPermissionsViewModel(settingsProvider, permissionChecker)\n        assertThat(viewModel.shouldAskForPermissions(), equalTo(true))\n    }\n\n    @Test\n    @Config(sdk = [32])\n    fun shouldAskForPermissions_whenAPIDoesNotNeedNotificationPermissions_returnsFalse() {\n        val viewModel = RequestPermissionsViewModel(settingsProvider, permissionChecker)\n        assertThat(viewModel.shouldAskForPermissions(), equalTo(false))\n    }\n\n    @Test\n    fun shouldAskForPermissions_whenPermissionsHaveAlreadyBeenGranted_returnsFalse() {\n        whenever(permissionChecker.isPermissionGranted(POST_NOTIFICATIONS)).doReturn(true)\n\n        val viewModel = RequestPermissionsViewModel(settingsProvider, permissionChecker)\n        assertThat(viewModel.shouldAskForPermissions(), equalTo(false))\n    }\n\n    @Test\n    fun shouldAskForPermissions_whenPermissionsHaveAlreadyBeenAskFor_returnsFalse() {\n        settingsProvider.getMetaSettings().save(MetaKeys.PERMISSIONS_REQUESTED, true)\n\n        val viewModel = RequestPermissionsViewModel(settingsProvider, permissionChecker)\n        assertThat(viewModel.shouldAskForPermissions(), equalTo(false))\n    }\n\n    @Test\n    fun permissionsRequested_marksPermissionsAsRequestedInSettings() {\n        val viewModel = RequestPermissionsViewModel(settingsProvider, permissionChecker)\n        viewModel.permissionsRequested()\n\n        assertThat(\n            settingsProvider.getMetaSettings().getBoolean(MetaKeys.PERMISSIONS_REQUESTED),\n            equalTo(true)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/notifications/NotificationManagerNotifierTest.kt",
    "content": "package org.odk.collect.android.notifications\n\nimport android.app.Application\nimport android.app.Notification\nimport android.app.NotificationManager\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.TestSettingsProvider\nimport org.odk.collect.android.formlists.blankformlist.BlankFormListActivity\nimport org.odk.collect.android.formmanagement.ServerFormDetails\nimport org.odk.collect.androidshared.utils.InMemUniqueIdGenerator\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.forms.ManifestFile\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.strings.localization.getLocalizedString\nimport org.robolectric.Shadows\n\n@RunWith(AndroidJUnit4::class)\nclass NotificationManagerNotifierTest {\n    private lateinit var notifier: NotificationManagerNotifier\n    private lateinit var notificationManager: NotificationManager\n    private val projectsRepository: ProjectsRepository = InMemProjectsRepository().apply {\n        save(Project.DEMO_PROJECT)\n    }\n\n    @Before\n    fun setup() {\n        val context = ApplicationProvider.getApplicationContext<Application>()\n        notificationManager =\n            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n        notifier = NotificationManagerNotifier(\n            context,\n            TestSettingsProvider.getSettingsProvider(),\n            projectsRepository,\n            InMemUniqueIdGenerator()\n        )\n    }\n\n    @Test\n    fun onSync_whenExceptionNull_clearsNotification() {\n        notifier.onSync(FormSourceException.FetchError(), Project.DEMO_PROJECT_ID)\n        assertThat(\n            Shadows.shadowOf(notificationManager).allNotifications.size,\n            equalTo(1)\n        )\n        notifier.onSync(null, Project.DEMO_PROJECT_ID)\n        assertThat(\n            Shadows.shadowOf(notificationManager).allNotifications.size,\n            equalTo(0)\n        )\n    }\n\n    @Test\n    fun onSyncStopped_sendsNotification() {\n        notifier.onSyncStopped(Project.DEMO_PROJECT_ID)\n\n        val context = ApplicationProvider.getApplicationContext<Application>()\n\n        val notifications = Shadows.shadowOf(notificationManager).allNotifications\n        val notification = notifications[0]\n        val intent = Shadows.shadowOf(notification.contentIntent).savedIntent\n\n        assertThat(notifications.size, equalTo(1))\n        assertThat(\n            notification.extras.getString(Notification.EXTRA_TITLE),\n            equalTo(context.getLocalizedString(org.odk.collect.strings.R.string.form_update_error))\n        )\n        assertThat(\n            notification.extras.getString(Notification.EXTRA_TEXT),\n            equalTo(context.getLocalizedString(org.odk.collect.strings.R.string.form_update_error_massage))\n        )\n        assertThat(\n            notification.extras.getString(Notification.EXTRA_SUB_TEXT),\n            equalTo(Project.DEMO_PROJECT_NAME)\n        )\n        assertThat(\n            intent.component?.className,\n            equalTo(BlankFormListActivity::class.java.name)\n        )\n    }\n\n    @Test\n    fun onUpdatesAvailable_whenUpdatesHaveBeenSeenBefore_doesNotNotifyASecondTime() {\n        val updates = listOf(\n            ServerFormDetails(\n                \"form-1\",\n                \"http://example.com/form-1\",\n                \"form-1\",\n                \"server\",\n                \"form-1-hash\",\n                false,\n                true,\n                null\n            )\n        )\n        notifier.onUpdatesAvailable(updates, Project.DEMO_PROJECT_ID)\n        assertThat(\n            Shadows.shadowOf(notificationManager).allNotifications.size,\n            equalTo(1)\n        )\n        notificationManager.cancelAll()\n        notifier.onUpdatesAvailable(updates, Project.DEMO_PROJECT_ID)\n        assertThat(\n            Shadows.shadowOf(notificationManager).allNotifications.size,\n            equalTo(0)\n        )\n    }\n\n    @Test\n    fun onUpdatesAvailable_whenFormHasNewHash_notifies() {\n        var updates = listOf(\n            ServerFormDetails(\n                \"form-1\",\n                \"http://example.com/form-1\",\n                \"form-1\",\n                \"server\",\n                \"form-1-hash\",\n                false,\n                true,\n                null\n            )\n        )\n        notifier.onUpdatesAvailable(updates, Project.DEMO_PROJECT_ID)\n        assertThat(\n            Shadows.shadowOf(notificationManager).allNotifications.size,\n            equalTo(1)\n        )\n        updates = listOf(\n            ServerFormDetails(\n                \"form-1\",\n                \"http://example.com/form-1\",\n                \"form-1\",\n                \"server\",\n                \"form-1-hash-changed\",\n                false,\n                true,\n                null\n            )\n        )\n        notificationManager.cancelAll()\n        notifier.onUpdatesAvailable(updates, Project.DEMO_PROJECT_ID)\n        assertThat(\n            Shadows.shadowOf(notificationManager).allNotifications.size,\n            equalTo(1)\n        )\n    }\n\n    @Test\n    fun onUpdatesAvailable_whenFormHasNewManifestHash_notifies() {\n        var updates = listOf(\n            ServerFormDetails(\n                \"form-1\",\n                \"http://example.com/form-1\",\n                \"form-1\",\n                \"server\",\n                \"form-1-hash\",\n                false,\n                true,\n                ManifestFile(\"manifest-hash\", emptyList())\n            )\n        )\n        notifier.onUpdatesAvailable(updates, Project.DEMO_PROJECT_ID)\n        assertThat(\n            Shadows.shadowOf(notificationManager).allNotifications.size,\n            equalTo(1)\n        )\n        updates = listOf(\n            ServerFormDetails(\n                \"form-1\",\n                \"http://example.com/form-1\",\n                \"form-1\",\n                \"server\",\n                \"form-1-hash\",\n                false,\n                true,\n                ManifestFile(\"manifest-hash-changed\", emptyList())\n            )\n        )\n        notificationManager.cancelAll()\n        notifier.onUpdatesAvailable(updates, Project.DEMO_PROJECT_ID)\n        assertThat(\n            Shadows.shadowOf(notificationManager).allNotifications.size,\n            equalTo(1)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/AppConfigurationGeneratorTest.kt",
    "content": "package org.odk.collect.android.preferences\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.json.JSONObject\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.configure.qr.AppConfigurationGenerator\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.AppConfigurationKeys\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\n\n@RunWith(AndroidJUnit4::class)\nclass AppConfigurationGeneratorTest {\n\n    private val settingsProvider = InMemSettingsProvider()\n\n    private val projectsDataService: ProjectsDataService = mock {\n        on { requireCurrentProject() } doReturn Project.Saved(\"1\", \"Project X\", \"X\", \"#cccccc\")\n    }\n\n    val projectDetails = mapOf(\n        AppConfigurationKeys.PROJECT_NAME to \"Project X\",\n        AppConfigurationKeys.PROJECT_ICON to \"X\",\n        AppConfigurationKeys.PROJECT_COLOR to \"#cccccc\"\n    )\n\n    private lateinit var appConfigurationGenerator: AppConfigurationGenerator\n\n    @Before\n    fun setup() {\n        appConfigurationGenerator =\n            AppConfigurationGenerator(settingsProvider, projectsDataService)\n    }\n\n    @Test\n    fun `When admin password included, should be present in json`() {\n        val adminPrefs = mapOf<String, Any>(ProtectedProjectKeys.KEY_ADMIN_PW to \"123456\")\n\n        settingsProvider.getProtectedSettings().saveAll(adminPrefs)\n\n        val jsonPrefs = appConfigurationGenerator.getAppConfigurationAsJson(\n            listOf(\n                ProtectedProjectKeys.KEY_ADMIN_PW\n            )\n        )\n        verifyJsonContent(jsonPrefs, emptyMap<String, Any>(), adminPrefs, projectDetails)\n    }\n\n    @Test\n    fun `When admin password excluded, should not be present in json`() {\n        val adminPrefs = mapOf<String, Any>(ProtectedProjectKeys.KEY_ADMIN_PW to \"123456\")\n\n        settingsProvider.getProtectedSettings().saveAll(adminPrefs)\n\n        val jsonPrefs = appConfigurationGenerator.getAppConfigurationAsJson()\n        verifyJsonContent(\n            jsonPrefs,\n            emptyMap<String, Any>(),\n            emptyMap<String, Any>(),\n            projectDetails\n        )\n    }\n\n    @Test\n    fun `When user password included, should be present in json`() {\n        val generalPrefs = mapOf<String, Any>(ProjectKeys.KEY_PASSWORD to \"123456\")\n\n        settingsProvider.getUnprotectedSettings().saveAll(generalPrefs)\n\n        val jsonPrefs =\n            appConfigurationGenerator.getAppConfigurationAsJson(listOf(ProjectKeys.KEY_PASSWORD))\n        verifyJsonContent(jsonPrefs, generalPrefs, emptyMap<String, Any>(), projectDetails)\n    }\n\n    @Test\n    fun `When user password excluded, should not be present in json`() {\n        val generalPrefs = mapOf<String, Any>(ProjectKeys.KEY_PASSWORD to \"123456\")\n\n        settingsProvider.getUnprotectedSettings().saveAll(generalPrefs)\n\n        val jsonPrefs = appConfigurationGenerator.getAppConfigurationAsJson()\n        verifyJsonContent(\n            jsonPrefs,\n            emptyMap<String, Any>(),\n            emptyMap<String, Any>(),\n            projectDetails\n        )\n    }\n\n    @Test\n    fun `Only saved settings should be included in json`() {\n        val generalPrefs = mapOf<String, Any>(\n            ProjectKeys.KEY_DELETE_AFTER_SEND to true,\n            ProjectKeys.KEY_GUIDANCE_HINT to \"no\"\n        )\n\n        val adminPrefs = mapOf<String, Any>(\n            ProtectedProjectKeys.KEY_GET_BLANK to false,\n            ProtectedProjectKeys.KEY_DELETE_SAVED to false\n        )\n\n        settingsProvider.getUnprotectedSettings().saveAll(generalPrefs)\n        settingsProvider.getProtectedSettings().saveAll(adminPrefs)\n\n        val jsonPrefs = appConfigurationGenerator.getAppConfigurationAsJson()\n\n        verifyJsonContent(jsonPrefs, generalPrefs, adminPrefs, projectDetails)\n    }\n\n    @Test\n    fun `Saved but default settings should not be included in json`() {\n        val generalPrefs = Defaults.unprotected.filter {\n            it.key == ProjectKeys.KEY_DELETE_AFTER_SEND || it.key == ProjectKeys.KEY_GUIDANCE_HINT\n        }\n\n        val adminPrefs = Defaults.protected.filter {\n            it.key == ProtectedProjectKeys.KEY_GET_BLANK || it.key == ProtectedProjectKeys.KEY_DELETE_SAVED\n        }\n\n        settingsProvider.getUnprotectedSettings().saveAll(generalPrefs)\n        settingsProvider.getProtectedSettings().saveAll(adminPrefs)\n\n        val jsonPrefs = appConfigurationGenerator.getAppConfigurationAsJson()\n\n        verifyJsonContent(\n            jsonPrefs,\n            emptyMap<String, Any>(),\n            emptyMap<String, Any>(),\n            projectDetails\n        )\n    }\n\n    @Test\n    fun `When server details provided should getAppConfigurationAsJsonWithServerDetails() generate proper json`() {\n        val generalPrefs = mapOf<String, Any>(\n            ProjectKeys.KEY_SERVER_URL to \"https://my-server.com\",\n            ProjectKeys.KEY_USERNAME to \"adam\",\n            ProjectKeys.KEY_PASSWORD to \"1234\"\n        )\n\n        val jsonPrefs = appConfigurationGenerator.getAppConfigurationAsJsonWithServerDetails(\n            \"https://my-server.com\",\n            \"adam\",\n            \"1234\"\n        )\n\n        verifyJsonContent(jsonPrefs, generalPrefs, emptyMap<String, Any>(), emptyMap())\n    }\n\n    private fun verifyJsonContent(\n        jsonPrefsString: String,\n        generalPrefs: Map<String, *>,\n        adminPrefs: Map<String, *>,\n        projectDetails: Map<String, String>\n    ) {\n        val jsonPrefs = JSONObject(jsonPrefsString)\n        assertThat(jsonPrefs.length(), `is`(3))\n        assertThat(jsonPrefs.has(AppConfigurationKeys.GENERAL), `is`(true))\n        assertThat(jsonPrefs.has(AppConfigurationKeys.ADMIN), `is`(true))\n        assertThat(jsonPrefs.has(AppConfigurationKeys.PROJECT), `is`(true))\n\n        val jsonGeneralPrefs = jsonPrefs.get(AppConfigurationKeys.GENERAL) as JSONObject\n        assertThat(jsonGeneralPrefs.length(), `is`(generalPrefs.size))\n        generalPrefs.entries.forEach {\n            assertThat(jsonGeneralPrefs.get(it.key), `is`(it.value))\n        }\n\n        val jsonAdminPrefs = jsonPrefs.get(AppConfigurationKeys.ADMIN) as JSONObject\n        assertThat(jsonAdminPrefs.length(), `is`(adminPrefs.size))\n        adminPrefs.entries.forEach {\n            assertThat(jsonAdminPrefs.get(it.key), `is`(it.value))\n        }\n\n        val projectDetailsJson = jsonPrefs.get(AppConfigurationKeys.PROJECT) as JSONObject\n        assertThat(projectDetailsJson.length(), `is`(projectDetails.size))\n        projectDetails.entries.forEach {\n            assertThat(projectDetailsJson.get(it.key), `is`(it.value))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/ProjectPreferencesViewModelTest.kt",
    "content": "package org.odk.collect.android.preferences\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.utilities.AdminPasswordProvider\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.testshared.RobolectricHelpers\n\n@RunWith(AndroidJUnit4::class)\nclass ProjectPreferencesViewModelTest {\n\n    private lateinit var projectPreferencesViewModel: ProjectPreferencesViewModel\n    private val adminPasswordProvider = mock<AdminPasswordProvider>()\n\n    @Test\n    fun `When admin password is set the initial stat should be Locked`() {\n        whenever(adminPasswordProvider.isAdminPasswordSet).thenReturn(true)\n\n        projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n        RobolectricHelpers.runLooper()\n\n        assertThat(projectPreferencesViewModel.state.value, `is`(Consumable(ProjectPreferencesViewModel.State.LOCKED)))\n    }\n\n    @Test\n    fun `When admin password is not set the initial stat should be NotProtected`() {\n        whenever(adminPasswordProvider.isAdminPasswordSet).thenReturn(false)\n\n        projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n        RobolectricHelpers.runLooper()\n\n        assertThat(projectPreferencesViewModel.state.value, `is`(Consumable(ProjectPreferencesViewModel.State.NOT_PROTECTED)))\n    }\n\n    @Test\n    fun `setStateLocked() should set state to Locked`() {\n        whenever(adminPasswordProvider.isAdminPasswordSet).thenReturn(false)\n\n        projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n        projectPreferencesViewModel.setStateLocked()\n        RobolectricHelpers.runLooper()\n\n        assertThat(projectPreferencesViewModel.state.value, `is`(Consumable(ProjectPreferencesViewModel.State.LOCKED)))\n    }\n\n    @Test\n    fun `setStateUnlocked() should set state to Unlocked`() {\n        whenever(adminPasswordProvider.isAdminPasswordSet).thenReturn(false)\n\n        projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n        projectPreferencesViewModel.setStateUnlocked()\n        RobolectricHelpers.runLooper()\n\n        assertThat(projectPreferencesViewModel.state.value, `is`(Consumable(ProjectPreferencesViewModel.State.UNLOCKED)))\n    }\n\n    @Test\n    fun `setStateNotProtected() should set state to NotProtected`() {\n        whenever(adminPasswordProvider.isAdminPasswordSet).thenReturn(false)\n\n        projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n        projectPreferencesViewModel.setStateNotProtected()\n        RobolectricHelpers.runLooper()\n\n        assertThat(projectPreferencesViewModel.state.value, `is`(Consumable(ProjectPreferencesViewModel.State.NOT_PROTECTED)))\n    }\n\n    @Test\n    fun `isStateLocked() should return true if state is Locked`() {\n        whenever(adminPasswordProvider.isAdminPasswordSet).thenReturn(false)\n\n        projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n\n        projectPreferencesViewModel.setStateLocked()\n        RobolectricHelpers.runLooper()\n        assertThat(projectPreferencesViewModel.isStateLocked(), `is`(true))\n\n        projectPreferencesViewModel.setStateUnlocked()\n        RobolectricHelpers.runLooper()\n        assertThat(projectPreferencesViewModel.isStateLocked(), `is`(false))\n\n        projectPreferencesViewModel.setStateNotProtected()\n        RobolectricHelpers.runLooper()\n        assertThat(projectPreferencesViewModel.isStateLocked(), `is`(false))\n    }\n\n    @Test\n    fun `isStateUnlocked() should return true if state is Unlocked`() {\n        whenever(adminPasswordProvider.isAdminPasswordSet).thenReturn(false)\n\n        projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n\n        projectPreferencesViewModel.setStateLocked()\n        RobolectricHelpers.runLooper()\n        assertThat(projectPreferencesViewModel.isStateUnlocked(), `is`(false))\n\n        projectPreferencesViewModel.setStateUnlocked()\n        RobolectricHelpers.runLooper()\n        assertThat(projectPreferencesViewModel.isStateUnlocked(), `is`(true))\n\n        projectPreferencesViewModel.setStateNotProtected()\n        RobolectricHelpers.runLooper()\n        assertThat(projectPreferencesViewModel.isStateUnlocked(), `is`(false))\n    }\n\n    @Test\n    fun `isStateNotProtected() should return true if state is NotProtected`() {\n        whenever(adminPasswordProvider.isAdminPasswordSet).thenReturn(false)\n\n        projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n\n        projectPreferencesViewModel.setStateLocked()\n        RobolectricHelpers.runLooper()\n        assertThat(projectPreferencesViewModel.isStateNotProtected(), `is`(false))\n\n        projectPreferencesViewModel.setStateUnlocked()\n        RobolectricHelpers.runLooper()\n        assertThat(projectPreferencesViewModel.isStateNotProtected(), `is`(false))\n\n        projectPreferencesViewModel.setStateNotProtected()\n        RobolectricHelpers.runLooper()\n        assertThat(projectPreferencesViewModel.isStateNotProtected(), `is`(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/ServerPreferencesAdderTest.java",
    "content": "package org.odk.collect.android.preferences;\n\nimport static junit.framework.Assert.assertFalse;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.preference.PreferenceFragmentCompat;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.R;\nimport org.robolectric.Robolectric;\nimport org.robolectric.shadows.ShadowToast;\n\n@RunWith(AndroidJUnit4.class)\npublic class ServerPreferencesAdderTest {\n\n    @Test\n    public void whenPreferencesAreAdded_returnsTrue() {\n        PreferenceFragmentCompat fragment = mock(PreferenceFragmentCompat.class);\n        ServerPreferencesAdder loader = new ServerPreferencesAdder(fragment);\n\n        boolean result = loader.add();\n        assertTrue(result);\n    }\n\n    @Test\n    public void whenAPreferenceHasAnIncorrectType_returnsFalse_andShowsToastError() {\n        FragmentActivity activity = Robolectric.setupActivity(FragmentActivity.class);\n        PreferenceFragmentCompat fragment = mock(PreferenceFragmentCompat.class);\n\n        doThrow(ClassCastException.class).when(fragment).addPreferencesFromResource(R.xml.odk_server_preferences);\n        when(fragment.getActivity()).thenReturn(activity);\n\n        ServerPreferencesAdder loader = new ServerPreferencesAdder(fragment);\n        boolean result = loader.add();\n        assertFalse(result);\n\n        String toastText = ShadowToast.getTextOfLatestToast();\n        assertEquals(toastText, getString(org.odk.collect.strings.R.string.corrupt_imported_preferences_error));\n    }\n\n    private String getString(int id) {\n        return ApplicationProvider.getApplicationContext().getString(id);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/dialogs/AdminPasswordDialogFragmentTest.kt",
    "content": "package org.odk.collect.android.preferences.dialogs\n\nimport android.text.InputType\nimport androidx.appcompat.app.AlertDialog\nimport androidx.lifecycle.ViewModel\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.pressBack\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.hamcrest.Matchers.nullValue\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoMoreInteractions\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.utilities.AdminPasswordProvider\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.testshared.RobolectricHelpers\nimport javax.inject.Inject\n\n@RunWith(AndroidJUnit4::class)\nclass AdminPasswordDialogFragmentTest {\n\n    private val adminPasswordProvider = mock<AdminPasswordProvider>()\n    private val projectPreferencesViewModel = mock<ProjectPreferencesViewModel>()\n\n    @Inject\n    lateinit var factory: ProjectPreferencesViewModel.Factory\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesAdminPasswordProvider(settingsProvider: SettingsProvider): AdminPasswordProvider {\n                return adminPasswordProvider\n            }\n\n            override fun providesProjectPreferencesViewModel(adminPasswordProvider: AdminPasswordProvider): ProjectPreferencesViewModel.Factory {\n                return object : ProjectPreferencesViewModel.Factory(adminPasswordProvider) {\n                    override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                        return projectPreferencesViewModel as T\n                    }\n                }\n            }\n        })\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on a device back button`() {\n        val scenario = launcherRule.launch(AdminPasswordDialogFragment::class.java)\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            onView(isRoot()).perform(pressBack())\n            assertThat(it.dialog, `is`(nullValue()))\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on 'OK'`() {\n        val scenario = launcherRule.launch(AdminPasswordDialogFragment::class.java)\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            (it.dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_POSITIVE).performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(nullValue()))\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on 'CANCEL'`() {\n        val scenario = launcherRule.launch(AdminPasswordDialogFragment::class.java)\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            (it.dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_NEGATIVE).performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(nullValue()))\n        }\n    }\n\n    @Test\n    fun `Entering correct password sets Unlocked state in view model`() {\n        whenever(adminPasswordProvider.isAdminPasswordSet).thenReturn(true)\n        whenever(adminPasswordProvider.adminPassword).thenReturn(\"password\")\n\n        val scenario = launcherRule.launch(AdminPasswordDialogFragment::class.java)\n        scenario.onFragment {\n            it.binding.editText.setText(\"password\")\n            (it.dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_POSITIVE).performClick()\n            RobolectricHelpers.runLooper()\n            verify(projectPreferencesViewModel).setStateUnlocked()\n            verifyNoMoreInteractions(projectPreferencesViewModel)\n        }\n    }\n\n    @Test\n    fun `Entering incorrect password does not set any state in view model`() {\n        whenever(adminPasswordProvider.isAdminPasswordSet).thenReturn(true)\n        whenever(adminPasswordProvider.adminPassword).thenReturn(\"password\")\n\n        val scenario = launcherRule.launch(AdminPasswordDialogFragment::class.java)\n        scenario.onFragment {\n            it.binding.editText.setText(\"incorrect_password\")\n            (it.dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_POSITIVE).performClick()\n            RobolectricHelpers.runLooper()\n            verifyNoMoreInteractions(projectPreferencesViewModel)\n        }\n    }\n\n    @Test\n    fun `When screen is rotated password and checkbox value is retained`() {\n        val scenario = launcherRule.launch(AdminPasswordDialogFragment::class.java)\n        scenario.onFragment {\n            it.binding.editText.setText(\"password\")\n            it.binding.checkBox.performClick()\n            scenario.recreate()\n            assertThat(it.binding.editText.text.toString(), `is`(\"password\"))\n            assertThat(it.binding.checkBox.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `'Show password' displays and hides password`() {\n        val scenario = launcherRule.launch(AdminPasswordDialogFragment::class.java)\n        scenario.onFragment {\n            it.binding.checkBox.performClick()\n            assertThat(it.binding.editText.inputType, `is`(InputType.TYPE_TEXT_VARIATION_PASSWORD))\n            it.binding.checkBox.performClick()\n            assertThat(it.binding.editText.inputType, `is`(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/dialogs/ChangeAdminPasswordDialogTest.kt",
    "content": "package org.odk.collect.android.preferences.dialogs\n\nimport android.content.Context\nimport android.text.InputType\nimport androidx.appcompat.app.AlertDialog\nimport androidx.lifecycle.ViewModel\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.action.ViewActions\nimport androidx.test.espresso.matcher.ViewMatchers\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoMoreInteractions\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.utilities.AdminPasswordProvider\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.testshared.RobolectricHelpers\nimport javax.inject.Inject\n\n@RunWith(AndroidJUnit4::class)\nclass ChangeAdminPasswordDialogTest {\n\n    private val settingsProvider = InMemSettingsProvider()\n    private val projectPreferencesViewModel = mock<ProjectPreferencesViewModel>()\n\n    @Inject\n    lateinit var factory: ProjectPreferencesViewModel.Factory\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesSettingsProvider(context: Context?): SettingsProvider {\n                return settingsProvider\n            }\n\n            override fun providesProjectPreferencesViewModel(adminPasswordProvider: AdminPasswordProvider): ProjectPreferencesViewModel.Factory {\n                return object : ProjectPreferencesViewModel.Factory(adminPasswordProvider) {\n                    override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                        return projectPreferencesViewModel as T\n                    }\n                }\n            }\n        })\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on a device back button`() {\n        val scenario = launcherRule.launch(ChangeAdminPasswordDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            Espresso.onView(ViewMatchers.isRoot()).perform(ViewActions.pressBack())\n            assertThat(it.dialog, `is`(Matchers.nullValue()))\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on 'OK'`() {\n        val scenario = launcherRule.launch(ChangeAdminPasswordDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            (it.dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_POSITIVE).performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(Matchers.nullValue()))\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on 'CANCEL'`() {\n        val scenario = launcherRule.launch(ChangeAdminPasswordDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, Matchers.`is`(true))\n            (it.dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_NEGATIVE).performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(Matchers.nullValue()))\n        }\n    }\n\n    @Test\n    fun `Setting password and accepting updates the password in settings`() {\n        val scenario = launcherRule.launch(ChangeAdminPasswordDialog::class.java)\n        scenario.onFragment {\n            settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_ADMIN_PW, \"\")\n            it.binding.pwdField.setText(\"password\")\n            (it.dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_POSITIVE).performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(settingsProvider.getProtectedSettings().getString(ProtectedProjectKeys.KEY_ADMIN_PW), `is`(\"password\"))\n        }\n    }\n\n    @Test\n    fun `Setting password and canceling does not update the password in settings`() {\n        val scenario = launcherRule.launch(ChangeAdminPasswordDialog::class.java)\n        scenario.onFragment {\n            settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_ADMIN_PW, \"\")\n            it.binding.pwdField.setText(\"password\")\n            (it.dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_NEGATIVE).performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(settingsProvider.getProtectedSettings().getString(ProtectedProjectKeys.KEY_ADMIN_PW), `is`(\"\"))\n        }\n    }\n\n    @Test\n    fun `Setting password sets Unlocked state in view model`() {\n        val scenario = launcherRule.launch(ChangeAdminPasswordDialog::class.java)\n        scenario.onFragment {\n            it.binding.pwdField.setText(\"password\")\n            (it.dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_POSITIVE).performClick()\n            RobolectricHelpers.runLooper()\n            verify(projectPreferencesViewModel).setStateUnlocked()\n            verifyNoMoreInteractions(projectPreferencesViewModel)\n        }\n    }\n\n    @Test\n    fun `Removing password sets NotProtected state in view model`() {\n        val scenario = launcherRule.launch(ChangeAdminPasswordDialog::class.java)\n        scenario.onFragment {\n            it.binding.pwdField.setText(\"\")\n            (it.dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_POSITIVE).performClick()\n            RobolectricHelpers.runLooper()\n            verify(projectPreferencesViewModel).setStateNotProtected()\n            verifyNoMoreInteractions(projectPreferencesViewModel)\n        }\n    }\n\n    @Test\n    fun `When screen is rotated password and checkbox value is retained`() {\n        val scenario = launcherRule.launch(ChangeAdminPasswordDialog::class.java)\n        scenario.onFragment {\n            it.binding.pwdField.setText(\"password\")\n            it.binding.checkBox2.performClick()\n            scenario.recreate()\n            assertThat(it.binding.pwdField.text.toString(), `is`(\"password\"))\n            assertThat(it.binding.checkBox2.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `'Show password' displays and hides password`() {\n        val scenario = launcherRule.launch(ChangeAdminPasswordDialog::class.java)\n        scenario.onFragment {\n            it.binding.checkBox2.performClick()\n            assertThat(it.binding.pwdField.inputType, `is`(InputType.TYPE_TEXT_VARIATION_PASSWORD))\n            it.binding.checkBox2.performClick()\n            assertThat(it.binding.pwdField.inputType, `is`(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/dialogs/DeleteProjectDialogTest.kt",
    "content": "package org.odk.collect.android.preferences.dialogs\n\nimport android.app.Application\nimport android.text.TextUtils\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.pressBack\nimport androidx.test.espresso.action.ViewActions.replaceText\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.espresso.matcher.ViewMatchers.isClickable\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.isEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.work.WorkManager\nimport com.google.android.material.textview.MaterialTextView\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.not\nimport org.hamcrest.Matchers.containsString\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.Mockito.mock\nimport org.mockito.Mockito.verify\nimport org.mockito.kotlin.verifyNoInteractions\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.backgroundwork.FormUpdateScheduler\nimport org.odk.collect.android.backgroundwork.InstanceSubmitScheduler\nimport org.odk.collect.android.formmanagement.FormsDataService\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.instancemanagement.InstancesDataService\nimport org.odk.collect.android.projects.DeleteProjectResult\nimport org.odk.collect.android.projects.ProjectDeleter\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.testshared.EspressoAssertions\nimport org.odk.collect.testshared.FakeScheduler\n\n@RunWith(AndroidJUnit4::class)\nclass DeleteProjectDialogTest {\n    private val projectDataService = mock<ProjectsDataService>()\n    private val projectDeleter = mock<ProjectDeleter>()\n    private val formsDataService = mock<FormsDataService>()\n    private val instancesDataService = mock<InstancesDataService>()\n    private val scheduler = FakeScheduler()\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n\n    private lateinit var projectId: String\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule(\n        FragmentFactoryBuilder()\n            .forClass(DeleteProjectDialog::class) {\n                DeleteProjectDialog(\n                    projectDeleter,\n                    projectDataService,\n                    formsDataService,\n                    instancesDataService,\n                    scheduler\n                )\n            }.build()\n    )\n\n    @Before\n    fun setup() {\n        projectId = CollectHelpers.setupDemoProject()\n        whenever(projectDataService.getCurrentProject()).thenReturn(MutableStateFlow(Project.DEMO_PROJECT))\n        whenever(formsDataService.getFormsCount(projectId)).thenReturn(MutableStateFlow(0))\n        whenever(instancesDataService.getSuccessfullySentCount(projectId)).thenReturn(MutableStateFlow(0))\n        whenever(instancesDataService.getSendableCount(projectId)).thenReturn(MutableStateFlow(0))\n        whenever(instancesDataService.getEditableCount(projectId)).thenReturn(MutableStateFlow(0))\n\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesProjectDeleter(\n                projectsRepository: ProjectsRepository,\n                projectsDataService: ProjectsDataService,\n                formUpdateScheduler: FormUpdateScheduler,\n                instanceSubmitScheduler: InstanceSubmitScheduler,\n                storagePathProvider: StoragePathProvider,\n                settingsProvider: SettingsProvider\n            ): ProjectDeleter {\n                return projectDeleter\n            }\n\n            override fun providesScheduler(workManager: WorkManager, application: Application): Scheduler {\n                return scheduler\n            }\n        })\n    }\n\n    @Test\n    fun `The dialog is dismissed after pressing the device back button`() {\n        val scenario = launcherRule.launch(DeleteProjectDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, equalTo(true))\n            onView(isRoot()).perform(pressBack())\n            assertThat(it.dialog, equalTo(null))\n            verifyNoInteractions(projectDeleter)\n        }\n    }\n\n    @Test\n    fun `The dialog is dismissed after clicking the Cancel button`() {\n        val scenario = launcherRule.launch(DeleteProjectDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, equalTo(true))\n            onView(withText(string.cancel)).inRoot(isDialog()).perform(click())\n            assertThat(it.dialog, equalTo(null))\n            verifyNoInteractions(projectDeleter)\n        }\n    }\n\n    @Test\n    fun `During data loading the loading spinner is displayed and other UI elements are disabled`() {\n        launcherRule.launch(DeleteProjectDialog::class.java)\n\n        onView(withId(R.id.progress_bar)).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(withId(R.id.title)).inRoot(isDialog()).check(matches(not(isDisplayed())))\n        onView(withId(R.id.message)).inRoot(isDialog()).check(matches(not(isDisplayed())))\n        onView(withId(R.id.confirmation_field)).inRoot(isDialog()).check(matches(not(isDisplayed())))\n        onView(withId(R.id.delete_button)).inRoot(isDialog()).check(matches(not(isClickable())))\n\n        scheduler.flush()\n\n        onView(withId(R.id.progress_bar)).inRoot(isDialog()).check(matches(not(isDisplayed())))\n        onView(withId(R.id.title)).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(withId(R.id.message)).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(withId(R.id.confirmation_field)).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(withId(R.id.delete_button)).inRoot(isDialog()).check(matches(isClickable()))\n    }\n\n    @Test\n    fun `The Delete Project button becomes enabled after typing Delete`() {\n        launcherRule.launch(DeleteProjectDialog::class.java)\n        scheduler.flush()\n\n        onView(withText(string.delete_project_confirm_button_text)).inRoot(isDialog()).check(matches(not(isEnabled())))\n        onView(withId(R.id.confirmation_field_input)).inRoot(isDialog()).perform(replaceText(\"Blah\"))\n        onView(withText(string.delete_project_confirm_button_text)).inRoot(isDialog()).check(matches(not(isEnabled())))\n        onView(withId(R.id.confirmation_field_input)).inRoot(isDialog()).perform(replaceText(\"Delete\"))\n        onView(withText(string.delete_project_confirm_button_text)).inRoot(isDialog()).check(matches(isEnabled()))\n    }\n\n    @Test\n    fun `The ProjectDeleter is called after clicking the Delete button`() {\n        whenever(projectDeleter.deleteProject(projectId)).thenReturn(DeleteProjectResult.DeletedSuccessfullyLastProject)\n\n        launcherRule.launch(DeleteProjectDialog::class.java)\n        scheduler.flush()\n\n        onView(withId(R.id.confirmation_field_input)).inRoot(isDialog()).perform(replaceText(\"Delete\"))\n        onView(withText(string.delete_project_confirm_button_text)).inRoot(isDialog()).perform(click())\n        scheduler.flush()\n\n        verify(projectDeleter).deleteProject(projectId)\n    }\n\n    @Test\n    fun `The title is single-lined with end ellipsize`() {\n        launcherRule.launch(DeleteProjectDialog::class.java)\n        scheduler.flush()\n        onView(withId(R.id.title)).inRoot(isDialog()).check { view, _ ->\n            val textView = view as MaterialTextView\n            assertThat(textView.maxLines, equalTo(1))\n            assertThat(textView.ellipsize, equalTo(TextUtils.TruncateAt.END))\n        }\n    }\n\n    @Test\n    fun `The message shows the correct number of blank forms`() {\n        whenever(formsDataService.getFormsCount(projectId)).thenReturn(MutableStateFlow(1))\n\n        launcherRule.launch(DeleteProjectDialog::class.java)\n        scheduler.flush()\n        assertCounts(forms = 1, sent = 0, unsent = 0, drafts = 0)\n    }\n\n    @Test\n    fun `The message shows the correct number of sent forms`() {\n        whenever(instancesDataService.getSuccessfullySentCount(projectId)).thenReturn(MutableStateFlow(1))\n\n        launcherRule.launch(DeleteProjectDialog::class.java)\n        scheduler.flush()\n        assertCounts(forms = 0, sent = 1, unsent = 0, drafts = 0)\n    }\n\n    @Test\n    fun `The message shows the correct number of unsent forms and drafts`() {\n        whenever(instancesDataService.getSendableCount(projectId)).thenReturn(MutableStateFlow(5))\n        whenever(instancesDataService.getEditableCount(projectId)).thenReturn(MutableStateFlow(3))\n\n        launcherRule.launch(DeleteProjectDialog::class.java)\n        scheduler.flush()\n        assertCounts(forms = 0, sent = 0, unsent = 5, drafts = 3)\n    }\n\n    private fun assertCounts(forms: Int, sent: Int, unsent: Int, drafts: Int) {\n        EspressoAssertions.assertVisible(\n            view = withText(containsString(context.getString(string.form_definitions_count, forms))),\n            root = isDialog()\n        )\n        EspressoAssertions.assertVisible(\n            view = withText(containsString(context.getString(string.sent_count, sent))),\n            root = isDialog()\n        )\n        EspressoAssertions.assertVisible(\n            view = withText(containsString(context.getString(string.unsent_count, unsent))),\n            root = isDialog()\n        )\n        EspressoAssertions.assertVisible(\n            view = withText(containsString(context.getString(string.drafts_count, drafts))),\n            root = isDialog()\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/dialogs/ResetProgressDialogTest.kt",
    "content": "package org.odk.collect.android.preferences.dialogs\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.permissions.R\nimport org.odk.collect.strings.localization.getLocalizedString\n\n@RunWith(AndroidJUnit4::class)\nclass ResetProgressDialogTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Test\n    fun `The dialog should not be dismissed after clicking out of its area or on device back button`() {\n        val scenario = launcherRule.launch(ResetProgressDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.isCancelable, `is`(false))\n        }\n    }\n\n    @Test\n    fun `The dialog should display proper content`() {\n        val scenario = launcherRule.launch(ResetProgressDialog::class.java)\n        scenario.onFragment {\n            // Title\n            assertThat(it.title, `is`(context.getLocalizedString(org.odk.collect.strings.R.string.please_wait)))\n\n            // Message\n            assertThat(it.message, `is`(context.getLocalizedString(org.odk.collect.strings.R.string.reset_in_progress)))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/dialogs/ServerAuthDialogFragmentTest.java",
    "content": "package org.odk.collect.android.preferences.dialogs;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\n\nimport android.widget.EditText;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.testing.FragmentScenario;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.TestSettingsProvider;\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.shared.settings.Settings;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\n@RunWith(AndroidJUnit4.class)\npublic class ServerAuthDialogFragmentTest {\n\n    private final Settings generalSettings = TestSettingsProvider.getUnprotectedSettings();\n\n    @Rule\n    public FragmentScenarioLauncherRule launcherRule = new FragmentScenarioLauncherRule();\n\n    @Test\n    public void prefillsUsernameAndPassword() {\n        generalSettings.save(ProjectKeys.KEY_USERNAME, \"Alpen\");\n        generalSettings.save(ProjectKeys.KEY_PASSWORD, \"swiss\");\n\n        FragmentScenario<ServerAuthDialogFragment> scenario = launcherRule.launch(ServerAuthDialogFragment.class);\n\n        scenario.onFragment(fragment -> {\n            EditText username = fragment.getDialogView().findViewById(R.id.username_edit);\n            EditText password = fragment.getDialogView().findViewById(R.id.password_edit);\n\n            assertThat(username.getText().toString(), is(\"Alpen\"));\n            assertThat(password.getText().toString(), is(\"swiss\"));\n        });\n    }\n\n    @Test\n    public void clickingOK_savesUsernameAndPasswordToGeneralPrefs() {\n        FragmentScenario<ServerAuthDialogFragment> scenario = launcherRule.launch(ServerAuthDialogFragment.class);\n\n        scenario.onFragment(fragment -> {\n            EditText username = fragment.getDialogView().findViewById(R.id.username_edit);\n            EditText password = fragment.getDialogView().findViewById(R.id.password_edit);\n\n            username.setText(\"Frederick Chilton\");\n            password.setText(\"chesapeake\");\n            ((AlertDialog) fragment.getDialog()).getButton(AlertDialog.BUTTON_POSITIVE).performClick();\n        });\n\n        RobolectricHelpers.runLooper();\n        assertThat(generalSettings.getString(ProjectKeys.KEY_USERNAME), is(\"Frederick Chilton\"));\n        assertThat(generalSettings.getString(ProjectKeys.KEY_PASSWORD), is(\"chesapeake\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragmentTest.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport androidx.preference.CheckBoxPreference\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.TestSettingsProvider\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.androidshared.utils.getPreference\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\n@RunWith(AndroidJUnit4::class)\nclass FormEntryAccessPreferencesFragmentTest {\n\n    private lateinit var adminSettings: Settings\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.setupDemoProject()\n        adminSettings = TestSettingsProvider.getProtectedSettings()\n    }\n\n    @Test\n    fun `when the 'Save as draft' option is unchecked, the 'Finalize' option can't be changed`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true)\n\n        val scenario = launcherRule.launch(FormEntryAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormEntryAccessPreferencesFragment ->\n            assertThat(\n                fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled,\n                equalTo(true)\n            )\n\n            assertThat(\n                fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled,\n                equalTo(false)\n            )\n        }\n    }\n\n    @Test\n    fun `when the 'Finalize' option is unchecked, the 'Save as draft' option can't be changed`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true)\n        adminSettings.save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false)\n\n        val scenario = launcherRule.launch(FormEntryAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormEntryAccessPreferencesFragment ->\n            assertThat(\n                fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled,\n                equalTo(false)\n            )\n\n            assertThat(\n                fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled,\n                equalTo(true)\n            )\n        }\n    }\n\n    @Test\n    fun `when the user unchecks the 'Save as draft' option, the 'Finalize' option becomes disabled`() {\n        val scenario = launcherRule.launch(FormEntryAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormEntryAccessPreferencesFragment ->\n            assertThat(\n                fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled,\n                equalTo(true)\n            )\n\n            fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).performClick()\n\n            assertThat(\n                fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled,\n                equalTo(false)\n            )\n            assertThat(\n                fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isChecked,\n                equalTo(true)\n            )\n        }\n    }\n\n    @Test\n    fun `when the user unchecks the 'Finalize' option, the 'Save as draft' option becomes disabled`() {\n        val scenario = launcherRule.launch(FormEntryAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormEntryAccessPreferencesFragment ->\n            assertThat(\n                fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled,\n                equalTo(true)\n            )\n\n            fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).performClick()\n\n            assertThat(\n                fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled,\n                equalTo(false)\n            )\n            assertThat(\n                fragment.getPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isChecked,\n                equalTo(true)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormManagementPreferencesFragmentTest.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.app.Application\nimport android.content.Context\nimport android.os.Looper\nimport androidx.lifecycle.ViewModel\nimport androidx.preference.CheckBoxPreference\nimport androidx.preference.ListPreference\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceCategory\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.TestSettingsProvider\nimport org.odk.collect.android.backgroundwork.InstanceSubmitScheduler\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.utilities.AdminPasswordProvider\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.enums.AutoSend\nimport org.odk.collect.settings.enums.FormUpdateMode\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.settings.Settings\nimport org.robolectric.Shadows\n\n@RunWith(AndroidJUnit4::class)\nclass FormManagementPreferencesFragmentTest {\n    private lateinit var projectID: String\n    private lateinit var context: Context\n    private lateinit var generalSettings: Settings\n    private lateinit var adminSettings: Settings\n\n    private val adminPasswordProvider = mock<AdminPasswordProvider> {\n        on { isAdminPasswordSet } doReturn false\n    }\n    private val instanceSubmitScheduler = mock<InstanceSubmitScheduler>()\n\n    private val projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesProjectPreferencesViewModel(adminPasswordProvider: AdminPasswordProvider): ProjectPreferencesViewModel.Factory {\n                return object : ProjectPreferencesViewModel.Factory(adminPasswordProvider) {\n                    override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                        return projectPreferencesViewModel as T\n                    }\n                }\n            }\n\n            override fun providesFormSubmitManager(scheduler: Scheduler, settingsProvider: SettingsProvider, application: Application): InstanceSubmitScheduler {\n                return instanceSubmitScheduler\n            }\n        })\n\n        projectID = CollectHelpers.setupDemoProject()\n        context = ApplicationProvider.getApplicationContext()\n        generalSettings = TestSettingsProvider.getUnprotectedSettings()\n        adminSettings = TestSettingsProvider.getProtectedSettings()\n    }\n\n    @Test\n    fun `When 'Manual Updates' enabled disables prefs`() {\n        generalSettings.save(ProjectKeys.KEY_FORM_UPDATE_MODE, FormUpdateMode.MANUAL.getValue(context))\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { f: FormManagementPreferencesFragment ->\n            assertThat(\n                f.findPreference<Preference>(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)!!.isEnabled,\n                `is`(false)\n            )\n            assertThat(\n                f.findPreference<Preference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)!!.isEnabled,\n                `is`(false)\n            )\n        }\n    }\n\n    @Test\n    fun `When 'Previously Downloaded Only 'enabled disables prefs`() {\n        generalSettings.save(ProjectKeys.KEY_FORM_UPDATE_MODE, FormUpdateMode.PREVIOUSLY_DOWNLOADED_ONLY.getValue(context))\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { f: FormManagementPreferencesFragment ->\n            assertThat(\n                f.findPreference<Preference>(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)!!.isEnabled,\n                `is`(true)\n            )\n            assertThat(\n                f.findPreference<Preference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)!!.isEnabled,\n                `is`(true)\n            )\n        }\n    }\n\n    @Test\n    fun `When 'Match Exactly' enabled disables prefs`() {\n        generalSettings.save(ProjectKeys.KEY_FORM_UPDATE_MODE, FormUpdateMode.MATCH_EXACTLY.getValue(context))\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { f: FormManagementPreferencesFragment ->\n            assertThat(\n                f.findPreference<Preference>(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)!!.isEnabled,\n                `is`(true)\n            )\n            assertThat(\n                f.findPreference<Preference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)!!.isEnabled,\n                `is`(false)\n            )\n        }\n    }\n\n    @Test\n    fun `When 'Match Exactly' enabled and 'Automatic Download' disabled shows 'Automatic Download' as checked`() {\n        generalSettings.save(ProjectKeys.KEY_FORM_UPDATE_MODE, FormUpdateMode.MATCH_EXACTLY.getValue(context))\n        generalSettings.save(ProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { f: FormManagementPreferencesFragment ->\n            val automaticDownload = f.findPreference<CheckBoxPreference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)\n            assertThat(automaticDownload!!.isChecked, `is`(true))\n            assertThat(\n                generalSettings.getBoolean(ProjectKeys.KEY_AUTOMATIC_UPDATE),\n                `is`(false)\n            )\n        }\n    }\n\n    @Test\n    fun `When 'Manual Updates' enabled and 'Automatic Download' enabled shows 'Automatic Download' as not checked`() {\n        generalSettings.save(ProjectKeys.KEY_FORM_UPDATE_MODE, FormUpdateMode.MANUAL.getValue(context))\n        generalSettings.save(ProjectKeys.KEY_AUTOMATIC_UPDATE, true)\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { f: FormManagementPreferencesFragment ->\n            val automaticDownload = f.findPreference<CheckBoxPreference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)\n            assertThat(automaticDownload!!.isChecked, `is`(false))\n            assertThat(\n                generalSettings.getBoolean(ProjectKeys.KEY_AUTOMATIC_UPDATE),\n                `is`(true)\n            )\n        }\n    }\n\n    @Test\n    fun `When 'Manual Updates' enabled and 'Automatic Download' disabled setting to 'Previously Downloaded' resets 'Automatic Download'`() {\n        generalSettings.save(ProjectKeys.KEY_FORM_UPDATE_MODE, FormUpdateMode.MANUAL.getValue(context))\n        generalSettings.save(ProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { f: FormManagementPreferencesFragment ->\n            val updateMode = f.findPreference<ListPreference>(ProjectKeys.KEY_FORM_UPDATE_MODE)\n            updateMode!!.value = FormUpdateMode.PREVIOUSLY_DOWNLOADED_ONLY.getValue(context)\n            Shadows.shadowOf(Looper.getMainLooper()).idle()\n            val automaticDownload = f.findPreference<CheckBoxPreference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)\n            assertThat(automaticDownload!!.isChecked, `is`(false))\n            assertThat(\n                generalSettings.getBoolean(ProjectKeys.KEY_AUTOMATIC_UPDATE),\n                `is`(false)\n            )\n        }\n    }\n\n    @Test\n    fun `Changing Form Update Mode should not cause any crash if related preferences are disabled in Protected Settings`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        val scenario = launcherRule.launch(\n            FormManagementPreferencesFragment::class.java\n        )\n        scenario.onFragment { f: FormManagementPreferencesFragment ->\n            assertThat(f.findPreference<Preference>(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)!!.isVisible, `is`(false))\n            assertThat(f.findPreference<Preference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)!!.isVisible, `is`(false))\n            val updateMode = f.findPreference<ListPreference>(ProjectKeys.KEY_FORM_UPDATE_MODE)\n            updateMode!!.value = FormUpdateMode.PREVIOUSLY_DOWNLOADED_ONLY.getValue(context)\n            updateMode.value = FormUpdateMode.MATCH_EXACTLY.getValue(context)\n            updateMode.value = FormUpdateMode.MANUAL.getValue(context)\n        }\n    }\n\n    @Test\n    fun `Enabled preferences should be visible in Locked mode`() {\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_UPDATE_MODE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOSEND)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_DELETE_AFTER_SEND)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIGH_RESOLUTION)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_IMAGE_SIZE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_GUIDANCE_HINT)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_EXTERNAL_APP_RECORDING)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_INSTANCE_SYNC)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Disabled preferences should be hidden in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_UPDATE_MODE)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOSEND)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_DELETE_AFTER_SEND)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIGH_RESOLUTION)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_IMAGE_SIZE)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_GUIDANCE_HINT)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_EXTERNAL_APP_RECORDING)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_INSTANCE_SYNC)!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Enabled preferences should be visible in Unlocked mode`() {\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_UPDATE_MODE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOSEND)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_DELETE_AFTER_SEND)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIGH_RESOLUTION)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_IMAGE_SIZE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_GUIDANCE_HINT)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_EXTERNAL_APP_RECORDING)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_INSTANCE_SYNC)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Disabled preferences should be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_UPDATE_MODE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOSEND)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_DELETE_AFTER_SEND)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIGH_RESOLUTION)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_IMAGE_SIZE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_GUIDANCE_HINT)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_EXTERNAL_APP_RECORDING)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_INSTANCE_SYNC)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Enabled preferences should be visible in NotProtected mode`() {\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_UPDATE_MODE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOSEND)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_DELETE_AFTER_SEND)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIGH_RESOLUTION)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_IMAGE_SIZE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_GUIDANCE_HINT)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_EXTERNAL_APP_RECORDING)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_INSTANCE_SYNC)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Disabled preferences should be hidden in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_UPDATE_MODE)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOMATIC_UPDATE)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_AUTOSEND)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_DELETE_AFTER_SEND)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_HIGH_RESOLUTION)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_IMAGE_SIZE)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_GUIDANCE_HINT)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_EXTERNAL_APP_RECORDING)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_INSTANCE_SYNC)!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When all preferences in 'Form update' category are hidden, the category should be hidden as well`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<PreferenceCategory>(\"form_update_category\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When al least one preference in 'Form update' category is visible, the category should be visible as well`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, true)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<PreferenceCategory>(\"form_update_category\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all preferences in 'Form submission' category are hidden, the category should be hidden as well`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, false)\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<PreferenceCategory>(\"form_submission\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When al least one preference in 'Form submission' category is visible, the category should be visible as well`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, true)\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<PreferenceCategory>(\"form_submission\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all preferences in 'Form filling' category are hidden, the category should be hidden as well`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<PreferenceCategory>(\"form_filling\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When al least one preference in 'Form filling' category is visible, the category should be visible as well`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, true)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<PreferenceCategory>(\"form_filling\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all preferences in 'Form import' category are hidden, the category should be hidden as well`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<PreferenceCategory>(\"form_import\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When al least one preference in 'Form import' category is visible, the category should be visible as well`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, true)\n\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            assertThat(fragment.findPreference<PreferenceCategory>(\"form_import\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When Auto send preference is enabled, finalized forms should be scheduled for submission`() {\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            fragment.findPreference<ListPreference>(ProjectKeys.KEY_AUTOSEND)!!.value = AutoSend.WIFI_ONLY.getValue(context)\n        }\n        verify(instanceSubmitScheduler).scheduleAutoSend(projectID)\n    }\n\n    @Test\n    fun `When Auto send preference is disabled, no submissions should be scheduled`() {\n        val scenario = launcherRule.launch(FormManagementPreferencesFragment::class.java)\n        scenario.onFragment { fragment: FormManagementPreferencesFragment ->\n            fragment.findPreference<ListPreference>(ProjectKeys.KEY_AUTOSEND)!!.value = AutoSend.OFF.getValue(context)\n        }\n        verify(instanceSubmitScheduler, never()).scheduleAutoSend(projectID)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormMetadataPreferencesFragmentTest.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport androidx.preference.Preference\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.metadata.InstallIDProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\n\n@RunWith(AndroidJUnit4::class)\nclass FormMetadataPreferencesFragmentTest {\n    private val installIDProvider = mock<InstallIDProvider>()\n    private val settingsProvider =\n        ApplicationProvider.getApplicationContext<Collect>().component!!.settingsProvider()\n\n    @get:Rule\n    var launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesInstallIDProvider(settingsProvider: SettingsProvider): InstallIDProvider {\n                return installIDProvider\n            }\n        })\n\n        CollectHelpers.setupDemoProject()\n    }\n\n    @Test\n    fun whenMetadataEmpty_preferenceSummariesAreNotSet() {\n        whenever(installIDProvider.installID).thenReturn(\"\")\n\n        launcherRule\n            .launch(FormMetadataPreferencesFragment::class.java)\n            .onFragment {\n                assertThat(\n                    it.findPreference<Preference>(\"metadata_username\")!!.summary,\n                    equalTo(\"Not set\")\n                )\n                assertThat(\n                    it.findPreference<Preference>(\"metadata_phonenumber\")!!.summary,\n                    equalTo(\"Not set\")\n                )\n                assertThat(\n                    it.findPreference<Preference>(\"metadata_email\")!!.summary,\n                    equalTo(\"Not set\")\n                )\n                assertThat(\n                    it.findPreference<Preference>(\"deviceid\")!!.summary,\n                    equalTo(it.context!!.getString(org.odk.collect.strings.R.string.preference_not_available))\n                )\n            }\n    }\n\n    @Test\n    fun whenMetadataNotEmpty_preferenceSummariesAreSet() {\n        whenever(installIDProvider.installID).thenReturn(\"123456789\")\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_METADATA_USERNAME, \"John\")\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_METADATA_PHONENUMBER, \"789\")\n        settingsProvider.getUnprotectedSettings()\n            .save(ProjectKeys.KEY_METADATA_EMAIL, \"john@gmail.com\")\n\n        launcherRule\n            .launch(FormMetadataPreferencesFragment::class.java)\n            .onFragment {\n                assertThat(\n                    it.findPreference<Preference>(\"metadata_username\")!!.summary,\n                    equalTo(\"John\")\n                )\n                assertThat(\n                    it.findPreference<Preference>(\"metadata_phonenumber\")!!.summary,\n                    equalTo(\"789\")\n                )\n                assertThat(\n                    it.findPreference<Preference>(\"metadata_email\")!!.summary,\n                    equalTo(\"john@gmail.com\")\n                )\n                assertThat(\n                    it.findPreference<Preference>(\"deviceid\")!!.summary,\n                    equalTo(\"123456789\")\n                )\n            }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/screens/IdentityPreferencesFragmentTest.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport androidx.lifecycle.ViewModel\nimport androidx.preference.CheckBoxPreference\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceCategory\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.TestSettingsProvider\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.utilities.AdminPasswordProvider\nimport org.odk.collect.android.version.VersionInformation\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\n@RunWith(AndroidJUnit4::class)\nclass IdentityPreferencesFragmentTest {\n    private lateinit var generalSettings: Settings\n    private lateinit var adminSettings: Settings\n\n    private val adminPasswordProvider = mock<AdminPasswordProvider> {\n        on { isAdminPasswordSet } doReturn false\n    }\n    private val projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n\n    private val versionInformation = mock<VersionInformation>()\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.setupDemoProject()\n\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesProjectPreferencesViewModel(adminPasswordProvider: AdminPasswordProvider): ProjectPreferencesViewModel.Factory {\n                return object : ProjectPreferencesViewModel.Factory(adminPasswordProvider) {\n                    override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                        return projectPreferencesViewModel as T\n                    }\n                }\n            }\n\n            override fun providesVersionInformation(): VersionInformation {\n                return versionInformation\n            }\n        })\n\n        generalSettings = TestSettingsProvider.getUnprotectedSettings()\n        adminSettings = TestSettingsProvider.getProtectedSettings()\n    }\n\n    @Test\n    fun `If 'Form Metadata' option is enabled in protected settings should be visible in Locked mode`() {\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_METADATA)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Form Metadata' option is disabled in protected settings should be hidden in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA, false)\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_METADATA)!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If 'Form Metadata' option is enabled in protected settings should be visible in Unlocked mode`() {\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_METADATA)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Form Metadata' option is disabled in protected settings should be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA, false)\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_METADATA)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Form Metadata' option is enabled in protected settings should be visible in NotProtected mode`() {\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_METADATA)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Form Metadata' option is disabled in protected settings should be hidden in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA, false)\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FORM_METADATA)!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If 'Collect anonymous usage data' option is enabled in protected settings should be visible in Locked mode`() {\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_ANALYTICS)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Collect anonymous usage data' option is disabled in protected settings should be hidden in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, false)\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_ANALYTICS)!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If 'Collect anonymous usage data' option is enabled in protected settings should be visible in Unlocked mode`() {\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_ANALYTICS)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Collect anonymous usage data' option is disabled in protected settings should be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, false)\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_ANALYTICS)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Collect anonymous usage data' option is enabled in protected settings should be visible in NotProtected mode`() {\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_ANALYTICS)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Collect anonymous usage data' option is disabled in protected settings should be hidden in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, false)\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_ANALYTICS)!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `'Collect anonymous usage data' option should be checked if it's enabled in settings`() {\n        generalSettings.save(ProjectKeys.KEY_ANALYTICS, true)\n        whenever(versionInformation.isBeta).thenReturn(false)\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_ANALYTICS)!!.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `'Collect anonymous usage data' option should be unchecked if it's disabled in settings`() {\n        generalSettings.save(ProjectKeys.KEY_ANALYTICS, false)\n        whenever(versionInformation.isBeta).thenReturn(false)\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_ANALYTICS)!!.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `'Collect anonymous usage data' option should be disabled in beta`() {\n        whenever(versionInformation.isBeta).thenReturn(true)\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProjectKeys.KEY_ANALYTICS)!!.isEnabled, `is`(false))\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProjectKeys.KEY_ANALYTICS)!!.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `'Collect anonymous usage data' option should be enabled if it's not beta`() {\n        whenever(versionInformation.isBeta).thenReturn(false)\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProjectKeys.KEY_ANALYTICS)!!.isEnabled, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Clicking on the 'Collect anonymous usage data' option should change its state`() {\n        whenever(versionInformation.isBeta).thenReturn(false)\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_ANALYTICS)\n            assertThat(option!!.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Fragment recreation should not change the state of the 'Collect anonymous usage data' option`() {\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_ANALYTICS)\n            assertThat(option!!.isChecked, `is`(true))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When all preferences in 'Usage data' category are hidden, the category should be hidden as well`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, false)\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<PreferenceCategory>(\"usage_data\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When al least one preference in 'Usage data' category is visible, the category should be visible as well`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, true)\n\n        val scenario = launcherRule.launch(IdentityPreferencesFragment::class.java)\n        scenario.onFragment { fragment: IdentityPreferencesFragment ->\n            assertThat(fragment.findPreference<PreferenceCategory>(\"usage_data\")!!.isVisible, `is`(true))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/screens/MainMenuAccessPreferencesTest.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.content.Context\nimport androidx.preference.CheckBoxPreference\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.TestSettingsProvider\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.settings.enums.FormUpdateMode\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\n@RunWith(AndroidJUnit4::class)\nclass MainMenuAccessPreferencesTest {\n    private lateinit var context: Context\n    private lateinit var generalSettings: Settings\n    private lateinit var adminSettings: Settings\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.setupDemoProject()\n        context = ApplicationProvider.getApplicationContext()\n        generalSettings = TestSettingsProvider.getUnprotectedSettings()\n        adminSettings = TestSettingsProvider.getProtectedSettings()\n    }\n\n    @Test\n    fun `Edit Saved Form option should be checked if it's enabled in settings`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_EDIT_SAVED)!!.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Edit Saved Form option should be unchecked if it's disabled in settings`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_EDIT_SAVED, false)\n\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_EDIT_SAVED)!!.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Clicking on the Edit Saved Form option should change its state`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_EDIT_SAVED)\n            assertThat(option!!.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Fragment recreation should not change the state of the Edit Saved Form option`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_EDIT_SAVED)\n            assertThat(option!!.isChecked, `is`(true))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Edit Saved Form option should be disabled if editing forms is disabled`() {\n        adminSettings.save(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, false)\n\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_EDIT_SAVED)!!.isEnabled, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Send Finalized Form option should be checked if it's enabled in settings`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_SEND_FINALIZED)!!.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Send Finalized Form option should be unchecked if it's disabled in settings`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_SEND_FINALIZED, false)\n\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_SEND_FINALIZED)!!.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Clicking on the Send Finalized Form option should change its state`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_SEND_FINALIZED)\n            assertThat(option!!.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Fragment recreation should not change the state of the Send Finalized Form option`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_SEND_FINALIZED)\n            assertThat(option!!.isChecked, `is`(true))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `View Sent Form option should be checked if it's enabled in settings`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_VIEW_SENT)!!.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `View Sent Form option should be unchecked if it's disabled in settings`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_VIEW_SENT, false)\n\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_VIEW_SENT)!!.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Clicking on the View Sent Form option should change its state`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_VIEW_SENT)\n            assertThat(option!!.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Fragment recreation should not change the state of the View Sent Form option`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_VIEW_SENT)\n            assertThat(option!!.isChecked, `is`(true))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Get Blank Form option should be checked if it's enabled in settings`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_GET_BLANK)!!.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Get Blank Form option should be unchecked if it's disabled in settings`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_GET_BLANK, false)\n\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_GET_BLANK)!!.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Clicking on the Get Blank Form option should change its state`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_GET_BLANK)\n            assertThat(option!!.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Fragment recreation should not change the state of the Get Blank Form option`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_GET_BLANK)\n            assertThat(option!!.isChecked, `is`(true))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Delete Saved Form option should be checked if it's enabled in settings`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_DELETE_SAVED)!!.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Delete Saved Form option should be unchecked if it's disabled in settings`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_SAVED, false)\n\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            assertThat(fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_DELETE_SAVED)!!.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Clicking on the Delete Saved Form option should change its state`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_DELETE_SAVED)\n            assertThat(option!!.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Fragment recreation should not change the state of the Delete Saved Form option`() {\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_DELETE_SAVED)\n            assertThat(option!!.isChecked, `is`(true))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(true))\n\n            option.performClick()\n            assertThat(option.isChecked, `is`(false))\n            scenario.recreate()\n            assertThat(option.isChecked, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When match exactly enabled shows the Get Blank Form option as unchecked and disabled`() {\n        generalSettings.save(ProjectKeys.KEY_FORM_UPDATE_MODE, FormUpdateMode.MATCH_EXACTLY.getValue(context))\n\n        val scenario = launcherRule.launch(MainMenuAccessPreferencesFragment::class.java)\n\n        scenario.onFragment { fragment: MainMenuAccessPreferencesFragment ->\n            val option = fragment.findPreference<CheckBoxPreference>(ProtectedProjectKeys.KEY_GET_BLANK)\n            assertThat(option!!.isEnabled, `is`(false))\n            assertThat(option.isChecked, `is`(false))\n            assertThat(adminSettings.getBoolean(ProtectedProjectKeys.KEY_GET_BLANK), `is`(true))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragmentTest.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.preference.Preference\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport com.google.gson.Gson\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.application.initialization.AnalyticsInitializer\nimport org.odk.collect.android.application.initialization.MapsInitializer\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.maps.layers.ReferenceLayer\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.shared.strings.UUIDGenerator\n\n@RunWith(AndroidJUnit4::class)\nclass MapsPreferencesFragmentTest {\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    private val project = Project.DEMO_PROJECT\n    private val projectsDataService = mock<ProjectsDataService>().apply {\n        whenever(requireCurrentProject()).thenReturn(project)\n    }\n    private val projectsRepository = mock<ProjectsRepository>().apply {\n        whenever(get(project.uuid)).thenReturn(project)\n    }\n    private val referenceLayerRepository = mock<ReferenceLayerRepository>()\n    private val settingsProvider = InMemSettingsProvider()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesCurrentProjectProvider(\n                application: Application,\n                settingsProvider: SettingsProvider,\n                projectsRepository: ProjectsRepository,\n                analyticsInitializer: AnalyticsInitializer,\n                context: Context,\n                mapsInitializer: MapsInitializer\n            ): ProjectsDataService {\n                return projectsDataService\n            }\n\n            override fun providesProjectsRepository(\n                uuidGenerator: UUIDGenerator,\n                gson: Gson,\n                settingsProvider: SettingsProvider\n            ): ProjectsRepository {\n                return projectsRepository\n            }\n\n            override fun providesSettingsProvider(context: Context): SettingsProvider {\n                return settingsProvider\n            }\n\n            override fun providesReferenceLayerRepository(\n                storagePathProvider: StoragePathProvider,\n                settingsProvider: SettingsProvider\n            ): ReferenceLayerRepository {\n                return referenceLayerRepository\n            }\n        })\n    }\n\n    @Test\n    fun `if saved layer does not exist it is set to 'none'`() {\n        val settings = settingsProvider.getUnprotectedSettings()\n        settings.save(ProjectKeys.KEY_REFERENCE_LAYER, \"blah\")\n\n        launcherRule.launch(MapsPreferencesFragment::class.java)\n\n        assertThat(settings.getString(ProjectKeys.KEY_REFERENCE_LAYER), equalTo(null))\n    }\n\n    @Test\n    fun `if saved layer exist its name is displayed`() {\n        val settings = settingsProvider.getUnprotectedSettings()\n        settings.save(ProjectKeys.KEY_REFERENCE_LAYER, \"blah\")\n        val layer = ReferenceLayer(\"blah\", TempFiles.createTempFile(), \"blah\")\n        whenever(referenceLayerRepository.get(\"blah\")).thenReturn(layer)\n\n        val scenario = launcherRule.launch(MapsPreferencesFragment::class.java)\n\n        scenario.onFragment {\n            assertThat(\n                it.findPreference<Preference>(ProjectKeys.KEY_REFERENCE_LAYER)!!.summary,\n                equalTo(\"blah\")\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/screens/ProjectDisplayPreferencesFragmentTest.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.preference.EditTextPreference\nimport androidx.preference.Preference\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport com.google.gson.Gson\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.Mockito.mock\nimport org.mockito.Mockito.`when`\nimport org.odk.collect.android.application.Collect\nimport org.odk.collect.android.application.initialization.AnalyticsInitializer\nimport org.odk.collect.android.application.initialization.MapsInitializer\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.shared.strings.UUIDGenerator\nimport org.odk.collect.strings.localization.getLocalizedString\n\n@RunWith(AndroidJUnit4::class)\nclass ProjectDisplayPreferencesFragmentTest {\n\n    lateinit var projectsDataService: ProjectsDataService\n    lateinit var projectsRepository: ProjectsRepository\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        projectsDataService = mock(ProjectsDataService::class.java)\n        projectsRepository = mock(ProjectsRepository::class.java)\n\n        `when`(projectsDataService.requireCurrentProject())\n            .thenReturn(Project.Saved(\"123\", \"Project X\", \"X\", \"#cccccc\"))\n\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesCurrentProjectProvider(\n                application: Application,\n                settingsProvider: SettingsProvider,\n                projectsRepository: ProjectsRepository,\n                analyticsInitializer: AnalyticsInitializer,\n                context: Context,\n                mapsInitializer: MapsInitializer\n            ): ProjectsDataService {\n                return projectsDataService\n            }\n\n            override fun providesProjectsRepository(uuidGenerator: UUIDGenerator, gson: Gson, settingsProvider: SettingsProvider): ProjectsRepository {\n                return projectsRepository\n            }\n        })\n    }\n\n    @Test\n    fun `Project Name preference should be visible`() {\n        val scenario = launcherRule.launch(ProjectDisplayPreferencesFragment::class.java)\n        scenario.onFragment {\n            assertThat(\n                it.findPreference<EditTextPreference>(ProjectDisplayPreferencesFragment.PROJECT_NAME_KEY)!!.isVisible,\n                `is`(true)\n            )\n        }\n    }\n\n    @Test\n    fun `Project Name preference should have proper title`() {\n        val scenario = launcherRule.launch(ProjectDisplayPreferencesFragment::class.java)\n        scenario.onFragment {\n            assertThat(\n                it.findPreference<EditTextPreference>(ProjectDisplayPreferencesFragment.PROJECT_NAME_KEY)!!.title,\n                `is`(\n                    ApplicationProvider.getApplicationContext<Collect>().getLocalizedString(\n                        org.odk.collect.strings.R.string.project_name\n                    )\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `Project Name preference should have proper summary`() {\n        val scenario = launcherRule.launch(ProjectDisplayPreferencesFragment::class.java)\n        scenario.onFragment {\n            assertThat(\n                it.findPreference<EditTextPreference>(ProjectDisplayPreferencesFragment.PROJECT_NAME_KEY)!!.summary,\n                `is`(\"Project X\")\n            )\n        }\n    }\n\n    @Test\n    fun `Project Icon preference should be visible`() {\n        val scenario = launcherRule.launch(ProjectDisplayPreferencesFragment::class.java)\n        scenario.onFragment {\n            assertThat(\n                it.findPreference<EditTextPreference>(ProjectDisplayPreferencesFragment.PROJECT_ICON_KEY)!!.isVisible,\n                `is`(true)\n            )\n        }\n    }\n\n    @Test\n    fun `Project Icon preference should have proper title`() {\n        val scenario = launcherRule.launch(ProjectDisplayPreferencesFragment::class.java)\n        scenario.onFragment {\n            assertThat(\n                it.findPreference<EditTextPreference>(ProjectDisplayPreferencesFragment.PROJECT_ICON_KEY)!!.title,\n                `is`(\n                    ApplicationProvider.getApplicationContext<Collect>().getLocalizedString(\n                        org.odk.collect.strings.R.string.project_icon\n                    )\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `Project Icon preference should have proper summary`() {\n        val scenario = launcherRule.launch(ProjectDisplayPreferencesFragment::class.java)\n        scenario.onFragment {\n            assertThat(\n                it.findPreference<EditTextPreference>(ProjectDisplayPreferencesFragment.PROJECT_ICON_KEY)!!.summary,\n                `is`(\"X\")\n            )\n        }\n    }\n\n    @Test\n    fun `Project Color preference should be visible`() {\n        val scenario = launcherRule.launch(ProjectDisplayPreferencesFragment::class.java)\n        scenario.onFragment {\n            assertThat(\n                it.findPreference<Preference>(ProjectDisplayPreferencesFragment.PROJECT_COLOR_KEY)!!.isVisible,\n                `is`(true)\n            )\n        }\n    }\n\n    @Test\n    fun `Project Color preference should have proper title`() {\n        val scenario = launcherRule.launch(ProjectDisplayPreferencesFragment::class.java)\n        scenario.onFragment {\n            assertThat(\n                it.findPreference<Preference>(ProjectDisplayPreferencesFragment.PROJECT_COLOR_KEY)!!.title,\n                `is`(\n                    ApplicationProvider.getApplicationContext<Collect>().getLocalizedString(\n                        org.odk.collect.strings.R.string.project_color\n                    )\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `Project Color preference should have proper summary`() {\n        val scenario = launcherRule.launch(ProjectDisplayPreferencesFragment::class.java)\n        scenario.onFragment {\n            assertThat(\n                it.findPreference<Preference>(ProjectDisplayPreferencesFragment.PROJECT_COLOR_KEY)!!.summary.toString(),\n                `is`(\"■\")\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/screens/ProjectPreferencesFragmentTest.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport androidx.lifecycle.ViewModel\nimport androidx.preference.Preference\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.TestSettingsProvider\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.utilities.AdminPasswordProvider\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\n@RunWith(AndroidJUnit4::class)\nclass ProjectPreferencesFragmentTest {\n    private lateinit var generalSettings: Settings\n    private lateinit var adminSettings: Settings\n\n    private val adminPasswordProvider = mock<AdminPasswordProvider> {\n        on { isAdminPasswordSet } doReturn false\n    }\n    private val projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n\n    private val fragmentFactory = FragmentFactoryBuilder()\n        .forClass(ProjectPreferencesFragment::class) {\n            ProjectPreferencesFragment(false)\n        }.build()\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule(defaultFactory = fragmentFactory)\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesProjectPreferencesViewModel(adminPasswordProvider: AdminPasswordProvider): ProjectPreferencesViewModel.Factory {\n                return object : ProjectPreferencesViewModel.Factory(adminPasswordProvider) {\n                    override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                        return projectPreferencesViewModel as T\n                    }\n                }\n            }\n        })\n\n        CollectHelpers.setupDemoProject()\n        generalSettings = TestSettingsProvider.getUnprotectedSettings()\n        adminSettings = TestSettingsProvider.getProtectedSettings()\n    }\n\n    @Test\n    fun `List of preferences should be updated after changing settings in protected settings`() {\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"protocol\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"project_display\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"user_interface\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"maps\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"form_management\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"user_and_device_identity\")!!.isVisible, `is`(true))\n        }\n\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_SERVER, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_PROJECT_DISPLAY, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_APP_LANGUAGE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_NAVIGATION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_MAPS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, false)\n\n        scenario.recreate()\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"protocol\")!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(\"project_display\")!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(\"user_interface\")!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(\"maps\")!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(\"form_management\")!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(\"user_and_device_identity\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If 'Server' option is enabled in protected settings should be visible in Locked mode`() {\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"protocol\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Server' option is disabled in protected settings should be hidden in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_SERVER, false)\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"protocol\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If 'Server' option is enabled in protected settings should be visible in Unlocked mode`() {\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"protocol\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Server' option is disabled in protected settings should be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_SERVER, false)\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"protocol\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Server' option is enabled in protected settings should be visible in NotProtected mode`() {\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"protocol\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Server' option is disabled in protected settings should be hidden in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_SERVER, false)\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"protocol\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If 'Project display' option is enabled in protected settings should be visible in Locked mode`() {\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"project_display\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Project display' option is disabled in protected settings should be hidden in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_PROJECT_DISPLAY, false)\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"project_display\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If 'Project display' option is enabled in protected settings should be visible in Unlocked mode`() {\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"project_display\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Project display' option is disabled in protected settings should be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_PROJECT_DISPLAY, false)\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"project_display\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Project display' option is enabled in protected settings should be visible in NotProtected mode`() {\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"project_display\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Project display' option is disabled in protected settings should be hidden in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_PROJECT_DISPLAY, false)\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"project_display\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When at least one user interface preference is enabled should preference be visible in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_APP_LANGUAGE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_NAVIGATION, true)\n\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_interface\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all user interface preferences are disabled should preference be hidden in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_APP_LANGUAGE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_NAVIGATION, false)\n\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_interface\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When at least one user interface preference is enabled should preference be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_APP_LANGUAGE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_NAVIGATION, true)\n\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_interface\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all user interface preferences are disabled should preference be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_APP_LANGUAGE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_NAVIGATION, false)\n\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_interface\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When at least one user interface preference is enabled should preference be visible in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_APP_LANGUAGE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_NAVIGATION, true)\n\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_interface\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all user interface preferences are disabled should preference be hidden in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_APP_LANGUAGE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_NAVIGATION, false)\n\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_interface\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If 'Maps' option is enabled in protected settings should be visible in Locked mode`() {\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"maps\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Maps' option is disabled in protected settings should be hidden in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_MAPS, false)\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"maps\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If 'Maps' option is enabled in protected settings should be visible in Unlocked mode`() {\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"maps\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Maps' option is disabled in protected settings should be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_MAPS, false)\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"maps\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Maps' option is enabled in protected settings should be visible in NotProtected mode`() {\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"maps\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If 'Maps' option is disabled in protected settings should be hidden in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_MAPS, false)\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"maps\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When at least one Form management preference is enabled should preference be visible in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, true)\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"form_management\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all Form management preferences are disabled should preference be hidden in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"form_management\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When at least one Form management preference is enabled should preference be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, true)\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"form_management\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all Form management preferences are disabled should preference be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"form_management\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When at least one Form management preference is enabled should preference be visible in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, true)\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"form_management\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all Form management preferences are disabled should preference be hidden in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_FORM_UPDATE_MODE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_PERIODIC_FORM_UPDATES_CHECK, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOMATIC_UPDATE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIDE_OLD_FORM_VERSIONS, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_DELETE_AFTER_SEND, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CONSTRAINT_BEHAVIOR, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_HIGH_RESOLUTION, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_IMAGE_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_GUIDANCE_HINT, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_EXTERNAL_APP_RECORDING, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_INSTANCE_FORM_SYNC, false)\n\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"form_management\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When at least one Identity preference is enabled should preference be visible in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, true)\n\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_and_device_identity\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all Identity preferences are disabled should preference be hidden in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, false)\n\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_and_device_identity\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `When at least one Identity preference is enabled should preference be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, true)\n\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_and_device_identity\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all Identity preferences are disabled should preference be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, false)\n\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_and_device_identity\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When at least one Identity preference is enabled should preference be visible in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, true)\n\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_and_device_identity\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When all Identity preferences are disabled should preference be hidden in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FORM_METADATA, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_ANALYTICS, false)\n\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"user_and_device_identity\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If in Locked state protected preferences should be hidden`() {\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"unlock_protected_settings\")!!.isVisible, `is`(true))\n\n            assertThat(fragment.findPreference<Preference>(\"admin_password\")!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(\"project_management\")!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(\"access_control\")!!.isVisible, `is`(false))\n        }\n\n        scenario.recreate()\n\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"unlock_protected_settings\")!!.isVisible, `is`(true))\n\n            assertThat(fragment.findPreference<Preference>(\"admin_password\")!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(\"project_management\")!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(\"access_control\")!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `If in Unlocked state protected preferences should be visible`() {\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"unlock_protected_settings\")!!.isVisible, `is`(false))\n\n            assertThat(fragment.findPreference<Preference>(\"admin_password\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"project_management\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"access_control\")!!.isVisible, `is`(true))\n        }\n\n        scenario.recreate()\n\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"unlock_protected_settings\")!!.isVisible, `is`(false))\n\n            assertThat(fragment.findPreference<Preference>(\"admin_password\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"project_management\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"access_control\")!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `If in NotProtected state protected preferences should be visible`() {\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(ProjectPreferencesFragment::class.java)\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"unlock_protected_settings\")!!.isVisible, `is`(false))\n\n            assertThat(fragment.findPreference<Preference>(\"admin_password\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"project_management\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"access_control\")!!.isVisible, `is`(true))\n        }\n\n        scenario.recreate()\n\n        scenario.onFragment { fragment: ProjectPreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(\"unlock_protected_settings\")!!.isVisible, `is`(false))\n\n            assertThat(fragment.findPreference<Preference>(\"admin_password\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"project_management\")!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(\"access_control\")!!.isVisible, `is`(true))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/screens/UserInterfacePreferencesFragmentTest.kt",
    "content": "package org.odk.collect.android.preferences.screens\n\nimport androidx.lifecycle.ViewModel\nimport androidx.preference.Preference\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.TestSettingsProvider\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.preferences.ProjectPreferencesViewModel\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.utilities.AdminPasswordProvider\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\n@RunWith(AndroidJUnit4::class)\nclass UserInterfacePreferencesFragmentTest {\n    private lateinit var generalSettings: Settings\n    private lateinit var adminSettings: Settings\n\n    private val adminPasswordProvider = mock<AdminPasswordProvider> {\n        on { isAdminPasswordSet } doReturn false\n    }\n    private val projectPreferencesViewModel = ProjectPreferencesViewModel(adminPasswordProvider)\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesProjectPreferencesViewModel(adminPasswordProvider: AdminPasswordProvider): ProjectPreferencesViewModel.Factory {\n                return object : ProjectPreferencesViewModel.Factory(adminPasswordProvider) {\n                    override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                        return projectPreferencesViewModel as T\n                    }\n                }\n            }\n        })\n\n        CollectHelpers.setupDemoProject()\n        generalSettings = TestSettingsProvider.getUnprotectedSettings()\n        adminSettings = TestSettingsProvider.getProtectedSettings()\n    }\n\n    @Test\n    fun `Enabled preferences should be visible in Locked mode`() {\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(UserInterfacePreferencesFragment::class.java)\n        scenario.onFragment { fragment: UserInterfacePreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_APP_LANGUAGE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FONT_SIZE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_NAVIGATION)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Disabled preferences should be hidden in Locked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_APP_LANGUAGE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_NAVIGATION, false)\n\n        projectPreferencesViewModel.setStateLocked()\n\n        val scenario = launcherRule.launch(UserInterfacePreferencesFragment::class.java)\n        scenario.onFragment { fragment: UserInterfacePreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_APP_LANGUAGE)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FONT_SIZE)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_NAVIGATION)!!.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `Enabled preferences should be visible in Unlocked mode`() {\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(UserInterfacePreferencesFragment::class.java)\n        scenario.onFragment { fragment: UserInterfacePreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_APP_LANGUAGE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FONT_SIZE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_NAVIGATION)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Disabled preferences should be visible in Unlocked mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_APP_LANGUAGE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_NAVIGATION, false)\n\n        projectPreferencesViewModel.setStateUnlocked()\n\n        val scenario = launcherRule.launch(UserInterfacePreferencesFragment::class.java)\n        scenario.onFragment { fragment: UserInterfacePreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_APP_LANGUAGE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FONT_SIZE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_NAVIGATION)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Enabled preferences should be visible in NotProtected mode`() {\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(UserInterfacePreferencesFragment::class.java)\n        scenario.onFragment { fragment: UserInterfacePreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_APP_LANGUAGE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FONT_SIZE)!!.isVisible, `is`(true))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_NAVIGATION)!!.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `Disabled preferences should be hidden in NotProtected mode`() {\n        adminSettings.save(ProtectedProjectKeys.KEY_APP_LANGUAGE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_CHANGE_FONT_SIZE, false)\n        adminSettings.save(ProtectedProjectKeys.KEY_NAVIGATION, false)\n\n        projectPreferencesViewModel.setStateNotProtected()\n\n        val scenario = launcherRule.launch(UserInterfacePreferencesFragment::class.java)\n        scenario.onFragment { fragment: UserInterfacePreferencesFragment ->\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_APP_LANGUAGE)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_FONT_SIZE)!!.isVisible, `is`(false))\n            assertThat(fragment.findPreference<Preference>(ProjectKeys.KEY_NAVIGATION)!!.isVisible, `is`(false))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/source/SettingsStoreTest.kt",
    "content": "package org.odk.collect.android.preferences.source\n\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.mockito.Mockito.mock\nimport org.mockito.Mockito.verify\nimport org.mockito.Mockito.`when`\nimport org.odk.collect.shared.settings.Settings\n\nclass SettingsStoreTest {\n    private lateinit var settingsStore: SettingsStore\n    private lateinit var settings: Settings\n\n    @Before\n    fun setup() {\n        settings = mock(Settings::class.java)\n        settingsStore = SettingsStore(settings)\n    }\n\n    @Test\n    fun `putString() saves string in settings`() {\n        settingsStore.putString(\"key\", \"value\")\n        verify(settings).save(\"key\", \"value\")\n    }\n\n    @Test\n    fun `putBoolean() saves boolean in settings`() {\n        settingsStore.putBoolean(\"key\", true)\n        verify(settings).save(\"key\", true)\n    }\n\n    @Test\n    fun `putLong() saves long in settings`() {\n        settingsStore.putLong(\"key\", 3L)\n        verify(settings).save(\"key\", 3L)\n    }\n\n    @Test\n    fun `putInt() saves int in settings`() {\n        settingsStore.putInt(\"key\", 5)\n        verify(settings).save(\"key\", 5)\n    }\n\n    @Test\n    fun `putFloat() saves float in settings`() {\n        settingsStore.putFloat(\"key\", 8.43f)\n        verify(settings).save(\"key\", 8.43f)\n    }\n\n    @Test\n    fun `putStringSet() saves set of strings in settings`() {\n        settingsStore.putStringSet(\"key\", setOf(\"x\", \"y\", \"z\"))\n        verify(settings).save(\"key\", setOf(\"x\", \"y\", \"z\"))\n    }\n\n    @Test\n    fun `getString() reads string from settings`() {\n        `when`(settings.getString(\"key\")).thenReturn(\"value\")\n        val value = settingsStore.getString(\"key\", \"\")\n        assertThat(value, `is`(\"value\"))\n    }\n\n    @Test\n    fun `getBoolean() reads boolean from settings`() {\n        `when`(settings.getBoolean(\"key\")).thenReturn(true)\n        val value = settingsStore.getBoolean(\"key\", false)\n        assertThat(value, `is`(true))\n    }\n\n    @Test\n    fun `getLong() reads long from settings`() {\n        `when`(settings.getLong(\"key\")).thenReturn(3L)\n        val value = settingsStore.getLong(\"key\", 0L)\n        assertThat(value, `is`(3L))\n    }\n\n    @Test\n    fun `getInt() reads int from settings`() {\n        `when`(settings.getInt(\"key\")).thenReturn(5)\n        val value = settingsStore.getInt(\"key\", 0)\n        assertThat(value, `is`(5))\n    }\n\n    @Test\n    fun `getFloat() reads float from settings`() {\n        `when`(settings.getFloat(\"key\")).thenReturn(8.43f)\n        val value = settingsStore.getFloat(\"key\", 1f)\n        assertThat(value, `is`(8.43f))\n    }\n\n    @Test\n    fun `getStringSet() reads set of strings from settings`() {\n        `when`(settings.getStringSet(\"key\")).thenReturn(setOf(\"x\", \"y\", \"z\"))\n        val value = settingsStore.getStringSet(\"key\", emptySet())\n        assertThat(value, `is`(setOf(\"x\", \"y\", \"z\")))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/source/SharedPreferencesSettingsProviderTest.kt",
    "content": "package org.odk.collect.android.preferences.source\n\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.CoreMatchers.not\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.settings.keys.MetaKeys.CURRENT_PROJECT_ID\n\n@RunWith(AndroidJUnit4::class)\nclass SharedPreferencesSettingsProviderTest {\n\n    private val settingsProvider = SharedPreferencesSettingsProvider(ApplicationProvider.getApplicationContext())\n\n    @Before\n    fun setup() {\n        settingsProvider.getMetaSettings().save(CURRENT_PROJECT_ID, null)\n    }\n\n    @Test\n    fun `The same mata settings should always be returned no matter what current project is`() {\n        val metaSettings = settingsProvider.getMetaSettings()\n        assertThat(metaSettings, `is`(settingsProvider.getMetaSettings()))\n\n        settingsProvider.getMetaSettings().save(CURRENT_PROJECT_ID, \"1234\")\n        assertThat(metaSettings, `is`(settingsProvider.getMetaSettings()))\n    }\n\n    @Test\n    fun `The same unprotected settings should always be returned for current project`() {\n        assertThat(settingsProvider.getUnprotectedSettings(), `is`(settingsProvider.getUnprotectedSettings()))\n    }\n\n    @Test\n    fun `The same unprotected settings should always be returned for given projectId`() {\n        assertThat(settingsProvider.getUnprotectedSettings(\"1234\"), `is`(settingsProvider.getUnprotectedSettings(\"1234\")))\n    }\n\n    @Test\n    fun `The same unprotected settings should be returned for given projectId and current project if the those are the same`() {\n        settingsProvider.getMetaSettings().save(CURRENT_PROJECT_ID, \"1234\")\n        assertThat(settingsProvider.getUnprotectedSettings(\"1234\"), `is`(settingsProvider.getUnprotectedSettings()))\n    }\n\n    @Test\n    fun `Different unprotected settings should be returned for different projectIds`() {\n        assertThat(settingsProvider.getUnprotectedSettings(\"1234\"), `is`(not(settingsProvider.getUnprotectedSettings(\"4321\"))))\n    }\n\n    @Test\n    fun `Different unprotected settings should be returned for given projectId and current project if those are not the same`() {\n        assertThat(settingsProvider.getUnprotectedSettings(\"1234\"), `is`(not(settingsProvider.getUnprotectedSettings())))\n    }\n\n    @Test\n    fun `The same protected settings should always be returned for current project`() {\n        assertThat(settingsProvider.getProtectedSettings(), `is`(settingsProvider.getProtectedSettings()))\n    }\n\n    @Test\n    fun `The same protected settings should always be returned for given projectId`() {\n        assertThat(settingsProvider.getProtectedSettings(\"1234\"), `is`(settingsProvider.getProtectedSettings(\"1234\")))\n    }\n\n    @Test\n    fun `The same protected settings should be returned for given projectId and current project if those are the same`() {\n        settingsProvider.getMetaSettings().save(CURRENT_PROJECT_ID, \"1234\")\n        assertThat(settingsProvider.getProtectedSettings(\"1234\"), `is`(settingsProvider.getProtectedSettings()))\n    }\n\n    @Test\n    fun `Different protected settings should be returned for different projectIds`() {\n        assertThat(settingsProvider.getProtectedSettings(\"1234\"), `is`(not(settingsProvider.getProtectedSettings(\"4321\"))))\n    }\n\n    @Test\n    fun `Different protected settings should be returned for given projectId and current project if those are not the same`() {\n        assertThat(settingsProvider.getProtectedSettings(\"1234\"), `is`(not(settingsProvider.getProtectedSettings())))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/preferences/source/SharedPreferencesSettingsTest.kt",
    "content": "package org.odk.collect.android.preferences.source\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport androidx.test.core.app.ApplicationProvider\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.hamcrest.Matchers.nullValue\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.Mockito.mock\nimport org.mockito.Mockito.never\nimport org.mockito.Mockito.verify\nimport org.odk.collect.shared.settings.Settings\nimport org.robolectric.RobolectricTestRunner\nimport java.math.BigDecimal\n\n@RunWith(RobolectricTestRunner::class)\nclass SharedPreferencesSettingsTest {\n    private lateinit var sharedPreferencesSettings: SharedPreferencesSettings\n    private lateinit var sharedPreferences: SharedPreferences\n\n    @Before\n    fun setup() {\n        sharedPreferences = ApplicationProvider.getApplicationContext<Context>()\n            .getSharedPreferences(\"prefs\", Context.MODE_PRIVATE)\n    }\n\n    @Test\n    fun `When nothing has been saved to a string setting and a custom default value exist, that value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        assertThat(sharedPreferencesSettings.getString(KEY_STRING), `is`(DEFAULT_STRING_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to a string setting and there are no custom defaults, the default value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences)\n        assertThat(sharedPreferencesSettings.getString(KEY_STRING), `is`(nullValue()))\n    }\n\n    @Test\n    fun `When a value has been saved to a string setting, the saved value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        sharedPreferencesSettings.save(KEY_STRING, CUSTOM_STRING_VALUE)\n        assertThat(sharedPreferencesSettings.getString(KEY_STRING), `is`(CUSTOM_STRING_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to a boolean setting and a custom default value exist, that value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        assertThat(sharedPreferencesSettings.getBoolean(KEY_BOOLEAN), `is`(DEFAULT_BOOLEAN_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to a boolean setting and there are no custom defaults, the default value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences)\n        assertThat(sharedPreferencesSettings.getBoolean(KEY_BOOLEAN), `is`(false))\n    }\n\n    @Test\n    fun `When a value has been saved to a boolean setting, the saved value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        sharedPreferencesSettings.save(KEY_BOOLEAN, CUSTOM_BOOLEAN_VALUE)\n        assertThat(sharedPreferencesSettings.getBoolean(KEY_BOOLEAN), `is`(CUSTOM_BOOLEAN_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to a long setting and a custom default value exist, that value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        assertThat(sharedPreferencesSettings.getLong(KEY_LONG), `is`(DEFAULT_LONG_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to a long setting and there are no custom defaults, the default value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences)\n        assertThat(sharedPreferencesSettings.getLong(KEY_LONG), `is`(0L))\n    }\n\n    @Test\n    fun `When a value has been saved to a long setting, the saved value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        sharedPreferencesSettings.save(KEY_LONG, CUSTOM_LONG_VALUE)\n        assertThat(sharedPreferencesSettings.getLong(KEY_LONG), `is`(CUSTOM_LONG_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to an int setting and a custom default value exist, that value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        assertThat(sharedPreferencesSettings.getInt(KEY_INT), `is`(DEFAULT_INT_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to an int setting and there are no custom defaults, the default value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences)\n        assertThat(sharedPreferencesSettings.getInt(KEY_INT), `is`(0))\n    }\n\n    @Test\n    fun `When a value has been saved to int setting, the saved value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        sharedPreferencesSettings.save(KEY_INT, CUSTOM_INT_VALUE)\n        assertThat(sharedPreferencesSettings.getInt(KEY_INT), `is`(CUSTOM_INT_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to a float setting and a custom default value exist, that value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        assertThat(sharedPreferencesSettings.getFloat(KEY_FLOAT), `is`(DEFAULT_FLOAT_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to a float setting and there are no custom defaults, the default value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences)\n        assertThat(sharedPreferencesSettings.getFloat(KEY_FLOAT), `is`(0f))\n    }\n\n    @Test\n    fun `When a value has been saved to a float setting, the saved value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        sharedPreferencesSettings.save(KEY_FLOAT, CUSTOM_FLOAT_VALUE)\n        assertThat(sharedPreferencesSettings.getFloat(KEY_FLOAT), `is`(CUSTOM_FLOAT_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to a stringSet setting and a custom default value exist, that value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        assertThat(sharedPreferencesSettings.getStringSet(KEY_SET), `is`(DEFAULT_SET_VALUE))\n    }\n\n    @Test\n    fun `When nothing has been saved to a stringSet setting and there are no custom defaults, the default value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences)\n        assertThat(sharedPreferencesSettings.getStringSet(KEY_SET), `is`(emptySet()))\n    }\n\n    @Test\n    fun `When a value has been saved to stringSet setting, the saved value should be returned`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        sharedPreferencesSettings.save(KEY_SET, CUSTOM_SET_VALUE)\n        assertThat(sharedPreferencesSettings.getStringSet(KEY_SET), `is`(CUSTOM_SET_VALUE))\n    }\n\n    @Test\n    fun `When multiple settings have been saved in one go, all values should be properly saved`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences)\n        sharedPreferencesSettings.saveAll(\n            mapOf(\n                KEY_STRING to CUSTOM_STRING_VALUE,\n                KEY_BOOLEAN to CUSTOM_BOOLEAN_VALUE,\n                KEY_FLOAT to CUSTOM_FLOAT_VALUE\n            )\n        )\n        assertThat(sharedPreferencesSettings.getString(KEY_STRING), `is`(CUSTOM_STRING_VALUE))\n        assertThat(sharedPreferencesSettings.getBoolean(KEY_BOOLEAN), `is`(CUSTOM_BOOLEAN_VALUE))\n        assertThat(sharedPreferencesSettings.getFloat(KEY_FLOAT), `is`(CUSTOM_FLOAT_VALUE))\n    }\n\n    @Test(expected = RuntimeException::class)\n    fun `When unsupported value passed to save, RuntimeException should be thrown`() {\n        sharedPreferencesSettings.save(KEY_FLOAT, BigDecimal(5))\n    }\n\n    @Test\n    fun `When remove() called, should saved setting be properly removed`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        sharedPreferencesSettings.save(KEY_STRING, CUSTOM_STRING_VALUE)\n        sharedPreferencesSettings.remove(KEY_STRING)\n        assertThat(sharedPreferencesSettings.getAll().containsKey(KEY_STRING), `is`(false))\n    }\n\n    @Test\n    fun `When reset() called, should default value be loaded for given key`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        sharedPreferencesSettings.save(KEY_STRING, CUSTOM_STRING_VALUE)\n        sharedPreferencesSettings.reset(KEY_STRING)\n        assertThat(sharedPreferencesSettings.getAll()[KEY_STRING], `is`(DEFAULT_STRING_VALUE))\n    }\n\n    @Test\n    fun `When clear() called, should all saved settings be removed and defaults should be loaded`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n\n        sharedPreferencesSettings.save(KEY_STRING, CUSTOM_STRING_VALUE)\n        sharedPreferencesSettings.save(KEY_BOOLEAN, CUSTOM_BOOLEAN_VALUE)\n        sharedPreferencesSettings.save(KEY_LONG, CUSTOM_LONG_VALUE)\n        sharedPreferencesSettings.save(KEY_INT, CUSTOM_INT_VALUE)\n        sharedPreferencesSettings.save(KEY_FLOAT, CUSTOM_FLOAT_VALUE)\n        sharedPreferencesSettings.save(KEY_SET, CUSTOM_SET_VALUE)\n        sharedPreferencesSettings.save(\"something_else\", \"something_else\")\n\n        sharedPreferencesSettings.clear()\n\n        assertDefaultPrefs(defaultSettings)\n        assertThat(sharedPreferencesSettings.contains(\"something_else\"), `is`(false))\n    }\n\n    @Test\n    fun `When getAll() called, should return all saved settings`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences)\n        sharedPreferencesSettings.saveAll(\n            mapOf(\n                KEY_STRING to CUSTOM_STRING_VALUE,\n                KEY_FLOAT to CUSTOM_FLOAT_VALUE\n            )\n        )\n        val allPreferences = sharedPreferencesSettings.getAll()\n        assertThat(allPreferences.size, `is`(2))\n        assertThat(allPreferences.containsKey(KEY_STRING), `is`(true))\n        assertThat(allPreferences.containsValue(CUSTOM_STRING_VALUE), `is`(true))\n        assertThat(allPreferences.containsKey(KEY_FLOAT), `is`(true))\n        assertThat(allPreferences.containsValue(CUSTOM_FLOAT_VALUE), `is`(true))\n    }\n\n    @Test\n    fun `When contains() called, should return true for saved settings`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences)\n        sharedPreferencesSettings.saveAll(\n            mapOf(\n                KEY_STRING to CUSTOM_STRING_VALUE,\n                KEY_FLOAT to CUSTOM_FLOAT_VALUE\n            )\n        )\n        assertThat(sharedPreferencesSettings.contains(KEY_STRING), `is`(true))\n        assertThat(sharedPreferencesSettings.contains(KEY_BOOLEAN), `is`(false))\n        assertThat(sharedPreferencesSettings.contains(KEY_LONG), `is`(false))\n        assertThat(sharedPreferencesSettings.contains(KEY_INT), `is`(false))\n        assertThat(sharedPreferencesSettings.contains(KEY_FLOAT), `is`(true))\n        assertThat(sharedPreferencesSettings.contains(KEY_SET), `is`(false))\n    }\n\n    @Test\n    fun `When setDefaultForAllSettingsWithoutValues() called, should save all defaults that do not exist`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        sharedPreferencesSettings.save(KEY_STRING, CUSTOM_STRING_VALUE)\n        assertThat(sharedPreferencesSettings.getAll().size, `is`(1))\n        sharedPreferencesSettings.setDefaultForAllSettingsWithoutValues()\n        val prefs = sharedPreferencesSettings.getAll()\n        assertThat(prefs.size, `is`(defaultSettings.size))\n        assertThat(prefs[KEY_STRING], `is`(CUSTOM_STRING_VALUE))\n        assertThat(prefs[KEY_BOOLEAN], `is`(DEFAULT_BOOLEAN_VALUE))\n        assertThat(prefs[KEY_LONG], `is`(DEFAULT_LONG_VALUE))\n        assertThat(prefs[KEY_INT], `is`(DEFAULT_INT_VALUE))\n        assertThat(prefs[KEY_FLOAT], `is`(DEFAULT_FLOAT_VALUE))\n        assertThat(prefs[KEY_SET], `is`(DEFAULT_SET_VALUE))\n    }\n\n    @Test\n    fun `When SettingChangeListener registered, should listen to changes in settings`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        val listener: Settings.OnSettingChangeListener =\n            mock(Settings.OnSettingChangeListener::class.java)\n        sharedPreferencesSettings.registerOnSettingChangeListener(listener)\n        sharedPreferencesSettings.save(\"test\", \"string\")\n        verify(listener).onSettingChanged(\"test\")\n\n        sharedPreferencesSettings.unregisterOnSettingChangeListener(listener)\n        sharedPreferencesSettings.save(\"test2\", \"string2\")\n        verify(listener, never()).onSettingChanged(\"test2\")\n    }\n\n    @Test\n    fun `can register multiple change listeners`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        val listener1: Settings.OnSettingChangeListener =\n            mock(Settings.OnSettingChangeListener::class.java)\n        sharedPreferencesSettings.registerOnSettingChangeListener(listener1)\n\n        val listener2: Settings.OnSettingChangeListener =\n            mock(Settings.OnSettingChangeListener::class.java)\n        sharedPreferencesSettings.registerOnSettingChangeListener(listener2)\n\n        sharedPreferencesSettings.save(\"test\", \"string\")\n        verify(listener1).onSettingChanged(\"test\")\n        verify(listener2).onSettingChanged(\"test\")\n\n        sharedPreferencesSettings.unregisterOnSettingChangeListener(listener1)\n        sharedPreferencesSettings.save(\"test2\", \"string2\")\n        verify(listener1, never()).onSettingChanged(\"test2\")\n        verify(listener2).onSettingChanged(\"test2\")\n    }\n\n    /*\n    Accounts for a possible bug listeners are unregistered but stay in the implementations list. In\n    that case the second unregister results in the wrong listener being unregistered from\n    SharedPreferences.\n     */\n    @Test\n    fun `unregister works multiple times for the same listener`() {\n        sharedPreferencesSettings = SharedPreferencesSettings(sharedPreferences, defaultSettings)\n        val listener: Settings.OnSettingChangeListener =\n            mock(Settings.OnSettingChangeListener::class.java)\n\n        sharedPreferencesSettings.registerOnSettingChangeListener(listener)\n        sharedPreferencesSettings.unregisterOnSettingChangeListener(listener)\n\n        sharedPreferencesSettings.registerOnSettingChangeListener(listener)\n        sharedPreferencesSettings.unregisterOnSettingChangeListener(listener)\n\n        sharedPreferencesSettings.save(\"test\", \"string\")\n        verify(listener, never()).onSettingChanged(\"test\")\n    }\n\n    fun assertDefaultPrefs(allSettings: Map<String, *>) {\n        assertThat(allSettings.size, `is`(defaultSettings.size))\n        assertThat(allSettings[KEY_STRING], `is`(DEFAULT_STRING_VALUE))\n        assertThat(allSettings[KEY_BOOLEAN], `is`(DEFAULT_BOOLEAN_VALUE))\n        assertThat(allSettings[KEY_LONG], `is`(DEFAULT_LONG_VALUE))\n        assertThat(allSettings[KEY_INT], `is`(DEFAULT_INT_VALUE))\n        assertThat(allSettings[KEY_FLOAT], `is`(DEFAULT_FLOAT_VALUE))\n        assertThat(allSettings[KEY_SET], `is`(DEFAULT_SET_VALUE))\n    }\n\n    private val defaultSettings: Map<String, Any>\n        get() = mapOf(\n            KEY_STRING to DEFAULT_STRING_VALUE,\n            KEY_BOOLEAN to DEFAULT_BOOLEAN_VALUE,\n            KEY_LONG to DEFAULT_LONG_VALUE,\n            KEY_INT to DEFAULT_INT_VALUE,\n            KEY_FLOAT to DEFAULT_FLOAT_VALUE,\n            KEY_SET to DEFAULT_SET_VALUE\n        )\n\n    companion object {\n        private const val KEY_STRING = \"keyString\"\n        private const val KEY_BOOLEAN = \"keyBoolean\"\n        private const val KEY_LONG = \"keyLong\"\n        private const val KEY_INT = \"keyInt\"\n        private const val KEY_FLOAT = \"keyFloat\"\n        private const val KEY_SET = \"keySet\"\n        private const val DEFAULT_STRING_VALUE = \"qwerty\"\n        private const val DEFAULT_BOOLEAN_VALUE = true\n        private const val DEFAULT_LONG_VALUE = 15L\n        private const val DEFAULT_INT_VALUE = 23\n        private const val DEFAULT_FLOAT_VALUE = 44.65f\n        private val DEFAULT_SET_VALUE: Set<String> = setOf(\"a\", \"b\", \"c\")\n        private const val CUSTOM_STRING_VALUE = \"12345\"\n        private const val CUSTOM_BOOLEAN_VALUE = false\n        private const val CUSTOM_LONG_VALUE = 5L\n        private const val CUSTOM_INT_VALUE = 3\n        private const val CUSTOM_FLOAT_VALUE = 8.25f\n        private val CUSTOM_SET_VALUE: Set<String> = setOf(\"x\", \"y\", \"z\")\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/ExistingProjectMigratorTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.preference.PreferenceManager\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.storage.StorageSubdirectory\nimport org.odk.collect.settings.keys.MetaKeys\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass ExistingProjectMigratorTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n    private val component = DaggerUtils.getComponent(context)\n    private val existingProjectMigrator = component.existingProjectMigrator()\n    private val storagePathProvider = component.storagePathProvider()\n    private val projectsRepository = component.projectsRepository()\n    private val settingsProvider = component.settingsProvider()\n    private val currentProjectProvider = component.currentProjectProvider()\n    private val rootDir = storagePathProvider.odkRootDirPath\n\n    @Before\n    fun setup() {\n        // Should not run when upgrading from version with LAST_LAUNCHED\n        settingsProvider.getMetaSettings().remove(MetaKeys.LAST_LAUNCHED)\n    }\n\n    @Test\n    fun `creates existing project with details based on its url`() {\n        PreferenceManager\n            .getDefaultSharedPreferences(context)\n            .edit()\n            .putString(ProjectKeys.KEY_SERVER_URL, \"https://my-server.com\")\n            .apply()\n\n        existingProjectMigrator.run()\n\n        assertThat(projectsRepository.getAll().size, `is`(1))\n\n        val project = projectsRepository.getAll()[0]\n        assertThat(project.name, `is`(\"my-server.com\"))\n        assertThat(project.icon, `is`(\"M\"))\n        assertThat(project.color, `is`(\"#53BDD4\"))\n    }\n\n    @Test\n    fun `moves files from root`() {\n        val legacyRootDirs = listOf(\n            File(rootDir, \"forms\"),\n            File(rootDir, \"instances\"),\n            File(rootDir, \"metadata\"),\n            File(rootDir, \"layers\"),\n            File(rootDir, \"settings\")\n        )\n\n        legacyRootDirs.forEach {\n            it.mkdir()\n            TempFiles.createTempFile(it, \"file\", \".temp\")\n        }\n\n        existingProjectMigrator.run()\n        val existingProject = currentProjectProvider.requireCurrentProject()\n\n        legacyRootDirs.forEach {\n            assertThat(it.exists(), `is`(false))\n        }\n\n        getProjectDirPaths(existingProject.uuid).forEach {\n            val dir = File(it)\n            assertThat(dir.exists(), `is`(true))\n            assertThat(dir.isDirectory, `is`(true))\n            if (!it.endsWith(\"cache\")) {\n                assertThat(dir.listFiles()!!.isEmpty(), `is`(false))\n            }\n        }\n    }\n\n    @Test\n    fun `deletes and does not move cache directory`() {\n        val cacheDir = File(rootDir, \".cache\")\n        cacheDir.mkdir()\n        TempFiles.createTempFile(cacheDir, \"file\", \".temp\")\n\n        existingProjectMigrator.run()\n        val existingProject = currentProjectProvider.requireCurrentProject()\n\n        assertThat(cacheDir.exists(), `is`(false))\n\n        getProjectDirPaths(existingProject.uuid).forEach {\n            val dir = File(it)\n            assertThat(dir.exists(), `is`(true))\n            assertThat(dir.isDirectory, `is`(true))\n            assertThat(dir.listFiles()!!.isEmpty(), `is`(true))\n        }\n    }\n\n    @Test\n    fun `still copies other files if a directory is missing`() {\n        val legacyRootDirsWithoutForms = listOf(\n            File(rootDir, \"instances\"),\n            File(rootDir, \"metadata\"),\n            File(rootDir, \"layers\"),\n            File(rootDir, \"settings\")\n        )\n\n        legacyRootDirsWithoutForms.forEach {\n            it.mkdir()\n            TempFiles.createTempFile(it, \"file\", \".temp\")\n        }\n\n        existingProjectMigrator.run()\n        val existingProject = currentProjectProvider.requireCurrentProject()\n        getProjectDirPaths(existingProject.uuid).forEach {\n            val dir = File(it)\n            assertThat(dir.exists(), `is`(true))\n            assertThat(dir.isDirectory, `is`(true))\n\n            if (it.endsWith(\"forms\") || it.endsWith(\"cache\")) {\n                assertThat(dir.listFiles()!!.isEmpty(), `is`(true))\n            } else {\n                assertThat(dir.listFiles()!!.isEmpty(), `is`(false))\n            }\n        }\n    }\n\n    @Test\n    fun `migrates unprotected and protected settings`() {\n        val oldGeneralSettings = PreferenceManager.getDefaultSharedPreferences(context)\n        oldGeneralSettings.edit().putString(\"generalKey\", \"generalValue\").apply()\n        val oldAdminSettings = context.getSharedPreferences(\"admin_prefs\", Context.MODE_PRIVATE)\n        oldAdminSettings.edit().putString(\"adminKey\", \"adminValue\").apply()\n\n        existingProjectMigrator.run()\n        val existingProject = currentProjectProvider.requireCurrentProject()\n\n        val generalSettings = settingsProvider.getUnprotectedSettings(existingProject.uuid)\n        assertThat(\n            generalSettings.getString(\"generalKey\"),\n            `is`(\"generalValue\")\n        )\n        val adminSettings = settingsProvider.getProtectedSettings(existingProject.uuid)\n        assertThat(adminSettings.getString(\"adminKey\"), `is`(\"adminValue\"))\n    }\n\n    @Test\n    fun `has key`() {\n        assertThat(existingProjectMigrator.key(), `is`(MetaKeys.EXISTING_PROJECT_IMPORTED))\n    }\n\n    /**\n     * Versions of the app before v2021.3.3 did not include a call to [AppUpgrader.install] and so\n     * this [Upgrade] would get incorrectly run on upgrades from versions after\n     * [AppUpgrader] and Projects (along with LAST_LAUNCHED) were added but before the call\n     * to [AppUpgrader.install] was introduced.\n     */\n    @Test\n    fun `does not run if previous app version already had projects`() {\n        PreferenceManager\n            .getDefaultSharedPreferences(context)\n            .edit()\n            .putString(ProjectKeys.KEY_SERVER_URL, \"https://my-server.com\")\n            .apply()\n\n        settingsProvider.getMetaSettings().save(MetaKeys.LAST_LAUNCHED, 3115)\n\n        existingProjectMigrator.run()\n        assertThat(projectsRepository.getAll().size, `is`(0))\n    }\n\n    private fun getProjectDirPaths(projectId: String): Array<String> {\n        return arrayOf(\n            storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, projectId),\n            storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, projectId),\n            storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, projectId),\n            storagePathProvider.getOdkDirPath(StorageSubdirectory.METADATA, projectId),\n            storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS, projectId),\n            storagePathProvider.getOdkDirPath(StorageSubdirectory.SETTINGS, projectId)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/ManualProjectCreatorDialogTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.pressBack\nimport androidx.test.espresso.action.ViewActions.replaceText\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.intent.Intents\nimport androidx.test.espresso.intent.matcher.IntentMatchers\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.espresso.matcher.ViewMatchers.withHint\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.hamcrest.Matchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.application.initialization.AnalyticsInitializer\nimport org.odk.collect.android.application.initialization.MapsInitializer\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.Matchers.isPasswordHidden\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectCreator\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.odk.collect.settings.SettingsProvider\nimport org.robolectric.shadows.ShadowToast\n\n@RunWith(AndroidJUnit4::class)\nclass ManualProjectCreatorDialogTest {\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Test\n    fun `Password should be protected`() {\n        val scenario = launcherRule.launch(ManualProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            onView(withHint(org.odk.collect.strings.R.string.server_url)).inRoot(isDialog())\n                .perform(replaceText(\"123456789\"))\n            onView(withHint(org.odk.collect.strings.R.string.server_url)).inRoot(isDialog())\n                .check(matches(not(isPasswordHidden())))\n\n            onView(withHint(org.odk.collect.strings.R.string.username)).inRoot(isDialog()).perform(replaceText(\"123456789\"))\n            onView(withHint(org.odk.collect.strings.R.string.username)).inRoot(isDialog())\n                .check(matches(not(isPasswordHidden())))\n\n            onView(withHint(org.odk.collect.strings.R.string.password)).inRoot(isDialog()).perform(replaceText(\"123456789\"))\n            onView(withHint(org.odk.collect.strings.R.string.password)).inRoot(isDialog())\n                .check(matches(isPasswordHidden()))\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on the 'Cancel' button`() {\n        val scenario = launcherRule.launch(ManualProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.isVisible, `is`(true))\n            onView(withText(org.odk.collect.strings.R.string.cancel)).inRoot(isDialog()).perform(click())\n            assertThat(it.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on a device back button`() {\n        val scenario = launcherRule.launch(ManualProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.isVisible, `is`(true))\n            onView(isRoot()).perform(pressBack())\n            assertThat(it.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `The 'Add' button should be disabled when url is blank`() {\n        val scenario = launcherRule.launch(ManualProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.isVisible, `is`(true))\n\n            onView(withText(org.odk.collect.strings.R.string.add)).inRoot(isDialog()).perform(click())\n            assertThat(it.isVisible, `is`(true))\n\n            onView(withHint(org.odk.collect.strings.R.string.server_url)).inRoot(isDialog()).perform(replaceText(\" \"))\n            onView(withText(org.odk.collect.strings.R.string.add)).inRoot(isDialog()).perform(click())\n            assertThat(it.isVisible, `is`(true))\n        }\n    }\n\n    @Test\n    fun `When URL has no protocol, a toast is displayed`() {\n        val scenario = launcherRule.launch(ManualProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            onView(withHint(org.odk.collect.strings.R.string.server_url)).inRoot(isDialog())\n                .perform(replaceText(\"demo.getodk.org\"))\n            onView(withText(org.odk.collect.strings.R.string.add)).inRoot(isDialog()).perform(click())\n            assertThat(it.isVisible, `is`(true))\n\n            val toastText = ShadowToast.getTextOfLatestToast()\n            assertThat(toastText, `is`(it.getString(org.odk.collect.strings.R.string.url_error)))\n        }\n    }\n\n    @Test\n    fun `Server project creation should be triggered after clicking on the 'Add' button`() {\n        val projectCreator = mock<ProjectCreator> {}\n        val projectsDataService = mock<ProjectsDataService> {\n            on { requireCurrentProject() } doReturn Project.DEMO_PROJECT\n        }\n\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesProjectCreator(\n                projectsRepository: ProjectsRepository,\n                projectsDataService: ProjectsDataService,\n                settingsImporter: ODKAppSettingsImporter,\n                settingsProvider: SettingsProvider\n            ): ProjectCreator {\n                return projectCreator\n            }\n\n            override fun providesCurrentProjectProvider(\n                application: Application,\n                settingsProvider: SettingsProvider,\n                projectsRepository: ProjectsRepository,\n                analyticsInitializer: AnalyticsInitializer,\n                context: Context,\n                mapsInitializer: MapsInitializer\n            ): ProjectsDataService {\n                return projectsDataService\n            }\n        })\n\n        val scenario = launcherRule.launch(ManualProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            onView(withHint(org.odk.collect.strings.R.string.server_url)).inRoot(isDialog())\n                .perform(replaceText(\"https://my-server.com\"))\n            onView(withHint(org.odk.collect.strings.R.string.username)).inRoot(isDialog()).perform(replaceText(\"adam\"))\n            onView(withHint(org.odk.collect.strings.R.string.password)).inRoot(isDialog()).perform(replaceText(\"1234\"))\n\n            onView(withText(org.odk.collect.strings.R.string.add)).inRoot(isDialog()).perform(click())\n            verify(projectCreator).createNewProject(\"{\\\"general\\\":{\\\"server_url\\\":\\\"https:\\\\/\\\\/my-server.com\\\",\\\"username\\\":\\\"adam\\\",\\\"password\\\":\\\"1234\\\"},\\\"admin\\\":{},\\\"project\\\":{}}\", true)\n        }\n    }\n\n    @Test\n    fun `Server project creation goes to main menu`() {\n        val scenario = launcherRule.launch(ManualProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            onView(withHint(org.odk.collect.strings.R.string.server_url)).inRoot(isDialog())\n                .perform(replaceText(\"https://my-server.com\"))\n\n            Intents.init()\n            onView(withText(org.odk.collect.strings.R.string.add)).inRoot(isDialog()).perform(click())\n            Intents.intended(IntentMatchers.hasComponent(MainMenuActivity::class.java.name))\n            Intents.release()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/ProjectCreatorImplTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Before\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoInteractions\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectConfigurationResult\nimport org.odk.collect.projects.ProjectCreator\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.shared.settings.Settings\n\nclass ProjectCreatorImplTest {\n    private val json = \"{\\\"general\\\":{\\\"server_url\\\":\\\"https:\\\\/\\\\/my-server.com\\\",\\\"username\\\":\\\"adam\\\",\\\"password\\\":\\\"1234\\\"},\\\"admin\\\":{}}\"\n    private val newProject = Project.New(\"my-server.com\", \"M\", \"#3e9fcc\")\n    private val savedProject = Project.Saved(\"1\", newProject)\n\n    private var projectsRepository = mock<ProjectsRepository> {\n        on { getAll() } doReturn listOf(savedProject)\n        on { save(Project.New(\"\", \"\", \"\")) } doReturn savedProject\n    }\n\n    private var projectsDataService = mock<ProjectsDataService> {\n        on { requireCurrentProject() } doReturn savedProject\n    }\n    private var settingsImporter = mock<ODKAppSettingsImporter> {}\n\n    private var unProtectedSettings = mock<Settings>()\n    private var protectedSettings = mock<Settings>()\n    private var settingsProvider = mock<SettingsProvider> {\n        on { getUnprotectedSettings(\"1\") } doReturn unProtectedSettings\n        on { getProtectedSettings(\"1\") } doReturn protectedSettings\n    }\n\n    private lateinit var projectCreator: ProjectCreator\n\n    @Before\n    fun setup() {\n        projectCreator = ProjectCreatorImpl(\n            projectsRepository,\n            projectsDataService,\n            settingsImporter,\n            settingsProvider\n        )\n    }\n\n    @Test\n    fun `Importing settings should be triggered with proper params`() {\n        projectCreator.createNewProject(json, true)\n        verify(settingsImporter).fromJSON(json, savedProject)\n    }\n\n    @Test\n    fun `When importing settings failed createNewProject() should return 'INVALID_SETTINGS'`() {\n        whenever(settingsImporter.fromJSON(json, savedProject)).thenReturn(ProjectConfigurationResult.INVALID_SETTINGS)\n\n        projectCreator.createNewProject(json, true)\n        assertThat(projectCreator.createNewProject(json, true), `is`(ProjectConfigurationResult.INVALID_SETTINGS))\n    }\n\n    @Test\n    fun `When importing settings contain GD protocol createNewProject() should return 'GD_PROJECT'`() {\n        whenever(settingsImporter.fromJSON(json, savedProject)).thenReturn(ProjectConfigurationResult.GD_PROJECT)\n\n        projectCreator.createNewProject(json, true)\n        assertThat(projectCreator.createNewProject(json, true), `is`(ProjectConfigurationResult.GD_PROJECT))\n    }\n\n    @Test\n    fun `When importing settings succeeded createNewProject() should return 'SUCCESS'`() {\n        whenever(settingsImporter.fromJSON(json, savedProject)).thenReturn(ProjectConfigurationResult.SUCCESS)\n\n        projectCreator.createNewProject(json, true)\n        assertThat(projectCreator.createNewProject(json, true), `is`(ProjectConfigurationResult.SUCCESS))\n    }\n\n    @Test\n    fun `When importing settings failed should created project be deleted`() {\n        whenever(settingsImporter.fromJSON(json, savedProject)).thenReturn(ProjectConfigurationResult.INVALID_SETTINGS)\n\n        projectCreator.createNewProject(json, true)\n        verify(projectsRepository).delete(savedProject.uuid)\n    }\n\n    @Test\n    fun `When importing settings contain GD protocol should created project be deleted`() {\n        whenever(settingsImporter.fromJSON(json, savedProject)).thenReturn(ProjectConfigurationResult.GD_PROJECT)\n\n        projectCreator.createNewProject(json, true)\n        verify(projectsRepository).delete(savedProject.uuid)\n    }\n\n    @Test\n    fun `When importing settings failed should prefs be cleared`() {\n        whenever(settingsImporter.fromJSON(json, savedProject)).thenReturn(ProjectConfigurationResult.INVALID_SETTINGS)\n\n        projectCreator.createNewProject(json, true)\n\n        verify(unProtectedSettings).clear()\n        verify(protectedSettings).clear()\n    }\n\n    @Test\n    fun `New project id should be set when switchToTheNewProject is true`() {\n        whenever(settingsImporter.fromJSON(json, savedProject)).thenReturn(ProjectConfigurationResult.SUCCESS)\n\n        projectCreator.createNewProject(json, true)\n        verify(projectsDataService).setCurrentProject(\"1\")\n    }\n\n    @Test\n    fun `New project id should not be set when switchToTheNewProject is false`() {\n        whenever(settingsImporter.fromJSON(json, savedProject)).thenReturn(ProjectConfigurationResult.SUCCESS)\n\n        projectCreator.createNewProject(json, false)\n        verifyNoInteractions(projectsDataService)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/ProjectDeleterTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.instanceOf\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.backgroundwork.FormUpdateScheduler\nimport org.odk.collect.android.backgroundwork.InstanceSubmitScheduler\nimport org.odk.collect.android.preferences.Defaults\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.androidshared.data.AppState\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.MetaKeys\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\nclass ProjectDeleterTest {\n    private val project1 = Project.Saved(\"1\", \"1\", \"1\", \"#ffffff\")\n    private val projectsRepository = InMemProjectsRepository().apply {\n        save(project1)\n    }\n    private val settingsProvider = InMemSettingsProvider()\n    private val projectsDataService = ProjectsDataService(AppState(), settingsProvider, projectsRepository, mock(), mock())\n    private val formUpdateScheduler = mock<FormUpdateScheduler>()\n    private val instanceSubmitScheduler = mock<InstanceSubmitScheduler>()\n    private val storagePathProvider = mock<StoragePathProvider>().apply {\n        whenever(getProjectRootDirPath(project1.uuid)).thenReturn(\"\")\n    }\n    private val deleter = ProjectDeleter(\n        projectsRepository,\n        projectsDataService,\n        formUpdateScheduler,\n        instanceSubmitScheduler,\n        storagePathProvider,\n        settingsProvider\n    )\n\n    @Test\n    fun `Deleting project removes it from projects repository`() {\n        val result = deleter.deleteProject(project1.uuid)\n\n        assertThat(result, instanceOf(DeleteProjectResult.DeletedSuccessfullyLastProject::class.java))\n        assertThat(projectsRepository.projects.size, equalTo(0))\n    }\n\n    @Test\n    fun `Deleting project cancels scheduled form updates and instance submits`() {\n        deleter.deleteProject(project1.uuid)\n\n        verify(formUpdateScheduler).cancelUpdates(project1.uuid)\n        verify(instanceSubmitScheduler).cancelSubmit(project1.uuid)\n    }\n\n    @Test\n    fun `Deleting project clears its settings`() {\n        settingsProvider.getMetaSettings().save(MetaKeys.KEY_INSTALL_ID, \"1234\")\n        settingsProvider.getUnprotectedSettings(project1.uuid).save(ProjectKeys.KEY_SERVER_URL, \"https://my-server.com\")\n        settingsProvider.getProtectedSettings(project1.uuid).save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n        settingsProvider.getUnprotectedSettings(\"2\").save(ProjectKeys.KEY_SERVER_URL, \"https://my-server.com\")\n        settingsProvider.getProtectedSettings(\"2\").save(ProtectedProjectKeys.KEY_AUTOSEND, false)\n\n        deleter.deleteProject(project1.uuid)\n\n        assertThat(settingsProvider.getMetaSettings().getString(MetaKeys.KEY_INSTALL_ID), equalTo(\"1234\"))\n        settingsProvider.getUnprotectedSettings(project1.uuid).getAll().forEach { (key, value) ->\n            assertThat(value, equalTo(Defaults.protected[key]))\n        }\n        settingsProvider.getProtectedSettings(project1.uuid).getAll().forEach { (key, value) ->\n            assertThat(value, equalTo(Defaults.protected[key]))\n        }\n\n        assertThat(settingsProvider.getUnprotectedSettings(\"2\").getString(ProjectKeys.KEY_SERVER_URL), equalTo(\"https://my-server.com\"))\n        assertThat(settingsProvider.getProtectedSettings(\"2\").getBoolean(ProtectedProjectKeys.KEY_AUTOSEND), equalTo(false))\n    }\n\n    @Test\n    fun `If the deleted project was the last one return DeletedSuccessfully with null parameter`() {\n        val result = deleter.deleteProject(project1.uuid)\n\n        assertThat(result, instanceOf(DeleteProjectResult.DeletedSuccessfullyLastProject::class.java))\n    }\n\n    @Test\n    fun `If the deleted project was the current one and not the last one set the current project and return the new current one`() {\n        val project2 = Project.Saved(\"2\", \"2\", \"2\", \"#cccccc\")\n        projectsRepository.save(project2)\n        projectsDataService.setCurrentProject(project1.uuid)\n\n        val result = deleter.deleteProject(project1.uuid)\n\n        assertThat(projectsDataService.requireCurrentProject().uuid, equalTo(project2.uuid))\n        assertThat((result as DeleteProjectResult.DeletedSuccessfullyCurrentProject).newCurrentProject, equalTo(project2))\n    }\n\n    @Test\n    fun `If the deleted project was not the current one and not the last one do not set the current project and return DeletedSuccessfully with null parameter`() {\n        val project2 = Project.Saved(\"2\", \"2\", \"2\", \"#cccccc\")\n        projectsRepository.save(project2)\n        projectsDataService.setCurrentProject(project2.uuid)\n\n        val result = deleter.deleteProject(project1.uuid)\n\n        assertThat(projectsDataService.requireCurrentProject().uuid, equalTo(project2.uuid))\n        assertThat(result, instanceOf(DeleteProjectResult.DeletedSuccessfullyInactiveProject::class.java))\n    }\n\n    @Test\n    fun `Project directory should be removed`() {\n        val projectDir = TempFiles.createTempDir()\n        File(projectDir, \"dir\").mkdir()\n\n        assertThat(projectDir.exists(), equalTo(true))\n        assertThat(projectDir.listFiles().size, equalTo(1))\n\n        whenever(storagePathProvider.getProjectRootDirPath(project1.uuid)).thenReturn(projectDir.absolutePath)\n\n        deleter.deleteProject(project1.uuid)\n\n        assertThat(projectDir.exists(), equalTo(false))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/ProjectIconViewTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.content.Context\nimport android.graphics.Color\nimport android.graphics.drawable.GradientDrawable\nimport android.widget.TextView\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.projects.Project\n\n@RunWith(AndroidJUnit4::class)\nclass ProjectIconViewTest {\n\n    private val context: Context by lazy { ApplicationProvider.getApplicationContext() }\n\n    @Test\n    fun `shows project icon with color as background`() {\n        val view = ProjectIconView(context)\n        view.project = Project.New(\"SOM\", \"S\", \"#ffffff\")\n        assertThat(view.findViewById<TextView>(R.id.project_icon_text).text, equalTo(\"S\"))\n\n        val background = view.findViewById<TextView>(R.id.project_icon_text).background as GradientDrawable\n        assertThat(background.color!!.defaultColor, Matchers.equalTo(Color.parseColor(\"#ffffff\")))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/ProjectListItemViewTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.content.Context\nimport android.graphics.Color\nimport android.graphics.drawable.GradientDrawable\nimport android.widget.TextView\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.R\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\n@RunWith(AndroidJUnit4::class)\nclass ProjectListItemViewTest {\n\n    private val context: Context by lazy { ApplicationProvider.getApplicationContext() }\n\n    @Test\n    fun `shows project name`() {\n        val generalSettings = mock<Settings> {\n            on { getString(ProjectKeys.KEY_USERNAME) } doReturn \"\"\n            on { getString(ProjectKeys.KEY_SERVER_URL) } doReturn \"\"\n        }\n\n        val view = ProjectListItemView(context)\n        view.setupView(Project.New(\"SOM\", \"S\", \"#ffffff\"), generalSettings)\n        assertThat(view.findViewById<TextView>(R.id.project_name).text, equalTo(\"SOM\"))\n    }\n\n    @Test\n    fun `shows project icon with color as background`() {\n        val generalSettings = mock<Settings> {\n            on { getString(ProjectKeys.KEY_USERNAME) } doReturn \"\"\n            on { getString(ProjectKeys.KEY_SERVER_URL) } doReturn \"\"\n        }\n\n        val view = ProjectListItemView(context)\n        view.setupView(Project.New(\"SOM\", \"S\", \"#ffffff\"), generalSettings)\n        assertThat(view.findViewById<TextView>(R.id.project_icon_text).text, equalTo(\"S\"))\n\n        val background = view.findViewById<TextView>(R.id.project_icon_text).background as GradientDrawable\n        assertThat(background.color!!.defaultColor, equalTo(Color.parseColor(\"#ffffff\")))\n    }\n\n    @Test\n    fun `shows project username and url`() {\n        val generalSettings = mock<Settings> {\n            on { getString(ProjectKeys.KEY_USERNAME) } doReturn \"Adam\"\n            on { getString(ProjectKeys.KEY_SERVER_URL) } doReturn \"https://my-project.com\"\n        }\n\n        val view = ProjectListItemView(context)\n        view.setupView(Project.New(\"SOM\", \"S\", \"#ffffff\"), generalSettings)\n        assertThat(view.findViewById<TextView>(R.id.project_subtext).text, equalTo(\"Adam / my-project.com\"))\n    }\n\n    @Test\n    fun `shows project only url if username is not set`() {\n        val generalSettings = mock<Settings> {\n            on { getString(ProjectKeys.KEY_USERNAME) } doReturn \"\"\n            on { getString(ProjectKeys.KEY_SERVER_URL) } doReturn \"https://my-project.com\"\n        }\n\n        val view = ProjectListItemView(context)\n        view.setupView(Project.New(\"SOM\", \"S\", \"#ffffff\"), generalSettings)\n        assertThat(view.findViewById<TextView>(R.id.project_subtext).text, equalTo(\"my-project.com\"))\n    }\n\n    @Test\n    fun `shows username only if url is not set`() {\n        val generalSettings = mock<Settings> {\n            on { getString(ProjectKeys.KEY_USERNAME) } doReturn \"foo@bar.baz\"\n            on { getString(ProjectKeys.KEY_SERVER_URL) } doReturn \"\"\n        }\n\n        val view = ProjectListItemView(context)\n        view.setupView(Project.New(\"SOM\", \"S\", \"#ffffff\"), generalSettings)\n        assertThat(view.findViewById<TextView>(R.id.project_subtext).text, equalTo(\"foo@bar.baz / \"))\n    }\n\n    @Test\n    fun `passes through URL value that can't be parsed as URL`() {\n        val generalSettings = mock<Settings> {\n            on { getString(ProjectKeys.KEY_USERNAME) } doReturn \"foo\"\n            on { getString(ProjectKeys.KEY_SERVER_URL) } doReturn \"something something\"\n        }\n\n        val view = ProjectListItemView(context)\n        view.setupView(Project.New(\"SOM\", \"S\", \"#ffffff\"), generalSettings)\n        assertThat(view.findViewById<TextView>(R.id.project_subtext).text, equalTo(\"foo / something something\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/ProjectResetterTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Assert.assertEquals\nimport org.junit.Assert.assertFalse\nimport org.junit.Assert.assertTrue\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.preferences.Defaults\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.storage.StorageSubdirectory\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.utilities.ChangeLockProvider\nimport org.odk.collect.android.utilities.FormsRepositoryProvider\nimport org.odk.collect.android.utilities.InstancesRepositoryProvider\nimport org.odk.collect.android.utilities.SavepointsRepositoryProvider\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.metadata.InstallIDProvider\nimport org.odk.collect.metadata.PropertyManager\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.keys.ProtectedProjectKeys\nimport org.odk.collect.shared.locks.BooleanChangeLock\nimport org.odk.collect.shared.settings.Settings\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass ProjectResetterTest {\n    private lateinit var projectResetter: ProjectResetter\n    private lateinit var storagePathProvider: StoragePathProvider\n    private lateinit var settingsProvider: SettingsProvider\n    private lateinit var formsRepositoryProvider: FormsRepositoryProvider\n    private lateinit var instancesRepositoryProvider: InstancesRepositoryProvider\n    private lateinit var savepointsRepositoryProvider: SavepointsRepositoryProvider\n    private lateinit var currentProjectId: String\n    private lateinit var anotherProjectId: String\n\n    private val propertyManager = mock<PropertyManager>()\n    private val changeLockProvider = ChangeLockProvider { BooleanChangeLock() }\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesPropertyManager(\n                installIDProvider: InstallIDProvider,\n                settingsProvider: SettingsProvider?\n            ): PropertyManager {\n                return propertyManager\n            }\n\n            override fun providesChangeLockProvider(): ChangeLockProvider {\n                return changeLockProvider\n            }\n        })\n\n        currentProjectId = CollectHelpers.setupDemoProject()\n        anotherProjectId = CollectHelpers.createProject(Project.New(\"Another project\", \"A\", \"#cccccc\"))\n\n        val component = DaggerUtils.getComponent(ApplicationProvider.getApplicationContext<Context>() as Application)\n        projectResetter = component.projectResetter()\n        storagePathProvider = component.storagePathProvider()\n        settingsProvider = component.settingsProvider()\n        formsRepositoryProvider = component.formsRepositoryProvider()\n        instancesRepositoryProvider = component.instancesRepositoryProvider()\n        savepointsRepositoryProvider = component.savepointsRepositoryProvider()\n    }\n\n    @Test\n    fun `Reset settings clears unprotected settings for current project`() {\n        setupTestGeneralSettings(currentProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_PREFERENCES))\n\n        assertThat(\n            getUnprotectedSettings(currentProjectId).getString(ProjectKeys.KEY_USERNAME),\n            `is`(\n                Defaults.unprotected[ProjectKeys.KEY_USERNAME]\n            )\n        )\n        assertThat(\n            getUnprotectedSettings(currentProjectId).getString(ProjectKeys.KEY_PASSWORD),\n            `is`(\n                Defaults.unprotected[ProjectKeys.KEY_PASSWORD]\n            )\n        )\n    }\n\n    @Test\n    fun `Reset settings does not clear unprotected settings for another projects`() {\n        setupTestGeneralSettings(anotherProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_PREFERENCES))\n\n        assertThat(\n            getUnprotectedSettings(anotherProjectId).getString(ProjectKeys.KEY_USERNAME),\n            `is`(\n                \"usernameTest\"\n            )\n        )\n        assertThat(\n            getUnprotectedSettings(anotherProjectId).getString(ProjectKeys.KEY_PASSWORD),\n            `is`(\n                \"passwordTest\"\n            )\n        )\n    }\n\n    @Test\n    fun `Reset settings clears protected settings for current project`() {\n        setupTestAdminSettings(currentProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_PREFERENCES))\n\n        assertThat(\n            getProtectedSettings(currentProjectId).getBoolean(ProtectedProjectKeys.KEY_VIEW_SENT),\n            `is`(\n                Defaults.protected[ProtectedProjectKeys.KEY_VIEW_SENT]\n            )\n        )\n    }\n\n    @Test\n    fun `Reset settings does not clear protected settings for another projects`() {\n        setupTestAdminSettings(anotherProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_PREFERENCES))\n\n        assertThat(\n            getProtectedSettings(anotherProjectId).getBoolean(ProtectedProjectKeys.KEY_VIEW_SENT),\n            `is`(\n                false\n            )\n        )\n    }\n\n    @Test\n    fun `Reset settings clears settings folder for current project`() {\n        setupTestSettingsFolder(currentProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_PREFERENCES))\n\n        assertFolderEmpty(storagePathProvider.getOdkDirPath(StorageSubdirectory.SETTINGS, currentProjectId))\n    }\n\n    @Test\n    fun `Reset settings does not clear settings folder for another projects`() {\n        setupTestSettingsFolder(anotherProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_PREFERENCES))\n\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.SETTINGS, anotherProjectId), \"settings.png\").exists())\n    }\n\n    @Test\n    fun `Reset settings reloads property manager`() {\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_PREFERENCES))\n\n        verify(propertyManager).reload()\n    }\n\n    @Test\n    fun `Reset forms clears forms for current project`() {\n        saveTestFormFiles(currentProjectId)\n        setupTestFormsDatabase(currentProjectId)\n        createTestItemsetsDatabaseFile(currentProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_FORMS))\n\n        assertEquals(0, formsRepositoryProvider.create(currentProjectId).all.size)\n        assertFolderEmpty(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, currentProjectId))\n        assertFalse(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.METADATA, currentProjectId) + \"/itemsets.db\").exists())\n    }\n\n    @Test\n    fun `Reset forms does not clear forms for another projects`() {\n        saveTestFormFiles(anotherProjectId)\n        setupTestFormsDatabase(anotherProjectId)\n        createTestItemsetsDatabaseFile(anotherProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_FORMS))\n\n        assertEquals(1, formsRepositoryProvider.create(anotherProjectId).all.size)\n        assertTestFormFiles(anotherProjectId)\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.METADATA, anotherProjectId) + \"/itemsets.db\").exists())\n    }\n\n    @Test\n    fun `Reset instances does not clear instances and savepoints if the instances database is locked`() {\n        saveTestInstanceFiles(currentProjectId)\n        setupTestInstancesDatabase(currentProjectId)\n        setupTestSavepointsDatabase(currentProjectId)\n\n        (changeLockProvider.create(currentProjectId).instancesLock as BooleanChangeLock).lock(\"blah\")\n        val failedResetActions = projectResetter.reset(listOf(ProjectResetter.ResetAction.RESET_INSTANCES))\n        assertEquals(1, failedResetActions.size)\n\n        assertEquals(1, instancesRepositoryProvider.create(currentProjectId).all.size)\n        assertTestInstanceFiles(currentProjectId)\n        assertEquals(1, savepointsRepositoryProvider.create(currentProjectId).getAll().size)\n    }\n\n    @Test\n    fun `Reset instances clears instances and savepoints for current project`() {\n        saveTestInstanceFiles(currentProjectId)\n        setupTestInstancesDatabase(currentProjectId)\n        setupTestSavepointsDatabase(anotherProjectId)\n        val instancesRepository = instancesRepositoryProvider.create(currentProjectId)\n        val instance = instancesRepository.all[0]\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_INSTANCES))\n\n        assertEquals(0, instancesRepository.all.size)\n        assertEquals(false, File(instance.instanceFilePath).parentFile.exists())\n        assertEquals(1, savepointsRepositoryProvider.create(anotherProjectId).getAll().size)\n    }\n\n    @Test\n    fun `Reset instances does not clear instances and savepoints for another projects`() {\n        saveTestInstanceFiles(anotherProjectId)\n        setupTestInstancesDatabase(anotherProjectId)\n        setupTestSavepointsDatabase(anotherProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_INSTANCES))\n\n        assertEquals(1, instancesRepositoryProvider.create(anotherProjectId).all.size)\n        assertTestInstanceFiles(anotherProjectId)\n        assertEquals(1, savepointsRepositoryProvider.create(anotherProjectId).getAll().size)\n    }\n\n    @Test\n    fun `Reset layers clears layers for current project`() {\n        saveTestLayerFiles(currentProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_LAYERS))\n\n        assertFolderEmpty(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS, currentProjectId))\n    }\n\n    @Test\n    fun `Reset layers does not clear layers for another projects`() {\n        saveTestLayerFiles(anotherProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_LAYERS))\n\n        assertTestLayerFiles(anotherProjectId)\n    }\n\n    @Test\n    fun `Reset cache clears cache and savepoints db for current project`() {\n        setupTestSavepointsDatabase(currentProjectId)\n        saveTestCacheFiles(currentProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_CACHE))\n\n        assertEquals(0, savepointsRepositoryProvider.create(currentProjectId).getAll().size)\n        assertFolderEmpty(storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, currentProjectId))\n    }\n\n    @Test\n    fun `Reset cache does not clear cache and savepoints db for another projects`() {\n        setupTestSavepointsDatabase(anotherProjectId)\n        saveTestCacheFiles(anotherProjectId)\n\n        resetAppState(listOf(ProjectResetter.ResetAction.RESET_CACHE))\n\n        assertEquals(1, savepointsRepositoryProvider.create(anotherProjectId).getAll().size)\n        assertTestCacheFiles(anotherProjectId)\n    }\n\n    private fun resetAppState(resetActions: List<Int>) {\n        val failedResetActions = projectResetter.reset(resetActions)\n        assertEquals(0, failedResetActions.size)\n    }\n\n    private fun setupTestGeneralSettings(uuid: String) {\n        getUnprotectedSettings(uuid).save(ProjectKeys.KEY_USERNAME, \"usernameTest\")\n        getUnprotectedSettings(uuid).save(ProjectKeys.KEY_PASSWORD, \"passwordTest\")\n    }\n\n    private fun setupTestAdminSettings(uuid: String) {\n        getProtectedSettings(uuid).save(ProtectedProjectKeys.KEY_VIEW_SENT, false)\n    }\n\n    private fun setupTestSettingsFolder(uuid: String) {\n        assertTrue(\n            File(storagePathProvider.getOdkDirPath(StorageSubdirectory.SETTINGS, uuid)).exists() || File(\n                storagePathProvider.getOdkDirPath(StorageSubdirectory.SETTINGS, uuid)\n            ).mkdir()\n        )\n\n        File(storagePathProvider.getOdkDirPath(StorageSubdirectory.SETTINGS, uuid), \"settings.png\").createNewFile()\n    }\n\n    private fun setupTestFormsDatabase(uuid: String) {\n        FormsRepositoryProvider(ApplicationProvider.getApplicationContext()).create(uuid).save(\n            Form.Builder()\n                .formId(\"jrFormId\")\n                .displayName(\"displayName\")\n                .formFilePath(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testFile1.xml\")\n                .build()\n        )\n        assertEquals(1, formsRepositoryProvider.create(uuid).all.size)\n    }\n\n    private fun setupTestInstancesDatabase(uuid: String) {\n        InstancesRepositoryProvider(ApplicationProvider.getApplicationContext()).create(uuid).save(\n            Instance.Builder()\n                .instanceFilePath(\"testDir1/testFile1\")\n                .submissionUri(\"submissionUri\")\n                .displayName(\"formName\")\n                .formId(\"jrformid\")\n                .formVersion(\"jrversion\")\n                .build()\n        )\n        assertEquals(1, instancesRepositoryProvider.create(uuid).all.size)\n    }\n\n    private fun setupTestSavepointsDatabase(uuid: String) {\n        SavepointsRepositoryProvider(ApplicationProvider.getApplicationContext(), storagePathProvider).create(uuid).save(\n            Savepoint(1, 1, \"blah\", \"blah\")\n        )\n        assertEquals(1, savepointsRepositoryProvider.create(uuid).getAll().size)\n    }\n\n    private fun createTestItemsetsDatabaseFile(uuid: String) {\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.METADATA, uuid) + \"/itemsets.db\").createNewFile())\n    }\n\n    private fun saveTestFormFiles(uuid: String) {\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testFile1.xml\").createNewFile())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testFile2.xml\").createNewFile())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testFile3.xml\").createNewFile())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testDir1/testFile1-media\").mkdirs())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testDir2/testFile2-media\").mkdirs())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testDir3/testFile3-media/testFile.csv\").mkdirs())\n    }\n\n    private fun assertTestFormFiles(uuid: String) {\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testFile1.xml\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testFile2.xml\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testFile3.xml\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testDir1/testFile1-media\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testDir2/testFile2-media\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS, uuid) + \"/testDir3/testFile3-media/testFile.csv\").exists())\n    }\n\n    private fun saveTestInstanceFiles(uuid: String) {\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, uuid) + \"/testDir1/testFile1.xml\").mkdirs())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, uuid) + \"/testDir2/testFile2.xml\").mkdirs())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, uuid) + \"/testDir3\").mkdirs())\n    }\n\n    private fun assertTestInstanceFiles(uuid: String) {\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, uuid) + \"/testDir1/testFile1.xml\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, uuid) + \"/testDir2/testFile2.xml\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES, uuid) + \"/testDir3\").exists())\n    }\n\n    private fun saveTestLayerFiles(uuid: String) {\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS, uuid) + \"/testFile1\").createNewFile())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS, uuid) + \"/testFile2\").createNewFile())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS, uuid) + \"/testFile3\").createNewFile())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS, uuid) + \"/testFile4\").createNewFile())\n    }\n\n    private fun assertTestLayerFiles(uuid: String) {\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS, uuid) + \"/testFile1\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS, uuid) + \"/testFile2\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS, uuid) + \"/testFile3\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS, uuid) + \"/testFile4\").exists())\n    }\n\n    private fun saveTestCacheFiles(uuid: String) {\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, uuid) + \"/testFile1\").createNewFile())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, uuid) + \"/testFile2\").createNewFile())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, uuid) + \"/testFile3\").createNewFile())\n    }\n\n    private fun assertTestCacheFiles(uuid: String) {\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, uuid) + \"/testFile1\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, uuid) + \"/testFile2\").exists())\n        assertTrue(File(storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE, uuid) + \"/testFile3\").exists())\n    }\n\n    private fun assertFolderEmpty(folder: String) {\n        assertTrue(File(folder).isDirectory)\n        assertTrue(File(folder).list().isEmpty())\n    }\n\n    fun getUnprotectedSettings(uuid: String): Settings {\n        return settingsProvider.getUnprotectedSettings(uuid)\n    }\n\n    fun getProtectedSettings(uuid: String): Settings {\n        return settingsProvider.getProtectedSettings(uuid)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/ProjectSettingsDialogTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport androidx.core.view.children\nimport androidx.core.view.size\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.viewmodel.initializer\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.pressBack\nimport androidx.test.espresso.intent.Intents\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport com.google.gson.Gson\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.hamcrest.Matchers.notNullValue\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.activities.AboutActivity\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.mainmenu.CurrentProjectViewModel\nimport org.odk.collect.android.preferences.screens.ProjectPreferencesActivity\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.shared.strings.UUIDGenerator\nimport org.odk.collect.testshared.FakeBarcodeScannerViewFactory\nimport org.odk.collect.testshared.RobolectricHelpers\n\n@RunWith(AndroidJUnit4::class)\nclass ProjectSettingsDialogTest {\n    private val currentProject = Project.Saved(\"x\", \"Project X\", \"X\", \"#ffffff\")\n    private val currentProjectLiveData = MutableLiveData(currentProject)\n    private val currentProjectViewModel: CurrentProjectViewModel = mock {\n        on { currentProject } doReturn currentProjectLiveData\n    }\n\n    private val projectsRepository = InMemProjectsRepository(UUIDGenerator())\n\n    private val viewModelFactory = viewModelFactory {\n        initializer {\n            currentProjectViewModel\n        }\n    }\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule(\n        defaultFactory = FragmentFactoryBuilder()\n            .forClass(ProjectSettingsDialog::class) { ProjectSettingsDialog(viewModelFactory) }\n            .build()\n    )\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesProjectsRepository(\n                uuidGenerator: UUIDGenerator?,\n                gson: Gson?,\n                settingsProvider: SettingsProvider?\n            ): ProjectsRepository {\n                return projectsRepository\n            }\n\n            override fun providesBarcodeScannerViewFactory(settingsProvider: SettingsProvider): BarcodeScannerViewContainer.Factory {\n                return FakeBarcodeScannerViewFactory()\n            }\n        })\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on the 'X' button`() {\n        val scenario = launcherRule.launch(ProjectSettingsDialog::class.java)\n\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            it.binding.closeIcon.performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(nullValue()))\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on a device back button`() {\n        val scenario = launcherRule.launch(ProjectSettingsDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            onView(isRoot()).perform(pressBack())\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(nullValue()))\n        }\n    }\n\n    @Test\n    fun `Project settings should be started after clicking on the 'Settings' button`() {\n        val scenario = launcherRule.launch(ProjectSettingsDialog::class.java)\n        scenario.onFragment {\n            Intents.init()\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            it.binding.generalSettingsButton.performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(nullValue()))\n            assertThat(\n                Intents.getIntents()[0],\n                hasComponent(ProjectPreferencesActivity::class.java.name)\n            )\n            Intents.release()\n        }\n    }\n\n    @Test\n    fun `About section should be started after clicking on the 'About' button`() {\n        val scenario = launcherRule.launch(ProjectSettingsDialog::class.java)\n        scenario.onFragment {\n            Intents.init()\n            assertThat(it.dialog!!.isShowing, `is`(true))\n            it.binding.aboutButton.performClick()\n            RobolectricHelpers.runLooper()\n            assertThat(it.dialog, `is`(nullValue()))\n            assertThat(Intents.getIntents()[0], hasComponent(AboutActivity::class.java.name))\n            Intents.release()\n        }\n    }\n\n    @Test\n    fun `QrCodeProjectCreatorDialog should be displayed after clicking on the 'Add project' button`() {\n        val scenario = launcherRule.launch(ProjectSettingsDialog::class.java)\n        scenario.onFragment {\n            it.binding.addProjectButton.performClick()\n            assertThat(\n                it.activity!!.supportFragmentManager.findFragmentByTag(\n                    QrCodeProjectCreatorDialog::class.java.name\n                ),\n                `is`(notNullValue())\n            )\n        }\n    }\n\n    @Test\n    fun `currentProjectViewModel should be notified when project switched`() {\n        val projectY = projectsRepository.save(Project.New(\"Project Y\", \"Y\", \"#ffffff\"))\n\n        val scenario = launcherRule.launch(ProjectSettingsDialog::class.java)\n        scenario.onFragment {\n            it.binding.projectList.children.iterator().asSequence().first().performClick()\n            verify(currentProjectViewModel).setCurrentProject(projectY)\n        }\n    }\n\n    @Test\n    fun `project list should not be duplicated on current project update`() {\n        projectsRepository.save(Project.New(\"Project Y\", \"Y\", \"#ffffff\"))\n\n        val scenario = launcherRule.launch(ProjectSettingsDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.binding.projectList.size, equalTo(1))\n            currentProjectLiveData.value = currentProject\n            assertThat(it.binding.projectList.size, equalTo(1))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/ProjectsDataServiceTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Test\nimport org.mockito.Mockito.verify\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.application.initialization.AnalyticsInitializer\nimport org.odk.collect.androidshared.data.AppState\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.MetaKeys\n\nclass ProjectsDataServiceTest {\n\n    private val projectsRepository = InMemProjectsRepository()\n    private val settingsProvider = InMemSettingsProvider()\n    private val metaSettings = settingsProvider.getMetaSettings()\n    private val analyticsInitializer = mock<AnalyticsInitializer>()\n    private val projectsDataService = ProjectsDataService(\n        AppState(),\n        settingsProvider,\n        projectsRepository,\n        analyticsInitializer,\n        mock()\n    )\n\n    @Test\n    fun `A project should be returned after calling requireCurrentProject if there is a project for given id`() {\n        val project = projectsRepository.save(Project.New(\"ProjectX\", \"X\", \"#00FF00\"))\n        metaSettings.save(MetaKeys.CURRENT_PROJECT_ID, project.uuid)\n\n        assertThat(projectsDataService.requireCurrentProject(), `is`(project))\n    }\n\n    @Test\n    fun `save() on meta settings should be called after current project is set`() {\n        projectsDataService.setCurrentProject(\"123e4567\")\n        assertThat(metaSettings.getString(MetaKeys.CURRENT_PROJECT_ID), equalTo(\"123e4567\"))\n    }\n\n    @Test(expected = IllegalStateException::class)\n    fun `requireCurrentProject throws IllegalStateException when there is no current project`() {\n        projectsDataService.requireCurrentProject()\n    }\n\n    @Test\n    fun `requireCurrentProject returns first project when there is no current project but there are projects`() {\n        val firstProject = projectsRepository.save(Project.New(\"ProjectX\", \"X\", \"#00FF00\"))\n        projectsRepository.save(Project.New(\"ProjectY\", \"Y\", \"#00FF00\"))\n        assertThat(projectsDataService.requireCurrentProject(), `is`(firstProject))\n    }\n\n    @Test(expected = IllegalStateException::class)\n    fun `requireCurrentProject throws IllegalStateException when current project does not exist`() {\n        projectsDataService.setCurrentProject(\"123e4567\")\n        projectsDataService.requireCurrentProject()\n    }\n\n    @Test\n    fun `setCurrentProject() re-initializes analytics`() {\n        projectsRepository.save(Project.Saved(\"456\", \"Project Y\", \"Y\", \"#ffffff\"))\n        projectsDataService.setCurrentProject(\"456\")\n        verify(analyticsInitializer).initialize()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/QrCodeProjectCreatorDialogTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport android.Manifest\nimport android.app.Application\nimport android.content.Context\nimport androidx.lifecycle.Lifecycle\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.pressBack\nimport androidx.test.espresso.action.ViewActions.scrollTo\nimport androidx.test.espresso.intent.Intents\nimport androidx.test.espresso.intent.matcher.IntentMatchers\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isRoot\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.work.WorkManager\nimport com.google.android.material.appbar.MaterialToolbar\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.`is`\nimport org.hamcrest.Matchers.notNullValue\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.Mockito.verifyNoInteractions\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.R\nimport org.odk.collect.android.fakes.FakePermissionsProvider\nimport org.odk.collect.android.injection.DaggerUtils\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.mainmenu.MainMenuActivity\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.permissions.PermissionsProvider\nimport org.odk.collect.projects.ProjectCreator\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.testshared.FakeBarcodeScannerViewFactory\nimport org.odk.collect.testshared.FakeScheduler\nimport org.robolectric.shadows.ShadowToast\n\n@RunWith(AndroidJUnit4::class)\nclass QrCodeProjectCreatorDialogTest {\n\n    private val permissionsProvider = FakePermissionsProvider()\n    private val barcodeScannerViewFactory = FakeBarcodeScannerViewFactory()\n    private val scheduler = FakeScheduler()\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        permissionsProvider.setPermissionGranted(true)\n\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesBarcodeScannerViewFactory(settingsProvider: SettingsProvider): BarcodeScannerViewContainer.Factory {\n                return barcodeScannerViewFactory\n            }\n\n            override fun providesPermissionsProvider(permissionsChecker: PermissionsChecker?): PermissionsProvider {\n                return permissionsProvider\n            }\n\n            override fun providesScheduler(workManager: WorkManager?, application: Application): Scheduler {\n                return scheduler\n            }\n        })\n    }\n\n    @Test\n    fun `If camera permission is not granted the dialog should not be dismissed`() {\n        permissionsProvider.setPermissionGranted(false)\n        val scenario = launcherRule.launch(QrCodeProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.isVisible, `is`(true))\n        }\n    }\n\n    // https://github.com/getodk/collect/issues/5266\n    @Test\n    fun `requestCameraPermission() should be called in onStart() to make sure it is called after returning to the dialog`() {\n        val scenario = launcherRule.launch(\n            fragmentClass = QrCodeProjectCreatorDialog::class.java,\n            initialState = Lifecycle.State.CREATED\n        )\n\n        assertThat(permissionsProvider.requestedPermissions, equalTo(emptyList()))\n\n        scenario.moveToState(Lifecycle.State.STARTED)\n\n        assertThat(\n            permissionsProvider.requestedPermissions,\n            equalTo(listOf(Manifest.permission.CAMERA))\n        )\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on the 'Cancel' button`() {\n        val scenario = launcherRule.launch(QrCodeProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.isVisible, `is`(true))\n            onView(withText(org.odk.collect.strings.R.string.cancel)).inRoot(isDialog())\n                .perform(click())\n            assertThat(it.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `The dialog should be dismissed after clicking on a device back button`() {\n        val scenario = launcherRule.launch(QrCodeProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.isVisible, `is`(true))\n            onView(isRoot()).perform(pressBack())\n            assertThat(it.isVisible, `is`(false))\n        }\n    }\n\n    @Test\n    fun `The dialog should have the option to import settings from file`() {\n        val scenario = launcherRule.launch(QrCodeProjectCreatorDialog::class.java)\n        scenario.onFragment { fragment ->\n            val toolbar = fragment.requireView()\n                .findViewById<MaterialToolbar>(org.odk.collect.androidshared.R.id.toolbar)\n            val importMenuItem = toolbar.menu.findItem(R.id.menu_item_scan_sd_card)\n\n            assertThat(importMenuItem, `is`(notNullValue()))\n        }\n    }\n\n    @Test\n    fun `The ManualProjectCreatorDialog should be displayed after switching to the manual mode`() {\n        val scenario = launcherRule.launch(QrCodeProjectCreatorDialog::class.java)\n        scenario.onFragment {\n            onView(withText(org.odk.collect.strings.R.string.configure_manually)).inRoot(isDialog())\n                .perform(scrollTo(), click())\n            assertThat(\n                it.activity!!.supportFragmentManager.findFragmentByTag(\n                    ManualProjectCreatorDialog::class.java.name\n                ),\n                `is`(notNullValue())\n            )\n        }\n    }\n\n    @Test\n    fun `Successful project creation goes to main menu`() {\n        Intents.init()\n        val scenario = launcherRule.launch(QrCodeProjectCreatorDialog::class.java)\n\n        barcodeScannerViewFactory.scan(\n            \"{\\n\" +\n                \"  \\\"general\\\": {\\n\" +\n                \"  },\\n\" +\n                \"  \\\"admin\\\": {\\n\" +\n                \"  }\\n\" +\n                \"}\"\n        )\n\n        scenario.onFragment {\n            Intents.intended(IntentMatchers.hasComponent(MainMenuActivity::class.java.name))\n            Intents.release()\n        }\n    }\n\n    @Test\n    fun `When a duplicate project is detected resume scanning after dismissing the dialog`() {\n        val scenario = launcherRule.launch(QrCodeProjectCreatorDialog::class.java)\n\n        val projectJson =\n            \"\"\"\n            {\n                \"general\": {\n                    \"server_url\": \"http://john.com\",\n                    \"username\": \"john\"\n                },\n                \"admin\": {}\n            }\n            \"\"\".trimIndent()\n\n        DaggerUtils\n            .getComponent(ApplicationProvider.getApplicationContext<Application>())\n            .projectCreator()\n            .createNewProject(projectJson, false)\n\n        barcodeScannerViewFactory.scan(projectJson)\n\n        scenario.onFragment {\n            assertThat(\n                it.childFragmentManager.findFragmentByTag(\n                    DuplicateProjectConfirmationDialog::class.java.name\n                ),\n                `is`(notNullValue())\n            )\n        }\n\n        onView(isRoot()).perform(pressBack())\n\n        scheduler.runForeground()\n        assertThat(barcodeScannerViewFactory.isScanning, equalTo(true))\n    }\n\n    @Test\n    fun `When QR code is invalid a toast should be displayed and scanning continues`() {\n        val projectCreator = mock<ProjectCreator>()\n        launcherRule.launch(QrCodeProjectCreatorDialog::class.java)\n\n        barcodeScannerViewFactory.scan(\"{*}\")\n        assertThat(\n            ShadowToast.getTextOfLatestToast(),\n            `is`(\n                ApplicationProvider.getApplicationContext<Context>()\n                    .getString(org.odk.collect.strings.R.string.invalid_qrcode)\n            )\n        )\n        verifyNoInteractions(projectCreator)\n\n        scheduler.runForeground()\n        assertThat(barcodeScannerViewFactory.isScanning, equalTo(true))\n    }\n\n    @Test\n    fun `When QR code contains GD protocol a toast should be displayed and scanning continues`() {\n        val projectCreator = mock<ProjectCreator>()\n        launcherRule.launch(QrCodeProjectCreatorDialog::class.java)\n\n        barcodeScannerViewFactory.scan(\n            \"{\\n\" +\n                \"  \\\"general\\\": {\\n\" +\n                \"       \\\"protocol\\\" : \\\"google_sheets\\\"\" +\n                \"  },\\n\" +\n                \"  \\\"admin\\\": {\\n\" +\n                \"  }\\n\" +\n                \"}\"\n        )\n        assertThat(\n            ShadowToast.getTextOfLatestToast(),\n            `is`(\n                ApplicationProvider.getApplicationContext<Context>()\n                    .getString(org.odk.collect.strings.R.string.settings_with_gd_protocol)\n            )\n        )\n        verifyNoInteractions(projectCreator)\n\n        scheduler.runForeground()\n        assertThat(barcodeScannerViewFactory.isScanning, equalTo(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/projects/SettingsConnectionMatcherImplTest.kt",
    "content": "package org.odk.collect.android.projects\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.preferences.Defaults\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\n\n@RunWith(AndroidJUnit4::class)\nclass SettingsConnectionMatcherImplTest {\n    private val inMemProjectsRepository = InMemProjectsRepository()\n    private val inMemSettingsProvider = InMemSettingsProvider()\n    private val settingsConnectionMatcher =\n        SettingsConnectionMatcherImpl(inMemProjectsRepository, inMemSettingsProvider)\n\n    @Test\n    fun `returns null when no projects exist`() {\n        val jsonSettings = getServerSettingsJson(\"https://example.com\")\n\n        assertThat(\n            settingsConnectionMatcher.getProjectWithMatchingConnection(jsonSettings),\n            `is`(nullValue())\n        )\n    }\n\n    @Test\n    fun `returns a matching project uuid when urls match`() {\n        createServerProject(\"a uuid\", \"https://example.com\", \"\")\n        val jsonSettings = getServerSettingsJson(\"https://example.com\")\n\n        assertThat(\n            settingsConnectionMatcher.getProjectWithMatchingConnection(jsonSettings),\n            `is`(\"a uuid\")\n        )\n    }\n\n    @Test\n    fun `returns a matching project uuid when a default project exists and a user tries to add another default project`() {\n        val defaultUrl = Defaults.unprotected[ProjectKeys.KEY_SERVER_URL] as String\n        createServerProject(\"a uuid\", defaultUrl, \"\")\n        val jsonSettings = getDefaultServerSettingsJson()\n\n        assertThat(\n            settingsConnectionMatcher.getProjectWithMatchingConnection(jsonSettings),\n            `is`(\"a uuid\")\n        )\n    }\n\n    @Test\n    fun `returns null when urls match and usernames don't match`() {\n        createServerProject(\"a uuid\", \"https://example.com\", \"\")\n        val jsonSettings = getServerSettingsJson(\"https://example.com\", \"foo\")\n\n        assertThat(\n            settingsConnectionMatcher.getProjectWithMatchingConnection(jsonSettings),\n            `is`(nullValue())\n        )\n    }\n\n    @Test\n    fun `returns a matching project uuid when urls and usernames match`() {\n        createServerProject(\"a uuid\", \"https://example.com\", \"foo\")\n        val jsonSettings = getServerSettingsJson(\"https://example.com\", \"foo\")\n\n        assertThat(\n            settingsConnectionMatcher.getProjectWithMatchingConnection(jsonSettings),\n            `is`(\"a uuid\")\n        )\n    }\n\n    @Test\n    fun `returns a matching project uuid when urls and usernames match and there are other settings that don't match`() {\n        createServerProject(\"a uuid\", \"https://example.com\", \"foo\")\n        val jsonSettings =\n            \"{ \\\"general\\\": { \\\"server_url\\\": \\\"https://example.com\\\", \\\"username\\\": \\\"foo\\\", \\\"password\\\": \\\"bar\\\" } }\"\n\n        assertThat(\n            settingsConnectionMatcher.getProjectWithMatchingConnection(jsonSettings),\n            `is`(\"a uuid\")\n        )\n    }\n\n    @Test\n    fun `returns a matching project uuid when there are multiple projects`() {\n        createServerProject(\"a uuid\", \"https://example.com\", \"foo\")\n        createServerProject(\"another uuid\", \"https://example2.com\", \"foo2\")\n        val jsonSettings = getServerSettingsJson(\"https://example2.com\", \"foo2\")\n\n        assertThat(\n            settingsConnectionMatcher.getProjectWithMatchingConnection(jsonSettings),\n            `is`(\"another uuid\")\n        )\n    }\n\n    @Test\n    fun `returns uuid of first matching project when there are multiple matching projects`() {\n        createServerProject(\"a uuid\", \"https://example.com\", \"foo\")\n        createServerProject(\"uuid 3\", \"https://foo.org\", \"foo\")\n        createServerProject(\"uuid 4\", \"https://foo.org\", \"foo\")\n\n        val jsonSettings = getServerSettingsJson(\"https://foo.org\", \"foo\")\n\n        assertThat(\n            settingsConnectionMatcher.getProjectWithMatchingConnection(jsonSettings),\n            `is`(\"uuid 3\")\n        )\n    }\n\n    private fun createServerProject(projectId: String, url: String, username: String) {\n        inMemProjectsRepository.save(Project.Saved(projectId, \"no-op\", \"n\", \"#ffffff\"))\n\n        val generalSettings = inMemSettingsProvider.getUnprotectedSettings(projectId)\n        generalSettings.save(ProjectKeys.KEY_SERVER_URL, url)\n        generalSettings.save(ProjectKeys.KEY_USERNAME, username)\n    }\n\n    private fun getDefaultServerSettingsJson(): String {\n        return \"{\\\"general\\\":{},\\\"admin\\\":{},\\\"project\\\":{\\\"name\\\":\\\"Demo project\\\",\\\"icon\\\":\\\"D\\\",\\\"color\\\":\\\"#3e9fcc\\\"}}\"\n    }\n\n    private fun getServerSettingsJson(url: String): String {\n        return \"{ \\\"general\\\": { \\\"server_url\\\": \\\"$url\\\" } }\"\n    }\n\n    private fun getServerSettingsJson(url: String, username: String): String {\n        return \"{ \\\"general\\\": { \\\"server_url\\\": \\\"$url\\\", \\\"username\\\": \\\"$username\\\" } }\"\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/savepoints/SavepointUseCasesTest.kt",
    "content": "package org.odk.collect.android.savepoints\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.external.FormsContract\nimport org.odk.collect.android.external.InstancesContract\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.formstest.FormUtils\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.formstest.InMemInstancesRepository\nimport org.odk.collect.formstest.InMemSavepointsRepository\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass SavepointUseCasesTest {\n    private var formV1: Form\n    private var formV2: Form\n\n    private var instance1: Instance\n    private var instance2: Instance\n\n    private val formsRepository = InMemFormsRepository().apply {\n        formV1 = save(\n            FormUtils.buildForm(\n                \"1\",\n                \"1\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n\n        Thread.sleep(100) // make sure the two forms have different creation dates\n\n        formV2 = save(\n            FormUtils.buildForm(\n                \"1\",\n                \"2\",\n                TempFiles.createTempDir().absolutePath\n            ).build()\n        )\n    }\n    private val instancesRepository = InMemInstancesRepository().apply {\n        instance1 = save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"2\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        Thread.sleep(100) // make sure the two instances have different creation dates\n\n        instance2 = save(\n            Instance.Builder()\n                .formId(\"1\")\n                .formVersion(\"2\")\n                .instanceFilePath(TempFiles.createTempFile(TempFiles.createTempDir()).absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n    }\n    private val savepointsRepository = InMemSavepointsRepository()\n\n    @Test\n    fun `getSavepoint called with the old form version uri returns null if only the new form version has a savepoint`() {\n        val savepointFile = createSavepointFile()\n        val savepoint = Savepoint(formV2.dbId, null, savepointFile.absolutePath, \"\")\n        savepointsRepository.save(savepoint)\n\n        if (savepointsRepository.getAll().size > 1) {\n            throw(Error(\"WTF?\"))\n        }\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                FormsContract.getUri(\"1\", formV1.dbId),\n                FormsContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun `getSavepoint called with the old form version uri returns savepoint if it has one`() {\n        val savepointFile = createSavepointFile()\n        val savepoint = Savepoint(formV1.dbId, null, savepointFile.absolutePath, \"\")\n        savepointsRepository.save(savepoint)\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                FormsContract.getUri(\"1\", formV1.dbId),\n                FormsContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(savepoint)\n        )\n    }\n\n    @Test\n    fun `getSavepoint called with the old form version uri returns savepoint that belongs to the old form if both form versions have ones`() {\n        val savepointFile1 = createSavepointFile()\n        val savepoint1 = Savepoint(formV1.dbId, null, savepointFile1.absolutePath, \"\")\n        savepointsRepository.save(savepoint1)\n\n        val savepointFile2 = createSavepointFile()\n        val savepoint2 = Savepoint(formV2.dbId, null, savepointFile2.absolutePath, \"\")\n        savepointsRepository.save(savepoint2)\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                FormsContract.getUri(\"1\", formV1.dbId),\n                FormsContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(savepoint1)\n        )\n    }\n\n    @Test\n    fun `getSavepoint called with the new form version uri returns savepoint if it has one`() {\n        val savepointFile = createSavepointFile()\n        val savepoint = Savepoint(formV2.dbId, null, savepointFile.absolutePath, \"\")\n        savepointsRepository.save(savepoint)\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                FormsContract.getUri(\"1\", formV2.dbId),\n                FormsContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(savepoint)\n        )\n    }\n\n    @Test\n    fun `getSavepoint called with the new form version uri returns savepoint that belongs to the old form if only the old form has one`() {\n        val savepointFile = createSavepointFile()\n        val savepoint = Savepoint(formV1.dbId, null, savepointFile.absolutePath, \"\")\n        savepointsRepository.save(savepoint)\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                FormsContract.getUri(\"1\", formV2.dbId),\n                FormsContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(savepoint)\n        )\n    }\n\n    @Test\n    fun `getSavepoint called with the new form version uri returns savepoint that belongs to the new form if both form versions have ones`() {\n        val savepointFile1 = createSavepointFile()\n        val savepoint1 = Savepoint(formV1.dbId, null, savepointFile1.absolutePath, \"\")\n        savepointsRepository.save(savepoint1)\n\n        val savepointFile2 = createSavepointFile()\n        val savepoint2 = Savepoint(formV2.dbId, null, savepointFile2.absolutePath, \"\")\n        savepointsRepository.save(savepoint2)\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                FormsContract.getUri(\"1\", formV2.dbId),\n                FormsContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(savepoint2)\n        )\n    }\n\n    @Test\n    fun `getSavepoint returns null if savepoint exists in the database but the file does not`() {\n        val savepointFile = createSavepointFile()\n        val savepoint = Savepoint(formV2.dbId, null, savepointFile.absolutePath, \"\")\n        savepointsRepository.save(savepoint)\n        savepointFile.delete()\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                FormsContract.getUri(\"1\", formV2.dbId),\n                FormsContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun `getSavepoint called for a saved form uri returns null if only its blank form has a savepoint`() {\n        val savepointFile = createSavepointFile()\n        val savepoint = Savepoint(formV2.dbId, null, savepointFile.absolutePath, instance1.instanceFilePath)\n        savepointsRepository.save(savepoint)\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                InstancesContract.getUri(\"1\", instance1.dbId),\n                InstancesContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun `getSavepoint called for a saved form uri returns savepoint if it has one`() {\n        val savepointFile = createSavepointFile()\n        val savepoint = Savepoint(formV2.dbId, instance1.dbId, savepointFile.absolutePath, instance1.instanceFilePath)\n        savepointsRepository.save(savepoint)\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                InstancesContract.getUri(\"1\", instance1.dbId),\n                InstancesContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(savepoint)\n        )\n    }\n\n    @Test\n    fun `getSavepoint called for a saved form uri returns null if savepoint exists in the database but the file does not`() {\n        val savepointFile = createSavepointFile()\n        val savepoint = Savepoint(formV2.dbId, instance1.dbId, savepointFile.absolutePath, instance1.instanceFilePath)\n        savepointsRepository.save(savepoint)\n        savepointFile.delete()\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                InstancesContract.getUri(\"1\", instance1.dbId),\n                InstancesContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun `getSavepoint called for a saved form uri returns null if it has one but the instance file has been modified later`() {\n        val savepointFile = createSavepointFile()\n        val savepoint = Savepoint(formV2.dbId, instance1.dbId, savepointFile.absolutePath, instance1.instanceFilePath)\n        savepointsRepository.save(savepoint)\n\n        instancesRepository.save(instance1)\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                InstancesContract.getUri(\"1\", instance1.dbId),\n                InstancesContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun `getSavepoint called for a saved form uri returns savepoint that belongs to that saved form if there are more savepoints in the database created for saved forms`() {\n        val savepointFile1 = createSavepointFile()\n        val savepoint1 = Savepoint(formV2.dbId, instance1.dbId, savepointFile1.absolutePath, instance1.instanceFilePath)\n        savepointsRepository.save(savepoint1)\n\n        val savepointFile2 = createSavepointFile()\n        val savepoint2 = Savepoint(formV2.dbId, instance2.dbId, savepointFile2.absolutePath, instance2.instanceFilePath)\n        savepointsRepository.save(savepoint2)\n\n        assertThat(\n            SavepointUseCases.getSavepoint(\n                InstancesContract.getUri(\"1\", instance1.dbId),\n                InstancesContract.CONTENT_ITEM_TYPE,\n                formsRepository,\n                instancesRepository,\n                savepointsRepository\n            ),\n            equalTo(savepoint1)\n        )\n    }\n\n    /**\n     * Tests seem to run fast enough so that sometimes two files created in succession end up with\n     * the same creation date. This isn't ideal as we depend on distinguishing the older files.\n     */\n    private fun createSavepointFile(): File {\n        Thread.sleep(100)\n        val savepointFile = TempFiles.createTempFile()\n        Thread.sleep(100)\n        return savepointFile\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/storage/StoragePathProviderTest.kt",
    "content": "package org.odk.collect.android.storage\n\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport org.hamcrest.CoreMatchers.`is`\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport org.mockito.Mockito.mock\nimport org.mockito.Mockito.`when`\nimport org.odk.collect.android.projects.ProjectsDataService\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\nclass StoragePathProviderTest {\n    private val root = TempFiles.createTempDir()\n    private var projectsRepository = mock(ProjectsRepository::class.java)\n    private lateinit var storagePathProvider: StoragePathProvider\n\n    @Before\n    fun setup() {\n        val projectsDataService = mock(ProjectsDataService::class.java)\n        `when`(projectsDataService.requireCurrentProject()).thenReturn(\n            Project.Saved(\n                \"123\",\n                \"Project\",\n                \"D\",\n                \"#ffffff\"\n            )\n        )\n        `when`(projectsRepository.get(\"123\")).thenReturn(\n            Project.Saved(\n                \"123\",\n                \"Project\",\n                \"D\",\n                \"#ffffff\"\n            )\n        )\n        storagePathProvider = StoragePathProvider(projectsDataService, projectsRepository, root.absolutePath)\n    }\n\n    @After\n    fun teardown() {\n        root.delete()\n    }\n\n    @Test\n    fun storageRootDirPath_returnsRoot() {\n        assertThat(storagePathProvider.odkRootDirPath, `is`(root.absolutePath))\n    }\n\n    @Test\n    fun projectRootDirPath_returnsAndCreatesDirForProject() {\n        val path = storagePathProvider.getProjectRootDirPath(\"123\")\n        assertThat(path, `is`(root.absolutePath + \"/projects/123\"))\n        assertThat(File(path).exists(), `is`(true))\n    }\n\n    @Test\n    fun projectRootDirPath_createsFileWithSanitizedProjectName() {\n        `when`(projectsRepository.get(\"123\")).thenReturn(\n            Project.Saved(\n                \"123\",\n                \"Project<>\",\n                \"D\",\n                \"#ffffff\"\n            )\n        )\n\n        val path = storagePathProvider.getProjectRootDirPath(\"123\")\n        assertThat(File(path + File.separator + \"Project__\").exists(), `is`(true))\n    }\n\n    @Test\n    fun odkDirPath_withForms_returnsAndCreatesFormsDirForCurrentProject() {\n        val path = storagePathProvider.getOdkDirPath(StorageSubdirectory.FORMS)\n        assertThat(path, `is`(root.absolutePath + \"/projects/123/forms\"))\n        assertThat(File(path).exists(), `is`(true))\n    }\n\n    @Test\n    fun odkDirPath_withInstances_returnsAndCreatesInstancesDirForCurrentProject() {\n        val path = storagePathProvider.getOdkDirPath(StorageSubdirectory.INSTANCES)\n        assertThat(path, `is`(root.absolutePath + \"/projects/123/instances\"))\n        assertThat(File(path).exists(), `is`(true))\n    }\n\n    @Test\n    fun odkDirPath_withMetadata_returnsAndCreatesMetadataDirForCurrentProject() {\n        val path = storagePathProvider.getOdkDirPath(StorageSubdirectory.METADATA)\n        assertThat(path, `is`(root.absolutePath + \"/projects/123/metadata\"))\n        assertThat(File(path).exists(), `is`(true))\n    }\n\n    @Test\n    fun odkDirPath_withCache_returnsAndCreatesCacheDirForCurrentProject() {\n        val path = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE)\n        assertThat(path, `is`(root.absolutePath + \"/projects/123/.cache\"))\n        assertThat(File(path).exists(), `is`(true))\n    }\n\n    @Test\n    fun odkDirPath_withLayers_returnsAndCreatesLayersDirForCurrentProject() {\n        val path = storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS)\n        assertThat(path, `is`(root.absolutePath + \"/projects/123/layers\"))\n        assertThat(File(path).exists(), `is`(true))\n    }\n\n    @Test\n    fun odkDirPath_withSettings_returnsAndCreatesSettingsDirForCurrentProject() {\n        val path = storagePathProvider.getOdkDirPath(StorageSubdirectory.SETTINGS)\n        assertThat(path, `is`(root.absolutePath + \"/projects/123/settings\"))\n        assertThat(File(path).exists(), `is`(true))\n    }\n\n    @Test\n    fun getOdkDirPath_withSharedLayers_returnsAndCreatesSharedLayersDir() {\n        val path = storagePathProvider.getOdkDirPath(StorageSubdirectory.SHARED_LAYERS)\n        assertThat(path, `is`(root.absolutePath + \"/layers\"))\n        assertThat(File(path).exists(), `is`(true))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/support/CollectHelpers.java",
    "content": "package org.odk.collect.android.support;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport android.app.Application;\nimport android.content.Intent;\n\nimport androidx.core.util.Pair;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.test.core.app.ApplicationProvider;\n\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.javarosa.core.reference.Reference;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.odk.collect.android.application.Collect;\nimport org.odk.collect.android.injection.DaggerUtils;\nimport org.odk.collect.android.injection.config.AppDependencyComponent;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.injection.config.DaggerAppDependencyComponent;\nimport org.odk.collect.projects.Project;\nimport org.odk.collect.testshared.RobolectricHelpers;\nimport org.robolectric.Robolectric;\nimport org.robolectric.android.controller.ActivityController;\n\nimport java.util.List;\n\npublic final class CollectHelpers {\n\n    private CollectHelpers() {\n\n    }\n\n    public static void overrideReferenceManager(ReferenceManager referenceManager) {\n        overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n        });\n    }\n\n    public static ReferenceManager setupFakeReferenceManager(List<Pair<String, String>> references) throws InvalidReferenceException {\n        ReferenceManager referenceManager = mock(ReferenceManager.class);\n\n        for (Pair<String, String> reference : references) {\n            createFakeReference(referenceManager, reference.first, reference.second);\n        }\n\n        return referenceManager;\n    }\n\n    private static String createFakeReference(ReferenceManager referenceManager, String referenceURI, String localURI) throws InvalidReferenceException {\n        Reference reference = mock(Reference.class);\n        when(reference.getLocalURI()).thenReturn(localURI);\n        when(referenceManager.deriveReference(referenceURI)).thenReturn(reference);\n\n        return localURI;\n    }\n\n    public static AppDependencyComponent overrideAppDependencyModule(AppDependencyModule appDependencyModule) {\n        AppDependencyComponent testComponent = DaggerAppDependencyComponent.builder()\n                .application(ApplicationProvider.getApplicationContext())\n                .appDependencyModule(appDependencyModule)\n                .build();\n        ((Collect) ApplicationProvider.getApplicationContext()).setComponent(testComponent);\n        return testComponent;\n    }\n\n    public static void resetProcess(AppDependencyModule dependencies) {\n        Collect application = ApplicationProvider.getApplicationContext();\n\n        application.getState().clear();\n\n        AppDependencyComponent newComponent = CollectHelpers.overrideAppDependencyModule(dependencies);\n        newComponent.applicationInitializer().initialize();\n    }\n\n    public static <T extends FragmentActivity> T createThemedActivity(Class<T> clazz) {\n        return RobolectricHelpers.createThemedActivity(clazz);\n    }\n\n    public static FragmentActivity createThemedActivity() {\n        return createThemedActivity(FragmentActivity.class);\n    }\n\n    public static <T extends FragmentActivity> ActivityController<T> buildThemedActivity(Class<T> clazz) {\n        ActivityController<T> activity = Robolectric.buildActivity(clazz);\n        activity.get().setTheme(com.google.android.material.R.style.Theme_MaterialComponents);\n\n        return activity;\n    }\n\n    public static <T extends FragmentActivity> ActivityController<T> buildThemedActivity(Class<T> clazz, Intent intent) {\n        ActivityController<T> activity = Robolectric.buildActivity(clazz, intent);\n        activity.get().setTheme(com.google.android.material.R.style.Theme_MaterialComponents);\n\n        return activity;\n    }\n\n    public static String setupDemoProject() {\n        createDemoProject();\n        DaggerUtils.getComponent(ApplicationProvider.<Application>getApplicationContext()).currentProjectProvider().setCurrentProject(Project.DEMO_PROJECT_ID);\n        return Project.DEMO_PROJECT_ID;\n    }\n\n    public static String createDemoProject() {\n        return createProject(Project.Companion.getDEMO_PROJECT());\n    }\n\n    public static String createProject(Project project) {\n        Project.Saved savedProject = DaggerUtils.getComponent(ApplicationProvider.<Application>getApplicationContext()).projectsRepository().save(project);\n        return savedProject.getUuid();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/support/Matchers.kt",
    "content": "package org.odk.collect.android.support\n\nimport android.text.method.PasswordTransformationMethod\nimport android.view.View\nimport android.widget.EditText\nimport androidx.test.espresso.matcher.BoundedMatcher\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\n\nobject Matchers {\n    fun isPasswordHidden(): Matcher<View> {\n        return object : BoundedMatcher<View, EditText>(EditText::class.java) {\n            override fun describeTo(description: Description) {\n                description.appendText(\"Password is hidden\")\n            }\n\n            override fun matchesSafely(editText: EditText): Boolean {\n                // returns true if password is hidden\n                return editText.transformationMethod is PasswordTransformationMethod\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/support/MockFormEntryPromptBuilder.java",
    "content": "package org.odk.collect.android.support;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static java.util.Arrays.asList;\n\nimport androidx.core.util.Pair;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.IFormElement;\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.model.instance.TreeElement;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class MockFormEntryPromptBuilder {\n\n    private final FormEntryPrompt prompt;\n\n    public MockFormEntryPromptBuilder() {\n        this.prompt = mock(FormEntryPrompt.class);\n\n        when(prompt.getIndex()).thenReturn(mock(FormIndex.class));\n        when(prompt.getIndex().toString()).thenReturn(\"0, 0\");\n        when(prompt.getFormElement()).thenReturn(mock(IFormElement.class));\n        when(prompt.getSelectChoiceText(null)).thenThrow(new NullPointerException());\n\n        // Make sure we have a non-null question\n        withQuestion(mock(QuestionDef.class));\n    }\n\n    public MockFormEntryPromptBuilder(FormEntryPrompt prompt) {\n        this.prompt = prompt;\n    }\n\n    public MockFormEntryPromptBuilder withLongText(String text) {\n        when(prompt.getLongText()).thenReturn(text);\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withIndex(String index) {\n        when(prompt.getIndex().toString()).thenReturn(index);\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withReadOnly(boolean readOnly) {\n        when(prompt.isReadOnly()).thenReturn(readOnly);\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withAudioURI(String audioURI) {\n        when(prompt.getAudioText()).thenReturn(audioURI);\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withImageURI(String imageURI) {\n        when(prompt.getImageText()).thenReturn(imageURI);\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withAdditionalAttribute(String name, String value) {\n        when(prompt.getQuestion().getAdditionalAttribute(null, name)).thenReturn(value);\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withSelectChoices(List<SelectChoice> choices) {\n        for (int i = 0; i < choices.size(); i++) {\n            choices.get(i).setIndex(i);\n            when(prompt.getSelectChoiceText(choices.get(i))).thenReturn(choices.get(i).getValue());\n        }\n\n        when(prompt.getSelectChoices()).thenReturn(choices);\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withSpecialFormSelectChoiceText(List<Pair<String, String>> formAndTexts) {\n        for (int i = 0; i < prompt.getSelectChoices().size(); i++) {\n            when(prompt.getSpecialFormSelectChoiceText(prompt.getSelectChoices().get(i), formAndTexts.get(i).first)).thenReturn(formAndTexts.get(i).second);\n        }\n\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withControlType(int controlType) {\n        when(prompt.getControlType()).thenReturn(controlType);\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withDataType(int dataType) {\n        when(prompt.getDataType()).thenReturn(dataType);\n        return this;\n    }\n\n    public FormEntryPrompt build() {\n        return prompt;\n    }\n\n    public MockFormEntryPromptBuilder withAppearance(String appearance) {\n        when(prompt.getAppearanceHint()).thenReturn(appearance);\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withAnswerDisplayText(String text) {\n        IAnswerData answer = mock(IAnswerData.class);\n        when(answer.getDisplayText()).thenReturn(text);\n        when(prompt.getAnswerText()).thenReturn(text);\n        when(prompt.getAnswerValue()).thenReturn(answer);\n\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withAnswer(IAnswerData answer) {\n        when(prompt.getAnswerValue()).thenReturn(answer);\n        when(prompt.getAnswerText()).thenCallRealMethod();\n\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withQuestion(QuestionDef questionDef) {\n        when(prompt.getQuestion()).thenReturn(questionDef);\n        return this;\n    }\n\n    public MockFormEntryPromptBuilder withBindAttribute(String namespace, String name, String value) {\n        TreeElement treeElement = TreeElement.constructAttributeElement(namespace, name, value);\n        treeElement.setValue(new StringData(value));\n\n        when(prompt.getBindAttributes()).thenReturn(asList(treeElement));\n\n        return this;\n    }\n\n    @NotNull\n    public MockFormEntryPromptBuilder withSelectChoiceText(@NotNull Map<SelectChoice, String> choiceToText) {\n        for (Map.Entry<SelectChoice, String> entry : choiceToText.entrySet()) {\n            when(prompt.getSelectChoiceText(entry.getKey())).thenReturn(entry.getValue());\n        }\n\n        return this;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/support/SwipableParentActivity.kt",
    "content": "package org.odk.collect.android.support\n\nimport androidx.fragment.app.FragmentActivity\nimport org.odk.collect.android.audio.AudioControllerView.SwipableParent\n\nclass SwipableParentActivity : FragmentActivity(), SwipableParent {\n    var isSwipingAllowed = false\n        private set\n\n    override fun allowSwiping(allowSwiping: Boolean) {\n        isSwipingAllowed = allowSwiping\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/support/WidgetTestActivity.kt",
    "content": "package org.odk.collect.android.support\n\nimport android.view.View\nimport androidx.appcompat.app.AppCompatActivity\nimport org.odk.collect.android.application.CollectComposeThemeProvider\n\nclass WidgetTestActivity : AppCompatActivity(), CollectComposeThemeProvider {\n    @JvmField\n    val viewsRegisterForContextMenu = mutableListOf<View>()\n\n    override fun registerForContextMenu(view: View) {\n        super.registerForContextMenu(view)\n        viewsRegisterForContextMenu.add(view)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/tasks/SaveFormIndexTaskTest.java",
    "content": "package org.odk.collect.android.tasks;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.storage.StoragePathProvider;\nimport org.odk.collect.android.storage.StorageSubdirectory;\nimport org.odk.collect.android.support.CollectHelpers;\n\nimport java.io.File;\n\n// Verify that a FormIndex can be saved to and restored from a file\n@RunWith(AndroidJUnit4.class)\npublic class SaveFormIndexTaskTest {\n\n    @Before\n    public void setup() {\n        CollectHelpers.setupDemoProject(); // Allows using StoragePathProvider\n    }\n\n    @Test\n    public void saveAndReadFormIndexTest() {\n        String instanceName = \"test.xml\";\n\n        // for loadFormIndexFromFile\n        File instancePath = new File(new StoragePathProvider().getOdkDirPath(StorageSubdirectory.INSTANCES) + File.separator + instanceName);\n        FormController formController = mock(FormController.class);\n        when(formController.getInstanceFile()).thenReturn(instancePath);\n\n        FormIndex originalFormIndex = FormIndex.createBeginningOfFormIndex();\n        File indexFile = SaveFormToDisk.getFormIndexFile(instanceName);\n        SaveFormIndexTask.exportFormIndexToFile(originalFormIndex, indexFile);\n\n        FormIndex readFormIndex = SaveFormIndexTask.loadFormIndexFromFile(formController);\n\n        assertEquals(originalFormIndex, readFormIndex);\n        assertNotNull(readFormIndex);\n        assertEquals(originalFormIndex.getReference(), readFormIndex.getReference());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/AdminPasswordProviderTest.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.core.IsNull.nullValue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.settings.keys.ProtectedProjectKeys.KEY_ADMIN_PW;\n\nimport org.junit.Test;\nimport org.odk.collect.shared.settings.Settings;\n\npublic class AdminPasswordProviderTest {\n\n    @Test\n    public void when_adminPassHasEmptyValue_should_isAdminPasswordSetReturnFalse() {\n        Settings adminSharedPreferences = mock(Settings.class);\n        when(adminSharedPreferences.getString(KEY_ADMIN_PW)).thenReturn(\"\");\n\n        AdminPasswordProvider adminPasswordProvider = new AdminPasswordProvider(adminSharedPreferences);\n        assertThat(adminPasswordProvider.isAdminPasswordSet(), is(false));\n    }\n\n    @Test\n    public void when_adminPassHasNullValue_should_isAdminPasswordSetReturnFalse() {\n        Settings adminSharedPreferences = mock(Settings.class);\n        when(adminSharedPreferences.getString(KEY_ADMIN_PW)).thenReturn(null);\n\n        AdminPasswordProvider adminPasswordProvider = new AdminPasswordProvider(adminSharedPreferences);\n        assertThat(adminPasswordProvider.isAdminPasswordSet(), is(false));\n    }\n\n    @Test\n    public void when_adminPassIsSetProperly_should_isAdminPasswordSetReturnTrue() {\n        Settings adminSharedPreferences = mock(Settings.class);\n        when(adminSharedPreferences.getString(KEY_ADMIN_PW)).thenReturn(\"123\");\n\n        AdminPasswordProvider adminPasswordProvider = new AdminPasswordProvider(adminSharedPreferences);\n        assertThat(adminPasswordProvider.isAdminPasswordSet(), is(true));\n    }\n\n    @Test\n    public void when_adminPassHasEmptyValue_should_getAdminPasswordReturnEmptyString() {\n        Settings adminSharedPreferences = mock(Settings.class);\n        when(adminSharedPreferences.getString(KEY_ADMIN_PW)).thenReturn(\"\");\n\n        AdminPasswordProvider adminPasswordProvider = new AdminPasswordProvider(adminSharedPreferences);\n        assertThat(adminPasswordProvider.getAdminPassword(), is(\"\"));\n    }\n\n    @Test\n    public void when_adminPassHasNullValue_should_getAdminPasswordReturnNull() {\n        Settings adminSharedPreferences = mock(Settings.class);\n        when(adminSharedPreferences.getString(KEY_ADMIN_PW)).thenReturn(null);\n\n        AdminPasswordProvider adminPasswordProvider = new AdminPasswordProvider(adminSharedPreferences);\n        assertThat(adminPasswordProvider.getAdminPassword(), is(nullValue()));\n    }\n\n    @Test\n    public void when_adminPassIsSetProperly_should_getAdminPasswordReturnCorrectValue() {\n        Settings adminSharedPreferences = mock(Settings.class);\n        when(adminSharedPreferences.getString(KEY_ADMIN_PW)).thenReturn(\"123\");\n\n        AdminPasswordProvider adminPasswordProvider = new AdminPasswordProvider(adminSharedPreferences);\n        assertThat(adminPasswordProvider.getAdminPassword(), is(\"123\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/AppearancesTest.kt",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.android.utilities\n\nimport android.content.res.Configuration\nimport junit.framework.TestCase.assertEquals\nimport junit.framework.TestCase.assertFalse\nimport junit.framework.TestCase.assertTrue\nimport org.javarosa.core.model.Constants\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.androidshared.utils.ScreenUtils\n\nclass AppearancesTest {\n    private val formEntryPrompt = mock<FormEntryPrompt>()\n    private val screenUtils = mock<ScreenUtils>()\n\n    @Test\n    fun `getSanitizedAppearanceHint returns an empty string if there is no appearance`() {\n        assertEquals(Appearances.getSanitizedAppearanceHint(formEntryPrompt), Appearances.NO_APPEARANCE)\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertEquals(Appearances.getSanitizedAppearanceHint(formEntryPrompt), Appearances.NO_APPEARANCE)\n    }\n\n    @Test\n    fun `getSanitizedAppearanceHint returns lowercase appearance`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"BLAH\")\n        assertEquals(Appearances.getSanitizedAppearanceHint(formEntryPrompt), \"blah\")\n    }\n\n    @Test\n    fun `getSanitizedAppearanceHint ignores search function`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah search('fruits') blah\")\n        assertEquals(Appearances.getSanitizedAppearanceHint(formEntryPrompt), \"blah  blah\")\n    }\n\n    @Test\n    fun `hasAppearance returns false if there is no appearance`() {\n        assertFalse(Appearances.hasAppearance(formEntryPrompt, \"blah\"))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertFalse(Appearances.hasAppearance(formEntryPrompt, \"blah\"))\n    }\n\n    @Test\n    fun `hasAppearance returns false if given appearance is not found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"something else something\")\n        assertFalse(Appearances.hasAppearance(formEntryPrompt, \"blah\"))\n    }\n\n    @Test\n    fun `hasAppearance returns true if given appearance is found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"something BLAH something\")\n        assertTrue(Appearances.hasAppearance(formEntryPrompt, \"blah\"))\n    }\n\n    @Test\n    fun `getNumberOfColumns returns 1 if there is no appearance`() {\n        assertEquals(1, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertEquals(1, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n    }\n\n    @Test\n    fun `getNumberOfColumns returns a correct number for valid appearances`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns-2\")\n        assertEquals(2, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah COLUMNS-10\")\n        assertEquals(10, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns-10 quick\")\n        assertEquals(10, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns-5 autocomplete\")\n        assertEquals(5, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"Columns-5 \")\n        assertEquals(5, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns-5  \")\n        assertEquals(5, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"  columns-5\")\n        assertEquals(5, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"quick columns-5\")\n        assertEquals(5, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"compact-5\")\n        assertEquals(5, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"COMPACT-9\")\n        assertEquals(9, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns-9\")\n        assertEquals(9, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n    }\n\n    @Test\n    fun `getNumberOfColumns returns 1 for invalid appearances`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns-10quick\")\n        assertEquals(1, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns-5autocomplete\")\n        assertEquals(1, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns--1\")\n        assertEquals(1, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns--10\")\n        assertEquals(1, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns-\")\n        assertEquals(1, Appearances.getNumberOfColumns(formEntryPrompt, mock()))\n    }\n\n    @Test\n    fun `getNumberOfColumns should return 2 for small screens if there is 'columns' appearance`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns\")\n        whenever(screenUtils.screenSizeConfiguration).thenReturn(Configuration.SCREENLAYOUT_SIZE_SMALL)\n        assertEquals(2, Appearances.getNumberOfColumns(formEntryPrompt, screenUtils))\n    }\n\n    @Test\n    fun `getNumberOfColumns should return 3 for normal screens if there is 'columns' appearance`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns\")\n        whenever(screenUtils.screenSizeConfiguration).thenReturn(Configuration.SCREENLAYOUT_SIZE_NORMAL)\n        assertEquals(3, Appearances.getNumberOfColumns(formEntryPrompt, screenUtils))\n    }\n\n    @Test\n    fun `getNumberOfColumns should return 4 for large screens if there is 'columns' appearance`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns\")\n        whenever(screenUtils.screenSizeConfiguration).thenReturn(Configuration.SCREENLAYOUT_SIZE_LARGE)\n        assertEquals(4, Appearances.getNumberOfColumns(formEntryPrompt, screenUtils))\n    }\n\n    @Test\n    fun `getNumberOfColumns should return 5 for extra large screens if there is 'columns' appearance`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns\")\n        whenever(screenUtils.screenSizeConfiguration).thenReturn(Configuration.SCREENLAYOUT_SIZE_XLARGE)\n        assertEquals(5, Appearances.getNumberOfColumns(formEntryPrompt, screenUtils))\n    }\n\n    @Test\n    fun `getNumberOfColumns should return 3 if there is 'columns' appearance and screen size can not be determined`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"columns\")\n        whenever(screenUtils.screenSizeConfiguration).thenReturn(99999)\n        assertEquals(3, Appearances.getNumberOfColumns(formEntryPrompt, screenUtils))\n    }\n\n    @Test\n    fun `isNoButtonsAppearance returns false when there is no appearance`() {\n        assertFalse(Appearances.isNoButtonsAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertFalse(Appearances.isNoButtonsAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isNoButtonsAppearance returns false when 'no-buttons' appearance is not found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah\")\n        assertFalse(Appearances.isNoButtonsAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isNoButtonsAppearance returns true when 'no-buttons' appearance is found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"NO-BUTTONS\")\n        assertTrue(Appearances.isNoButtonsAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"no-buttons\")\n        assertTrue(Appearances.isNoButtonsAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"No-buttonsCompact\")\n        assertTrue(Appearances.isNoButtonsAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isCompactAppearance returns false when there is no appearance`() {\n        assertFalse(Appearances.isCompactAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertFalse(Appearances.isCompactAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isCompactAppearance returns false when 'compact' appearance is not found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah\")\n        assertFalse(Appearances.isCompactAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isCompactAppearance returns true when 'compact' appearance is found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"COMPACT\")\n        assertTrue(Appearances.isCompactAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"compact\")\n        assertTrue(Appearances.isCompactAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"CompactCompact\")\n        assertTrue(Appearances.isCompactAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `useThousandSeparator returns false when there is no appearance`() {\n        assertFalse(Appearances.useThousandSeparator(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertFalse(Appearances.useThousandSeparator(formEntryPrompt))\n    }\n\n    @Test\n    fun `useThousandSeparator returns false when 'thousands-sep' appearance is not found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertFalse(Appearances.useThousandSeparator(formEntryPrompt))\n    }\n\n    @Test\n    fun `useThousandSeparator returns false when 'thousands-sep' appearance is found but mixed with 'masked' for text questions`() {\n        whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT)\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"thousands-sep masked\")\n        assertFalse(Appearances.useThousandSeparator(formEntryPrompt))\n    }\n\n    @Test\n    fun `useThousandSeparator returns true when 'thousands-sep' appearance is found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"THOUSANDS-SEP\")\n        assertTrue(Appearances.useThousandSeparator(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"thousands-sep\")\n        assertTrue(Appearances.useThousandSeparator(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"Thousands-sepCompact\")\n        assertTrue(Appearances.useThousandSeparator(formEntryPrompt))\n    }\n\n    @Test\n    fun `isFrontCameraAppearance returns false when there is no appearance`() {\n        assertFalse(Appearances.isFrontCameraAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertFalse(Appearances.isFrontCameraAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isFrontCameraAppearance returns false when non of supported appearances is not found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah\")\n        assertFalse(Appearances.isFrontCameraAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isFrontCameraAppearance returns true when front appearance is found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"FRONT\")\n        assertTrue(Appearances.isFrontCameraAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"front\")\n        assertTrue(Appearances.isFrontCameraAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"FrontCompact\")\n        assertTrue(Appearances.isFrontCameraAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isFrontCameraAppearance returns true when new front appearance is found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"NEW-FRONT\")\n        assertTrue(Appearances.isFrontCameraAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"new-front\")\n        assertTrue(Appearances.isFrontCameraAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"New-frontCompact\")\n        assertTrue(Appearances.isFrontCameraAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isFrontCameraAppearance returns true when selfie appearance is found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"SELFIE\")\n        assertTrue(Appearances.isFrontCameraAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"selfie\")\n        assertTrue(Appearances.isFrontCameraAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"SelfieCompact\")\n        assertTrue(Appearances.isFrontCameraAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isFlexAppearance returns false when there is no appearance`() {\n        assertFalse(Appearances.isFlexAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertFalse(Appearances.isFlexAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isFlexAppearance returns false when appearance contains 'compact-n'`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"BLAH COMPACT-N_BLAH\")\n        assertFalse(Appearances.isFlexAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah compact-n_blah\")\n        assertFalse(Appearances.isFlexAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isFlexAppearance returns true when appearance contains 'compact'`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"BLAH COMPACT_BLAH\")\n        assertTrue(Appearances.isFlexAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah compact_blah\")\n        assertTrue(Appearances.isFlexAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isFlexAppearance returns true when appearance contains 'quickcompact'`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"BLAH QUICKCOMPACT_BLAH\")\n        assertTrue(Appearances.isFlexAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah quickcompact_blah\")\n        assertTrue(Appearances.isFlexAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isFlexAppearance returns true when appearance contains 'columns-pack'`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"BLAH COLUMNS-PACK_BLAH\")\n        assertTrue(Appearances.isFlexAppearance(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah columns-pack_blah\")\n        assertTrue(Appearances.isFlexAppearance(formEntryPrompt))\n    }\n\n    @Test\n    fun `isAutocomplete returns false when there is no appearance`() {\n        assertFalse(Appearances.isAutocomplete(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertFalse(Appearances.isAutocomplete(formEntryPrompt))\n    }\n\n    @Test\n    fun `isAutocomplete returns false when non of supported appearances is found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah\")\n        assertFalse(Appearances.isAutocomplete(formEntryPrompt))\n    }\n\n    @Test\n    fun `isAutocomplete returns true when 'autocomplete' or 'search' appearance is found`() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"autocomplete\")\n        assertTrue(Appearances.isAutocomplete(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"search\")\n        assertTrue(Appearances.isAutocomplete(formEntryPrompt))\n    }\n\n    @Test\n    fun `isMasked returns false when there is no appearance`() {\n        whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT)\n        assertFalse(Appearances.isMasked(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n        assertFalse(Appearances.isMasked(formEntryPrompt))\n    }\n\n    @Test\n    fun `isMasked returns false when non of supported appearances is found`() {\n        whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT)\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah\")\n        assertFalse(Appearances.isMasked(formEntryPrompt))\n    }\n\n    @Test\n    fun `isMasked returns true when 'masked' appearance is found for text questions`() {\n        whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT)\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"masked\")\n        assertTrue(Appearances.isMasked(formEntryPrompt))\n    }\n\n    @Test\n    fun `isMasked returns false when 'masked' appearance is found for numeric questions`() {\n        whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_INTEGER)\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"masked\")\n        assertFalse(Appearances.isMasked(formEntryPrompt))\n\n        whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_DECIMAL)\n        assertFalse(Appearances.isMasked(formEntryPrompt))\n    }\n\n    @Test\n    fun `isMasked returns false when 'masked' appearance is found for text questions with 'numbers' appearance`() {\n        whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT)\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"masked numbers\")\n        assertFalse(Appearances.isMasked(formEntryPrompt))\n    }\n\n    @Test\n    fun `isQuick returns true when 'quick' appearance is found for select_one questions`() {\n        whenever(formEntryPrompt.controlType).thenReturn(Constants.CONTROL_SELECT_ONE)\n        assertFalse(Appearances.isQuick(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"quick\")\n        assertTrue(Appearances.isQuick(formEntryPrompt))\n    }\n\n    @Test\n    fun `isQuick returns false when 'quick' appearance is not found for select_one questions`() {\n        whenever(formEntryPrompt.controlType).thenReturn(Constants.CONTROL_SELECT_ONE)\n        assertFalse(Appearances.isQuick(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"blah\")\n        assertFalse(Appearances.isQuick(formEntryPrompt))\n    }\n\n    @Test\n    fun `isQuick returns false when 'quick' appearance is found for select_multiple questions`() {\n        whenever(formEntryPrompt.controlType).thenReturn(Constants.CONTROL_SELECT_MULTI)\n        assertFalse(Appearances.isQuick(formEntryPrompt))\n\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"quick\")\n        assertFalse(Appearances.isQuick(formEntryPrompt))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/ArrayUtilsTest.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertArrayEquals;\nimport static org.odk.collect.android.utilities.ArrayUtils.toPrimitive;\nimport static org.odk.collect.android.utilities.ArrayUtils.toObject;\n\npublic class ArrayUtilsTest {\n\n    @Test\n    public void toPrimitiveCreatesPrimitiveLongArray() throws Exception {\n        assertArrayEquals(new long[] {1, 2, 3, 4, 5}, toPrimitive(new Long[] {1L, 2L, 3L, 4L, 5L}));\n    }\n\n    @Test\n    public void nullToPrimitiveCreatesEmptyPrimitiveLongArray() throws Exception {\n        assertArrayEquals(new long[0], toPrimitive(null));\n    }\n\n    @Test(expected = NullPointerException.class)\n    public void arrayContainingNullCausesNpe() {\n        toPrimitive(new Long[] {1L, null, 3L, 4L, 5L});\n    }\n\n    @Test(expected = NullPointerException.class)\n    public void arrayStartingWithNullCausesNpe() {\n        toPrimitive(new Long[] {null, 3L, 4L, 5L});\n    }\n\n    @Test(expected = NullPointerException.class)\n    public void arrayEndingWithNullCausesNpe() {\n        toPrimitive(new Long[] {1L, 3L, 4L, null});\n    }\n\n    @Test\n    public void toObjectCreatesLongArray() throws Exception {\n        assertArrayEquals(new Long[] {1L, 2L, 3L, 4L, 5L}, toObject(new long[] {1, 2, 3, 4, 5}));\n    }\n\n    @Test\n    public void nullBecomesEmptyLongArray() throws Exception {\n        assertArrayEquals(new Long[0], toObject(null));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/CSVUtilsTest.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport org.junit.Test;\n\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.hamcrest.CoreMatchers.nullValue;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.odk.collect.android.utilities.CSVUtils.getEscapedValueForCsv;\n\npublic class CSVUtilsTest {\n    @Test\n    public void null_shouldBePassedThrough() {\n        assertThat(getEscapedValueForCsv(null), is(nullValue()));\n    }\n\n    @Test\n    public void stringsWithoutQuotesCommasOrNewlines_shouldBePassedThrough() {\n        assertThat(getEscapedValueForCsv(\"a b c d e\"), is(\"a b c d e\"));\n    }\n\n    @Test\n    public void quotes_shouldBeEscaped_andSurroundedByQuotes() {\n        assertThat(getEscapedValueForCsv(\"a\\\"b\\\"\"), is(\"\\\"a\\\"\\\"b\\\"\\\"\\\"\"));\n    }\n\n    @Test\n    public void commas_shouldBeSurroundedByQuotes() {\n        assertThat(getEscapedValueForCsv(\"a,b\"), is(\"\\\"a,b\\\"\"));\n    }\n\n    @Test\n    public void newlines_shouldBeSurroundedByQuotes() {\n        assertThat(getEscapedValueForCsv(\"a\\nb\"), is(\"\\\"a\\nb\\\"\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/ChangeLockProviderTest.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.junit.Test\n\nclass ChangeLockProviderTest {\n\n    @Test\n    fun `getFormLock returns a different lock for different projects`() {\n        val changeLockProvider = ChangeLockProvider()\n        val lock1 = changeLockProvider.getFormLock(\"blah1\")\n        val lock2 = changeLockProvider.getFormLock(\"blah2\")\n        assertThat(lock1, not(equalTo(lock2)))\n    }\n\n    @Test\n    fun `getFormLock returns the same lock every time for a project`() {\n        val changeLockProvider = ChangeLockProvider()\n        val lock1 = changeLockProvider.getFormLock(\"blah\")\n        val lock2 = changeLockProvider.getFormLock(\"blah\")\n        assertThat(lock1, equalTo(lock2))\n    }\n\n    @Test\n    fun `getInstanceLock returns a different lock for different projects`() {\n        val changeLockProvider = ChangeLockProvider()\n        val lock1 = changeLockProvider.getInstanceLock(\"blah1\")\n        val lock2 = changeLockProvider.getInstanceLock(\"blah2\")\n        assertThat(lock1, not(equalTo(lock2)))\n    }\n\n    @Test\n    fun `getInstanceLock returns the same lock every time for a project`() {\n        val changeLockProvider = ChangeLockProvider()\n        val lock1 = changeLockProvider.getInstanceLock(\"blah\")\n        val lock2 = changeLockProvider.getInstanceLock(\"blah\")\n        assertThat(lock1, equalTo(lock2))\n    }\n\n    @Test\n    fun `getInstanceLock and getFormLock return different locks for the same project`() {\n        val changeLockProvider = ChangeLockProvider()\n        val formLock = changeLockProvider.getFormLock(\"blah1\")\n        val instanceLock = changeLockProvider.getInstanceLock(\"blah1\")\n        assertThat(formLock, not(equalTo(instanceLock)))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/ExternalAppIntentProviderTest.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\n\n@RunWith(AndroidJUnit4::class)\nclass ExternalAppIntentProviderTest {\n    private lateinit var formEntryPrompt: FormEntryPrompt\n    private lateinit var externalAppIntentProvider: ExternalAppIntentProvider\n\n    @Before\n    fun setup() {\n        formEntryPrompt = mock()\n        externalAppIntentProvider = ExternalAppIntentProvider()\n        whenever(formEntryPrompt.index).thenReturn(mock())\n    }\n\n    @Test\n    fun intentAction_shouldBeSetProperlyIfThePackageNameEndsWithBrackets() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"ex:com.example.collectanswersprovider()\")\n        val resultIntent = externalAppIntentProvider.getIntentToRunExternalApp(null, formEntryPrompt)\n        assertThat(resultIntent.action, `is`(\"com.example.collectanswersprovider\"))\n    }\n\n    @Test\n    fun intentAction_shouldBeSetProperlyIfThePackageNameDoesNotEndWithBrackets() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"ex:com.example.collectanswersprovider\")\n        val resultIntent = externalAppIntentProvider.getIntentToRunExternalApp(null, formEntryPrompt)\n        assertThat(resultIntent.action, `is`(\"com.example.collectanswersprovider\"))\n    }\n\n    @Test\n    fun whenNoParamsSpecified_shouldIntentHaveNoExtras() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"ex:com.example.collectanswersprovider()\")\n        val resultIntent = externalAppIntentProvider.getIntentToRunExternalApp(null, formEntryPrompt)\n        assertThat(resultIntent.extras, nullValue())\n    }\n\n    @Test\n    fun whenParamsSpecified_shouldIntentHaveExtras() {\n        whenever(formEntryPrompt.appearanceHint)\n            .thenReturn(\"ex:com.example.collectanswersprovider(param1='value1', param2='value2')\")\n        val resultIntent = externalAppIntentProvider.getIntentToRunExternalApp(null, formEntryPrompt)\n        assertThat(resultIntent.extras!!.keySet().size, `is`(2))\n        assertThat(resultIntent.extras!!.getString(\"param1\"), `is`(\"value1\"))\n        assertThat(resultIntent.extras!!.getString(\"param2\"), `is`(\"value2\"))\n    }\n\n    @Test\n    fun whenParamsContainUri_shouldThatUriBeAddedAsIntentData() {\n        whenever(formEntryPrompt.appearanceHint)\n            .thenReturn(\"ex:com.example.collectanswersprovider(param1='value1', uri_data='file:///tmp/android.txt')\")\n        val resultIntent = externalAppIntentProvider.getIntentToRunExternalApp(null, formEntryPrompt)\n        assertThat(resultIntent.data.toString(), `is`(\"file:///tmp/android.txt\"))\n        assertThat(resultIntent.extras!!.keySet().size, `is`(1))\n        assertThat(resultIntent.extras!!.getString(\"param1\"), `is`(\"value1\"))\n    }\n\n    @Test\n    fun packageNameCanBeMixedWithOtherAppearances() {\n        whenever(formEntryPrompt.appearanceHint)\n            .thenReturn(\"masked ex:com.example.collectanswersprovider(param1='value1', param2='value2') thousands-sep\")\n        val resultIntent = externalAppIntentProvider.getIntentToRunExternalApp(null, formEntryPrompt)\n        assertThat(resultIntent.action, `is`(\"com.example.collectanswersprovider\"))\n        assertThat(resultIntent.extras!!.keySet().size, `is`(2))\n        assertThat(resultIntent.extras!!.getString(\"param1\"), `is`(\"value1\"))\n        assertThat(resultIntent.extras!!.getString(\"param2\"), `is`(\"value2\"))\n    }\n\n    @Test\n    fun parametersCanContainParentheses() {\n        whenever(formEntryPrompt.appearanceHint)\n            .thenReturn(\"ex:com.example.collectanswersprovider(param='blah()')\")\n        val resultIntent = externalAppIntentProvider.getIntentToRunExternalApp(null, formEntryPrompt)\n        assertThat(resultIntent.extras!!.getString(\"param\"), `is`(\"blah()\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/ExternalAppUtilsTest.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport org.junit.Test;\nimport org.odk.collect.android.dynamicpreload.ExternalAppsUtils;\n\nimport java.util.Map;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\n\npublic class ExternalAppUtilsTest {\n\n    @Test\n    public void extractIntentNameTest() {\n        assertEquals(\"org.opendatakit.counter\", ExternalAppsUtils.extractIntentName(\"org.opendatakit.counter(form_id='counter-form', form_name='Counter Form', question_id='1', question_name='Counter 1', increment=true())\"));\n        assertEquals(\"org.opendatakit.counter\", ExternalAppsUtils.extractIntentName(\"org.opendatakit.counter()\"));\n        assertEquals(\"org.opendatakit.counter\", ExternalAppsUtils.extractIntentName(\"org.opendatakit.counter\"));\n    }\n\n    @Test\n    public void extractParametersTest() {\n        // Simple case\n        Map<String, String> result = ExternalAppsUtils.extractParameters(\"org.opendatakit.counter(form_id='counter-form', form_name='Counter Form', question_id='1', question_name='Counter 1', increment=true())\");\n        assertCounterAppParameters(result);\n\n        // No spaces at all\n        result = ExternalAppsUtils.extractParameters(\"org.opendatakit.counter(form_id='counter-form',form_name='Counter Form',question_id='1',question_name='Counter 1',increment=true())\");\n        assertCounterAppParameters(result);\n\n        // Spaces everywhere\n        result = ExternalAppsUtils.extractParameters(\"   org.opendatakit.counter ( form_id = 'counter-form' , form_name = 'Counter Form' , question_id = '1' , question_name = 'Counter 1' , increment = true()   )   \");\n        assertCounterAppParameters(result);\n\n        // Commas in values\n        result = ExternalAppsUtils.extractParameters(\"org.companyX.appX(parameter1='value1', parameter2='value2a, value2b'\");\n        assertEquals(2, result.size());\n        assertTrue(result.keySet().contains(\"parameter1\"));\n        assertTrue(result.keySet().contains(\"parameter2\"));\n\n        assertEquals(\"'value1'\", result.get(\"parameter1\"));\n        assertEquals(\"'value2a, value2b'\", result.get(\"parameter2\"));\n\n        // Regex with commas\n        result = ExternalAppsUtils.extractParameters(\"org.companyX.appX(regex1='/^([a-z0-9_\\\\.-]+)@([\\\\da-z\\\\.-]+)\\\\.([a-z\\\\.]{2,6})$/', regex2='/^[a-z0-9_-]{6,18}$/', regex3='/^(https?:\\\\/\\\\/)?([\\\\da-z\\\\.-]+)\\\\.([a-z\\\\.]{2,6})([\\\\/\\\\w \\\\.-]*)*\\\\/?$/'\");\n        assertEquals(3, result.size());\n        assertTrue(result.keySet().contains(\"regex1\"));\n        assertTrue(result.keySet().contains(\"regex2\"));\n        assertTrue(result.keySet().contains(\"regex3\"));\n\n        assertEquals(\"'/^([a-z0-9_\\\\.-]+)@([\\\\da-z\\\\.-]+)\\\\.([a-z\\\\.]{2,6})$/'\", result.get(\"regex1\"));\n        assertEquals(\"'/^[a-z0-9_-]{6,18}$/'\", result.get(\"regex2\"));\n        assertEquals(\"'/^(https?:\\\\/\\\\/)?([\\\\da-z\\\\.-]+)\\\\.([a-z\\\\.]{2,6})([\\\\/\\\\w \\\\.-]*)*\\\\/?$/'\", result.get(\"regex3\"));\n\n        // Values populated from a form\n        result = ExternalAppsUtils.extractParameters(\"android.intent.action.SENDTO(sms_body= /send-sms-or-email-in-form/message , uri_data= /send-sms-or-email-in-form/send_to )\");\n        assertEquals(2, result.size());\n        assertTrue(result.keySet().contains(\"sms_body\"));\n        assertTrue(result.keySet().contains(\"uri_data\"));\n\n        assertEquals(\"/send-sms-or-email-in-form/message\", result.get(\"sms_body\"));\n        assertEquals(\"/send-sms-or-email-in-form/send_to\", result.get(\"uri_data\"));\n\n        // Real sample from a user: https://forum.getodk.org/t/external-app-with-intent-parameters/20125/5?u=grzesiek2010\n        result = ExternalAppsUtils.extractParameters(\"ex:ch.novelt.odkcompanion.OPEN(current_value=/afp_report/group_mini_cif/cn, match='^[0-9]{4}W[0-9]{2}-[0-9]{1,5}$', filter='^AFP:([0-9]{4}W[0-9]{2}-[0-9]{1,5})')\");\n        assertEquals(3, result.size());\n        assertTrue(result.keySet().contains(\"current_value\"));\n        assertTrue(result.keySet().contains(\"match\"));\n        assertTrue(result.keySet().contains(\"filter\"));\n\n        assertEquals(\"/afp_report/group_mini_cif/cn\", result.get(\"current_value\"));\n        assertEquals(\"'^[0-9]{4}W[0-9]{2}-[0-9]{1,5}$'\", result.get(\"match\"));\n        assertEquals(\"'^AFP:([0-9]{4}W[0-9]{2}-[0-9]{1,5})'\", result.get(\"filter\"));\n    }\n\n    @Test\n    public void asStringDataTest() {\n        assertNull(ExternalAppsUtils.asStringData(null));\n        assertEquals(\"Test Value\", ExternalAppsUtils.asStringData(\"Test Value\").getValue().toString());\n        assertEquals(\"TestValue\", ExternalAppsUtils.asStringData(\"TestValue\").getValue().toString());\n        assertEquals(\"Test Value 3\", ExternalAppsUtils.asStringData(\"Test Value 3\").getValue().toString());\n        assertEquals(\" Test Value 4 \", ExternalAppsUtils.asStringData(\" Test Value 4 \").getValue().toString());\n    }\n\n    @Test\n    public void asIntegerDataTest() {\n        assertNull(ExternalAppsUtils.asIntegerData(null));\n        assertNull(ExternalAppsUtils.asIntegerData(\"\"));\n        assertNull(ExternalAppsUtils.asIntegerData(\"5.4\"));\n        assertEquals(\"5\", ExternalAppsUtils.asIntegerData(\"5\").getValue().toString());\n        assertEquals(\"-5\", ExternalAppsUtils.asIntegerData(\"-5\").getValue().toString());\n        assertEquals(\"125\", ExternalAppsUtils.asIntegerData(\"125\").getValue().toString());\n    }\n\n    @Test\n    public void asDecimalDataTest() {\n        assertNull(ExternalAppsUtils.asDecimalData(null));\n        assertNull(ExternalAppsUtils.asDecimalData(\"\"));\n        assertNull(ExternalAppsUtils.asDecimalData(\"5..24\"));\n        assertNull(ExternalAppsUtils.asDecimalData(\"5.24c\"));\n        assertEquals(\"5.0\", ExternalAppsUtils.asDecimalData(\"5\").getValue().toString());\n        assertEquals(\"5.0\", ExternalAppsUtils.asDecimalData(\"5.00\").getValue().toString());\n        assertEquals(\"5.0\", ExternalAppsUtils.asDecimalData(\"05\").getValue().toString());\n        assertEquals(\"5.24\", ExternalAppsUtils.asDecimalData(\"5.24\").getValue().toString());\n        assertEquals(\"5.24\", ExternalAppsUtils.asDecimalData(\"5.240\").getValue().toString());\n        assertEquals(\"5.204\", ExternalAppsUtils.asDecimalData(\"5.204\").getValue().toString());\n        assertEquals(\"-5.0\", ExternalAppsUtils.asDecimalData(\"-5\").getValue().toString());\n        assertEquals(\"-27.333\", ExternalAppsUtils.asDecimalData(\"-27.333\").getValue().toString());\n    }\n\n    private void assertCounterAppParameters(Map<String, String> result) {\n        assertEquals(5, result.size());\n        assertTrue(result.keySet().contains(\"form_id\"));\n        assertTrue(result.keySet().contains(\"form_name\"));\n        assertTrue(result.keySet().contains(\"question_id\"));\n        assertTrue(result.keySet().contains(\"question_name\"));\n        assertTrue(result.keySet().contains(\"increment\"));\n\n        assertEquals(\"'counter-form'\", result.get(\"form_id\"));\n        assertEquals(\"'Counter Form'\", result.get(\"form_name\"));\n        assertEquals(\"'1'\", result.get(\"question_id\"));\n        assertEquals(\"'Counter 1'\", result.get(\"question_name\"));\n        assertEquals(\"true()\", result.get(\"increment\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/FileUtilsTest.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport org.hamcrest.Matchers;\nimport org.junit.Test;\n\nimport java.io.File;\n\nimport static org.hamcrest.CoreMatchers.is;\nimport static org.hamcrest.CoreMatchers.nullValue;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.empty;\nimport static org.junit.Assert.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class FileUtilsTest {\n\n    @Test\n    public void mediaDirNameIsCorrect() {\n        String expected = \"sample-file-media\";\n\n        assertEquals(expected, FileUtils.constructMediaPath(\"sample-file.xml\"));\n        assertEquals(expected, FileUtils.constructMediaPath(\"sample-file.extension\"));\n        assertEquals(expected, FileUtils.constructMediaPath(\"sample-file.123\"));\n        assertEquals(expected, FileUtils.constructMediaPath(\"sample-file.docx\"));\n    }\n\n    @Test\n    @SuppressWarnings(\"PMD.DoNotHardCodeSDCard\")\n    public void simplifyScopedStoragePathTest() {\n        assertThat(FileUtils.expandAndroidStoragePath(null), is(nullValue()));\n        assertThat(FileUtils.expandAndroidStoragePath(\"\"), is(\"\"));\n        assertThat(FileUtils.expandAndroidStoragePath(\"blahblahblah\"), is(\"blahblahblah\"));\n        assertThat(FileUtils.expandAndroidStoragePath(\"/storage/emulated/0/Android/data/org.odk.collect.android/files/layers\"), is(\"/sdcard/Android/data/org.odk.collect.android/files/layers\"));\n        assertThat(FileUtils.expandAndroidStoragePath(\"/storage/emulated/0/Android/data/org.odk.collect.android/files/layers/countries/countries-raster.mbtiles\"), is(\"/sdcard/Android/data/org.odk.collect.android/files/layers/countries/countries-raster.mbtiles\"));\n    }\n\n    @Test\n    public void whenTryToListFilesOnNullFile_shouldReturnEmptyArray() {\n        assertThat(FileUtils.listFiles(null), Matchers.is(empty()));\n    }\n\n    @Test\n    public void whenTryToListFilesOnFileThatDoesNotExist_shouldReturnEmptyArray() {\n        File file = mock(File.class);\n        when(file.exists()).thenReturn(false);\n        assertThat(FileUtils.listFiles(file), Matchers.is(empty()));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/FormNameUtilsTest.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport org.junit.Test;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.core.IsNull.nullValue;\nimport static org.odk.collect.android.utilities.FormNameUtils.normalizeFormName;\nimport static org.odk.collect.android.utilities.FormNameUtils.formatFilenameFromFormName;\n\npublic class FormNameUtilsTest {\n\n    @Test\n    public void normalizeFormNameTest() {\n        assertThat(normalizeFormName(null, false), is(nullValue()));\n        assertThat(normalizeFormName(\"Lorem\", false), is(\"Lorem\"));\n        assertThat(normalizeFormName(\"Lorem ipsum\", false), is(\"Lorem ipsum\"));\n        assertThat(normalizeFormName(\"Lorem\\nipsum\", false), is(\"Lorem ipsum\"));\n        assertThat(normalizeFormName(\"Lorem\\n\\nipsum\", false), is(\"Lorem  ipsum\"));\n        assertThat(normalizeFormName(\"\\nLorem\\nipsum\\n\", false), is(\" Lorem ipsum \"));\n\n        assertThat(normalizeFormName(null, true), is(nullValue()));\n        assertThat(normalizeFormName(\"Lorem\", true), is(nullValue()));\n        assertThat(normalizeFormName(\"Lorem ipsum\", true), is(nullValue()));\n        assertThat(normalizeFormName(\"Lorem\\nipsum\", true), is(\"Lorem ipsum\"));\n        assertThat(normalizeFormName(\"Lorem\\n\\nipsum\", true), is(\"Lorem  ipsum\"));\n        assertThat(normalizeFormName(\"\\nLorem\\nipsum\\n\", true), is(\" Lorem ipsum \"));\n    }\n\n    @Test\n    public void formatFilenameFromFormNameTest() {\n        assertThat(formatFilenameFromFormName(null), is(nullValue()));\n        assertThat(formatFilenameFromFormName(\"simple\"), is(\"simple\"));\n        assertThat(formatFilenameFromFormName(\"CamelCase\"), is(\"CamelCase\"));\n        assertThat(formatFilenameFromFormName(\"01234566789\"), is(\"01234566789\"));\n\n        assertThat(formatFilenameFromFormName(\" trimWhitespace \"), is(\"trimWhitespace\"));\n        assertThat(formatFilenameFromFormName(\"keep internal spaces\"), is(\"keep internal spaces\"));\n        assertThat(formatFilenameFromFormName(\"other\\n\\twhitespace\"), is(\"other whitespace\"));\n        assertThat(formatFilenameFromFormName(\"repeated         whitespace\"), is(\"repeated whitespace\"));\n\n        assertThat(formatFilenameFromFormName(\"Turkish İ kept\"), is(\"Turkish İ kept\"));\n        assertThat(formatFilenameFromFormName(\"registered symbol ® stripped\"), is(\"registered symbol stripped\"));\n        assertThat(formatFilenameFromFormName(\"unicode fragment \\ud800 stripped\"), is(\"unicode fragment stripped\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/FormsDownloadResultInterpreterTest.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.formmanagement.ServerFormDetails\nimport org.odk.collect.android.formmanagement.download.FormDownloadException\nimport org.odk.collect.android.formmanagement.download.FormDownloadExceptionMapper\n\n@RunWith(AndroidJUnit4::class)\nclass FormsDownloadResultInterpreterTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n    private val formDetails1 = ServerFormDetails(\"Form 1\", \"\", \"1\", \"1\", \"\", false, true, null)\n    private val formDetails2 = ServerFormDetails(\"Form 2\", \"\", \"5\", \"4\", \"\", false, true, null)\n\n    private var resultWithoutErrors = mapOf<ServerFormDetails, FormDownloadException?>(\n        formDetails1 to null,\n        formDetails2 to null\n    )\n\n    private var resultWithOneError = mapOf<ServerFormDetails, FormDownloadException?>(\n        formDetails1 to null,\n        formDetails2 to FormDownloadException.FormParsingError(RuntimeException())\n    )\n\n    @Test\n    fun `When all forms downloaded successfully getFailures() should return an empty list`() {\n        assertThat(\n            FormsDownloadResultInterpreter.getFailures(resultWithoutErrors, context).size,\n            `is`(0)\n        )\n    }\n\n    @Test\n    fun `When not all forms downloaded successfully getFailures() should return list of failures`() {\n        assertThat(\n            FormsDownloadResultInterpreter.getFailures(resultWithOneError, context).size,\n            `is`(1)\n        )\n        assertThat(\n            FormsDownloadResultInterpreter.getFailures(resultWithOneError, context)[0].title,\n            `is`(\"Form 2\")\n        )\n        assertThat(\n            FormsDownloadResultInterpreter.getFailures(\n                resultWithOneError,\n                context\n            )[0].secondaryText,\n            `is`(context.getString(org.odk.collect.strings.R.string.form_details, \"5\", \"4\"))\n        )\n        assertThat(\n            FormsDownloadResultInterpreter.getFailures(\n                resultWithOneError,\n                context\n            )[0].supportingText,\n            `is`(FormDownloadExceptionMapper(context).getMessage(resultWithOneError[formDetails2]))\n        )\n    }\n\n    @Test\n    fun `When all forms downloaded successfully getNumberOfFailures() should return zero`() {\n        assertThat(FormsDownloadResultInterpreter.getNumberOfFailures(resultWithoutErrors), `is`(0))\n    }\n\n    @Test\n    fun `When not all forms downloaded successfully getNumberOfFailures() should return number of failures`() {\n        assertThat(FormsDownloadResultInterpreter.getNumberOfFailures(resultWithOneError), `is`(1))\n    }\n\n    @Test\n    fun `When all forms downloaded successfully allFormsDownloadedSuccessfully() should return true`() {\n        assertThat(\n            FormsDownloadResultInterpreter.allFormsDownloadedSuccessfully(resultWithoutErrors),\n            `is`(true)\n        )\n    }\n\n    @Test\n    fun `When not all forms downloaded successfully allFormsDownloadedSuccessfully() should return false`() {\n        assertThat(\n            FormsDownloadResultInterpreter.allFormsDownloadedSuccessfully(resultWithOneError),\n            `is`(false)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/FormsRepositoryProviderTest.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.Matchers.startsWith\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.storage.StoragePaths\nimport org.odk.collect.formstest.FormUtils.buildForm\nimport org.odk.collect.shared.TempFiles.createTempDir\n\n@RunWith(AndroidJUnit4::class)\nclass FormsRepositoryProviderTest {\n\n    private val metaDir = createTempDir()\n    private val formsDir = createTempDir()\n    private val cacheDir = createTempDir()\n\n    @Test\n    fun `returned repository uses project directory when passed`() {\n        val context = ApplicationProvider.getApplicationContext<Application>()\n\n        val projectId = \"projectId\"\n\n        val formsRepositoryProvider = FormsRepositoryProvider(\n            context,\n            {\n                StoragePaths(\n                    \"\",\n                    formsDir.absolutePath,\n                    \"\",\n                    cacheDir.absolutePath,\n                    metaDir.absolutePath,\n                    \"\",\n                    \"\"\n                )\n            },\n            mock()\n        )\n        val repository = formsRepositoryProvider.create(projectId)\n\n        val form = repository.save(buildForm(\"id\", \"version\", formsDir.absolutePath).build())\n        assertThat(form.formFilePath, startsWith(formsDir.absolutePath))\n        assertThat(form.jrCacheFilePath, startsWith(cacheDir.absolutePath))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/FormsUploadResultInterpreterTest.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.android.upload.FormUploadException\nimport org.odk.collect.forms.instances.Instance\n\n@RunWith(AndroidJUnit4::class)\nclass FormsUploadResultInterpreterTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n\n    private val instance1 = Instance.Builder()\n        .formId(\"1\")\n        .formVersion(\"1\")\n        .displayName(\"Instance 1\")\n        .instanceFilePath(\"filepath1\")\n        .build()\n\n    private val instance2 = Instance.Builder()\n        .formId(\"2\")\n        .formVersion(\"2\")\n        .displayName(\"Instance 2\")\n        .instanceFilePath(\"filepath2\")\n        .build()\n\n    private var resultWithoutErrors = mapOf<Instance, FormUploadException?>(\n        instance1 to null,\n        instance2 to null\n    )\n\n    private var resultWithOneError = mapOf<Instance, FormUploadException?>(\n        instance1 to null,\n        instance2 to FormUploadException(\"Something went wrong!\")\n    )\n\n    @Test\n    fun `When all forms uploaded successfully getFailures() should return an empty list`() {\n        assertThat(FormsUploadResultInterpreter.getFailures(resultWithoutErrors, context).size, `is`(0))\n    }\n\n    @Test\n    fun `When not all forms uploaded successfully getFailures() should return list of failures`() {\n        assertThat(FormsUploadResultInterpreter.getFailures(resultWithOneError, context).size, `is`(1))\n        assertThat(FormsUploadResultInterpreter.getFailures(resultWithOneError, context)[0].title, `is`(\"Instance 2\"))\n        assertThat(\n            FormsUploadResultInterpreter.getFailures(resultWithOneError, context)[0].secondaryText,\n            `is`(context.getString(org.odk.collect.strings.R.string.form_details, \"2\", \"2\"))\n        )\n        assertThat(FormsUploadResultInterpreter.getFailures(resultWithOneError, context)[0].supportingText, `is`(\"Something went wrong!\"))\n    }\n\n    @Test\n    fun `When all forms uploaded successfully getNumberOfFailures() should return zero`() {\n        assertThat(FormsUploadResultInterpreter.getNumberOfFailures(resultWithoutErrors), `is`(0))\n    }\n\n    @Test\n    fun `When not all forms uploaded successfully getNumberOfFailures() should return number of failures`() {\n        assertThat(FormsUploadResultInterpreter.getNumberOfFailures(resultWithOneError), `is`(1))\n    }\n\n    @Test\n    fun `When all forms uploaded successfully allFormsUploadedSuccessfully() should return true`() {\n        assertThat(FormsUploadResultInterpreter.allFormsUploadedSuccessfully(resultWithoutErrors), `is`(true))\n    }\n\n    @Test\n    fun `When not all forms uploaded successfully allFormsUploadedSuccessfully() should return false`() {\n        assertThat(FormsUploadResultInterpreter.allFormsUploadedSuccessfully(resultWithOneError), `is`(false))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/HtmlUtilsTest.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\n@RunWith(AndroidJUnit4::class)\nclass HtmlUtilsTest {\n    @Test\n    fun `#textToHtml null becomes empty string`() {\n        val observed = HtmlUtils.textToHtml(null)\n        assertThat(observed, equalTo(\"\"))\n    }\n\n    @Test\n    fun `#textToHtml should be trimmed`() {\n        val observed = HtmlUtils.textToHtml(\"<p style=\\\"text-align:center\\\">Text</p>\")\n        assertThat(observed.toString(), equalTo(\"Text\"))\n    }\n\n    @Test\n    fun `#textToHtml ignores invalid styles`() {\n        var observed = HtmlUtils.textToHtml(\"<p style=>Text</p>\")\n        assertThat(observed.toString(), equalTo(\"Text\"))\n\n        observed = HtmlUtils.textToHtml(\"<span style=\\\"color:\\\">Text</span>\")\n        assertThat(observed.toString(), equalTo(\"Text\"))\n\n        observed = HtmlUtils.textToHtml(\"<span style=\\\"font-family\\\">Text</span>\")\n        assertThat(observed.toString(), equalTo(\"Text\"))\n    }\n\n    @Test\n    fun `#markDownToHtml escapes backslash`() {\n        val tests = arrayOf(\n            arrayOf(\"A\\\\_B\\\\_C\", \"A_B_C\"),\n            arrayOf(\"_A\\\\_B\\\\_C_\", \"<em>A_B_C</em>\"),\n            arrayOf(\"A_B\\\\_C\", \"A_B_C\"),\n            arrayOf(\"A\\\\_B_C\", \"A_B_C\"),\n            arrayOf(\"A_B_C\", \"A_B_C\"),\n            arrayOf(\"A _B_ C\", \"A <em>B</em> C\"),\n            arrayOf(\"A_B_ C\", \"A_B_ C\"),\n            arrayOf(\"A _B_C\", \"A _B_C\"),\n            arrayOf(\"_A_5\", \"_A_5\"),\n            arrayOf(\"_A_\", \"<em>A</em>\"),\n            arrayOf(\"(_A_)\", \"(<em>A</em>)\"),\n            arrayOf(\"_A_?\", \"<em>A</em>?\"),\n            arrayOf(\"*_A_\", \"*<em>A</em>\"),\n            arrayOf(\"blah _A_!\", \"blah <em>A</em>!\"),\n            arrayOf(\" _A_! blah\", \" <em>A</em>! blah\"),\n            arrayOf(\"\\\\_ _AB\\\\__\", \"_ <em>AB_</em>\"),\n            arrayOf(\"\\\\\\\\ _AB\\\\_\\\\\\\\_\", \"\\\\ <em>AB_\\\\</em>\"),\n            arrayOf(\"A\\\\*B\\\\*C\", \"A*B*C\"),\n            arrayOf(\"*A\\\\*B\\\\*C*\", \"<em>A*B*C</em>\"),\n            arrayOf(\"A*B\\\\*C\", \"A*B*C\"),\n            arrayOf(\"A*B*C\", \"A<em>B</em>C\"),\n            arrayOf(\"\\\\**AB\\\\**\", \"*<em>AB*</em>\"),\n            arrayOf(\"\\\\\\\\*AB\\\\*\\\\\\\\*\", \"\\\\<em>AB*\\\\</em>\"),\n            arrayOf(\"\\\\a\\\\ b\\\\*c\\\\d\\\\_e\", \"\\\\a\\\\ b*c\\\\d_e\"),\n            arrayOf(\"\\\\#1\", \"#1\"),\n            arrayOf(\"\\\\#\\\\# 2\", \"## 2\"),\n            arrayOf(\"works \\\\#when not required too\", \"works #when not required too\"),\n            arrayOf(\"\\\\\", \"\\\\\"),\n            arrayOf(\"\\\\\\\\\", \"\\\\\"),\n            arrayOf(\"\\\\\\\\\\\\\", \"\\\\\\\\\")\n        )\n\n        for (testCase in tests) {\n            assertThat(testCase[1], equalTo(HtmlUtils.markdownToHtml(testCase[0])))\n        }\n    }\n\n    @Test\n    fun `#markDownToHtml escapes less than`() {\n        val tests = arrayOf(\n            arrayOf(\"<1\", \"&lt;1\"),\n            arrayOf(\"<1>\", \"&lt;1>\"),\n            arrayOf(\"< span>\", \"&lt; span>\"),\n            arrayOf(\"< 1\", \"&lt; 1\"),\n            arrayOf(\"< 1/>\", \"&lt; 1/>\"),\n            arrayOf(\"test< 1/>\", \"test&lt; 1/>\"),\n            arrayOf(\"test < 1/>\", \"test &lt; 1/>\")\n        )\n        for (testCase in tests) {\n            assertThat(testCase[1], equalTo(HtmlUtils.markdownToHtml(testCase[0])))\n        }\n    }\n\n    @Test\n    fun `#markDownToHtml supports HTML`() {\n        val tests = arrayOf(\n            \"<span\",\n            \"<span>\",\n            \"<notarealtag>\",\n            \"<CAPSTAG\",\n            \"</closetag>\"\n        )\n        for (testCase in tests) {\n            assertThat(testCase, equalTo(HtmlUtils.markdownToHtml(testCase)))\n        }\n    }\n\n    @Test\n    fun `#textToHtml supports escaped lt`() {\n        val tests = arrayOf(\n            \"<1\",\n        )\n\n        for (testCase in tests) {\n            assertThat(testCase, equalTo(HtmlUtils.textToHtml(testCase).toString()))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.javarosa.core.model.instance.TreeElement\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoInteractions\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.widgets.QuestionWidget\nimport org.odk.collect.androidshared.bitmap.ImageCompressor\n\n@RunWith(AndroidJUnit4::class)\nclass ImageCompressionControllerTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n    private val formEntryPrompt = mock<FormEntryPrompt>()\n    private val questionWidget = mock<QuestionWidget>().also {\n        whenever(it.formEntryPrompt).thenReturn(formEntryPrompt)\n    }\n    private val treeElement = mock<TreeElement>().also {\n        whenever(it.attributeValue).thenReturn(\"123\")\n        whenever(it.name).thenReturn(\"max-pixels\")\n        whenever(it.namespace).thenReturn(ApplicationConstants.Namespaces.XML_OPENROSA_NAMESPACE)\n    }\n    private val imageCompressor = mock<ImageCompressor>()\n    private val imageCompressionController = ImageCompressionController(imageCompressor)\n\n    @Test\n    fun `when 'max-pixels' is not specified on a form level and expected image size in setting is 'original_image_size', image compression should not be triggered`() {\n        whenever(formEntryPrompt.bindAttributes).thenReturn(emptyList())\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"original_image_size\")\n\n        verifyNoInteractions(imageCompressor)\n    }\n\n    @Test\n    fun `when 'max-pixels' is not specified on a form level and expected image size in setting is 'very_small', image compression should be triggered for 640px`() {\n        whenever(formEntryPrompt.bindAttributes).thenReturn(emptyList())\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"very_small\")\n\n        verify(imageCompressor).execute(\"/path\", 640)\n    }\n\n    @Test\n    fun `when 'max-pixels' is not specified on a form level and expected image size in setting is 'small', image compression should be triggered for 1024px`() {\n        whenever(formEntryPrompt.bindAttributes).thenReturn(emptyList())\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"small\")\n\n        verify(imageCompressor).execute(\"/path\", 1024)\n    }\n\n    @Test\n    fun `when 'max-pixels' is not specified on a form level and expected image size in setting is 'medium', image compression should be triggered for 2048px`() {\n        whenever(formEntryPrompt.bindAttributes).thenReturn(emptyList())\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"medium\")\n\n        verify(imageCompressor).execute(\"/path\", 2048)\n    }\n\n    @Test\n    fun `when 'max-pixels' is not specified on a form level and expected image size in setting is 'large', image compression should be triggered for 3072px`() {\n        whenever(formEntryPrompt.bindAttributes).thenReturn(emptyList())\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"large\")\n\n        verify(imageCompressor).execute(\"/path\", 3072)\n    }\n\n    @Test\n    fun `when 'max-pixels' is specified on a form level and expected image size in setting is 'original_image_size', image compression should be triggered for value stored in 'max-pixels'`() {\n        whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement))\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"original_image_size\")\n\n        verify(imageCompressor).execute(\"/path\", 123)\n    }\n\n    @Test\n    fun `when 'max-pixels' is specified on a form level and expected image size in setting is 'very_small', image compression should be triggered and the value stored in for value stored in 'max-pixels' should take precedence`() {\n        whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement))\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"very_small\")\n\n        verify(imageCompressor).execute(\"/path\", 123)\n    }\n\n    @Test\n    fun `when 'max-pixels' is specified on a form level and expected image size in setting is 'small', image compression should be triggered and the value stored in for value stored in 'max-pixels' should take precedence`() {\n        whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement))\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"small\")\n\n        verify(imageCompressor).execute(\"/path\", 123)\n    }\n\n    @Test\n    fun `when 'max-pixels' is specified on a form level and expected image size in setting is 'medium', image compression should be triggered and the value stored in for value stored in 'max-pixels' should take precedence`() {\n        whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement))\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"medium\")\n\n        verify(imageCompressor).execute(\"/path\", 123)\n    }\n\n    @Test\n    fun `when 'max-pixels' is specified on a form level and expected image size in setting is 'large', image compression should be triggered and the value stored in for value stored in 'max-pixels' should take precedence`() {\n        whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement))\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"large\")\n\n        verify(imageCompressor).execute(\"/path\", 123)\n    }\n\n    @Test\n    fun `when 'max-pixels' is specified on a form level but it is not a valid integer value and expected image size in setting is 'original_image_size', image compression should not be triggered`() {\n        whenever(treeElement.attributeValue).thenReturn(\"123.5\")\n        whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement))\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"original_image_size\")\n\n        verifyNoInteractions(imageCompressor)\n    }\n\n    @Test\n    fun `when 'max-pixels' is specified on a form level but it is not a valid integer value and expected image size in setting is 'very_small', image compression should be triggered for 640px`() {\n        whenever(treeElement.attributeValue).thenReturn(\"123.5\")\n        whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement))\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"very_small\")\n\n        verify(imageCompressor).execute(\"/path\", 640)\n    }\n\n    @Test\n    fun `when 'max-pixels' is specified on a form level but it is not a valid integer value and expected image size in setting is 'small', image compression should be triggered for 1024px`() {\n        whenever(treeElement.attributeValue).thenReturn(\"123.5\")\n        whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement))\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"small\")\n\n        verify(imageCompressor).execute(\"/path\", 1024)\n    }\n\n    @Test\n    fun `when 'max-pixels' is specified on a form level but it is not a valid integer value and expected image size in setting is 'medium', image compression should be triggered for 2048px`() {\n        whenever(treeElement.attributeValue).thenReturn(\"123.5\")\n        whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement))\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"medium\")\n\n        verify(imageCompressor).execute(\"/path\", 2048)\n    }\n\n    @Test\n    fun `when 'max-pixels' is specified on a form level but it is not a valid integer value and expected image size in setting is 'large', image compression should be triggered for 3072px`() {\n        whenever(treeElement.attributeValue).thenReturn(\"123.5\")\n        whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement))\n\n        imageCompressionController.execute(\"/path\", questionWidget, context, \"large\")\n\n        verify(imageCompressor).execute(\"/path\", 3072)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/InstanceAutoDeleteCheckerTest.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport org.junit.Assert.assertFalse\nimport org.junit.Assert.assertTrue\nimport org.junit.Test\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.formstest.FormUtils\nimport org.odk.collect.formstest.InMemFormsRepository\nimport org.odk.collect.shared.TempFiles\n\nclass InstanceAutoDeleteCheckerTest {\n    private val formsRepository = InMemFormsRepository()\n\n    @Test\n    fun `Instance should be deleted if auto-delete enabled in project settings and not set on a form level`() {\n        val instance = saveInstance()\n        assertTrue(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, true, instance))\n    }\n\n    @Test\n    fun `Instance should be deleted if auto-delete enabled in project settings and set on a form level but with an unsupported value`() {\n        val instance = saveInstance(\"anything\")\n        assertTrue(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, true, instance))\n    }\n\n    @Test\n    fun `Instance should not be deleted if auto-delete enabled in project settings but disabled on a form level`() {\n        val instance = saveInstance(\"false\")\n        assertFalse(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, true, instance))\n    }\n\n    @Test\n    fun `Disabling aut-delete should not be case sensitive`() {\n        val instance = saveInstance(\" FaLsE \")\n        assertFalse(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, true, instance))\n    }\n\n    @Test\n    fun `Instance should be deleted if auto-delete enabled in project settings and on a form level`() {\n        val instance = saveInstance(\"true\")\n        assertTrue(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, true, instance))\n    }\n\n    @Test\n    fun `Instance should be deleted if auto-delete disabled in project settings but enabled on a form level`() {\n        val instance = saveInstance(\"true\")\n        assertTrue(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, false, instance))\n    }\n\n    @Test\n    fun `Enabling auto-delete should not be case sensitive`() {\n        val instance = saveInstance(\" TrUe \")\n        assertTrue(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, false, instance))\n    }\n\n    @Test\n    fun `Instance should not be deleted if auto-delete disabled in project settings and disabled on a form level`() {\n        val instance = saveInstance(\"false\")\n        assertFalse(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, false, instance))\n    }\n\n    @Test\n    fun `Instance should not be deleted if auto-delete disabled in project settings and not set on a form level`() {\n        val instance = saveInstance()\n        assertFalse(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, false, instance))\n    }\n\n    @Test\n    fun `Instance should not be deleted if auto-delete disabled in project settings and set on a form level with unsupported value`() {\n        val instance = saveInstance(\"anything\")\n        assertFalse(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, false, instance))\n    }\n\n    @Test\n    fun `Only instances of a form version with auto-delete enabled should be deleted`() {\n        val formV1 = formsRepository.save(\n            Form.Builder()\n                .formId(\"1\")\n                .version(\"1\")\n                .autoDelete(\"false\")\n                .formFilePath(FormUtils.createXFormFile(\"1\", \"1\").absolutePath)\n                .build()\n        )\n\n        val instanceV1 = Instance.Builder()\n            .formId(formV1.formId)\n            .formVersion(formV1.version)\n            .instanceFilePath(TempFiles.createTempDir().absolutePath)\n            .build()\n\n        val formV2 = formsRepository.save(\n            Form.Builder(formV1)\n                .version(\"2\")\n                .autoDelete(\"true\")\n                .build()\n        )\n\n        val instanceV2 = Instance.Builder()\n            .formId(formV2.formId)\n            .formVersion(formV2.version)\n            .instanceFilePath(TempFiles.createTempDir().absolutePath)\n            .build()\n\n        assertFalse(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, false, instanceV1))\n        assertTrue(InstanceAutoDeleteChecker.shouldInstanceBeDeleted(formsRepository, false, instanceV2))\n    }\n\n    private fun saveInstance(autoDelete: String = \"\"): Instance {\n        val form = formsRepository.save(\n            Form.Builder()\n                .formId(\"1\")\n                .version(\"1\")\n                .autoDelete(autoDelete)\n                .formFilePath(FormUtils.createXFormFile(\"1\", \"1\").absolutePath)\n                .build()\n        )\n\n        return Instance.Builder()\n            .formId(form.formId)\n            .formVersion(form.version)\n            .instanceFilePath(TempFiles.createTempDir().absolutePath)\n            .build()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/InstanceUploaderUtilsTest.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.formstest.InMemInstancesRepository;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\n\n@RunWith(AndroidJUnit4.class)\npublic class InstanceUploaderUtilsTest {\n    /**\n     * 1000 instances is a big number that would product a very long sql query that would cause\n     * SQLiteException: Expression tree is too large if we didn't split it into parts.\n     */\n    private static final int NUMBER_OF_INSTANCES_TO_SEND = 1000;\n\n    @Test\n    public void getUploadResultMessageContainsEditNumbers() {\n        InMemInstancesRepository instancesRepository = new InMemInstancesRepository();\n        Instance originalInstance = new Instance.Builder()\n                .dbId(1L)\n                .displayName(\"InstanceTest\")\n                .formId(\"instanceTest\")\n                .status(Instance.STATUS_COMPLETE)\n                .build();\n\n        Instance editedInstance = new Instance.Builder(originalInstance)\n                .dbId(originalInstance.getDbId() + 1)\n                .editOf(originalInstance.getDbId())\n                .editNumber(1L)\n                .build();\n\n        instancesRepository.save(originalInstance);\n        instancesRepository.save(editedInstance);\n\n        String uploadResult = InstanceUploaderUtils.getUploadResultMessage(\n                instancesRepository,\n                ApplicationProvider.getApplicationContext(),\n                getTestUploadResult()\n        );\n        assertThat(uploadResult, is(\"InstanceTest - Success\\n\\nInstanceTest (Edit 1) - Success\"));\n    }\n\n    @Test\n    public void getUploadResultMessageTest() {\n        assertThat(InstanceUploaderUtils.getUploadResultMessage(\n                getTestInstancesRepository(),\n                        ApplicationProvider.getApplicationContext(),\n                        getTestUploadResult()\n                ),\n                is(getExpectedResultMsg()));\n    }\n\n    private InMemInstancesRepository getTestInstancesRepository() {\n        InMemInstancesRepository instancesRepository = new InMemInstancesRepository();\n\n        for (int i = 1; i <= NUMBER_OF_INSTANCES_TO_SEND; i++) {\n            long time = System.currentTimeMillis();\n            Instance instance = new Instance.Builder()\n                    .dbId((long) i)\n                    .displayName(\"InstanceTest\")\n                    .formId(\"instanceTest\")\n                    .status(Instance.STATUS_COMPLETE)\n                    .lastStatusChangeDate(time)\n                    .build();\n\n            instancesRepository.save(instance);\n        }\n        return instancesRepository;\n    }\n\n    private Map<String, String> getTestUploadResult() {\n        Map<String, String> result = new HashMap<>();\n        for (int i = 1; i <= NUMBER_OF_INSTANCES_TO_SEND; i++) {\n            result.put(String.valueOf(i), \"full submission upload was successful!\");\n        }\n        return result;\n    }\n\n    private String getExpectedResultMsg() {\n        StringBuilder expectedResultMsg = new StringBuilder();\n        for (int i = 1; i <= NUMBER_OF_INSTANCES_TO_SEND; i++) {\n            expectedResultMsg.append(\"InstanceTest - Success\\n\\n\");\n        }\n        return expectedResultMsg.toString().trim();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/InstancesRepositoryProviderTest.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.startsWith\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.storage.StoragePaths\nimport org.odk.collect.formstest.InstanceUtils\nimport org.odk.collect.shared.TempFiles\n\n@RunWith(AndroidJUnit4::class)\nclass InstancesRepositoryProviderTest {\n\n    private val dbDir = TempFiles.createTempDir()\n    private val instancesDir = TempFiles.createTempDir()\n\n    @Test\n    fun `returned repository uses project directory when passed`() {\n        val context = ApplicationProvider.getApplicationContext<Application>()\n        val instancesRepositoryProvider = InstancesRepositoryProvider(context) {\n            StoragePaths(\n                \"\",\n                \"\",\n                instancesDir.absolutePath,\n                \"\",\n                dbDir.absolutePath,\n                \"\",\n                \"\"\n            )\n        }\n\n        val repository = instancesRepositoryProvider.create(\"projectId\")\n        val instance = repository.save(\n            InstanceUtils.buildInstance(\n                \"formId\",\n                \"formVersion\",\n                instancesDir.absolutePath\n            ).build()\n        )\n\n        assertThat(instance.instanceFilePath, startsWith(instancesDir.absolutePath))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/MediaUtilsTest.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.widget.Toast\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.argumentCaptor\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.BuildConfig\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.robolectric.shadows.ShadowToast\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass MediaUtilsTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n    private var intentLauncher = mock<IntentLauncher>()\n    private var contentUriProvider = mock<ContentUriProvider>()\n    private var mediaUtils = MediaUtils(intentLauncher, contentUriProvider)\n\n    @Test\n    fun `When file that we try to open does not exist a toast should be displayed`() {\n        val file = File(\"file://image.png\")\n\n        mediaUtils.openFile(context, file, \"image/*\")\n\n        assertThat(ShadowToast.getTextOfLatestToast(), `is`(\"File: file:/image.png is missing.\"))\n        assertThat(ShadowToast.getLatestToast().duration, `is`(Toast.LENGTH_LONG))\n        verify(intentLauncher, never()).launch(any(), any(), any())\n    }\n\n    @Test\n    fun `When URI for file we try to open is null a toast should be displayed`() {\n        val file = File.createTempFile(\"image\", \".png\")\n        whenever(\n            contentUriProvider.getUriForFile(\n                context,\n                BuildConfig.APPLICATION_ID + \".provider\",\n                file\n            )\n        ).thenReturn(null)\n\n        mediaUtils.openFile(context, file, \"image/*\")\n\n        assertThat(ShadowToast.getTextOfLatestToast(), `is`(\"Can't open file. If you are on a Huawei device, this is expected and will not be fixed.\"))\n        assertThat(ShadowToast.getLatestToast().duration, `is`(Toast.LENGTH_LONG))\n        verify(intentLauncher, never()).launch(any(), any(), any())\n    }\n\n    @Test\n    fun `When open file, launch on intentLauncher should be called with proper parameters`() {\n        val file = File.createTempFile(\"image\", \".png\")\n        whenever(\n            contentUriProvider.getUriForFile(\n                context,\n                BuildConfig.APPLICATION_ID + \".provider\",\n                file\n            )\n        ).thenReturn(\n            Uri.parse(\"content://org.odk.collect.android.provider/external_files/Android/data/org.odk.collect.android/files/image.png\")\n        )\n\n        val contextCaptor = argumentCaptor<Context>()\n        val intentCaptor = argumentCaptor<Intent>()\n        val runnableCaptor = argumentCaptor<() -> Unit>()\n\n        mediaUtils.openFile(context, file, \"image/*\")\n\n        verify(intentLauncher).launch(\n            contextCaptor.capture(),\n            intentCaptor.capture(),\n            runnableCaptor.capture()\n        )\n        assertThat(contextCaptor.firstValue, `is`(context))\n        assertThat(intentCaptor.firstValue.action, `is`(Intent.ACTION_VIEW))\n        assertThat(\n            intentCaptor.firstValue.data.toString(),\n            `is`(\"content://org.odk.collect.android.provider/external_files/Android/data/org.odk.collect.android/files/image.png\")\n        )\n        assertThat(intentCaptor.firstValue.type, `is`(\"image/*\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/MyanmarDateUtilsTest.java",
    "content": "/*\n * Copyright 2019 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.utilities;\n\nimport org.joda.time.DateTimeZone;\nimport org.joda.time.LocalDateTime;\nimport org.joda.time.tz.UTCProvider;\nimport org.junit.Test;\n\nimport mmcalendar.Language;\nimport mmcalendar.MyanmarCalendarKernel;\nimport mmcalendar.MyanmarDate;\n\nimport static org.junit.Assert.assertEquals;\n\n// Results confirmed with https://yan9a.github.io/mcal/\npublic class MyanmarDateUtilsTest {\n\n    @Test\n    public void convertDatesTest() {\n        DateTimeZone.setProvider(new UTCProvider());\n\n        LocalDateTime gregorianDateForConverting = new LocalDateTime()\n                .withYear(1900)\n                .withMonthOfYear(1)\n                .withDayOfMonth(28);\n\n        MyanmarDate myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1261, myanmarDate.getYearValue());\n        assertEquals(\"ပြာသို\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(29, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        int monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(1912)\n                .withMonthOfYear(2)\n                .withDayOfMonth(15);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1273, myanmarDate.getYearValue());\n        assertEquals(\"တပို့တွဲ\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(28, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(1924)\n                .withMonthOfYear(3)\n                .withDayOfMonth(7);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1285, myanmarDate.getYearValue());\n        assertEquals(\"တပေါင်း\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(3, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(1938)\n                .withMonthOfYear(4)\n                .withDayOfMonth(10);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1299, myanmarDate.getYearValue());\n        assertEquals(\"နှောင်းတန်ခူး\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(11, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(1944)\n                .withMonthOfYear(5)\n                .withDayOfMonth(29);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1306, myanmarDate.getYearValue());\n        assertEquals(\"နယုန်\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(8, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(1959)\n                .withMonthOfYear(6)\n                .withDayOfMonth(1);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1321, myanmarDate.getYearValue());\n        assertEquals(\"ကဆုန်\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(26, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(1963)\n                .withMonthOfYear(7)\n                .withDayOfMonth(11);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1325, myanmarDate.getYearValue());\n        assertEquals(\"ဝါဆို\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(21, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(1972)\n                .withMonthOfYear(8)\n                .withDayOfMonth(16);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1334, myanmarDate.getYearValue());\n        assertEquals(\"ဝါခေါင်\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(7, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(1986)\n                .withMonthOfYear(9)\n                .withDayOfMonth(20);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1348, myanmarDate.getYearValue());\n        assertEquals(\"တော်သလင်း\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(17, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(1991)\n                .withMonthOfYear(10)\n                .withDayOfMonth(20);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1353, myanmarDate.getYearValue());\n        assertEquals(\"သီတင်းကျွတ်\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(12, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(2000)\n                .withMonthOfYear(11)\n                .withDayOfMonth(25);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1362, myanmarDate.getYearValue());\n        assertEquals(\"တန်ဆောင်မုန်း\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(30, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(2013)\n                .withMonthOfYear(12)\n                .withDayOfMonth(30);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1375, myanmarDate.getYearValue());\n        assertEquals(\"နတ်တော်\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(28, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(2027)\n                .withMonthOfYear(1)\n                .withDayOfMonth(2);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1388, myanmarDate.getYearValue());\n        assertEquals(\"နတ်တော်\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(24, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(2033)\n                .withMonthOfYear(2)\n                .withDayOfMonth(12);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1394, myanmarDate.getYearValue());\n        assertEquals(\"တပို့တွဲ\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(13, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(2048)\n                .withMonthOfYear(3)\n                .withDayOfMonth(15);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1409, myanmarDate.getYearValue());\n        assertEquals(\"နှောင်းတန်ခူး\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(2, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(2059)\n                .withMonthOfYear(4)\n                .withDayOfMonth(21);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1421, myanmarDate.getYearValue());\n        assertEquals(\"တန်ခူး\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(9, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(2064)\n                .withMonthOfYear(5)\n                .withDayOfMonth(24);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1426, myanmarDate.getYearValue());\n        assertEquals(\"နယုန်\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(10, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(2077)\n                .withMonthOfYear(6)\n                .withDayOfMonth(4);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1439, myanmarDate.getYearValue());\n        assertEquals(\"နယုန်\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(14, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(2085)\n                .withMonthOfYear(7)\n                .withDayOfMonth(19);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1447, myanmarDate.getYearValue());\n        assertEquals(\"ဝါဆို\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(28, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n\n        gregorianDateForConverting = new LocalDateTime()\n                .withYear(2097)\n                .withMonthOfYear(8)\n                .withDayOfMonth(22);\n\n        myanmarDate = MyanmarDateUtils.gregorianDateToMyanmarDate(gregorianDateForConverting);\n        assertEquals(1459, myanmarDate.getYearValue());\n        assertEquals(\"ဝါခေါင်\", myanmarDate.getMonthName(Language.MYANMAR));\n        assertEquals(15, myanmarDate.getDayOfMonth());\n\n        assertGregorianDatesAreEqual(gregorianDateForConverting, MyanmarDateUtils.myanmarDateToGregorianDate(myanmarDate));\n        monthIndex = MyanmarCalendarKernel.calculateRelatedMyanmarMonths(myanmarDate.getYearValue(), 1).getMonthList().get(MyanmarDateUtils.getMonthId(myanmarDate));\n        assertMyanmarDatesAreEqual(myanmarDate, MyanmarDateUtils.createMyanmarDate(myanmarDate.getYearValue(), monthIndex, myanmarDate.getDayOfMonth()));\n    }\n\n    private void assertGregorianDatesAreEqual(LocalDateTime firstDate, LocalDateTime secondDate) {\n        assertEquals(firstDate.getYear(), secondDate.getYear());\n        assertEquals(firstDate.getMonthOfYear(), secondDate.getMonthOfYear());\n        assertEquals(firstDate.getDayOfMonth(), secondDate.getDayOfMonth());\n    }\n\n    private void assertMyanmarDatesAreEqual(MyanmarDate firstDate, MyanmarDate secondDate) {\n        assertEquals(firstDate.getYearValue(), secondDate.getYearValue());\n        assertEquals(firstDate.getMonth(), secondDate.getMonth());\n        assertEquals(firstDate.getYearValue(), secondDate.getYearValue());\n    }\n\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/QuestionFontSizeUtilsTest.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_FONT_SIZE;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.TestSettingsProvider;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\n\n@RunWith(AndroidJUnit4.class)\npublic class QuestionFontSizeUtilsTest {\n\n    @Test\n    public void whenFontSizeNotSpecified_shouldReturnDefaultValue() {\n        assertThat(QuestionFontSizeUtils.getQuestionFontSize(), is(QuestionFontSizeUtils.DEFAULT_FONT_SIZE - 1));\n    }\n\n    @Test\n    public void whenFontSizeSpecified_shouldReturnSelectedValue() {\n        TestSettingsProvider.getUnprotectedSettings().save(KEY_FONT_SIZE, \"30\");\n        assertThat(QuestionFontSizeUtils.getQuestionFontSize(), is(29));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt",
    "content": "package org.odk.collect.android.utilities\n\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.core.services.transport.payload.ByteArrayPayload\nimport org.javarosa.form.api.FormEntryCaption\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.exception.JavaRosaException\nimport org.odk.collect.android.formentry.audit.AuditEventLogger\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.javarosawrapper.InstanceMetadata\nimport org.odk.collect.android.javarosawrapper.SuccessValidationResult\nimport org.odk.collect.android.javarosawrapper.ValidationResult\nimport org.odk.collect.entities.javarosa.finalization.EntitiesExtra\nimport java.io.File\n\nopen class StubFormController : FormController {\n    override fun getFormDef(): FormDef? = null\n\n    override fun getMediaFolder(): File? = null\n\n    override fun getInstanceFile(): File? = null\n\n    override fun setInstanceFile(instanceFile: File?) {}\n\n    override fun getAbsoluteInstancePath(): String? = null\n\n    override fun getLastSavedPath(): String? = null\n\n    override fun getIndexWaitingForData(): FormIndex? = null\n\n    override fun setIndexWaitingForData(index: FormIndex?) {}\n\n    override fun getAuditEventLogger(): AuditEventLogger? = null\n\n    override fun isEditing(): Boolean {\n        return false\n    }\n\n    override fun getXPath(index: FormIndex?): String? = null\n\n    override fun getIndexFromXPath(xpath: String): FormIndex? = null\n\n    override fun getEvent(): Int = -1\n\n    override fun getEvent(index: FormIndex?): Int = -1\n\n    override fun getFormIndex(): FormIndex = FormIndex.createBeginningOfFormIndex()\n\n    override fun getLanguage(): String? = null\n\n    override fun getLanguages(): Array<String>? = null\n\n    override fun setLanguage(language: String?) {}\n\n    override fun getFormTitle(): String? = null\n\n    override fun getCaptionPrompt(): FormEntryCaption? = null\n\n    override fun getCaptionPrompt(index: FormIndex?): FormEntryCaption? = null\n\n    override fun finalizeForm() {}\n\n    override fun usesDatabaseExternalDataFeature(index: FormIndex): Boolean = false\n\n    override fun indexIsInFieldList(): Boolean = false\n\n    override fun indexIsInFieldList(index: FormIndex?): Boolean = false\n\n    override fun currentPromptIsQuestion(): Boolean = false\n\n    override fun isCurrentQuestionFirstInForm(): Boolean = false\n\n    override fun answerQuestion(index: FormIndex?, data: IAnswerData?): Int = -1\n\n    override fun validateAnswerConstraint(index: FormIndex, answer: IAnswerData?): ValidationResult = SuccessValidationResult\n\n    @Throws(JavaRosaException::class)\n    override fun validateAnswers(moveToInvalidIndex: Boolean): ValidationResult = SuccessValidationResult\n\n    override fun saveAnswer(index: FormIndex?, data: IAnswerData?): Boolean = false\n\n    override fun stepToNextEvent(stepIntoGroup: Boolean): Int = -1\n\n    override fun stepToPreviousEvent(): Int = -1\n\n    override fun jumpToIndex(index: FormIndex?): Int = -1\n\n    override fun stepOverGroup(): Int = -1\n\n    @Throws(JavaRosaException::class)\n    override fun stepToPreviousScreenEvent(): Int = -1\n\n    @Throws(JavaRosaException::class)\n    override fun stepToNextScreenEvent(): Int = -1\n\n    override fun stepToOuterScreenEvent(): Int = -1\n\n    override fun jumpToNewRepeatPrompt() {}\n\n    override fun isDisplayableGroup(index: FormIndex?): Boolean = false\n\n    override fun saveOneScreenAnswer(\n        index: FormIndex?,\n        data: IAnswerData?,\n        evaluateConstraints: Boolean\n    ): ValidationResult = SuccessValidationResult\n\n    @Throws(JavaRosaException::class)\n    override fun saveAllScreenAnswers(\n        answers: HashMap<FormIndex, IAnswerData>?,\n        evaluateConstraints: Boolean\n    ): ValidationResult = SuccessValidationResult\n\n    override fun newRepeat() {}\n\n    override fun deleteRepeat() {}\n\n    override fun getQuestionPrompt(): FormEntryPrompt? = null\n\n    override fun getQuestionPrompt(index: FormIndex?): FormEntryPrompt? = null\n\n    override fun getQuestionPrompts(index: FormIndex): Array<FormEntryPrompt> = emptyArray()\n\n    override fun getQuestionPromptConstraintText(index: FormIndex?): String? = null\n\n    override fun getQuestionPromptRequiredText(index: FormIndex?): String? = null\n\n    override fun currentCaptionPromptIsQuestion(): Boolean = false\n\n    override fun isGroupRelevant(): Boolean = false\n\n    override fun getGroupsForIndex(formIndex: FormIndex?): Array<FormEntryCaption> = emptyArray()\n\n    override fun indexContainsRepeatableGroup(): Boolean = false\n\n    override fun indexContainsRepeatableGroup(formIndex: FormIndex?): Boolean = false\n\n    override fun getLastRepeatedGroupRepeatCount(): Int = -1\n\n    override fun getLastRepeatedGroupName(): String? = null\n\n    override fun getLastGroupText(): String? = null\n\n    override fun isSubmissionEntireForm(): Boolean = false\n\n    override fun getFilledInFormXml(): ByteArrayPayload = ByteArrayPayload()\n\n    override fun getSubmissionXml(): ByteArrayPayload? = null\n\n    override fun getSubmissionMetadata(): InstanceMetadata? = null\n\n    override fun currentFormAuditsLocation(): Boolean = false\n\n    override fun currentFormCollectsBackgroundLocation(): Boolean = false\n\n    override fun getAnswer(treeReference: TreeReference?): IAnswerData? = null\n\n    override fun getEntities(): EntitiesExtra? = null\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/utilities/WebCredentialsUtilsTest.java",
    "content": "package org.odk.collect.android.utilities;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\nimport org.junit.Test;\nimport org.odk.collect.metadata.PropertyManager;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.shared.settings.Settings;\n\npublic class WebCredentialsUtilsTest {\n\n    @Test\n    public void saveCredentialsPreferencesMethod_shouldSaveNewCredentialsAndReloadPropertyManager() {\n        Settings generalSettings = mock(Settings.class);\n        WebCredentialsUtils webCredentialsUtils = new WebCredentialsUtils(generalSettings);\n        PropertyManager propertyManager = mock(PropertyManager.class);\n\n        webCredentialsUtils.saveCredentialsPreferences(\"username\", \"password\", propertyManager);\n\n        verify(generalSettings, times(1)).save(ProjectKeys.KEY_USERNAME, \"username\");\n        verify(generalSettings, times(1)).save(ProjectKeys.KEY_PASSWORD, \"password\");\n        verify(propertyManager, times(1)).reload();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/version/VersionInformationTest.java",
    "content": "package org.odk.collect.android.version;\n\nimport org.junit.Test;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\n\n\npublic class VersionInformationTest {\n\n    @Test\n    public void getSemanticVersion_returnsFirstComponent() {\n        VersionInformation versionInformation = new VersionInformation(() -> \"777\");\n        assertThat(versionInformation.getSemanticVersion(), is(\"777\"));\n\n        versionInformation = new VersionInformation(() -> \"my-cool-version\");\n        assertThat(versionInformation.getSemanticVersion(), is(\"my\"));\n    }\n\n    @Test\n    public void getSemanticVersion_whenEmpty_returnsEmpty() {\n        VersionInformation versionInformation = new VersionInformation(() -> \"\");\n        assertThat(versionInformation.getSemanticVersion(), is(\"\"));\n    }\n\n    @Test\n    public void isRelease_whenDescriptionIsOneComponent_returnsTrue() {\n        VersionInformation versionInformation = new VersionInformation(() -> \"blah\");\n        assertThat(versionInformation.isRelease(), is(true));\n    }\n\n    @Test\n    public void isRelease_whenDescriptionIsTwoComponents_returnsFalse() {\n        VersionInformation versionInformation = new VersionInformation(() -> \"something-blah\");\n        assertThat(versionInformation.isRelease(), is(false));\n    }\n}"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/views/ChoicesRecyclerViewTest.java",
    "content": "package org.odk.collect.android.views;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.instanceOf;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.testshared.RobolectricHelpers.populateRecyclerView;\nimport static java.util.Arrays.asList;\n\nimport android.app.Activity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CheckBox;\nimport android.widget.FrameLayout;\nimport android.widget.RadioButton;\nimport android.widget.TextView;\n\nimport androidx.recyclerview.widget.DividerItemDecoration;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport com.google.android.flexbox.FlexboxLayoutManager;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.javarosa.core.reference.InvalidReferenceException;\nimport org.javarosa.core.reference.Reference;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.adapters.AbstractSelectListAdapter;\nimport org.odk.collect.android.adapters.SelectMultipleListAdapter;\nimport org.odk.collect.android.adapters.SelectOneListAdapter;\nimport org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel;\nimport org.odk.collect.android.listeners.SelectItemClickListener;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.imageloader.ImageLoader;\nimport org.odk.collect.testshared.RobolectricHelpers;\nimport org.robolectric.android.controller.ActivityController;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@RunWith(AndroidJUnit4.class)\npublic class ChoicesRecyclerViewTest {\n    private ChoicesRecyclerView recyclerView;\n\n    private ActivityController<WidgetTestActivity> activityController;\n\n    private FormEntryPrompt formEntryPrompt;\n    private ReferenceManager referenceManager;\n\n    @Before\n    public void setUp() throws InvalidReferenceException {\n        activityController = CollectHelpers.buildThemedActivity(WidgetTestActivity.class);\n        Activity activity = activityController.get();\n        FrameLayout frameLayout = new FrameLayout(activity);\n        activity.setContentView(frameLayout);\n        activityController.create().start().visible();\n        recyclerView = new ChoicesRecyclerView(activity);\n        frameLayout.addView(recyclerView);\n        populateRecyclerView(recyclerView);\n        setUpReferenceManager();\n    }\n\n    @Test\n    public void whenNonFLexAppearanceIsUsed_shouldGridLayoutManagerBeUsed() {\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, null, null, new ArrayList<>(), null, null, null, 0, 1, false, mock(MediaUtils.class));\n        initRecyclerView(adapter, false);\n        assertThat(recyclerView.getLayoutManager().getClass().getName(), equalTo(GridLayoutManager.class.getName()));\n    }\n\n    @Test\n    public void whenFLexAppearanceIsUsed_shouldFlexboxLayoutManagerBeUsed() {\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, null, null, new ArrayList<>(), null, null, null, 0, 1, false, mock(MediaUtils.class));\n        initRecyclerView(adapter, true);\n        assertThat(recyclerView.getLayoutManager().getClass().getName(), equalTo(FlexboxLayoutManager.class.getName()));\n    }\n\n    @Test\n    public void whenNonFLexAppearanceIsUsedWithOneColumn_shouldDividersBeAdded() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, null, activityController.get(), items, formEntryPrompt, null, null, 0, 1, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        assertThat(recyclerView.getItemDecorationCount(), is(1));\n        assertThat(recyclerView.getItemDecorationAt(0), is(instanceOf(DividerItemDecoration.class)));\n    }\n\n    @Test\n    public void whenNonFLexAppearanceIsUsedWithMoreThanOneColumn_shouldNotDividersBeAdded() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, null, activityController.get(), items, formEntryPrompt, null, null, 0, 2, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        assertThat(recyclerView.getItemDecorationCount(), is(0));\n    }\n\n    @Test\n    public void whenFLexAppearanceIsUsed_shouldFlexItemDecorationBeAdded() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, null, activityController.get(), items, formEntryPrompt, null, null, 0, 2, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, true);\n\n        assertThat(recyclerView.getItemDecorationCount(), is(1));\n        assertThat(recyclerView.getItemDecorationAt(0), is(instanceOf(ChoicesRecyclerView.FlexItemDecoration.class)));\n    }\n\n    @Test\n    public void whenChoicesFiltered_shouldProperValuesBeReturnedInSelectOneButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, null, activityController.get(), items, formEntryPrompt, null, null, 0, 1, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        assertVisibleItemsInButtonsMode(\"AAA\", \"BBB\");\n        filterList(adapter, \"b\");\n        assertVisibleItemsInButtonsMode(\"BBB\");\n        filterList(adapter, \"bc\");\n        assertVisibleItemsInButtonsMode();\n        filterList(adapter, \"b\");\n        assertVisibleItemsInButtonsMode(\"BBB\");\n        filterList(adapter, \"\");\n        assertVisibleItemsInButtonsMode(\"AAA\", \"BBB\");\n    }\n\n    @Test\n    public void whenChoicesFiltered_shouldProperValuesBeReturnedInSelectMultiButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectMultipleListAdapter adapter = new SelectMultipleListAdapter(new ArrayList<>(), null, activityController.get(), items, formEntryPrompt, null, null, 0, 1, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        assertVisibleItemsInButtonsMode(\"AAA\", \"BBB\");\n        filterList(adapter, \"b\");\n        assertVisibleItemsInButtonsMode(\"BBB\");\n        filterList(adapter, \"bc\");\n        assertVisibleItemsInButtonsMode();\n        filterList(adapter, \"b\");\n        assertVisibleItemsInButtonsMode(\"BBB\");\n        filterList(adapter, \"\");\n        assertVisibleItemsInButtonsMode(\"AAA\", \"BBB\");\n    }\n\n    @Test\n    public void whenChoicesFiltered_shouldProperValuesBeReturnedInSelectOneNoButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"no-buttons\");\n\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, null, activityController.get(), items, formEntryPrompt, null, null, 0, 1, true, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        assertVisibleItemsInNoButtonsMode(\"AAA\", \"BBB\");\n        filterList(adapter, \"b\");\n        assertVisibleItemsInNoButtonsMode(\"BBB\");\n        filterList(adapter, \"bc\");\n        assertVisibleItemsInNoButtonsMode();\n        filterList(adapter, \"b\");\n        assertVisibleItemsInNoButtonsMode(\"BBB\");\n        filterList(adapter, \"\");\n        assertVisibleItemsInNoButtonsMode(\"AAA\", \"BBB\");\n    }\n\n    @Test\n    public void whenChoicesFiltered_shouldProperValuesBeReturnedInSelectMultiNoButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"no-buttons\");\n\n        SelectMultipleListAdapter adapter = new SelectMultipleListAdapter(new ArrayList<>(), null, activityController.get(), items, formEntryPrompt, null, null, 0, 1, true, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        assertVisibleItemsInNoButtonsMode(\"AAA\", \"BBB\");\n        filterList(adapter, \"b\");\n        assertVisibleItemsInNoButtonsMode(\"BBB\");\n        filterList(adapter, \"bc\");\n        assertVisibleItemsInNoButtonsMode();\n        filterList(adapter, \"b\");\n        assertVisibleItemsInNoButtonsMode(\"BBB\");\n        filterList(adapter, \"\");\n        assertVisibleItemsInNoButtonsMode(\"AAA\", \"BBB\");\n    }\n\n    @Test\n    public void whenClickOneOption_shouldPreviouslySelectedOptionBeUnselectedInSelectOneButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, listener, activityController.get(), items, formEntryPrompt, null, null, 0, 1, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Select AAA\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(false));\n\n        clickChoice(1); // Select BBB\n        assertThat(isItemSelected(0), is(false));\n        assertThat(isItemSelected(1), is(true));\n    }\n\n    @Test\n    public void whenClickOneOption_shouldPreviouslySelectedOptionRemainSelectedInSelectMultiButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectMultipleListAdapter adapter = new SelectMultipleListAdapter(new ArrayList<>(), listener, activityController.get(), items, formEntryPrompt, null, null, 0, 1, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Select AAA\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(false));\n\n        clickChoice(1); // Select BBB\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(true));\n    }\n\n    @Test\n    public void whenClickOneOption_shouldPreviouslySelectedOptionBeUnselectedInSelectOneNoButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"no-buttons\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, listener, activityController.get(), items, formEntryPrompt, null, mock(), 0, 1, true, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Select AAA\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(false));\n\n        clickChoice(1); // Select BBB\n        assertThat(isItemSelected(0), is(false));\n        assertThat(isItemSelected(1), is(true));\n    }\n\n    @Test\n    public void whenClickOneOption_shouldPreviouslySelectedOptionRemainSelectedInSelectMultiNoButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"no-buttons\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectMultipleListAdapter adapter = new SelectMultipleListAdapter(new ArrayList<>(), listener, activityController.get(), items, formEntryPrompt, null, mock(), 0, 1, true, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Select AAA\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(false));\n\n        clickChoice(1); // Select BBB\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(true));\n    }\n\n    @Test\n    public void whenClickOneElementTwice_shouldThatElementRemainSelectedInSelectOneButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, listener, activityController.get(), items, formEntryPrompt, null, null, 0, 1, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Select AAA\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(false));\n\n        clickChoice(0); // Select AAA again\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(false));\n    }\n\n    @Test\n    public void whenClickOneElementTwice_shouldThatElementBeUnselectedInSelectMultiButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectMultipleListAdapter adapter = new SelectMultipleListAdapter(new ArrayList<>(), listener, activityController.get(), items, formEntryPrompt, null, null, 0, 1, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Select AAA\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(false));\n\n        clickChoice(0); // Select AAA again\n        assertThat(isItemSelected(0), is(false));\n        assertThat(isItemSelected(1), is(false));\n    }\n\n    @Test\n    public void whenClickOneElementTwice_shouldThatElementRemainSelectedInSelectOneNoButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"no-buttons\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectOneListAdapter adapter = new SelectOneListAdapter(null, listener, activityController.get(), items, formEntryPrompt, null, mock(), 0, 1, true, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Select AAA\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(false));\n\n        clickChoice(0); // Select AAA again\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(false));\n    }\n\n    @Test\n    public void whenClickOneElementTwice_shouldThatElementBeUnselectedInSelectMultiNoButtonsMode() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"no-buttons\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectMultipleListAdapter adapter = new SelectMultipleListAdapter(new ArrayList<>(), listener, activityController.get(), items, formEntryPrompt, null, mock(), 0, 1, true, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Select AAA\n        assertThat(isItemSelected(0), is(true));\n        assertThat(isItemSelected(1), is(false));\n\n        clickChoice(0); // Select AAA again\n        assertThat(isItemSelected(0), is(false));\n        assertThat(isItemSelected(1), is(false));\n    }\n\n    @Test\n    public void whenButtonsModeIsUsed_shouldViewAndItsElementsBeLongClickableToSupportRemovingAnswers() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectMultipleListAdapter adapter = new SelectMultipleListAdapter(new ArrayList<>(), listener, activityController.get(), items, formEntryPrompt, null, null, 0, 1, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        AudioVideoImageTextLabel view = (AudioVideoImageTextLabel) getChoiceView(0);\n        File file = mock(File.class);\n        when(file.exists()).thenReturn(true);\n        view.setImage(file, mock(ImageLoader.class));\n        view.setVideo(file);\n        view.setAudio(\"file://audio.mp3\", mock());\n\n        assertThat(view.isLongClickable(), is(true));\n        assertThat(view.getImageView().isLongClickable(), is(true));\n        assertThat(view.getVideoButton().isLongClickable(), is(true));\n        assertThat(view.getAudioButton().isLongClickable(), is(true));\n        assertThat(view.getLabelTextView().isLongClickable(), is(true));\n    }\n\n    @Test\n    public void whenNoButtonsModeIsUsed_shouldViewBeLongClickableToSupportRemovingAnswers() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"no-buttons\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectMultipleListAdapter adapter = new SelectMultipleListAdapter(new ArrayList<>(), listener, activityController.get(), items, formEntryPrompt, null, null, 0, 1, true, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        assertThat(getChoiceView(0).isLongClickable(), is(true));\n    }\n\n    @Test\n    public void whenChangingAnswer_shouldHasAnswerChangedReturnCorrectValue() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        List<Selection> selectedItems = new ArrayList<>();\n        selectedItems.add(items.get(0).selection());\n        SelectMultipleListAdapter adapter = new SelectMultipleListAdapter(selectedItems, listener, activityController.get(), items, formEntryPrompt, null, null, 0, 1, false, mock(MediaUtils.class));\n\n        initRecyclerView(adapter, false);\n\n        clickChoice(1); // Select BBB\n        assertThat(adapter.hasAnswerChanged(), is(true));\n\n        clickChoice(0); // Unselect AAA\n        assertThat(adapter.hasAnswerChanged(), is(true));\n\n        clickChoice(1); // Unselect BBB\n        assertThat(adapter.hasAnswerChanged(), is(true));\n\n        clickChoice(0); // Select AAA\n        assertThat(adapter.hasAnswerChanged(), is(false));\n    }\n\n    @Test\n    public void whenChoiceSelectedInSelectOneNoButtonsMode_shouldTryToPlayAudio() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"no-buttons\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectOneListAdapter adapter = spy(new SelectOneListAdapter(null, listener, activityController.get(), items, formEntryPrompt, null, mock(), 0, 1, true, mock(MediaUtils.class)));\n\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Select AAA\n        verify(adapter).playAudio(any());\n    }\n\n    @Test\n    public void whenChoiceSelectedInSelectMultiNoButtonsMode_shouldTryToPlayAudio() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"no-buttons\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectMultipleListAdapter adapter = spy(new SelectMultipleListAdapter(new ArrayList<>(), listener, activityController.get(), items, formEntryPrompt, null, mock(), 0, 1, true, mock(MediaUtils.class)));\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Select AAA\n        verify(adapter).playAudio(any());\n    }\n\n    @Test\n    public void whenChoiceUnselectedInSelectMultiNoButtonsMode_shouldStopPlayingAudio() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"no-buttons\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        List<Selection> selectedItems = new ArrayList<>();\n        selectedItems.add(items.get(0).selection());\n        SelectMultipleListAdapter adapter = spy(new SelectMultipleListAdapter(selectedItems, listener, activityController.get(), items, formEntryPrompt, null, mock(), 0, 1, true, mock(MediaUtils.class)));\n        initRecyclerView(adapter, false);\n\n        clickChoice(0); // Unselect AAA\n        verify(adapter.getAudioPlayer()).stop();\n        verify(adapter, never()).playAudio(any());\n    }\n\n    @Test\n    public void whenColumnsPackAppearanceIsUsed_shouldMediaElementsBeHidden() {\n        List<SelectChoice> items = getTestChoices();\n        setUpFormEntryPrompt(items, \"columns-pack\");\n\n        SelectItemClickListener listener = mock(SelectItemClickListener.class);\n        SelectMultipleListAdapter adapter = spy(new SelectMultipleListAdapter(new ArrayList<>(), listener, activityController.get(), items, formEntryPrompt, referenceManager, null, 0, 1, false, mock(MediaUtils.class)));\n        initRecyclerView(adapter, true);\n\n        assertThat(getAudioVideoImageTextLabelView(0).getImageView().getVisibility(), is(View.GONE));\n        assertThat(getAudioVideoImageTextLabelView(0).getVideoButton().getVisibility(), is(View.GONE));\n        assertThat(getAudioVideoImageTextLabelView(0).getAudioButton().getVisibility(), is(View.GONE));\n    }\n\n    private void setUpReferenceManager() throws InvalidReferenceException {\n        referenceManager = mock(ReferenceManager.class);\n        Reference reference = mock(Reference.class);\n        when(reference.getLocalURI()).thenReturn(\"\");\n        when(referenceManager.deriveReference(any())).thenReturn(reference);\n    }\n\n    private List<SelectChoice> getTestChoices() {\n        return asList(\n                new SelectChoice(\"AAA\", \"AAA\"),\n                new SelectChoice(\"BBB\", \"BBB\")\n        );\n    }\n\n    private void setUpFormEntryPrompt(List<SelectChoice> items, String appearance) {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(items)\n                .withAppearance(appearance)\n                .build();\n    }\n\n    private void clickChoice(int index) {\n        if (Appearances.isNoButtonsAppearance(formEntryPrompt)) {\n            clickNoButtonChoice(index);\n        } else {\n            clickButtonChoice(index);\n        }\n    }\n\n    private void clickNoButtonChoice(int index) {\n        recyclerView.getChildAt(index).performClick();\n    }\n\n    private void clickButtonChoice(int index) {\n        ((AudioVideoImageTextLabel) getChoiceView(index)).getLabelTextView().performClick();\n    }\n\n    private void assertVisibleItemsInButtonsMode(String... items) {\n        assertThat(recyclerView.getAdapter().getItemCount(), is(items.length));\n        for (int i = 0; i < getVisibleItems().size(); i++) {\n            if (recyclerView.getAdapter() instanceof SelectOneListAdapter) {\n                assertThat(getRadioButton(i).getText().toString(), is(items[i]));\n            } else {\n                assertThat(getCheckBox(i).getText().toString(), is(items[i]));\n            }\n        }\n    }\n\n    private void assertVisibleItemsInNoButtonsMode(String... items) {\n        assertThat(recyclerView.getAdapter().getItemCount(), is(items.length));\n        for (int i = 0; i < getVisibleItems().size(); i++) {\n            assertThat(((TextView) getChoiceView(i).findViewById(R.id.label)).getText().toString(), is(items[i]));\n        }\n    }\n\n    private List<SelectChoice> getVisibleItems() {\n        return ((AbstractSelectListAdapter) recyclerView.getAdapter())\n                .getFilteredItems();\n    }\n\n    private RadioButton getRadioButton(int index) {\n        return (RadioButton) getAudioVideoImageTextLabelView(index).getLabelTextView();\n    }\n\n    private CheckBox getCheckBox(int index) {\n        return (CheckBox) getAudioVideoImageTextLabelView(index).getLabelTextView();\n    }\n\n    private ViewGroup getChoiceView(int index) {\n        return (ViewGroup) recyclerView.getChildAt(index);\n    }\n\n    private AudioVideoImageTextLabel getAudioVideoImageTextLabelView(int index) {\n        return (AudioVideoImageTextLabel) recyclerView.getChildAt(index);\n    }\n\n    private boolean isItemSelected(int index) {\n        return Appearances.isNoButtonsAppearance(formEntryPrompt)\n                ? isNoButtonItemSelected(index)\n                : isButtonItemSelected(index);\n    }\n\n    private boolean isNoButtonItemSelected(int index) {\n        return getChoiceView(index).getBackground() != null;\n    }\n\n    private boolean isButtonItemSelected(int index) {\n        return recyclerView.getAdapter() instanceof SelectOneListAdapter\n                ? getRadioButton(index).isChecked()\n                : getCheckBox(index).isChecked();\n    }\n\n    private void initRecyclerView(AbstractSelectListAdapter adapter, boolean isFlex) {\n        recyclerView.initRecyclerView(adapter, isFlex);\n        RobolectricHelpers.runLooper();\n    }\n\n    private void filterList(AbstractSelectListAdapter adapter, String text) {\n        adapter.getFilter().filter(text);\n        RobolectricHelpers.runLooper();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/views/TrackingTouchSliderTest.java",
    "content": "package org.odk.collect.android.views;\n\nimport android.widget.LinearLayout;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.core.view.MotionEventBuilder;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static android.view.MotionEvent.ACTION_DOWN;\nimport static android.view.MotionEvent.ACTION_UP;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\n\n@RunWith(AndroidJUnit4.class)\npublic class TrackingTouchSliderTest {\n\n    private TrackingTouchSlider slider;\n\n    @Before\n    public void setUp() {\n        ApplicationProvider.getApplicationContext().setTheme(com.google.android.material.R.style.Theme_MaterialComponents);\n        LinearLayout linearLayout = new LinearLayout(ApplicationProvider.getApplicationContext());\n        slider = new TrackingTouchSlider(ApplicationProvider.getApplicationContext(), null);\n\n        linearLayout.addView(slider);\n    }\n\n    @Test\n    public void touchEventOnSlider_suppressFlingGesture() {\n        slider.onTouchEvent(MotionEventBuilder.newBuilder().setAction(ACTION_UP).build());\n        assertThat(slider.isTrackingTouch(), equalTo(false));\n\n        slider.onTouchEvent(MotionEventBuilder.newBuilder().setAction(ACTION_DOWN).build());\n        assertThat(slider.isTrackingTouch(), equalTo(true));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/views/helpers/PromptAutoplayerTest.java",
    "content": "package org.odk.collect.android.views.helpers;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.odk.collect.android.support.CollectHelpers.setupFakeReferenceManager;\nimport static org.odk.collect.android.utilities.Appearances.COMPACT;\nimport static org.odk.collect.android.utilities.Appearances.MINIMAL;\nimport static org.odk.collect.android.utilities.Appearances.NO_BUTTONS;\nimport static java.util.Arrays.asList;\n\nimport androidx.core.util.Pair;\n\nimport org.javarosa.core.model.Constants;\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.odk.collect.android.formentry.media.PromptAutoplayer;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.audioclips.Clip;\n\nimport java.util.List;\n\npublic class PromptAutoplayerTest {\n\n    private final AudioPlayer audioPlayer = mock(AudioPlayer.class);\n\n    private PromptAutoplayer autoplayer;\n\n    @Before\n    public void setup() throws Exception {\n        ReferenceManager referenceManager = setupFakeReferenceManager(REFERENCES);\n        autoplayer = new PromptAutoplayer(audioPlayer, referenceManager);\n    }\n\n    @Test\n    public void whenPromptHasAutoplayAudio_playsAudio() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withAudioURI(REFERENCES.get(0).first)\n                .withAdditionalAttribute(\"autoplay\", \"audio\")\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(true));\n        verify(audioPlayer).playInOrder(asList(new Clip(prompt.getIndex().toString(), REFERENCES.get(0).second)));\n    }\n\n    @Test\n    public void whenPromptHasAutoplayAudio_withDifferentCapitalization_playsAudio() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withAudioURI(REFERENCES.get(0).first)\n                .withAdditionalAttribute(\"autoplay\", \"aUdio\")\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(true));\n        verify(audioPlayer).playInOrder(asList(new Clip(prompt.getIndex().toString(), REFERENCES.get(0).second)));\n    }\n\n    @Test\n    public void whenPromptHasAutoplayAudio_butNoAudioURI_returnsFalse() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withAudioURI(null)\n                .withAdditionalAttribute(\"autoplay\", \"audio\")\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(false));\n        verify(audioPlayer, never()).playInOrder(any());\n    }\n\n    @Test\n    public void whenPromptHasAutoplayAudio_andIsSelectOne_playsAudioInOrder() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(Constants.CONTROL_SELECT_ONE)\n                .withAudioURI(REFERENCES.get(0).first)\n                .withAdditionalAttribute(\"autoplay\", \"audio\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .withSpecialFormSelectChoiceText(asList(\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(1).first),\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(2).first)\n                ))\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(true));\n        verify(audioPlayer).playInOrder(asList(\n                new Clip(prompt.getIndex().toString(), REFERENCES.get(0).second),\n                new Clip(prompt.getIndex().toString() + \" 0\", REFERENCES.get(1).second),\n                new Clip(prompt.getIndex().toString() + \" 1\", REFERENCES.get(2).second)\n        ));\n    }\n\n    @Test\n    public void whenPromptHasAutoplayAudio_andIsSelectMultiple_playsAllAudioInOrder() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(Constants.CONTROL_SELECT_MULTI)\n                .withAudioURI(REFERENCES.get(0).first)\n                .withAdditionalAttribute(\"autoplay\", \"audio\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .withSpecialFormSelectChoiceText(asList(\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(1).first),\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(2).first)\n                ))\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(true));\n        verify(audioPlayer).playInOrder(asList(\n                new Clip(prompt.getIndex().toString(), REFERENCES.get(0).second),\n                new Clip(prompt.getIndex().toString() + \" 0\", REFERENCES.get(1).second),\n                new Clip(prompt.getIndex().toString() + \" 1\", REFERENCES.get(2).second)\n        ));\n    }\n\n    @Test\n    public void whenPromptHasAutoplayAudio_butNoAudioURI_andIsSelectOne_playsAllSelectAudioInOrder() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(Constants.CONTROL_SELECT_ONE)\n                .withAdditionalAttribute(\"autoplay\", \"audio\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .withSpecialFormSelectChoiceText(asList(\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(0).first),\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(1).first)\n                ))\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(true));\n        verify(audioPlayer).playInOrder(asList(\n                new Clip(prompt.getIndex().toString() + \" 0\", REFERENCES.get(0).second),\n                new Clip(prompt.getIndex().toString() + \" 1\", REFERENCES.get(1).second)\n        ));\n    }\n\n    @Test\n    public void whenPromptHasAutoplayAudio_andIsSelectOne_butNoSelectChoiceAudio_playsPromptAudio() throws Exception {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(Constants.CONTROL_SELECT_ONE)\n                .withAudioURI(REFERENCES.get(0).first)\n                .withAdditionalAttribute(\"autoplay\", \"audio\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(true));\n        verify(audioPlayer).playInOrder(asList(new Clip(prompt.getIndex().toString(), REFERENCES.get(0).second)));\n    }\n\n    @Test\n    public void whenPromptHasAutoplayAudio_andIsSelectOne_withMinimalAppearance_playsPromptAudio() throws Exception {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(Constants.CONTROL_SELECT_ONE)\n                .withAppearance(MINIMAL)\n                .withAudioURI(REFERENCES.get(0).first)\n                .withAdditionalAttribute(\"autoplay\", \"audio\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .withSpecialFormSelectChoiceText(asList(\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(1).first),\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(2).first)\n                ))\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(true));\n        verify(audioPlayer).playInOrder(asList(new Clip(prompt.getIndex().toString(), REFERENCES.get(0).second)));\n    }\n\n    @Test\n    public void whenPromptHasAutoplayAudio_andIsSelectOne_withNoButtonsAppearance_playsPromptAudio() throws Exception {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(Constants.CONTROL_SELECT_ONE)\n                .withAppearance(\"whatever \" + NO_BUTTONS)\n                .withAudioURI(REFERENCES.get(0).first)\n                .withAdditionalAttribute(\"autoplay\", \"audio\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .withSpecialFormSelectChoiceText(asList(\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(1).first),\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(2).first)\n                ))\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(true));\n        verify(audioPlayer).playInOrder(asList(new Clip(prompt.getIndex().toString(), REFERENCES.get(0).second)));\n    }\n\n    @Test\n    public void whenPromptHasAutoplayAudio_andIsSelectOne_withDeprecatedCompactAppearance_playsPromptAudio() throws Exception {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withControlType(Constants.CONTROL_SELECT_ONE)\n                .withAppearance(COMPACT)\n                .withAudioURI(REFERENCES.get(0).first)\n                .withAdditionalAttribute(\"autoplay\", \"audio\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .withSpecialFormSelectChoiceText(asList(\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(1).first),\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(2).first)\n                ))\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(true));\n        verify(audioPlayer).playInOrder(asList(new Clip(prompt.getIndex().toString(), REFERENCES.get(0).second)));\n    }\n\n    @Test // We only support audio autoplaying with the helper right now\n    public void whenPromptHasAutoplayVideo_returnsFalse() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withAdditionalAttribute(\"autoplay\", \"video\")\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(false));\n        verify(audioPlayer, never()).playInOrder(any());\n    }\n\n    @Test\n    public void whenPromptHasNoAutoplay_returnsFalse() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withAdditionalAttribute(\"autoplay\", null)\n                .build();\n\n        assertThat(autoplayer.autoplayIfNeeded(prompt), equalTo(false));\n        verify(audioPlayer, never()).playInOrder(any());\n    }\n\n    private static final List<Pair<String, String>> REFERENCES = asList(\n            new Pair<>(\"ref\", \"file://audio.mp3\"),\n            new Pair<>(\"ref1\", \"file://audio1.mp3\"),\n            new Pair<>(\"ref2\", \"file://audio2.mp3\")\n    );\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/AnnotateWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.content.Intent;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.provider.MediaStore;\nimport android.view.View;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.util.Pair;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.jetbrains.annotations.NotNull;\nimport org.junit.Test;\nimport org.odk.collect.android.R;\nimport org.odk.collect.draw.DrawActivity;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.widgets.base.FileWidgetTest;\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.support.SynchronousImageLoader;\nimport org.odk.collect.imageloader.ImageLoader;\nimport org.odk.collect.shared.TempFiles;\nimport org.robolectric.shadows.ShadowToast;\n\nimport java.io.File;\nimport java.io.IOException;\n\nimport static java.util.Collections.singletonList;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.support.CollectHelpers.overrideReferenceManager;\nimport static org.odk.collect.android.support.CollectHelpers.setupFakeReferenceManager;\nimport static org.robolectric.Shadows.shadowOf;\n\n/**\n *  @author James Knight\n */\npublic class AnnotateWidgetTest extends FileWidgetTest<AnnotateWidget> {\n\n    private File currentFile;\n    private FakeQuestionMediaManager questionMediaManager;\n\n    private final File file = TempFiles.createTempFile(\"sample\", \".jpg\");\n\n    @NonNull\n    @Override\n    public AnnotateWidget createWidget() {\n        questionMediaManager = new FakeQuestionMediaManager() {\n            @Override\n            public File getAnswerFile(String fileName) {\n                File result;\n                if (currentFile == null) {\n                    result = super.getAnswerFile(fileName);\n                } else {\n                    result = fileName.equals(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER) ? currentFile : null;\n                }\n                return result;\n            }\n        };\n        return new AnnotateWidget(activity,\n                new QuestionDetails(formEntryPrompt, readOnlyOverride),\n                questionMediaManager, new FakeWaitingForDataRegistry(), TempFiles.getPathInTempDir(), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public StringData getNextAnswer() {\n        return new StringData(file.getName());\n    }\n\n    @Override\n    public Object createBinaryData(@NotNull StringData answerData) {\n        return file;\n    }\n\n    @Test\n    public void buttonsShouldLaunchCorrectIntentsWhenThereIsNoCustomPackage() {\n        stubAllRuntimePermissionsGranted(true);\n\n        Intent intent = getIntentLaunchedByClick(R.id.capture_button);\n        assertActionEquals(MediaStore.ACTION_IMAGE_CAPTURE, intent);\n        assertThat(intent.getPackage(), equalTo(null));\n\n        intent = getIntentLaunchedByClick(R.id.choose_button);\n        assertActionEquals(Intent.ACTION_GET_CONTENT, intent);\n\n        intent = getIntentLaunchedByClick(R.id.annotate_button);\n        assertComponentEquals(activity, DrawActivity.class, intent);\n        assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_ANNOTATE, intent);\n    }\n\n    @Test\n    public void buttonsShouldLaunchCorrectIntentsWhenCustomPackageIsSet() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAdditionalAttribute(\"intent\", \"com.customcameraapp\")\n                .build();\n\n        stubAllRuntimePermissionsGranted(true);\n\n        Intent intent = getIntentLaunchedByClick(R.id.capture_button);\n        assertActionEquals(MediaStore.ACTION_IMAGE_CAPTURE, intent);\n        assertThat(intent.getPackage(), equalTo(\"com.customcameraapp\"));\n\n        intent = getIntentLaunchedByClick(R.id.choose_button);\n        assertActionEquals(Intent.ACTION_GET_CONTENT, intent);\n        assertTypeEquals(\"image/*\", intent);\n    }\n\n    @Test\n    public void buttonsShouldNotLaunchIntentsWhenPermissionsDenied() {\n        stubAllRuntimePermissionsGranted(false);\n\n        assertNull(getIntentLaunchedByClick(R.id.capture_button));\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n\n        assertThat(getSpyWidget().binding.captureButton.getVisibility(), is(View.GONE));\n        assertThat(getSpyWidget().binding.chooseButton.getVisibility(), is(View.GONE));\n        assertThat(getSpyWidget().binding.annotateButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisabled() {\n        readOnlyOverride = true;\n        when(formEntryPrompt.isReadOnly()).thenReturn(false);\n\n        assertThat(getSpyWidget().binding.captureButton.getVisibility(), is(View.GONE));\n        assertThat(getSpyWidget().binding.chooseButton.getVisibility(), is(View.GONE));\n        assertThat(getSpyWidget().binding.annotateButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_hideImageViewAndErrorMessage() {\n        AnnotateWidget widget = createWidget();\n\n        assertThat(widget.getImageView().getVisibility(), is(View.GONE));\n        assertThat(widget.getImageView().getDrawable(), nullValue());\n\n        assertThat(widget.getErrorTextView().getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenGifFileSelected_doNotAttachItAndDisplayAMessage() {\n        AnnotateWidget widget = createWidget();\n\n        File file = TempFiles.createTempFile(\"sample\", \".gif\");\n        questionMediaManager.addAnswerFile(file);\n        widget.setData(file);\n\n        assertThat(widget.getImageView().getVisibility(), is(View.GONE));\n        assertThat(widget.getImageView().getDrawable(), nullValue());\n\n        assertThat(ShadowToast.getTextOfLatestToast(), is(\"Gif files are not supported\"));\n    }\n\n    @Test\n    public void whenTheAnswerImageCanNotBeLoaded_hideImageViewAndShowErrorMessage() throws IOException {\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader(true);\n            }\n        });\n\n        String imagePath = File.createTempFile(\"current\", \".bmp\").getAbsolutePath();\n        currentFile = new File(imagePath);\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER)\n                .build();\n\n        AnnotateWidget widget = createWidget();\n\n        assertThat(widget.getImageView().getVisibility(), is(View.GONE));\n        assertThat(widget.getImageView().getDrawable(), nullValue());\n\n        assertThat(widget.getErrorTextView().getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenPromptHasDefaultAnswer_showsInImageView() throws Exception {\n        String imagePath = File.createTempFile(\"default\", \".bmp\").getAbsolutePath();\n\n        ReferenceManager referenceManager = setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, imagePath)\n        ));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        AnnotateWidget widget = createWidget();\n        ImageView imageView = widget.getImageView();\n        assertThat(imageView.getVisibility(), is(View.VISIBLE));\n        Drawable drawable = imageView.getDrawable();\n        assertThat(drawable, notNullValue());\n\n        String loadedPath = shadowOf(((BitmapDrawable) drawable).getBitmap()).getCreatedFromPath();\n        assertThat(loadedPath, equalTo(imagePath));\n    }\n\n    @Test\n    public void whenPromptHasCurrentAnswer_showsInImageView() throws Exception {\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        String imagePath = File.createTempFile(\"current\", \".bmp\").getAbsolutePath();\n        currentFile = new File(imagePath);\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER)\n                .build();\n\n        AnnotateWidget widget = createWidget();\n        ImageView imageView = widget.getImageView();\n        assertThat(imageView.getVisibility(), is(View.VISIBLE));\n        Drawable drawable = imageView.getDrawable();\n        assertThat(drawable, notNullValue());\n\n        String loadedPath = shadowOf(((BitmapDrawable) drawable).getBitmap()).getCreatedFromPath();\n        assertThat(loadedPath, equalTo(imagePath));\n    }\n\n    @Test\n    public void markupButtonShouldBeDisabledIfImageAbsent() throws Exception {\n        String badPath = \"bad_path\";\n        overrideReferenceManager(setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, badPath)\n        )));\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        assertThat(getWidget().binding.annotateButton.isEnabled(), is(false));\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER)\n                .build();\n\n        assertThat(getWidget().binding.annotateButton.isEnabled(), is(false));\n    }\n\n    @Test\n    public void whenPromptHasDefaultAnswer_passUriToDrawActivity() throws Exception {\n        File file = File.createTempFile(\"default\", \".bmp\");\n        String imagePath = file.getAbsolutePath();\n\n        ReferenceManager referenceManager = setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, imagePath)\n        ));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        Intent intent = getIntentLaunchedByClick(R.id.annotate_button);\n        assertComponentEquals(activity, DrawActivity.class, intent);\n        assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_ANNOTATE, intent);\n        assertExtraEquals(DrawActivity.REF_IMAGE, Uri.fromFile(file), intent);\n    }\n\n    @Test\n    public void whenPromptHasDefaultAnswerThatDoesNotExist_doNotPassUriToDrawActivity() throws Exception {\n        ReferenceManager referenceManager = setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, \"/something\")\n        ));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n        });\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        Intent intent = getIntentLaunchedByClick(R.id.annotate_button);\n        assertComponentEquals(activity, DrawActivity.class, intent);\n        assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_ANNOTATE, intent);\n        assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_doNotPassUriToDrawActivity() {\n        Intent intent = getIntentLaunchedByClick(R.id.annotate_button);\n        assertComponentEquals(activity, DrawActivity.class, intent);\n        assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_ANNOTATE, intent);\n        assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/ArbitraryFileWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.data.StringData\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.android.support.WidgetTestActivity\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.android.utilities.MediaUtils\nimport org.odk.collect.android.widgets.arbitraryfile.ArbitraryFileWidget\nimport org.odk.collect.android.widgets.base.FileWidgetTest\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidtest.onNodeWithClickLabel\nimport org.odk.collect.strings.R.string\nimport java.io.File\n\nclass ArbitraryFileWidgetTest : FileWidgetTest<ArbitraryFileWidget>() {\n    @get:Rule\n    val composeRule = createAndroidComposeRule<WidgetTestActivity>()\n    private val mediaUtils = mock<MediaUtils>().also {\n        whenever(it.isAudioFile(any())).thenReturn(true)\n    }\n    private val questionMediaManager = FakeQuestionMediaManager()\n    private val mediaWidgetAnswerViewModel = MediaWidgetAnswerViewModel(mock(), questionMediaManager, mediaUtils)\n    private val dependencies = QuestionWidget.Dependencies(\n        null,\n        mediaWidgetAnswerViewModel\n    )\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesMediaUtils(intentLauncher: IntentLauncher): MediaUtils {\n                return mediaUtils\n            }\n        })\n        formEntryPrompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_FILE_CAPTURE)\n            .build()\n    }\n\n    override fun getInitialAnswer(): StringData {\n        return StringData(\"document.pdf\")\n    }\n\n    override fun getNextAnswer(): StringData {\n        return StringData(\"document.xlsx\")\n    }\n\n    override fun createWidget(): ArbitraryFileWidget {\n        return ArbitraryFileWidget(\n            composeRule.activity,\n            QuestionDetails(formEntryPrompt, readOnlyOverride),\n            dependencies,\n            questionMediaManager,\n            FakeWaitingForDataRegistry()\n        ).also {\n            composeRule.activity.setContentView(it)\n            activity = composeRule.activity\n        }\n    }\n\n    @Test\n    fun `Display the answer text when there is answer`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(initialAnswer.displayText))\n            .build()\n        createWidget()\n        composeRule.onNodeWithText(initialAnswer.displayText).assertIsDisplayed()\n    }\n\n    @Test\n    fun `File picker should be called when clicking on button`() {\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.choose_file)).performClick()\n        verify(mediaUtils).pickFile(activity, \"*/*\", ApplicationConstants.RequestCodes.ARBITRARY_FILE_CHOOSER)\n    }\n\n    @Test\n    fun `File viewer should be called when clicking on answer`() {\n        val file = questionMediaManager.addAnswerFile(File.createTempFile(\"document\", \".pdf\"))\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(file.name))\n            .build()\n\n        createWidget()\n        composeRule.onNodeWithText(file.name).performClick()\n        verify(mediaUtils).openFile(activity, file, null)\n    }\n\n    @Test\n    fun `Hide the answer when clear answer is called`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(initialAnswer.displayText))\n            .build()\n\n        val widget = createWidget()\n        widget.clearAnswer()\n        composeRule.onNodeWithText(initialAnswer.displayText).assertDoesNotExist()\n    }\n\n    @Test\n    fun `All clickable elements should be disabled when read-only override option is used`() {\n        readOnlyOverride = true\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withReadOnly(false)\n            .withAnswer(StringData(initialAnswer.displayText))\n            .build()\n\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.choose_file)).assertDoesNotExist()\n        composeRule.onNodeWithText(initialAnswer.displayText).assertIsDisplayed()\n    }\n\n    @Test\n    override fun usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withReadOnly(true)\n            .withAnswer(StringData(initialAnswer.displayText))\n            .build()\n\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.choose_file)).assertDoesNotExist()\n        composeRule.onNodeWithText(initialAnswer.displayText).assertIsDisplayed()\n    }\n\n    @Test\n    override fun settingANewAnswerShouldCallDeleteMediaToRemoveTheOldFile() {\n        val file = questionMediaManager.addAnswerFile(File.createTempFile(\"document\", \".pdf\"))\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(file.name))\n            .build()\n\n        val widget = createWidget()\n        widget.setData(createBinaryData(nextAnswer))\n\n        assertThat(\n            questionMediaManager.originalFiles[formEntryPrompt.index.toString()],\n            equalTo(file.absolutePath)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/AudioWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static android.view.View.GONE;\nimport static android.view.View.VISIBLE;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnly;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.testshared.RobolectricHelpers.setupMediaPlayerDataSource;\nimport static org.robolectric.shadows.ShadowDialog.getLatestDialog;\n\nimport android.util.Pair;\nimport android.view.View;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.jetbrains.annotations.NotNull;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.audio.AudioControllerView;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.testshared.FakeAudioPlayer;\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager;\nimport org.odk.collect.android.widgets.utilities.AudioFileRequester;\nimport org.odk.collect.android.widgets.utilities.RecordingRequester;\nimport org.odk.collect.android.widgets.utilities.RecordingStatusHandler;\nimport org.odk.collect.audioclips.Clip;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport java.io.File;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n@RunWith(AndroidJUnit4.class)\npublic class AudioWidgetTest {\n\n    private final FakeQuestionMediaManager questionMediaManager = new FakeQuestionMediaManager();\n    private final FakeRecordingRequester recordingRequester = new FakeRecordingRequester();\n    private final AudioFileRequester audioFileRequester = mock(AudioFileRequester.class);\n\n    private WidgetTestActivity widgetActivity;\n    private FormIndex formIndex;\n    private FakeAudioPlayer audioPlayer;\n\n    @Before\n    public void setUp() throws Exception {\n        widgetActivity = CollectHelpers.buildThemedActivity(WidgetTestActivity.class).get();\n\n        formIndex = mock(FormIndex.class);\n        when(formIndex.toString()).thenReturn(\"questionIndex\");\n\n        audioPlayer = new FakeAudioPlayer();\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_showsButtons() {\n        AudioWidget widget = createWidget(promptWithAnswer(null));\n\n        assertThat(widget.binding.audioPlayer.audioController.getVisibility(), is(GONE));\n        assertThat(widget.binding.audioPlayer.recordingDuration.getVisibility(), is(GONE));\n        assertThat(widget.binding.audioPlayer.waveform.getVisibility(), is(GONE));\n        assertThat(widget.binding.recordAudioButton.getVisibility(), is(VISIBLE));\n        assertThat(widget.binding.chooseAudioButton.getVisibility(), is(VISIBLE));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_showsAudioController() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())));\n\n        assertThat(widget.binding.recordAudioButton.getVisibility(), is(GONE));\n        assertThat(widget.binding.chooseAudioButton.getVisibility(), is(GONE));\n        assertThat(widget.binding.audioPlayer.waveform.getVisibility(), is(GONE));\n        assertThat(widget.binding.audioPlayer.recordingDuration.getVisibility(), is(GONE));\n        assertThat(widget.binding.audioPlayer.audioController.getVisibility(), is(VISIBLE));\n    }\n\n    @Test\n    public void usingReadOnlyOption_doesNotShowCaptureAndChooseButtons() {\n        AudioWidget widget = createWidget(promptWithReadOnly());\n        assertThat(widget.binding.recordAudioButton.getVisibility(), equalTo(GONE));\n        assertThat(widget.binding.chooseAudioButton.getVisibility(), equalTo(GONE));\n    }\n\n    @Test\n    public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {\n        AudioWidget widget = createWidget(promptWithAnswer(null));\n        assertThat(widget.getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())));\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(answerFile.getName()));\n    }\n\n    @Test\n    public void whenWidgetIsNew_chooseSoundButtonIsNotShown() {\n        FormEntryPrompt prompt = promptWithReadOnly();\n        when(prompt.getAppearanceHint()).thenReturn(Appearances.NEW);\n        AudioWidget widget = createWidget(prompt);\n\n        assertThat(widget.binding.chooseAudioButton.getVisibility(), equalTo(GONE));\n    }\n\n    @Test\n    public void deleteFile_removesWidgetAnswerAndStopsPlayingMedia() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())));\n\n        widget.deleteFile();\n\n        assertThat(widget.getAnswer(), nullValue());\n        assertThat(audioPlayer.getCurrentClip(), nullValue());\n    }\n\n    @Test\n    public void deleteFile_setsFileAsideForDeleting() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        FormEntryPrompt prompt = promptWithAnswer(new StringData(answerFile.getName()));\n        when(prompt.getIndex()).thenReturn(formIndex);\n\n        AudioWidget widget = createWidget(prompt);\n        widget.deleteFile();\n\n        assertThat(questionMediaManager.originalFiles.get(\"questionIndex\"), equalTo(answerFile.getAbsolutePath()));\n    }\n\n    @Test\n    public void clearAnswer_removesAnswerAndHidesPlayer() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())));\n        widget.clearAnswer();\n\n        assertThat(widget.getAnswer(), nullValue());\n        assertThat(widget.binding.audioPlayer.audioController.getVisibility(), is(GONE));\n    }\n\n    @Test\n    public void clearAnswer_setsFileAsideForDeleting() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        FormEntryPrompt prompt = promptWithAnswer(new StringData(answerFile.getName()));\n        when(prompt.getIndex()).thenReturn(formIndex);\n\n        AudioWidget widget = createWidget(prompt);\n        widget.clearAnswer();\n\n        assertThat(questionMediaManager.originalFiles.get(\"questionIndex\"), equalTo(answerFile.getAbsolutePath()));\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListeners() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.clearAnswer();\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void setData_whenFileExists_replacesOriginalFileWithNewFile() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah1\", \".mp3\"));\n        FormEntryPrompt prompt = promptWithAnswer(new StringData(answerFile.getName()));\n        when(prompt.getIndex()).thenReturn(formIndex);\n        AudioWidget widget = createWidget(prompt);\n\n        File newFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah2\", \".mp3\"));\n        widget.setData(newFile);\n\n        assertThat(questionMediaManager.recentFiles.get(\"questionIndex\"), equalTo(newFile.getAbsolutePath()));\n    }\n\n    @Test\n    public void setData_whenPromptHasDifferentAudioFile_deletesOriginalAnswer() throws Exception {\n        File originalFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah1\", \".mp3\"));\n        FormEntryPrompt prompt = promptWithAnswer(new StringData(originalFile.getName()));\n        when(prompt.getIndex()).thenReturn(formIndex);\n\n        AudioWidget widget = createWidget(prompt);\n\n        File newFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah2\", \".mp3\"));\n        widget.setData(newFile);\n\n        assertThat(questionMediaManager.originalFiles.get(\"questionIndex\"), equalTo(originalFile.getAbsolutePath()));\n    }\n\n    @Test\n    public void setData_whenPromptDoesNotHaveAnswer_doesNotDeleteOriginalAnswer() throws Exception {\n        AudioWidget widget = createWidget(promptWithAnswer(null));\n        File newFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        widget.setData(newFile);\n\n        assertThat(questionMediaManager.originalFiles.isEmpty(), equalTo(true));\n    }\n\n    @Test\n    public void setData_whenFileDoesNotExist_doesNotUpdateWidgetAnswer() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())));\n        File newFile = new File(\"newFile.mp3\");\n        widget.setData(newFile);\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(answerFile.getName()));\n    }\n\n    @Test\n    public void setData_whenFileExists_updatesWidgetAnswer() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah1\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())));\n\n        File newFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah2\", \".mp3\"));\n        widget.setData(newFile);\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(newFile.getName()));\n    }\n\n    /**\n     * Currently choosing audio is locked into the {@link org.odk.collect.android.tasks.MediaLoadingTask}\n     * flow and so we'd need to rip this apart to let us drop support for accepting File as data. In\n     * this case it will just grab the name off the file and use {@link org.odk.collect.android.utilities.QuestionMediaManager}\n     * to handle grabbing the actual file\n     */\n    @Test\n    public void setData_supportsFilesAsWellAsStrings() throws Exception {\n        AudioWidget widget = createWidget(promptWithAnswer(null));\n\n        File newFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        widget.setData(newFile);\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(newFile.getName()));\n    }\n\n    @Test\n    public void setData_whenFileExists_callsValueChangeListener() throws Exception {\n        AudioWidget widget = createWidget(promptWithAnswer(null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n\n        File newFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        widget.setData(newFile);\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void setData_whenFileExists_hidesButtonsAndShowsAudioController() throws Exception {\n        AudioWidget widget = createWidget(promptWithAnswer(null));\n\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        widget.setData(answerFile);\n\n        assertThat(widget.binding.recordAudioButton.getVisibility(), is(GONE));\n        assertThat(widget.binding.recordAudioButton.getVisibility(), is(GONE));\n        assertThat(widget.binding.audioPlayer.audioController.getVisibility(), is(VISIBLE));\n    }\n\n    @Test\n    public void clickingButtonsForLong_callsOnLongClickListeners() {\n        View.OnLongClickListener listener = mock(View.OnLongClickListener.class);\n        AudioWidget widget = createWidget(promptWithAnswer(null));\n        widget.setOnLongClickListener(listener);\n\n        widget.binding.recordAudioButton.performLongClick();\n        widget.binding.chooseAudioButton.performLongClick();\n\n        verify(listener).onLongClick(widget.binding.recordAudioButton);\n        verify(listener).onLongClick(widget.binding.chooseAudioButton);\n    }\n\n    @Test\n    public void clickingChooseButton_requestsAudioFile() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        AudioWidget widget = createWidget(prompt);\n\n        widget.binding.chooseAudioButton.performClick();\n        verify(audioFileRequester).requestFile(prompt);\n    }\n\n    @Test\n    public void clickingCaptureButton_clearsWaveform() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        AudioWidget widget = createWidget(prompt);\n\n        recordingRequester.setAmplitude(prompt.getIndex().toString(), 11);\n        widget.binding.recordAudioButton.performClick();\n        assertThat(widget.binding.audioPlayer.waveform.getLatestAmplitude(), nullValue());\n    }\n\n    @Test\n    public void clickingCaptureButton_clearsError() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        AudioWidget widget = createWidget(prompt);\n        widget.displayError(\"Required question!\");\n\n        assertThat(widget.errorLayout.getVisibility(), equalTo(VISIBLE));\n        widget.binding.recordAudioButton.performClick();\n        assertThat(widget.errorLayout.getVisibility(), equalTo(GONE));\n    }\n\n    @Test\n    public void whenRecordingRequesterStopsRecording_enablesButtons() {\n        AudioWidget widget = createWidget(promptWithAnswer(null));\n\n        recordingRequester.startRecording();\n        assertThat(widget.binding.recordAudioButton.isEnabled(), is(false));\n        assertThat(widget.binding.chooseAudioButton.isEnabled(), is(false));\n\n        recordingRequester.stopRecording();\n        assertThat(widget.binding.recordAudioButton.isEnabled(), is(true));\n        assertThat(widget.binding.chooseAudioButton.isEnabled(), is(true));\n    }\n\n    @Test\n    public void whenRecordingInProgress_showsDurationAndWaveform() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        AudioWidget widget = createWidget(prompt);\n\n        recordingRequester.setDuration(prompt.getIndex().toString(), 0);\n        assertThat(widget.binding.recordAudioButton.getVisibility(), is(GONE));\n        assertThat(widget.binding.chooseAudioButton.getVisibility(), is(GONE));\n        assertThat(widget.binding.audioPlayer.audioController.getVisibility(), is(GONE));\n        assertThat(widget.binding.audioPlayer.recordingDuration.getVisibility(), is(VISIBLE));\n        assertThat(widget.binding.audioPlayer.waveform.getVisibility(), is(VISIBLE));\n    }\n\n    @Test\n    public void whenRecordingInProgress_updatesDuration() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        AudioWidget widget = createWidget(prompt);\n\n        recordingRequester.setDuration(prompt.getIndex().toString(), 0);\n        assertThat(widget.binding.audioPlayer.recordingDuration.getText(), is(\"00:00\"));\n\n        recordingRequester.setDuration(prompt.getIndex().toString(), 42000);\n        assertThat(widget.binding.audioPlayer.recordingDuration.getText(), is(\"00:42\"));\n    }\n\n    @Test\n    public void whenRecordingInProgress_updatesWaveform() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        AudioWidget widget = createWidget(prompt);\n\n        recordingRequester.setAmplitude(prompt.getIndex().toString(), 5);\n        assertThat(widget.binding.audioPlayer.waveform.getLatestAmplitude(), is(5));\n\n        recordingRequester.setAmplitude(prompt.getIndex().toString(), 67);\n        assertThat(widget.binding.audioPlayer.waveform.getLatestAmplitude(), is(67));\n    }\n\n    @Test\n    public void whenRecordingFinished_showsButtons() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        AudioWidget widget = createWidget(prompt);\n\n        recordingRequester.setDuration(prompt.getIndex().toString(), 5);\n        recordingRequester.reset();\n\n        assertThat(widget.binding.audioPlayer.audioController.getVisibility(), is(GONE));\n        assertThat(widget.binding.audioPlayer.recordingDuration.getVisibility(), is(GONE));\n        assertThat(widget.binding.audioPlayer.waveform.getVisibility(), is(GONE));\n        assertThat(widget.binding.recordAudioButton.getVisibility(), is(VISIBLE));\n        assertThat(widget.binding.chooseAudioButton.getVisibility(), is(VISIBLE));\n    }\n\n    @Test\n    public void afterSetBinaryData_clickingPlayAndPause_playsAndPausesAudio() throws Exception {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        AudioWidget widget = createWidget(prompt);\n\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        Clip expectedClip = getExpectedClip(prompt, answerFile.getName());\n        widget.setData(answerFile);\n\n        AudioControllerView audioController = widget.binding.audioPlayer.audioController;\n        assertThat(audioController.getVisibility(), is(VISIBLE));\n        audioController.binding.play.performClick();\n        assertThat(audioPlayer.getCurrentClip(), is(expectedClip));\n\n        audioController.binding.play.performClick();\n        assertThat(audioPlayer.getCurrentClip(), is(expectedClip));\n        assertThat(audioPlayer.isPlaying(), is(false));\n\n        audioController.binding.play.performClick();\n        assertThat(audioPlayer.getCurrentClip(), is(expectedClip));\n        assertThat(audioPlayer.isPlaying(), is(true));\n    }\n\n    @Test\n    public void afterSetBinaryData_whenPositionOfClipChanges_updatesPosition() throws Exception {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        Clip expectedClip = getExpectedClip(prompt, answerFile.getName());\n        setupMediaPlayerDataSource(expectedClip.getURI(), 322450);\n\n        AudioWidget widget = createWidget(prompt);\n        widget.setData(answerFile);\n\n        AudioControllerView audioController = widget.binding.audioPlayer.audioController;\n        assertThat(audioController.binding.currentDuration.getText().toString(), is(\"00:00\"));\n\n        audioPlayer.setPosition(expectedClip.getClipID(), 42000);\n        assertThat(audioController.binding.currentDuration.getText().toString(), is(\"00:42\"));\n    }\n\n    @Test\n    public void afterSetBinaryData_showsDurationOfAudio() throws Exception {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        Clip expectedClip = getExpectedClip(prompt, answerFile.getName());\n        setupMediaPlayerDataSource(expectedClip.getURI(), 322450);\n\n        AudioWidget widget = createWidget(prompt);\n        widget.setData(answerFile);\n\n        AudioControllerView audioController = widget.binding.audioPlayer.audioController;\n        assertThat(audioController.binding.totalDuration.getText().toString(), is(\"05:22\"));\n    }\n\n    @Test\n    public void clickingRemove_andConfirming_clearsAnswer() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())));\n        widget.binding.audioPlayer.audioController.binding.remove.performClick();\n\n        AlertDialog dialog = (AlertDialog) getLatestDialog();\n        dialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();\n        RobolectricHelpers.runLooper();\n        assertThat(widget.getAnswer(), nullValue());\n    }\n\n    @Test\n    public void clickingRemove_andCancelling_doesNothing() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())));\n        widget.binding.audioPlayer.audioController.binding.remove.performClick();\n\n        AlertDialog dialog = (AlertDialog) getLatestDialog();\n        dialog.getButton(AlertDialog.BUTTON_NEGATIVE).performClick();\n\n        assertThat(widget.getAnswer(), notNullValue());\n    }\n\n    @Test\n    public void clickingRemove_andConfirming_hidesAudioControllerAndShowsButtons() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())));\n        widget.binding.audioPlayer.audioController.binding.remove.performClick();\n\n        AlertDialog dialog = (AlertDialog) getLatestDialog();\n        dialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();\n        RobolectricHelpers.runLooper();\n        assertThat(widget.binding.audioPlayer.audioController.getVisibility(), is(GONE));\n        assertThat(widget.binding.recordAudioButton.getVisibility(), is(VISIBLE));\n        assertThat(widget.binding.chooseAudioButton.getVisibility(), is(VISIBLE));\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithReadOnlyAndAnswer(new StringData(answerFile.getName())));\n        widget.binding.audioPlayer.audioController.binding.remove.performClick();\n\n        assertThat(widget.binding.recordAudioButton.getVisibility(), is(View.GONE));\n        assertThat(widget.binding.chooseAudioButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisabled() throws Exception {\n        File answerFile = questionMediaManager.addAnswerFile(File.createTempFile(\"blah\", \".mp3\"));\n        AudioWidget widget = createWidget(promptWithAnswer(new StringData(answerFile.getName())), true);\n        widget.binding.audioPlayer.audioController.binding.remove.performClick();\n\n        assertThat(widget.binding.recordAudioButton.getVisibility(), is(View.GONE));\n        assertThat(widget.binding.chooseAudioButton.getVisibility(), is(View.GONE));\n    }\n\n    public AudioWidget createWidget(FormEntryPrompt prompt) {\n        return new AudioWidget(\n                widgetActivity,\n                new QuestionDetails(prompt),\n                questionMediaManager,\n                audioPlayer,\n                recordingRequester,\n                audioFileRequester,\n                recordingRequester,\n                widgetDependencies());\n    }\n\n    public AudioWidget createWidget(FormEntryPrompt prompt, boolean readOnlyOverride) {\n        return new AudioWidget(\n                widgetActivity,\n                new QuestionDetails(prompt, readOnlyOverride),\n                questionMediaManager,\n                audioPlayer,\n                recordingRequester,\n                audioFileRequester,\n                recordingRequester,\n                widgetDependencies());\n    }\n\n    @NotNull\n    private Clip getExpectedClip(FormEntryPrompt prompt, String fileName) {\n        return new Clip(\n                \"audio:\" + prompt.getIndex().toString(),\n                questionMediaManager.getAnswerFile(fileName).getAbsolutePath()\n        );\n    }\n\n    private static class FakeRecordingRequester implements RecordingRequester, RecordingStatusHandler {\n\n        FormEntryPrompt requestedRecordingFor;\n        private Consumer<Boolean> isRecordingListener;\n        private final Map<String, Consumer<Pair<Long, Integer>>> inProgressListeners = new HashMap<>();\n\n        @Override\n        public void requestRecording(FormEntryPrompt prompt) {\n            requestedRecordingFor = prompt;\n        }\n\n        @Override\n        public void onBlockedStatusChange(Consumer<Boolean> blockedStatusListener) {\n            this.isRecordingListener = blockedStatusListener;\n        }\n\n        @Override\n        public void onRecordingStatusChange(FormEntryPrompt prompt, Consumer<Pair<Long, Integer>> statusListener) {\n            inProgressListeners.put(prompt.getIndex().toString(), statusListener);\n        }\n\n        public void startRecording() {\n            isRecordingListener.accept(true);\n        }\n\n        public void stopRecording() {\n            isRecordingListener.accept(false);\n        }\n\n        public void setDuration(String sessionId, long duration) {\n            inProgressListeners.get(sessionId).accept(new Pair<>(duration, 0));\n        }\n\n        public void setAmplitude(String sessionId, int amplitude) {\n            inProgressListeners.get(sessionId).accept(new Pair<>(0L, amplitude));\n        }\n\n        public void reset() {\n            inProgressListeners.forEach((sessionId, listener) -> {\n                listener.accept(null);\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport net.bytebuddy.utility.RandomString\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.data.StringData\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.mockito.ArgumentMatchers\nimport org.mockito.Mockito.verify\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.fakes.FakePermissionsProvider\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.android.support.WidgetTestActivity\nimport org.odk.collect.android.utilities.Appearances\nimport org.odk.collect.android.widgets.barcode.BarcodeWidget\nimport org.odk.collect.android.widgets.base.QuestionWidgetTest\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers\nimport org.odk.collect.androidshared.system.CameraUtils\nimport org.odk.collect.androidtest.onNodeWithClickLabel\nimport org.odk.collect.strings.R.string\nimport org.robolectric.Shadows\nimport org.robolectric.shadows.ShadowToast\n\nclass BarcodeWidgetTest : QuestionWidgetTest<BarcodeWidget, StringData>() {\n    @get:Rule\n    val composeRule = createAndroidComposeRule<WidgetTestActivity>()\n    private val waitingForDataRegistry = FakeWaitingForDataRegistry()\n    private val permissionsProvider = FakePermissionsProvider().apply {\n        setPermissionGranted(true)\n    }\n    private val cameraUtils = mock<CameraUtils>()\n\n    override fun createWidget(): BarcodeWidget {\n        return BarcodeWidget(\n            composeRule.activity,\n            QuestionDetails(formEntryPrompt),\n            dependencies,\n            waitingForDataRegistry,\n            cameraUtils\n        ).also {\n            composeRule.activity.setContentView(it)\n            activity = composeRule.activity\n        }\n    }\n\n    override fun getNextAnswer(): StringData? {\n        return StringData(RandomString.make())\n    }\n\n    @Before\n    fun setup() {\n        formEntryPrompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_INPUT)\n            .withDataType(Constants.DATATYPE_BARCODE)\n            .build()\n    }\n\n    @Test\n    fun `The button is hidden in read-only mode`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withReadOnly(true)\n            .build()\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).assertDoesNotExist()\n    }\n\n    @Test\n    fun `Display the 'Replace Barcode' button if answer is present`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(\"blah\"))\n            .build()\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.replace_barcode)).assertIsDisplayed()\n    }\n\n    @Test\n    fun `Display the answer if answer is present`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(\"blah\"))\n            .build()\n        createWidget()\n        composeRule.onNodeWithText(\"blah\").assertIsDisplayed()\n    }\n\n    @Test\n    fun `#getAnswer returns null when there is no answer`() {\n        val widget = createWidget()\n        assertThat(widget.answer, equalTo(null))\n    }\n\n    @Test\n    fun `#getAnswer returns the answer when there is answer`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(\"blah\"))\n            .build()\n        val widget = createWidget()\n        assertThat(widget.answer!!.displayText, equalTo(\"blah\"))\n    }\n\n    @Test\n    fun `#clearAnswer removes answer and updates button title`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(\"blah\"))\n            .build()\n        val widget = createWidget()\n        widget.clearAnswer()\n\n        assertThat(\n            widget.answer,\n            equalTo(null)\n        )\n        composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).assertIsDisplayed()\n    }\n\n    @Test\n    fun `#clearAnswer calls #valueChangeListener`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(\"blah\"))\n            .build()\n        val widget = createWidget()\n        val valueChangedListener = QuestionWidgetHelpers.mockValueChangedListener(widget)\n        widget.clearAnswer()\n\n        verify(valueChangedListener).widgetValueChanged(widget)\n    }\n\n    @Test\n    fun `#setData displays sanitized answer`() {\n        val widget = createWidget()\n        widget.setData(\"\\ud800blah\\b\")\n        assertThat(\n            widget.answer!!.displayText,\n            equalTo(\"blah\")\n        )\n    }\n\n    @Test\n    fun `#setData updates the button title`() {\n        val widget = createWidget()\n        widget.setData(\"\\ud800blah\\b\")\n        composeRule.onNodeWithClickLabel(activity.getString(string.replace_barcode)).assertIsDisplayed()\n    }\n\n    @Test\n    fun `#setData calls #valueChangeListener`() {\n        val widget = createWidget()\n        val valueChangedListener = QuestionWidgetHelpers.mockValueChangedListener(widget)\n        widget.setData(\"blah\")\n\n        verify(valueChangedListener).widgetValueChanged(widget)\n    }\n\n    @Test\n    fun `pressing the button with permission not granted does not launch anything`() {\n        val widget = createWidget()\n        permissionsProvider.setPermissionGranted(false)\n        widget.setPermissionsProvider(permissionsProvider)\n        composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).performClick()\n\n        assertThat(Shadows.shadowOf(activity).nextStartedActivity, equalTo(null))\n        assertThat(waitingForDataRegistry.waiting.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun `pressing the button with permission granted registers widget for data waiting`() {\n        whenever(formEntryPrompt.index).thenReturn(formIndex)\n\n        val widget = createWidget()\n        widget.setPermissionsProvider(permissionsProvider)\n        composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).performClick()\n\n        assertThat(\n            waitingForDataRegistry.waiting.contains(formIndex),\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `pressing the button when front camera should be used but it is not available displays a toast`() {\n        whenever(cameraUtils.isFrontCameraAvailable(ArgumentMatchers.any())).thenReturn(false)\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAppearance(Appearances.FRONT)\n            .build()\n        val widget = createWidget()\n        widget.setPermissionsProvider(permissionsProvider)\n        composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).performClick()\n\n        assertThat(\n            ShadowToast.getTextOfLatestToast(),\n            equalTo(activity.getString(string.error_front_camera_unavailable))\n        )\n    }\n\n    @Test\n    fun `pressing the button when front camera should be used and it is available launches correct intent`() {\n        whenever(cameraUtils.isFrontCameraAvailable(ArgumentMatchers.any())).thenReturn(true)\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAppearance(Appearances.FRONT)\n            .build()\n        val widget = createWidget()\n        widget.setPermissionsProvider(permissionsProvider)\n        composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).performClick()\n\n        assertThat(\n            Shadows.shadowOf(activity).nextStartedActivity.getBooleanExtra(Appearances.FRONT, false),\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `The answer is not displayed with hidden mode`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAppearance(Appearances.HIDDEN_ANSWER)\n            .withAnswer(StringData(\"original contents\"))\n            .build()\n\n        val widget = createWidget()\n\n        // Check initial value is not shown\n        composeRule.onNodeWithClickLabel(activity.getString(string.replace_barcode)).assertIsDisplayed()\n        composeRule.onNodeWithText(\"original contents\").assertDoesNotExist()\n        assertThat(widget.answer, equalTo(StringData(\"original contents\")))\n\n        // Check updates aren't shown\n        widget.setData(\"updated contents\")\n        composeRule.onNodeWithClickLabel(activity.getString(string.replace_barcode)).assertIsDisplayed()\n        composeRule.onNodeWithText(\"updated contents\").assertDoesNotExist()\n        assertThat(\n            widget.answer,\n            equalTo(StringData(\"updated contents\"))\n        )\n    }\n\n    override fun usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withReadOnly(true)\n            .build()\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).assertDoesNotExist()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/BearingWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.content.ComponentName;\nimport android.hardware.Sensor;\nimport android.hardware.SensorManager;\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.activities.BearingActivity;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.utilities.ApplicationConstants;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.robolectric.shadows.ShadowActivity;\nimport org.robolectric.shadows.ShadowToast;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnly;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\nimport static org.robolectric.Shadows.shadowOf;\n\n/**\n * @author James Knight\n */\n@RunWith(AndroidJUnit4.class)\npublic class BearingWidgetTest {\n    private final FakeWaitingForDataRegistry fakeWaitingForDataRegistry = new FakeWaitingForDataRegistry();\n\n    private WidgetTestActivity widgetActivity;\n    private ShadowActivity shadowActivity;\n    private SensorManager sensorManager;\n    private View.OnLongClickListener listener;\n\n    @Before\n    public void setUp() {\n        widgetActivity = widgetTestActivity();\n        shadowActivity = shadowOf(widgetActivity);\n\n        sensorManager = mock(SensorManager.class);\n        listener = mock(View.OnLongClickListener.class);\n\n        Sensor sensor = mock(Sensor.class);\n        when(sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)).thenReturn(sensor);\n        when(sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)).thenReturn(sensor);\n    }\n\n    @Test\n    public void usingReadOnlyOption_hidesBearingButton() {\n        assertThat(createWidget(promptWithReadOnly()).binding.bearingButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_getBearingButtonIsShown() {\n        assertThat(createWidget(promptWithAnswer(null)).binding.bearingButton.getText(),\n                is(widgetActivity.getString(org.odk.collect.strings.R.string.get_bearing)));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_replaceBearingButtonIsShown() {\n        assertThat(createWidget(promptWithAnswer(new StringData(\"blah\"))).binding.bearingButton.getText(),\n                is(widgetActivity.getString(org.odk.collect.strings.R.string.replace_bearing)));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_answerTextViewShowsCorrectAnswer() {\n        assertThat(createWidget(promptWithAnswer(new StringData(\"blah\"))).binding.widgetAnswerText.getAnswer(), is(\"blah\"));\n    }\n\n    @Test\n    public void getAnswer_whenPromptAnswerDoesNotHaveAnswer_returnsNull() {\n        assertThat(createWidget(promptWithAnswer(null)).getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        assertThat(createWidget(promptWithAnswer(new StringData(\"blah\"))).getAnswer().getDisplayText(), is(\"blah\"));\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        BearingWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        widget.clearAnswer();\n        assertThat(widget.binding.widgetAnswerText.getAnswer(), is(\"\"));\n    }\n\n    @Test\n    public void clearAnswer_updatesButtonLabel() {\n        BearingWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        widget.clearAnswer();\n        assertThat(widget.binding.bearingButton.getText(), is(widgetActivity.getString(org.odk.collect.strings.R.string.get_bearing)));\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListeners() {\n        BearingWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n\n        widget.clearAnswer();\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void setData_updatesWidgetAnswer() {\n        BearingWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(\"blah\");\n        assertThat(widget.binding.widgetAnswerText.getAnswer(), is(\"blah\"));\n    }\n\n    @Test\n    public void setData_updatesButtonLabel() {\n        BearingWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(\"blah\");\n        assertThat(widget.binding.bearingButton.getText(), is(widgetActivity.getString(org.odk.collect.strings.R.string.replace_bearing)));\n    }\n\n    @Test\n    public void setData_callsValueChangeListeners() {\n        BearingWidget widget = createWidget(promptWithAnswer(null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n\n        widget.setData(\"blah\");\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void clickingAnswerTextForLong_callsOnLongClickListener() {\n        BearingWidget widget = createWidget(promptWithAnswer(null));\n        widget.setOnLongClickListener(listener);\n\n        widget.binding.widgetAnswerText.performLongClick();\n        verify(listener).onLongClick(widget.binding.widgetAnswerText);\n    }\n\n    @Test\n    public void clickingBearingButtonForLong_callOnLongClickListener() {\n        BearingWidget widget = createWidget(promptWithAnswer(null));\n        widget.setOnLongClickListener(listener);\n\n        widget.binding.bearingButton.performLongClick();\n        verify(listener).onLongClick(widget.binding.bearingButton);\n    }\n\n    @Test\n    public void clickingBearingButtonForLong_whenSensorIsAvailable_callsOnLongClickListener() {\n        BearingWidget widget = createWidget(promptWithAnswer(null));\n        widget.setOnLongClickListener(listener);\n\n        widget.binding.bearingButton.performLongClick();\n        verify(listener).onLongClick(widget.binding.bearingButton);\n    }\n\n    @Test\n    public void clickingBearingButton_whenAccelerometerSensorIsNotAvailable_doesNotLaunchAnyIntent() {\n        assertNoIntentLaunchedWhenSensorIsUnavailable(Sensor.TYPE_ACCELEROMETER);\n    }\n\n    @Test\n    public void clickingBearingButton_whenAccelerometerSensorIsNotAvailable_disablesBearingButton() {\n        assertBearingButtonIsDisabledWhenSensorIsUnavailable(Sensor.TYPE_ACCELEROMETER);\n    }\n\n    @Test\n    public void clickingBearingButton_whenAccelerometerSensorIsNotAvailable_makesEditTextEditable() {\n        assertAnswerTextIsEditableWhenSensorIsUnavailable(Sensor.TYPE_ACCELEROMETER);\n    }\n\n    @Test\n    public void clickingBearingButton_whenMagneticSensorIsNotAvailable_doesNotLaunchAnyIntent() {\n        assertNoIntentLaunchedWhenSensorIsUnavailable(Sensor.TYPE_MAGNETIC_FIELD);\n    }\n\n    @Test\n    public void clickingBearingButton_whenMagneticSensorIsNotAvailable_disablesBearingButton() {\n        assertBearingButtonIsDisabledWhenSensorIsUnavailable(Sensor.TYPE_MAGNETIC_FIELD);\n    }\n\n    @Test\n    public void clickingBearingButton_whenMagneticSensorIsNotAvailable_makesEditTextEditable() {\n        assertAnswerTextIsEditableWhenSensorIsUnavailable(Sensor.TYPE_MAGNETIC_FIELD);\n    }\n\n    @Test\n    public void clickingBearingButton_whenSensorIsAvailable_setsWidgetWaitingForData() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        FormIndex formIndex = mock(FormIndex.class);\n        when(prompt.getIndex()).thenReturn(formIndex);\n\n        BearingWidget widget = createWidget(prompt);\n        widget.binding.bearingButton.performClick();\n        assertThat(fakeWaitingForDataRegistry.waiting.contains(formIndex), is(true));\n    }\n\n    @Test\n    public void clickingBearingButton_whenSensorIsAvailable_launchesCorrectIntent() {\n        BearingWidget widget = createWidget(promptWithAnswer(null));\n        widget.binding.bearingButton.performClick();\n\n        assertThat(shadowActivity.getNextStartedActivity().getComponent(), is(new ComponentName(widgetActivity, BearingActivity.class)));\n        assertThat(shadowActivity.getNextStartedActivityForResult().requestCode, is(ApplicationConstants.RequestCodes.BEARING_CAPTURE));\n    }\n\n    private BearingWidget createWidget(FormEntryPrompt prompt) {\n        return new BearingWidget(widgetActivity, new QuestionDetails(prompt), fakeWaitingForDataRegistry, sensorManager, widgetDependencies());\n    }\n\n    private void assertNoIntentLaunchedWhenSensorIsUnavailable(int sensorType) {\n        when(sensorManager.getDefaultSensor(sensorType)).thenReturn(null);\n        BearingWidget widget = createWidget(promptWithAnswer(null));\n        widget.binding.bearingButton.performClick();\n\n        assertThat(shadowActivity.getNextStartedActivity(), nullValue());\n        assertThat(ShadowToast.getTextOfLatestToast(), is(widgetActivity.getString(org.odk.collect.strings.R.string.bearing_lack_of_sensors)));\n    }\n\n    private void assertAnswerTextIsEditableWhenSensorIsUnavailable(int sensorType) {\n        when(sensorManager.getDefaultSensor(sensorType)).thenReturn(null);\n        BearingWidget widget = createWidget(promptWithAnswer(null));\n        widget.binding.bearingButton.performClick();\n\n        assertThat(widget.binding.widgetAnswerText.isEditableState(), is(true));\n    }\n\n    private void assertBearingButtonIsDisabledWhenSensorIsUnavailable(int sensorType) {\n        when(sensorManager.getDefaultSensor(sensorType)).thenReturn(null);\n        BearingWidget widget = createWidget(promptWithAnswer(null));\n        widget.binding.bearingButton.performClick();\n\n        assertThat(widget.binding.bearingButton.isEnabled(), is(false));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/CounterWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.core.model.data.IntegerData\nimport org.junit.Test\nimport org.mockito.Mockito.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.widgets.CounterWidget.Companion.MAX_VALUE\nimport org.odk.collect.android.widgets.base.QuestionWidgetTest\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers\n\nclass CounterWidgetTest : QuestionWidgetTest<CounterWidget, IAnswerData>() {\n\n    override fun createWidget() = CounterWidget(\n        activity,\n        QuestionDetails(formEntryPrompt),\n        dependencies\n    )\n\n    override fun getNextAnswer() = IntegerData(10)\n\n    @Test\n    override fun usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        whenever(formEntryPrompt.isReadOnly).thenReturn(true)\n\n        assertThat(widget.binding.minusButton.isEnabled, equalTo(false))\n        assertThat(widget.binding.plusButton.isEnabled, equalTo(false))\n    }\n\n    @Test\n    fun `only the minus button should be disabled when there is no answer`() {\n        assertThat(widget.binding.minusButton.isEnabled, equalTo(false))\n        assertThat(widget.binding.plusButton.isEnabled, equalTo(true))\n    }\n\n    @Test\n    fun `only the minus button should be disabled when the answer is 0`() {\n        whenever(formEntryPrompt.answerValue).thenReturn(IntegerData(0))\n\n        assertThat(widget.binding.minusButton.isEnabled, equalTo(false))\n        assertThat(widget.binding.plusButton.isEnabled, equalTo(true))\n    }\n\n    @Test\n    fun `both buttons should be enabled when the answer is greater than 0`() {\n        whenever(formEntryPrompt.answerValue).thenReturn(IntegerData(1))\n\n        assertThat(widget.binding.minusButton.isEnabled, equalTo(true))\n        assertThat(widget.binding.plusButton.isEnabled, equalTo(true))\n    }\n\n    @Test\n    fun `only the plus button should be disabled when the answer is 999 999 999`() {\n        whenever(formEntryPrompt.answerValue).thenReturn(IntegerData(MAX_VALUE))\n\n        assertThat(widget.binding.minusButton.isEnabled, equalTo(true))\n        assertThat(widget.binding.plusButton.isEnabled, equalTo(false))\n    }\n\n    @Test\n    fun `the min supported value is 0`() {\n        whenever(formEntryPrompt.answerValue).thenReturn(IntegerData(-1))\n\n        assertThat(widget.binding.value.text, equalTo(\"\"))\n        assertThat(widget.answer, equalTo(null))\n    }\n\n    @Test\n    fun `the max supported value is 999 999 999`() {\n        whenever(formEntryPrompt.answerValue).thenReturn(IntegerData(MAX_VALUE + 1))\n\n        assertThat(widget.binding.value.text, equalTo(\"\"))\n        assertThat(widget.answer, equalTo(null))\n    }\n\n    @Test\n    fun `clicking the minus button decreases the value`() {\n        whenever(formEntryPrompt.answerValue).thenReturn(IntegerData(10))\n\n        widget.binding.minusButton.performClick()\n        assertThat(widget.binding.value.text.toString(), equalTo(\"9\"))\n\n        widget.binding.minusButton.performClick()\n        assertThat(widget.binding.value.text.toString(), equalTo(\"8\"))\n    }\n\n    @Test\n    fun `clicking the plus button increases the value`() {\n        widget.binding.plusButton.performClick()\n        assertThat(widget.binding.value.text.toString(), equalTo(\"1\"))\n\n        widget.binding.plusButton.performClick()\n        assertThat(widget.binding.value.text.toString(), equalTo(\"2\"))\n    }\n\n    @Test\n    fun `clicking the minus button disables the button when the value is the min supported one`() {\n        whenever(formEntryPrompt.answerValue).thenReturn(IntegerData(1))\n\n        widget.binding.minusButton.performClick()\n        assertThat(widget.binding.value.text.toString(), equalTo(\"0\"))\n        assertThat(widget.binding.minusButton.isEnabled, equalTo(false))\n    }\n\n    @Test\n    fun `clicking the plus button disables the button when the value is the max supported one`() {\n        whenever(formEntryPrompt.answerValue).thenReturn(IntegerData(MAX_VALUE - 1))\n\n        widget.binding.plusButton.performClick()\n        assertThat(widget.binding.value.text.toString(), equalTo(MAX_VALUE.toString()))\n        assertThat(widget.binding.plusButton.isEnabled, equalTo(false))\n    }\n\n    @Test\n    fun `clicking the minus button calls widgetValueChanged listener`() {\n        whenever(formEntryPrompt.answerValue).thenReturn(IntegerData(1))\n        val valueChangedListener = QuestionWidgetHelpers.mockValueChangedListener<QuestionWidget>(widget)\n\n        widget.binding.minusButton.performClick()\n        verify(valueChangedListener).widgetValueChanged(widget)\n    }\n\n    @Test\n    fun `clicking the plus button calls widgetValueChanged listener`() {\n        val valueChangedListener = QuestionWidgetHelpers.mockValueChangedListener<QuestionWidget>(widget)\n\n        widget.binding.plusButton.performClick()\n        verify(valueChangedListener).widgetValueChanged(widget)\n    }\n\n    @Test\n    fun `#clearAnswer should remove the displayed value`() {\n        whenever(formEntryPrompt.answerValue).thenReturn(IntegerData(10))\n\n        assertThat(widget.binding.value.text.toString(), equalTo(\"10\"))\n        widget.clearAnswer()\n        assertThat(widget.binding.value.text.toString(), equalTo(\"\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/DecimalWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static junit.framework.TestCase.assertEquals;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.core.Is.is;\nimport static org.junit.Assert.assertSame;\nimport static org.junit.Assert.assertThat;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP;\n\nimport android.text.InputType;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.Constants;\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.data.DecimalData;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.base.GeneralStringWidgetTest;\n\nimport java.text.NumberFormat;\nimport java.util.Locale;\nimport java.util.Random;\n\npublic class DecimalWidgetTest extends GeneralStringWidgetTest<DecimalWidget, DecimalData> {\n\n    private final Random random = new Random();\n\n    @Mock\n    IAnswerData answerData;\n\n    @Mock\n    QuestionDef questionDef;\n\n    @NonNull\n    @Override\n    public DecimalWidget createWidget() {\n        when(formEntryPrompt.getDataType()).thenReturn(Constants.DATATYPE_DECIMAL);\n        return new DecimalWidget(activity, new QuestionDetails(formEntryPrompt, readOnlyOverride), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public DecimalData getNextAnswer() {\n        // Need to keep under 15 digits:\n        double d = random.nextDouble();\n        NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);\n        nf.setMaximumFractionDigits(13); // The Widget internally truncatest this further.\n        nf.setMaximumIntegerDigits(13);\n        nf.setGroupingUsed(false);\n\n        String formattedValue = nf.format(d);\n        return new DecimalData(Double.parseDouble(formattedValue));\n    }\n\n    @Override\n    public void setUp() throws Exception {\n        super.setUp();\n\n        when(formEntryPrompt.getLongText()).thenReturn(\"A decimal questionDef\");\n        when(formElement.getAdditionalAttribute(null, \"playColor\")).thenReturn(null);\n\n        when(formEntryPrompt.getQuestion()).thenReturn(questionDef);\n        when(questionDef.getAdditionalAttribute(null, \"rows\")).thenReturn(null);\n    }\n\n    @Test\n    // Fails when double is turned to string with toString or String.format(Locale.US, \"%f\", d))\n    public void integerValueShouldDisplayNoDecimalPoint() {\n        Double integerDouble = 0.;\n        String integerString = \"0\";\n        when(formEntryPrompt.getAnswerValue()).thenReturn(answerData);\n        when(answerData.getValue()).thenReturn(integerDouble);\n\n        DecimalWidget decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(integerString)));\n\n        decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(integerString)));\n    }\n\n    @Test\n    // Fails when double is turned to string using String.format(Locale.US, \"%f\", d)\n    // because default precision for %f is 6\n    // NOTE: in the case of a decimal value with trailing 0s, it's probably not possible to maintain\n    // that precision. For example, 9.00 becomes 9\n    public void decimalValueShouldNotAddPrecision() {\n        Double twoDecimalDouble = 9.99;\n        String twoDecimalString = \"9.99\";\n        when(formEntryPrompt.getAnswerValue()).thenReturn(answerData);\n        when(answerData.getValue()).thenReturn(twoDecimalDouble);\n\n        DecimalWidget decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(twoDecimalString)));\n\n        decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(twoDecimalString)));\n    }\n\n    @Test\n    // Fails when double is turned to string with toString or String.format(Locale.US, \"%f\", d)\n    public void negativeIntegerShouldDisplayNegativeWithNoDecimalPoint() {\n        Double negativeIntegerDouble = -999.;\n        String negativeIntegerString = \"-999\";\n        when(formEntryPrompt.getAnswerValue()).thenReturn(answerData);\n        when(answerData.getValue()).thenReturn(negativeIntegerDouble);\n\n        DecimalWidget decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(negativeIntegerString)));\n\n        decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(negativeIntegerString)));\n    }\n\n    @Test\n    // Fails when double is turned to string with toString because of scientific notation\n    // https://docs.oracle.com/javase/7/docs/api/java/lang/Double.html#toString(double)\n    public void fifteenDigitValueShouldDisplayAllDigits() {\n        Double fifteenDigitDouble = 999999999999999.;\n        String fifteenDigitString = \"999999999999999\";\n        assertSame(15, fifteenDigitString.length());\n\n        when(formEntryPrompt.getAnswerValue()).thenReturn(answerData);\n        when(answerData.getValue()).thenReturn(fifteenDigitDouble);\n\n        DecimalWidget decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(fifteenDigitString)));\n\n        decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(fifteenDigitString)));\n    }\n\n    @Test\n    // Fails when double is turned to string with toString because of scientific notation\n    // https://docs.oracle.com/javase/7/docs/api/java/lang/Double.html#toString(double)\n    public void fifteenDigitNegativeValueShouldDisplayAllDigits() {\n        Double fifteenDigitNegativeDouble = -99999999999999.;\n        String fifteenDigitNegativeString = \"-99999999999999\";\n        assertSame(15, fifteenDigitNegativeString.length());\n\n        when(formEntryPrompt.getAnswerValue()).thenReturn(answerData);\n        when(answerData.getValue()).thenReturn(fifteenDigitNegativeDouble);\n\n        DecimalWidget decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(fifteenDigitNegativeString)));\n\n        decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(fifteenDigitNegativeString)));\n    }\n\n    @Test\n    // Fails when double is turned to string using String.format(Locale.US, \"%f\", d) because default\n    // precision for %f is 6\n    public void fifteenDigitDecimalValueShouldDisplayAllDigits() {\n        Double fifteenDigitDecimalDouble = 0.9999999999999;\n        String fifteenDigitDecimalString = \"0.9999999999999\";\n        assertSame(15, fifteenDigitDecimalString.length());\n\n        when(formEntryPrompt.getAnswerValue()).thenReturn(answerData);\n        when(answerData.getValue()).thenReturn(fifteenDigitDecimalDouble);\n\n        DecimalWidget decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(fifteenDigitDecimalString)));\n\n        decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(fifteenDigitDecimalString)));\n    }\n\n    @Test\n    // This should never be possible because the EditText has a limit on it\n    public void digitsAboveLimitOfFifteenShouldBeTruncatedFromRight() {\n        Double eighteenDigitDouble = 9999999999999994.;\n        String fifteenDigitString = \"999999999999994\";\n        assertSame(15, fifteenDigitString.length());\n\n        when(formEntryPrompt.getAnswerValue()).thenReturn(answerData);\n        when(answerData.getValue()).thenReturn(eighteenDigitDouble);\n\n        DecimalWidget decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(fifteenDigitString)));\n\n        decimalWidget = new DecimalWidget(activity, new QuestionDetails(formEntryPrompt), dependencies);\n\n        assertThat(decimalWidget.getAnswerText(), is(equalTo(fifteenDigitString)));\n    }\n\n    @Test\n    public void separatorsShouldBeAddedWhenEnabled() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(THOUSANDS_SEP);\n        getWidget().widgetAnswerText.setAnswer(\"123456789.54\");\n\n        assertEquals(\"123,456,789.54\", getWidget().widgetAnswerText.getAnswer());\n        assertEquals(\"123,456,789.54\", getWidget().widgetAnswerText.getBinding().editText.getText().toString());\n        assertEquals(\"123,456,789.54\", getWidget().widgetAnswerText.getBinding().textView.getText().toString());\n    }\n\n    @Override\n    @Test\n    public void verifyInputType() {\n        DecimalWidget widget = getWidget();\n        assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL));\n        assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/DrawWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.content.Intent;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.view.View;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.util.Pair;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport net.bytebuddy.utility.RandomString;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.R;\nimport org.odk.collect.draw.DrawActivity;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.base.FileWidgetTest;\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.support.SynchronousImageLoader;\nimport org.odk.collect.imageloader.ImageLoader;\nimport org.odk.collect.shared.TempFiles;\n\nimport java.io.File;\nimport java.io.IOException;\n\nimport static java.util.Collections.singletonList;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.support.CollectHelpers.setupFakeReferenceManager;\nimport static org.robolectric.Shadows.shadowOf;\n\n/**\n * @author James Knight\n */\n@RunWith(AndroidJUnit4.class)\npublic class DrawWidgetTest extends FileWidgetTest<DrawWidget> {\n\n    //Package visibility for sharing with related tests\n    static final String DEFAULT_IMAGE_ANSWER = \"jr://images/referenceURI\";\n    static final String USER_SPECIFIED_IMAGE_ANSWER = \"current.bmp\";\n\n    private File currentFile;\n\n    @NonNull\n    @Override\n    public DrawWidget createWidget() {\n        QuestionMediaManager fakeQuestionMediaManager = new FakeQuestionMediaManager() {\n            @Override\n            public File getAnswerFile(String fileName) {\n                File result;\n                if (currentFile == null) {\n                    result = super.getAnswerFile(fileName);\n                } else {\n                    result = fileName.equals(USER_SPECIFIED_IMAGE_ANSWER) ? currentFile : null;\n                }\n                return result;\n            }\n        };\n        return new DrawWidget(activity,\n                new QuestionDetails(formEntryPrompt, readOnlyOverride),\n                fakeQuestionMediaManager, new FakeWaitingForDataRegistry(), TempFiles.getPathInTempDir(), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public StringData getNextAnswer() {\n        return new StringData(RandomString.make());\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n\n        assertThat(getSpyWidget().binding.drawButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisabled() {\n        readOnlyOverride = true;\n        when(formEntryPrompt.isReadOnly()).thenReturn(false);\n\n        assertThat(getSpyWidget().binding.drawButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_hideImageViewAndErrorMessage() {\n        DrawWidget widget = createWidget();\n\n        assertThat(widget.getImageView().getVisibility(), is(View.GONE));\n        assertThat(widget.getImageView().getDrawable(), nullValue());\n\n        assertThat(widget.getErrorTextView().getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenTheAnswerImageCanNotBeLoaded_hideImageViewAndShowErrorMessage() throws IOException {\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader(true);\n            }\n        });\n\n        String imagePath = File.createTempFile(\"current\", \".bmp\").getAbsolutePath();\n        currentFile = new File(imagePath);\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER)\n                .build();\n\n        DrawWidget widget = createWidget();\n\n        assertThat(widget.getImageView().getVisibility(), is(View.GONE));\n        assertThat(widget.getImageView().getDrawable(), nullValue());\n\n        assertThat(widget.getErrorTextView().getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenPromptHasDefaultAnswer_showsInImageView() throws Exception {\n        String imagePath = File.createTempFile(\"default\", \".bmp\").getAbsolutePath();\n        ReferenceManager referenceManager = setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, imagePath)\n        ));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        DrawWidget widget = createWidget();\n        ImageView imageView = widget.getImageView();\n        assertThat(imageView.getVisibility(), is(View.VISIBLE));\n        Drawable drawable = imageView.getDrawable();\n        assertThat(drawable, notNullValue());\n\n        String loadedPath = shadowOf(((BitmapDrawable) drawable).getBitmap()).getCreatedFromPath();\n        assertThat(loadedPath, equalTo(imagePath));\n    }\n\n    @Test\n    public void whenPromptHasCurrentAnswer_showsInImageView() throws Exception {\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        String imagePath = File.createTempFile(\"current\", \".bmp\").getAbsolutePath();\n        currentFile = new File(imagePath);\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(USER_SPECIFIED_IMAGE_ANSWER)\n                .build();\n\n        DrawWidget widget = createWidget();\n        ImageView imageView = widget.getImageView();\n        assertThat(imageView.getVisibility(), is(View.VISIBLE));\n        Drawable drawable = imageView.getDrawable();\n        assertThat(drawable, notNullValue());\n\n        String loadedPath = shadowOf(((BitmapDrawable) drawable).getBitmap()).getCreatedFromPath();\n        assertThat(loadedPath, equalTo(imagePath));\n    }\n\n    @Test\n    public void whenPromptHasDefaultAnswer_passUriToDrawActivity() throws Exception {\n        File file = File.createTempFile(\"default\", \".bmp\");\n        String imagePath = file.getAbsolutePath();\n\n        ReferenceManager referenceManager = setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, imagePath)\n        ));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        Intent intent = getIntentLaunchedByClick(R.id.draw_button);\n        assertComponentEquals(activity, DrawActivity.class, intent);\n        assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_DRAW, intent);\n        assertExtraEquals(DrawActivity.REF_IMAGE, Uri.fromFile(file), intent);\n    }\n\n    @Test\n    public void whenPromptHasDefaultAnswerThatDoesNotExist_doNotPassUriToDrawActivity() throws Exception {\n        ReferenceManager referenceManager = setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, \"/something\")\n        ));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n        });\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        Intent intent = getIntentLaunchedByClick(R.id.draw_button);\n        assertComponentEquals(activity, DrawActivity.class, intent);\n        assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_DRAW, intent);\n        assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_doNotPassUriToDrawActivity() {\n        Intent intent = getIntentLaunchedByClick(R.id.draw_button);\n        assertComponentEquals(activity, DrawActivity.class, intent);\n        assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_DRAW, intent);\n        assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/ExArbitraryFileWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.data.StringData\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.android.support.WidgetTestActivity\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.android.utilities.MediaUtils\nimport org.odk.collect.android.widgets.arbitraryfile.ExArbitraryFileWidget\nimport org.odk.collect.android.widgets.base.FileWidgetTest\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry\nimport org.odk.collect.android.widgets.utilities.FileRequester\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidtest.onNodeWithClickLabel\nimport org.odk.collect.strings.R.string\nimport java.io.File\n\nclass ExArbitraryFileWidgetTest : FileWidgetTest<ExArbitraryFileWidget>() {\n    @get:Rule\n    val composeRule = createAndroidComposeRule<WidgetTestActivity>()\n    private val fileRequester = mock<FileRequester>()\n    private val mediaUtils = mock<MediaUtils>()\n    private val questionMediaManager = FakeQuestionMediaManager()\n    private val mediaWidgetAnswerViewModel = MediaWidgetAnswerViewModel(mock(), questionMediaManager, mediaUtils)\n    private val dependencies = QuestionWidget.Dependencies(\n        null,\n        mediaWidgetAnswerViewModel\n    )\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesMediaUtils(intentLauncher: IntentLauncher): MediaUtils {\n                return mediaUtils\n            }\n        })\n        formEntryPrompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_FILE_CAPTURE)\n            .build()\n    }\n\n    override fun getInitialAnswer(): StringData {\n        return StringData(\"document.pdf\")\n    }\n\n    override fun getNextAnswer(): StringData {\n        return StringData(\"document.xlsx\")\n    }\n\n    override fun createWidget(): ExArbitraryFileWidget {\n        return ExArbitraryFileWidget(\n            composeRule.activity,\n            QuestionDetails(formEntryPrompt, readOnlyOverride),\n            dependencies,\n            questionMediaManager,\n            FakeWaitingForDataRegistry(),\n            fileRequester\n        ).also {\n            composeRule.activity.setContentView(it)\n            activity = composeRule.activity\n        }\n    }\n\n    @Test\n    fun `Display the answer text when there is answer`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(initialAnswer.displayText))\n            .build()\n        createWidget()\n        composeRule.onNodeWithText(initialAnswer.displayText).assertIsDisplayed()\n    }\n\n    @Test\n    fun `External file picker should be called when clicking on button`() {\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).performClick()\n        verify(fileRequester).launch(activity, ApplicationConstants.RequestCodes.EX_ARBITRARY_FILE_CHOOSER, formEntryPrompt)\n    }\n\n    @Test\n    fun `File viewer should be called when clicking on answer`() {\n        val file = questionMediaManager.addAnswerFile(File.createTempFile(\"document\", \".pdf\"))\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(file.name))\n            .build()\n\n        createWidget()\n        composeRule.onNodeWithText(file.name).performClick()\n        verify(mediaUtils).openFile(activity, file, null)\n    }\n\n    @Test\n    fun `Hide the answer when clear answer is called`() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(initialAnswer.displayText))\n            .build()\n\n        val widget = createWidget()\n        widget.clearAnswer()\n        composeRule.onNodeWithText(initialAnswer.displayText).assertDoesNotExist()\n    }\n\n    @Test\n    fun `All clickable elements should be disabled when read-only override option is used`() {\n        readOnlyOverride = true\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withReadOnly(false)\n            .withAnswer(StringData(initialAnswer.displayText))\n            .build()\n\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertDoesNotExist()\n        composeRule.onNodeWithText(initialAnswer.displayText).assertIsDisplayed()\n    }\n\n    @Test\n    override fun usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withReadOnly(true)\n            .withAnswer(StringData(initialAnswer.displayText))\n            .build()\n\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertDoesNotExist()\n        composeRule.onNodeWithText(initialAnswer.displayText).assertIsDisplayed()\n    }\n\n    @Test\n    override fun settingANewAnswerShouldCallDeleteMediaToRemoveTheOldFile() {\n        val file = questionMediaManager.addAnswerFile(File.createTempFile(\"document\", \".pdf\"))\n        formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt)\n            .withAnswer(StringData(file.name))\n            .build()\n\n        val widget = createWidget()\n        widget.setData(createBinaryData(nextAnswer))\n\n        assertThat(\n            questionMediaManager.originalFiles[formEntryPrompt.index.toString()],\n            equalTo(file.absolutePath)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/ExAudioWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_FONT_SIZE;\n\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.utilities.ApplicationConstants;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.android.widgets.base.FileWidgetTest;\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.android.widgets.utilities.FileRequester;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.androidshared.system.IntentLauncher;\nimport org.robolectric.shadows.ShadowToast;\n\nimport java.io.File;\nimport java.io.IOException;\n\npublic class ExAudioWidgetTest extends FileWidgetTest<ExAudioWidget> {\n    @Mock\n    AudioPlayer audioPlayer;\n\n    @Mock\n    FileRequester fileRequester;\n\n    private MediaUtils mediaUtils;\n\n    @Before\n    public void setup() {\n        mediaUtils = mock(MediaUtils.class);\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public MediaUtils providesMediaUtils(IntentLauncher intentLauncher) {\n                return mediaUtils;\n            }\n        });\n        when(mediaUtils.isAudioFile(any())).thenReturn(true);\n    }\n\n    @Override\n    public StringData getInitialAnswer() {\n        return new StringData(\"audio1.mp3\");\n    }\n\n    @NonNull\n    @Override\n    public StringData getNextAnswer() {\n        return new StringData(\"audio2.mp3\");\n    }\n\n    @NonNull\n    @Override\n    public ExAudioWidget createWidget() {\n        return new ExAudioWidget(activity, new QuestionDetails(formEntryPrompt, readOnlyOverride),\n                new FakeQuestionMediaManager(), audioPlayer, new FakeWaitingForDataRegistry(), fileRequester, dependencies);\n    }\n\n    @Test\n    public void whenWidgetCreated_shouldLaunchButtonBeVisible() {\n        assertThat(getWidget().binding.launchExternalAppButton.getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenWidgetCreated_shouldLaunchButtonHaveProperName() {\n        assertThat(getWidget().binding.launchExternalAppButton.getText(), is(\"Launch\"));\n    }\n\n    @Test\n    public void whenFontSizeChanged_CustomFontSizeShouldBeUsed() {\n        settingsProvider.getUnprotectedSettings().save(KEY_FONT_SIZE, \"30\");\n\n        assertThat((int) getWidget().binding.launchExternalAppButton.getTextSize(), is(QuestionFontSizeUtils.getFontSize(settingsProvider.getUnprotectedSettings(), QuestionFontSizeUtils.FontSize.BODY_LARGE)));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_shouldAudioPlayerBeHidden() {\n        assertThat(getWidget().binding.audioPlayer.recordingDuration.getVisibility(), is(View.GONE));\n        assertThat(getWidget().binding.audioPlayer.waveform.getVisibility(), is(View.GONE));\n        assertThat(getWidget().binding.audioPlayer.audioController.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenThereIsAnswer_shouldAudioPlayerBeDisplayed() {\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        assertThat(getWidget().binding.audioPlayer.audioController.getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenThereIsAnswer_shouldLaunchButtonBeHidden() {\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        assertThat(getWidget().binding.launchExternalAppButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenAnswerCleared_shouldAudioPlayerBeHidden() {\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        ExAudioWidget widget = getWidget();\n        widget.clearAnswer();\n        assertThat(getWidget().binding.audioPlayer.audioController.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenLaunchButtonClicked_exWidgetIntentLauncherShouldBeStarted() {\n        getWidget().binding.launchExternalAppButton.performClick();\n        verify(fileRequester).launch(activity, ApplicationConstants.RequestCodes.EX_AUDIO_CHOOSER, formEntryPrompt);\n    }\n\n    @Test\n    public void whenSetDataCalledWithNull_shouldExistedAnswerBeRemoved() {\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        ExAudioWidget widget = getWidget();\n        widget.setData(null);\n        assertThat(widget.getAnswer(), is(nullValue()));\n        assertThat(getWidget().binding.audioPlayer.audioController.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenUnsupportedFileTypeAttached_shouldNotThatFileBeAdded() throws IOException {\n        ExAudioWidget widget = getWidget();\n        File answer = File.createTempFile(\"doc\", \".pdf\");\n        when(mediaUtils.isAudioFile(answer)).thenReturn(false);\n        widget.setData(answer);\n        assertThat(widget.getAnswer(), is(nullValue()));\n        assertThat(getWidget().binding.audioPlayer.audioController.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenUnsupportedFileTypeAttached_shouldTheFileBeRemoved() throws IOException {\n        ExAudioWidget widget = getWidget();\n        File answer = File.createTempFile(\"doc\", \".pdf\");\n        when(mediaUtils.isAudioFile(answer)).thenReturn(false);\n        widget.setData(answer);\n        verify(mediaUtils).deleteMediaFile(answer.getAbsolutePath());\n    }\n\n    @Test\n    public void whenUnsupportedFileTypeAttached_shouldToastBeDisplayed() throws IOException {\n        ExAudioWidget widget = getWidget();\n        File answer = File.createTempFile(\"doc\", \".pdf\");\n        when(mediaUtils.isAudioFile(answer)).thenReturn(false);\n        widget.setData(answer);\n        assertThat(ShadowToast.getTextOfLatestToast(), is(\"Application returned an invalid file type\"));\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        ExAudioWidget widget = getWidget();\n        assertThat(widget.binding.launchExternalAppButton.getVisibility(), is(View.GONE));\n        assertThat(getWidget().binding.audioPlayer.audioController.getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisabled() {\n        readOnlyOverride = true;\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        ExAudioWidget widget = getWidget();\n        assertThat(widget.binding.launchExternalAppButton.getVisibility(), is(View.GONE));\n        assertThat(getWidget().binding.audioPlayer.audioController.getVisibility(), is(View.VISIBLE));\n    }\n}\n\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/ExDecimalWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static junit.framework.TestCase.assertEquals;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.core.Is.is;\nimport static org.junit.Assert.assertSame;\nimport static org.junit.Assert.assertThat;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAppearance;\n\nimport android.text.InputType;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\n\nimport org.hamcrest.CoreMatchers;\nimport org.hamcrest.Matchers;\nimport org.javarosa.core.model.Constants;\nimport org.javarosa.core.model.data.DecimalData;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.base.GeneralExStringWidgetTest;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers;\nimport org.odk.collect.android.widgets.utilities.StringRequester;\n\nimport java.text.NumberFormat;\nimport java.util.Locale;\n\n/**\n * @author James Knight\n */\n\npublic class ExDecimalWidgetTest extends GeneralExStringWidgetTest<ExDecimalWidget, DecimalData> {\n\n    @Mock\n    IAnswerData answerData;\n\n    @Mock\n    StringRequester stringRequester;\n\n    @NonNull\n    @Override\n    public ExDecimalWidget createWidget() {\n        when(formEntryPrompt.getDataType()).thenReturn(Constants.DATATYPE_DECIMAL);\n        return new ExDecimalWidget(activity, new QuestionDetails(formEntryPrompt), new FakeWaitingForDataRegistry(), stringRequester, dependencies);\n    }\n\n    @NonNull\n    @Override\n    public DecimalData getNextAnswer() {\n        // Need to keep under 15 digits:\n        double d = random.nextDouble();\n        NumberFormat nf = NumberFormat.getNumberInstance(Locale.US);\n        nf.setMaximumFractionDigits(13); // The Widget internally truncatest this further.\n        nf.setMaximumIntegerDigits(13);\n        nf.setGroupingUsed(false);\n\n        String formattedValue = nf.format(d);\n        return new DecimalData(Double.parseDouble(formattedValue));\n    }\n\n    @Override\n    public void setUp() throws Exception {\n        super.setUp();\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"\");\n    }\n\n    @Test\n    // This should never be possible because the EditText has a limit on it\n    public void digitsAboveLimitOfFifteenShouldBeTruncatedFromRight() {\n        Double eighteenDigitDouble = 9999999999999994.;\n        String fifteenDigitString = \"999999999999994\";\n        assertSame(15, fifteenDigitString.length());\n\n        when(formEntryPrompt.getAnswerValue()).thenReturn(answerData);\n        when(answerData.getValue()).thenReturn(eighteenDigitDouble);\n\n        ExDecimalWidget exDecimalWidget = new ExDecimalWidget(activity, new QuestionDetails(formEntryPrompt), new FakeWaitingForDataRegistry(), stringRequester, dependencies);\n\n        assertThat(exDecimalWidget.binding.widgetAnswerText.getAnswer(), is(equalTo(fifteenDigitString)));\n\n        exDecimalWidget = new ExDecimalWidget(activity, new QuestionDetails(formEntryPrompt), new FakeWaitingForDataRegistry(), stringRequester, dependencies);\n\n        assertThat(exDecimalWidget.binding.widgetAnswerText.getAnswer(), is(equalTo(fifteenDigitString)));\n    }\n\n    @Test\n    public void separatorsShouldBeAddedWhenEnabled() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(THOUSANDS_SEP);\n        getWidget().binding.widgetAnswerText.setAnswer(\"123456789.54\");\n\n        assertEquals(\"123,456,789.54\", getWidget().binding.widgetAnswerText.getAnswer());\n        assertEquals(\"123,456,789.54\", getWidget().binding.widgetAnswerText.getBinding().editText.getText().toString());\n        assertEquals(\"123,456,789.54\", getWidget().binding.widgetAnswerText.getBinding().textView.getText().toString());\n    }\n\n    @Override\n    @Test\n    public void verifyInputType() {\n        ExDecimalWidget widget = getWidget();\n        assertThat(widget.binding.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL));\n        assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null));\n        assertThat(widget.binding.widgetAnswerText.getBinding().textView.getTransformationMethod(), equalTo(null));\n    }\n\n    @Override\n    @Test\n    public void whenPromptHasHiddenAnswerAppearance_answerIsNotDisplayed() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder(promptWithAppearance(Appearances.HIDDEN_ANSWER))\n                .withAnswer(new DecimalData(10.5))\n                .build();\n\n        WidgetTestActivity widgetTestActivity = QuestionWidgetHelpers.widgetTestActivity();\n        ExDecimalWidget widget = new ExDecimalWidget(widgetTestActivity, new QuestionDetails(prompt),\n                new FakeWaitingForDataRegistry(), stringRequester, dependencies);\n\n        // Check initial value is not shown\n        assertThat(widget.binding.widgetAnswerText.getVisibility(), Matchers.equalTo(View.GONE));\n        assertThat(widget.getAnswer(), CoreMatchers.equalTo(new DecimalData(10.5)));\n\n        // Check updates aren't shown\n        widget.setData(15.5);\n        assertThat(widget.binding.widgetAnswerText.getVisibility(), Matchers.equalTo(View.GONE));\n        assertThat(widget.getAnswer(), CoreMatchers.equalTo(new DecimalData(15.5)));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/ExImageWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_FONT_SIZE;\n\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.utilities.ApplicationConstants;\nimport org.odk.collect.android.utilities.MediaUtils;\nimport org.odk.collect.android.widgets.base.FileWidgetTest;\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.utilities.FileRequester;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.androidshared.system.IntentLauncher;\nimport org.robolectric.shadows.ShadowToast;\n\nimport java.io.File;\nimport java.io.IOException;\n\npublic class ExImageWidgetTest extends FileWidgetTest<ExImageWidget> {\n    @Mock\n    FileRequester fileRequester;\n\n    private MediaUtils mediaUtils;\n\n    @Before\n    public void setup() {\n        mediaUtils = mock(MediaUtils.class);\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public MediaUtils providesMediaUtils(IntentLauncher intentLauncher) {\n                return mediaUtils;\n            }\n        });\n        when(mediaUtils.isImageFile(any())).thenReturn(true);\n    }\n\n    @Override\n    public StringData getInitialAnswer() {\n        return new StringData(\"image1.png\");\n    }\n\n    @NonNull\n    @Override\n    public StringData getNextAnswer() {\n        return new StringData(\"image2.png\");\n    }\n\n    @NonNull\n    @Override\n    public ExImageWidget createWidget() {\n        return new ExImageWidget(activity, new QuestionDetails(formEntryPrompt, readOnlyOverride),\n                new FakeQuestionMediaManager(), new FakeWaitingForDataRegistry(), fileRequester, dependencies);\n    }\n\n    @Test\n    public void whenWidgetCreated_shouldLaunchButtonBeVisible() {\n        assertThat(getWidget().binding.launchExternalAppButton.getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenWidgetCreated_shouldLaunchButtonHaveProperName() {\n        assertThat(getWidget().binding.launchExternalAppButton.getText(), is(\"Launch\"));\n    }\n\n    @Test\n    public void whenFontSizeChanged_CustomFontSizeShouldBeUsed() {\n        settingsProvider.getUnprotectedSettings().save(KEY_FONT_SIZE, \"30\");\n\n        assertThat((int) getWidget().binding.launchExternalAppButton.getTextSize(), is(QuestionFontSizeUtils.getFontSize(settingsProvider.getUnprotectedSettings(), QuestionFontSizeUtils.FontSize.BODY_LARGE)));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_shouldImageViewBeHidden() {\n        assertThat(getWidget().binding.imageView.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenThereIsAnswer_shouldImageViewBeDisplayed() {\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        assertThat(getWidget().binding.imageView.getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenAnswerCleared_shouldImageViewBeHidden() {\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        ExImageWidget widget = getWidget();\n        widget.clearAnswer();\n        assertThat(getWidget().binding.imageView.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenLaunchButtonClicked_exWidgetIntentLauncherShouldBeStarted() {\n        getWidget().binding.launchExternalAppButton.performClick();\n        verify(fileRequester).launch(activity, ApplicationConstants.RequestCodes.EX_IMAGE_CHOOSER, formEntryPrompt);\n    }\n\n    @Test\n    public void whenImageViewClicked_shouldFileViewerByCalled() {\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        ExImageWidget widget = getWidget();\n        widget.binding.imageView.performClick();\n        verify(mediaUtils).openFile(activity, widget.answerFile, \"image/*\");\n    }\n\n    @Test\n    public void whenSetDataCalledWithNull_shouldExistedAnswerBeRemoved() {\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        ExImageWidget widget = getWidget();\n        widget.setData(null);\n        assertThat(widget.getAnswer(), is(nullValue()));\n        assertThat(widget.binding.imageView.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenUnsupportedFileTypeAttached_shouldNotThatFileBeAdded() throws IOException {\n        ExImageWidget widget = getWidget();\n        File answer = File.createTempFile(\"doc\", \".pdf\");\n        when(mediaUtils.isImageFile(answer)).thenReturn(false);\n        widget.setData(answer);\n        assertThat(widget.getAnswer(), is(nullValue()));\n        assertThat(widget.binding.imageView.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenUnsupportedFileTypeAttached_shouldTheFileBeRemoved() throws IOException {\n        ExImageWidget widget = getWidget();\n        File answer = File.createTempFile(\"doc\", \".pdf\");\n        when(mediaUtils.isImageFile(answer)).thenReturn(false);\n        widget.setData(answer);\n        verify(mediaUtils).deleteMediaFile(answer.getAbsolutePath());\n    }\n\n    @Test\n    public void whenUnsupportedFileTypeAttached_shouldToastBeDisplayed() throws IOException {\n        ExImageWidget widget = getWidget();\n        File answer = File.createTempFile(\"doc\", \".pdf\");\n        when(mediaUtils.isImageFile(answer)).thenReturn(false);\n        widget.setData(answer);\n        assertThat(ShadowToast.getTextOfLatestToast(), is(\"Application returned an invalid file type\"));\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        ExImageWidget widget = getWidget();\n        assertThat(widget.binding.launchExternalAppButton.getVisibility(), is(View.GONE));\n        assertThat(widget.binding.imageView.getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisabled() {\n        readOnlyOverride = true;\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        ExImageWidget widget = getWidget();\n        assertThat(widget.binding.launchExternalAppButton.getVisibility(), is(View.GONE));\n        assertThat(widget.binding.imageView.getVisibility(), is(View.VISIBLE));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/ExIntegerWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static junit.framework.TestCase.assertEquals;\nimport static org.hamcrest.CoreMatchers.equalTo;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAppearance;\n\nimport android.text.InputType;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\n\nimport org.hamcrest.Matchers;\nimport org.javarosa.core.model.Constants;\nimport org.javarosa.core.model.data.IntegerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.base.GeneralExStringWidgetTest;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers;\nimport org.odk.collect.android.widgets.utilities.StringRequester;\n\n/**\n * @author James Knight\n */\n\npublic class ExIntegerWidgetTest extends GeneralExStringWidgetTest<ExIntegerWidget, IntegerData> {\n\n    @Mock\n    StringRequester stringRequester;\n\n    @NonNull\n    @Override\n    public ExIntegerWidget createWidget() {\n        when(formEntryPrompt.getDataType()).thenReturn(Constants.DATATYPE_INTEGER);\n        return new ExIntegerWidget(activity, new QuestionDetails(formEntryPrompt), new FakeWaitingForDataRegistry(), stringRequester, dependencies);\n    }\n\n    @NonNull\n    @Override\n    public IntegerData getNextAnswer() {\n        return new IntegerData(randomInteger());\n    }\n\n    @Override\n    public void setUp() throws Exception {\n        super.setUp();\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"\");\n    }\n\n    private int randomInteger() {\n        return Math.abs(random.nextInt()) % 1_000_000_000;\n    }\n\n    @Test\n    public void digitsAboveLimitOfNineShouldBeTruncatedFromRight() {\n        getWidget().binding.widgetAnswerText.setAnswer(\"123456789123\");\n        assertEquals(\"123456789\", getWidget().binding.widgetAnswerText.getAnswer());\n    }\n\n    @Test\n    public void separatorsShouldBeAddedWhenEnabled() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(THOUSANDS_SEP);\n        getWidget().binding.widgetAnswerText.setAnswer(\"123456789\");\n\n        assertEquals(\"123,456,789\", getWidget().binding.widgetAnswerText.getAnswer());\n        assertEquals(\"123,456,789\", getWidget().binding.widgetAnswerText.getBinding().editText.getText().toString());\n        assertEquals(\"123,456,789\", getWidget().binding.widgetAnswerText.getBinding().textView.getText().toString());\n    }\n\n    @Override\n    @Test\n    public void verifyInputType() {\n        ExIntegerWidget widget = getWidget();\n        assertThat(widget.binding.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED));\n        assertThat(widget.binding.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null));\n        assertThat(widget.binding.widgetAnswerText.getBinding().textView.getTransformationMethod(), equalTo(null));\n    }\n\n    @Override\n    @Test\n    public void whenPromptHasHiddenAnswerAppearance_answerIsNotDisplayed() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder(promptWithAppearance(Appearances.HIDDEN_ANSWER))\n                .withAnswer(new IntegerData(10))\n                .build();\n\n        WidgetTestActivity widgetTestActivity = QuestionWidgetHelpers.widgetTestActivity();\n        ExIntegerWidget widget = new ExIntegerWidget(widgetTestActivity, new QuestionDetails(prompt),\n                new FakeWaitingForDataRegistry(), stringRequester, dependencies);\n\n        // Check initial value is not shown\n        assertThat(widget.binding.widgetAnswerText.getVisibility(), Matchers.equalTo(View.GONE));\n        assertThat(widget.getAnswer(), equalTo(new IntegerData(10)));\n\n        // Check updates aren't shown\n        widget.setData(15);\n        assertThat(widget.binding.widgetAnswerText.getVisibility(), Matchers.equalTo(View.GONE));\n        assertThat(widget.getAnswer(), equalTo(new IntegerData(15)));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/ExStringWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport android.text.InputType\nimport android.text.method.PasswordTransformationMethod\nimport android.view.Gravity\nimport android.view.View\nimport net.bytebuddy.utility.RandomString\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.data.StringData\nimport org.junit.Test\nimport org.mockito.Mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.android.utilities.Appearances\nimport org.odk.collect.android.widgets.base.GeneralExStringWidgetTest\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers\nimport org.odk.collect.android.widgets.utilities.StringRequester\n\nclass ExStringWidgetTest : GeneralExStringWidgetTest<ExStringWidget, StringData>() {\n    @Mock\n    var stringRequester: StringRequester? = null\n\n    override fun createWidget(): ExStringWidget {\n        whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT)\n\n        return ExStringWidget(\n            activity,\n            QuestionDetails(formEntryPrompt),\n            FakeWaitingForDataRegistry(),\n            stringRequester,\n            dependencies\n        )\n    }\n\n    override fun getNextAnswer(): StringData {\n        return StringData(RandomString.make())\n    }\n\n    @Throws(Exception::class)\n    override fun setUp() {\n        super.setUp()\n        whenever(formEntryPrompt.appearanceHint).thenReturn(\"\")\n    }\n\n    @Test\n    override fun verifyInputType() {\n        assertThat(\n            widget.binding.widgetAnswerText.binding.editText.inputType,\n            equalTo(\n                InputType.TYPE_CLASS_TEXT or\n                    InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or\n                    InputType.TYPE_TEXT_FLAG_MULTI_LINE\n            )\n        )\n        assertThat(\n            widget.binding.widgetAnswerText.binding.editText.transformationMethod,\n            equalTo(null)\n        )\n        assertThat(\n            widget.binding.widgetAnswerText.binding.textView.transformationMethod,\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun verifyInputTypeWithMaskedAppearance() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(Appearances.MASKED)\n        assertThat(\n            widget.binding.widgetAnswerText.binding.editText.inputType,\n            equalTo(\n                InputType.TYPE_CLASS_TEXT or\n                    InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or\n                    InputType.TYPE_TEXT_FLAG_MULTI_LINE or\n                    InputType.TYPE_TEXT_VARIATION_PASSWORD\n            )\n        )\n        assertThat(\n            widget.binding.widgetAnswerText.binding.editText.transformationMethod.javaClass,\n            equalTo(PasswordTransformationMethod::class.java)\n        )\n        assertThat(\n            widget.binding.widgetAnswerText.binding.textView.transformationMethod.javaClass,\n            equalTo(PasswordTransformationMethod::class.java)\n        )\n    }\n\n    @Test\n    fun answersShouldNotBeMaskedIfMaskedAppearanceIsNotUsed() {\n        assertThat(\n            spyWidget.binding.widgetAnswerText.binding.editText.transformationMethod,\n            equalTo(null)\n        )\n        assertThat(\n            spyWidget!!.binding.widgetAnswerText.binding.textView.transformationMethod,\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun answersShouldBeMaskedIfMaskedAppearanceIsUsed() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(Appearances.MASKED)\n\n        assertThat(\n            spyWidget.binding.widgetAnswerText.binding.editText.transformationMethod.javaClass,\n            equalTo(PasswordTransformationMethod::class.java)\n        )\n        assertThat(\n            spyWidget.binding.widgetAnswerText.binding.textView.transformationMethod.javaClass,\n            equalTo(PasswordTransformationMethod::class.java)\n        )\n    }\n\n    @Test\n    fun textFieldShouldUseTopStartGravityNoMatterHowManyRowsItContains() {\n        assertThat(\n            widget.binding.widgetAnswerText.binding.editText.gravity,\n            equalTo(Gravity.TOP or Gravity.START)\n        )\n\n        whenever(questionDef.getAdditionalAttribute(null, \"rows\")).thenReturn(\"5\")\n        assertThat(\n            widget.binding.widgetAnswerText.binding.editText.gravity,\n            equalTo(Gravity.TOP or Gravity.START)\n        )\n\n        whenever(questionDef.getAdditionalAttribute(null, \"rows\")).thenReturn(null)\n        whenever(formEntryPrompt.appearanceHint).thenReturn(Appearances.MULTILINE)\n        assertThat(\n            widget.binding.widgetAnswerText.binding.editText.gravity,\n            equalTo(Gravity.TOP or Gravity.START)\n        )\n    }\n\n    @Test\n    fun whenNumberOfRowsNotSpecifiedAndMultilineAppearanceNotUsesEditTextShouldHaveProperNumberOfLines() {\n        assertThat(widget.binding.widgetAnswerText.binding.editText.minLines, equalTo(0))\n        assertThat(widget.binding.widgetAnswerText.binding.editText.maxLines, equalTo(Int.MAX_VALUE))\n    }\n\n    @Test\n    fun whenNumberOfRowsSpecifiedEditTextShouldHaveProperNumberOfLines() {\n        whenever(questionDef.getAdditionalAttribute(null, \"rows\")).thenReturn(\"5\")\n\n        assertThat(widget.binding.widgetAnswerText.binding.editText.minLines, equalTo(5))\n        assertThat(widget.binding.widgetAnswerText.binding.editText.maxLines, equalTo(Int.MAX_VALUE))\n    }\n\n    @Test\n    fun whenMultilineAppearanceUsedEditTextShouldHaveProperNumberOfLines() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(Appearances.MULTILINE)\n\n        assertThat(widget.binding.widgetAnswerText.binding.editText.minLines, equalTo(4))\n        assertThat(widget.binding.widgetAnswerText.binding.editText.maxLines, equalTo(4))\n    }\n\n    @Test\n    override fun whenPromptHasHiddenAnswerAppearance_answerIsNotDisplayed() {\n        val prompt =\n            MockFormEntryPromptBuilder(QuestionWidgetHelpers.promptWithAppearance(Appearances.HIDDEN_ANSWER))\n                .withAnswer(StringData(\"original contents\"))\n                .build()\n\n        val widgetTestActivity = QuestionWidgetHelpers.widgetTestActivity()\n        val widget = ExStringWidget(\n            widgetTestActivity, QuestionDetails(prompt),\n            FakeWaitingForDataRegistry(), stringRequester, dependencies\n        )\n\n        // Check initial value is not shown\n        assertThat(\n            widget.binding.widgetAnswerText.visibility,\n            equalTo(View.GONE)\n        )\n        assertThat(widget.answer, equalTo(StringData(\"original contents\")))\n\n        // Check updates aren't shown\n        widget.setData(\"updated contents\")\n        assertThat(\n            widget.binding.widgetAnswerText.visibility,\n            equalTo(View.GONE)\n        )\n        assertThat(widget.answer, equalTo(StringData(\"updated contents\")))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/ExVideoWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.assertTextEquals\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.performClick\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.data.StringData\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.mockito.Mockito.mock\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.WidgetTestActivity\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.android.utilities.MediaUtils\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.android.widgets.base.FileWidgetTest\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry\nimport org.odk.collect.android.widgets.utilities.FileRequester\nimport org.odk.collect.android.widgets.video.ExVideoWidget\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidtest.onNodeWithClickLabel\nimport org.odk.collect.strings.R.string\nimport org.robolectric.shadows.ShadowToast\nimport java.io.File\nimport java.io.IOException\n\nclass ExVideoWidgetTest : FileWidgetTest<ExVideoWidget>() {\n    @get:Rule\n    val composeRule = createAndroidComposeRule<WidgetTestActivity>()\n    private var fileRequester = mock<FileRequester>()\n    private var questionMediaManager = mock<QuestionMediaManager>()\n    private var mediaUtils: MediaUtils = mock<MediaUtils>().apply {\n        whenever(isVideoFile(any())).thenReturn(true)\n    }\n    private val mediaWidgetAnswerViewModel = MediaWidgetAnswerViewModel(mock(), questionMediaManager, mediaUtils)\n    private val dependencies = QuestionWidget.Dependencies(\n        null,\n        mediaWidgetAnswerViewModel\n    )\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesMediaUtils(intentLauncher: IntentLauncher): MediaUtils {\n                return mediaUtils\n            }\n        })\n        whenever(formEntryPrompt.controlType).thenReturn(Constants.CONTROL_VIDEO_CAPTURE)\n        activity = composeRule.activity\n    }\n\n    override fun getInitialAnswer(): StringData {\n        return StringData(\"video1.mp4\")\n    }\n\n    override fun getNextAnswer(): StringData {\n        return StringData(\"video2.mp4\")\n    }\n\n    override fun createWidget(): ExVideoWidget {\n        return ExVideoWidget(\n            activity,\n            QuestionDetails(formEntryPrompt, readOnlyOverride),\n            dependencies,\n            FakeQuestionMediaManager(),\n            FakeWaitingForDataRegistry(),\n            fileRequester!!\n        ).also {\n            composeRule.activity.setContentView(it)\n        }\n    }\n\n    @Test\n    fun whenWidgetCreated_shouldTheLaunchButtonBeVisible() {\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertIsDisplayed()\n    }\n\n    @Test\n    fun whenWidgetCreated_shouldTheButtonHaveProperName() {\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertTextEquals(\"Launch\")\n    }\n\n    @Test\n    fun whenThereIsNoAnswer_shouldOnlyLaunchButtonBeVisible() {\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertIsDisplayed()\n        composeRule.onNodeWithClickLabel(activity.getString(string.play_video)).assertDoesNotExist()\n    }\n\n    @Test\n    fun whenThereIsAnswer_shouldBothTheButtonAndTheVideoViewBeVisible() {\n        whenever(formEntryPrompt.getAnswerText()).thenReturn(initialAnswer.displayText)\n        createWidget()\n\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertIsDisplayed()\n        composeRule.onNodeWithClickLabel(activity.getString(string.play_video)).assertIsDisplayed()\n    }\n\n    @Test\n    fun whenClearAnswerCall_shouldVideoViewBeHidden() {\n        whenever(formEntryPrompt.getAnswerText()).thenReturn(initialAnswer.displayText)\n\n        widget.clearAnswer()\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertIsDisplayed()\n        composeRule.onNodeWithClickLabel(activity.getString(string.play_video)).assertDoesNotExist()\n    }\n\n    @Test\n    fun whenCaptureVideoButtonClicked_exWidgetIntentLauncherShouldBeStarted() {\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).performClick()\n        verify(fileRequester).launch(activity, ApplicationConstants.RequestCodes.EX_VIDEO_CHOOSER, formEntryPrompt)\n    }\n\n    @Test\n    fun whenClickingOnPlayButton_shouldFileViewerByCalled() {\n        whenever(formEntryPrompt.getAnswerText()).thenReturn(initialAnswer.displayText)\n        whenever(questionMediaManager.getAnswerFile(initialAnswer.displayText)).thenReturn(File(initialAnswer.displayText))\n        createWidget()\n        composeRule.onNodeWithClickLabel(activity.getString(string.play_video)).performClick()\n\n        verify(mediaUtils).openFile(any(), any(), any<String>())\n    }\n\n    @Test\n    @Throws(IOException::class)\n    fun whenUnsupportedFileTypeAttached_shouldNotThatFileBeAdded() {\n        val answer = File.createTempFile(\"doc\", \".pdf\")\n        whenever(mediaUtils.isVideoFile(answer)).thenReturn(false)\n        widget.setData(answer)\n        assertThat(widget.answer, equalTo(null))\n    }\n\n    @Test\n    @Throws(IOException::class)\n    fun whenUnsupportedFileTypeAttached_shouldTheFileBeRemoved() {\n        val answer = File.createTempFile(\"doc\", \".pdf\")\n        whenever(mediaUtils.isVideoFile(answer)).thenReturn(false)\n        widget.setData(answer)\n        verify(mediaUtils).deleteMediaFile(answer.absolutePath)\n    }\n\n    @Test\n    @Throws(IOException::class)\n    fun whenUnsupportedFileTypeAttached_shouldToastBeDisplayed() {\n        val answer = File.createTempFile(\"doc\", \".pdf\")\n        whenever(mediaUtils.isVideoFile(answer)).thenReturn(false)\n        widget.setData(answer)\n        assertThat(\n            ShadowToast.getTextOfLatestToast(),\n            equalTo(\"Application returned an invalid file type\")\n        )\n    }\n\n    @Test\n    override fun usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        whenever(formEntryPrompt.isReadOnly).thenReturn(true)\n        createWidget()\n\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertDoesNotExist()\n    }\n\n    @Test\n    fun whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisabled() {\n        readOnlyOverride = true\n        whenever(formEntryPrompt.isReadOnly).thenReturn(false)\n        createWidget()\n\n        composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertDoesNotExist()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/GeoPointMapWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.data.GeoPointData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.widgets.interfaces.GeoDataRequester;\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.odk.collect.android.widgets.support.GeoWidgetHelpers.getRandomDoubleArray;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnly;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\nimport static org.odk.collect.geo.GeoUtils.parseGeometryPoint;\n\n@RunWith(AndroidJUnit4.class)\npublic class GeoPointMapWidgetTest {\n    private final GeoPointData answer = new GeoPointData(getRandomDoubleArray());\n\n    private GeoDataRequester geoDataRequester;\n    private WaitingForDataRegistry waitingForDataRegistry;\n\n    @Before\n    public void setup() {\n        geoDataRequester = mock(GeoDataRequester.class);\n        waitingForDataRegistry = mock(WaitingForDataRegistry.class);\n    }\n\n    @Test\n    public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(null));\n        assertNull(widget.getAnswer());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsPromptAnswer() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(answer));\n        assertEquals(widget.getAnswer().getDisplayText(),\n                new GeoPointData(parseGeometryPoint(answer.getDisplayText())).getDisplayText());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasInvalidAnswer_returnsNull() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        assertNull(widget.getAnswer());\n    }\n\n    @Test\n    public void creatingWidgetWithInvalidValue_doesNotUpdateWidgetDisplayedAnswer() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        assertEquals(widget.binding.geoAnswerText.getText(), \"\");\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_point));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_answerTextViewShowsCorrectAnswer() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(answer));\n        assertEquals(widget.binding.geoAnswerText.getText(), GeoWidgetUtils.getGeoPointAnswerToDisplay(widget.getContext(), answer.getDisplayText()));\n    }\n\n    @Test\n    public void whenPromptIsReadOnlyAndDoesNotHaveAnswer_geoButtonIsNotDisplayed() {\n        GeoPointMapWidget widget = createWidget(promptWithReadOnly());\n        assertEquals(widget.binding.simpleButton.getVisibility(), View.GONE);\n    }\n\n    @Test\n    public void whenPromptIsReadOnlyAndHasAnswer_viewGeoPointButtonIsShown() {\n        GeoPointMapWidget widget = createWidget(promptWithReadOnlyAndAnswer(answer));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.view_point));\n    }\n\n    @Test\n    public void whenPromptIsNotReadOnlyAndDoesNotHaveAnswer_startGeoPointButtonIsShown() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(null));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_point));\n    }\n\n    @Test\n    public void whenPromptIsNotReadOnlyAndHasAnswer_viewOrChangeLocationButtonIsShown() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(answer));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.view_or_change_point));\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(answer));\n        widget.clearAnswer();\n\n        assertEquals(widget.binding.geoAnswerText.getText(), \"\");\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_point));\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListeners() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.clearAnswer();\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void setData_updatesWidgetAnswer() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(answer.getDisplayText());\n        assertEquals(widget.getAnswer().getDisplayText(), answer.getDisplayText());\n    }\n\n    @Test\n    public void setDataWithInvalidValue_doesNotUpdateWidgetAnswer() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(\"blah\");\n        assertEquals(widget.getAnswer(), null);\n    }\n\n    @Test\n    public void setData_updatesWidgetDisplayedAnswer() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(answer.getDisplayText());\n        assertEquals(widget.binding.geoAnswerText.getText(), GeoWidgetUtils.getGeoPointAnswerToDisplay(widget.getContext(), answer.getDisplayText()));\n    }\n\n    @Test\n    public void setDataWithInvalidValue_doesNotUpdateWidgetDisplayedAnswer() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(\"blah\");\n        assertEquals(widget.binding.geoAnswerText.getText(), \"\");\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_point));\n    }\n\n    @Test\n    public void setData_whenDataIsNull_updatesButtonLabel() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(answer));\n        widget.setData(\"\");\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_point));\n    }\n\n    @Test\n    public void setData_whenDataIsNotNull_updatesButtonLabel() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(answer.getDisplayText());\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.view_or_change_point));\n    }\n\n    @Test\n    public void setData_callsValueChangeListener() {\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.setData(answer.getDisplayText());\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void clickingButtonAndAnswerTextViewForLong_callsLongClickListener() {\n        View.OnLongClickListener listener = mock(View.OnLongClickListener.class);\n        GeoPointMapWidget widget = createWidget(promptWithAnswer(null));\n\n        widget.setOnLongClickListener(listener);\n        widget.binding.simpleButton.performLongClick();\n        widget.binding.geoAnswerText.performLongClick();\n\n        verify(listener).onLongClick(widget.binding.simpleButton);\n        verify(listener).onLongClick(widget.binding.geoAnswerText);\n    }\n\n    @Test\n    public void buttonClick_requestsGeoPoint() {\n        FormEntryPrompt prompt = promptWithAnswer(answer);\n        GeoPointMapWidget widget = createWidget(prompt);\n        widget.binding.simpleButton.performClick();\n        verify(geoDataRequester).requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry);\n    }\n\n    @Test\n    public void buttonClick_requestsGeoPoint_whenAnswerIsCleared() {\n        FormEntryPrompt prompt = promptWithAnswer(answer);\n        GeoPointMapWidget widget = createWidget(prompt);\n        widget.clearAnswer();\n        widget.binding.simpleButton.performClick();\n\n        verify(geoDataRequester).requestGeoPoint(prompt, null, waitingForDataRegistry);\n    }\n\n    @Test\n    public void buttonClick_requestsGeoPoint_whenAnswerIsUpdated() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        GeoPointMapWidget widget = createWidget(prompt);\n        widget.setData(answer);\n        widget.binding.simpleButton.performClick();\n\n        verify(geoDataRequester).requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry);\n    }\n\n    private GeoPointMapWidget createWidget(FormEntryPrompt prompt) {\n        return new GeoPointMapWidget(widgetTestActivity(), new QuestionDetails(prompt),\n                waitingForDataRegistry, geoDataRequester, widgetDependencies());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/GeoPointWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.data.GeoPointData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.widgets.interfaces.GeoDataRequester;\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.odk.collect.android.widgets.support.GeoWidgetHelpers.getRandomDoubleArray;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnly;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\n@RunWith(AndroidJUnit4.class)\npublic class GeoPointWidgetTest {\n    private final GeoPointData answer = new GeoPointData(getRandomDoubleArray());\n\n    private GeoDataRequester geoDataRequester;\n    private WaitingForDataRegistry waitingForDataRegistry;\n\n    @Before\n    public void setup() {\n        geoDataRequester = mock(GeoDataRequester.class);\n        waitingForDataRegistry = mock(WaitingForDataRegistry.class);\n    }\n\n    @Test\n    public void usingReadOnlyOption_doesNotShowTheGeoButton() {\n        GeoPointWidget widget = createWidget(promptWithReadOnly());\n        assertEquals(widget.binding.simpleButton.getVisibility(), View.GONE);\n    }\n\n    @Test\n    public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(null));\n        assertNull(widget.getAnswer());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(answer));\n        assertEquals(widget.getAnswer().getDisplayText(), answer.getDisplayText());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasInvalidAnswer_returnsNull() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        assertNull(widget.getAnswer());\n    }\n\n    @Test\n    public void creatingWidgetWithInvalidValue_doesNotUpdateWidgetDisplayedAnswer() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        assertEquals(widget.binding.geoAnswerText.getText(), \"\");\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_point));\n    }\n\n    @Test\n    public void answerTextViewShouldShowCorrectAnswer() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(answer));\n        assertEquals(widget.binding.geoAnswerText.getText(), GeoWidgetUtils.getGeoPointAnswerToDisplay(widget.getContext(), answer.getDisplayText()));\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveHasAnswer_buttonShowsCorrectText() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(null));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_point));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_buttonShowsCorrectText() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(answer));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.change_point));\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(answer));\n        widget.clearAnswer();\n\n        assertEquals(widget.binding.geoAnswerText.getText(), \"\");\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_point));\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListeners() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.clearAnswer();\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void clickingButtonAndAnswerTextViewForLong_callsLongClickListeners() {\n        View.OnLongClickListener listener = mock(View.OnLongClickListener.class);\n        GeoPointWidget widget = createWidget(promptWithAnswer(null));\n\n        widget.setOnLongClickListener(listener);\n        widget.binding.simpleButton.performLongClick();\n        widget.binding.geoAnswerText.performLongClick();\n\n        verify(listener).onLongClick(widget.binding.simpleButton);\n        verify(listener).onLongClick(widget.binding.geoAnswerText);\n    }\n\n    @Test\n    public void setData_updatesWidgetAnswer() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(answer.getDisplayText());\n        assertEquals(widget.getAnswer().getDisplayText(), answer.getDisplayText());\n    }\n\n    @Test\n    public void setDataWithInvalidValue_doesNotUpdateWidgetAnswer() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(\"blah\");\n        assertEquals(widget.getAnswer(), null);\n    }\n\n    @Test\n    public void setData_updatesWidgetDisplayedAnswer() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(answer.getDisplayText());\n        assertEquals(widget.binding.geoAnswerText.getText(), GeoWidgetUtils.getGeoPointAnswerToDisplay(widget.getContext(), answer.getDisplayText()));\n    }\n\n    @Test\n    public void setDataWithInvalidValue_doesNotUpdateWidgetDisplayedAnswer() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(\"blah\");\n        assertEquals(widget.binding.geoAnswerText.getText(), \"\");\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_point));\n    }\n\n    @Test\n    public void setData_whenDataIsNull_updatesButtonLabel() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(answer));\n        widget.setData(\"\");\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_point));\n    }\n\n    @Test\n    public void setData_whenDataIsNotNull_updatesButtonLabel() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(answer.getDisplayText());\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.change_point));\n    }\n\n    @Test\n    public void setData_callsValueChangeListener() {\n        GeoPointWidget widget = createWidget(promptWithAnswer(null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.setData(answer.getDisplayText());\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void buttonClick_requestsGeoPoint() {\n        FormEntryPrompt prompt = promptWithAnswer(answer);\n        GeoPointWidget widget = createWidget(prompt);\n        widget.binding.simpleButton.performClick();\n        verify(geoDataRequester).requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry);\n    }\n\n    @Test\n    public void buttonClick_requestsGeoPoint_whenAnswerIsCleared() {\n        FormEntryPrompt prompt = promptWithAnswer(answer);\n        GeoPointWidget widget = createWidget(prompt);\n        widget.clearAnswer();\n        widget.binding.simpleButton.performClick();\n\n        verify(geoDataRequester).requestGeoPoint(prompt, null, waitingForDataRegistry);\n    }\n\n    @Test\n    public void buttonClick_requestsGeoPoint_whenAnswerIsUpdated() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        GeoPointWidget widget = createWidget(prompt);\n        widget.setData(answer);\n        widget.binding.simpleButton.performClick();\n\n        verify(geoDataRequester).requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry);\n    }\n\n    private GeoPointWidget createWidget(FormEntryPrompt prompt) {\n        return new GeoPointWidget(widgetTestActivity(), new QuestionDetails(prompt),\n                waitingForDataRegistry, geoDataRequester, widgetDependencies());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/GeoShapeWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.odk.collect.android.widgets.support.GeoWidgetHelpers.stringFromDoubleList;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnly;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.interfaces.GeoDataRequester;\n\n@RunWith(AndroidJUnit4.class)\npublic class GeoShapeWidgetTest {\n    private final String answer = stringFromDoubleList();\n\n    private GeoDataRequester geoDataRequester;\n\n    @Before\n    public void setup() {\n        geoDataRequester = mock(GeoDataRequester.class);\n    }\n\n    @Test\n    public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {\n        GeoShapeWidget widget = createWidget(promptWithAnswer(null));\n        assertNull(widget.getAnswer());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        GeoShapeWidget widget = createWidget(promptWithAnswer(new StringData(answer)));\n        assertEquals(widget.getAnswer().getDisplayText(), answer);\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_textViewDisplaysEmptyString() {\n        GeoShapeWidget widget = createWidget(promptWithAnswer(null));\n        assertEquals(widget.binding.geoAnswerText.getText().toString(), \"\");\n    }\n\n    @Test\n    public void whenPromptHasAnswer_textViewDisplaysAnswer() {\n        GeoShapeWidget widget = createWidget(promptWithAnswer(new StringData(answer)));\n        assertEquals(widget.binding.geoAnswerText.getText().toString(), answer);\n    }\n\n    @Test\n    public void whenPromptIsReadOnlyAndDoesNotHaveAnswer_geoButtonIsNotDisplayed() {\n        GeoShapeWidget widget = createWidget(promptWithReadOnly());\n        assertEquals(widget.binding.simpleButton.getVisibility(), View.GONE);\n    }\n\n    @Test\n    public void whenPromptIsReadOnlyAndHasAnswer_viewGeoShapeButtonIsShown() {\n        GeoShapeWidget widget = createWidget(promptWithReadOnlyAndAnswer(new StringData(answer)));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.view_polygon));\n    }\n\n    @Test\n    public void whenPromptIsNotReadOnlyAndDoesNotHaveAnswer_startGeoShapeButtonIsShown() {\n        GeoShapeWidget widget = createWidget(promptWithAnswer(null));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_polygon));\n    }\n\n    @Test\n    public void whenPromptIsNotReadOnlyAndHasAnswer_viewOrChangeGeoShapeButtonIsShown() {\n        GeoShapeWidget widget = createWidget(promptWithAnswer(new StringData(answer)));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.view_or_change_polygon));\n    }\n\n    @Test\n    public void clickingButtonAndAnswerTextViewForLong_callsLongClickListeners() {\n        View.OnLongClickListener listener = mock(View.OnLongClickListener.class);\n        GeoShapeWidget widget = createWidget(promptWithAnswer(null));\n\n        widget.setOnLongClickListener(listener);\n        widget.binding.simpleButton.performLongClick();\n        widget.binding.geoAnswerText.performLongClick();\n\n        verify(listener).onLongClick(widget.binding.simpleButton);\n        verify(listener).onLongClick(widget.binding.geoAnswerText);\n    }\n\n    @Test\n    public void buttonClick_requestsGeoShape() {\n        FormEntryPrompt prompt = promptWithAnswer(new StringData(answer));\n        GeoShapeWidget widget = createWidget(prompt);\n        widget.binding.simpleButton.performClick();\n        verify(geoDataRequester).requestGeoPoly(prompt);\n    }\n\n    @Test\n    public void buttonClick_requestsGeoShape_whenAnswerIsCleared() {\n        FormEntryPrompt prompt = promptWithAnswer(new StringData(answer));\n        GeoShapeWidget widget = createWidget(prompt);\n        widget.clearAnswer();\n        widget.binding.simpleButton.performClick();\n\n        verify(geoDataRequester).requestGeoPoly(prompt);\n    }\n\n    private GeoShapeWidget createWidget(FormEntryPrompt prompt) {\n        return new GeoShapeWidget(widgetTestActivity(), new QuestionDetails(prompt),\n                geoDataRequester, widgetDependencies());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/GeoTraceWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.GeoWidgetHelpers.stringFromDoubleList;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnly;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.interfaces.GeoDataRequester;\nimport org.odk.collect.maps.MapConfigurator;\n\n@RunWith(AndroidJUnit4.class)\npublic class GeoTraceWidgetTest {\n    private final String answer = stringFromDoubleList();\n\n    private GeoDataRequester geoDataRequester;\n    private MapConfigurator mapConfigurator;\n\n    @Before\n    public void setup() {\n        geoDataRequester = mock(GeoDataRequester.class);\n        mapConfigurator = mock(MapConfigurator.class);\n        when(mapConfigurator.isAvailable(any())).thenReturn(true);\n    }\n\n    @Test\n    public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {\n        GeoTraceWidget widget = createWidget(promptWithAnswer(null));\n        assertNull(widget.getAnswer());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        GeoTraceWidget widget = createWidget(promptWithAnswer(new StringData(answer)));\n        assertEquals(widget.getAnswer().getDisplayText(), answer);\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_textViewDisplaysEmptyString() {\n        GeoTraceWidget widget = createWidget(promptWithAnswer(null));\n        assertEquals(widget.binding.geoAnswerText.getText().toString(), \"\");\n    }\n\n    @Test\n    public void whenPromptHasAnswer_textViewDisplaysAnswer() {\n        GeoTraceWidget widget = createWidget(promptWithAnswer(new StringData(answer)));\n        assertEquals(widget.binding.geoAnswerText.getText().toString(), answer);\n    }\n\n    @Test\n    public void whenPromptIsReadOnlyAndDoesNotHaveAnswer_geoButtonIsNotDisplayed() {\n        GeoTraceWidget widget = createWidget(promptWithReadOnly());\n        assertEquals(widget.binding.simpleButton.getVisibility(), View.GONE);\n    }\n\n    @Test\n    public void whenPromptIsReadOnlyAndHasAnswer_viewGeoShapeButtonIsShown() {\n        GeoTraceWidget widget = createWidget(promptWithReadOnlyAndAnswer(new StringData(answer)));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.view_line));\n    }\n\n    @Test\n    public void whenPromptIsNotReadOnlyAndDoesNotHaveAnswer_startGeoShapeButtonIsShown() {\n        GeoTraceWidget widget = createWidget(promptWithAnswer(null));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.get_line));\n    }\n\n    @Test\n    public void whenPromptIsNotReadOnlyAndHasAnswer_viewOrChangeGeoShapeButtonIsShown() {\n        GeoTraceWidget widget = createWidget(promptWithAnswer(new StringData(answer)));\n        assertEquals(widget.binding.simpleButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.view_or_change_line));\n    }\n\n    @Test\n    public void clickingButtonAndAnswerTextViewForLong_callsLongClickListener() {\n        View.OnLongClickListener listener = mock(View.OnLongClickListener.class);\n        GeoTraceWidget widget = createWidget(promptWithAnswer(null));\n\n        widget.setOnLongClickListener(listener);\n        widget.binding.simpleButton.performLongClick();\n        widget.binding.geoAnswerText.performLongClick();\n\n        verify(listener).onLongClick(widget.binding.simpleButton);\n        verify(listener).onLongClick(widget.binding.geoAnswerText);\n    }\n\n    @Test\n    public void buttonClick_whenMapConfiguratorIsUnavailable_doesNotRequestGeoTrace() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        GeoTraceWidget widget = createWidget(prompt);\n\n        when(mapConfigurator.isAvailable(widget.getContext())).thenReturn(false);\n        widget.binding.simpleButton.performClick();\n\n        verify(geoDataRequester, never()).requestGeoPoly(prompt);\n        verify(mapConfigurator).showUnavailableMessage(widget.getContext());\n    }\n\n    @Test\n    public void buttonClick_whenMapConfiguratorIsAvailable_requestsGeoTrace() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        GeoTraceWidget widget = createWidget(prompt);\n        widget.binding.simpleButton.performClick();\n\n        verify(geoDataRequester).requestGeoPoly(prompt);\n    }\n\n    @Test\n    public void buttonClick_requestsGeoTrace_whenAnswerIsCleared() {\n        FormEntryPrompt prompt = promptWithAnswer(new StringData(answer));\n        GeoTraceWidget widget = createWidget(prompt);\n        widget.clearAnswer();\n        widget.binding.simpleButton.performClick();\n\n        verify(geoDataRequester).requestGeoPoly(prompt);\n    }\n\n    private GeoTraceWidget createWidget(FormEntryPrompt prompt) {\n        return new GeoTraceWidget(widgetTestActivity(), new QuestionDetails(prompt),\n                mapConfigurator, geoDataRequester, widgetDependencies());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/ImageWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.content.Intent;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.provider.MediaStore;\nimport android.view.View;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.util.Pair;\n\nimport net.bytebuddy.utility.RandomString;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.junit.Test;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.base.FileWidgetTest;\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.support.SynchronousImageLoader;\nimport org.odk.collect.imageloader.ImageLoader;\nimport org.odk.collect.shared.TempFiles;\n\nimport java.io.File;\nimport java.io.IOException;\n\nimport static java.util.Collections.singletonList;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.support.CollectHelpers.setupFakeReferenceManager;\nimport static org.robolectric.Shadows.shadowOf;\n\n/**\n * @author James Knight\n */\npublic class ImageWidgetTest extends FileWidgetTest<ImageWidget> {\n\n    private File currentFile;\n\n    @NonNull\n    @Override\n    public ImageWidget createWidget() {\n        QuestionMediaManager fakeQuestionMediaManager = new FakeQuestionMediaManager() {\n            @Override\n            public File getAnswerFile(String fileName) {\n                File result;\n                if (currentFile == null) {\n                    result = super.getAnswerFile(fileName);\n                } else {\n                    result = fileName.equals(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER) ? currentFile : null;\n                }\n                return result;\n            }\n        };\n        return new ImageWidget(activity, new QuestionDetails(formEntryPrompt, readOnlyOverride),\n                fakeQuestionMediaManager, new FakeWaitingForDataRegistry(), TempFiles.getPathInTempDir(), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public StringData getNextAnswer() {\n        return new StringData(RandomString.make());\n    }\n\n    @Test\n    public void buttonsShouldLaunchCorrectIntentsWhenThereIsNoCustomPackage() {\n        stubAllRuntimePermissionsGranted(true);\n\n        Intent intent = getIntentLaunchedByClick(R.id.capture_button);\n        assertActionEquals(MediaStore.ACTION_IMAGE_CAPTURE, intent);\n        assertThat(intent.getPackage(), equalTo(null));\n\n        intent = getIntentLaunchedByClick(R.id.choose_button);\n        assertActionEquals(Intent.ACTION_GET_CONTENT, intent);\n        assertTypeEquals(\"image/*\", intent);\n    }\n\n    @Test\n    public void buttonsShouldLaunchCorrectIntentsWhenCustomPackageIsSet() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAdditionalAttribute(\"intent\", \"com.customcameraapp\")\n                .build();\n\n        stubAllRuntimePermissionsGranted(true);\n\n        Intent intent = getIntentLaunchedByClick(R.id.capture_button);\n        assertActionEquals(MediaStore.ACTION_IMAGE_CAPTURE, intent);\n        assertThat(intent.getPackage(), equalTo(\"com.customcameraapp\"));\n\n        intent = getIntentLaunchedByClick(R.id.choose_button);\n        assertActionEquals(Intent.ACTION_GET_CONTENT, intent);\n        assertTypeEquals(\"image/*\", intent);\n    }\n\n    @Test\n    public void buttonsShouldNotLaunchIntentsWhenPermissionsDenied() {\n        stubAllRuntimePermissionsGranted(false);\n\n        assertNull(getIntentLaunchedByClick(R.id.capture_button));\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n\n        assertThat(getSpyWidget().binding.captureButton.getVisibility(), is(View.GONE));\n        assertThat(getSpyWidget().binding.chooseButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisabled() {\n        readOnlyOverride = true;\n        when(formEntryPrompt.isReadOnly()).thenReturn(false);\n\n        assertThat(getSpyWidget().binding.captureButton.getVisibility(), is(View.GONE));\n        assertThat(getSpyWidget().binding.chooseButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_hideImageViewAndErrorMessage() {\n        ImageWidget widget = createWidget();\n\n        assertThat(widget.getImageView().getVisibility(), is(View.GONE));\n        assertThat(widget.getImageView().getDrawable(), nullValue());\n\n        assertThat(widget.getErrorTextView().getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenTheAnswerImageCanNotBeLoaded_hideImageViewAndShowErrorMessage() throws IOException {\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader(true);\n            }\n        });\n\n        String imagePath = File.createTempFile(\"current\", \".bmp\").getAbsolutePath();\n        currentFile = new File(imagePath);\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER)\n                .build();\n\n        ImageWidget widget = createWidget();\n\n        assertThat(widget.getImageView().getVisibility(), is(View.GONE));\n        assertThat(widget.getImageView().getDrawable(), nullValue());\n\n        assertThat(widget.getErrorTextView().getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenPromptHasDefaultAnswer_doesNotShow() throws Exception {\n        String imagePath = File.createTempFile(\"default\", \".bmp\").getAbsolutePath();\n        ReferenceManager referenceManager = setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, imagePath)\n        ));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        ImageWidget widget = createWidget();\n        ImageView imageView = widget.getImageView();\n        assertThat(imageView.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenPromptHasCurrentAnswer_showsInImageView() throws Exception {\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        String imagePath = File.createTempFile(\"current\", \".bmp\").getAbsolutePath();\n        currentFile = new File(imagePath);\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER)\n                .build();\n\n        ImageWidget widget = createWidget();\n        ImageView imageView = widget.getImageView();\n        assertThat(imageView.getVisibility(), is(View.VISIBLE));\n        Drawable drawable = imageView.getDrawable();\n        assertThat(drawable, notNullValue());\n\n        String loadedPath = shadowOf(((BitmapDrawable) drawable).getBitmap()).getCreatedFromPath();\n        assertThat(loadedPath, equalTo(imagePath));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/IntegerWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static junit.framework.TestCase.assertEquals;\nimport static org.hamcrest.CoreMatchers.equalTo;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP;\n\nimport android.text.InputType;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.Constants;\nimport org.javarosa.core.model.data.IntegerData;\nimport org.junit.Test;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.base.GeneralStringWidgetTest;\n\n/**\n * @author James Knight\n */\npublic class IntegerWidgetTest extends GeneralStringWidgetTest<IntegerWidget, IntegerData> {\n\n    @NonNull\n    @Override\n    public IntegerWidget createWidget() {\n        when(formEntryPrompt.getDataType()).thenReturn(Constants.DATATYPE_INTEGER);\n        return new IntegerWidget(activity, new QuestionDetails(formEntryPrompt, readOnlyOverride), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public IntegerData getNextAnswer() {\n        return new IntegerData(randomInteger());\n    }\n\n    private int randomInteger() {\n        return Math.abs(random.nextInt()) % 1_000_000_000;\n    }\n\n    @Test\n    public void digitsAboveLimitOfNineShouldBeTruncatedFromRight() {\n        getWidget().widgetAnswerText.setAnswer(\"123456789123\");\n        assertEquals(\"123456789\", getWidget().getAnswerText());\n    }\n\n    @Test\n    public void separatorsShouldBeAddedWhenEnabled() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(THOUSANDS_SEP);\n        getWidget().widgetAnswerText.setAnswer(\"123456789\");\n\n        assertEquals(\"123,456,789\", getWidget().widgetAnswerText.getAnswer());\n        assertEquals(\"123,456,789\", getWidget().widgetAnswerText.getBinding().editText.getText().toString());\n        assertEquals(\"123,456,789\", getWidget().widgetAnswerText.getBinding().textView.getText().toString());\n    }\n\n    @Override\n    @Test\n    public void verifyInputType() {\n        IntegerWidget widget = getWidget();\n        assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED));\n        assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/OSMWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.content.Intent;\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport com.google.common.collect.ImmutableList;\n\nimport org.javarosa.core.model.FormDef;\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.model.osm.OSMTag;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.javarosawrapper.InstanceMetadata;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.utilities.ApplicationConstants;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.androidshared.system.IntentLauncherImpl;\nimport org.odk.collect.testshared.ErrorIntentLauncher;\nimport org.odk.collect.androidshared.system.IntentLauncher;\nimport org.robolectric.shadows.ShadowActivity;\n\nimport java.io.File;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnly;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\nimport static org.robolectric.Shadows.shadowOf;\n\n/**\n * @author James Knight\n */\n@RunWith(AndroidJUnit4.class)\npublic class OSMWidgetTest {\n    private final FakeWaitingForDataRegistry fakeWaitingForDataRegistry = new FakeWaitingForDataRegistry();\n\n    private final File instancePath = new File(\"instancePath/blah\");\n    private final File mediaFolder = new File(\"mediaFolderPath\");\n\n    private WidgetTestActivity widgetActivity;\n    private ShadowActivity shadowActivity;\n    private IntentLauncher intentLauncher;\n    private FormController formController;\n    private QuestionDef questionDef;\n\n    @Before\n    public void setUp() {\n        widgetActivity = widgetTestActivity();\n        shadowActivity = shadowOf(widgetActivity);\n\n        intentLauncher = IntentLauncherImpl.INSTANCE;\n        formController = mock(FormController.class);\n        FormDef formDef = mock(FormDef.class);\n        questionDef = mock(QuestionDef.class);\n\n        when(formController.getInstanceFile()).thenReturn(instancePath);\n        when(formController.getMediaFolder()).thenReturn(mediaFolder);\n        when(formController.getSubmissionMetadata()).thenReturn(\n                new InstanceMetadata(\"instanceId\", \"instanceTesTName\", null)\n        );\n        when(formController.getFormDef()).thenReturn(formDef);\n        when(formDef.getID()).thenReturn(0);\n    }\n\n    @Test\n    public void usingReadOnlyOption_doesNotShowButton() {\n        assertThat(createWidget(promptWithReadOnly()).binding.launchOpenMapKitButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_widgetShowsNullAnswer() {\n        OSMWidget widget = createWidget(promptWithAnswer(null));\n\n        assertThat(widget.binding.errorText.getVisibility(), is(View.GONE));\n        assertThat(widget.binding.osmFileHeaderText.getVisibility(), is(View.GONE));\n        assertThat(widget.binding.osmFileText.getText(), is(\"\"));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_widgetShowsCorrectAnswer() {\n        OSMWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n\n        assertThat(widget.binding.errorText.getVisibility(), is(View.GONE));\n        assertThat(widget.binding.osmFileHeaderText.getText(), is(widgetActivity.getString(org.odk.collect.strings.R.string.edited_osm_file)));\n        assertThat(widget.binding.osmFileText.getText(), is(\"blah\"));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_recaptureOsmButtonIsDisplayed() {\n        OSMWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        assertThat(widget.binding.launchOpenMapKitButton.getText(), is(widgetActivity.getString(org.odk.collect.strings.R.string.recapture_osm)));\n    }\n\n    @Test\n    public void getAnswer_whenPromptAnswerDoesNotHaveAnswer_returnsNull() {\n        assertThat(createWidget(promptWithAnswer(null)).getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        OSMWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(\"blah\"));\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        OSMWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        widget.clearAnswer();\n        assertThat(widget.binding.osmFileHeaderText.getVisibility(), is(View.GONE));\n        assertThat(widget.binding.osmFileText.getText(), is(\"\"));\n    }\n\n    @Test\n    public void clearAnswer_showsCaptureOsmButton() {\n        OSMWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        widget.clearAnswer();\n        assertThat(widget.binding.launchOpenMapKitButton.getText(), is(widgetActivity.getString(org.odk.collect.strings.R.string.capture_osm)));\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListeners() {\n        OSMWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n\n        widget.clearAnswer();\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void clickingButtonAndAnswerTextViewForLong_callsLongClickListener() {\n        View.OnLongClickListener listener = mock(View.OnLongClickListener.class);\n        OSMWidget widget = createWidget(promptWithAnswer(null));\n        widget.setOnLongClickListener(listener);\n\n        widget.binding.launchOpenMapKitButton.performLongClick();\n        widget.binding.osmFileText.performLongClick();\n        widget.binding.osmFileHeaderText.performLongClick();\n        widget.binding.errorText.performLongClick();\n\n        verify(listener).onLongClick(widget.binding.launchOpenMapKitButton);\n        verify(listener).onLongClick(widget.binding.osmFileText);\n\n        verify(listener, never()).onLongClick(widget.binding.osmFileHeaderText);\n        verify(listener, never()).onLongClick(widget.binding.errorText);\n    }\n\n    @Test\n    public void setData_updatesWidgetDisplayedAnswer() {\n        OSMWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(\"blah\");\n\n        assertThat(widget.binding.osmFileHeaderText.getVisibility(), is(View.VISIBLE));\n        assertThat(widget.binding.osmFileText.getVisibility(), is(View.VISIBLE));\n        assertThat(widget.binding.osmFileText.getText(), is(\"blah\"));\n    }\n\n    @Test\n    public void setData_showsRecaptureOsmButton() {\n        OSMWidget widget = createWidget(promptWithAnswer(null));\n        widget.setData(\"blah\");\n        assertThat(widget.binding.launchOpenMapKitButton.getText(), is(widgetActivity.getString(org.odk.collect.strings.R.string.recapture_osm)));\n    }\n\n    @Test\n    public void setData_callsValueChangeListeners() {\n        OSMWidget widget = createWidget(promptWithAnswer(null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n\n        widget.setData(\"blah\");\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void clickingButton_whenActivityIsNotAvailable_showsErrorTextView() {\n        intentLauncher = new ErrorIntentLauncher();\n\n        OSMWidget widget = createWidget(promptWithAnswer(null));\n        widget.binding.launchOpenMapKitButton.performClick();\n\n        assertThat(widget.binding.errorText.getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void clickingButton_whenActivityIsNotAvailable_CancelsWaitingForData() {\n        intentLauncher = new ErrorIntentLauncher();\n\n        OSMWidget widget = createWidget(promptWithAnswer(null));\n        widget.binding.launchOpenMapKitButton.performClick();\n\n        assertThat(fakeWaitingForDataRegistry.waiting.isEmpty(), is(true));\n    }\n\n    @Test\n    public void clickingButton_whenActivityIsAvailable_setsWidgetWaitingForData() {\n        FormIndex formIndex = mock(FormIndex.class);\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        when(prompt.getIndex()).thenReturn(formIndex);\n\n        OSMWidget widget = createWidget(prompt);\n        widget.binding.launchOpenMapKitButton.performClick();\n\n        assertThat(fakeWaitingForDataRegistry.waiting.contains(formIndex), is(true));\n    }\n\n    @Test\n    public void clickingButton_whenActivityIsAvailableAndPromptDoesNotHaveAnswer_launchesCorrectIntent() {\n        OSMWidget widget = createWidget(promptWithAnswer(null));\n        widget.binding.launchOpenMapKitButton.performClick();\n        assertIntentExtrasEquals(null);\n    }\n\n    @Test\n    public void clickingButton_whenActivityIsAvailableAndPromptHasAnswer_launchesCorrectIntent() {\n        OSMWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        widget.binding.launchOpenMapKitButton.performClick();\n        assertIntentExtrasEquals(\"blah\");\n    }\n\n    private OSMWidget createWidget(FormEntryPrompt prompt) {\n        when(prompt.getQuestion()).thenReturn(questionDef);\n        when(questionDef.getOsmTags()).thenReturn(ImmutableList.<OSMTag>of());\n\n        return new OSMWidget(widgetActivity, new QuestionDetails(prompt),\n                fakeWaitingForDataRegistry, intentLauncher, formController, widgetDependencies());\n    }\n\n    private void assertIntentExtrasEquals(String fileName) {\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertThat(shadowActivity.getNextStartedActivityForResult().requestCode, is(ApplicationConstants.RequestCodes.OSM_CAPTURE));\n        assertThat(startedIntent.getAction(), is(Intent.ACTION_SEND));\n        assertThat(startedIntent.getType(), is(\"text/plain\"));\n\n        assertThat(startedIntent.getStringExtra(OSMWidget.FORM_ID), is(\"0\"));\n        assertThat(startedIntent.getStringExtra(OSMWidget.INSTANCE_ID), is(\"instanceId\"));\n        assertThat(startedIntent.getStringExtra(OSMWidget.INSTANCE_DIR), is(\"instancePath\"));\n        assertThat(startedIntent.getStringExtra(OSMWidget.FORM_FILE_NAME), is(\"mediaFolderPath\"));\n        assertThat(startedIntent.getStringExtra(OSMWidget.OSM_EDIT_FILE_NAME), is(fileName));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/PrinterWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.data.IAnswerData\nimport org.junit.Test\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoInteractions\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.formentry.PrinterWidgetViewModel\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.support.WidgetTestActivity\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.android.widgets.base.QuestionWidgetTest\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\nimport org.odk.collect.printer.HtmlPrinter\nimport org.odk.collect.qrcode.zxing.QRCodeCreator\nimport org.odk.collect.testshared.FakeScheduler\n\nclass PrinterWidgetTest : QuestionWidgetTest<PrinterWidget, IAnswerData>() {\n    private val scheduler = FakeScheduler()\n    private val questionMediaManager = mock<QuestionMediaManager>()\n    private val qrCodeCreator = mock<QRCodeCreator>()\n    private val htmlPrinter = mock<HtmlPrinter>()\n\n    override fun createWidget() = PrinterWidget(\n        activity,\n        QuestionDetails(formEntryPrompt),\n        PrinterWidgetViewModel(scheduler, qrCodeCreator, htmlPrinter),\n        questionMediaManager,\n        dependencies\n    )\n\n    @Test\n    fun `clicking the button should trigger printing html document if answer exists`() {\n        whenever(formEntryPrompt.answerText).thenReturn(\"blah\")\n\n        val widget = createWidget()\n        widget.findViewById<MultiClickSafeMaterialButton>(R.id.printer_button).performClick()\n        scheduler.runBackground()\n        scheduler.runForeground()\n\n        verify(htmlPrinter).print(any(), any())\n    }\n\n    @Test\n    fun `clicking the button should not trigger printing if there is no answer`() {\n        whenever(formEntryPrompt.answerText).thenReturn(null)\n\n        val widget = createWidget()\n        widget.findViewById<MultiClickSafeMaterialButton>(R.id.printer_button).performClick()\n\n        verifyNoInteractions(htmlPrinter)\n    }\n\n    @Test\n    override fun widgetShouldBeRegisteredForContextMenu() {\n        val viewsRegisterForContextMenu = (activity as WidgetTestActivity).viewsRegisterForContextMenu\n\n        assertThat(viewsRegisterForContextMenu.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    override fun usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() = Unit // ignore\n\n    @Test\n    override fun callingClearShouldRemoveTheExistingAnswer() = Unit // ignore\n\n    @Test\n    override fun callingClearShouldCallValueChangeListeners() = Unit // ignore\n\n    @Test\n    override fun getAnswerShouldReturnExistingAnswerIfPromptHasExistingAnswer() = Unit // ignore\n\n    @Test\n    override fun whenReadOnlyQuestionHasAnswer_answerContainerShouldBeDisplayed() = Unit // ignore\n\n    override fun getNextAnswer() = null\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/QuestionWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.odk.collect.android.support.CollectHelpers.setupFakeReferenceManager;\nimport static java.util.Arrays.asList;\n\nimport android.content.Context;\n\nimport androidx.core.util.Pair;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.audio.AudioButton;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.audioclips.Clip;\n\n@RunWith(AndroidJUnit4.class)\npublic class QuestionWidgetTest {\n\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    @Before\n    public void setup() throws Exception {\n        overrideDependencyModule();\n    }\n\n    @Test\n    public void whenQuestionHasAudio_audioButtonUsesIndexAsClipID() throws Exception {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withAudioURI(\"ref\")\n                .build();\n\n        WidgetTestActivity activity = CollectHelpers.createThemedActivity(WidgetTestActivity.class);\n        AudioPlayer audioPlayer = mock();\n        TestWidget widget = new TestWidget(activity, new QuestionDetails(prompt), new QuestionWidget.Dependencies(audioPlayer, null));\n\n        AudioButton audioButton = widget.getAudioVideoImageTextLabel().findViewById(R.id.audioButton);\n        audioButton.performClick();\n        verify(audioPlayer).play(new Clip(\"i am index\", \"blah.mp3\"));\n    }\n\n    private void overrideDependencyModule() throws Exception {\n        ReferenceManager referenceManager = setupFakeReferenceManager(asList(new Pair<>(\"ref\", \"blah.mp3\")));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n        });\n    }\n\n    private static class TestWidget extends QuestionWidget {\n\n        TestWidget(Context context, QuestionDetails questionDetails, Dependencies dependencies) {\n            super(context, dependencies, questionDetails);\n        }\n\n        @Override\n        public IAnswerData getAnswer() {\n            return null;\n        }\n\n        @Override\n        public void clearAnswer() {\n\n        }\n\n        @Override\n        public void setOnLongClickListener(OnLongClickListener l) {\n\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/RatingWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.view.View;\n\nimport androidx.test.core.view.MotionEventBuilder;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.RangeQuestion;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\n\nimport java.math.BigDecimal;\n\nimport static android.view.MotionEvent.ACTION_DOWN;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\n\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithQuestionAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndQuestionDef;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\n@RunWith(AndroidJUnit4.class)\npublic class RatingWidgetTest {\n\n    private RangeQuestion rangeQuestion;\n\n    @Before\n    public void setup() {\n        rangeQuestion = mock(RangeQuestion.class);\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(5));\n    }\n\n    @Test\n    public void usingReadOnly_makesAllClickableElementsDisabled() {\n        RatingWidget widget = createWidget(promptWithReadOnlyAndQuestionDef(rangeQuestion));\n        assertThat(widget.binding.ratingBar1.isEnabled(), equalTo(false));\n        assertThat(widget.binding.ratingBar2.isEnabled(), equalTo(false));\n    }\n\n    @Test\n    public void ratingWidgetShowsCorrectViewForLessNumberOfStars() {\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(4));\n        RatingWidget widget = createWidget(promptWithReadOnlyAndQuestionDef(rangeQuestion));\n\n        assertThat(widget.binding.ratingBar1.getNumStars(), equalTo(4));\n        assertThat(widget.binding.ratingBar1.getMax(), equalTo(4));\n        assertThat(widget.binding.ratingBar2.getVisibility(), equalTo(View.GONE));\n    }\n\n    @Test\n    public void ratingWidgetShowsCorrectViewForMoreNumberOfStars() {\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(8));\n        RatingWidget widget = createWidget(promptWithReadOnlyAndQuestionDef(rangeQuestion));\n\n        assertThat(widget.binding.ratingBar1.getNumStars(), equalTo(5));\n        assertThat(widget.binding.ratingBar1.getMax(), equalTo(5));\n        assertThat(widget.binding.ratingBar2.getVisibility(), equalTo(View.VISIBLE));\n        assertThat(widget.binding.ratingBar2.getNumStars(), equalTo(3));\n        assertThat(widget.binding.ratingBar2.getMax(), equalTo(3));\n    }\n\n    @Test\n    public void getAnswer_whenPromptAnswerDoesNotHaveAnswer_returnsNull() {\n        assertThat(createWidget(promptWithReadOnlyAndQuestionDef(rangeQuestion)).getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer_forRatingBarInSingleLine() {\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, new StringData(\"3\")));\n        assertThat(widget.getAnswer().getValue(), equalTo(3));\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer_forRatingBarInMultipleLines() {\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(10));\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, new StringData(\"7\")));\n        assertThat(widget.getAnswer().getValue(), equalTo(7));\n    }\n\n    @Test\n    public void settingRatingOnTopRatingBar_deselectsAllStarsOnBottomRatingBar() {\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(10));\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, null));\n        widget.binding.ratingBar1.setRating(4.0F);\n\n        assertThat(widget.binding.ratingBar2.getRating(), equalTo(0.0F));\n    }\n\n    @Test\n    public void settingRatingOnBottomRatingBar_selectsAllStarsOnTopRatingBar() {\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(10));\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, null));\n        widget.binding.ratingBar2.setRating(4.0F);\n\n        assertThat(widget.binding.ratingBar1.getRating(), equalTo(5.0F));\n    }\n\n    @Test\n    public void whenUserTouchesTopRatingBar_bottomRatingBarShowsZeroRating() {\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(10));\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, new StringData(\"8\")));\n        widget.binding.ratingBar1.onTouchEvent(MotionEventBuilder.newBuilder().setAction(ACTION_DOWN).build());\n        widget.binding.ratingBar1.setRating(5.0F);\n\n        assertThat(widget.binding.ratingBar2.getRating(), equalTo(0.0F));\n    }\n\n    @Test\n    public void whenUserTouchesBottomRatingBar_topRatingBarShowsMaximumRating() {\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(10));\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, new StringData(\"8\")));\n        widget.binding.ratingBar2.onTouchEvent(MotionEventBuilder.newBuilder().setAction(ACTION_DOWN).build());\n\n        assertThat(widget.binding.ratingBar1.getRating(), equalTo(5.0F));\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_noStarsAreHighlightedOnRatingBar() {\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, null));\n        assertThat(widget.binding.ratingBar1.getRating(), equalTo(0.0F));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_correctNumberOfStarsAreHighlighted_forSmallerRatingBar() {\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, new StringData(\"3\")));\n        assertThat(widget.binding.ratingBar1.getRating(), equalTo(3.0F));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_correctNumberOfStarsAreHighlighted_forRatingBarInMultipleLines() {\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(10));\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, new StringData(\"7\")));\n        assertThat(widget.binding.ratingBar2.getRating(), equalTo(2.0F));\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, new StringData(\"3\")));\n        widget.clearAnswer();\n        assertThat(widget.binding.ratingBar1.getRating(), equalTo(0.0F));\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListeners() {\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, new StringData(\"3\")));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.setValueChangedListener(valueChangedListener);\n        widget.clearAnswer();\n\n        verify(valueChangedListener, atLeastOnce()).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void changingRating_callsValueChangeListeners_forRatingBarInSingleLine() {\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.setValueChangedListener(valueChangedListener);\n        widget.binding.ratingBar1.setRating(4.0F);\n\n        verify(valueChangedListener, atLeastOnce()).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void changingRating_callsValueChangeListeners_forRatingBarInMultipleLines() {\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(10));\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.setValueChangedListener(valueChangedListener);\n        widget.binding.ratingBar2.setRating(4.0F);\n\n        verify(valueChangedListener, atLeastOnce()).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void ratingBar_doesNotAllowUserToSetDecimalRating_forRatingBarInSingleLine() {\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, null));\n        widget.binding.ratingBar1.setRating(1.8F);\n        assertThat(widget.binding.ratingBar1.getRating(), equalTo(2.0F));\n    }\n\n    @Test\n    public void ratingBar_doesNotAllowUserToSetDecimalRating_forRatingBarInMultipleLines() {\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(10));\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, null));\n        widget.binding.ratingBar2.setRating(1.8F);\n\n        assertThat(widget.binding.ratingBar2.getRating(), equalTo(2.0F));\n    }\n\n    @Test\n    public void clickingRatingBarForLong_callsLongClickListener() {\n        View.OnLongClickListener listener = mock(View.OnLongClickListener.class);\n        RatingWidget widget = createWidget(promptWithQuestionAndAnswer(rangeQuestion, null));\n        widget.setOnLongClickListener(listener);\n        widget.binding.ratingBar1.performLongClick();\n        widget.binding.ratingBar2.performLongClick();\n\n        verify(listener).onLongClick(widget.binding.ratingBar1);\n        verify(listener).onLongClick(widget.binding.ratingBar2);\n    }\n\n    private RatingWidget createWidget(FormEntryPrompt prompt) {\n        return new RatingWidget(widgetTestActivity(), new QuestionDetails(prompt), widgetDependencies());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/SignatureWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.support.CollectHelpers.setupFakeReferenceManager;\nimport static org.robolectric.Shadows.shadowOf;\nimport static java.util.Collections.singletonList;\n\nimport android.content.Intent;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.view.View;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.util.Pair;\n\nimport net.bytebuddy.utility.RandomString;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.junit.Test;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.android.widgets.base.FileWidgetTest;\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.support.SynchronousImageLoader;\nimport org.odk.collect.draw.DrawActivity;\nimport org.odk.collect.imageloader.ImageLoader;\nimport org.odk.collect.shared.TempFiles;\n\nimport java.io.File;\nimport java.io.IOException;\n\n/**\n *  @author James Knight\n */\npublic class SignatureWidgetTest extends FileWidgetTest<SignatureWidget> {\n\n    private File currentFile;\n\n    @NonNull\n    @Override\n    public SignatureWidget createWidget() {\n        QuestionMediaManager fakeQuestionMediaManager = new FakeQuestionMediaManager() {\n            @Override\n            public File getAnswerFile(String fileName) {\n                File result;\n                if (currentFile == null) {\n                    result = super.getAnswerFile(fileName);\n                } else {\n                    result = fileName.equals(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER) ? currentFile : null;\n                }\n                return result;\n            }\n        };\n        return new SignatureWidget(activity, new QuestionDetails(formEntryPrompt, readOnlyOverride),\n                fakeQuestionMediaManager, new FakeWaitingForDataRegistry(), TempFiles.getPathInTempDir(), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public StringData getNextAnswer() {\n        return new StringData(RandomString.make());\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n\n        assertThat(getSpyWidget().binding.signButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisabled() {\n        readOnlyOverride = true;\n        when(formEntryPrompt.isReadOnly()).thenReturn(false);\n\n        assertThat(getSpyWidget().binding.signButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_hideImageViewAndErrorMessage() {\n        SignatureWidget widget = createWidget();\n\n        assertThat(widget.getImageView().getVisibility(), is(View.GONE));\n        assertThat(widget.getImageView().getDrawable(), nullValue());\n\n        assertThat(widget.getErrorTextView().getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenTheAnswerImageCanNotBeLoaded_hideImageViewAndShowErrorMessage() throws IOException {\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader(true);\n            }\n        });\n\n        String imagePath = File.createTempFile(\"current\", \".bmp\").getAbsolutePath();\n        currentFile = new File(imagePath);\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER)\n                .build();\n\n        SignatureWidget widget = createWidget();\n\n        assertThat(widget.getImageView().getVisibility(), is(View.GONE));\n        assertThat(widget.getImageView().getDrawable(), nullValue());\n\n        assertThat(widget.getErrorTextView().getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenPromptHasDefaultAnswer_showsInImageView() throws Exception {\n        String imagePath = File.createTempFile(\"default\", \".bmp\").getAbsolutePath();\n\n        ReferenceManager referenceManager = setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, imagePath)\n        ));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        SignatureWidget widget = createWidget();\n        ImageView imageView = widget.getImageView();\n        assertThat(imageView.getVisibility(), is(View.VISIBLE));\n        Drawable drawable = imageView.getDrawable();\n        assertThat(drawable, notNullValue());\n\n        String loadedPath = shadowOf(((BitmapDrawable) drawable).getBitmap()).getCreatedFromPath();\n        assertThat(loadedPath, equalTo(imagePath));\n    }\n\n    @Test\n    public void whenPromptHasCurrentAnswer_showsInImageView() throws Exception {\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        String imagePath = File.createTempFile(\"current\", \".bmp\").getAbsolutePath();\n        currentFile = new File(imagePath);\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.USER_SPECIFIED_IMAGE_ANSWER)\n                .build();\n\n        SignatureWidget widget = createWidget();\n        ImageView imageView = widget.getImageView();\n        assertThat(imageView.getVisibility(), is(View.VISIBLE));\n        Drawable drawable = imageView.getDrawable();\n        assertThat(drawable, notNullValue());\n\n        String loadedPath = shadowOf(((BitmapDrawable) drawable).getBitmap()).getCreatedFromPath();\n        assertThat(loadedPath, equalTo(imagePath));\n    }\n\n    @Test\n    public void whenPromptHasDefaultAnswer_passUriToDrawActivity() throws Exception {\n        File file = File.createTempFile(\"default\", \".bmp\");\n        String imagePath = file.getAbsolutePath();\n\n        ReferenceManager referenceManager = setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, imagePath)\n        ));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n\n            @Override\n            public ImageLoader providesImageLoader() {\n                return new SynchronousImageLoader();\n            }\n        });\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        Intent intent = getIntentLaunchedByClick(R.id.sign_button);\n        assertComponentEquals(activity, DrawActivity.class, intent);\n        assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_SIGNATURE, intent);\n        assertExtraEquals(DrawActivity.REF_IMAGE, Uri.fromFile(file), intent);\n    }\n\n    @Test\n    public void whenPromptHasDefaultAnswerThatDoesNotExist_doNotPassUriToDrawActivity() throws Exception {\n        ReferenceManager referenceManager = setupFakeReferenceManager(singletonList(\n                new Pair<>(DrawWidgetTest.DEFAULT_IMAGE_ANSWER, \"/something\")\n        ));\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n        });\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withAnswerDisplayText(DrawWidgetTest.DEFAULT_IMAGE_ANSWER)\n                .build();\n\n        Intent intent = getIntentLaunchedByClick(R.id.sign_button);\n        assertComponentEquals(activity, DrawActivity.class, intent);\n        assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_SIGNATURE, intent);\n        assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_doNotPassUriToDrawActivity() {\n        Intent intent = getIntentLaunchedByClick(R.id.sign_button);\n        assertComponentEquals(activity, DrawActivity.class, intent);\n        assertExtraEquals(DrawActivity.OPTION, DrawActivity.OPTION_SIGNATURE, intent);\n        assertThat(intent.hasExtra(DrawActivity.REF_IMAGE), is(false));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/StringNumberWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static junit.framework.TestCase.assertEquals;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.utilities.Appearances.THOUSANDS_SEP;\n\nimport android.text.InputType;\n\nimport androidx.annotation.NonNull;\n\nimport net.bytebuddy.utility.RandomString;\n\nimport org.javarosa.core.model.Constants;\nimport org.javarosa.core.model.data.StringData;\nimport org.junit.Test;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.base.GeneralStringWidgetTest;\n\n/**\n * @author James Knight\n */\npublic class StringNumberWidgetTest extends GeneralStringWidgetTest<StringNumberWidget, StringData> {\n\n    @NonNull\n    @Override\n    public StringNumberWidget createWidget() {\n        when(formEntryPrompt.getDataType()).thenReturn(Constants.DATATYPE_TEXT);\n        return new StringNumberWidget(activity, new QuestionDetails(formEntryPrompt, readOnlyOverride), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public StringData getNextAnswer() {\n        return new StringData(RandomString.make());\n    }\n\n    @Test\n    public void digitsNumberShouldNotBeLimited() {\n        getWidget().widgetAnswerText.setAnswer(\"123456789123456789123456789123456789\");\n        assertEquals(\"123456789123456789123456789123456789\", getWidget().getAnswerText());\n    }\n\n    @Test\n    public void separatorsShouldBeAddedWhenEnabled() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(THOUSANDS_SEP);\n        getWidget().widgetAnswerText.setAnswer(\"123456789123456789123456789123456789\");\n\n        assertEquals(\"123,456,789,123,456,789,123,456,789,123,456,789\", getWidget().widgetAnswerText.getAnswer());\n        assertEquals(\"123,456,789,123,456,789,123,456,789,123,456,789\", getWidget().widgetAnswerText.getBinding().editText.getText().toString());\n        assertEquals(\"123,456,789,123,456,789,123,456,789,123,456,789\", getWidget().widgetAnswerText.getBinding().textView.getText().toString());\n    }\n\n    @Override\n    @Test\n    public void verifyInputType() {\n        StringNumberWidget widget = getWidget();\n        assertThat(widget.widgetAnswerText.getBinding().editText.getInputType(), equalTo(InputType.TYPE_CLASS_NUMBER));\n        assertThat(widget.widgetAnswerText.getBinding().editText.getTransformationMethod(), equalTo(null));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/StringWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport android.text.InputType\nimport android.text.method.PasswordTransformationMethod\nimport android.view.Gravity\nimport net.bytebuddy.utility.RandomString\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.data.StringData\nimport org.junit.Test\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.utilities.Appearances\nimport org.odk.collect.android.widgets.base.GeneralStringWidgetTest\n\nclass StringWidgetTest : GeneralStringWidgetTest<StringWidget, StringData>() {\n    override fun createWidget(): StringWidget {\n        whenever(formEntryPrompt.dataType).thenReturn(Constants.DATATYPE_TEXT)\n\n        return StringWidget(\n            activity,\n            QuestionDetails(formEntryPrompt, readOnlyOverride),\n            dependencies\n        )\n    }\n\n    override fun getNextAnswer(): StringData {\n        return StringData(RandomString.make())\n    }\n\n    @Test\n    override fun verifyInputType() {\n        assertThat(\n            widget.widgetAnswerText.binding.editText.inputType,\n            equalTo(\n                InputType.TYPE_CLASS_TEXT or\n                    InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or\n                    InputType.TYPE_TEXT_FLAG_MULTI_LINE\n            )\n        )\n        assertThat(\n            widget.widgetAnswerText.binding.editText.transformationMethod,\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun verifyInputTypeWithMaskedAppearance() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(Appearances.MASKED)\n\n        assertThat(\n            widget.widgetAnswerText.binding.editText.inputType,\n            equalTo(\n                InputType.TYPE_CLASS_TEXT or\n                    InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or\n                    InputType.TYPE_TEXT_FLAG_MULTI_LINE or\n                    InputType.TYPE_TEXT_VARIATION_PASSWORD\n            )\n        )\n        assertThat(\n            widget.widgetAnswerText.binding.editText.transformationMethod.javaClass,\n            equalTo(PasswordTransformationMethod::class.java)\n        )\n    }\n\n    @Test\n    fun answersShouldNotBeMaskedIfMaskedAppearanceIsNotUsed() {\n        assertThat(\n            spyWidget.widgetAnswerText.binding.editText.transformationMethod,\n            equalTo(null)\n        )\n        assertThat(\n            spyWidget.widgetAnswerText.binding.textView.transformationMethod,\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun answersShouldBeMaskedIfMaskedAppearanceIsUsed() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(Appearances.MASKED)\n\n        assertThat(\n            spyWidget.widgetAnswerText.binding.editText.transformationMethod.javaClass,\n            equalTo(PasswordTransformationMethod::class.java)\n        )\n        assertThat(\n            spyWidget.widgetAnswerText.binding.textView.transformationMethod.javaClass,\n            equalTo(PasswordTransformationMethod::class.java)\n        )\n    }\n\n    @Test\n    fun textFieldShouldUseTopStartGravityNoMatterHowManyRowsItContains() {\n        assertThat(widget.widgetAnswerText.binding.editText.gravity, equalTo(Gravity.TOP or Gravity.START))\n\n        whenever(questionDef.getAdditionalAttribute(null, \"rows\")).thenReturn(\"5\")\n        assertThat(widget.widgetAnswerText.binding.editText.gravity, equalTo(Gravity.TOP or Gravity.START))\n\n        whenever(questionDef.getAdditionalAttribute(null, \"rows\")).thenReturn(null)\n        whenever(formEntryPrompt.appearanceHint).thenReturn(Appearances.MULTILINE)\n        assertThat(widget.widgetAnswerText.binding.editText.gravity, equalTo(Gravity.TOP or Gravity.START))\n    }\n\n    @Test\n    fun whenNumberOfRowsNotSpecifiedAndMultilineAppearanceNotUsesEditTextShouldHaveProperNumberOfLines() {\n        assertThat(widget.widgetAnswerText.binding.editText.minLines, equalTo(0))\n        assertThat(widget.widgetAnswerText.binding.editText.maxLines, equalTo(Int.MAX_VALUE))\n    }\n\n    @Test\n    fun whenNumberOfRowsSpecifiedEditTextShouldHaveProperNumberOfLines() {\n        whenever(questionDef.getAdditionalAttribute(null, \"rows\")).thenReturn(\"5\")\n\n        assertThat(widget.widgetAnswerText.binding.editText.minLines, equalTo(5))\n        assertThat(widget.widgetAnswerText.binding.editText.maxLines, equalTo(Int.MAX_VALUE))\n    }\n\n    @Test\n    fun whenMultilineAppearanceUsedEditTextShouldHaveProperNumberOfLines() {\n        whenever(formEntryPrompt.appearanceHint).thenReturn(Appearances.MULTILINE)\n\n        assertThat(widget.widgetAnswerText.binding.editText.minLines, equalTo(4))\n        assertThat(widget.widgetAnswerText.binding.editText.maxLines, equalTo(4))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/TriggerWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport android.view.View;\nimport android.widget.CheckBox;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.not;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.verify;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnly;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\n@RunWith(AndroidJUnit4.class)\npublic class TriggerWidgetTest {\n\n    @Test\n    public void getAnswer_whenPromptAnswerDoesNotHaveAnswer_returnsNull() {\n        assertThat(createWidget(promptWithAnswer(null)).getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        TriggerWidget widget = createWidget(promptWithAnswer(new StringData(\"OK\")));\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(\"OK\"));\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        TriggerWidget widget = createWidget(promptWithAnswer(new StringData(\"OK\")));\n\n        widget.clearAnswer();\n        assertThat(widget.getAnswer(), nullValue());\n    }\n\n    @Test\n    public void usingReadOnlyOption_makesAllClickableElementsDisabled() {\n        TriggerWidget widget = createWidget(promptWithReadOnly());\n        assertThat(widget.getCheckBox().getVisibility(), equalTo(View.VISIBLE));\n        assertThat(widget.getCheckBox().isEnabled(), equalTo(Boolean.FALSE));\n    }\n\n    @Test\n    public void whenPromptAnswerDoesNotHaveAnswer_checkboxIsUnchecked() {\n        TriggerWidget widget = createWidget(promptWithAnswer(null));\n        assertThat(widget.getCheckBox().isChecked(), equalTo(false));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_checkboxIsChecked() {\n        TriggerWidget widget = createWidget(promptWithAnswer(new StringData(\"OK\")));\n        assertThat(widget.getCheckBox().isChecked(), equalTo(true));\n    }\n\n    @Test\n    public void checkingCheckbox_setsAnswer() {\n        TriggerWidget widget = createWidget(promptWithAnswer(null));\n        CheckBox triggerButton = widget.getCheckBox();\n\n        triggerButton.setChecked(true);\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(\"OK\"));\n\n        triggerButton.setChecked(false);\n        assertThat(widget.getAnswer(), nullValue());\n    }\n\n    @Test\n    public void checkingCheckbox_callsValueChangeListeners() {\n        TriggerWidget widget = createWidget(promptWithAnswer(null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        CheckBox triggerButton = widget.getCheckBox();\n\n        triggerButton.setChecked(true);\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5523\n    public void everyTriggerWidgetShouldHaveCheckboxWithUniqueID() {\n        TriggerWidget widget1 = createWidget(promptWithAnswer(new StringData(\"OK\")));\n        TriggerWidget widget2 = createWidget(promptWithAnswer(new StringData(\"OK\")));\n\n        assertThat(widget1.getCheckBox().getId(), not(equalTo(widget2.getCheckBox().getId())));\n    }\n\n    private TriggerWidget createWidget(FormEntryPrompt prompt) {\n        return new TriggerWidget(widgetTestActivity(), new QuestionDetails(prompt), widgetDependencies());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/UrlWidgetTest.java",
    "content": "package org.odk.collect.android.widgets;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\nimport android.app.Activity;\nimport android.net.Uri;\nimport android.view.View.OnLongClickListener;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.webpage.WebPageService;\nimport org.robolectric.shadows.ShadowToast;\n\n/**\n * @author James Knight\n */\n\n@RunWith(AndroidJUnit4.class)\npublic class UrlWidgetTest {\n    private WidgetTestActivity spyActivity;\n    private WebPageService webPageService;\n    private OnLongClickListener listener;\n\n    @Before\n    public void setUp() {\n        spyActivity = spy(widgetTestActivity());\n        webPageService = mock(WebPageService.class);\n        listener = mock(OnLongClickListener.class);\n    }\n\n    @Test\n    public void getAnswer_whenPromptAnswerDoesNotHaveAnswer_returnsNull() {\n        assertThat(createWidget(promptWithAnswer(null)).getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        UrlWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(\"blah\"));\n    }\n\n    @Test\n    public void clearAnswer_doesNotClearWidgetAnswer() {\n        UrlWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        widget.clearAnswer();\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(\"blah\"));\n    }\n\n    @Test\n    public void clearAnswer_showsToastThatTheUrlIsReadOnly() {\n        UrlWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        widget.clearAnswer();\n        assertThat(ShadowToast.getTextOfLatestToast(), equalTo(\"URL is readonly\"));\n    }\n\n    @Test\n    public void clickingButton_whenUrlIsEmpty_doesNotOpensUri() {\n        UrlWidget widget = createWidget(promptWithAnswer(null));\n        widget.binding.urlButton.performClick();\n\n        verify(webPageService, never()).openWebPage(null, null);\n    }\n\n    @Test\n    public void clickingButton_whenUrlIsEmpty_showsToastMessage() {\n        UrlWidget widget = createWidget(promptWithAnswer(null));\n        widget.binding.urlButton.performClick();\n\n        verify(webPageService, never()).openWebPage(null, null);\n        assertThat(ShadowToast.getTextOfLatestToast(), equalTo(\"No URL set\"));\n    }\n\n    @Test\n    public void clickingButton_whenUrlIsNotEmpty_opensUriAndBindsCustomTabService() {\n        UrlWidget widget = createWidget(promptWithAnswer(new StringData(\"blah\")));\n        widget.binding.urlButton.performClick();\n\n        verify(webPageService).openWebPage((Activity) widget.getContext(), Uri.parse(\"blah\"));\n    }\n\n    @Test\n    public void clickingButtonForLong_callsLongClickListener() {\n        UrlWidget widget = createWidget(promptWithAnswer(null));\n        widget.setOnLongClickListener(listener);\n        widget.binding.urlButton.performLongClick();\n        verify(listener).onLongClick(widget.binding.urlButton);\n    }\n\n    private UrlWidget createWidget(FormEntryPrompt prompt) {\n        return new UrlWidget(spyActivity, new QuestionDetails(prompt), webPageService, widgetDependencies());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/VideoWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport android.content.Intent\nimport android.provider.MediaStore\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.performClick\nimport net.bytebuddy.utility.RandomString\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.data.StringData\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.support.WidgetTestActivity\nimport org.odk.collect.android.utilities.MediaUtils\nimport org.odk.collect.android.utilities.QuestionMediaManager\nimport org.odk.collect.android.widgets.base.FileWidgetTest\nimport org.odk.collect.android.widgets.support.FakeQuestionMediaManager\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry\nimport org.odk.collect.android.widgets.video.VideoWidget\nimport org.odk.collect.androidtest.onNodeWithClickLabel\nimport org.odk.collect.shared.TempFiles.createTempDir\nimport org.odk.collect.shared.TempFiles.createTempFile\nimport org.odk.collect.strings.R.string\nimport org.robolectric.Shadows\n\nclass VideoWidgetTest : FileWidgetTest<VideoWidget>() {\n    @get:Rule\n    val composeRule = createAndroidComposeRule<WidgetTestActivity>()\n    private var destinationName: String? = null\n    private var questionMediaManager = mock<QuestionMediaManager>()\n    private var mediaUtils = mock<MediaUtils>()\n    private val mediaWidgetAnswerViewModel = MediaWidgetAnswerViewModel(mock(), questionMediaManager, mediaUtils)\n    private val dependencies = QuestionWidget.Dependencies(\n        null,\n        mediaWidgetAnswerViewModel\n    )\n\n    override fun createWidget(): VideoWidget {\n        return VideoWidget(\n            activity,\n            QuestionDetails(formEntryPrompt, readOnlyOverride),\n            dependencies,\n            FakeQuestionMediaManager(),\n            FakeWaitingForDataRegistry()\n        ).also {\n            composeRule.activity.setContentView(it)\n        }\n    }\n\n    override fun getNextAnswer(): StringData {\n        return StringData(destinationName!!)\n    }\n\n    @Before\n    override fun setUp() {\n        super.setUp()\n        whenever(formEntryPrompt.controlType).thenReturn(Constants.CONTROL_VIDEO_CAPTURE)\n        destinationName = RandomString.make()\n        activity = composeRule.activity\n    }\n\n    @Test\n    fun buttonsShouldLaunchCorrectIntents() {\n        createWidget()\n        stubAllRuntimePermissionsGranted(true)\n\n        composeRule.onNodeWithClickLabel(activity.getString(string.capture_video)).performClick()\n        var intent = Shadows.shadowOf(activity).nextStartedActivity\n        assertActionEquals(MediaStore.ACTION_VIDEO_CAPTURE, intent)\n\n        composeRule.onNodeWithClickLabel(activity.getString(string.choose_video)).performClick()\n        intent = Shadows.shadowOf(activity).nextStartedActivity\n        assertActionEquals(Intent.ACTION_GET_CONTENT, intent)\n        assertTypeEquals(\"video/*\", intent)\n\n        val newFile = createTempFile(createTempDir())\n        getWidget()!!.setData(newFile)\n        whenever(questionMediaManager.getAnswerFile(newFile.name)).thenReturn(newFile)\n\n        composeRule.onNodeWithClickLabel(activity.getString(string.play_video)).performClick()\n        verify(mediaUtils).openFile(any(), any(), any<String>())\n    }\n\n    @Test\n    fun buttonsShouldNotLaunchIntentsWhenPermissionsDenied() {\n        createWidget()\n        stubAllRuntimePermissionsGranted(false)\n\n        composeRule.onNodeWithClickLabel(activity.getString(string.capture_video)).performClick()\n        assertThat(Shadows.shadowOf(activity).nextStartedActivity, equalTo(null))\n    }\n\n    @Test\n    override fun usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        whenever(formEntryPrompt.isReadOnly).thenReturn(true)\n        createWidget()\n\n        composeRule.onNodeWithClickLabel(activity.getString(string.capture_video)).assertDoesNotExist()\n        composeRule.onNodeWithClickLabel(activity.getString(string.choose_video)).assertDoesNotExist()\n        composeRule.onNodeWithClickLabel(activity.getString(string.play_video)).assertDoesNotExist()\n    }\n\n    @Test\n    fun whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisabled() {\n        readOnlyOverride = true\n        whenever(formEntryPrompt.isReadOnly).thenReturn(false)\n        createWidget()\n\n        composeRule.onNodeWithClickLabel(activity.getString(string.capture_video)).assertDoesNotExist()\n        composeRule.onNodeWithClickLabel(activity.getString(string.choose_video)).assertDoesNotExist()\n        composeRule.onNodeWithClickLabel(activity.getString(string.play_video)).assertDoesNotExist()\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/WidgetFactoryTest.kt",
    "content": "package org.odk.collect.android.widgets\n\nimport androidx.fragment.app.FragmentActivity\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.instanceOf\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.Constants\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.odk.collect.android.formentry.FormEntryViewModel\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.android.support.WidgetTestActivity\nimport org.odk.collect.android.widgets.items.LabelWidget\nimport org.odk.collect.android.widgets.items.LikertWidget\nimport org.odk.collect.android.widgets.items.ListMultiWidget\nimport org.odk.collect.android.widgets.items.ListWidget\nimport org.odk.collect.android.widgets.items.SelectMultiImageMapWidget\nimport org.odk.collect.android.widgets.items.SelectMultiMinimalWidget\nimport org.odk.collect.android.widgets.items.SelectMultiWidget\nimport org.odk.collect.android.widgets.items.SelectOneFromMapWidget\nimport org.odk.collect.android.widgets.items.SelectOneImageMapWidget\nimport org.odk.collect.android.widgets.items.SelectOneMinimalWidget\nimport org.odk.collect.android.widgets.items.SelectOneWidget\n\n@RunWith(AndroidJUnit4::class)\nclass WidgetFactoryTest {\n    private val activity: FragmentActivity = CollectHelpers.buildThemedActivity(WidgetTestActivity::class.java).get()\n\n    private var widgetFactory = WidgetFactory(\n        activity,\n        false,\n        null,\n        null,\n        null,\n        null,\n        mock<FormEntryViewModel>(),\n        null,\n        null,\n        null,\n        null,\n        null,\n        null,\n        null\n    )\n\n    @Test\n    fun testCreatingSelectOneMinimalWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_ONE)\n            .withAppearance(\"something miNimal something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(SelectOneMinimalWidget::class.java))\n    }\n\n    @Test\n    fun testCreatingLikertWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_ONE)\n            .withAppearance(\"something lIkErt something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(LikertWidget::class.java))\n    }\n\n    @Test\n    fun testCreatingSelectOneListNoLabelWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_ONE)\n            .withAppearance(\"something LisT-nOLabeL something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(ListWidget::class.java))\n        assertThat((widget as ListWidget).shouldDisplayLabel(), equalTo(false))\n    }\n\n    @Test\n    fun testCreatingSelectOneListWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_ONE)\n            .withAppearance(\"something LisT something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(ListWidget::class.java))\n        assertThat((widget as ListWidget).shouldDisplayLabel(), equalTo(true))\n    }\n\n    @Test\n    fun testCreatingSelectOneLabelWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_ONE)\n            .withAppearance(\"something lAbeL something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(LabelWidget::class.java))\n    }\n\n    @Test\n    fun testCreatingSelectOneImageMapWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_ONE)\n            .withAppearance(\"something imaGe-Map something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(SelectOneImageMapWidget::class.java))\n    }\n\n    @Test\n    fun testCreatingSelectOneWidget() {\n        var prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_ONE)\n            .withAppearance(\"\")\n            .build()\n\n        var widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(SelectOneWidget::class.java))\n\n        prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_ONE)\n            .withAppearance(\"lorem ipsum\")\n            .build()\n\n        widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(SelectOneWidget::class.java))\n    }\n\n    @Test\n    fun testCreatingSelectMultipleMinimalWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_MULTI)\n            .withAppearance(\"something miNimal something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(SelectMultiMinimalWidget::class.java))\n    }\n\n    @Test\n    fun testCreatingSelectMultipleListNoLabelWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_MULTI)\n            .withAppearance(\"something LisT-nOLabeL something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(ListMultiWidget::class.java))\n        assertThat((widget as ListMultiWidget).shouldDisplayLabel(), equalTo(false))\n    }\n\n    @Test\n    fun testCreatingSelectMultipleListWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_MULTI)\n            .withAppearance(\"something LisT something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(ListMultiWidget::class.java))\n        assertThat((widget as ListMultiWidget).shouldDisplayLabel(), equalTo(true))\n    }\n\n    @Test\n    fun testCreatingSelectMultipleOneLabelWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_MULTI)\n            .withAppearance(\"something lAbeL something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(LabelWidget::class.java))\n    }\n\n    @Test\n    fun testCreatingSelectMultipleImageMapWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_MULTI)\n            .withAppearance(\"something imaGe-Map something\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(SelectMultiImageMapWidget::class.java))\n    }\n\n    @Test\n    fun testCreatingSelectMultipleWidget() {\n        var prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_MULTI)\n            .withAppearance(\"\")\n            .build()\n\n        var widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(SelectMultiWidget::class.java))\n\n        prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_MULTI)\n            .withAppearance(\"lorem ipsum\")\n            .build()\n\n        widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(SelectMultiWidget::class.java))\n    }\n\n    @Test\n    fun testCreatingSelectOneFromMapWidget() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_SELECT_ONE)\n            .withAppearance(\"map\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(SelectOneFromMapWidget::class.java))\n    }\n\n    @Test\n    fun exStringWidgetShouldBeCreatedIfThePackageNameIsMixedWithOtherAppearances() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_INPUT)\n            .withDataType(Constants.DATATYPE_TEXT)\n            .withAppearance(\"masked ex:change.uw.android.BREATHCOUNT thousands-sep\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(ExStringWidget::class.java))\n    }\n\n    @Test\n    fun exIntegerWidgetShouldBeCreatedIfThePackageNameIsMixedWithOtherAppearances() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_INPUT)\n            .withDataType(Constants.DATATYPE_INTEGER)\n            .withAppearance(\"masked ex:change.uw.android.BREATHCOUNT thousands-sep\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(ExIntegerWidget::class.java))\n    }\n\n    @Test\n    fun exDecimalWidgetShouldBeCreatedIfThePackageNameIsMixedWithOtherAppearances() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withControlType(Constants.CONTROL_INPUT)\n            .withDataType(Constants.DATATYPE_DECIMAL)\n            .withAppearance(\"masked ex:change.uw.android.BREATHCOUNT thousands-sep\")\n            .build()\n\n        val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n        assertThat(widget, instanceOf(ExDecimalWidget::class.java))\n    }\n\n    @Test\n    fun testCreatingCounterWidget() {\n        listOf(\"counter\", \"CouNTer\").forEach { appearance ->\n            val prompt = MockFormEntryPromptBuilder()\n                .withControlType(Constants.CONTROL_INPUT)\n                .withDataType(Constants.DATATYPE_INTEGER)\n                .withAppearance(appearance)\n                .build()\n\n            val widget = widgetFactory.createWidgetFromPrompt(prompt, null)\n            assertThat(widget, instanceOf(CounterWidget::class.java))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/base/BinaryWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.base;\n\nimport android.content.ComponentName;\nimport android.content.Context;\nimport android.content.Intent;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.junit.Test;\nimport org.odk.collect.android.fakes.FakePermissionsProvider;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;\nimport org.odk.collect.android.widgets.interfaces.Widget;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.when;\nimport static org.robolectric.Shadows.shadowOf;\n\n/**\n * @author James Knight\n */\npublic abstract class BinaryWidgetTest<W extends Widget, A extends IAnswerData>\n        extends QuestionWidgetTest<W, A> {\n\n    private final FakePermissionsProvider permissionsProvider;\n\n    public BinaryWidgetTest() {\n        permissionsProvider = new FakePermissionsProvider();\n    }\n\n    protected void stubAllRuntimePermissionsGranted(boolean isGranted) {\n        permissionsProvider.setPermissionGranted(isGranted);\n        ((QuestionWidget) getWidget()).setPermissionsProvider(permissionsProvider);\n    }\n\n    protected Intent getIntentLaunchedByClick(int buttonId) {\n        ((QuestionWidget) getSpyWidget()).findViewById(buttonId).performClick();\n        return shadowOf(activity).getNextStartedActivity();\n    }\n\n    protected void assertComponentEquals(String pkg, String cls, Intent intent) {\n        assertEquals(new ComponentName(pkg, cls), intent.getComponent());\n    }\n\n    protected void assertComponentEquals(Context context, Class<?> cls, Intent intent) {\n        assertEquals(new ComponentName(context, cls), intent.getComponent());\n    }\n\n    protected void assertActionEquals(String expectedAction, Intent intent) {\n        assertEquals(expectedAction, intent.getAction());\n    }\n\n    protected void assertTypeEquals(String type, Intent intent) {\n        assertEquals(type, intent.getType());\n    }\n\n    protected void assertExtraEquals(String key, Object value, Intent intent) {\n        assertEquals(intent.getExtras().get(key), value);\n    }\n\n    public abstract Object createBinaryData(A answerData);\n\n    @Test\n    public void getAnswerShouldReturnCorrectAnswerAfterBeingSet() {\n        W widget = getSpyWidget();\n        assertNull(widget.getAnswer());\n\n        A answer = getNextAnswer();\n        Object binaryData = createBinaryData(answer);\n\n        ((WidgetDataReceiver) widget).setData(binaryData);\n\n        IAnswerData answerData = widget.getAnswer();\n\n        assertNotNull(answerData);\n        assertEquals(answerData.getDisplayText(), answer.getDisplayText());\n    }\n\n    @Test\n    public void settingANewAnswerShouldRemoveTheOldAnswer() {\n        A answer = getInitialAnswer();\n        when(formEntryPrompt.getAnswerText()).thenReturn(answer.getDisplayText());\n\n        W widget = getSpyWidget();\n\n        A newAnswer = getNextAnswer();\n        Object binaryData = createBinaryData(newAnswer);\n\n        ((WidgetDataReceiver) widget).setData(binaryData);\n\n        IAnswerData answerData = widget.getAnswer();\n\n        assertNotNull(answerData);\n        assertEquals(answerData.getDisplayText(), newAnswer.getDisplayText());\n    }\n}"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/base/FileWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.base;\n\nimport androidx.annotation.NonNull;\n\nimport net.bytebuddy.utility.RandomString;\n\nimport org.javarosa.core.model.data.StringData;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.odk.collect.android.widgets.interfaces.FileWidget;\nimport org.odk.collect.shared.TempFiles;\n\nimport java.io.File;\n\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\npublic abstract class FileWidgetTest<W extends FileWidget> extends BinaryWidgetTest<W, StringData> {\n\n    @Mock\n    public File instancePath;\n\n    @NonNull\n    @Override\n    public StringData getInitialAnswer() {\n        return new StringData(RandomString.make());\n    }\n\n    @Override\n    public Object createBinaryData(StringData answerData) {\n        return TempFiles.createTempFileWithName(answerData.getDisplayText());\n    }\n\n    @Override\n    public void setUp() throws Exception {\n        super.setUp();\n\n        when(formController.getInstanceFile()).thenReturn(instancePath);\n        when(instancePath.getParent()).thenReturn(\"\");\n    }\n\n    @Test\n    public void settingANewAnswerShouldCallDeleteMediaToRemoveTheOldFile() {\n        super.settingANewAnswerShouldRemoveTheOldAnswer();\n\n        W widget = getSpyWidget();\n        verify(widget).deleteFile();\n    }\n\n    @Test\n    public void callingClearAnswerShouldCallDeleteMediaAndRemoveTheExistingAnswer() {\n        super.callingClearShouldRemoveTheExistingAnswer();\n\n        W widget = getSpyWidget();\n        verify(widget).deleteFile();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralExStringWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.base;\n\nimport static junit.framework.Assert.assertTrue;\n\nimport android.view.View;\nimport android.widget.TextView;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.junit.Test;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.widgets.ExStringWidget;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\n\n/**\n * @author James Knight\n */\n\npublic abstract class GeneralExStringWidgetTest<W extends ExStringWidget, A extends IAnswerData> extends BinaryWidgetTest<W, A> {\n\n    @Override\n    public Object createBinaryData(A answerData) {\n        return answerData.getDisplayText();\n    }\n\n    // TODO we should have such tests for every widget like we have to confirm readOnly option\n    @Test\n    public void testElementsVisibilityAndAvailability() {\n        assertThat(getSpyWidget().binding.launchAppButton.getVisibility(), is(View.VISIBLE));\n        assertThat(getSpyWidget().binding.launchAppButton.isEnabled(), is(Boolean.TRUE));\n        assertThat(getSpyWidget().binding.widgetAnswerText.isEditableState(), is(false));\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n\n        assertThat(getSpyWidget().binding.launchAppButton.getVisibility(), is(View.GONE));\n        assertThat(getSpyWidget().binding.widgetAnswerText.isEditableState(), is(false));\n    }\n\n    /**\n     * Unlike other widgets, String widgets that contain EditText should not be registered to\n     * context menu as a whole because the Clipboard menu would be broken.\n     *\n     * https://github.com/getodk/collect/pull/4860\n     */\n    @Test\n    public void widgetShouldBeRegisteredForContextMenu() {\n        ExStringWidget widget = createWidget();\n        List<View> viewsRegisterForContextMenu = ((WidgetTestActivity) activity).viewsRegisterForContextMenu;\n\n        assertThat(viewsRegisterForContextMenu.size(), is(3));\n\n        assertTrue(viewsRegisterForContextMenu.contains(widget.findViewWithTag(R.id.question_label)));\n        assertTrue(viewsRegisterForContextMenu.contains(widget.findViewWithTag(R.id.help_text)));\n        assertTrue(viewsRegisterForContextMenu.contains(widget.findViewWithTag(R.id.error_message_container)));\n\n        assertThat(viewsRegisterForContextMenu.get(0).getId(), is(widget.getId()));\n        assertThat(viewsRegisterForContextMenu.get(1).getId(), is(widget.getId()));\n    }\n\n    @Test\n    public void errorDisappearsOnSetData() {\n        ExStringWidget widget = getWidget();\n        widget.displayError(\"blah\");\n        widget.setData(\"answer\");\n\n        assertThat(widget.errorLayout.getVisibility(), equalTo(TextView.GONE));\n        assertThat(widget.getBackground(), equalTo(null));\n    }\n\n    @Test\n    public void errorDisappearsOnAddingAnswerManuallyViaTheTextField() {\n        ExStringWidget widget = getWidget();\n        widget.displayError(\"blah\");\n        widget.binding.widgetAnswerText.getBinding().editText.setText(\"answer\");\n\n        assertThat(widget.errorLayout.getVisibility(), equalTo(TextView.GONE));\n        assertThat(widget.getBackground(), equalTo(null));\n    }\n\n    @Test\n    public abstract void verifyInputType();\n\n    @Test\n    public abstract void whenPromptHasHiddenAnswerAppearance_answerIsNotDisplayed();\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralSelectMultiWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.base;\n\nimport androidx.annotation.NonNull;\n\nimport com.google.common.collect.ImmutableList;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.SelectMultiData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.odk.collect.android.widgets.interfaces.MultiChoiceWidget;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.when;\n\n/**\n * @author James Knight\n */\n\npublic abstract class GeneralSelectMultiWidgetTest<W extends MultiChoiceWidget>\n        extends SelectWidgetTest<W, SelectMultiData> {\n\n    @Before\n    public void setUp() throws Exception {\n        super.setUp();\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"\");\n    }\n\n    @NonNull\n    @Override\n    public SelectMultiData getNextAnswer() {\n        List<SelectChoice> selectChoices = getSelectChoices();\n\n        int selectedIndex = Math.abs(random.nextInt()) % selectChoices.size();\n        SelectChoice selectChoice = selectChoices.get(selectedIndex);\n\n        Selection selection = new Selection(selectChoice);\n        return new SelectMultiData(ImmutableList.of(selection));\n    }\n\n    @Test\n    public void getAnswerShouldReflectWhichSelectionsWereMade() {\n        W widget = getSpyWidget();\n        assertNull(widget.getAnswer());\n\n        List<SelectChoice> selectChoices = getSelectChoices();\n        List<String> selectedValues = new ArrayList<>();\n\n        boolean atLeastOneSelected = false;\n\n        for (int i = 0; i < widget.getChoiceCount(); i++) {\n            boolean shouldBeSelected = random.nextBoolean();\n            widget.setChoiceSelected(i, shouldBeSelected);\n\n            atLeastOneSelected = atLeastOneSelected || shouldBeSelected;\n\n            if (shouldBeSelected) {\n                SelectChoice selectChoice = selectChoices.get(i);\n                selectedValues.add(selectChoice.getValue());\n            }\n        }\n\n        // Make sure at least one item is selected, so we're not just retesting the\n        // null answer case:\n        if (!atLeastOneSelected) {\n            int randomIndex = Math.abs(random.nextInt()) % widget.getChoiceCount();\n            widget.setChoiceSelected(randomIndex, true);\n\n            SelectChoice selectChoice = selectChoices.get(randomIndex);\n            selectedValues.add(selectChoice.getValue());\n        }\n\n        SelectMultiData answer = (SelectMultiData) widget.getAnswer();\n\n        @SuppressWarnings(\"unchecked\")\n        List<Selection> answerSelections = (List<Selection>) answer.getValue();\n        List<String> answerValues = selectionsToValues(answerSelections);\n\n        for (String selectedValue : selectedValues) {\n            assertTrue(answerValues.contains(selectedValue));\n        }\n    }\n\n    private List<String> selectionsToValues(List<Selection> selections) {\n        List<String> values = new ArrayList<>();\n        for (Selection selection : selections) {\n            values.add(selection.getValue());\n        }\n\n        return values;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralSelectOneWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.base;\n\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.junit.Test;\nimport org.odk.collect.android.widgets.interfaces.MultiChoiceWidget;\n\nimport java.util.List;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNull;\n\n/**\n * @author James Knight\n */\n\npublic abstract class GeneralSelectOneWidgetTest<W extends MultiChoiceWidget>\n        extends SelectWidgetTest<W, SelectOneData> {\n\n    @NonNull\n    @Override\n    public SelectOneData getNextAnswer() {\n        List<SelectChoice> selectChoices = getSelectChoices();\n\n        int selectedIndex = Math.abs(random.nextInt()) % selectChoices.size();\n        SelectChoice selectChoice = selectChoices.get(selectedIndex);\n\n        Selection selection = new Selection(selectChoice);\n        return new SelectOneData(selection);\n    }\n\n    @Test\n    public void getAnswerShouldReflectTheCurrentlySelectedChoice() {\n        W widget = getSpyWidget();\n        assertNull(widget.getAnswer());\n\n        List<SelectChoice> selectChoices = getSelectChoices();\n\n        for (int i = 0; i < widget.getChoiceCount(); i++) {\n            widget.setChoiceSelected(i, true);\n\n            SelectChoice selectChoice = selectChoices.get(i);\n            IAnswerData answer = widget.getAnswer();\n\n            assertEquals(selectChoice.getValue(), answer.getDisplayText());\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/base/GeneralStringWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.base;\n\nimport static junit.framework.Assert.assertTrue;\n\nimport android.view.View;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.junit.Test;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.widgets.StringWidget;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.junit.Assert.assertEquals;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\n\n/**\n * @author James Knight\n */\npublic abstract class GeneralStringWidgetTest<W extends StringWidget, A extends IAnswerData>\n        extends QuestionWidgetTest<W, A> {\n\n    @Override\n    public void callingClearShouldRemoveTheExistingAnswer() {\n        super.callingClearShouldRemoveTheExistingAnswer();\n\n        W widget = getSpyWidget();\n        assertEquals(widget.getAnswerText(), \"\");\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void getAnswerShouldReturnExistingAnswerIfPromptHasExistingAnswer() {\n        super.getAnswerShouldReturnExistingAnswerIfPromptHasExistingAnswer();\n\n        W widget = getSpyWidget();\n        IAnswerData answer = widget.getAnswer();\n\n        assertEquals(widget.getAnswerText(), answer.getDisplayText());\n    }\n\n    @Test\n    public void getAnswerShouldReturnNewAnswerWhenTextFieldIsUpdated() {\n        // Make sure it starts null:\n        super.getAnswerShouldReturnNullIfPromptDoesNotHaveExistingAnswer();\n\n        W widget = getSpyWidget();\n        IAnswerData answer = getNextAnswer();\n\n        when(widget.getAnswerText()).thenReturn(answer.getDisplayText());\n        IAnswerData computedAnswer = widget.getAnswer();\n\n        assertEquals(answer.getDisplayText(), computedAnswer.getDisplayText());\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n\n        assertThat(getSpyWidget().widgetAnswerText.isEditableState(), is(false));\n    }\n\n    @Test\n    public void whenReadOnlyOverrideOptionIsUsed_shouldAllClickableElementsBeDisabled() {\n        readOnlyOverride = true;\n        when(formEntryPrompt.isReadOnly()).thenReturn(false);\n\n        assertThat(getSpyWidget().widgetAnswerText.isEditableState(), is(false));\n    }\n\n    /**\n     * Unlike other widgets, String widgets that contain EditText should not be registered to\n     * context menu as a whole because the Clipboard menu would be broken.\n     *\n     * https://github.com/getodk/collect/pull/4860\n     */\n    @Test\n    public void widgetShouldBeRegisteredForContextMenu() {\n        StringWidget widget = createWidget();\n        List<View> viewsRegisterForContextMenu = ((WidgetTestActivity) activity).viewsRegisterForContextMenu;\n\n        assertThat(viewsRegisterForContextMenu.size(), is(3));\n\n        assertTrue(viewsRegisterForContextMenu.contains(widget.findViewWithTag(R.id.question_label)));\n        assertTrue(viewsRegisterForContextMenu.contains(widget.findViewWithTag(R.id.help_text)));\n        assertTrue(viewsRegisterForContextMenu.contains(widget.findViewWithTag(R.id.error_message_container)));\n\n        assertThat(viewsRegisterForContextMenu.get(0).getId(), is(widget.getId()));\n        assertThat(viewsRegisterForContextMenu.get(1).getId(), is(widget.getId()));\n    }\n\n    @Test\n    public abstract void verifyInputType();\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/base/QuestionWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.base;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\n\nimport android.app.Activity;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.javarosawrapper.FormController;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.interfaces.Widget;\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers;\n\nimport java.util.List;\nimport java.util.Random;\n\npublic abstract class QuestionWidgetTest<W extends Widget, A extends IAnswerData>\n        extends WidgetTest {\n\n    protected Random random = new Random();\n    protected Activity activity = QuestionWidgetHelpers.widgetTestActivity();\n    protected QuestionWidget.Dependencies dependencies = QuestionWidgetHelpers.widgetDependencies();\n\n    private W widget;\n    private W actualWidget;\n\n    @Mock\n    public FormIndex formIndex;\n\n    @Mock\n    public FormController formController;\n\n    @NonNull\n    public abstract W createWidget();\n\n    public abstract A getNextAnswer();\n\n    public A getInitialAnswer() {\n        return getNextAnswer();\n    }\n\n    /**\n     * @return Real {@link Widget} object if present otherwise creates one\n     * <p>\n     * This should be used for mutating the {@link org.odk.collect.android.widgets.QuestionWidget}\n     */\n    public W getWidget() {\n        if (actualWidget == null) {\n            actualWidget = createWidget();\n        }\n\n        return actualWidget;\n    }\n\n    /**\n     * @return {@link org.mockito.Spy} of the {@link #actualWidget}\n     * <p>\n     * This should be unless we want to mutate {@link org.odk.collect.android.widgets.QuestionWidget}\n     * This is because a spy is not the real object and changing it won't have any effect on the real object\n     */\n    public W getSpyWidget() {\n        if (widget == null) {\n            widget = spy(getWidget());\n        }\n\n        return widget;\n    }\n\n    public void resetWidget() {\n        actualWidget = null;\n        widget = null;\n    }\n\n    public void setUp() throws Exception {\n        super.setUp();\n\n        when(formEntryPrompt.getIndex()).thenReturn(formIndex);\n\n        widget = null;\n    }\n\n    @Test\n    public void getAnswerShouldReturnNullIfPromptDoesNotHaveExistingAnswer() {\n        W widget = getSpyWidget();\n        assertNull(widget.getAnswer());\n    }\n\n    @Test\n    public void getAnswerShouldReturnExistingAnswerIfPromptHasExistingAnswer() {\n        A answer = getInitialAnswer();\n        if (answer instanceof StringData) {\n            when(formEntryPrompt.getAnswerText()).thenReturn((String) answer.getValue());\n        } else {\n            when(formEntryPrompt.getAnswerValue()).thenReturn(answer);\n        }\n\n        W widget = getSpyWidget();\n        IAnswerData newAnswer = widget.getAnswer();\n\n        assertNotNull(newAnswer);\n        assertEquals(newAnswer.getDisplayText(), answer.getDisplayText());\n    }\n\n    @Test\n    public void callingClearShouldRemoveTheExistingAnswer() {\n        getAnswerShouldReturnExistingAnswerIfPromptHasExistingAnswer();\n        widget.clearAnswer();\n\n        assertNull(widget.getAnswer());\n    }\n\n    @Test\n    public void callingClearShouldCallValueChangeListeners() {\n        when(formEntryPrompt.getAnswerText()).thenReturn(getInitialAnswer().getDisplayText());\n\n        QuestionWidget widget = (QuestionWidget) getWidget();\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.clearAnswer();\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void whenReadOnlyQuestionHasNoAnswer_answerContainerShouldNotBeDisplayed() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n        QuestionWidget widget = (QuestionWidget) getWidget();\n        assertThat(widget.findViewById(R.id.answer_container).getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenReadOnlyQuestionHasAnswer_answerContainerShouldBeDisplayed() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n        when(formEntryPrompt.getAnswerValue()).thenReturn(getInitialAnswer());\n        QuestionWidget widget = (QuestionWidget) getWidget();\n        assertThat(widget.findViewById(R.id.answer_container).getVisibility(), is(View.VISIBLE));\n    }\n\n    // The whole widget should be registered for context menu to support removing answers/groups\n    @Test\n    public void widgetShouldBeRegisteredForContextMenu() {\n        QuestionWidget widget = (QuestionWidget) createWidget();\n        List<View> viewsRegisterForContextMenu = ((WidgetTestActivity) activity).viewsRegisterForContextMenu;\n\n        assertThat(viewsRegisterForContextMenu.size(), is(1));\n        assertThat(viewsRegisterForContextMenu.get(0), is(widget));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/base/SelectWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.base;\n\nimport net.bytebuddy.utility.RandomString;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.odk.collect.android.widgets.interfaces.Widget;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.mockito.Mockito.when;\n\n/**\n * @author James Knight\n */\npublic abstract class SelectWidgetTest<W extends Widget, A extends IAnswerData>\n        extends QuestionWidgetTest<W, A> {\n\n    private List<SelectChoice> selectChoices;\n\n    @Override\n    public void setUp() throws Exception {\n        super.setUp();\n        selectChoices = createSelectChoices();\n\n        when(formEntryPrompt.getSelectChoices()).thenReturn(selectChoices);\n    }\n\n    public List<SelectChoice> getSelectChoices() {\n        return selectChoices;\n    }\n\n    private List<SelectChoice> createSelectChoices() {\n        int choiceCount = (Math.abs(random.nextInt()) % 3) + 2;\n\n        List<SelectChoice> selectChoices = new ArrayList<>();\n        for (int i = 0; i < choiceCount; i++) {\n            SelectChoice selectChoice = new SelectChoice(Integer.toString(i), RandomString.make());\n\n            selectChoice.setIndex(i);\n            selectChoices.add(selectChoice);\n        }\n\n        return selectChoices;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/base/WidgetTest.java",
    "content": "package org.odk.collect.android.widgets.base;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.IFormElement;\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\nimport org.odk.collect.android.TestSettingsProvider;\nimport org.odk.collect.settings.SettingsProvider;\n\nimport javax.annotation.OverridingMethodsMustInvokeSuper;\n\n@RunWith(AndroidJUnit4.class)\npublic abstract class WidgetTest {\n\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    @Mock\n    public FormEntryPrompt formEntryPrompt;\n\n    @Mock\n    public IFormElement formElement;\n\n    public boolean readOnlyOverride;\n\n    protected final SettingsProvider settingsProvider = TestSettingsProvider.getSettingsProvider();\n\n    @Mock\n    protected QuestionDef questionDef;\n\n    @Before\n    @OverridingMethodsMustInvokeSuper\n    public void setUp() throws Exception {\n        settingsProvider.getUnprotectedSettings().clear();\n        settingsProvider.getUnprotectedSettings().setDefaultForAllSettingsWithoutValues();\n        settingsProvider.getProtectedSettings().clear();\n        settingsProvider.getProtectedSettings().setDefaultForAllSettingsWithoutValues();\n\n        when(formEntryPrompt.getIndex()).thenReturn(mock(FormIndex.class));\n        when(formEntryPrompt.getIndex().toString()).thenReturn(\"0, 0\");\n        when(formEntryPrompt.getFormElement()).thenReturn(formElement);\n        when(formEntryPrompt.getQuestion()).thenReturn(questionDef);\n    }\n\n    @Test\n    public abstract void getAnswerShouldReturnNullIfPromptDoesNotHaveExistingAnswer();\n\n    @Test\n    public abstract void getAnswerShouldReturnExistingAnswerIfPromptHasExistingAnswer();\n\n    @Test\n    public abstract void callingClearShouldRemoveTheExistingAnswer();\n\n    @Test\n    public abstract void callingClearShouldCallValueChangeListeners();\n\n    @Test\n    public abstract void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled();\n\n//    @Test\n//    public abstract void setData_callsValueChangeListener();\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/DateTimeUtilsTest.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.datetime;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.data.TimeData;\nimport org.joda.time.DateTime;\nimport org.joda.time.LocalDateTime;\nimport org.joda.time.chrono.GregorianChronology;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.testshared.TimeZoneSetter;\n\nimport static org.junit.Assert.assertEquals;\n\nimport java.util.TimeZone;\n\n@RunWith(AndroidJUnit4.class)\npublic class DateTimeUtilsTest {\n    private final LocalDateTime date = new LocalDateTime().withDate(2010, 5, 12);\n    private final LocalDateTime time = new LocalDateTime().withTime(12, 10, 0, 0);\n\n    @Test\n    public void getCurrentDateTime_returnsCurrentDateAndTimeData() {\n        LocalDateTime localDateTime = new LocalDateTime()\n                .withDate(DateTime.now().getYear(), DateTime.now().getMonthOfYear(), DateTime.now().getDayOfMonth())\n                .withTime(DateTime.now().getHourOfDay(), DateTime.now().getMinuteOfHour(), 0, 0);\n        assertEquals(DateTimeUtils.getCurrentDateTime(), localDateTime);\n    }\n\n    @Test\n    public void getSelectedDate_returnsCorrectDateAndTimeData() {\n        LocalDateTime localDateTime = new LocalDateTime()\n                .withDate(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth())\n                .withTime(time.getHourOfDay(), time.getMinuteOfHour(), 0, 0);\n        assertEquals(DateTimeUtils.getSelectedDate(date, time), localDateTime);\n    }\n\n    @Test\n    public void getDateAsGregorian_returnsCorrectDateAndTimeData() {\n        LocalDateTime localDateTime = DateTimeUtils.skipDaylightSavingGapIfExists(date)\n                .toDateTime()\n                .withChronology(GregorianChronology.getInstance())\n                .toLocalDateTime();\n        assertEquals(DateTimeUtils.getDateAsGregorian(date), localDateTime);\n    }\n\n    @Test\n    public void getSelectedTime_returnsCorrectDateAndTimeData() {\n        LocalDateTime localDateTime = new LocalDateTime()\n                .withDate(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth())\n                .withTime(time.getHourOfDay(), time.getMinuteOfHour(), 0, 0);\n        assertEquals(DateTimeUtils.getSelectedTime(time, date), localDateTime);\n    }\n\n    @Test\n    public void getTimeData_returnsCorrectTime() {\n        assertEquals(DateTimeUtils.getTimeData(time.toDateTime()).getDisplayText(), new TimeData(time.toDate()).getDisplayText());\n    }\n\n    @Test\n    public void getDateWithSkippedDaylightSavingGapIfExists_returnsCorrectDateAndTimeData() {\n        LocalDateTime localDateTime = DateTimeUtils.skipDaylightSavingGapIfExists(date)\n                .toDateTime()\n                .toLocalDateTime();\n        assertEquals(DateTimeUtils.getDateWithSkippedDaylightSavingGapIfExists(date), localDateTime);\n    }\n\n    @Test\n    public void skipDaylightSavingGapIfExistsTest() {\n        TimeZone originalDefaultTimeZone = TimeZone.getDefault();\n        TimeZoneSetter.setTimezone(TimeZone.getTimeZone(\"Europe/Warsaw\"));\n\n        // 29 March 2020 at 02:00:00 clocks were turned forward to 03:00:00\n        LocalDateTime ldtOriginal = new LocalDateTime().withYear(2020).withMonthOfYear(3).withDayOfMonth(29).withHourOfDay(2).withMinuteOfHour(30).withSecondOfMinute(0).withMillisOfSecond(0);\n        LocalDateTime ldtExpected = new LocalDateTime().withYear(2020).withMonthOfYear(3).withDayOfMonth(29).withHourOfDay(3).withMinuteOfHour(0).withSecondOfMinute(0).withMillisOfSecond(0);\n\n        assertEquals(ldtExpected, DateTimeUtils.skipDaylightSavingGapIfExists(ldtOriginal));\n        TimeZoneSetter.setTimezone(originalDefaultTimeZone);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/DateTimeWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.datetime;\n\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.data.DateData;\nimport org.javarosa.core.model.data.DateTimeData;\nimport org.javarosa.core.model.data.TimeData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.joda.time.DateTime;\nimport org.joda.time.LocalDateTime;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithQuestionDefAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndQuestionDef;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\n@RunWith(AndroidJUnit4.class)\npublic class DateTimeWidgetTest {\n    private WidgetTestActivity widgetActivity;\n    private DateTimeWidgetUtils widgetUtils;\n    private View.OnLongClickListener onLongClickListener;\n\n    private QuestionDef questionDef;\n    private LocalDateTime localDateTime;\n\n    @Before\n    public void setUp() {\n        widgetActivity = widgetTestActivity();\n\n        questionDef = mock(QuestionDef.class);\n        onLongClickListener = mock(View.OnLongClickListener.class);\n        widgetUtils = mock(DateTimeWidgetUtils.class);\n\n        localDateTime = new LocalDateTime()\n                .withDate(2010, 5, 12)\n                .withTime(12, 10, 0, 0);\n    }\n\n    @Test\n    public void usingReadOnlyOption_doesNotShowButtons() {\n        DateTimeWidget widget = createWidget(promptWithReadOnlyAndQuestionDef(questionDef));\n\n        assertEquals(widget.binding.dateWidget.dateButton.getVisibility(), View.GONE);\n        assertEquals(widget.binding.timeWidget.timeButton.getVisibility(), View.GONE);\n    }\n\n    @Test\n    public void whenPromptIsNotReadOnly_buttonShowsCorrectText() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n\n        assertEquals(widget.binding.dateWidget.dateButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.select_date));\n        assertEquals(widget.binding.timeWidget.timeButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.select_time));\n    }\n\n    @Test\n    public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {\n        assertThat(createWidget(promptWithQuestionDefAndAnswer(questionDef, null)).getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasDateAnswer_returnsDateAnswerAndCurrentTime() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new DateTimeData(localDateTime.toDate())));\n        widget.binding.timeWidget.timeAnswerText.setText(widget.getContext().getString(org.odk.collect.strings.R.string.no_time_selected));\n\n        assertEquals(widget.getAnswer().getDisplayText(),\n                new DateTimeData(DateTimeUtils.getSelectedDate(localDateTime, LocalDateTime.now()).toDate()).getDisplayText());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasTimeAnswer_returnsTimeAnswerAndCurrentDate() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new DateTimeData(localDateTime.toDate())));\n        widget.binding.dateWidget.dateAnswerText.setText(widget.getContext().getString(org.odk.collect.strings.R.string.no_date_selected));\n\n        assertEquals(widget.getAnswer().getDisplayText(),\n                new DateTimeData(DateTimeUtils.getSelectedTime(localDateTime, LocalDateTime.now()).toDate()).getDisplayText());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new DateTimeData(localDateTime.toDate())));\n        assertEquals(widget.getAnswer().getDisplayText(), new DateTimeData(localDateTime.toDate()).getDisplayText());\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_answerTextViewShowsNoValueSelected() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n\n        assertEquals(widget.binding.dateWidget.dateAnswerText.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.no_date_selected));\n        assertEquals(widget.binding.timeWidget.timeAnswerText.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.no_time_selected));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_answerTextViewShowsCorrectDateAndTime() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, new DateTimeData(localDateTime.toDate()));\n        DatePickerDetails datePickerDetails = DateTimeWidgetUtils.getDatePickerDetails(prompt.getAppearanceHint());\n        DateTimeWidget widget = createWidget(prompt);\n\n        assertEquals(widget.binding.dateWidget.dateAnswerText.getText(),\n                DateTimeWidgetUtils.getDateTimeLabel(localDateTime.toDate(), datePickerDetails, false, widget.getContext()));\n        assertEquals(widget.binding.timeWidget.timeAnswerText.getText(), DateTimeUtils.getTimeData(localDateTime.toDateTime()).getDisplayText());\n    }\n\n    @Test\n    public void clickingSetDateButton_callsDisplayDatePickerDialogWithCurrentDate_whenPromptDoesNotHaveAnswer() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, null);\n        DateTimeWidget widget = createWidget(prompt);\n        widget.binding.dateWidget.dateButton.performClick();\n\n        verify(widgetUtils).showDatePickerDialog(widgetActivity, DateTimeWidgetUtils.getDatePickerDetails(\n                prompt.getAppearanceHint()), DateTimeUtils.getCurrentDateTime());\n    }\n\n    @Test\n    public void clickingSetTimeButton_callsDisplayTimePickerDialogWithCurrentTime_whenPromptDoesNotHaveAnswer() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, null);\n        DateTimeWidget widget = createWidget(prompt);\n        widget.binding.timeWidget.timeButton.performClick();\n\n        verify(widgetUtils).showTimePickerDialog(widgetActivity, DateTimeUtils.getCurrentDateTime());\n    }\n\n    @Test\n    public void clickingSetDateButton_callsDisplayDatePickerDialogWithSelectedDate_whenPromptHasAnswer() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, new DateData(localDateTime.toDate()));\n        DateTimeWidget widget = createWidget(prompt);\n        widget.binding.dateWidget.dateButton.performClick();\n\n        verify(widgetUtils).showDatePickerDialog(widgetActivity, DateTimeWidgetUtils.getDatePickerDetails(prompt.getAppearanceHint()),\n                DateTimeUtils.getSelectedDate(localDateTime, new LocalDateTime().withTime(0, 0, 0, 0)));\n    }\n\n    @Test\n    public void clickingSetTimeButton_callsDisplayTimePickerDialogWithSelectedTime_whenPromptHasAnswer() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, new TimeData(localDateTime.toDateTime().toDate()));\n        DateTimeWidget widget = createWidget(prompt);\n        widget.binding.timeWidget.timeButton.performClick();\n\n        verify(widgetUtils).showTimePickerDialog(widgetActivity, localDateTime);\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new DateTimeData(localDateTime.toDate())));\n        widget.clearAnswer();\n\n        assertThat(widget.getAnswer(), nullValue());\n        assertEquals(widget.binding.dateWidget.dateAnswerText.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.no_date_selected));\n        assertEquals(widget.binding.timeWidget.timeAnswerText.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.no_time_selected));\n    }\n\n    @Test\n    public void clearAnswer_callValueChangeListener() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new DateTimeData(localDateTime.toDate())));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.clearAnswer();\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void clickingButtonForLong_callsLongClickListener() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        widget.setOnLongClickListener(onLongClickListener);\n\n        widget.binding.dateWidget.dateButton.performLongClick();\n        widget.binding.timeWidget.timeButton.performLongClick();\n\n        verify(onLongClickListener).onLongClick(widget.binding.dateWidget.dateButton);\n        verify(onLongClickListener).onLongClick(widget.binding.timeWidget.timeButton);\n    }\n\n    @Test\n    public void clickingAnswerTextViewForLong_callsLongClickListener() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        widget.setOnLongClickListener(onLongClickListener);\n\n        widget.binding.dateWidget.dateAnswerText.performLongClick();\n        widget.binding.timeWidget.timeAnswerText.performLongClick();\n\n        verify(onLongClickListener).onLongClick(widget.binding.dateWidget.dateAnswerText);\n        verify(onLongClickListener).onLongClick(widget.binding.timeWidget.timeAnswerText);\n    }\n\n    @Test\n    public void setDateData_updatesValueShownInDateAnswerTextView() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, null);\n        DatePickerDetails datePickerDetails = DateTimeWidgetUtils.getDatePickerDetails(prompt.getAppearanceHint());\n        DateTimeWidget widget = createWidget(prompt);\n        widget.setData(new LocalDateTime().withDate(2010, 5, 12));\n\n        assertEquals(widget.binding.dateWidget.dateAnswerText.getText(),\n                DateTimeWidgetUtils.getDateTimeLabel(localDateTime.toDate(), datePickerDetails, false, widget.getContext()));\n    }\n\n    @Test\n    public void setDateData_updatesWidgetAnswer() {\n        LocalDateTime answer = new LocalDateTime().withDate(2010, 5, 12);\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        widget.setData(answer);\n\n        assertEquals(widget.getAnswer().getDisplayText(),\n                new DateTimeData(DateTimeUtils.getSelectedDate((LocalDateTime) answer, LocalDateTime.now()).toDate()).getDisplayText());\n    }\n\n    @Test\n    public void setTimeData_updatesValueShownInTimeAnswerTextView() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        widget.setData(new DateTime().withTime(12, 10, 0, 0));\n        assertEquals(widget.binding.timeWidget.timeAnswerText.getText(), DateTimeUtils.getTimeData(localDateTime.toDateTime()).getDisplayText());\n    }\n\n    @Test\n    public void setTimeData_updatesWidgetAnswer() {\n        DateTime answer = new DateTime().withTime(12, 10, 0, 0);\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        widget.setData(answer);\n\n        assertEquals(widget.getAnswer().getDisplayText(),\n                new DateTimeData(DateTimeUtils.getSelectedTime(((DateTime) answer).toLocalDateTime(), LocalDateTime.now()).toDate()).getDisplayText());\n    }\n\n    @Test\n    public void setData_callsValueChangeListener() {\n        DateTimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.setValueChangedListener(valueChangedListener);\n\n        widget.setData(new LocalDateTime().withDate(2010, 5, 12));\n        verify(valueChangedListener, times(1)).widgetValueChanged(widget);\n\n        widget.setData(new DateTime().withDate(2010, 5, 12));\n        verify(valueChangedListener, times(2)).widgetValueChanged(widget);\n    }\n\n    private DateTimeWidget createWidget(FormEntryPrompt prompt) {\n        return new DateTimeWidget(widgetActivity, new QuestionDetails(prompt), widgetUtils, new FakeWaitingForDataRegistry(), widgetDependencies());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/DateTimeWidgetUtilsTest.java",
    "content": "package org.odk.collect.android.widgets.datetime;\n\nimport androidx.fragment.app.DialogFragment;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.joda.time.LocalDateTime;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.widgets.datetime.pickers.BikramSambatDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.BuddhistDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.CopticDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.CustomTimePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.EthiopianDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.FixedDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.IslamicDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.MyanmarDatePickerDialog;\nimport org.odk.collect.android.widgets.datetime.pickers.PersianDatePickerDialog;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.testshared.RobolectricHelpers;\nimport org.odk.collect.android.support.WidgetTestActivity;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.datetime.DatePickerDetails.DatePickerType.BIKRAM_SAMBAT;\nimport static org.odk.collect.android.widgets.datetime.DatePickerDetails.DatePickerType.BUDDHIST;\nimport static org.odk.collect.android.widgets.datetime.DatePickerDetails.DatePickerType.COPTIC;\nimport static org.odk.collect.android.widgets.datetime.DatePickerDetails.DatePickerType.ETHIOPIAN;\nimport static org.odk.collect.android.widgets.datetime.DatePickerDetails.DatePickerType.GREGORIAN;\nimport static org.odk.collect.android.widgets.datetime.DatePickerDetails.DatePickerType.ISLAMIC;\nimport static org.odk.collect.android.widgets.datetime.DatePickerDetails.DatePickerType.MYANMAR;\nimport static org.odk.collect.android.widgets.datetime.DatePickerDetails.DatePickerType.PERSIAN;\n\nimport android.app.DatePickerDialog;\n\n@RunWith(AndroidJUnit4.class)\npublic class DateTimeWidgetUtilsTest {\n    private DateTimeWidgetUtils dateTimeWidgetUtils;\n\n    private WidgetTestActivity activity;\n    private DatePickerDetails datePickerDetails;\n    private LocalDateTime date;\n\n    private DatePickerDetails gregorian;\n    private DatePickerDetails gregorianSpinners;\n    private DatePickerDetails gregorianMonthYear;\n    private DatePickerDetails gregorianYear;\n\n    private DatePickerDetails ethiopian;\n    private DatePickerDetails ethiopianMonthYear;\n    private DatePickerDetails ethiopianYear;\n\n    private DatePickerDetails coptic;\n    private DatePickerDetails copticMonthYear;\n    private DatePickerDetails copticYear;\n\n    private DatePickerDetails islamic;\n    private DatePickerDetails islamicMonthYear;\n    private DatePickerDetails islamicYear;\n\n    private DatePickerDetails bikramSambat;\n    private DatePickerDetails bikramSambatMonthYear;\n    private DatePickerDetails bikramSambatYear;\n\n    private DatePickerDetails myanmar;\n    private DatePickerDetails myanmarMonthYear;\n    private DatePickerDetails myanmarYear;\n\n    private DatePickerDetails persian;\n    private DatePickerDetails persianMonthYear;\n    private DatePickerDetails persianYear;\n\n    private DatePickerDetails buddhist;\n    private DatePickerDetails buddhistMonthYear;\n    private DatePickerDetails buddhistYear;\n\n    @Before\n    public void setUp() {\n        dateTimeWidgetUtils = new DateTimeWidgetUtils();\n\n        activity = RobolectricHelpers.createThemedActivity(WidgetTestActivity.class);\n        datePickerDetails = mock(DatePickerDetails.class);\n\n        when(datePickerDetails.getDatePickerType()).thenReturn(GREGORIAN);\n        date  = new LocalDateTime().withYear(2010).withMonthOfYear(5).withDayOfMonth(12);\n\n        gregorian = new DatePickerDetails(DatePickerDetails.DatePickerType.GREGORIAN, DatePickerDetails.DatePickerMode.CALENDAR);\n        gregorianSpinners = new DatePickerDetails(DatePickerDetails.DatePickerType.GREGORIAN, DatePickerDetails.DatePickerMode.SPINNERS);\n        gregorianMonthYear = new DatePickerDetails(DatePickerDetails.DatePickerType.GREGORIAN, DatePickerDetails.DatePickerMode.MONTH_YEAR);\n        gregorianYear = new DatePickerDetails(DatePickerDetails.DatePickerType.GREGORIAN, DatePickerDetails.DatePickerMode.YEAR);\n\n        ethiopian = new DatePickerDetails(DatePickerDetails.DatePickerType.ETHIOPIAN, DatePickerDetails.DatePickerMode.SPINNERS);\n        ethiopianMonthYear = new DatePickerDetails(DatePickerDetails.DatePickerType.ETHIOPIAN, DatePickerDetails.DatePickerMode.MONTH_YEAR);\n        ethiopianYear = new DatePickerDetails(DatePickerDetails.DatePickerType.ETHIOPIAN, DatePickerDetails.DatePickerMode.YEAR);\n\n        coptic = new DatePickerDetails(DatePickerDetails.DatePickerType.COPTIC, DatePickerDetails.DatePickerMode.SPINNERS);\n        copticMonthYear = new DatePickerDetails(DatePickerDetails.DatePickerType.COPTIC, DatePickerDetails.DatePickerMode.MONTH_YEAR);\n        copticYear = new DatePickerDetails(DatePickerDetails.DatePickerType.COPTIC, DatePickerDetails.DatePickerMode.YEAR);\n\n        islamic = new DatePickerDetails(DatePickerDetails.DatePickerType.ISLAMIC, DatePickerDetails.DatePickerMode.SPINNERS);\n        islamicMonthYear = new DatePickerDetails(DatePickerDetails.DatePickerType.ISLAMIC, DatePickerDetails.DatePickerMode.MONTH_YEAR);\n        islamicYear = new DatePickerDetails(DatePickerDetails.DatePickerType.ISLAMIC, DatePickerDetails.DatePickerMode.YEAR);\n\n        bikramSambat = new DatePickerDetails(DatePickerDetails.DatePickerType.BIKRAM_SAMBAT, DatePickerDetails.DatePickerMode.SPINNERS);\n        bikramSambatMonthYear = new DatePickerDetails(DatePickerDetails.DatePickerType.BIKRAM_SAMBAT, DatePickerDetails.DatePickerMode.MONTH_YEAR);\n        bikramSambatYear = new DatePickerDetails(DatePickerDetails.DatePickerType.BIKRAM_SAMBAT, DatePickerDetails.DatePickerMode.YEAR);\n\n        myanmar = new DatePickerDetails(DatePickerDetails.DatePickerType.MYANMAR, DatePickerDetails.DatePickerMode.SPINNERS);\n        myanmarMonthYear = new DatePickerDetails(DatePickerDetails.DatePickerType.MYANMAR, DatePickerDetails.DatePickerMode.MONTH_YEAR);\n        myanmarYear = new DatePickerDetails(DatePickerDetails.DatePickerType.MYANMAR, DatePickerDetails.DatePickerMode.YEAR);\n\n        persian = new DatePickerDetails(DatePickerDetails.DatePickerType.PERSIAN, DatePickerDetails.DatePickerMode.SPINNERS);\n        persianMonthYear = new DatePickerDetails(DatePickerDetails.DatePickerType.PERSIAN, DatePickerDetails.DatePickerMode.MONTH_YEAR);\n        persianYear = new DatePickerDetails(DatePickerDetails.DatePickerType.PERSIAN, DatePickerDetails.DatePickerMode.YEAR);\n\n        buddhist = new DatePickerDetails(DatePickerDetails.DatePickerType.BUDDHIST, DatePickerDetails.DatePickerMode.SPINNERS);\n        buddhistMonthYear = new DatePickerDetails(DatePickerDetails.DatePickerType.BUDDHIST, DatePickerDetails.DatePickerMode.MONTH_YEAR);\n        buddhistYear = new DatePickerDetails(DatePickerDetails.DatePickerType.BUDDHIST, DatePickerDetails.DatePickerMode.YEAR);\n    }\n\n    @Test\n    public void getDatePickerDetailsTest() {\n        assertEquals(gregorian, DateTimeWidgetUtils.getDatePickerDetails(null));\n        String appearance = \"something\";\n        assertEquals(gregorian, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"no-calendar\";\n        assertEquals(gregorianSpinners, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"NO-CALENDAR\";\n        assertEquals(gregorianSpinners, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"month-year\";\n        assertEquals(gregorianMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"MONTH-year\";\n        assertEquals(gregorianMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"year\";\n        assertEquals(gregorianYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Year\";\n        assertEquals(gregorianYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n\n        appearance = \"ethiopian\";\n        assertEquals(ethiopian, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Ethiopian month-year\";\n        assertEquals(ethiopianMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"month-year ethiopian\";\n        assertEquals(ethiopianMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Ethiopian year\";\n        assertEquals(ethiopianYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"year ethiopian\";\n        assertEquals(ethiopianYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n\n        appearance = \"coptic\";\n        assertEquals(coptic, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Coptic month-year\";\n        assertEquals(copticMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"month-year coptic\";\n        assertEquals(copticMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Coptic year\";\n        assertEquals(copticYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"year coptic\";\n        assertEquals(copticYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n\n        appearance = \"islamic\";\n        assertEquals(islamic, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Islamic month-year\";\n        assertEquals(islamicMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"month-year islamic\";\n        assertEquals(islamicMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Islamic year\";\n        assertEquals(islamicYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"year islamic\";\n        assertEquals(islamicYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n\n        appearance = \"bikram-sambat\";\n        assertEquals(bikramSambat, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Bikram-sambat month-year\";\n        assertEquals(bikramSambatMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"month-year bikram-sambat\";\n        assertEquals(bikramSambatMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Bikram-sambat year\";\n        assertEquals(bikramSambatYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"year bikram-sambat\";\n        assertEquals(bikramSambatYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n\n        appearance = \"myanmar\";\n        assertEquals(myanmar, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Myanmar month-year\";\n        assertEquals(myanmarMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"month-year myanmar\";\n        assertEquals(myanmarMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Myanmar year\";\n        assertEquals(myanmarYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"year myanmar\";\n        assertEquals(myanmarYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n\n        appearance = \"persian\";\n        assertEquals(persian, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Persian month-year\";\n        assertEquals(persianMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"month-year persian\";\n        assertEquals(persianMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Persian year\";\n        assertEquals(persianYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"year persian\";\n        assertEquals(persianYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n\n        appearance = \"buddhist\";\n        assertEquals(buddhist, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Buddhist month-year\";\n        assertEquals(buddhistMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"month-year buddhist\";\n        assertEquals(buddhistMonthYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"Buddhist year\";\n        assertEquals(buddhistYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n        appearance = \"year buddhist\";\n        assertEquals(buddhistYear, DateTimeWidgetUtils.getDatePickerDetails(appearance));\n    }\n\n    @Test\n    public void displayTimePickerDialog_showsCustomTimePickerDialog() {\n        dateTimeWidgetUtils.showTimePickerDialog(activity, new LocalDateTime().withHourOfDay(12).withMinuteOfHour(10));\n        CustomTimePickerDialog dialog = (CustomTimePickerDialog) activity.getSupportFragmentManager()\n                .findFragmentByTag(CustomTimePickerDialog.class.getName());\n\n        assertNotNull(dialog);\n    }\n\n    @Test\n    public void displayDatePickerDialog_showsFixedDatePickerDialog_whenDatePickerTypeIsGregorian() {\n        assertDialogIsShowing(GREGORIAN, FixedDatePickerDialog.class);\n    }\n\n    @Test\n    public void displayDatePickerDialog_showsEthiopianDatePickerDialog_whenDatePickerTypeIsEthiopian() {\n        assertDialogIsShowing(ETHIOPIAN, EthiopianDatePickerDialog.class);\n    }\n\n    @Test\n    public void displayDatePickerDialog_showsCopticDatePickerDialog_whenDatePickerTypeIsCoptic() {\n        assertDialogIsShowing(COPTIC, CopticDatePickerDialog.class);\n    }\n\n    @Test\n    public void displayDatePickerDialog_showsIslamicDatePickerDialog_whenDatePickerTypeIsIslamic() {\n        assertDialogIsShowing(ISLAMIC, IslamicDatePickerDialog.class);\n    }\n\n    @Test\n    public void displayDatePickerDialog_showsBikramSambatDatePickerDialog_whenDatePickerTypeIsBikramSambat() {\n        assertDialogIsShowing(BIKRAM_SAMBAT, BikramSambatDatePickerDialog.class);\n    }\n\n    @Test\n    public void displayDatePickerDialog_showsMyanmarDatePickerDialog_whenDatePickerTypeIsMyanmar() {\n        assertDialogIsShowing(MYANMAR, MyanmarDatePickerDialog.class);\n    }\n\n    @Test\n    public void displayDatePickerDialog_showsPersianDatePickerDialog_whenDatePickerTypeIsPersian() {\n        assertDialogIsShowing(PERSIAN, PersianDatePickerDialog.class);\n    }\n\n    @Test\n    public void displayDatePickerDialog_showsBuddhistDatePickerDialog_whenDatePickerTypeIsBuddhist() {\n        assertDialogIsShowing(BUDDHIST, BuddhistDatePickerDialog.class);\n    }\n\n    @Test\n    public void displayDatePickerDialogWithYearMode_showsDatePickerWithDayAndMonthFixedToJanuaryFirst() {\n        when(datePickerDetails.getDatePickerType()).thenReturn(GREGORIAN);\n\n        dateTimeWidgetUtils.showDatePickerDialog(activity, gregorianYear, date);\n        DialogFragment dialog = (DialogFragment) activity.getSupportFragmentManager()\n                .findFragmentByTag(FixedDatePickerDialog.class.getName());\n\n        assertThat(((DatePickerDialog) dialog.getDialog()).getDatePicker().getYear(), is(date.getYear()));\n        assertThat(((DatePickerDialog) dialog.getDialog()).getDatePicker().getMonth(), is(0));\n        assertThat(((DatePickerDialog) dialog.getDialog()).getDatePicker().getDayOfMonth(), is(1));\n    }\n\n    private void assertDialogIsShowing(DatePickerDetails.DatePickerType datePickerType, Class dialogClass) {\n        when(datePickerDetails.getDatePickerType()).thenReturn(datePickerType);\n\n        dateTimeWidgetUtils.showDatePickerDialog(activity, datePickerDetails, date);\n        DialogFragment dialog = (DialogFragment) activity.getSupportFragmentManager()\n                .findFragmentByTag(dialogClass.getName());\n\n        assertNotNull(dialog);\n        assertEquals(dialog.getClass(), dialogClass);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/DateWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.datetime;\n\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.data.DateData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.joda.time.LocalDateTime;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithQuestionDefAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndQuestionDef;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\n@RunWith(AndroidJUnit4.class)\npublic class DateWidgetTest {\n    private WidgetTestActivity widgetActivity;\n    private DateTimeWidgetUtils widgetUtils;\n    private View.OnLongClickListener onLongClickListener;\n\n    private QuestionDef questionDef;\n    private LocalDateTime dateAnswer;\n\n    @Before\n    public void setUp() {\n        widgetActivity = widgetTestActivity();\n\n        questionDef = mock(QuestionDef.class);\n        onLongClickListener = mock(View.OnLongClickListener.class);\n        widgetUtils = mock(DateTimeWidgetUtils.class);\n\n        dateAnswer = DateTimeUtils.getSelectedDate(new LocalDateTime().withDate(2010, 5, 12), LocalDateTime.now());\n    }\n\n    @Test\n    public void usingReadOnlyOption_doesNotShowButton() {\n        DateWidget widget = createWidget(promptWithReadOnlyAndQuestionDef(questionDef));\n        assertEquals(widget.binding.dateButton.getVisibility(), View.GONE);\n    }\n\n    @Test\n    public void whenPromptIsNotReadOnly_buttonShowsCorrectText() {\n        DateWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        assertEquals(widget.binding.dateButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.select_date));\n    }\n\n    @Test\n    public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {\n        assertThat(createWidget(promptWithQuestionDefAndAnswer(questionDef, null)).getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsDate() {\n        DateWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new DateData(dateAnswer.toDate())));\n        assertEquals(widget.getAnswer().getDisplayText(), new DateData(dateAnswer.toDate()).getDisplayText());\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_answerTextViewShowsNoDateSelected() {\n        DateWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        assertEquals(widget.binding.dateAnswerText.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.no_date_selected));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_answerTextViewShowsCorrectDate() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, new DateData(dateAnswer.toDate()));\n        DatePickerDetails datePickerDetails = DateTimeWidgetUtils.getDatePickerDetails(prompt.getAppearanceHint());\n        DateWidget widget = createWidget(prompt);\n\n        assertEquals(widget.binding.dateAnswerText.getText(),\n                DateTimeWidgetUtils.getDateTimeLabel(dateAnswer.toDate(), datePickerDetails, false, widget.getContext()));\n    }\n\n    @Test\n    public void clickingButton_callsDisplayDatePickerDialogWithCurrentDate_whenPromptDoesNotHaveAnswer() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, null);\n        DateWidget widget = createWidget(prompt);\n        widget.binding.dateButton.performClick();\n\n        verify(widgetUtils).showDatePickerDialog(widgetActivity, DateTimeWidgetUtils.getDatePickerDetails(\n                prompt.getAppearanceHint()), DateTimeUtils.getCurrentDateTime());\n    }\n\n    @Test\n    public void clickingButton_callsDisplayDatePickerDialogWithSelectedDate_whenPromptHasAnswer() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, new DateData(dateAnswer.toDate()));\n        DateWidget widget = createWidget(prompt);\n        widget.binding.dateButton.performClick();\n\n        verify(widgetUtils).showDatePickerDialog(widgetActivity, DateTimeWidgetUtils.getDatePickerDetails(\n                prompt.getAppearanceHint()), dateAnswer);\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        DateWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        widget.clearAnswer();\n        assertEquals(widget.binding.dateAnswerText.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.no_date_selected));\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListener() {\n        DateWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new DateData(dateAnswer.toDate())));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.clearAnswer();\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void clickingButtonAndAnswerTextViewForLong_callsLongClickListener() {\n        DateWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new DateData(dateAnswer.toDate())));\n        widget.setOnLongClickListener(onLongClickListener);\n        widget.binding.dateButton.performLongClick();\n        widget.binding.dateAnswerText.performLongClick();\n\n        verify(onLongClickListener).onLongClick(widget.binding.dateButton);\n        verify(onLongClickListener).onLongClick(widget.binding.dateAnswerText);\n    }\n\n    @Test\n    public void setData_updatesWidgetAnswer() {\n        DateWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        widget.setData(dateAnswer);\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(new DateData(dateAnswer.toDate()).getDisplayText()));\n    }\n\n    @Test\n    public void setData_updatesValueDisplayedInAnswerTextView() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, null);\n        DatePickerDetails datePickerDetails = DateTimeWidgetUtils.getDatePickerDetails(prompt.getAppearanceHint());\n        DateWidget widget = createWidget(prompt);\n        widget.setData(dateAnswer);\n\n        assertEquals(widget.binding.dateAnswerText.getText(),\n                DateTimeWidgetUtils.getDateTimeLabel(dateAnswer.toDate(), datePickerDetails, false, widget.getContext()));\n    }\n\n    @Test\n    public void setData_callsValueChangeListener() {\n        DateWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.setValueChangedListener(valueChangedListener);\n        widget.setData(dateAnswer);\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    private DateWidget createWidget(FormEntryPrompt prompt) {\n        return new DateWidget(widgetActivity, new QuestionDetails(prompt), widgetUtils, new FakeWaitingForDataRegistry(), widgetDependencies());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/DaylightSavingTest.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.android.widgets.datetime;\n\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\n\nimport android.app.DatePickerDialog;\nimport android.widget.DatePicker;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.IFormElement;\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.joda.time.DateTime;\nimport org.joda.time.LocalDateTime;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\nimport org.odk.collect.testshared.TimeZoneSetter;\n\nimport java.util.TimeZone;\n\n@RunWith(AndroidJUnit4.class)\n/** https://github.com/getodk/collect/issues/356\n * Verify that the {@link DateWidget} and {@link DateTimeWidget} widget skips over\n * \"daylight savings gaps\".\n * This is needed on the day and time of a daylight savings transition because that date/time\n * doesn't exist.\n * In this test we set time to daylight saving gap and check if any crash occur*/\npublic class DaylightSavingTest {\n\n    private static final String EAT_IME_ZONE = \"Africa/Nairobi\";\n    private static final String CET_TIME_ZONE = \"Europe/Warsaw\";\n\n    private WidgetTestActivity widgetActivity;\n    private DateTimeWidgetUtils widgetUtils;\n    private TimeZone currentTimeZone;\n\n    @Before\n    public void setUp() {\n        widgetActivity = CollectHelpers.buildThemedActivity(WidgetTestActivity.class).get();\n        widgetUtils = mock(DateTimeWidgetUtils.class);\n        currentTimeZone = TimeZone.getDefault();\n    }\n\n    @After\n    public void tearDown() {\n        TimeZoneSetter.setTimezone(currentTimeZone);\n    }\n\n    @Test\n    // 26 Mar 2017 at 02:00:00 clocks were turned forward to 03:00:00.\n    public void testESTTimeZoneWithDateTimeWidget() {\n        TimeZoneSetter.setTimezone(TimeZone.getTimeZone(CET_TIME_ZONE));\n\n        DateTimeWidget dateTimeWidget = prepareDateTimeWidget(2017, 3, 26, 2, 30);\n\n        /*\n         * We would get crash in this place using old approach {@link org.joda.time.DateTime} instead of\n         * {@link org.joda.time.LocalDateTime}\n         */\n        dateTimeWidget.getAnswer();\n    }\n\n    @Test\n    // 1 Jan 1960 at 00:00:00 clocks were turned forward to 00:15:00\n    public void testEATTimezoneWithDateWidget() {\n        TimeZoneSetter.setTimezone(TimeZone.getTimeZone(EAT_IME_ZONE));\n\n        DateWidget dateWidget = prepareDateWidget(1960, 0, 1);\n\n        /*\n         * We would get crash in this place using old approach {@link org.joda.time.DateTime} instead of\n         * {@link org.joda.time.LocalDateTime}\n         */\n        dateWidget.getAnswer();\n    }\n\n    private DateWidget prepareDateWidget(int year, int month, int day) {\n        QuestionDef questionDefStub = mock(QuestionDef.class);\n\n        FormEntryPrompt formEntryPromptStub = new MockFormEntryPromptBuilder()\n                .withIndex(\"index\")\n                .build();\n        IFormElement iformElementStub = formEntryPromptStub.getFormElement();\n\n        when(iformElementStub.getAdditionalAttribute(anyString(), anyString())).thenReturn(null);\n        when(formEntryPromptStub.getQuestion()).thenReturn(questionDefStub);\n        when(formEntryPromptStub.getFormElement()).thenReturn(iformElementStub);\n        when(formEntryPromptStub.getAppearanceHint()).thenReturn(\"no-calendar\");\n\n        DatePickerDialog datePickerDialog = mock(DatePickerDialog.class);\n        DatePicker datePicker = mock(DatePicker.class);\n        when(datePickerDialog.getDatePicker()).thenReturn(datePicker);\n        when(datePickerDialog.getDatePicker().getYear()).thenReturn(year);\n        when(datePickerDialog.getDatePicker().getMonth()).thenReturn(month);\n        when(datePickerDialog.getDatePicker().getDayOfMonth()).thenReturn(day);\n\n        return new DateWidget(widgetActivity, new QuestionDetails(formEntryPromptStub), widgetUtils, null, widgetDependencies());\n    }\n\n    private DateTimeWidget prepareDateTimeWidget(int year, int month, int day, int hour, int minute) {\n        QuestionDef questionDefStub = mock(QuestionDef.class);\n\n        FormEntryPrompt formEntryPromptStub = new MockFormEntryPromptBuilder()\n                .withIndex(\"index\")\n                .build();\n        IFormElement iformElementStub = formEntryPromptStub.getFormElement();\n\n        when(iformElementStub.getAdditionalAttribute(anyString(), anyString())).thenReturn(null);\n        when(formEntryPromptStub.getQuestion()).thenReturn(questionDefStub);\n        when(formEntryPromptStub.getFormElement()).thenReturn(iformElementStub);\n        when(formEntryPromptStub.getAppearanceHint()).thenReturn(\"no-calendar\");\n\n        DateTimeWidget dateTimeWidget = new DateTimeWidget(widgetActivity, new QuestionDetails(formEntryPromptStub), widgetUtils, null, widgetDependencies());\n        dateTimeWidget.setData(new LocalDateTime().withDate(year, month, day));\n        dateTimeWidget.setData(new DateTime().withTime(hour, minute, 0, 0));\n\n        return dateTimeWidget;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/TimeWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.datetime;\n\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.data.TimeData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.joda.time.LocalDateTime;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithQuestionDefAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndQuestionDef;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\n@RunWith(AndroidJUnit4.class)\npublic class TimeWidgetTest {\n    private WidgetTestActivity widgetActivity;\n    private DateTimeWidgetUtils widgetUtils;\n    private View.OnLongClickListener onLongClickListener;\n\n    private QuestionDef questionDef;\n    private LocalDateTime timeAnswer;\n\n    @Before\n    public void setUp() {\n        widgetActivity = widgetTestActivity();\n\n        questionDef = mock(QuestionDef.class);\n        onLongClickListener = mock(View.OnLongClickListener.class);\n        widgetUtils = mock(DateTimeWidgetUtils.class);\n\n        timeAnswer = DateTimeUtils.getSelectedTime(new LocalDateTime().withTime(12, 10, 0, 0), LocalDateTime.now());\n    }\n\n    @Test\n    public void usingReadOnlyOption_doesNotShowButton() {\n        TimeWidget widget = createWidget(promptWithReadOnlyAndQuestionDef(questionDef));\n        assertEquals(widget.binding.timeButton.getVisibility(), View.GONE);\n    }\n\n    @Test\n    public void whenPromptIsNotReadOnly_buttonShowsCorrectText() {\n        TimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        assertEquals(widget.binding.timeButton.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.select_time));\n    }\n\n    @Test\n    public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {\n        assertThat(createWidget(promptWithQuestionDefAndAnswer(questionDef, null)).getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsTime() {\n        TimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new TimeData(timeAnswer.toDateTime().toDate())));\n        assertEquals(widget.getAnswer().getDisplayText(), new TimeData(timeAnswer.toDateTime().toDate()).getDisplayText());\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_answerTextViewShowsNoTimeSelected() {\n        TimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        assertEquals(widget.binding.timeAnswerText.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.no_time_selected));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_answerTextViewShowsCorrectTime() {\n        TimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new TimeData(timeAnswer.toDateTime().toDate())));\n        assertEquals(widget.binding.timeAnswerText.getText(), DateTimeUtils.getTimeData(timeAnswer.toDateTime()).getDisplayText());\n    }\n\n    @Test\n    public void clickingButton_callsDisplayTimePickerDialogWithCurrentTime_whenPromptDoesNotHaveAnswer() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, null);\n        TimeWidget widget = createWidget(prompt);\n        widget.binding.timeButton.performClick();\n\n        verify(widgetUtils).showTimePickerDialog(widgetActivity, DateTimeUtils.getCurrentDateTime());\n    }\n\n    @Test\n    public void clickingButton_callsDisplayTimePickerDialogWithSelectedTime_whenPromptHasAnswer() {\n        FormEntryPrompt prompt = promptWithQuestionDefAndAnswer(questionDef, new TimeData(timeAnswer.toDateTime().toDate()));\n        TimeWidget widget = createWidget(prompt);\n        widget.binding.timeButton.performClick();\n\n        verify(widgetUtils).showTimePickerDialog(widgetActivity, timeAnswer);\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        TimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new TimeData(timeAnswer.toDateTime().toDate())));\n        widget.clearAnswer();\n        assertEquals(widget.binding.timeAnswerText.getText(), widget.getContext().getString(org.odk.collect.strings.R.string.no_time_selected));\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListener() {\n        TimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, new TimeData(timeAnswer.toDateTime().toDate())));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.clearAnswer();\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void clickingButtonAndAnswerTextViewForLong_callsLongClickListener() {\n        TimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        widget.setOnLongClickListener(onLongClickListener);\n        widget.binding.timeButton.performLongClick();\n        widget.binding.timeAnswerText.performLongClick();\n\n        verify(onLongClickListener).onLongClick(widget.binding.timeButton);\n        verify(onLongClickListener).onLongClick(widget.binding.timeAnswerText);\n    }\n\n    @Test\n    public void setData_updatesWidgetAnswer() {\n        TimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        widget.setData(timeAnswer.toDateTime());\n        assertEquals(widget.getAnswer().getDisplayText(), new TimeData(timeAnswer.toDateTime().toDate()).getDisplayText());\n    }\n\n    @Test\n    public void setData_updatesValueDisplayedInAnswerTextView() {\n        TimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        widget.setData(timeAnswer.toDateTime());\n        assertEquals(widget.binding.timeAnswerText.getText(), DateTimeUtils.getTimeData(timeAnswer.toDateTime()).getDisplayText());\n    }\n\n    @Test\n    public void setData_callsValueChangeListener() {\n        TimeWidget widget = createWidget(promptWithQuestionDefAndAnswer(questionDef, null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.setValueChangedListener(valueChangedListener);\n        widget.setData(timeAnswer.toDateTime());\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    private TimeWidget createWidget(FormEntryPrompt prompt) {\n        return new TimeWidget(widgetActivity, new QuestionDetails(prompt), widgetUtils, new FakeWaitingForDataRegistry(), widgetDependencies());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/pickers/BikramSambatDatePickerDialogTest.java",
    "content": "package org.odk.collect.android.widgets.datetime.pickers;\n\nimport static org.mockito.Mockito.when;\n\nimport android.content.DialogInterface;\n\nimport androidx.fragment.app.FragmentManager;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.fragments.support.DialogFragmentHelpers;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\n@RunWith(AndroidJUnit4.class)\npublic class BikramSambatDatePickerDialogTest {\n    private FragmentManager fragmentManager;\n    private BikramSambatDatePickerDialog dialogFragment;\n    private DatePickerDetails datePickerDetails;\n    private DialogFragmentHelpers.DatePickerTestActivity activity;\n\n    @Before\n    public void setup() {\n        activity = CollectHelpers.createThemedActivity(DialogFragmentHelpers.DatePickerTestActivity.class);\n        fragmentManager = activity.getSupportFragmentManager();\n\n        dialogFragment = new BikramSambatDatePickerDialog();\n        datePickerDetails = DialogFragmentHelpers.setUpDatePickerDetails(DatePickerDetails.DatePickerType.BIKRAM_SAMBAT);\n        dialogFragment.setArguments(DialogFragmentHelpers.getDialogFragmentArguments(datePickerDetails));\n    }\n\n    @Test\n    public void dialogIsCancellable() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsCancellable(true);\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDate(2077, 0, 30, \"30 बैशाख 2077 (May 12, 2020)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forYearMode() {\n        when(datePickerDetails.isYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForYearMode(2077, \"2077 (2020)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forMonthMode() {\n        when(datePickerDetails.isMonthYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForMonthMode(2077, 0, \"बैशाख 2077 (Apr 2020)\");\n    }\n\n    @Test\n    public void settingDateInDatePicker_changesDateShownInTextView() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogTextViewUpdatesDate(\"30 बैशाख 2077 (May 12, 2020)\", 2077, 0, 30);\n    }\n\n    @Test\n    public void whenScreenIsRotated_dialogShouldRetainDateInDatePickerAndTextView() {\n        DialogFragmentHelpers.assertDialogRetainsDateOnScreenRotation(dialogFragment, \"30 बैशाख 2077 (May 12, 2020)\", 2077, 0, 30);\n    }\n\n    @Test\n    public void clickingOk_updatesDateInActivity() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDateUpdateInActivity(activity, 2077, 0, 30);\n    }\n\n    @Test\n    public void clickingOk_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_POSITIVE);\n    }\n\n    @Test\n    public void clickingCancel_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_NEGATIVE);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/pickers/BuddhistDatePickerDialogTest.kt",
    "content": "package org.odk.collect.android.widgets.datetime.pickers\n\nimport android.os.Bundle\nimport androidx.fragment.app.testing.FragmentScenario\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.notNullValue\nimport org.hamcrest.Matchers.nullValue\nimport org.joda.time.LocalDateTime\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails.DatePickerMode\nimport org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.testshared.EspressoAssertions\nimport org.odk.collect.testshared.EspressoInteractions\nimport org.odk.collect.testshared.ViewActions\n\n@RunWith(AndroidJUnit4::class)\nclass BuddhistDatePickerDialogTest {\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Test\n    fun `The dialog is cancelable`() {\n        val scenario = launchFragment(2010, 5, 12)\n        scenario.onFragment {\n            assertThat(it.isCancelable, equalTo(true))\n        }\n    }\n\n    @Test\n    fun `The dialog shows correct date`() {\n        launchFragment(2010, 5, 12, DatePickerMode.SPINNERS)\n        EspressoAssertions.assertVisible(withText(\"12 พฤษภาคม 2553 (May 12, 2010)\"), isDialog())\n\n        updateDate(6, 0, 2447)\n        EspressoAssertions.assertVisible(withText(\"6 มกราคม 2447 (Jan 06, 1904)\"), isDialog())\n\n        updateDate(13, 1, 2459)\n        EspressoAssertions.assertVisible(withText(\"13 กุมภาพันธ์ 2459 (Feb 13, 1916)\"), isDialog())\n\n        updateDate(21, 2, 2467)\n        EspressoAssertions.assertVisible(withText(\"21 มีนาคม 2467 (Mar 21, 1924)\"), isDialog())\n\n        updateDate(10, 3, 2479)\n        EspressoAssertions.assertVisible(withText(\"10 เมษายน 2479 (Apr 10, 1936)\"), isDialog())\n\n        updateDate(18, 4, 2487)\n        EspressoAssertions.assertVisible(withText(\"18 พฤษภาคม 2487 (May 18, 1944)\"), isDialog())\n\n        updateDate(27, 5, 2499)\n        EspressoAssertions.assertVisible(withText(\"27 มิถุนายน 2499 (Jun 27, 1956)\"), isDialog())\n\n        updateDate(8, 6, 2507)\n        EspressoAssertions.assertVisible(withText(\"8 กรกฎาคม 2507 (Jul 08, 1964)\"), isDialog())\n\n        updateDate(15, 7, 2519)\n        EspressoAssertions.assertVisible(withText(\"15 สิงหาคม 2519 (Aug 15, 1976)\"), isDialog())\n\n        updateDate(23, 8, 2527)\n        EspressoAssertions.assertVisible(withText(\"23 กันยายน 2527 (Sep 23, 1984)\"), isDialog())\n\n        updateDate(30, 9, 2539)\n        EspressoAssertions.assertVisible(withText(\"30 ตุลาคม 2539 (Oct 30, 1996)\"), isDialog())\n\n        updateDate(5, 10, 2547)\n        EspressoAssertions.assertVisible(withText(\"5 พฤศจิกายน 2547 (Nov 05, 2004)\"), isDialog())\n\n        updateDate(12, 11, 2559)\n        EspressoAssertions.assertVisible(withText(\"12 ธันวาคม 2559 (Dec 12, 2016)\"), isDialog())\n\n        updateDate(7, 0, 2567)\n        EspressoAssertions.assertVisible(withText(\"7 มกราคม 2567 (Jan 07, 2024)\"), isDialog())\n\n        updateDate(14, 1, 2579)\n        EspressoAssertions.assertVisible(withText(\"14 กุมภาพันธ์ 2579 (Feb 14, 2036)\"), isDialog())\n\n        updateDate(22, 2, 2587)\n        EspressoAssertions.assertVisible(withText(\"22 มีนาคม 2587 (Mar 22, 2044)\"), isDialog())\n\n        updateDate(11, 3, 2599)\n        EspressoAssertions.assertVisible(withText(\"11 เมษายน 2599 (Apr 11, 2056)\"), isDialog())\n\n        updateDate(19, 4, 2607)\n        EspressoAssertions.assertVisible(withText(\"19 พฤษภาคม 2607 (May 19, 2064)\"), isDialog())\n\n        updateDate(26, 5, 2619)\n        EspressoAssertions.assertVisible(withText(\"26 มิถุนายน 2619 (Jun 26, 2076)\"), isDialog())\n\n        updateDate(9, 6, 2627)\n        EspressoAssertions.assertVisible(withText(\"9 กรกฎาคม 2627 (Jul 09, 2084)\"), isDialog())\n\n        updateDate(16, 7, 2639)\n        EspressoAssertions.assertVisible(withText(\"16 สิงหาคม 2639 (Aug 16, 2096)\"), isDialog())\n    }\n\n    @Test\n    fun `The dialog shows correct date for 'year' mode`() {\n        launchFragment(2010, 5, 12, DatePickerMode.YEAR)\n        EspressoAssertions.assertVisible(withText(\"2553 (2010)\"), isDialog())\n\n        updateDate(year = 2447)\n        EspressoAssertions.assertVisible(withText(\"2447 (1904)\"), isDialog())\n\n        updateDate(year = 2453)\n        EspressoAssertions.assertVisible(withText(\"2453 (1910)\"), isDialog())\n\n        updateDate(year = 2461)\n        EspressoAssertions.assertVisible(withText(\"2461 (1918)\"), isDialog())\n\n        updateDate(year = 2468)\n        EspressoAssertions.assertVisible(withText(\"2468 (1925)\"), isDialog())\n\n        updateDate(year = 2474)\n        EspressoAssertions.assertVisible(withText(\"2474 (1931)\"), isDialog())\n\n        updateDate(year = 2483)\n        EspressoAssertions.assertVisible(withText(\"2483 (1940)\"), isDialog())\n\n        updateDate(year = 2501)\n        EspressoAssertions.assertVisible(withText(\"2501 (1958)\"), isDialog())\n\n        updateDate(year = 2509)\n        EspressoAssertions.assertVisible(withText(\"2509 (1966)\"), isDialog())\n\n        updateDate(year = 2517)\n        EspressoAssertions.assertVisible(withText(\"2517 (1974)\"), isDialog())\n\n        updateDate(year = 2525)\n        EspressoAssertions.assertVisible(withText(\"2525 (1982)\"), isDialog())\n\n        updateDate(year = 2542)\n        EspressoAssertions.assertVisible(withText(\"2542 (1999)\"), isDialog())\n\n        updateDate(year = 2551)\n        EspressoAssertions.assertVisible(withText(\"2551 (2008)\"), isDialog())\n\n        updateDate(year = 2559)\n        EspressoAssertions.assertVisible(withText(\"2559 (2016)\"), isDialog())\n\n        updateDate(year = 2567)\n        EspressoAssertions.assertVisible(withText(\"2567 (2024)\"), isDialog())\n\n        updateDate(year = 2575)\n        EspressoAssertions.assertVisible(withText(\"2575 (2032)\"), isDialog())\n\n        updateDate(year = 2583)\n        EspressoAssertions.assertVisible(withText(\"2583 (2040)\"), isDialog())\n\n        updateDate(year = 2599)\n        EspressoAssertions.assertVisible(withText(\"2599 (2056)\"), isDialog())\n\n        updateDate(year = 2608)\n        EspressoAssertions.assertVisible(withText(\"2608 (2065)\"), isDialog())\n\n        updateDate(year = 2616)\n        EspressoAssertions.assertVisible(withText(\"2616 (2073)\"), isDialog())\n\n        updateDate(year = 2624)\n        EspressoAssertions.assertVisible(withText(\"2624 (2081)\"), isDialog())\n\n        updateDate(year = 2632)\n        EspressoAssertions.assertVisible(withText(\"2632 (2089)\"), isDialog())\n\n        updateDate(year = 2638)\n        EspressoAssertions.assertVisible(withText(\"2638 (2095)\"), isDialog())\n    }\n\n    @Test\n    fun `The dialog shows correct date for 'month-year' mode`() {\n        launchFragment(2010, 5, 12, DatePickerMode.MONTH_YEAR)\n        EspressoAssertions.assertVisible(withText(\"พฤษภาคม 2553 (May 2010)\"), isDialog())\n\n        updateDate(month = 0, year = 2448)\n        EspressoAssertions.assertVisible(withText(\"มกราคม 2448 (Jan 1905)\"), isDialog())\n\n        updateDate(month = 1, year = 2454)\n        EspressoAssertions.assertVisible(withText(\"กุมภาพันธ์ 2454 (Feb 1911)\"), isDialog())\n\n        updateDate(month = 2, year = 2464)\n        EspressoAssertions.assertVisible(withText(\"มีนาคม 2464 (Mar 1921)\"), isDialog())\n\n        updateDate(month = 3, year = 2473)\n        EspressoAssertions.assertVisible(withText(\"เมษายน 2473 (Apr 1930)\"), isDialog())\n\n        updateDate(month = 4, year = 2486)\n        EspressoAssertions.assertVisible(withText(\"พฤษภาคม 2486 (May 1943)\"), isDialog())\n\n        updateDate(month = 5, year = 2495)\n        EspressoAssertions.assertVisible(withText(\"มิถุนายน 2495 (Jun 1952)\"), isDialog())\n\n        updateDate(month = 6, year = 2508)\n        EspressoAssertions.assertVisible(withText(\"กรกฎาคม 2508 (Jul 1965)\"), isDialog())\n\n        updateDate(month = 7, year = 2519)\n        EspressoAssertions.assertVisible(withText(\"สิงหาคม 2519 (Aug 1976)\"), isDialog())\n\n        updateDate(month = 8, year = 2527)\n        EspressoAssertions.assertVisible(withText(\"กันยายน 2527 (Sep 1984)\"), isDialog())\n\n        updateDate(month = 9, year = 2538)\n        EspressoAssertions.assertVisible(withText(\"ตุลาคม 2538 (Oct 1995)\"), isDialog())\n\n        updateDate(month = 10, year = 2549)\n        EspressoAssertions.assertVisible(withText(\"พฤศจิกายน 2549 (Nov 2006)\"), isDialog())\n\n        updateDate(month = 11, year = 2560)\n        EspressoAssertions.assertVisible(withText(\"ธันวาคม 2560 (Dec 2017)\"), isDialog())\n\n        updateDate(month = 0, year = 2571)\n        EspressoAssertions.assertVisible(withText(\"มกราคม 2571 (Jan 2028)\"), isDialog())\n\n        updateDate(month = 1, year = 2582)\n        EspressoAssertions.assertVisible(withText(\"กุมภาพันธ์ 2582 (Feb 2039)\"), isDialog())\n\n        updateDate(month = 2, year = 2592)\n        EspressoAssertions.assertVisible(withText(\"มีนาคม 2592 (Mar 2049)\"), isDialog())\n\n        updateDate(month = 2, year = 2595)\n        EspressoAssertions.assertVisible(withText(\"มีนาคม 2595 (Mar 2052)\"), isDialog())\n\n        updateDate(month = 3, year = 2604)\n        EspressoAssertions.assertVisible(withText(\"เมษายน 2604 (Apr 2061)\"), isDialog())\n\n        updateDate(month = 4, year = 2615)\n        EspressoAssertions.assertVisible(withText(\"พฤษภาคม 2615 (May 2072)\"), isDialog())\n\n        updateDate(month = 5, year = 2626)\n        EspressoAssertions.assertVisible(withText(\"มิถุนายน 2626 (Jun 2083)\"), isDialog())\n\n        updateDate(month = 6, year = 2637)\n        EspressoAssertions.assertVisible(withText(\"กรกฎาคม 2637 (Jul 2094)\"), isDialog())\n    }\n\n    @Test\n    fun `recreating maintains the date`() {\n        val scenario = launchFragment(2010, 5, 12)\n        EspressoAssertions.assertVisible(withText(\"12 พฤษภาคม 2553 (May 12, 2010)\"), isDialog())\n        scenario.recreate()\n        EspressoAssertions.assertVisible(withText(\"12 พฤษภาคม 2553 (May 12, 2010)\"), isDialog())\n    }\n\n    @Test\n    fun `clicking OK dismisses the dialog`() {\n        val scenario = launchFragment(2010, 5, 12)\n        scenario.onFragment {\n            assertThat(it.dialog, notNullValue())\n            EspressoInteractions.clickOn(withText(string.ok), isDialog())\n            assertThat(it.dialog, nullValue())\n        }\n    }\n\n    @Test\n    fun `clicking CANCEL dismisses the dialog`() {\n        val scenario = launchFragment(2010, 5, 12)\n        scenario.onFragment {\n            assertThat(it.dialog, notNullValue())\n            EspressoInteractions.clickOn(withText(string.cancel), isDialog())\n            assertThat(it.dialog, nullValue())\n        }\n    }\n\n    private fun updateDate(day: Int? = null, month: Int? = null, year: Int? = null) {\n        day?.let {\n            onView(withId(R.id.day_picker)).inRoot(isDialog()).perform(ViewActions.scrollNumberPickerToValue(it))\n        }\n        month?.let {\n            onView(withId(R.id.month_picker)).inRoot(isDialog()).perform(ViewActions.scrollNumberPickerToValue(it))\n        }\n        year?.let {\n            onView(withId(R.id.year_picker)).inRoot(isDialog()).perform(ViewActions.scrollNumberPickerToValue(it))\n        }\n    }\n\n    private fun launchFragment(\n        year: Int,\n        month: Int,\n        day: Int,\n        datePickerMode: DatePickerMode = DatePickerMode.SPINNERS\n    ): FragmentScenario<BuddhistDatePickerDialog> {\n        val args = Bundle().apply {\n            putSerializable(\n                DateTimeWidgetUtils.DATE,\n                LocalDateTime().withYear(year).withMonthOfYear(month).withDayOfMonth(day)\n            )\n            putSerializable(\n                DateTimeWidgetUtils.DATE_PICKER_DETAILS,\n                DatePickerDetails(\n                    DatePickerDetails.DatePickerType.BUDDHIST,\n                    datePickerMode\n                )\n            )\n        }\n\n        return launcherRule.launch(BuddhistDatePickerDialog::class.java, args)\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/pickers/CopticDatePickerDialogTest.java",
    "content": "package org.odk.collect.android.widgets.datetime.pickers;\n\nimport android.content.DialogInterface;\n\nimport androidx.fragment.app.FragmentManager;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.fragments.support.DialogFragmentHelpers;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport static org.mockito.Mockito.when;\n\n@RunWith(AndroidJUnit4.class)\npublic class CopticDatePickerDialogTest {\n    private FragmentManager fragmentManager;\n    private CopticDatePickerDialog dialogFragment;\n    private DatePickerDetails datePickerDetails;\n    private DialogFragmentHelpers.DatePickerTestActivity activity;\n\n    @Before\n    public void setup() {\n        activity = CollectHelpers.createThemedActivity(DialogFragmentHelpers.DatePickerTestActivity.class);\n        fragmentManager = activity.getSupportFragmentManager();\n\n        dialogFragment = new CopticDatePickerDialog();\n        datePickerDetails = DialogFragmentHelpers.setUpDatePickerDetails(DatePickerDetails.DatePickerType.COPTIC);\n        dialogFragment.setArguments(DialogFragmentHelpers.getDialogFragmentArguments(datePickerDetails));\n    }\n\n    @Test\n    public void dialogIsCancellable() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsCancellable(true);\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDate(1736, 8, 4, \"4 Pashons 1736 (May 12, 2020)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forYearMode() {\n        when(datePickerDetails.isYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForYearMode(1736, \"1736 (2019)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forMonthMode() {\n        when(datePickerDetails.isMonthYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForMonthMode(1736, 8, \"Pashons 1736 (May 2020)\");\n    }\n\n    @Test\n    public void settingDateInDatePicker_changesDateShownInTextView() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogTextViewUpdatesDate(\"4 Pashons 1736 (May 12, 2020)\", 1736, 8, 4);\n    }\n\n    @Test\n    public void whenScreenIsRotated_dialogShouldRetainDateInDatePickerAndTextView() {\n        DialogFragmentHelpers.assertDialogRetainsDateOnScreenRotation(dialogFragment, \"4 Pashons 1736 (May 12, 2020)\", 1736, 8, 4);\n    }\n\n    @Test\n    public void clickingOk_updatesDateInActivity() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDateUpdateInActivity(activity, 1736, 8, 4);\n    }\n\n    @Test\n    public void clickingOk_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_POSITIVE);\n    }\n\n    @Test\n    public void clickingCancel_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_NEGATIVE);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/pickers/EthiopianDatePickerDialogTest.java",
    "content": "package org.odk.collect.android.widgets.datetime.pickers;\n\nimport android.content.DialogInterface;\n\nimport androidx.fragment.app.FragmentManager;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.fragments.support.DialogFragmentHelpers;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport static org.mockito.Mockito.when;\n\n@RunWith(AndroidJUnit4.class)\npublic class EthiopianDatePickerDialogTest {\n    private FragmentManager fragmentManager;\n    private EthiopianDatePickerDialog dialogFragment;\n    private DatePickerDetails datePickerDetails;\n    private DialogFragmentHelpers.DatePickerTestActivity activity;\n\n    @Before\n    public void setup() {\n        activity = CollectHelpers.createThemedActivity(DialogFragmentHelpers.DatePickerTestActivity.class);\n        fragmentManager = activity.getSupportFragmentManager();\n\n        dialogFragment = new EthiopianDatePickerDialog();\n        datePickerDetails = DialogFragmentHelpers.setUpDatePickerDetails(DatePickerDetails.DatePickerType.ETHIOPIAN);\n        dialogFragment.setArguments(DialogFragmentHelpers.getDialogFragmentArguments(datePickerDetails));\n    }\n\n    @Test\n    public void dialogIsCancellable() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsCancellable(true);\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDate(2012, 8, 4, \"4 Ginbot 2012 (May 12, 2020)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forYearMode() {\n        when(datePickerDetails.isYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForYearMode(2012, \"2012 (2019)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forMonthMode() {\n        when(datePickerDetails.isMonthYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForMonthMode(2012, 8, \"Ginbot 2012 (May 2020)\");\n    }\n\n    @Test\n    public void settingDateInDatePicker_changesDateShownInTextView() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogTextViewUpdatesDate(\"4 Ginbot 2012 (May 12, 2020)\", 2012, 8, 4);\n    }\n\n    @Test\n    public void whenScreenIsRotated_dialogShouldRetainDateInDatePickerAndTextView() {\n        DialogFragmentHelpers.assertDialogRetainsDateOnScreenRotation(dialogFragment, \"4 Ginbot 2012 (May 12, 2020)\", 2012, 8, 4);\n    }\n\n    @Test\n    public void clickingOk_updatesDateInActivity() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDateUpdateInActivity(activity, 2012, 8, 4);\n    }\n\n    @Test\n    public void clickingOk_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_POSITIVE);\n    }\n\n    @Test\n    public void clickingCancel_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_NEGATIVE);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/pickers/IslamicDatePickerDialogTest.java",
    "content": "package org.odk.collect.android.widgets.datetime.pickers;\n\nimport android.content.DialogInterface;\n\nimport androidx.fragment.app.FragmentManager;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.fragments.support.DialogFragmentHelpers;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport static org.mockito.Mockito.when;\n\n@RunWith(AndroidJUnit4.class)\npublic class IslamicDatePickerDialogTest {\n    private FragmentManager fragmentManager;\n    private IslamicDatePickerDialog dialogFragment;\n    private DatePickerDetails datePickerDetails;\n    private DialogFragmentHelpers.DatePickerTestActivity activity;\n\n    @Before\n    public void setup() {\n        activity = CollectHelpers.createThemedActivity(DialogFragmentHelpers.DatePickerTestActivity.class);\n        fragmentManager = activity.getSupportFragmentManager();\n\n        dialogFragment = new IslamicDatePickerDialog();\n        datePickerDetails = DialogFragmentHelpers.setUpDatePickerDetails(DatePickerDetails.DatePickerType.ISLAMIC);\n        dialogFragment.setArguments(DialogFragmentHelpers.getDialogFragmentArguments(datePickerDetails));\n    }\n\n    @Test\n    public void dialogIsCancellable() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsCancellable(true);\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDate(1441, 8, 19, \"19 Ramadan 1441 (May 12, 2020)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forYearMode() {\n        when(datePickerDetails.isYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForYearMode(1441, \"1441 (2019)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forMonthMode() {\n        when(datePickerDetails.isMonthYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForMonthMode(1441, 8, \"Ramadan 1441 (Apr 2020)\");\n    }\n\n    @Test\n    public void settingDateInDatePicker_changesDateShownInTextView() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogTextViewUpdatesDate(\"19 Ramadan 1441 (May 12, 2020)\", 1441, 8, 19);\n    }\n\n    @Test\n    public void whenScreenIsRotated_dialogShouldRetainDateInDatePickerAndTextView() {\n        DialogFragmentHelpers.assertDialogRetainsDateOnScreenRotation(dialogFragment, \"19 Ramadan 1441 (May 12, 2020)\", 1441, 8, 19);\n    }\n\n    @Test\n    public void clickingOk_updatesDateInActivity() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDateUpdateInActivity(activity, 1441, 8, 19);\n    }\n\n    @Test\n    public void clickingOk_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_POSITIVE);\n    }\n\n    @Test\n    public void clickingCancel_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_NEGATIVE);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/pickers/MyanmarDatePickerDialogTest.java",
    "content": "package org.odk.collect.android.widgets.datetime.pickers;\n\nimport android.content.DialogInterface;\n\nimport androidx.fragment.app.FragmentManager;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.fragments.support.DialogFragmentHelpers;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport static org.mockito.Mockito.when;\n\n@RunWith(AndroidJUnit4.class)\npublic class MyanmarDatePickerDialogTest {\n    private FragmentManager fragmentManager;\n    private MyanmarDatePickerDialog dialogFragment;\n    private DatePickerDetails datePickerDetails;\n    private DialogFragmentHelpers.DatePickerTestActivity activity;\n\n    @Before\n    public void setup() {\n        activity = CollectHelpers.createThemedActivity(DialogFragmentHelpers.DatePickerTestActivity.class);\n        fragmentManager = activity.getSupportFragmentManager();\n\n        dialogFragment = new MyanmarDatePickerDialog();\n        datePickerDetails = DialogFragmentHelpers.setUpDatePickerDetails(DatePickerDetails.DatePickerType.MYANMAR);\n        dialogFragment.setArguments(DialogFragmentHelpers.getDialogFragmentArguments(datePickerDetails));\n    }\n\n    @Test\n    public void dialogIsCancellable() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsCancellable(true);\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDate(1382, 1, 21, \"21 ကဆုန် 1382 (May 12, 2020)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forYearMode() {\n        when(datePickerDetails.isYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForYearMode(1382, \"1381 (2020)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forMonthMode() {\n        when(datePickerDetails.isMonthYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForMonthMode(1382, 1, \"ကဆုန် 1382 (Apr 2020)\");\n    }\n\n    @Test\n    public void settingDateInDatePicker_changesDateShownInTextView() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogTextViewUpdatesDate(\"21 ကဆုန် 1382 (May 12, 2020)\", 1382, 1, 21);\n    }\n\n    @Test\n    public void whenScreenIsRotated_dialogShouldRetainDateInDatePickerAndTextView() {\n        DialogFragmentHelpers.assertDialogRetainsDateOnScreenRotation(dialogFragment, \"21 ကဆုန် 1382 (May 12, 2020)\", 1382, 1, 21);\n    }\n\n    @Test\n    public void clickingOk_updatesDateInActivity() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDateUpdateInActivity(activity, 1382, 1, 21);\n    }\n\n    @Test\n    public void clickingOk_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_POSITIVE);\n    }\n\n    @Test\n    public void clickingCancel_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_NEGATIVE);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/datetime/pickers/PersianDatePickerDialogTest.java",
    "content": "package org.odk.collect.android.widgets.datetime.pickers;\n\nimport android.content.DialogInterface;\n\nimport androidx.fragment.app.FragmentManager;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.fragments.support.DialogFragmentHelpers;\nimport org.odk.collect.android.widgets.datetime.DatePickerDetails;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.testshared.RobolectricHelpers;\n\nimport static org.mockito.Mockito.when;\n\n@RunWith(AndroidJUnit4.class)\npublic class PersianDatePickerDialogTest {\n    private FragmentManager fragmentManager;\n    private PersianDatePickerDialog dialogFragment;\n    private DatePickerDetails datePickerDetails;\n    private DialogFragmentHelpers.DatePickerTestActivity activity;\n\n    @Before\n    public void setup() {\n        activity = CollectHelpers.createThemedActivity(DialogFragmentHelpers.DatePickerTestActivity.class);\n        fragmentManager = activity.getSupportFragmentManager();\n\n        dialogFragment = new PersianDatePickerDialog();\n        datePickerDetails = DialogFragmentHelpers.setUpDatePickerDetails(DatePickerDetails.DatePickerType.PERSIAN);\n        dialogFragment.setArguments(DialogFragmentHelpers.getDialogFragmentArguments(datePickerDetails));\n    }\n\n    @Test\n    public void dialogIsCancellable() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsCancellable(true);\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDate(1399, 1, 23, \"23 Ordibehesht 1399 (May 12, 2020)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forYearMode() {\n        when(datePickerDetails.isYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForYearMode(1399, \"1399 (2020)\");\n    }\n\n    @Test\n    public void dialogShouldShowCorrectDate_forMonthMode() {\n        when(datePickerDetails.isMonthYearMode()).thenReturn(true);\n        when(datePickerDetails.isSpinnerMode()).thenReturn(false);\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogShowsCorrectDateForMonthMode(1399, 1, \"Ordibehesht 1399 (Apr 2020)\");\n    }\n\n    @Test\n    public void settingDateInDatePicker_changesDateShownInTextView() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogTextViewUpdatesDate(\"23 Ordibehesht 1399 (May 12, 2020)\", 1399, 1, 23);\n    }\n\n    @Test\n    public void whenScreenIsRotated_dialogShouldRetainDateInDatePickerAndTextView() {\n        DialogFragmentHelpers.assertDialogRetainsDateOnScreenRotation(dialogFragment, \"23 Ordibehesht 1399 (May 12, 2020)\", 1399, 1, 23);\n    }\n\n    @Test\n    public void clickingOk_updatesDateInActivity() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDateUpdateInActivity(activity, 1399, 1, 23);\n    }\n\n    @Test\n    public void clickingOk_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_POSITIVE);\n    }\n\n    @Test\n    public void clickingCancel_dismissesTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        DialogFragmentHelpers.assertDialogIsDismissedOnButtonClick(DialogInterface.BUTTON_NEGATIVE);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/LikertWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport android.view.View;\nimport android.widget.LinearLayout;\nimport android.widget.RadioButton;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.widgets.base.QuestionWidgetTest;\nimport org.odk.collect.android.widgets.support.FormEntryPromptSelectChoiceLoader;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\nimport static java.util.Arrays.asList;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.when;\n\npublic class LikertWidgetTest extends QuestionWidgetTest<LikertWidget, SelectOneData> {\n    private List<SelectChoice> options = new ArrayList<>();\n\n    @Before\n    public void setup() {\n        options = asList(\n                new SelectChoice(\"1\", \"1\"),\n                new SelectChoice(\"2\", \"2\"),\n                new SelectChoice(\"3\", \"3\"),\n                new SelectChoice(\"4\", \"4\"),\n                new SelectChoice(\"5\", \"5\"));\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withSelectChoices(options)\n                .build();\n    }\n\n    @NonNull\n    @Override\n    public LikertWidget createWidget() {\n        return new LikertWidget(activity, new QuestionDetails(formEntryPrompt), new FormEntryPromptSelectChoiceLoader(), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public SelectOneData getNextAnswer() {\n        return new SelectOneData(new Selection(options.get(0)));\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n\n        for (int i = 0; i < getSpyWidget().view.getChildCount(); i++) {\n            LinearLayout optionView = (LinearLayout) getSpyWidget().view.getChildAt(0);\n            assertThat(optionView.getVisibility(), is(View.VISIBLE));\n            assertThat(optionView.isEnabled(), is(Boolean.FALSE));\n        }\n\n        for (Map.Entry<RadioButton, String> radioButtonStringEntry : getSpyWidget().buttonsToName.entrySet()) {\n            assertThat(((RadioButton) ((Map.Entry) radioButtonStringEntry).getKey()).getVisibility(), is(View.VISIBLE));\n            assertThat(((RadioButton) ((Map.Entry) radioButtonStringEntry).getKey()).isEnabled(), is(Boolean.FALSE));\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/ListMultiWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport android.view.View;\nimport android.widget.CheckBox;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.junit.Test;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.widgets.base.GeneralSelectMultiWidgetTest;\nimport org.odk.collect.android.widgets.support.FormEntryPromptSelectChoiceLoader;\n\nimport static java.util.Arrays.asList;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.when;\n\n/**\n * @author James Knight\n */\n\npublic class ListMultiWidgetTest extends GeneralSelectMultiWidgetTest<ListMultiWidget> {\n    @NonNull\n    @Override\n    public ListMultiWidget createWidget() {\n        return new ListMultiWidget(activity, new QuestionDetails(formEntryPrompt), true, new FormEntryPromptSelectChoiceLoader(), dependencies);\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n\n        for (CheckBox checkBox : getSpyWidget().checkBoxes) {\n            assertThat(checkBox.getVisibility(), is(View.VISIBLE));\n            assertThat(checkBox.isEnabled(), is(Boolean.FALSE));\n        }\n    }\n\n    @Test\n    public void whenSpacesInUnderlyingValuesExist_shouldAppropriateWarningBeDisplayed() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(asList(\n                        new SelectChoice(\"a\", \"a a\"),\n                        new SelectChoice(\"a\", \"b b\")\n                ))\n                .build();\n\n        TextView warningTv = getWidget().findViewById(R.id.warning_text);\n        assertThat(warningTv.getVisibility(), is(View.VISIBLE));\n        assertThat(warningTv.getText(), is(\"Warning: underlying values a a, b b have spaces\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/ListWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport android.view.View;\nimport android.widget.RadioButton;\n\nimport androidx.annotation.NonNull;\n\nimport org.junit.Test;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.base.GeneralSelectOneWidgetTest;\nimport org.odk.collect.android.widgets.support.FormEntryPromptSelectChoiceLoader;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.when;\n\n/**\n * @author James Knight\n */\n\npublic class ListWidgetTest extends GeneralSelectOneWidgetTest<ListWidget> {\n    @NonNull\n    @Override\n    public ListWidget createWidget() {\n        return new ListWidget(activity, new QuestionDetails(formEntryPrompt), false, false, new FormEntryPromptSelectChoiceLoader(), dependencies);\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n\n        for (RadioButton radioButton : getSpyWidget().buttons) {\n            assertThat(radioButton.getVisibility(), is(View.VISIBLE));\n            assertThat(radioButton.isEnabled(), is(Boolean.FALSE));\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/RankingWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.MultipleItemsData;\nimport org.javarosa.core.model.data.SelectMultiData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.junit.Test;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.widgets.base.SelectWidgetTest;\nimport org.odk.collect.android.widgets.support.FormEntryPromptSelectChoiceLoader;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static java.util.Arrays.asList;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\n\npublic class RankingWidgetTest extends SelectWidgetTest<RankingWidget, MultipleItemsData> {\n\n    @NonNull\n    @Override\n    public RankingWidget createWidget() {\n        return new RankingWidget(activity, new QuestionDetails(formEntryPrompt), null, new FormEntryPromptSelectChoiceLoader(), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public MultipleItemsData getNextAnswer() {\n        List<SelectChoice> selectChoices = getSelectChoices();\n\n        List<Selection> selections = new ArrayList<>();\n        for (SelectChoice selectChoice : selectChoices) {\n            selections.add(new Selection(selectChoice));\n        }\n\n        return new SelectMultiData(selections);\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n\n        assertThat(getSpyWidget().binding.rankItemsButton.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenSpacesInUnderlyingValuesExist_shouldAppropriateWarningBeDisplayed() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(asList(\n                        new SelectChoice(\"a\", \"a a\"),\n                        new SelectChoice(\"a\", \"b b\")\n                ))\n                .build();\n\n        TextView warningTv = getWidget().findViewById(R.id.warning_text);\n        assertThat(warningTv.getVisibility(), is(View.VISIBLE));\n        assertThat(warningTv.getText(), is(\"Warning: underlying values a a, b b have spaces\"));\n    }\n\n    @Test\n    public void setData_callsValueChangeListener() {\n        RankingWidget widget = createWidget();\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.setValueChangedListener(valueChangedListener);\n        widget.setData(\n                Arrays.asList(\n                        new SelectChoice(\"a\", \"a a\"),\n                        new SelectChoice(\"a\", \"b b\")\n                )\n        );\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectChoicesMapDataTest.kt",
    "content": "package org.odk.collect.android.widgets.items\n\nimport android.app.Application\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.android.widgets.support.FormElementFixtures.selectChoice\nimport org.odk.collect.android.widgets.support.FormElementFixtures.treeElement\nimport org.odk.collect.androidtest.getOrAwaitValue\nimport org.odk.collect.entities.javarosa.parse.EntitySchema\nimport org.odk.collect.geo.selection.IconifiedText\nimport org.odk.collect.geo.selection.MappableSelectItem\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.testshared.FakeScheduler\n\n@RunWith(AndroidJUnit4::class)\nclass SelectChoicesMapDataTest {\n\n    private val scheduler = FakeScheduler()\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    @Test\n    fun `choices with geo trace format geometry have multiple points`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 3 4; 12.1 -1.0 3 4\"))\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .withSelectChoiceText(mapOf(choices[0] to \"A\"))\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        assertThat(data.getItemCount().getOrAwaitValue(), equalTo(1))\n\n        val mappableItems = data.getMappableItems().getOrAwaitValue()!!\n        assertThat(mappableItems.size, equalTo(1))\n\n        val points = (mappableItems[0] as MappableSelectItem.MappableSelectLine).points\n        assertThat(\n            points,\n            equalTo(listOf(MapPoint(12.0, -1.0, 3.0, 4.0), MapPoint(12.1, -1.0, 3.0, 4.0)))\n        )\n    }\n\n    @Test\n    fun `choices with geo shape format geometry have multiple points`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 3 4; 12.1 -1.0 3 4; 12.0 -1.0 3 4\"))\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .withSelectChoiceText(mapOf(choices[0] to \"A\"))\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        assertThat(data.getItemCount().getOrAwaitValue(), equalTo(1))\n\n        val mappableItems = data.getMappableItems().getOrAwaitValue()!!\n        assertThat(mappableItems.size, equalTo(1))\n\n        val points = (mappableItems[0] as MappableSelectItem.MappableSelectPolygon).points\n        assertThat(\n            points,\n            equalTo(listOf(MapPoint(12.0, -1.0, 3.0, 4.0), MapPoint(12.1, -1.0, 3.0, 4.0), MapPoint(12.0, -1.0, 3.0, 4.0)))\n        )\n    }\n\n    @Test\n    fun `choices without geometry are not included in mappable items`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(children = listOf(treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 305 0\")))\n            ),\n            selectChoice(\n                value = \"b\",\n                item = treeElement(children = emptyList())\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .withSelectChoiceText(mapOf(choices[0] to \"A\", choices[1] to \"B\"))\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        assertThat(data.getItemCount().value, equalTo(2))\n        assertThat(data.getMappableItems().value!!.size, equalTo(1))\n        assertThat(data.getMappableItems().value!![0].name, equalTo(\"A\"))\n    }\n\n    @Test\n    fun `additional children are returned as properties`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 305 0\"),\n                        treeElement(\"property\", \"blah\")\n                    )\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .withSelectChoiceText(mapOf(choices[0] to \"A\"))\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        val properties = data.getMappableItems().value!![0].properties\n        assertThat(properties.size, equalTo(1))\n        assertThat(properties[0], equalTo(IconifiedText(null, \"property: blah\")))\n    }\n\n    @Test\n    fun `isLoading is true and items is null when items are being loaded from choices`() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withSelectChoices(emptyList())\n            .withSelectChoiceText(emptyMap())\n            .build()\n\n        val resources = ApplicationProvider.getApplicationContext<Application>().resources\n        val data = SelectChoicesMapData(resources, scheduler, prompt, null)\n        assertThat(data.isLoading().value, equalTo(true))\n        assertThat(data.getMappableItems().value, equalTo(null))\n\n        scheduler.runBackground()\n        assertThat(data.isLoading().value, equalTo(true))\n\n        scheduler.runForeground()\n        assertThat(data.isLoading().value, equalTo(false))\n    }\n\n    @Test\n    fun `choices with incorrect geometry are ignored`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"0 170.00 0 0\")\n                    )\n                )\n            ),\n            // Invalid\n            selectChoice(\n                value = \"b\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"blah\")\n                    )\n                )\n            ),\n            // Out of bounds\n            selectChoice(\n                value = \"c\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"0 180.1 0 0\")\n                    )\n                )\n            ),\n            // Second point out of bounds\n            selectChoice(\n                value = \"c\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"0 180 0 0; 0 180.1 0 0\")\n                    )\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        assertThat(data.getMappableItems().value!!.size, equalTo(1))\n        assertThat(data.getMappableItems().value!![0].name, equalTo(\"a\"))\n    }\n\n    /**\n     * Attributes names come from properties defined at\n     * https://github.com/mapbox/simplestyle-spec/tree/master/1.1.0.\n     */\n    @Test\n    fun `marker symbol and color are pulled from simple style attributes`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 305 0\"),\n                        treeElement(SelectChoicesMapData.MARKER_SYMBOL, \"A\"),\n                        treeElement(SelectChoicesMapData.MARKER_COLOR, \"#ffffff\")\n                    )\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .withSelectChoiceText(mapOf(choices[0] to \"A\"))\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        val item = data.getMappableItems().getOrAwaitValue()!![0] as MappableSelectItem.MappableSelectPoint\n        assertThat(item.symbol, equalTo(\"A\"))\n        assertThat(item.color, equalTo(\"#ffffff\"))\n    }\n\n    /**\n     * Attributes names come from properties defined at\n     * https://github.com/mapbox/simplestyle-spec/tree/master/1.1.0.\n     */\n    @Test\n    fun `line stroke color is pulled from simple style attributes`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 3 4; 12.1 -1.0 3 4\"),\n                        treeElement(SelectChoicesMapData.STROKE_WIDTH, \"10\"),\n                        treeElement(SelectChoicesMapData.STROKE, \"#ffffff\")\n                    )\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .withSelectChoiceText(mapOf(choices[0] to \"A\"))\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        val item = data.getMappableItems().getOrAwaitValue()!![0] as MappableSelectItem.MappableSelectLine\n        assertThat(item.strokeWidth, equalTo(\"10\"))\n        assertThat(item.strokeColor, equalTo(\"#ffffff\"))\n    }\n\n    /**\n     * Attributes names come from properties defined at\n     * https://github.com/mapbox/simplestyle-spec/tree/master/1.1.0.\n     */\n    @Test\n    fun `polygon stroke width, stroke color and fill color are pulled from simple style attributes`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 3 4; 12.1 -1.0 3 4; 12.0 -1.0 3 4\"),\n                        treeElement(SelectChoicesMapData.STROKE_WIDTH, \"10\"),\n                        treeElement(SelectChoicesMapData.STROKE, \"#000000\"),\n                        treeElement(SelectChoicesMapData.FILL, \"#ffffff\")\n                    )\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .withSelectChoiceText(mapOf(choices[0] to \"A\"))\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        val item = data.getMappableItems().getOrAwaitValue()!![0] as MappableSelectItem.MappableSelectPolygon\n        assertThat(item.strokeWidth, equalTo(\"10\"))\n        assertThat(item.strokeColor, equalTo(\"#000000\"))\n        assertThat(item.fillColor, equalTo(\"#ffffff\"))\n    }\n\n    @Test\n    fun `uses marker without cutout if marker-symbol is defined`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 305 0\"),\n                        treeElement(SelectChoicesMapData.MARKER_SYMBOL, \"A\")\n                    )\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .withSelectChoiceText(mapOf(choices[0] to \"A\"))\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        val item = data.getMappableItems().getOrAwaitValue()!![0] as MappableSelectItem.MappableSelectPoint\n        assertThat(item.smallIcon, equalTo(org.odk.collect.icons.R.drawable.ic_map_marker_small))\n        assertThat(item.largeIcon, equalTo(org.odk.collect.icons.R.drawable.ic_map_marker_big))\n    }\n\n    @Test\n    fun `uses marker with cutout if marker-symbol is not defined or blank`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 305 0\")\n                    )\n                )\n            ),\n            selectChoice(\n                value = \"b\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"0 170.00 0 0\"),\n                        treeElement(SelectChoicesMapData.MARKER_SYMBOL, \" \")\n                    )\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n\n        val firstItem = data.getMappableItems().getOrAwaitValue()!![0] as MappableSelectItem.MappableSelectPoint\n        assertThat(firstItem.smallIcon, equalTo(org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small))\n        assertThat(firstItem.largeIcon, equalTo(org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big))\n\n        val secondItem = data.getMappableItems().getOrAwaitValue()!![1] as MappableSelectItem.MappableSelectPoint\n        assertThat(secondItem.smallIcon, equalTo(org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small))\n        assertThat(secondItem.largeIcon, equalTo(org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big))\n    }\n\n    @Test\n    fun `entity system properties are filtered out`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 305 0\"),\n                        treeElement(EntitySchema.VERSION, \"version\"),\n                        treeElement(EntitySchema.TRUNK_VERSION, \"trunk-version\"),\n                        treeElement(EntitySchema.BRANCH_ID, \"branch-id\")\n                    )\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        val properties = data.getMappableItems().getOrAwaitValue()!![0].properties\n        assertThat(properties.size, equalTo(0))\n    }\n\n    @Test\n    fun `marker styling properties are filtered out`() {\n        val choices = listOf(\n            selectChoice(\n                value = \"a\",\n                item = treeElement(\n                    children = listOf(\n                        treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 305 0\"),\n                        treeElement(SelectChoicesMapData.MARKER_COLOR, \"#ffffff\"),\n                        treeElement(SelectChoicesMapData.MARKER_SYMBOL, \"A\"),\n                        treeElement(SelectChoicesMapData.STROKE, \"#ffffff\"),\n                        treeElement(SelectChoicesMapData.STROKE_WIDTH, \"5\"),\n                        treeElement(SelectChoicesMapData.FILL, \"#ffffff\"),\n                    )\n                )\n            )\n        )\n\n        val prompt = MockFormEntryPromptBuilder()\n            .withLongText(\"Which is your favourite place?\")\n            .withSelectChoices(choices)\n            .build()\n\n        val data = loadDataForPrompt(prompt)\n        val properties = data.getMappableItems().getOrAwaitValue()!![0].properties\n        assertThat(properties.size, equalTo(0))\n    }\n\n    private fun loadDataForPrompt(prompt: FormEntryPrompt): SelectChoicesMapData {\n        val resources = ApplicationProvider.getApplicationContext<Application>().resources\n        val data = SelectChoicesMapData(resources, scheduler, prompt, null)\n        scheduler.flush()\n        return data\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectImageMapWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport android.view.MotionEvent;\nimport android.view.View;\n\nimport androidx.core.util.Pair;\n\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.junit.Test;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.widgets.base.SelectWidgetTest;\n\nimport static java.util.Arrays.asList;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.support.CollectHelpers.setupFakeReferenceManager;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\n\npublic abstract class SelectImageMapWidgetTest<W extends SelectImageMapWidget, A extends IAnswerData>\n        extends SelectWidgetTest<W, A> {\n\n    @Override\n    public void setUp() throws Exception {\n        super.setUp();\n        overrideDependencyModule();\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withImageURI(\"jr://images/body.svg\")\n                .build();\n    }\n\n    private void overrideDependencyModule() throws Exception {\n        ReferenceManager referenceManager = setupFakeReferenceManager(asList(\n                new Pair<>(\"jr://images/body.svg\", \"body.svg\")\n        ));\n\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n        });\n    }\n\n    @Override\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        formEntryPrompt = new MockFormEntryPromptBuilder(formEntryPrompt)\n                .withReadOnly(true)\n                .build();\n        MotionEvent motionEvent = mock(MotionEvent.class);\n        when(motionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);\n\n        assertThat(getSpyWidget().binding.imageMap.getVisibility(), is(View.VISIBLE));\n        assertThat(getSpyWidget().binding.imageMap.isClickable(), is(Boolean.FALSE));\n    }\n\n    @Test\n    public void selectArea_callsValueChangeListener() {\n        SelectImageMapWidget widget = getWidget();\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.setValueChangedListener(valueChangedListener);\n        widget.selectArea(\"1\");\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectMultiImageMapWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport com.google.common.collect.ImmutableList;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.SelectMultiData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.junit.Test;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.widgets.support.FormEntryPromptSelectChoiceLoader;\n\nimport java.util.List;\n\nimport static java.util.Arrays.asList;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\n\npublic class SelectMultiImageMapWidgetTest extends SelectImageMapWidgetTest<SelectMultiImageMapWidget, SelectMultiData> {\n    @NonNull\n    @Override\n    public SelectMultiImageMapWidget createWidget() {\n        return new SelectMultiImageMapWidget(activity, new QuestionDetails(formEntryPrompt), new FormEntryPromptSelectChoiceLoader(), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public SelectMultiData getNextAnswer() {\n        List<SelectChoice> selectChoices = getSelectChoices();\n\n        int selectedIndex = Math.abs(random.nextInt()) % selectChoices.size();\n        SelectChoice selectChoice = selectChoices.get(selectedIndex);\n\n        Selection selection = new Selection(selectChoice);\n        return new SelectMultiData(ImmutableList.of(selection));\n    }\n\n    @Test\n    public void whenSpacesInUnderlyingValuesExist_shouldAppropriateWarningBeDisplayed() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(asList(\n                        new SelectChoice(\"a\", \"a a\"),\n                        new SelectChoice(\"a\", \"b b\")\n                ))\n                .withImageURI(\"jr://images/body.svg\")\n                .build();\n\n        TextView warningTv = getWidget().findViewById(R.id.warning_text);\n        assertThat(warningTv.getVisibility(), is(View.VISIBLE));\n        assertThat(warningTv.getText(), is(\"Warning: underlying values a a, b b have spaces\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectMultiMinimalWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.SelectMultiData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.junit.Test;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.widgets.base.GeneralSelectMultiWidgetTest;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.support.FormEntryPromptSelectChoiceLoader;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport static java.util.Arrays.asList;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\n\npublic class SelectMultiMinimalWidgetTest extends GeneralSelectMultiWidgetTest<SelectMultiMinimalWidget> {\n\n    @NonNull\n    @Override\n    public SelectMultiMinimalWidget createWidget() {\n        return new SelectMultiMinimalWidget(activity, new QuestionDetails(formEntryPrompt), new FakeWaitingForDataRegistry(), new FormEntryPromptSelectChoiceLoader(), dependencies);\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n        assertThat(getSpyWidget().binding.answer.getVisibility(), is(View.VISIBLE));\n        assertThat(getSpyWidget().binding.answer.isEnabled(), is(Boolean.FALSE));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_shouldDefaultTextBeDisplayed() {\n        assertThat(getSpyWidget().binding.answer.getText().toString(), is(\"Select Answer\"));\n    }\n\n    @Test\n    public void whenThereIsAnswer_shouldSelectedChoicesBeDisplayed() {\n        SelectMultiData answer = getInitialAnswer();\n        Selection selectedChoice = ((List<Selection>) answer.getValue()).get(0);\n        when(formEntryPrompt.getAnswerValue()).thenReturn(answer);\n        when(formEntryPrompt.getSelectItemText(selectedChoice)).thenReturn(selectedChoice.getValue());\n\n        assertThat(getSpyWidget().binding.answer.getText().toString(), is(selectedChoice.getValue()));\n    }\n\n    @Test\n    public void whenAnswerChanges_shouldAnswerLabelBeUpdated() {\n        assertThat(getSpyWidget().binding.answer.getText().toString(), is(\"Select Answer\"));\n\n        SelectMultiData answer = getInitialAnswer();\n        Selection selectedChoice = ((List<Selection>) answer.getValue()).get(0);\n        when(formEntryPrompt.getSelectItemText(selectedChoice)).thenReturn(selectedChoice.getValue());\n        getSpyWidget().setData(Collections.singletonList(selectedChoice));\n\n        assertThat(getSpyWidget().binding.answer.getText().toString(), is(selectedChoice.getValue()));\n        getSpyWidget().clearAnswer();\n        assertThat(getSpyWidget().binding.answer.getText().toString(), is(\"Select Answer\"));\n    }\n\n    @Test\n    public void whenAnswerChanges_shouldValueChangeListenersBeCalled() {\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(getSpyWidget());\n\n        SelectMultiData answer = getInitialAnswer();\n        Selection selectedChoice = ((List<Selection>) answer.getValue()).get(0);\n        getSpyWidget().setData(Collections.singletonList(selectedChoice));\n\n        verify(valueChangedListener).widgetValueChanged(getSpyWidget());\n    }\n\n    @Test\n    public void whenSpacesInUnderlyingValuesExist_shouldAppropriateWarningBeDisplayed() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(asList(\n                        new SelectChoice(\"a\", \"a a\"),\n                        new SelectChoice(\"a\", \"b b\")\n                ))\n                .build();\n\n        TextView warningTv = getWidget().findViewById(R.id.warning_text);\n        assertThat(warningTv.getVisibility(), is(View.VISIBLE));\n        assertThat(warningTv.getText(), is(\"Warning: underlying values a a, b b have spaces\"));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectMultiWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.support.CollectHelpers.setupFakeReferenceManager;\nimport static org.odk.collect.testshared.RobolectricHelpers.populateRecyclerView;\nimport static java.util.Arrays.asList;\n\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CheckBox;\nimport android.widget.FrameLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.util.Pair;\nimport androidx.recyclerview.widget.GridLayoutManager;\n\nimport com.google.android.flexbox.FlexboxLayoutManager;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel;\nimport org.odk.collect.android.formentry.questions.NoButtonsItem;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.SoftKeyboardController;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.base.GeneralSelectMultiWidgetTest;\nimport org.odk.collect.android.widgets.support.FormEntryPromptSelectChoiceLoader;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.audioclips.Clip;\n\nimport java.util.List;\n\n/**\n * @author James Knight\n */\npublic class SelectMultiWidgetTest extends GeneralSelectMultiWidgetTest<SelectMultiWidget> {\n\n    @NonNull\n    @Override\n    public SelectMultiWidget createWidget() {\n        SelectMultiWidget selectMultiWidget = new SelectMultiWidget(activity, new QuestionDetails(formEntryPrompt), new FormEntryPromptSelectChoiceLoader(), dependencies);\n        selectMultiWidget.setFocus(activity);\n        return selectMultiWidget;\n    }\n\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    @Before\n    public void setup() throws Exception {\n        overrideDependencyModule();\n    }\n\n    @Test\n    public void byDefault_shouldGridLayoutManagerBeUsed() {\n        assertThat(getWidget().binding.choicesRecyclerView.getLayoutManager().getClass().getName(), is(GridLayoutManager.class.getName()));\n    }\n\n    @Test\n    public void whenColumnsPackAppearanceExist_shouldFlexboxLayoutManagerBeUsed() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"columns-pack\");\n        assertThat(getWidget().binding.choicesRecyclerView.getLayoutManager().getClass().getName(), is(FlexboxLayoutManager.class.getName()));\n    }\n\n    @Test\n    public void whenButtonsModeExist_shouldFrameLayoutBeUsedAsItemView() {\n        populateRecyclerView(getWidget());\n        assertThat(getChoiceView(getWidget(), 0).getClass().getName(), is(AudioVideoImageTextLabel.class.getName()));\n    }\n\n    @Test\n    public void whenNoButtonsModeExist_shouldFrameLayoutBeUsedAsItemView() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"no-buttons\");\n        populateRecyclerView(getWidget());\n        assertThat(getChoiceView(getWidget(), 0).getClass().getName(), is(NoButtonsItem.class.getName()));\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceExist_shouldTextSizeBeSetProperly() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"autocomplete\");\n        assertThat((int) getSpyWidget().binding.choicesSearchBox.getTextSize(), is(QuestionFontSizeUtils.getFontSize(settingsProvider.getUnprotectedSettings(), QuestionFontSizeUtils.FontSize.HEADLINE_6)));\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceExist_shouldSearchBoxBeVisible() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"autocomplete\");\n        assertThat(getSpyWidget().binding.choicesSearchBox.getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceDoesNotExist_shouldSearchBoxBeHidden() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"\");\n        assertThat(getSpyWidget().binding.choicesSearchBox.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceDoesNotExist_shouldNotKeyboardBeDisplayed() {\n        SelectMultiWidget widget = getSpyWidget();\n        verify(widget.softKeyboardController, never()).showSoftKeyboard(widget.binding.choicesSearchBox);\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceExist_shouldKeyboardBeDisplayed() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"autocomplete\");\n        SelectMultiWidget widget = getSpyWidget();\n        verify(widget.softKeyboardController).showSoftKeyboard(widget.binding.choicesSearchBox);\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceExistAndWidgetIsReadOnly_shouldNotKeyboardBeDisplayed() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"autocomplete\");\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n        SelectMultiWidget widget = getSpyWidget();\n        verify(widget.softKeyboardController, never()).showSoftKeyboard(widget.binding.choicesSearchBox);\n    }\n\n    @Test\n    public void whenChoicesHaveAudio_audioButtonUsesIndexAsClipID() throws Exception {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .withSpecialFormSelectChoiceText(asList(\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(0).first),\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(1).first)\n                ))\n                .build();\n\n        AudioPlayer audioPlayer = mock();\n        dependencies = new QuestionWidget.Dependencies(audioPlayer, null);\n\n        populateRecyclerView(getWidget());\n\n        getChoiceView(getWidget(), 0).findViewById(R.id.audioButton).performClick();\n        verify(audioPlayer).play(eq(new Clip(\"i am index 0\", REFERENCES.get(0).second)));\n        getChoiceView(getWidget(), 1).findViewById(R.id.audioButton).performClick();\n        verify(audioPlayer).play(eq(new Clip(\"i am index 1\", REFERENCES.get(1).second)));\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        // No appearance\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .withReadOnly(true)\n                .build();\n\n        populateRecyclerView(getWidget());\n\n        SelectMultiWidget a = getSpyWidget();\n        AudioVideoImageTextLabel avitLabel = (AudioVideoImageTextLabel) a.binding.choicesRecyclerView.getLayoutManager().getChildAt(0);\n        assertThat(avitLabel.isEnabled(), is(Boolean.FALSE));\n\n        resetWidget();\n\n        // No-buttons appearance\n        formEntryPrompt = new MockFormEntryPromptBuilder(formEntryPrompt)\n                .withAppearance(Appearances.NO_BUTTONS)\n                .build();\n\n        populateRecyclerView(getWidget());\n\n        FrameLayout view = (FrameLayout) getSpyWidget().binding.choicesRecyclerView.getLayoutManager().getChildAt(0);\n        assertThat(view.isEnabled(), is(Boolean.FALSE));\n    }\n\n    @Test\n    public void whenSpacesInUnderlyingValuesExist_shouldAppropriateWarningBeDisplayed() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(asList(\n                        new SelectChoice(\"a\", \"a a\"),\n                        new SelectChoice(\"a\", \"b b\")\n                ))\n                .build();\n\n        TextView warningTv = getWidget().findViewById(R.id.warning_text);\n        assertThat(warningTv.getVisibility(), is(View.VISIBLE));\n        assertThat(warningTv.getText(), is(\"Warning: underlying values a a, b b have spaces\"));\n    }\n\n    @Test\n    public void checkingCheckboxOnItem_callsValueChangedListener() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .build();\n\n        SelectMultiWidget widget = getWidget();\n        WidgetValueChangedListener valueChangedListener = mock();\n        widget.setValueChangedListener(valueChangedListener);\n        populateRecyclerView(widget);\n\n        ((CheckBox) ((AudioVideoImageTextLabel) getWidget().binding.choicesRecyclerView.getChildAt(0)).getLabelTextView()).setChecked(true);\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void whenPromptHasNoButtonsAppearance_clickingItem_callsValueChangedListener() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withAppearance(\"no-buttons\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .build();\n\n        SelectMultiWidget widget = getWidget();\n        WidgetValueChangedListener valueChangedListener = mock();\n        widget.setValueChangedListener(valueChangedListener);\n        populateRecyclerView(widget);\n\n        getWidget().binding.choicesRecyclerView.getChildAt(0).performClick();\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    private void overrideDependencyModule() throws Exception {\n        ReferenceManager referenceManager = setupFakeReferenceManager(REFERENCES);\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n\n            @Override\n            public SoftKeyboardController provideSoftKeyboardController() {\n                return mock(SoftKeyboardController.class);\n            }\n        });\n    }\n\n    private ViewGroup getChoiceView(SelectMultiWidget widget, int index) {\n        return (ViewGroup) widget.binding.choicesRecyclerView.getChildAt(index);\n    }\n\n    private static final List<Pair<String, String>> REFERENCES = asList(\n            new Pair<>(\"ref\", \"file://audio.mp3\"),\n            new Pair<>(\"ref1\", \"file://audio1.mp3\")\n    );\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragmentTest.kt",
    "content": "package org.odk.collect.android.widgets.items\n\nimport android.app.Application\nimport android.os.Bundle\nimport androidx.activity.ComponentDialog\nimport androidx.core.os.bundleOf\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewmodel.CreationExtras\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.work.WorkManager\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.notNullValue\nimport org.javarosa.core.model.data.SelectOneData\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.databinding.WidgetAnswerDialogLayoutBinding\nimport org.odk.collect.android.formentry.FormEntryViewModel\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.android.utilities.Appearances\nimport org.odk.collect.android.widgets.items.SelectOneFromMapDialogFragment.Companion.ARG_SELECTED_INDEX\nimport org.odk.collect.android.widgets.support.FormElementFixtures.selectChoice\nimport org.odk.collect.android.widgets.support.FormElementFixtures.treeElement\nimport org.odk.collect.android.widgets.support.NoOpMapFragment\nimport org.odk.collect.android.widgets.utilities.WidgetAnswerDialogFragment.Companion.ARG_FORM_INDEX\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.geo.selection.IconifiedText\nimport org.odk.collect.geo.selection.MappableSelectItem\nimport org.odk.collect.geo.selection.SelectionMapFragment\nimport org.odk.collect.geo.selection.SelectionMapFragment.Companion.REQUEST_SELECT_ITEM\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.testshared.FakeScheduler\n\n@RunWith(AndroidJUnit4::class)\nclass SelectOneFromMapDialogFragmentTest {\n\n    private val selectChoices = listOf(\n        selectChoice(\n            value = \"a\",\n            item = treeElement(children = listOf(treeElement(SelectChoicesMapData.GEOMETRY, \"12.0 -1.0 305 0\")))\n        ),\n        selectChoice(\n            value = \"b\",\n            item = treeElement(children = listOf(treeElement(SelectChoicesMapData.GEOMETRY, \"13.0 -1.0 305 0\")))\n        )\n    )\n\n    private val prompt = MockFormEntryPromptBuilder()\n        .withLongText(\"Which is your favourite place?\")\n        .withSelectChoices(\n            selectChoices\n        )\n        .withSelectChoiceText(\n            mapOf(\n                selectChoices[0] to \"A\",\n                selectChoices[1] to \"B\"\n            )\n        )\n        .build()\n\n    private val formEntryViewModel = mock<FormEntryViewModel> {\n        on { getQuestionPrompt(prompt.index) } doReturn prompt\n    }\n\n    private val application = ApplicationProvider.getApplicationContext<Application>()\n    private val scheduler = FakeScheduler()\n\n    private val viewModelFactory = object : ViewModelProvider.Factory {\n        override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {\n            return formEntryViewModel as T\n        }\n    }\n\n    @get:Rule\n    val launcherRule =\n        FragmentScenarioLauncherRule(\n            FragmentFactoryBuilder()\n                .forClass(SelectOneFromMapDialogFragment::class.java) {\n                    SelectOneFromMapDialogFragment(viewModelFactory)\n                }.build()\n        )\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesMapFragmentFactory(settingsProvider: SettingsProvider): MapFragmentFactory {\n                return object : MapFragmentFactory {\n                    override fun createMapFragment(): MapFragment {\n                        return NoOpMapFragment()\n                    }\n                }\n            }\n\n            override fun providesScheduler(workManager: WorkManager?, application: Application): Scheduler {\n                return scheduler\n            }\n\n            override fun providesReferenceLayerRepository(\n                storagePathProvider: StoragePathProvider,\n                settingsProvider: SettingsProvider\n            ): ReferenceLayerRepository {\n                return mock()\n            }\n        })\n    }\n\n    @Test\n    fun `pressing back dismisses dialog`() {\n        val scenario = launcherRule.launch(\n            SelectOneFromMapDialogFragment::class.java,\n            Bundle().also {\n                it.putSerializable(ARG_FORM_INDEX, prompt.index)\n            }\n        )\n\n        scheduler.flush()\n\n        scenario.onFragment {\n            Espresso.pressBack()\n            assertThat(it.isVisible, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `sets up SelectionMapFragment`() {\n        val scenario = launcherRule.launch(\n            SelectOneFromMapDialogFragment::class.java,\n            Bundle().also {\n                it.putSerializable(ARG_FORM_INDEX, prompt.index)\n            }\n        )\n\n        scenario.onFragment {\n            val binding = WidgetAnswerDialogLayoutBinding.bind(it.view!!)\n            val fragment = binding.answerFragment.getFragment<SelectionMapFragment>()\n            assertThat(fragment, notNullValue())\n            assertThat(fragment.skipSummary, equalTo(false))\n            assertThat(fragment.showNewItemButton, equalTo(false))\n\n            val dialogBackPressedDispatcher =\n                (it.requireDialog() as ComponentDialog).onBackPressedDispatcher\n            assertThat(fragment.onBackPressedDispatcher?.invoke(), equalTo(dialogBackPressedDispatcher))\n        }\n    }\n\n    @Test\n    fun `gives SelectionMapFragment correct data`() {\n        val scenario = launcherRule.launch(\n            SelectOneFromMapDialogFragment::class.java,\n            Bundle().also {\n                it.putSerializable(ARG_FORM_INDEX, prompt.index)\n            }\n        )\n\n        scenario.onFragment {\n            val binding = WidgetAnswerDialogLayoutBinding.bind(it.view!!)\n            val fragment = binding.answerFragment.getFragment<SelectionMapFragment>()\n\n            val data = fragment.selectionMapData\n            scheduler.flush()\n\n            assertThat(data.getMapTitle().value, equalTo(prompt.longText))\n            assertThat(data.getItemCount().value, equalTo(prompt.selectChoices.size))\n            val firstFeatureGeometry = selectChoices[0].getChild(SelectChoicesMapData.GEOMETRY)!!.split(\" \")\n            val secondFeatureGeometry = selectChoices[1].getChild(SelectChoicesMapData.GEOMETRY)!!.split(\" \")\n            assertThat(\n                data.getMappableItems().value,\n                equalTo(\n                    listOf(\n                        MappableSelectItem.MappableSelectPoint(\n                            0,\n                            \"A\",\n                            point = MapPoint(\n                                firstFeatureGeometry[0].toDouble(),\n                                firstFeatureGeometry[1].toDouble(),\n                                firstFeatureGeometry[2].toDouble(),\n                                firstFeatureGeometry[3].toDouble()\n                            ),\n                            smallIcon = org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small,\n                            largeIcon = org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big,\n                            action = IconifiedText(\n                                org.odk.collect.icons.R.drawable.ic_save,\n                                application.getString(org.odk.collect.strings.R.string.select_item)\n                            )\n                        ),\n                        MappableSelectItem.MappableSelectPoint(\n                            1,\n                            \"B\",\n                            point = MapPoint(\n                                secondFeatureGeometry[0].toDouble(),\n                                secondFeatureGeometry[1].toDouble(),\n                                secondFeatureGeometry[2].toDouble(),\n                                secondFeatureGeometry[3].toDouble()\n                            ),\n                            smallIcon = org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small,\n                            largeIcon = org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big,\n                            action = IconifiedText(\n                                org.odk.collect.icons.R.drawable.ic_save,\n                                application.getString(org.odk.collect.strings.R.string.select_item)\n                            )\n                        )\n                    )\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `contains SelectionMapFragment with correct data with selected index`() {\n        val scenario = launcherRule.launch(\n            SelectOneFromMapDialogFragment::class.java,\n            Bundle().also {\n                it.putSerializable(ARG_FORM_INDEX, prompt.index)\n                it.putSerializable(ARG_SELECTED_INDEX, selectChoices[1].index)\n            }\n        )\n\n        scheduler.flush()\n\n        scenario.onFragment {\n            val binding = WidgetAnswerDialogLayoutBinding.bind(it.view!!)\n            val fragment = binding.answerFragment.getFragment<SelectionMapFragment>()\n            assertThat(fragment, notNullValue())\n            assertThat(fragment.skipSummary, equalTo(false))\n            assertThat(fragment.showNewItemButton, equalTo(false))\n\n            val data = fragment.selectionMapData\n            assertThat(data.getMappableItems().value!![1].selected, equalTo(true))\n        }\n    }\n\n    @Test\n    fun `contains SelectionMapFragment with correct data for quick appearance`() {\n        val prompt = MockFormEntryPromptBuilder()\n            .withAppearance(\"${Appearances.MAP} ${Appearances.QUICK}\")\n            .build()\n        whenever(formEntryViewModel.getQuestionPrompt(prompt.index)).thenReturn(prompt)\n\n        val scenario = launcherRule.launch(\n            SelectOneFromMapDialogFragment::class.java,\n            Bundle().also {\n                it.putSerializable(ARG_FORM_INDEX, prompt.index)\n            }\n        )\n\n        scheduler.runBackground()\n\n        scenario.onFragment {\n            val binding = WidgetAnswerDialogLayoutBinding.bind(it.view!!)\n            val fragment = binding.answerFragment.getFragment<SelectionMapFragment>()\n            assertThat(fragment.skipSummary, equalTo(true))\n        }\n    }\n\n    @Test\n    fun `selecting a choice on the map answers question and dismisses`() {\n        val scenario = launcherRule.launch(\n            SelectOneFromMapDialogFragment::class.java,\n            Bundle().also {\n                it.putSerializable(ARG_FORM_INDEX, prompt.index)\n            }\n        )\n\n        scheduler.runBackground()\n\n        scenario.onFragment {\n            val result = bundleOf(SelectionMapFragment.RESULT_SELECTED_ITEM to 1L)\n            it.childFragmentManager.setFragmentResult(REQUEST_SELECT_ITEM, result)\n            assertThat(it.isVisible, equalTo(false))\n        }\n\n        verify(formEntryViewModel).answerQuestion(\n            prompt.index,\n            SelectOneData(selectChoices[1].selection())\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets.items\n\nimport android.content.Context\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewmodel.CreationExtras\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.notNullValue\nimport org.hamcrest.Matchers.nullValue\nimport org.javarosa.core.model.data.SelectOneData\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.fakes.FakePermissionsProvider\nimport org.odk.collect.android.formentry.FormEntryViewModel\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.injection.config.AppDependencyModule\nimport org.odk.collect.android.storage.StoragePathProvider\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.android.support.WidgetTestActivity\nimport org.odk.collect.android.widgets.support.FormElementFixtures.selectChoice\nimport org.odk.collect.android.widgets.support.NoOpMapFragment\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils.FontSize\nimport org.odk.collect.android.widgets.utilities.WidgetAnswerDialogFragment.Companion.ARG_FORM_INDEX\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.permissions.PermissionsProvider\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.enums.GuidanceHintMode\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.testshared.RobolectricHelpers.getFragmentByClass\nimport org.robolectric.Robolectric\n\n@RunWith(AndroidJUnit4::class)\nclass SelectOneFromMapWidgetTest {\n\n    private val activityController = Robolectric.buildActivity(WidgetTestActivity::class.java)\n    private val formEntryViewModel = mock<FormEntryViewModel>()\n\n    private val permissionsProvider = FakePermissionsProvider().also {\n        it.setPermissionGranted(true)\n    }\n\n    private val settingsProvider = InMemSettingsProvider().also {\n        it.getUnprotectedSettings().save(ProjectKeys.KEY_FONT_SIZE, \"12\")\n        it.getUnprotectedSettings().save(\n            ProjectKeys.KEY_GUIDANCE_HINT,\n            GuidanceHintMode.YES.getValue(activityController.get())\n        )\n    }\n\n    @Before\n    fun setup() {\n        CollectHelpers.overrideAppDependencyModule(object : AppDependencyModule() {\n            override fun providesPermissionsProvider(permissionsChecker: PermissionsChecker): PermissionsProvider =\n                permissionsProvider\n\n            override fun providesSettingsProvider(context: Context): SettingsProvider =\n                settingsProvider\n\n            override fun providesMapFragmentFactory(settingsProvider: SettingsProvider?): MapFragmentFactory {\n                return object : MapFragmentFactory {\n                    override fun createMapFragment(): MapFragment {\n                        return NoOpMapFragment()\n                    }\n                }\n            }\n\n            override fun providesReferenceLayerRepository(\n                storagePathProvider: StoragePathProvider,\n                settingsProvider: SettingsProvider\n            ): ReferenceLayerRepository {\n                return mock()\n            }\n        })\n    }\n\n    @Test\n    fun `button uses app font size`() {\n        val settings = settingsProvider.getUnprotectedSettings()\n        val widget = SelectOneFromMapWidget(\n            activityController.get(),\n            QuestionDetails(promptWithAnswer(null)),\n            widgetDependencies()\n        )\n\n        assertThat(\n            widget.binding.button.textSize.toInt(),\n            equalTo(QuestionFontSizeUtils.getFontSize(settings, FontSize.BODY_LARGE))\n        )\n    }\n\n    @Test\n    fun `clicking button opens SelectOneFromMapDialogFragment`() {\n        val activity = activityController.setup().get()\n        activity.supportFragmentManager.fragmentFactory = FragmentFactoryBuilder().forClass(SelectOneFromMapDialogFragment::class.java) {\n            SelectOneFromMapDialogFragment(object : ViewModelProvider.Factory {\n                override fun <T : ViewModel> create(\n                    modelClass: Class<T>,\n                    extras: CreationExtras\n                ): T {\n                    return formEntryViewModel as T\n                }\n            })\n        }.build()\n\n        val prompt = promptWithAnswer(null)\n        val widget =\n            SelectOneFromMapWidget(activity, QuestionDetails(prompt), widgetDependencies())\n        whenever(formEntryViewModel.getQuestionPrompt(prompt.index)).doReturn(prompt)\n\n        widget.binding.button.performClick()\n\n        val fragment = getFragmentByClass(\n            activityController.get().supportFragmentManager,\n            SelectOneFromMapDialogFragment::class.java\n        )\n        assertThat(fragment, notNullValue())\n        assertThat(\n            fragment?.requireArguments()\n                ?.getSerializable(ARG_FORM_INDEX),\n            equalTo(prompt.index)\n        )\n    }\n\n    @Test\n    fun `clicking button when location permissions denied does nothing`() {\n        val widget = SelectOneFromMapWidget(\n            activityController.get(),\n            QuestionDetails(promptWithAnswer(null)),\n            widgetDependencies()\n        )\n\n        permissionsProvider.setPermissionGranted(false)\n        widget.binding.button.performClick()\n\n        val fragment = getFragmentByClass(\n            activityController.get().supportFragmentManager,\n            SelectOneFromMapDialogFragment::class.java\n        )\n        assertThat(fragment, nullValue())\n    }\n\n    @Test\n    fun `shows answer`() {\n        val choices = listOf(selectChoice(\"a\"), selectChoice(\"b\"))\n        val prompt = MockFormEntryPromptBuilder()\n            .withSelectChoices(choices)\n            .withSelectChoiceText(mapOf(choices[0] to \"A\", choices[1] to \"B\"))\n            .withAnswer(SelectOneData(choices[0].selection()))\n            .build()\n\n        val widget = SelectOneFromMapWidget(\n            activityController.get(),\n            QuestionDetails(prompt),\n            widgetDependencies()\n        )\n        assertThat(widget.binding.answer.text, equalTo(\"A\"))\n    }\n\n    @Test\n    fun `answer uses app font size`() {\n        val settings = settingsProvider.getUnprotectedSettings()\n        val widget = SelectOneFromMapWidget(\n            activityController.get(),\n            QuestionDetails(promptWithAnswer(null)),\n            widgetDependencies()\n        )\n\n        assertThat(\n            widget.binding.answer.textSize.toInt(),\n            equalTo(QuestionFontSizeUtils.getFontSize(settings, FontSize.HEADLINE_6))\n        )\n    }\n\n    @Test\n    fun `prompt answer is returned from getAnswer`() {\n        val selectChoice = selectChoice(value = \"a\")\n        val answer = SelectOneData(selectChoice.selection())\n\n        val widget = SelectOneFromMapWidget(\n            activityController.get(),\n            QuestionDetails(promptWithAnswer(answer)),\n            widgetDependencies()\n        )\n        assertThat(widget.answer, equalTo(answer))\n    }\n\n    @Test\n    fun `clearAnswer removes answer`() {\n        val selectChoice = selectChoice(value = \"a\")\n        val answer = SelectOneData(selectChoice.selection())\n\n        val widget = SelectOneFromMapWidget(\n            activityController.get(),\n            QuestionDetails(promptWithAnswer(answer)),\n            widgetDependencies()\n        )\n        widget.clearAnswer()\n        assertThat(widget.answer, equalTo(null))\n    }\n\n    @Test\n    fun `clearAnswer calls value changed listener`() {\n        val selectChoice = selectChoice(value = \"a\")\n        val answer = SelectOneData(selectChoice.selection())\n\n        val widget = SelectOneFromMapWidget(\n            activityController.get(),\n            QuestionDetails(promptWithAnswer(answer)),\n            widgetDependencies()\n        )\n\n        val mockValueChangedListener = mockValueChangedListener(widget)\n        widget.clearAnswer()\n        verify(mockValueChangedListener).widgetValueChanged(widget)\n    }\n\n    @Test\n    fun `clearAnswer updates shown answer`() {\n        val choices = listOf(selectChoice(\"a\"), selectChoice(\"b\"))\n        val prompt = MockFormEntryPromptBuilder()\n            .withSelectChoices(choices)\n            .withSelectChoiceText(mapOf(choices[0] to \"A\", choices[1] to \"B\"))\n            .withAnswer(SelectOneData(choices[0].selection()))\n            .build()\n\n        val widget = SelectOneFromMapWidget(\n            activityController.get(),\n            QuestionDetails(prompt),\n            widgetDependencies()\n        )\n\n        widget.clearAnswer()\n        assertThat(widget.binding.answer.text, equalTo(\"\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneImageMapWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.widgets.support.FormEntryPromptSelectChoiceLoader;\n\nimport java.util.List;\n\npublic class SelectOneImageMapWidgetTest extends SelectImageMapWidgetTest<SelectOneImageMapWidget, SelectOneData> {\n    @NonNull\n    @Override\n    public SelectOneImageMapWidget createWidget() {\n        return new SelectOneImageMapWidget(activity, new QuestionDetails(formEntryPrompt), false, new FormEntryPromptSelectChoiceLoader(), dependencies);\n    }\n\n    @NonNull\n    @Override\n    public SelectOneData getNextAnswer() {\n        List<SelectChoice> selectChoices = getSelectChoices();\n\n        int selectedIndex = Math.abs(random.nextInt()) % selectChoices.size();\n        SelectChoice selectChoice = selectChoices.get(selectedIndex);\n\n        Selection selection = new Selection(selectChoice);\n        return new SelectOneData(selection);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneMinimalWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport android.text.InputType;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\n\nimport org.javarosa.core.model.data.SelectOneData;\nimport org.javarosa.core.model.data.helper.Selection;\nimport org.junit.Test;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.widgets.base.GeneralSelectOneWidgetTest;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.android.widgets.support.FormEntryPromptSelectChoiceLoader;\n\nimport java.util.Collections;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\n\npublic class SelectOneMinimalWidgetTest extends GeneralSelectOneWidgetTest<SelectOneMinimalWidget> {\n    @NonNull\n    @Override\n    public SelectOneMinimalWidget createWidget() {\n        return new SelectOneMinimalWidget(activity, new QuestionDetails(formEntryPrompt), false, new FakeWaitingForDataRegistry(), new FormEntryPromptSelectChoiceLoader(), dependencies);\n    }\n\n    @Test\n    public void answerView_shouldBeMultilineTextNoSuggestion() {\n        assertThat(getSpyWidget().binding.answer.getInputType(), is(InputType.TYPE_CLASS_TEXT + InputType.TYPE_TEXT_FLAG_MULTI_LINE + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS));\n    }\n\n    @Test\n    public void answerView_shouldHaveSavingStateDisabled() {\n        assertThat(getSpyWidget().binding.answer.isSaveEnabled(), is(false));\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n        assertThat(getSpyWidget().binding.answer.getVisibility(), is(View.VISIBLE));\n        assertThat(getSpyWidget().binding.answer.isEnabled(), is(Boolean.FALSE));\n    }\n\n    @Test\n    public void whenThereIsNoAnswer_shouldDefaultTextBeDisplayed() {\n        assertThat(getSpyWidget().binding.answer.getText().toString(), is(\"Select Answer\"));\n    }\n\n    @Test\n    public void whenThereIsAnswer_shouldSelectedChoicesBeDisplayed() {\n        SelectOneData answer = getInitialAnswer();\n        Selection selectedChoice = (Selection) answer.getValue();\n        when(formEntryPrompt.getAnswerValue()).thenReturn(answer);\n        when(formEntryPrompt.getSelectItemText(selectedChoice)).thenReturn(selectedChoice.getValue());\n\n        assertThat(getSpyWidget().binding.answer.getText().toString(), is(selectedChoice.getValue()));\n    }\n\n    @Test\n    public void whenAnswerChanges_shouldAnswerLabelBeUpdated() {\n        assertThat(getSpyWidget().binding.answer.getText().toString(), is(\"Select Answer\"));\n\n        SelectOneData answer = getInitialAnswer();\n        Selection selectedChoice = (Selection) answer.getValue();\n        when(formEntryPrompt.getSelectItemText(selectedChoice)).thenReturn(selectedChoice.getValue());\n        getSpyWidget().setData(Collections.singletonList(selectedChoice));\n\n        assertThat(getSpyWidget().binding.answer.getText().toString(), is(selectedChoice.getValue()));\n        getSpyWidget().clearAnswer();\n        assertThat(getSpyWidget().binding.answer.getText().toString(), is(\"Select Answer\"));\n    }\n\n    @Test\n    public void whenAnswerChanges_shouldValueChangeListenersBeCalled() {\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(getSpyWidget());\n\n        SelectOneData answer = getInitialAnswer();\n        Selection selectedChoice = (Selection) answer.getValue();\n        getSpyWidget().setData(Collections.singletonList(selectedChoice));\n\n        verify(valueChangedListener).widgetValueChanged(getSpyWidget());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.items;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.support.CollectHelpers.setupFakeReferenceManager;\nimport static org.odk.collect.testshared.RobolectricHelpers.populateRecyclerView;\nimport static java.util.Arrays.asList;\n\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.util.Pair;\nimport androidx.recyclerview.widget.GridLayoutManager;\n\nimport com.google.android.flexbox.FlexboxLayoutManager;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.javarosa.core.reference.ReferenceManager;\nimport org.javarosa.form.api.FormEntryCaption;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\nimport org.odk.collect.android.R;\nimport org.odk.collect.android.formentry.questions.AudioVideoImageTextLabel;\nimport org.odk.collect.android.formentry.questions.NoButtonsItem;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.injection.config.AppDependencyModule;\nimport org.odk.collect.android.listeners.AdvanceToNextListener;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.utilities.SoftKeyboardController;\nimport org.odk.collect.android.widgets.QuestionWidget;\nimport org.odk.collect.android.widgets.base.GeneralSelectOneWidgetTest;\nimport org.odk.collect.android.widgets.support.FormEntryPromptSelectChoiceLoader;\nimport org.odk.collect.audioclips.AudioPlayer;\nimport org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils;\nimport org.odk.collect.audioclips.Clip;\n\nimport java.util.List;\n\n/**\n * @author James Knight\n */\npublic class SelectOneWidgetTest extends GeneralSelectOneWidgetTest<SelectOneWidget> {\n    @Mock\n    private AdvanceToNextListener listener;\n\n    @NonNull\n    @Override\n    public SelectOneWidget createWidget() {\n        SelectOneWidget selectOneWidget = new SelectOneWidget(activity, new QuestionDetails(formEntryPrompt), isQuick(), null, new FormEntryPromptSelectChoiceLoader(), dependencies);\n        if (isQuick()) {\n            selectOneWidget.setListener(listener);\n        }\n        selectOneWidget.setFocus(activity);\n        return selectOneWidget;\n    }\n\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    @Before\n    public void setup() throws Exception {\n        overrideDependencyModule();\n    }\n\n    @Test\n    public void byDefault_shouldGridLayoutManagerBeUsed() {\n        assertThat(getWidget().binding.choicesRecyclerView.getLayoutManager().getClass().getName(), is(GridLayoutManager.class.getName()));\n    }\n\n    @Test\n    public void whenColumnsPackAppearanceExist_shouldFlexboxLayoutManagerBeUsed() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"columns-pack\");\n        assertThat(getWidget().binding.choicesRecyclerView.getLayoutManager().getClass().getName(), is(FlexboxLayoutManager.class.getName()));\n    }\n\n    @Test\n    public void whenButtonsModeExist_shouldFrameLayoutBeUsedAsItemView() {\n        populateRecyclerView(getWidget());\n        assertThat(getChoiceView(getWidget(), 0).getClass().getName(), is(AudioVideoImageTextLabel.class.getName()));\n    }\n\n    @Test\n    public void whenNoButtonsModeExist_shouldFrameLayoutBeUsedAsItemView() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"no-buttons\");\n        populateRecyclerView(getWidget());\n        assertThat(getChoiceView(getWidget(), 0).getClass().getName(), is(NoButtonsItem.class.getName()));\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceExist_shouldTextSizeBeSetProperly() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"autocomplete\");\n        assertThat((int) getSpyWidget().binding.choicesSearchBox.getTextSize(), is(QuestionFontSizeUtils.getFontSize(settingsProvider.getUnprotectedSettings(), QuestionFontSizeUtils.FontSize.HEADLINE_6)));\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceExist_shouldSearchBoxBeVisible() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"autocomplete\");\n        assertThat(getSpyWidget().binding.choicesSearchBox.getVisibility(), is(View.VISIBLE));\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceDoesNotExist_shouldSearchBoxBeHidden() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"\");\n        assertThat(getSpyWidget().binding.choicesSearchBox.getVisibility(), is(View.GONE));\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceDoesNotExist_shouldNotKeyboardBeDisplayed() {\n        SelectOneWidget widget = getSpyWidget();\n        verify(widget.softKeyboardController, never()).showSoftKeyboard(widget.binding.choicesSearchBox);\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceExist_shouldKeyboardBeDisplayed() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"autocomplete\");\n        SelectOneWidget widget = getSpyWidget();\n        verify(widget.softKeyboardController).showSoftKeyboard(widget.binding.choicesSearchBox);\n    }\n\n    @Test\n    public void whenAutocompleteAppearanceExistAndWidgetIsReadOnly_shouldNotKeyboardBeDisplayed() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(\"autocomplete\");\n        when(formEntryPrompt.isReadOnly()).thenReturn(true);\n        SelectOneWidget widget = getSpyWidget();\n        verify(widget.softKeyboardController, never()).showSoftKeyboard(widget.binding.choicesSearchBox);\n    }\n\n    @Test\n    public void whenQuickAppearanceIsUsed_shouldAdvanceToNextListenerBeCalledInButtonsMode() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(asList(\n                        new SelectChoice(\"AAA\", \"AAA\"),\n                        new SelectChoice(\"BBB\", \"BBB\")\n                ))\n                .withAppearance(\"quick\")\n                .build();\n\n        SelectOneWidget widget = getWidget();\n        populateRecyclerView(widget);\n\n        clickChoice(widget, 0); // Select AAA\n        assertThat(widget.getAnswer().getDisplayText(), is(\"AAA\"));\n\n        verify(listener).advance();\n    }\n\n    @Test\n    public void whenQuickAppearanceIsUsed_shouldAdvanceToNextListenerBeCalledInNoButtonsMode() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(asList(\n                        new SelectChoice(\"AAA\", \"AAA\"),\n                        new SelectChoice(\"BBB\", \"BBB\")\n                ))\n                .withAppearance(\"quick no-buttons\")\n                .build();\n\n        SelectOneWidget widget = getWidget();\n        populateRecyclerView(widget);\n\n        clickChoice(widget, 0); // Select AAA\n        assertThat(widget.getAnswer().getDisplayText(), is(\"AAA\"));\n\n        verify(listener).advance();\n    }\n\n    @Test\n    public void whenQuickAppearanceIsNotUsed_shouldNotAdvanceToNextListenerBeCalledInButtonsMode() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(asList(\n                        new SelectChoice(\"AAA\", \"AAA\"),\n                        new SelectChoice(\"BBB\", \"BBB\")\n                ))\n                .build();\n\n        SelectOneWidget widget = getWidget();\n        populateRecyclerView(widget);\n\n        clickChoice(widget, 0); // Select AAA\n        assertThat(widget.getAnswer().getDisplayText(), is(\"AAA\"));\n\n        verify(listener, times(0)).advance();\n    }\n\n    @Test\n    public void whenQuickAppearanceIsNotUsed_shouldNotAdvanceToNextListenerBeCalledInNoButtonsMode() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(asList(\n                        new SelectChoice(\"AAA\", \"AAA\"),\n                        new SelectChoice(\"BBB\", \"BBB\")\n                ))\n                .withAppearance(\"no-buttons\")\n                .build();\n\n        SelectOneWidget widget = getWidget();\n        populateRecyclerView(widget);\n\n        clickChoice(widget, 0); // Select AAA\n        assertThat(widget.getAnswer().getDisplayText(), is(\"AAA\"));\n\n        verify(listener, times(0)).advance();\n    }\n\n    @Test\n    public void whenChoicesHaveAudio_audioButtonUsesIndexAsClipID() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .withSpecialFormSelectChoiceText(asList(\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(0).first),\n                        new Pair<>(FormEntryCaption.TEXT_FORM_AUDIO, REFERENCES.get(1).first)\n                ))\n                .build();\n\n        AudioPlayer audioPlayer = mock();\n        dependencies = new QuestionWidget.Dependencies(audioPlayer, null);\n        populateRecyclerView(getWidget());\n\n        getChoiceView(getWidget(), 0).findViewById(R.id.audioButton).performClick();\n        verify(audioPlayer).play(eq(new Clip(\"i am index 0\", REFERENCES.get(0).second)));\n        getChoiceView(getWidget(), 1).findViewById(R.id.audioButton).performClick();\n        verify(audioPlayer).play(eq(new Clip(\"i am index 1\", REFERENCES.get(1).second)));\n    }\n\n    @Test\n    public void whenAChoiceValueIsNull_selecting_doesNotSetAnswer() {\n        SelectChoice selectChoice = new SelectChoice(); // The two arg constructor protects against null values\n        selectChoice.setTextID(\"1\");\n\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withSelectChoices(asList(selectChoice))\n                .build();\n\n        SelectOneWidget widget = getWidget();\n        populateRecyclerView(widget);\n\n        clickChoice(widget, 0);\n        assertThat(widget.getAnswer(), nullValue());\n    }\n\n    @Test\n    public void usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() {\n        // No appearance\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .withReadOnly(true)\n                .build();\n\n        populateRecyclerView(getWidget());\n\n        AudioVideoImageTextLabel avitLabel = (AudioVideoImageTextLabel) getSpyWidget().binding.choicesRecyclerView.getLayoutManager().getChildAt(0);\n        assertThat(avitLabel.isEnabled(), is(Boolean.FALSE));\n\n        resetWidget();\n\n        // No-buttons appearance\n        formEntryPrompt = new MockFormEntryPromptBuilder(formEntryPrompt)\n                .withAppearance(Appearances.NO_BUTTONS)\n                .build();\n\n        populateRecyclerView(getWidget());\n\n        FrameLayout view = (FrameLayout) getSpyWidget().binding.choicesRecyclerView.getLayoutManager().getChildAt(0);\n        assertThat(view.isEnabled(), is(Boolean.FALSE));\n    }\n\n    @Test\n    public void clickingItem_callsValueChangedListener() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .build();\n\n        SelectOneWidget widget = getWidget();\n        WidgetValueChangedListener valueChangedListener = mock();\n        widget.setValueChangedListener(valueChangedListener);\n        populateRecyclerView(widget);\n\n        ((AudioVideoImageTextLabel) getWidget().binding.choicesRecyclerView.getChildAt(0)).getLabelTextView().performClick();\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void whenPromptHasNoButtonsAppearance_clickingItem_callsValueChangedListener() {\n        formEntryPrompt = new MockFormEntryPromptBuilder()\n                .withIndex(\"i am index\")\n                .withAppearance(\"no-buttons\")\n                .withSelectChoices(asList(\n                        new SelectChoice(\"1\", \"1\"),\n                        new SelectChoice(\"2\", \"2\")\n                ))\n                .build();\n\n        SelectOneWidget widget = getWidget();\n        WidgetValueChangedListener valueChangedListener = mock();\n        widget.setValueChangedListener(valueChangedListener);\n        populateRecyclerView(widget);\n\n        getWidget().binding.choicesRecyclerView.getChildAt(0).performClick();\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    private void overrideDependencyModule() throws Exception {\n        ReferenceManager referenceManager = setupFakeReferenceManager(REFERENCES);\n        CollectHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n\n            @Override\n            public ReferenceManager providesReferenceManager() {\n                return referenceManager;\n            }\n\n            @Override\n            public SoftKeyboardController provideSoftKeyboardController() {\n                return mock(SoftKeyboardController.class);\n            }\n        });\n    }\n\n    private void clickChoice(SelectOneWidget widget, int index) {\n        if (Appearances.isNoButtonsAppearance(formEntryPrompt)) {\n            clickNoButtonChoice(widget, index);\n        } else {\n            clickButtonChoice(widget, index);\n        }\n    }\n\n    private void clickNoButtonChoice(SelectOneWidget widget, int index) {\n        widget.binding.choicesRecyclerView.getChildAt(index).performClick();\n    }\n\n    private void clickButtonChoice(SelectOneWidget widget, int index) {\n        ((AudioVideoImageTextLabel) getChoiceView(widget, index)).getLabelTextView().performClick();\n    }\n\n    private ViewGroup getChoiceView(SelectOneWidget widget, int index) {\n        return (ViewGroup) widget.binding.choicesRecyclerView.getChildAt(index);\n    }\n\n    private static final List<Pair<String, String>> REFERENCES = asList(\n            new Pair<>(\"ref\", \"file://audio.mp3\"),\n            new Pair<>(\"ref1\", \"file://audio1.mp3\")\n    );\n\n    private boolean isQuick() {\n        return Appearances.getSanitizedAppearanceHint(formEntryPrompt).contains(\"quick\");\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/range/RangeDecimalWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.range;\n\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.RangeQuestion;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.testshared.SliderExtKt;\n\nimport java.math.BigDecimal;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.not;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithQuestionDefAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndQuestionDef;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\n@RunWith(AndroidJUnit4.class)\npublic class RangeDecimalWidgetTest {\n    private static final String NO_TICKS_APPEARANCE = \"no-ticks\";\n\n    private RangeQuestion rangeQuestion;\n\n    @Before\n    public void setup() {\n        rangeQuestion = mock(RangeQuestion.class);\n        when(rangeQuestion.getRangeStart()).thenReturn(BigDecimal.valueOf(1.5));\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(5.5));\n        when(rangeQuestion.getRangeStep()).thenReturn(BigDecimal.valueOf(0.5));\n    }\n\n    @Test\n    public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {\n        assertThat(createWidget(promptWithReadOnlyAndQuestionDef(rangeQuestion)).getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"2.5\")));\n        assertThat(widget.getAnswer().getValue(), equalTo(2.5));\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_sliderShowsNoAnswerMarked() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        assertThat(widget.slider.getValue(), equalTo(1.5F));\n        assertThat(widget.slider.getThumbRadius(), equalTo(0));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_sliderShowsCorrectAnswer() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"2.5\")));\n        assertThat(widget.slider.getValue(), equalTo(2.5F));\n        assertThat(widget.slider.getThumbRadius(), not(0));\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_widgetShouldShowNullAnswer() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        assertThat(widget.currentValue.getText(), equalTo(\"\"));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_widgetShouldShowCorrectAnswer() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"2.5\")));\n        assertThat(widget.currentValue.getText(), equalTo(\"2.5\"));\n    }\n\n    @Test\n    public void whenSliderIsDiscrete_widgetShowsCorrectSlider() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"2.5\")));\n\n        assertThat(widget.slider.getValueFrom(), equalTo(1.5F));\n        assertThat(widget.slider.getValueTo(), equalTo(5.5F));\n        assertThat(widget.slider.getValue(), equalTo(2.5F));\n        assertThat(widget.slider.isTickVisible(), equalTo(true));\n    }\n\n    @Test\n    public void whenSliderIsContinuous_widgetShowsCorrectSlider() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withQuestion(rangeQuestion)\n                .withAnswer(new StringData(\"2.5\"))\n                .withAppearance(NO_TICKS_APPEARANCE)\n                .build();\n        RangeDecimalWidget widget = createWidget(prompt);\n\n        assertThat(widget.slider.getValueFrom(), equalTo(1.5F));\n        assertThat(widget.slider.getValueTo(), equalTo(5.5F));\n        assertThat(widget.slider.getValue(), equalTo(2.5F));\n        assertThat(widget.slider.isTickVisible(), equalTo(false));\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"2.5\")));\n        widget.clearAnswer();\n        assertThat(widget.currentValue.getText(), equalTo(\"\"));\n    }\n\n    @Test\n    public void clearAnswer_hidesSliderThumb() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"2.5\")));\n        widget.clearAnswer();\n        assertThat(widget.slider.getThumbRadius(), equalTo(0));\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListener() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"2.5\")));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.clearAnswer();\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void changingSliderValue_updatesAnswer() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        SliderExtKt.clickOnMaxValue(widget.slider);\n        assertThat(widget.currentValue.getText(), equalTo(\"5.5\"));\n    }\n\n    @Test\n    public void changingSliderValue_showsSliderThumb() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        SliderExtKt.clickOnMinValue(widget.slider);\n        assertThat(widget.slider.getThumbRadius(), not(0));\n    }\n\n    @Test\n    public void changingSliderValue_whenRangeStartIsGreaterThanRangeEnd_updatesAnswer() {\n        when(rangeQuestion.getRangeStart()).thenReturn(BigDecimal.valueOf(5.5));\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.valueOf(1.5));\n\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        SliderExtKt.clickOnMaxValue(widget.slider);\n\n        assertThat(widget.currentValue.getText(), equalTo(\"1.5\"));\n    }\n\n    @Test\n    public void changingSliderValue_callsValueChangeListener() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        SliderExtKt.clickOnMaxValue(widget.slider);\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void changingSliderValueProgramatically_doesNotUpdateAnswer() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        widget.slider.setValue(2.5F);\n        assertThat(widget.currentValue.getText(), equalTo(\"\"));\n    }\n\n    @Test\n    public void changingSliderValueProgramatically_doesNotCallValueChangeListener() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.slider.setValue(2.5F);\n        verify(valueChangedListener, never()).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void clickingSliderForLong_doesNotCallLongClickListener() {\n        View.OnLongClickListener listener = mock(View.OnLongClickListener.class);\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        widget.setOnLongClickListener(listener);\n        widget.slider.performLongClick();\n\n        verify(listener, never()).onLongClick(widget.slider);\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5530\n    public void everyTriggerWidgetShouldHaveCheckboxWithUniqueID() {\n        RangeDecimalWidget widget1 = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        RangeDecimalWidget widget2 = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n\n        assertThat(widget1.slider.getId(), not(equalTo(widget2.slider.getId())));\n    }\n\n    @Test\n    public void changingSliderValueToTheMinOneWhenSliderHasNoValue_setsTheValue() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n\n        SliderExtKt.clickOnMinValue(widget.slider);\n\n        assertThat(widget.currentValue.getText(), equalTo(\"1.5\"));\n    }\n\n    @Test\n    public void changingSliderValueToAnyOtherThanTheMinOne_setsTheValueCorrectly() {\n        RangeDecimalWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n\n        SliderExtKt.clickOnMaxValue(widget.slider);\n\n        assertThat(widget.currentValue.getText(), equalTo(\"5.5\"));\n    }\n\n    private RangeDecimalWidget createWidget(FormEntryPrompt prompt) {\n        RangeDecimalWidget widget = new RangeDecimalWidget(widgetTestActivity(), new QuestionDetails(prompt), widgetDependencies());\n        widget.slider.layout(0, 0, 100, 10);\n        return widget;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/range/RangeIntegerWidgetTest.java",
    "content": "package org.odk.collect.android.widgets.range;\n\nimport android.view.View;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.RangeQuestion;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.questions.QuestionDetails;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.testshared.SliderExtKt;\n\nimport java.math.BigDecimal;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.not;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithQuestionDefAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndQuestionDef;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\n@RunWith(AndroidJUnit4.class)\npublic class RangeIntegerWidgetTest {\n    private static final String NO_TICKS_APPEARANCE = \"no-ticks\";\n\n    private RangeQuestion rangeQuestion;\n\n    @Before\n    public void setup() {\n        rangeQuestion = mock(RangeQuestion.class);\n        when(rangeQuestion.getRangeStart()).thenReturn(BigDecimal.ONE);\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.TEN);\n        when(rangeQuestion.getRangeStep()).thenReturn(BigDecimal.ONE);\n    }\n\n    @Test\n    public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {\n        assertNull(createWidget(promptWithReadOnlyAndQuestionDef(rangeQuestion)).getAnswer());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"4\")));\n        assertEquals(widget.getAnswer().getValue(), 4);\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_sliderShowsNoAnswerMarked() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        assertThat(widget.slider.getValue(), equalTo(1.0F));\n        assertThat(widget.slider.getThumbRadius(), equalTo(0));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_sliderShowsCorrectAnswer() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"4\")));\n        assertThat(widget.slider.getValue(), equalTo(4.0F));\n        assertThat(widget.slider.getThumbRadius(), not(0));\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_widgetShowsNullAnswer() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        assertThat(widget.currentValue.getText(), equalTo(\"\"));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_widgetShouldShowCorrectAnswer() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"4\")));\n        assertThat(widget.currentValue.getText(), equalTo(\"4\"));\n    }\n\n    @Test\n    public void whenSliderIsDiscrete_widgetShowsCorrectSliderValues() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"4\")));\n\n        assertThat(widget.slider.getValueFrom(), equalTo(1.0F));\n        assertThat(widget.slider.getValueTo(), equalTo(10.0F));\n        assertThat(widget.slider.getValue(), equalTo(4.0F));\n        assertThat(widget.slider.isTickVisible(), equalTo(true));\n    }\n\n    @Test\n    public void whenSliderIsContinuous_widgetShowsCorrectSliderValues() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withQuestion(rangeQuestion)\n                .withAnswer(new StringData(\"4\"))\n                .withAppearance(NO_TICKS_APPEARANCE)\n                .build();\n        RangeIntegerWidget widget = createWidget(prompt);\n\n        assertThat(widget.slider.getValueFrom(), equalTo(1.0F));\n        assertThat(widget.slider.getValueTo(), equalTo(10.0F));\n        assertThat(widget.slider.getValue(), equalTo(4.0F));\n        assertThat(widget.slider.isTickVisible(), equalTo(false));\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"4\")));\n        widget.clearAnswer();\n        assertThat(widget.currentValue.getText(), equalTo(\"\"));\n    }\n\n    @Test\n    public void clearAnswer_hidesSliderThumb() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, new StringData(\"2\")));\n        widget.clearAnswer();\n        assertThat(widget.slider.getThumbRadius(), equalTo(0));\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListener() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.clearAnswer();\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void changingSliderValue_showsSliderThumb() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        SliderExtKt.clickOnMinValue(widget.slider);\n        assertThat(widget.slider.getThumbRadius(), not(0));\n    }\n\n    @Test\n    public void changingSliderValue_whenRangeStartIsSmallerThanRangeEnd_updatesAnswer() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        SliderExtKt.clickOnMaxValue(widget.slider);\n        assertThat(widget.currentValue.getText(), equalTo(\"10\"));\n    }\n\n    @Test\n    public void changingSliderValue_whenRangeStartIsGreaterThanRangeEnd_updatesAnswer() {\n        when(rangeQuestion.getRangeStart()).thenReturn(BigDecimal.TEN);\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.ONE);\n\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        SliderExtKt.clickOnMaxValue(widget.slider);\n\n        assertThat(widget.currentValue.getText(), equalTo(\"1\"));\n    }\n\n    @Test\n    public void changingSliderValue_callsValueChangeListener() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        SliderExtKt.clickOnMaxValue(widget.slider);\n\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void changingSliderValueProgramatically_doesNotUpdateAnswer() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        widget.slider.setValue(4);\n        assertThat(widget.currentValue.getText(), equalTo(\"\"));\n    }\n\n    @Test\n    public void changingSliderValueProgramatically_doesNotCallValueChangeListener() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        widget.slider.setValue(4);\n\n        verify(valueChangedListener, never()).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void clickingSliderForLong_doesNotCallLongClickListener() {\n        View.OnLongClickListener listener = mock(View.OnLongClickListener.class);\n\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        widget.setOnLongClickListener(listener);\n        widget.slider.performLongClick();\n\n        verify(listener, never()).onLongClick(widget.slider);\n    }\n\n    @Test // https://github.com/getodk/collect/issues/5530\n    public void everyTriggerWidgetShouldHaveCheckboxWithUniqueID() {\n        RangeIntegerWidget widget1 = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        RangeIntegerWidget widget2 = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n\n        assertThat(widget1.slider.getId(), not(equalTo(widget2.slider.getId())));\n    }\n\n    @Test\n    public void changingSliderValueToTheMinOneWhenSliderHasNoValue_setsTheValueCorrectly() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n\n        SliderExtKt.clickOnMinValue(widget.slider);\n\n        assertThat(widget.currentValue.getText(), equalTo(\"1\"));\n    }\n\n    @Test\n    public void changingSliderValueToAnyOtherThanTheMinOne_setsTheValueCorrectly() {\n        RangeIntegerWidget widget = createWidget(promptWithQuestionDefAndAnswer(rangeQuestion, null));\n\n        SliderExtKt.clickOnMaxValue(widget.slider);\n\n        assertThat(widget.currentValue.getText(), equalTo(\"10\"));\n    }\n\n    private RangeIntegerWidget createWidget(FormEntryPrompt prompt) {\n        RangeIntegerWidget widget = new RangeIntegerWidget(widgetTestActivity(), new QuestionDetails(prompt), widgetDependencies());\n        widget.slider.layout(0, 0, 100, 10);\n        return widget;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/range/RangePickerWidgetTest.kt",
    "content": "package org.odk.collect.android.widgets.range\n\nimport android.view.View.OnLongClickListener\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.nullValue\nimport org.javarosa.core.model.RangeQuestion\nimport org.javarosa.core.model.data.DecimalData\nimport org.javarosa.core.model.data.IntegerData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.formentry.questions.QuestionDetails\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies\nimport java.math.BigDecimal\n\n@RunWith(AndroidJUnit4::class)\nclass RangePickerWidgetTest {\n    private val integerRangeQuestion = RangeQuestion().also {\n        it.rangeStart = BigDecimal(1)\n        it.rangeEnd = BigDecimal(10)\n        it.rangeStep = BigDecimal(1)\n    }\n\n    private val decimalRangeQuestion = RangeQuestion().also {\n        it.rangeStart = BigDecimal(1.5)\n        it.rangeEnd = BigDecimal(5.5)\n        it.rangeStep = BigDecimal(0.5)\n    }\n\n    @Test\n    fun `answer returns null when prompt does not have answer`() {\n        assertThat(\n            createWidget(\n                QuestionWidgetHelpers.promptWithReadOnlyAndQuestionDef(\n                    integerRangeQuestion\n                ),\n                false\n            ).answer,\n            nullValue()\n        )\n    }\n\n    @Test\n    fun `answer returns answer when prompt has answer`() {\n        assertThat(\n            createWidget(\n                QuestionWidgetHelpers.promptWithQuestionDefAndAnswer(\n                    integerRangeQuestion,\n                    IntegerData(4)\n                ),\n                false\n            ).answer!!.value,\n            equalTo(4)\n        )\n    }\n\n    @Test\n    fun `answer returns decimal answer when decimal is true and prompt has answer`() {\n        assertThat(\n            createWidget(\n                QuestionWidgetHelpers.promptWithQuestionDefAndAnswer(\n                    decimalRangeQuestion,\n                    DecimalData(4.0)\n                ),\n                true\n            ).answer!!.value,\n            equalTo(4.0)\n        )\n    }\n\n    @Test\n    fun `clearAnswer clears widget answer`() {\n        val widget = createWidget(\n            QuestionWidgetHelpers.promptWithQuestionDefAndAnswer(\n                integerRangeQuestion,\n                IntegerData(4)\n            ),\n            false\n        )\n        widget.clearAnswer()\n\n        assertThat(widget.answer, nullValue())\n        assertThat(\n            widget.binding.widgetAnswerText.text,\n            equalTo(widget.context.getString(org.odk.collect.strings.R.string.no_value_selected))\n        )\n    }\n\n    @Test\n    fun `clearAnswer calls valueChangeListener`() {\n        val widget = createWidget(\n            QuestionWidgetHelpers.promptWithQuestionDefAndAnswer(integerRangeQuestion, null),\n            false\n        )\n        val valueChangedListener = QuestionWidgetHelpers.mockValueChangedListener(widget)\n        widget.clearAnswer()\n\n        verify(valueChangedListener).widgetValueChanged(widget)\n    }\n\n    @Test\n    fun `clicking widget for long calls longClickListener`() {\n        val listener = mock<OnLongClickListener>()\n        val widget = createWidget(\n            QuestionWidgetHelpers.promptWithQuestionDefAndAnswer(integerRangeQuestion, null),\n            false\n        )\n\n        widget.setOnLongClickListener(listener)\n        widget.binding.widgetButton.performLongClick()\n        widget.binding.widgetAnswerText.performLongClick()\n\n        verify(listener).onLongClick(widget.binding.widgetButton)\n        verify(listener).onLongClick(widget.binding.widgetAnswerText)\n    }\n\n    private fun createWidget(prompt: FormEntryPrompt, decimal: Boolean): RangePickerWidget {\n        return RangePickerWidget(\n            QuestionWidgetHelpers.widgetTestActivity(),\n            QuestionDetails(prompt),\n            widgetDependencies(),\n            decimal\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/range/RangePickerWidgetUtilsTest.kt",
    "content": "package org.odk.collect.android.widgets.range\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.DataType\nimport org.javarosa.core.model.RangeQuestion\nimport org.javarosa.core.model.data.IntegerData\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport java.math.BigDecimal\n\nclass RangePickerWidgetUtilsTest {\n\n    @Test\n    fun `list of numbers should contain only rangeStart when range has equal start and end`() {\n        val rangeQuestion = RangeQuestion()\n        rangeQuestion.rangeStart = BigDecimal(5.0)\n        rangeQuestion.rangeEnd = BigDecimal(5.0)\n        rangeQuestion.rangeStep = BigDecimal(1.0)\n        val prompt = MockFormEntryPromptBuilder()\n            .withQuestion(rangeQuestion)\n            .withDataType(Constants.DATATYPE_DECIMAL)\n            .build()\n\n        assertThat(\n            RangePickerWidgetUtils.getNumbersFromRangeAsc(prompt),\n            equalTo(arrayOf(\"5.0\"))\n        )\n    }\n\n    @Test\n    fun `list of numbers should contain only rangeStart when step is bigger than range`() {\n        val rangeQuestion = RangeQuestion()\n        rangeQuestion.rangeStart = BigDecimal(-5)\n        rangeQuestion.rangeEnd = BigDecimal(5)\n        rangeQuestion.rangeStep = BigDecimal(100)\n        val prompt = MockFormEntryPromptBuilder()\n            .withQuestion(rangeQuestion)\n            .withDataType(Constants.DATATYPE_INTEGER)\n            .build()\n\n        assertThat(\n            RangePickerWidgetUtils.getNumbersFromRangeAsc(prompt),\n            equalTo(arrayOf(\"-5\"))\n        )\n    }\n\n    @Test\n    fun `list of numbers should contain numbers in ascending order when range is increasing`() {\n        val rangeQuestion = RangeQuestion()\n        rangeQuestion.rangeStart = BigDecimal(-5.0)\n        rangeQuestion.rangeEnd = BigDecimal(5.0)\n        rangeQuestion.rangeStep = BigDecimal(1.5)\n        val prompt = MockFormEntryPromptBuilder()\n            .withQuestion(rangeQuestion)\n            .withDataType(Constants.DATATYPE_DECIMAL)\n            .build()\n\n        assertThat(\n            RangePickerWidgetUtils.getNumbersFromRangeAsc(prompt),\n            equalTo(arrayOf(\"-5.0\", \"-3.5\", \"-2.0\", \"-0.5\", \"1.0\", \"2.5\", \"4.0\"))\n        )\n    }\n\n    @Test\n    fun `list of numbers should contain numbers in ascending order when range is decreasing`() {\n        val rangeQuestion = RangeQuestion()\n        rangeQuestion.rangeStart = BigDecimal(5)\n        rangeQuestion.rangeEnd = BigDecimal(-5)\n        rangeQuestion.rangeStep = BigDecimal(1)\n        val prompt = MockFormEntryPromptBuilder()\n            .withQuestion(rangeQuestion)\n            .withDataType(Constants.DATATYPE_INTEGER)\n            .build()\n\n        assertThat(\n            RangePickerWidgetUtils.getNumbersFromRangeAsc(prompt),\n            equalTo(arrayOf(\"-5\", \"-4\", \"-3\", \"-2\", \"-1\", \"0\", \"1\", \"2\", \"3\", \"4\", \"5\"))\n        )\n    }\n\n    @Test\n    fun `list of numbers should contain numbers in ascending order when range is decreasing and step is a negative number`() {\n        val rangeQuestion = RangeQuestion()\n        rangeQuestion.rangeStart = BigDecimal(5.0)\n        rangeQuestion.rangeEnd = BigDecimal(-5.0)\n        rangeQuestion.rangeStep = BigDecimal(-1.5)\n        val prompt = MockFormEntryPromptBuilder()\n            .withQuestion(rangeQuestion)\n            .withDataType(Constants.DATATYPE_DECIMAL)\n            .build()\n\n        assertThat(\n            RangePickerWidgetUtils.getNumbersFromRangeAsc(prompt),\n            equalTo(arrayOf(\"-5.0\", \"-3.5\", \"-2.0\", \"-0.5\", \"1.0\", \"2.5\", \"4.0\"))\n        )\n    }\n\n    @Test\n    fun `list of numbers should contain numbers in ascending order when step is bigger than 1`() {\n        val rangeQuestion = RangeQuestion()\n        rangeQuestion.rangeStart = BigDecimal(-5)\n        rangeQuestion.rangeEnd = BigDecimal(5)\n        rangeQuestion.rangeStep = BigDecimal(2)\n        val prompt = MockFormEntryPromptBuilder()\n            .withQuestion(rangeQuestion)\n            .withDataType(Constants.DATATYPE_INTEGER)\n            .build()\n\n        assertThat(\n            RangePickerWidgetUtils.getNumbersFromRangeAsc(prompt),\n            equalTo(arrayOf(\"-5\", \"-3\", \"-1\", \"1\", \"3\", \"5\"))\n        )\n    }\n\n    @Test\n    fun `getProgressFromPrompt() returns the position of the value in the list`() {\n        val prompt = mock<FormEntryPrompt>().apply {\n            whenever(this.answerValue).thenReturn(IntegerData(2))\n        }\n\n        val listOfValuesAsc = arrayOf(\"1\", \"2\", \"3\", \"4\", \"5\")\n\n        assertThat(\n            RangePickerWidgetUtils.getProgressFromPrompt(prompt, listOfValuesAsc),\n            equalTo(1)\n        )\n    }\n\n    @Test\n    fun `getProgressFromPrompt() returns 0 if the current value does not exist in the list of values`() {\n        val prompt = mock<FormEntryPrompt>().apply {\n            whenever(this.answerValue).thenReturn(IntegerData(10))\n        }\n\n        val listOfValuesAsc = arrayOf(\"1\", \"2\", \"3\", \"4\", \"5\")\n\n        assertThat(\n            RangePickerWidgetUtils.getProgressFromPrompt(prompt, listOfValuesAsc),\n            equalTo(0)\n        )\n    }\n\n    @Test\n    fun `getProgressFromPrompt() returns 0 if the current value is null`() {\n        val prompt = mock<FormEntryPrompt>().apply {\n            whenever(this.answerValue).thenReturn(null)\n        }\n\n        val listOfValuesAsc = arrayOf(\"1\", \"2\", \"3\", \"4\", \"5\")\n\n        assertThat(\n            RangePickerWidgetUtils.getProgressFromPrompt(prompt, listOfValuesAsc),\n            equalTo(0)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/support/FakeQuestionMediaManager.java",
    "content": "package org.odk.collect.android.widgets.support;\n\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.MutableLiveData;\n\nimport com.google.common.io.Files;\n\nimport org.odk.collect.android.utilities.QuestionMediaManager;\nimport org.odk.collect.utilities.Result;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class FakeQuestionMediaManager implements QuestionMediaManager {\n\n    public final List<File> answerFiles = new ArrayList<>();\n    public final Map<String, String> originalFiles = new HashMap<>();\n    public final Map<String, String> recentFiles = new HashMap<>();\n    private final File tempDir = Files.createTempDir();\n\n    @Override\n    public LiveData<Result<File>> createAnswerFile(File file) {\n        File answerFile = addAnswerFile(file);\n        return new MutableLiveData<>(new Result<>(answerFile));\n    }\n\n    @Override\n    public File getAnswerFile(String fileName) {\n        if (fileName == null) {\n            return null;\n        }\n\n        File existing = answerFiles.stream().filter(f -> f.getName().equals(fileName)).findFirst().orElse(null);\n\n        if (existing != null) {\n            return existing;\n        } else {\n            return new File(tempDir, fileName);\n        }\n    }\n\n    @Override\n    public void deleteAnswerFile(String questionIndex, String fileName) {\n        originalFiles.put(questionIndex, fileName);\n    }\n\n    @Override\n    public void replaceAnswerFile(String questionIndex, String fileName) {\n        recentFiles.put(questionIndex, fileName);\n    }\n\n    public File addAnswerFile(File file) {\n        File answerFile = new File(tempDir, file.getName());\n        try {\n            Files.copy(file, answerFile);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        answerFiles.add(answerFile);\n        return answerFile;\n    }\n\n    public File getDir() {\n        return tempDir;\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/support/FakeWaitingForDataRegistry.java",
    "content": "package org.odk.collect.android.widgets.support;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class FakeWaitingForDataRegistry implements WaitingForDataRegistry {\n\n    public List<FormIndex> waiting = new ArrayList<>();\n\n    @Override\n    public void waitForData(FormIndex index) {\n        waiting.add(index);\n    }\n\n    @Override\n    public boolean isWaitingForData(FormIndex index) {\n        return waiting.contains(index);\n    }\n\n    @Override\n    public void cancelWaitingForData() {\n        waiting.clear();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/support/FormElementFixtures.kt",
    "content": "package org.odk.collect.android.widgets.support\n\nimport org.javarosa.core.model.SelectChoice\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.core.model.instance.TreeElement\n\nobject FormElementFixtures {\n\n    fun selectChoice(\n        value: String = \"foo\",\n        index: Int = 0,\n        item: TreeElement? = null\n    ): SelectChoice {\n        return SelectChoice(null, value, false, item, \"label\").also { it.index = index }\n    }\n\n    fun treeElement(\n        name: String = \"\",\n        value: String = \"\"\n    ): TreeElement {\n        return TreeElement(name).also {\n            it.value = StringData(value)\n        }\n    }\n\n    fun treeElement(\n        children: List<TreeElement> = emptyList()\n    ): TreeElement {\n        return TreeElement(\"\").also {\n            children.forEach(it::addChild)\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/support/FormEntryPromptSelectChoiceLoader.kt",
    "content": "package org.odk.collect.android.widgets.support\n\nimport org.javarosa.core.model.SelectChoice\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.odk.collect.android.widgets.interfaces.SelectChoiceLoader\n\nclass FormEntryPromptSelectChoiceLoader : SelectChoiceLoader {\n\n    override fun loadSelectChoices(prompt: FormEntryPrompt): List<SelectChoice> {\n        return prompt.selectChoices\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/support/GeoWidgetHelpers.java",
    "content": "package org.odk.collect.android.widgets.support;\n\nimport java.util.ArrayList;\nimport java.util.Random;\n\npublic final class GeoWidgetHelpers {\n\n    private GeoWidgetHelpers() {\n    }\n\n    public static double[] getRandomDoubleArray() {\n        Random random = new Random();\n        return new double[]{\n                random.nextDouble(),\n                random.nextDouble(),\n                random.nextDouble(),\n                random.nextDouble()\n        };\n    }\n\n    public static String stringFromDoubleList() {\n        StringBuilder b = new StringBuilder();\n        boolean first = true;\n        for (double[] doubles : getRandomDoubleArrayList()) {\n            if (!first) {\n                b.append(';');\n            }\n            first = false;\n            b.append(stringFromDoubles(doubles));\n        }\n        return b.toString();\n    }\n\n    private static ArrayList<double[]> getRandomDoubleArrayList() {\n        Random random = new Random();\n        ArrayList<double[]> doubleList = new ArrayList<>();\n\n        int pointCount = Math.max(1, random.nextInt() % 5);\n        for (int i = 0; i < pointCount; ++i) {\n            doubleList.add(getRandomDoubleArray());\n        }\n\n        return doubleList;\n    }\n\n    private static String stringFromDoubles(double[] doubles) {\n        StringBuilder b = new StringBuilder();\n        for (int i = 0; i < doubles.length; i++) {\n            b.append(doubles[i]);\n            if (i != doubles.length - 1) {\n                b.append(' ');\n            }\n        }\n\n        return b.toString();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt",
    "content": "package org.odk.collect.android.widgets.support\n\nimport androidx.fragment.app.Fragment\nimport org.odk.collect.maps.traces.LineDescription\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.circles.CircleDescription\nimport org.odk.collect.maps.traces.PolygonDescription\nimport org.odk.collect.maps.markers.MarkerDescription\nimport org.odk.collect.maps.markers.MarkerIconDescription\n\nclass NoOpMapFragment : Fragment(), MapFragment {\n\n    override fun init(\n        readyListener: MapFragment.ReadyListener?,\n        errorListener: MapFragment.ErrorListener?\n    ) {\n    }\n\n    override fun getCenter(): MapPoint {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun getZoom(): Double {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun setCenter(center: MapPoint?, animate: Boolean) {\n    }\n\n    override fun zoomToCurrentLocation(center: MapPoint?) {\n    }\n\n    override fun zoomToPoint(center: MapPoint?, animate: Boolean) {\n    }\n\n    override fun zoomToPoint(center: MapPoint?, zoom: Double, animate: Boolean) {\n    }\n\n    override fun zoomToBoundingBox(\n        points: Iterable<MapPoint>,\n        scaleFactor: Double,\n        animate: Boolean\n    ) {\n    }\n\n    override fun addMarker(markerDescription: MarkerDescription): Int {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun updateMarker(\n        featureId: Int,\n        markerDescription: MarkerDescription\n    ) {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun addMarkers(markers: List<MarkerDescription>): MutableList<Int> {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun setMarkerIcon(featureId: Int, markerIconDescription: MarkerIconDescription) {\n    }\n\n    override fun getMarkerPoint(featureId: Int): MapPoint {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun addPolyLine(lineDescription: LineDescription): Int {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun updatePolyLine(\n        featureId: Int,\n        lineDescription: LineDescription\n    ) {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun addPolygon(polygonDescription: PolygonDescription): Int {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun updatePolygon(\n        featureId: Int,\n        polygonDescription: PolygonDescription\n    ) {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun addCircle(circleDescription: CircleDescription): Int {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun updateCircle(\n        featureId: Int,\n        circleDescription: CircleDescription\n    ) {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun getPolyPoints(featureId: Int): MutableList<MapPoint> {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun clearFeatures() {\n    }\n\n    override fun setClickListener(listener: MapFragment.PointListener?) {\n    }\n\n    override fun setLongPressListener(listener: MapFragment.PointListener?) {\n    }\n\n    override fun setFeatureClickListener(listener: MapFragment.FeatureListener?) {\n    }\n\n    override fun setDragEndListener(listener: MapFragment.FeatureListener?) {\n    }\n\n    override fun setGpsLocationEnabled(enabled: Boolean) {\n    }\n\n    override fun getGpsLocation(): MapPoint? {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun setGpsLocationListener(listener: MapFragment.PointListener?) {\n    }\n\n    override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) {\n    }\n\n    override fun hasCenter(): Boolean {\n        return false\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/support/QuestionWidgetHelpers.java",
    "content": "package org.odk.collect.android.widgets.support;\n\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.data.IAnswerData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.odk.collect.android.listeners.WidgetValueChangedListener;\nimport org.odk.collect.android.support.CollectHelpers;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.support.WidgetTestActivity;\nimport org.odk.collect.android.widgets.QuestionWidget;\n\nimport static org.mockito.Mockito.mock;\n\npublic final class QuestionWidgetHelpers {\n\n    private QuestionWidgetHelpers() {\n\n    }\n\n    public static WidgetTestActivity widgetTestActivity() {\n        return CollectHelpers.buildThemedActivity(WidgetTestActivity.class).get();\n    }\n\n    public static QuestionWidget.Dependencies widgetDependencies() {\n        return new QuestionWidget.Dependencies(mock(), mock());\n    }\n\n    public static <T extends QuestionWidget> WidgetValueChangedListener mockValueChangedListener(T widget) {\n        WidgetValueChangedListener valueChangedListener = mock(WidgetValueChangedListener.class);\n        widget.setValueChangedListener(valueChangedListener);\n        return valueChangedListener;\n    }\n\n    public static FormEntryPrompt promptWithAnswer(IAnswerData answer) {\n        return new MockFormEntryPromptBuilder()\n                .withAnswer(answer)\n                .build();\n    }\n\n    public static FormEntryPrompt promptWithReadOnly() {\n        return new MockFormEntryPromptBuilder()\n                .withReadOnly(true)\n                .build();\n    }\n\n    public static FormEntryPrompt promptWithReadOnlyAndAnswer(IAnswerData answer) {\n        return new MockFormEntryPromptBuilder()\n                .withReadOnly(true)\n                .withAnswer(answer)\n                .build();\n    }\n\n    public static FormEntryPrompt promptWithQuestionAndAnswer(QuestionDef questionDef, IAnswerData answer) {\n        return new MockFormEntryPromptBuilder()\n                .withQuestion(questionDef)\n                .withAnswer(answer)\n                .build();\n    }\n\n    public static FormEntryPrompt promptWithQuestionDefAndAnswer(QuestionDef questionDef, IAnswerData answer) {\n        return new MockFormEntryPromptBuilder()\n                .withQuestion(questionDef)\n                .withAnswer(answer)\n                .build();\n    }\n\n    public static FormEntryPrompt promptWithReadOnlyAndQuestionDef(QuestionDef questionDef) {\n        return new MockFormEntryPromptBuilder()\n                .withReadOnly(true)\n                .withQuestion(questionDef)\n                .build();\n    }\n\n    public static FormEntryPrompt promptWithAppearance(String appearance) {\n        return new MockFormEntryPromptBuilder()\n                .withAppearance(appearance)\n                .build();\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/support/SynchronousImageLoader.kt",
    "content": "package org.odk.collect.android.widgets.support\n\nimport android.graphics.BitmapFactory\nimport android.widget.ImageView\nimport org.odk.collect.imageloader.GlideImageLoader\nimport org.odk.collect.imageloader.ImageLoader\nimport java.io.File\n\nclass SynchronousImageLoader(private val fail: Boolean = false) : ImageLoader {\n    override fun loadImage(\n        imageView: ImageView,\n        imageFile: File?,\n        scaleType: ImageView.ScaleType,\n        requestListener: GlideImageLoader.ImageLoaderCallback?\n    ) {\n        if (fail) {\n            requestListener?.onLoadFailed()\n        } else {\n            imageView.setImageBitmap(BitmapFactory.decodeFile(imageFile?.absolutePath))\n            requestListener?.onLoadSucceeded()\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/ActivityGeoDataRequesterTest.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport static junit.framework.TestCase.assertEquals;\nimport static junit.framework.TestCase.assertTrue;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes.LOCATION_CAPTURE;\nimport static org.odk.collect.android.widgets.support.GeoWidgetHelpers.getRandomDoubleArray;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\nimport static org.odk.collect.geo.Constants.EXTRA_DRAGGABLE_ONLY;\nimport static org.odk.collect.geo.Constants.EXTRA_READ_ONLY;\nimport static org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY;\nimport static org.robolectric.Shadows.shadowOf;\nimport static java.util.Arrays.asList;\n\nimport android.content.ComponentName;\nimport android.content.Intent;\nimport android.os.Bundle;\n\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.FormIndex;\nimport org.javarosa.core.model.QuestionDef;\nimport org.javarosa.core.model.data.GeoPointData;\nimport org.javarosa.core.model.instance.TreeElement;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.fakes.FakePermissionsProvider;\nimport org.odk.collect.android.utilities.Appearances;\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry;\nimport org.odk.collect.geo.geopoint.GeoPointActivity;\nimport org.odk.collect.geo.geopoint.GeoPointMapActivity;\nimport org.odk.collect.maps.MapPoint;\nimport org.odk.collect.testshared.MockDialogFragment;\nimport org.odk.collect.testshared.MockFragmentFactory;\nimport org.robolectric.Robolectric;\nimport org.robolectric.shadows.ShadowActivity;\n\n@RunWith(AndroidJUnit4.class)\npublic class ActivityGeoDataRequesterTest {\n\n    private final FakePermissionsProvider permissionsProvider = new FakePermissionsProvider();\n    private final FakeWaitingForDataRegistry waitingForDataRegistry = new FakeWaitingForDataRegistry();\n    private final GeoPointData answer = new GeoPointData(getRandomDoubleArray());\n\n    private FragmentActivity testActivity;\n    private ShadowActivity shadowActivity;\n    private FormEntryPrompt prompt;\n    private FormIndex formIndex;\n    private QuestionDef questionDef;\n\n    private ActivityGeoDataRequester activityGeoDataRequester;\n\n    @Before\n    public void setUp() {\n        testActivity = Robolectric.buildActivity(FragmentActivity.class).setup().get();\n        shadowActivity = shadowOf(testActivity);\n        testActivity.getSupportFragmentManager().setFragmentFactory(new MockFragmentFactory());\n\n        prompt = promptWithAnswer(null);\n        formIndex = mock(FormIndex.class);\n        questionDef = mock(QuestionDef.class);\n\n        permissionsProvider.setPermissionGranted(true);\n        when(prompt.getQuestion()).thenReturn(questionDef);\n        when(prompt.getIndex()).thenReturn(formIndex);\n\n        activityGeoDataRequester = new ActivityGeoDataRequester(permissionsProvider, testActivity);\n    }\n\n    @Test\n    public void whenPermissionIsNotGranted_requestGeoPoint_doesNotLaunchAnyIntent() {\n        permissionsProvider.setPermissionGranted(false);\n        activityGeoDataRequester.requestGeoPoint(prompt, \"\", waitingForDataRegistry);\n\n        assertNull(shadowActivity.getNextStartedActivity());\n        assertTrue(waitingForDataRegistry.waiting.isEmpty());\n    }\n\n    @Test\n    public void whenPermissionIsNotGranted_requestGeoTrace_doesNotOpenDialog() {\n        permissionsProvider.setPermissionGranted(false);\n        activityGeoDataRequester.requestGeoPoly(prompt);\n\n        assertThat(testActivity.getSupportFragmentManager().getFragments().size(), equalTo(0));\n    }\n\n    @Test\n    public void whenPermissionIGranted_requestGeoPoint_setsFormIndexWaitingForData() {\n        activityGeoDataRequester.requestGeoPoint(prompt, \"\", waitingForDataRegistry);\n        assertTrue(waitingForDataRegistry.waiting.contains(formIndex));\n    }\n\n    @Test\n    public void requestGeoPoint_launchesCorrectIntent() {\n        activityGeoDataRequester.requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry);\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class));\n        assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE);\n\n        Bundle bundle = startedIntent.getExtras();\n        assertThat(bundle.getFloat(GeoPointActivity.EXTRA_ACCURACY_THRESHOLD), equalTo(ActivityGeoDataRequester.DEFAULT_ACCURACY_THRESHOLD));\n        assertThat(bundle.getFloat(GeoPointActivity.EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD), equalTo(ActivityGeoDataRequester.DEFAULT_UNACCEPTABLE_ACCURACY_THRESHOLD));\n    }\n\n    @Test\n    public void requestGeoPoint_whenAnswerIsPresent_addsToIntent() {\n        activityGeoDataRequester.requestGeoPoint(prompt, \"1.0 2.0 3 4\", waitingForDataRegistry);\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class));\n        assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE);\n\n        Bundle bundle = startedIntent.getExtras();\n        assertThat(bundle.getParcelable(GeoPointMapActivity.EXTRA_LOCATION), equalTo(new MapPoint(1.0, 2.0, 3, 4)));\n    }\n\n    @Test\n    public void requestGeoPoint_whenAnswerIsPresentButInvalid_doesNotAddToIntent() {\n        activityGeoDataRequester.requestGeoPoint(prompt, \"something\", waitingForDataRegistry);\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class));\n        assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE);\n\n        Bundle bundle = startedIntent.getExtras();\n        assertThat(bundle.getParcelable(GeoPointMapActivity.EXTRA_LOCATION), equalTo(null));\n    }\n\n    @Test\n    public void whenWidgetHasAccuracyValue_requestGeoPoint_launchesCorrectIntent() {\n        when(questionDef.getAdditionalAttribute(null, \"accuracyThreshold\")).thenReturn(\"10\");\n\n        activityGeoDataRequester.requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry);\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class));\n        assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE);\n\n        Bundle bundle = startedIntent.getExtras();\n        assertThat(bundle.getFloat(GeoPointActivity.EXTRA_ACCURACY_THRESHOLD), equalTo(10.0f));\n    }\n\n    @Test\n    public void whenWidgetHasInvalidAccuracyValue_requestGeoPoint_launchesCorrectIntentWithDefaultThreshold() {\n        when(questionDef.getAdditionalAttribute(null, \"accuracyThreshold\")).thenReturn(\"blah\");\n\n        activityGeoDataRequester.requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry);\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class));\n        assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE);\n\n        Bundle bundle = startedIntent.getExtras();\n        assertThat(bundle.getFloat(GeoPointActivity.EXTRA_ACCURACY_THRESHOLD), equalTo(ActivityGeoDataRequester.DEFAULT_ACCURACY_THRESHOLD));\n    }\n\n    @Test\n    public void whenWidgetHasUnacceptableAccuracyValue_requestGeoPoint_launchesCorrectIntent() {\n        when(questionDef.getAdditionalAttribute(null, \"unacceptableAccuracyThreshold\")).thenReturn(\"20\");\n\n        activityGeoDataRequester.requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry);\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class));\n        assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE);\n\n        Bundle bundle = startedIntent.getExtras();\n        assertThat(bundle.getFloat(GeoPointActivity.EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD), equalTo(20.0f));\n    }\n\n    @Test\n    public void whenWidgetHasInvalidUnacceptableAccuracyValue_requestGeoPoint_launchesCorrectIntentWithDefaultThreshold() {\n        when(questionDef.getAdditionalAttribute(null, \"unacceptableAccuracyThreshold\")).thenReturn(\"blah\");\n\n        activityGeoDataRequester.requestGeoPoint(prompt, answer.getDisplayText(), waitingForDataRegistry);\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class));\n        assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE);\n\n        Bundle bundle = startedIntent.getExtras();\n        assertThat(bundle.getFloat(GeoPointActivity.EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD), equalTo(ActivityGeoDataRequester.DEFAULT_UNACCEPTABLE_ACCURACY_THRESHOLD));\n    }\n\n    @Test\n    public void whenWidgetHasMapsAppearance_requestGeoPoint_launchesCorrectIntent() {\n        when(prompt.getAppearanceHint()).thenReturn(Appearances.MAPS);\n\n        activityGeoDataRequester.requestGeoPoint(prompt, \"\", waitingForDataRegistry);\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointMapActivity.class));\n        assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE);\n\n        Bundle bundle = startedIntent.getExtras();\n        assertThat(bundle.getDoubleArray(GeoPointMapActivity.EXTRA_LOCATION), equalTo(null));\n        assertThat(bundle.getBoolean(EXTRA_READ_ONLY), equalTo(false));\n        assertThat(bundle.getBoolean(EXTRA_DRAGGABLE_ONLY), equalTo((Object) false));\n    }\n\n    @Test\n    public void whenWidgetHasMapsAppearance_andIsReadOnly_requestGeoPoint_launchesCorrectIntent() {\n        when(prompt.getAppearanceHint()).thenReturn(Appearances.MAPS);\n\n        when(prompt.isReadOnly()).thenReturn(true);\n        activityGeoDataRequester.requestGeoPoint(prompt, \"\", waitingForDataRegistry);\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointMapActivity.class));\n        assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE);\n\n        Bundle bundle = startedIntent.getExtras();\n        assertThat(bundle.getDoubleArray(GeoPointMapActivity.EXTRA_LOCATION), equalTo(null));\n        assertThat(bundle.getBoolean(EXTRA_READ_ONLY), equalTo(true));\n        assertThat(bundle.getBoolean(EXTRA_DRAGGABLE_ONLY), equalTo((Object) false));\n    }\n\n    @Test\n    public void whenWidgetHasPlacementMapAppearance_requestGeoPoint_launchesCorrectIntent() {\n        when(prompt.getAppearanceHint()).thenReturn(Appearances.PLACEMENT_MAP);\n\n        activityGeoDataRequester.requestGeoPoint(prompt, \"\", waitingForDataRegistry);\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointMapActivity.class));\n        assertEquals(shadowActivity.getNextStartedActivityForResult().requestCode, LOCATION_CAPTURE);\n\n        Bundle bundle = startedIntent.getExtras();\n        assertThat(bundle.getDoubleArray(GeoPointMapActivity.EXTRA_LOCATION), equalTo(null));\n        assertThat(bundle.getBoolean(EXTRA_READ_ONLY), equalTo(false));\n        assertThat(bundle.getBoolean(EXTRA_DRAGGABLE_ONLY), equalTo((Object) true));\n    }\n\n    @Test\n    public void requestGeoTrace_opensDialog() {\n        activityGeoDataRequester.requestGeoPoly(prompt);\n        MockDialogFragment mockFragment = (MockDialogFragment) testActivity.getSupportFragmentManager().getFragments().get(0);\n        assertThat(mockFragment.getFragmentClass(), equalTo(GeoPolyDialogFragment.class));\n        assertThat(mockFragment.requireArguments().getSerializable(WidgetAnswerDialogFragment.ARG_FORM_INDEX), equalTo(prompt.getIndex()));\n    }\n\n    @Test\n    public void requestGeoPoint_whenWidgetHasAllowMockAccuracy_addsItToIntent() {\n        when(prompt.getBindAttributes())\n                .thenReturn(asList(TreeElement.constructAttributeElement(\"odk\", \"allow-mock-accuracy\", \"true\")));\n\n        activityGeoDataRequester.requestGeoPoint(prompt, \"1.0 2.0 3 4\", waitingForDataRegistry);\n\n        Intent startedIntent = shadowActivity.getNextStartedActivity();\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class));\n        assertTrue(startedIntent.getBooleanExtra(EXTRA_RETAIN_MOCK_ACCURACY, false));\n\n        when(prompt.getBindAttributes())\n                .thenReturn(asList(TreeElement.constructAttributeElement(\"odk\", \"allow-mock-accuracy\", \"false\")));\n\n        activityGeoDataRequester.requestGeoPoint(prompt, \"1.0 2.0 3 4\", waitingForDataRegistry);\n\n        startedIntent = shadowActivity.getNextStartedActivity();\n        assertEquals(startedIntent.getComponent(), new ComponentName(testActivity, GeoPointActivity.class));\n        assertFalse(startedIntent.getBooleanExtra(EXTRA_RETAIN_MOCK_ACCURACY, true));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/AudioRecorderRecordingStatusHandlerTest.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport android.util.Pair;\n\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.formentry.FormEntryViewModel;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.odk.collect.audiorecorder.recording.RecordingSession;\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData;\nimport org.odk.collect.androidtest.FakeLifecycleOwner;\n\nimport java.util.function.Consumer;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\n\n@RunWith(AndroidJUnit4.class)\npublic class AudioRecorderRecordingStatusHandlerTest {\n\n    private final AudioRecorder audioRecorder = mock(AudioRecorder.class);\n    private final FormEntryViewModel formEntryViewModel = mock(FormEntryViewModel.class);\n\n    AudioRecorderRecordingStatusHandler provider;\n\n    @Before\n    public void setup() {\n        when(formEntryViewModel.hasBackgroundRecording()).thenReturn(new MutableNonNullLiveData<>(false));\n        provider = new AudioRecorderRecordingStatusHandler(audioRecorder, formEntryViewModel, new FakeLifecycleOwner());\n    }\n\n    @Test\n    public void onIsRecordingChangedBlocked_listensToCurrentSession() {\n        MutableLiveData<RecordingSession> liveData = new MutableLiveData<>(null);\n        when(audioRecorder.getCurrentSession()).thenReturn(liveData);\n\n        Consumer<Boolean> listener = mock(Consumer.class);\n        provider.onBlockedStatusChange(listener);\n        verify(listener).accept(false);\n\n        liveData.setValue(new RecordingSession(\"blah\", null, 0, 0, false));\n        verify(listener).accept(true);\n    }\n\n    @Test\n    public void onIsRecordingChangedBlocked_whenFormHasBackgroundAudio_isAlwaysTrue() {\n        when(formEntryViewModel.hasBackgroundRecording()).thenReturn(new MutableNonNullLiveData<>(true));\n\n        MutableLiveData<RecordingSession> liveData = new MutableLiveData<>(null);\n        when(audioRecorder.getCurrentSession()).thenReturn(liveData);\n\n        Consumer<Boolean> listener = mock(Consumer.class);\n        provider.onBlockedStatusChange(listener);\n        verify(listener).accept(true);\n    }\n\n    @Test\n    public void whenViewModelSessionUpdates_callsInProgressListener() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        MutableLiveData<RecordingSession> sessionLiveData = new MutableLiveData<>(null);\n        when(audioRecorder.getCurrentSession()).thenReturn(sessionLiveData);\n\n        Consumer<Pair<Long, Integer>> listener = mock(Consumer.class);\n        provider.onRecordingStatusChange(prompt, listener);\n        verify(listener).accept(null);\n\n        sessionLiveData.setValue(new RecordingSession(prompt.getIndex(), null, 1200L, 25, false));\n        verify(listener).accept(new Pair<>(1200L, 25));\n    }\n\n    @Test\n    public void whenViewModelSessionUpdates_forDifferentSession_callsInProgressListenerWithNull() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        MutableLiveData<RecordingSession> sessionLiveData = new MutableLiveData<>(null);\n        when(audioRecorder.getCurrentSession()).thenReturn(sessionLiveData);\n\n        Consumer<Pair<Long, Integer>> listener = mock(Consumer.class);\n        provider.onRecordingStatusChange(prompt, listener);\n        verify(listener).accept(null);\n\n        sessionLiveData.setValue(new RecordingSession(\"something else\", null, 1200L, 0, false));\n        verify(listener, times(2)).accept(null);\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/ExternalAppRecordingRequesterTest.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.app.Activity\nimport android.provider.MediaStore\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.android.fakes.FakePermissionsProvider\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidshared.system.IntentLauncherImpl\nimport org.odk.collect.testshared.ErrorIntentLauncher\nimport org.robolectric.Robolectric\nimport org.robolectric.Shadows\nimport org.robolectric.shadows.ShadowToast\n\n@RunWith(AndroidJUnit4::class)\nclass ExternalAppRecordingRequesterTest {\n    private lateinit var intentLauncher: IntentLauncher\n    private val permissionsProvider = FakePermissionsProvider()\n    private val waitingForDataRegistry = FakeWaitingForDataRegistry()\n    private lateinit var activity: Activity\n    private lateinit var requester: ExternalAppRecordingRequester\n\n    @Before\n    fun setup() {\n        activity = Robolectric.buildActivity(Activity::class.java).get()\n    }\n\n    private fun setupRequester() {\n        requester = ExternalAppRecordingRequester(activity, intentLauncher, waitingForDataRegistry, permissionsProvider)\n    }\n\n    @Test\n    fun requestRecording_whenIntentIsNotAvailable_doesNotStartAnyIntentAndCancelsWaitingForData() {\n        intentLauncher = ErrorIntentLauncher()\n        setupRequester()\n        permissionsProvider.setPermissionGranted(true)\n        requester.requestRecording(QuestionWidgetHelpers.promptWithAnswer(null))\n        val startedActivity = Shadows.shadowOf(activity).nextStartedActivity\n        val toastMessage = ShadowToast.getTextOfLatestToast()\n        assertThat(startedActivity, nullValue())\n        assertThat(waitingForDataRegistry.waiting.isEmpty(), `is`(true))\n        assertThat(\n            toastMessage,\n            equalTo(\n                activity.getString(\n                    org.odk.collect.strings.R.string.activity_not_found,\n                    activity.getString(org.odk.collect.strings.R.string.capture_audio)\n                )\n            )\n        )\n    }\n\n    @Test\n    fun requestRecording_whenPermissionIsNotGranted_doesNotStartAnyIntentAndCancelsWaitingForData() {\n        intentLauncher = IntentLauncherImpl\n        setupRequester()\n        permissionsProvider.setPermissionGranted(false)\n        requester.requestRecording(QuestionWidgetHelpers.promptWithAnswer(null))\n        val startedActivity = Shadows.shadowOf(activity).nextStartedActivity\n        assertThat(startedActivity, nullValue())\n        assertThat(waitingForDataRegistry.waiting.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun requestRecording_whenPermissionIsGranted_startsRecordSoundIntentAndSetsWidgetWaitingForData() {\n        intentLauncher = IntentLauncherImpl\n        setupRequester()\n        permissionsProvider.setPermissionGranted(true)\n        val prompt = QuestionWidgetHelpers.promptWithAnswer(null)\n        requester.requestRecording(prompt)\n        val startedActivity = Shadows.shadowOf(activity).nextStartedActivity\n        assertThat(\n            startedActivity.action,\n            equalTo(MediaStore.Audio.Media.RECORD_SOUND_ACTION)\n        )\n        assertThat(\n            startedActivity.getStringExtra(MediaStore.EXTRA_OUTPUT),\n            equalTo(\n                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI\n                    .toString()\n            )\n        )\n        val intentForResult = Shadows.shadowOf(activity).nextStartedActivityForResult\n        assertThat(\n            intentForResult.requestCode,\n            equalTo(ApplicationConstants.RequestCodes.AUDIO_CAPTURE)\n        )\n        assertThat(\n            waitingForDataRegistry.waiting.contains(prompt.index),\n            equalTo(true)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/FileRequesterImplTest.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.spy\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.utilities.ExternalAppIntentProvider\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.robolectric.Robolectric\nimport org.robolectric.shadows.ShadowToast\nimport java.lang.Exception\n\n@RunWith(AndroidJUnit4::class)\nclass FileRequesterImplTest {\n    private val intentLauncher = spy(FakeIntentLauncher())\n    private val requestCode = 99\n    private val externalAppIntentProvider = mock<ExternalAppIntentProvider>()\n    private val formEntryPrompt = mock<FormEntryPrompt>()\n    private val availableIntent = Intent()\n    private val unAvailableIntent = Intent().also {\n        it.putExtra(\"fail\", \"fail\")\n    }\n\n    private val formController: FormController = mock()\n\n    private lateinit var activity: Activity\n    private lateinit var fileRequester: FileRequester\n\n    @Before\n    fun setup() {\n        activity = Robolectric.buildActivity(Activity::class.java).get()\n        fileRequester = FileRequesterImpl(intentLauncher, externalAppIntentProvider, formController)\n    }\n\n    @Test\n    fun `When exception is thrown by ExternalAppIntentProvider#getIntentToRunExternalApp a toast should be displayed`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).then {\n            throw Exception(\"exception\")\n        }\n        fileRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt\n        )\n        val toastText = ShadowToast.getTextOfLatestToast()\n        assertThat(toastText, `is`(\"exception\"))\n    }\n\n    @Test\n    fun `When exception is thrown by ExternalAppIntentProvider#getIntentToRunExternalAppWithoutDefaultCategory a toast should be displayed`() {\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).then {\n            throw Exception(\"exception\")\n        }\n        fileRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt\n        )\n        val toastText = ShadowToast.getTextOfLatestToast()\n        assertThat(toastText, `is`(\"exception\"))\n    }\n\n    @Test\n    fun `When error is thrown by ExternalAppIntentProvider#getIntentToRunExternalApp a toast should be displayed`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).then {\n            throw Exception(\"error\")\n        }\n        fileRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt\n        )\n        val toastText = ShadowToast.getTextOfLatestToast()\n        assertThat(toastText, `is`(\"error\"))\n    }\n\n    @Test\n    fun `When error is thrown by ExternalAppIntentProvider#getIntentToRunExternalAppWithoutDefaultCategory a toast should be displayed`() {\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).then {\n            throw Exception(\"error\")\n        }\n        fileRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt\n        )\n        val toastText = ShadowToast.getTextOfLatestToast()\n        assertThat(toastText, `is`(\"error\"))\n    }\n\n    @Test\n    fun `If the first attempt to start activity succeeded nothing else should happen`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            availableIntent\n        )\n\n        fileRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt\n        )\n        assertThat(intentLauncher.callCounter, `is`(1))\n        assertThat(intentLauncher.errorCounter, `is`(0))\n    }\n\n    @Test\n    fun `If the first attempt to start activity failed there should be another one`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            unAvailableIntent\n        )\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).thenReturn(availableIntent)\n\n        fileRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt\n        )\n\n        assertThat(intentLauncher.callCounter, `is`(2))\n        assertThat(intentLauncher.errorCounter, `is`(1))\n    }\n\n    @Test\n    fun `If both attempts to start activity failed a toast should be displayed`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            unAvailableIntent\n        )\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).thenReturn(unAvailableIntent)\n\n        fileRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt\n        )\n\n        assertThat(intentLauncher.callCounter, `is`(2))\n        assertThat(intentLauncher.errorCounter, `is`(2))\n\n        val toastText = ShadowToast.getTextOfLatestToast()\n        assertThat(toastText, `is`(activity.getString(org.odk.collect.strings.R.string.no_app)))\n    }\n\n    @Test\n    fun `If both attempts to start activity failed a toast with custom message should be displayed if it is set`() {\n        whenever(formEntryPrompt.getSpecialFormQuestionText(\"noAppErrorString\")).thenReturn(\"Custom message\")\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            unAvailableIntent\n        )\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).thenReturn(unAvailableIntent)\n\n        fileRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt\n        )\n\n        val toastText = ShadowToast.getTextOfLatestToast()\n        assertThat(toastText, `is`(\"Custom message\"))\n    }\n\n    class FakeIntentLauncher : IntentLauncher {\n        var callCounter = 0\n        var errorCounter = 0\n\n        override fun launch(context: Context, intent: Intent?, onError: () -> Unit) {\n        }\n\n        override fun launchForResult(\n            activity: Activity,\n            intent: Intent?,\n            requestCode: Int,\n            onError: () -> Unit\n        ) {\n            callCounter++\n            if (intent!!.hasExtra(\"fail\")) {\n                errorCounter++\n                onError()\n            }\n        }\n\n        override fun launchForResult(\n            resultLauncher: ActivityResultLauncher<Intent>,\n            intent: Intent?,\n            onError: () -> Unit\n        ) {\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/GeoPolyDialogFragmentTest.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.R\nimport android.os.Bundle\nimport androidx.core.os.bundleOf\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.viewmodel.CreationExtras\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.Constants\nimport org.javarosa.core.model.data.GeoShapeData\nimport org.javarosa.core.model.data.GeoTraceData\nimport org.javarosa.form.api.FormEntryController\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.verify\nimport org.odk.collect.android.formentry.FormEntryViewModel\nimport org.odk.collect.android.javarosawrapper.FailedValidationResult\nimport org.odk.collect.android.javarosawrapper.SuccessValidationResult\nimport org.odk.collect.android.javarosawrapper.ValidationResult\nimport org.odk.collect.android.support.CollectHelpers\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.android.widgets.utilities.AdditionalAttributes.INCREMENTAL\nimport org.odk.collect.android.widgets.utilities.WidgetAnswerDialogFragment.Companion.ARG_FORM_INDEX\nimport org.odk.collect.android.widgets.viewmodels.QuestionViewModel\nimport org.odk.collect.androidshared.ui.DisplayString\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.geo.geopoly.GeoPolyFragment\nimport org.odk.collect.geo.geopoly.GeoPolyFragment.OutputMode\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.testshared.getOrAwaitValue\n\n@RunWith(AndroidJUnit4::class)\nclass GeoPolyDialogFragmentTest {\n\n    private var prompt = MockFormEntryPromptBuilder().build()\n    private val constraintValidationResult = MutableLiveData<ValidationResult>(SuccessValidationResult)\n    private val formEntryViewModel = mock<FormEntryViewModel> {\n        on { getQuestionPrompt(prompt.index) } doReturn prompt\n    }\n\n    private val questionViewModel = mock<QuestionViewModel> {\n        on { constraintValidationResult } doReturn constraintValidationResult\n    }\n\n    private val viewModelFactory = object : ViewModelProvider.Factory {\n        override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {\n            return when (modelClass) {\n                FormEntryViewModel::class.java -> formEntryViewModel as T\n                QuestionViewModel::class.java -> questionViewModel as T\n                else -> throw IllegalArgumentException()\n            }\n        }\n    }\n\n    @get:Rule\n    val launcherRule =\n        FragmentScenarioLauncherRule(\n            FragmentFactoryBuilder()\n                .forClass(GeoPolyDialogFragment::class) {\n                    GeoPolyDialogFragment(viewModelFactory)\n                }.build()\n        )\n\n    @Before\n    fun setup() {\n        CollectHelpers.setupDemoProject()\n    }\n\n    @Test\n    fun `configures GeoPolyFragment with readOnly from prompt`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .withReadOnly(true)\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.readOnly, equalTo(true))\n        }\n\n        prompt = MockFormEntryPromptBuilder(prompt).withReadOnly(false).build()\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.readOnly, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `configures GeoPolyFragment with geoshape output mode when prompt is geoshape`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOSHAPE)\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.outputMode, equalTo(OutputMode.GEOSHAPE))\n        }\n    }\n\n    @Test\n    fun `configures GeoPolyFragment with geotrace output mode when prompt is geotrace`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.outputMode, equalTo(OutputMode.GEOTRACE))\n        }\n    }\n\n    @Test(expected = IllegalArgumentException::class)\n    fun `throws exception when prompt is something else`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_DATE)\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.outputMode, equalTo(OutputMode.GEOTRACE))\n        }\n    }\n\n    @Test\n    fun `configures GeoPolyFragment with retainMockAccruacy from allow-mock-accuracy bind attribute`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.retainMockAccuracy, equalTo(false))\n        }\n\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withBindAttribute(null, \"allow-mock-accuracy\", \"true\")\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.retainMockAccuracy, equalTo(true))\n        }\n\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withBindAttribute(null, \"allow-mock-accuracy\", \"false\")\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.retainMockAccuracy, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `configures GeoPolyFragment inputPolygon with existing answer`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.inputPolygon, equalTo(emptyList()))\n        }\n\n        val points = listOf(MapPoint(0.0, 0.0, 1.0, 1.0), MapPoint(0.0, 1.0, 1.0, 1.0))\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .withAnswer(geoTraceOf(points))\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.inputPolygon, equalTo(points))\n        }\n\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOSHAPE)\n            .withAnswer(geoShapeOf(points))\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.inputPolygon, equalTo(points))\n        }\n    }\n\n    @Test\n    fun `sets null answer when REQUEST_GEOPOLY with empty value is returned for GEOTRACE prompt`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        val answer = \"\"\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY to answer)\n            )\n        }\n\n        verify(formEntryViewModel).answerQuestion(prompt.index, null)\n    }\n\n    @Test\n    fun `sets GeoTraceData answer when REQUEST_GEOPOLY is returned for GEOTRACE prompt`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        val answer = \"0.0 0.0 1.0 1.0; 0.0 1.0 1.0 1.0\"\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY to answer)\n            )\n        }\n\n        verify(formEntryViewModel).answerQuestion(prompt.index, geoTraceOf(answer))\n    }\n\n    @Test\n    fun `sets null answer when REQUEST_GEOPOLY with empty value is returned for GEOSHAPE prompt`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOSHAPE)\n            .build()\n\n        val answer = \"\"\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY to answer)\n            )\n        }\n\n        verify(formEntryViewModel).answerQuestion(prompt.index, null)\n    }\n\n    @Test\n    fun `sets GeoShapeData answer when REQUEST_GEOPOLY is returned for GEOSHAPE prompt`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOSHAPE)\n            .build()\n\n        val answer = \"0.0 0.0 1.0 1.0; 0.0 1.0 1.0 1.0; 1.0 1.0 0.0 0.0\"\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY to answer)\n            )\n        }\n\n        verify(formEntryViewModel).answerQuestion(prompt.index, geoShapeOf(answer))\n    }\n\n    @Test\n    fun `dismisses when REQUEST_GEOPOLY is returned`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        val answer = \"0.0 0.0 1.0 1.0; 0.0 1.0 1.0 1.0\"\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            assertThat(it.dialog!!.isShowing, equalTo(true))\n\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY to answer)\n            )\n\n            assertThat(it.dialog!!.isShowing, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `validate GeoTraceData answer when REQUEST_GEOPOLY_CHANGE is returned if question is incremental`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .withAdditionalAttribute(INCREMENTAL, \"true\")\n            .build()\n\n        val answer = \"0.0 0.0 1.0 1.0; 0.0 1.0 1.0 1.0\"\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY_CHANGE to answer)\n            )\n        }\n\n        verify(questionViewModel).validate(prompt.index, geoTraceOf(answer))\n    }\n\n    @Test\n    fun `validate GeoShapeData answer when REQUEST_GEOPOLY_CHANGE is returned if GEOSHAPE question is incremental`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withAdditionalAttribute(INCREMENTAL, \"true\")\n            .withDataType(Constants.DATATYPE_GEOSHAPE)\n            .build()\n\n        val answer = \"0.0 0.0 1.0 1.0; 0.0 1.0 1.0 1.0\"\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY_CHANGE to answer)\n            )\n        }\n\n        verify(questionViewModel).validate(prompt.index, geoShapeOf(answer))\n    }\n\n    @Test\n    fun `does not validate answer when REQUEST_GEOPOLY_CHANGE is returned if question is not incremental`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        val answer = \"0.0 0.0 1.0 1.0; 0.0 1.0 1.0 1.0\"\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY_CHANGE to answer)\n            )\n        }\n\n        verify(formEntryViewModel, never()).answerQuestion(prompt.index, geoTraceOf(answer))\n\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withAdditionalAttribute(INCREMENTAL, \"false\")\n            .build()\n\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY_CHANGE to answer)\n            )\n        }\n\n        verify(questionViewModel, never()).validate(prompt.index, geoTraceOf(answer))\n    }\n\n    @Test\n    fun `does not dismiss when REQUEST_GEOPOLY_CHANGE is returned regardless of incremental value`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .withAdditionalAttribute(INCREMENTAL, \"true\")\n            .build()\n\n        val answer = \"0.0 0.0 1.0 1.0; 0.0 1.0 1.0 1.0\"\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            assertThat(it.dialog!!.isShowing, equalTo(true))\n\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY_CHANGE to answer)\n            )\n\n            assertThat(it.dialog!!.isShowing, equalTo(true))\n        }\n\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withAdditionalAttribute(INCREMENTAL, \"false\")\n            .build()\n\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(\n                GeoPolyFragment.REQUEST_GEOPOLY,\n                bundleOf(GeoPolyFragment.RESULT_GEOPOLY_CHANGE to answer)\n            )\n\n            assertThat(it.dialog!!.isShowing, equalTo(true))\n        }\n    }\n\n    @Test\n    fun `does not set answer when REQUEST_GEOPOLY is cancelled`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(GeoPolyFragment.REQUEST_GEOPOLY, Bundle.EMPTY)\n        }\n\n        verify(formEntryViewModel, never()).answerQuestion(any(), any())\n    }\n\n    @Test\n    fun `dismisses when REQUEST_GEOPOLY is cancelled`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ).onFragment {\n            it.childFragmentManager.setFragmentResult(GeoPolyFragment.REQUEST_GEOPOLY, Bundle.EMPTY)\n            assertThat(it.dialog!!.isShowing, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `uses validation result message for invalidMessage`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.invalidMessage.getOrAwaitValue(), equalTo(null))\n        }\n\n        constraintValidationResult.value = FailedValidationResult(prompt.index, FormEntryController.ANSWER_CONSTRAINT_VIOLATED, \"blah\", 0)\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.invalidMessage.getOrAwaitValue(), equalTo(DisplayString.Raw(\"blah\")))\n        }\n    }\n\n    @Test\n    fun `uses validation result default message for invalidMessage if there's no custom message`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.invalidMessage.getOrAwaitValue(), equalTo(null))\n        }\n\n        constraintValidationResult.value =\n            FailedValidationResult(prompt.index, FormEntryController.ANSWER_CONSTRAINT_VIOLATED, null, R.string.cancel)\n        launcherRule.launchAndAssertOnChild<GeoPolyFragment>(\n            GeoPolyDialogFragment::class,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        ) {\n            assertThat(it.invalidMessage.getOrAwaitValue(), equalTo(DisplayString.Resource(R.string.cancel)))\n        }\n    }\n\n    /**\n     * This scenario can cause a crash if there's a [androidx.fragment.app.Fragment.getString]\n     * call in the evaluation chain for however [GeoPolyDialogFragment] or its children deal with\n     * the invalid message [LiveData].\n     */\n    @Test\n    fun `recreating and setting a default failed constraint does not crash`() {\n        prompt = MockFormEntryPromptBuilder(prompt)\n            .withDataType(Constants.DATATYPE_GEOTRACE)\n            .build()\n\n        val scenario = launcherRule.launch(\n            GeoPolyDialogFragment::class.java,\n            bundleOf(ARG_FORM_INDEX to prompt.index)\n        )\n\n        scenario.recreate()\n        constraintValidationResult.value =\n            FailedValidationResult(prompt.index, FormEntryController.ANSWER_CONSTRAINT_VIOLATED, null, R.string.cancel)\n    }\n\n    private fun geoTraceOf(points: List<MapPoint>): GeoTraceData {\n        return GeoTraceData(\n            GeoTraceData.GeoTrace(\n                ArrayList(\n                    points.map {\n                        doubleArrayOf(\n                            it.latitude,\n                            it.longitude,\n                            it.altitude,\n                            it.accuracy\n                        )\n                    }\n                )\n            )\n        )\n    }\n\n    private fun geoShapeOf(points: List<MapPoint>): GeoShapeData {\n        return GeoShapeData(\n            GeoShapeData.GeoShape(\n                ArrayList(\n                    points.map {\n                        doubleArrayOf(\n                            it.latitude,\n                            it.longitude,\n                            it.altitude,\n                            it.accuracy\n                        )\n                    }\n                )\n            )\n        )\n    }\n\n    private fun geoTraceOf(answer: String): GeoTraceData {\n        return GeoTraceData().also { it.value = answer }\n    }\n\n    private fun geoShapeOf(answer: String): GeoShapeData {\n        return GeoShapeData().also { it.value = answer }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/GeoWidgetUtilsTest.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport junit.framework.TestCase.assertEquals\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.data.GeoPointData\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.widgets.support.GeoWidgetHelpers\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils.convertCoordinatesIntoDegreeFormat\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils.floor\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils.getGeoPointAnswerToDisplay\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils.getGeoPolyAnswerToDisplay\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils.isWithinMapBounds\nimport org.odk.collect.android.widgets.utilities.GeoWidgetUtils.truncateDouble\nimport org.odk.collect.maps.MapPoint\n\n@RunWith(AndroidJUnit4::class)\nclass GeoWidgetUtilsTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n    private val answer = GeoPointData(GeoWidgetHelpers.getRandomDoubleArray())\n\n    @Test\n    fun getAnswerToDisplay_whenAnswerIsNull_returnsEmptyString() {\n        assertEquals(getGeoPointAnswerToDisplay(context, null), \"\")\n    }\n\n    @Test\n    fun getAnswerToDisplay_whenAnswerIsNotConvertible_returnsEmptyString() {\n        assertEquals(getGeoPointAnswerToDisplay(context, \"blah\"), \"\")\n    }\n\n    @Test\n    fun getAnswerToDisplay_whenAnswerIsNotNullAndConvertible_returnsAnswer() {\n        val stringAnswer = answer.displayText\n        val parts = stringAnswer.split(\" \").toTypedArray()\n        assertEquals(\n            getGeoPointAnswerToDisplay(context, stringAnswer),\n            context.getString(\n                org.odk.collect.strings.R.string.gps_result,\n                convertCoordinatesIntoDegreeFormat(context, parts[0].toDouble(), \"lat\"),\n                convertCoordinatesIntoDegreeFormat(context, parts[1].toDouble(), \"lon\"),\n                truncateDouble(parts[2]),\n                truncateDouble(parts[3])\n            )\n        )\n    }\n\n    @Test // Results confirmed with https://www.sunearthtools.com/dp/tools/conversion.php\n    fun convertCoordinatesIntoDegreeFormatTest() {\n        assertEquals(\n            \"N 37°27'5\\\"\",\n            convertCoordinatesIntoDegreeFormat(context, 37.45153333333334, \"lat\")\n        )\n        assertEquals(\n            \"W 122°9'19\\\"\",\n            convertCoordinatesIntoDegreeFormat(context, -122.15539166666667, \"lon\")\n        )\n        assertEquals(\n            \"N 3°51'4\\\"\",\n            convertCoordinatesIntoDegreeFormat(context, 3.8513583333333337, \"lat\")\n        )\n        assertEquals(\n            \"W 70°2'11\\\"\",\n            convertCoordinatesIntoDegreeFormat(context, -70.03650333333333, \"lon\")\n        )\n        assertEquals(\n            \"S 31°8'40\\\"\",\n            convertCoordinatesIntoDegreeFormat(context, -31.144546666666663, \"lat\")\n        )\n        assertEquals(\n            \"E 138°16'15\\\"\",\n            convertCoordinatesIntoDegreeFormat(context, 138.27083666666667, \"lon\")\n        )\n        assertEquals(\n            \"N 61°23'15\\\"\",\n            convertCoordinatesIntoDegreeFormat(context, 61.38757333333333, \"lat\")\n        )\n        assertEquals(\n            \"W 150°55'37\\\"\",\n            convertCoordinatesIntoDegreeFormat(context, -150.92708666666667, \"lon\")\n        )\n        assertEquals(\"N 0°0'0\\\"\", convertCoordinatesIntoDegreeFormat(context, 0.0, \"lat\"))\n        assertEquals(\"E 0°0'0\\\"\", convertCoordinatesIntoDegreeFormat(context, 0.0, \"lon\"))\n    }\n\n    @Test\n    fun floorTest() {\n        assertEquals(\"5\", floor(\"5\"))\n        assertEquals(\"-5\", floor(\"-5\"))\n        assertEquals(\"5\", floor(\"5.55\"))\n        assertEquals(\"-5\", floor(\"-5.55\"))\n        assertEquals(\"\", floor(\"\"))\n        assertEquals(\"\", floor(null))\n        assertEquals(\"qwerty\", floor(\"qwerty\"))\n    }\n\n    @Test\n    fun truncateDoubleTest() {\n        assertEquals(\"5\", truncateDouble(\"5\"))\n        assertEquals(\"-5\", truncateDouble(\"-5\"))\n        assertEquals(\"5.12\", truncateDouble(\"5.12\"))\n        assertEquals(\"-5.12\", truncateDouble(\"-5.12\"))\n        assertEquals(\"5.12\", truncateDouble(\"5.1234\"))\n        assertEquals(\"-5.12\", truncateDouble(\"-5.1234\"))\n        assertEquals(\"\", truncateDouble(\"\"))\n        assertEquals(\"\", truncateDouble(null))\n        assertEquals(\"\", truncateDouble(\"qwerty\"))\n    }\n\n    @Test\n    fun isWithinMapBoundsTest() {\n        assertThat(isWithinMapBounds(MapPoint(90.0, 0.0, 0.0, 0.0)), equalTo(true))\n        assertThat(isWithinMapBounds(MapPoint(-90.0, 0.0, 0.0, 0.0)), equalTo(true))\n        assertThat(isWithinMapBounds(MapPoint(0.0, 180.0, 0.0, 0.0)), equalTo(true))\n        assertThat(isWithinMapBounds(MapPoint(0.0, -180.0, 0.0, 0.0)), equalTo(true))\n\n        assertThat(isWithinMapBounds(MapPoint(90.1, 0.0, 0.0, 0.0)), equalTo(false))\n        assertThat(isWithinMapBounds(MapPoint(-90.1, 0.0, 0.0, 0.0)), equalTo(false))\n        assertThat(isWithinMapBounds(MapPoint(0.0, 180.1, 0.0, 0.0)), equalTo(false))\n        assertThat(isWithinMapBounds(MapPoint(0.0, -180.1, 0.0, 0.0)), equalTo(false))\n    }\n\n    @Test\n    fun getGeoPolyAnswerToDisplayTest() {\n        assertThat(getGeoPolyAnswerToDisplay(\"\"), equalTo(\"\"))\n        assertThat(getGeoPolyAnswerToDisplay(\";\"), equalTo(\"\"))\n        assertThat(getGeoPolyAnswerToDisplay(\"; \"), equalTo(\"\"))\n        assertThat(getGeoPolyAnswerToDisplay(\";\\n\"), equalTo(\"\"))\n        assertThat(getGeoPolyAnswerToDisplay(\";\\r\"), equalTo(\"\"))\n        assertThat(getGeoPolyAnswerToDisplay(\"12.0 13.0 5 6;\"), equalTo(\"12.0 13.0 5 6\"))\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/GetContentAudioFileRequesterTest.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.app.Activity\nimport android.content.Intent\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.android.R\nimport org.odk.collect.android.utilities.ApplicationConstants\nimport org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry\nimport org.odk.collect.android.widgets.support.QuestionWidgetHelpers\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.odk.collect.androidshared.system.IntentLauncherImpl\nimport org.odk.collect.testshared.ErrorIntentLauncher\nimport org.robolectric.Robolectric\nimport org.robolectric.Shadows\nimport org.robolectric.shadows.ShadowToast\n\n@RunWith(AndroidJUnit4::class)\nclass GetContentAudioFileRequesterTest {\n    private lateinit var intentLauncher: IntentLauncher\n    private val waitingForDataRegistry = FakeWaitingForDataRegistry()\n    private lateinit var activity: Activity\n    private lateinit var requester: GetContentAudioFileRequester\n\n    @Before\n    fun setup() {\n        activity = Robolectric.buildActivity(Activity::class.java).get()\n    }\n\n    private fun setupRequester() {\n        requester = GetContentAudioFileRequester(activity, intentLauncher, waitingForDataRegistry)\n    }\n\n    @Test\n    fun requestFile_whenIntentIsNotAvailable_doesNotStartAnyIntentAndCancelsWaitingForData() {\n        intentLauncher = ErrorIntentLauncher()\n        setupRequester()\n        requester.requestFile(QuestionWidgetHelpers.promptWithAnswer(null))\n        val startedActivity = Shadows.shadowOf(activity).nextStartedActivity\n        val toastMessage = ShadowToast.getTextOfLatestToast()\n        assertThat(startedActivity, nullValue())\n        assertThat(waitingForDataRegistry.waiting.isEmpty(), equalTo(true))\n        assertThat(\n            toastMessage,\n            equalTo(\n                activity.getString(\n                    org.odk.collect.strings.R.string.activity_not_found,\n                    activity.getString(org.odk.collect.strings.R.string.choose_sound)\n                )\n            )\n        )\n    }\n\n    @Test\n    fun requestFile_startsChooseAudioFileActivityAndSetsWidgetWaitingForData() {\n        intentLauncher = IntentLauncherImpl\n        setupRequester()\n        val prompt = QuestionWidgetHelpers.promptWithAnswer(null)\n        requester.requestFile(prompt)\n        val startedActivity = Shadows.shadowOf(activity).nextStartedActivity\n        assertThat(\n            startedActivity.action,\n            equalTo(Intent.ACTION_GET_CONTENT)\n        )\n        assertThat(startedActivity.type, equalTo(\"audio/*\"))\n        val intentForResult = Shadows.shadowOf(activity).nextStartedActivityForResult\n        assertThat(\n            intentForResult.requestCode,\n            equalTo(ApplicationConstants.RequestCodes.AUDIO_CHOOSER)\n        )\n        assertThat(\n            waitingForDataRegistry.waiting.contains(prompt.index),\n            equalTo(true)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/InternalRecordingRequesterTest.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport androidx.activity.ComponentActivity;\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.fakes.FakePermissionsProvider;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.audiorecorder.recorder.Output;\nimport org.odk.collect.audiorecorder.recording.AudioRecorder;\nimport org.robolectric.Robolectric;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;\n\n@RunWith(AndroidJUnit4.class)\npublic class InternalRecordingRequesterTest {\n\n    private final FakePermissionsProvider permissionsProvider = new FakePermissionsProvider();\n    private final AudioRecorder audioRecorder = mock(AudioRecorder.class);\n\n    private InternalRecordingRequester requester;\n\n    @Before\n    public void setup() {\n        ApplicationProvider.getApplicationContext().setTheme(com.google.android.material.R.style.Theme_MaterialComponents);\n        ComponentActivity activity = Robolectric.buildActivity(ComponentActivity.class).get();\n        when(audioRecorder.getCurrentSession()).thenReturn(new MutableLiveData<>(null));\n\n        requester = new InternalRecordingRequester(activity, audioRecorder, permissionsProvider);\n        permissionsProvider.setPermissionGranted(true);\n    }\n\n    @Test\n    public void requestRecording_startsWithAAC() {\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        requester.requestRecording(prompt);\n\n        verify(audioRecorder).start(prompt.getIndex(), Output.AAC);\n    }\n\n    @Test\n    public void requestRecording_whenPromptQualityIsVoiceOnly_startsWithAMR() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withBindAttribute(\"odk\", \"quality\", \"voice-only\")\n                .build();\n\n        requester.requestRecording(prompt);\n\n        verify(audioRecorder).start(prompt.getIndex(), Output.AMR);\n    }\n\n    @Test\n    public void requestRecording_whenPromptQualityIsLow_startsWithAACLow() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withBindAttribute(\"odk\", \"quality\", \"low\")\n                .build();\n\n        requester.requestRecording(prompt);\n\n        verify(audioRecorder).start(prompt.getIndex(), Output.AAC_LOW);\n    }\n\n    @Test\n    public void requestRecording_whenPermissionDenied_doesNothing() {\n        permissionsProvider.setPermissionGranted(false);\n\n        FormEntryPrompt prompt = promptWithAnswer(null);\n        requester.requestRecording(prompt);\n\n        verify(audioRecorder, never()).start(any(), any());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/RangeWidgetUtilsTest.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithQuestionDefAndAnswer;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnlyAndQuestionDef;\nimport static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;\n\nimport android.content.Context;\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.javarosa.core.model.RangeQuestion;\nimport org.javarosa.core.model.data.IntegerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.databinding.RangePickerWidgetAnswerBinding;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\nimport org.odk.collect.android.views.TrackingTouchSlider;\nimport org.robolectric.shadows.ShadowToast;\n\nimport java.math.BigDecimal;\n\n@RunWith(AndroidJUnit4.class)\npublic class RangeWidgetUtilsTest {\n    private static final String VERTICAL_APPEARANCE = \"vertical\";\n\n    private RangePickerWidgetAnswerBinding binding;\n    private RangeQuestion rangeQuestion;\n    private TrackingTouchSlider slider;\n    private TextView sampleTextView1;\n    private TextView sampleTextView2;\n    private Context context;\n\n    @Before\n    public void setup() {\n        context = ApplicationProvider.getApplicationContext();\n        context.setTheme(com.google.android.material.R.style.Theme_MaterialComponents);\n\n        slider = new TrackingTouchSlider(context, null);\n        sampleTextView1 = new TextView(context);\n        sampleTextView2 = new TextView(context);\n\n        binding = RangePickerWidgetAnswerBinding.inflate((widgetTestActivity()).getLayoutInflater());\n\n        rangeQuestion = mock(RangeQuestion.class);\n        when(rangeQuestion.getRangeStart()).thenReturn(BigDecimal.ONE);\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.TEN);\n        when(rangeQuestion.getRangeStep()).thenReturn(BigDecimal.ONE);\n    }\n\n    @Test\n    public void usingReadOnlyOption_disablesTheSlider() {\n        RangeWidgetUtils.RangeWidgetLayoutElements layoutElements = RangeWidgetUtils.setUpLayoutElements(\n                widgetTestActivity(), promptWithReadOnlyAndQuestionDef(rangeQuestion));\n        assertFalse(layoutElements.getSlider().isEnabled());\n    }\n\n    @Test\n    public void usingReadOnlyOption_hidesPickerButton() {\n        RangeWidgetUtils.setUpRangePickerWidget(widgetTestActivity(), binding, promptWithReadOnlyAndQuestionDef(rangeQuestion));\n        assertThat(binding.widgetButton.getVisibility(), equalTo(View.GONE));\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_answerTextViewShowsNoValueSelected() {\n        RangeWidgetUtils.setUpRangePickerWidget(widgetTestActivity(), binding, promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        assertThat(binding.widgetAnswerText.getText(), equalTo(widgetTestActivity().getString(org.odk.collect.strings.R.string.no_value_selected)));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_answerTextViewShowsCorrectAnswer() {\n        RangeWidgetUtils.setUpRangePickerWidget(widgetTestActivity(), binding, promptWithQuestionDefAndAnswer(\n                rangeQuestion, new StringData(\"4\")));\n        assertThat(binding.widgetAnswerText.getText(), equalTo(\"4\"));\n    }\n\n    @Test\n    public void whenPromptHasAnswerInIncreasingRange_sliderValueShouldBeSet() {\n        FormEntryPrompt prompt = mock(FormEntryPrompt.class);\n        IntegerData answerValue = new IntegerData(7);\n        when(prompt.getAnswerValue()).thenReturn(answerValue);\n        when(prompt.getQuestion()).thenReturn(rangeQuestion);\n\n        BigDecimal value = RangeWidgetUtils.setUpSlider(prompt, slider, true);\n        assertThat(value, equalTo(new BigDecimal(7)));\n        assertThat(slider.getValue(), equalTo(7f));\n    }\n\n    @Test\n    public void whenPromptHasAnswerInDecreasingRange_sliderValueShouldBeSet() {\n        when(rangeQuestion.getRangeStart()).thenReturn(BigDecimal.TEN);\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.ONE);\n        when(rangeQuestion.getRangeStep()).thenReturn(BigDecimal.ONE);\n\n        FormEntryPrompt prompt = mock(FormEntryPrompt.class);\n        IntegerData answerValue = new IntegerData(7);\n        when(prompt.getAnswerValue()).thenReturn(answerValue);\n        when(prompt.getQuestion()).thenReturn(rangeQuestion);\n\n        BigDecimal value = RangeWidgetUtils.setUpSlider(prompt, slider, true);\n        assertThat(value, equalTo(new BigDecimal(7)));\n        assertThat(slider.getValue(), equalTo(4f));\n    }\n\n    @Test\n    public void whenPromptHasAnswerThatIsOutOfRange_sliderValueShouldNotBeSet() {\n        slider.setValue(5);\n\n        // Value bigger than rangeEnd\n        FormEntryPrompt prompt = mock(FormEntryPrompt.class);\n        IntegerData biggerAnswerValue = new IntegerData(rangeQuestion.getRangeEnd().intValue() + 1);\n        when(prompt.getAnswerValue()).thenReturn(biggerAnswerValue);\n        when(prompt.getQuestion()).thenReturn(rangeQuestion);\n\n        BigDecimal value = RangeWidgetUtils.setUpSlider(prompt, slider, true);\n        assertNull(value);\n        assertThat(slider.getValue(), equalTo(5f));\n\n        // Value smaller than rangeStart\n        IntegerData smallerAnswerValue = new IntegerData(rangeQuestion.getRangeStart().intValue() - 1);\n        when(prompt.getAnswerValue()).thenReturn(smallerAnswerValue);\n        when(prompt.getQuestion()).thenReturn(rangeQuestion);\n\n        value = RangeWidgetUtils.setUpSlider(prompt, slider, true);\n        assertNull(value);\n        assertThat(slider.getValue(), equalTo(5f));\n    }\n\n    @Test\n    public void whenPromptDoesNotHaveAnswer_pickerButtonShowsNoValueSelected() {\n        RangeWidgetUtils.setUpRangePickerWidget(widgetTestActivity(), binding, promptWithQuestionDefAndAnswer(rangeQuestion, null));\n        assertThat(binding.widgetButton.getText(), equalTo(widgetTestActivity().getString(org.odk.collect.strings.R.string.select_value)));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_pickerButtonShowsCorrectAnswer() {\n        RangeWidgetUtils.setUpRangePickerWidget(widgetTestActivity(), binding, promptWithQuestionDefAndAnswer(\n                rangeQuestion, new StringData(\"4\")));\n        assertThat(binding.widgetButton.getText(), equalTo(widgetTestActivity().getString(org.odk.collect.strings.R.string.edit_value)));\n    }\n\n    @Test\n    public void setNumberPickerValue_updatesAnswer() {\n        RangeWidgetUtils.getNumberPickerProgress(binding, rangeQuestion.getRangeStart(), rangeQuestion.getRangeStep(), rangeQuestion.getRangeEnd(), 4);\n        assertThat(binding.widgetAnswerText.getText(), equalTo(\"6\"));\n    }\n\n    @Test\n    public void setNumberPickerValue_whenRangeStartIsGreaterThenRangeEnd_updatesAnswer() {\n        when(rangeQuestion.getRangeStart()).thenReturn(BigDecimal.TEN);\n        when(rangeQuestion.getRangeEnd()).thenReturn(BigDecimal.ONE);\n\n        RangeWidgetUtils.getNumberPickerProgress(binding, rangeQuestion.getRangeStart(), rangeQuestion.getRangeStep(), rangeQuestion.getRangeEnd(), 4);\n        assertThat(binding.widgetAnswerText.getText(), equalTo(\"5\"));\n    }\n\n    @Test\n    public void setUpLayoutElements_forHorizontalSliderWidget_shouldShowCorrectSlider() {\n        RangeWidgetUtils.RangeWidgetLayoutElements layoutElements = RangeWidgetUtils.setUpLayoutElements(\n                widgetTestActivity(), promptWithReadOnlyAndQuestionDef(rangeQuestion));\n        assertThat(layoutElements.getSlider().getRotation(), equalTo(0.0F));\n    }\n\n    @Test\n    public void setUpLayoutElements_forVerticalSliderWidget_shouldShowCorrectSlider() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withQuestion(rangeQuestion)\n                .withAppearance(VERTICAL_APPEARANCE)\n                .build();\n        RangeWidgetUtils.RangeWidgetLayoutElements layoutElements = RangeWidgetUtils.setUpLayoutElements(\n                widgetTestActivity(), prompt);\n        assertThat(layoutElements.getSlider().getRotation(), equalTo(270.0F));\n    }\n\n    @Test\n    public void setUpWidgetParameters_showsCorrectMinAndMaxValues() {\n        RangeWidgetUtils.setUpWidgetParameters(rangeQuestion, sampleTextView1, sampleTextView2);\n\n        assertThat(sampleTextView1.getText(), equalTo(\"1\"));\n        assertThat(sampleTextView2.getText(), equalTo(\"10\"));\n    }\n\n    @Test\n    public void whenRangeQuestionHasZeroRangeStep_invalidWidgetToastIsShown() {\n        when(rangeQuestion.getRangeStep()).thenReturn(BigDecimal.ZERO);\n        assertThat(RangeWidgetUtils.isWidgetValid(context, rangeQuestion), equalTo(false));\n\n        String toastText = ShadowToast.getTextOfLatestToast();\n        assertThat(toastText, equalTo(ApplicationProvider.getApplicationContext().getString(org.odk.collect.strings.R.string.invalid_range_widget)));\n    }\n\n    @Test\n    public void whenPromptHasInvalidWidgetParameters_invalidWidgetToastIsShown() {\n        when(rangeQuestion.getRangeStep()).thenReturn(new BigDecimal(2));\n        assertThat(RangeWidgetUtils.isWidgetValid(context, rangeQuestion), equalTo(false));\n\n        String toastText = ShadowToast.getTextOfLatestToast();\n        assertThat(toastText, equalTo(ApplicationProvider.getApplicationContext().getString(org.odk.collect.strings.R.string.invalid_range_widget)));\n    }\n\n    @Test\n    public void whenRangeQuestionHasZeroRangeStep_sliderIsDisabled() {\n        when(rangeQuestion.getRangeStep()).thenReturn(BigDecimal.ZERO);\n        RangeWidgetUtils.isRangeSliderWidgetValid(rangeQuestion, slider);\n        assertFalse(slider.isEnabled());\n    }\n\n    @Test\n    public void whenPromptHasInvalidWidgetParameters_sliderIsDisabled() {\n        when(rangeQuestion.getRangeStep()).thenReturn(new BigDecimal(2));\n        RangeWidgetUtils.isRangeSliderWidgetValid(rangeQuestion, slider);\n        assertFalse(slider.isEnabled());\n    }\n\n    @Test\n    public void whenRangeQuestionHasZeroRangeStep_pickerButtonIsDisabled() {\n        when(rangeQuestion.getRangeStep()).thenReturn(BigDecimal.ZERO);\n        RangeWidgetUtils.setUpRangePickerWidget(widgetTestActivity(), binding, promptWithReadOnlyAndQuestionDef(rangeQuestion));\n        assertFalse(binding.widgetButton.isEnabled());\n    }\n\n    @Test\n    public void whenPromptHasInvalidWidgetParameters_pickerButtonIsDisabled() {\n        when(rangeQuestion.getRangeStep()).thenReturn(new BigDecimal(2));\n        RangeWidgetUtils.setUpRangePickerWidget(widgetTestActivity(), binding, promptWithReadOnlyAndQuestionDef(rangeQuestion));\n        assertFalse(binding.widgetButton.isEnabled());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/RecordingRequesterProviderTest.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Test;\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.instanceOf;\nimport static org.mockito.Mockito.mock;\n\npublic class RecordingRequesterProviderTest {\n\n    private final RecordingRequesterProvider provider = new RecordingRequesterProvider(\n            mock(InternalRecordingRequester.class),\n            mock(ExternalAppRecordingRequester.class)\n    );\n\n    @Test\n    public void whenNoQualitySpecified_andSettingExternalNotPreferred_createsInternalRecordingRequester() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .build();\n\n        RecordingRequester recordingRequester = provider.create(prompt, false);\n        assertThat(recordingRequester, instanceOf(InternalRecordingRequester.class));\n    }\n\n    @Test\n    public void whenNoQualitySpecified_andSettingExternalPreferred_createsExternalRecordingRequester() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .build();\n\n        RecordingRequester recordingRequester = provider.create(prompt, true);\n        assertThat(recordingRequester, instanceOf(ExternalAppRecordingRequester.class));\n    }\n\n    @Test\n    public void whenQualityIsNormal_andSettingExternalNotPreferred_createsInternalRecordingRequester() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withBindAttribute(\"odk\", \"quality\", \"normal\")\n                .build();\n\n        RecordingRequester recordingRequester = provider.create(prompt, false);\n        assertThat(recordingRequester, instanceOf(InternalRecordingRequester.class));\n    }\n\n    @Test\n    public void whenQualityIsNormal_andSettingExternalPreferred_createsInternalRecordingRequester() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withBindAttribute(\"odk\", \"quality\", \"normal\")\n                .build();\n\n        RecordingRequester recordingRequester = provider.create(prompt, true);\n        assertThat(recordingRequester, instanceOf(InternalRecordingRequester.class));\n    }\n\n    @Test\n    public void whenQualityIsVoiceOnly_andSettingExternalNotPreferred_createsInternalRecordingRequester() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withBindAttribute(\"odk\", \"quality\", \"voice-only\")\n                .build();\n\n        RecordingRequester recordingRequester = provider.create(prompt, false);\n        assertThat(recordingRequester, instanceOf(InternalRecordingRequester.class));\n    }\n\n    @Test\n    public void whenQualityIsVoiceOnly_andSettingExternalPreferred_createsInternalRecordingRequester() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withBindAttribute(\"odk\", \"quality\", \"voice-only\")\n                .build();\n\n        RecordingRequester recordingRequester = provider.create(prompt, true);\n        assertThat(recordingRequester, instanceOf(InternalRecordingRequester.class));\n    }\n\n    @Test\n    public void whenQualityIsLow_andSettingExternalNotPreferred_createsInternalRecordingRequester() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withBindAttribute(\"odk\", \"quality\", \"low\")\n                .build();\n\n        RecordingRequester recordingRequester = provider.create(prompt, false);\n        assertThat(recordingRequester, instanceOf(InternalRecordingRequester.class));\n    }\n\n    @Test\n    public void whenQualityIsLow_andSettingExternalPreferred_createsInternalRecordingRequester() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withBindAttribute(\"odk\", \"quality\", \"low\")\n                .build();\n\n        RecordingRequester recordingRequester = provider.create(prompt, true);\n        assertThat(recordingRequester, instanceOf(InternalRecordingRequester.class));\n    }\n\n    @Test\n    public void whenQualityIsExternal_andSettingExternalPreferred_createsExternalRecordingRequester() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withBindAttribute(\"odk\", \"quality\", \"external\")\n                .build();\n\n        RecordingRequester recordingRequester = provider.create(prompt, true);\n        assertThat(recordingRequester, instanceOf(ExternalAppRecordingRequester.class));\n    }\n\n    @Test\n    public void whenQualityIsExternal_andSettingExternalNotPreferred_createsExternalRecordingRequester() {\n        FormEntryPrompt prompt = new MockFormEntryPromptBuilder()\n                .withBindAttribute(\"odk\", \"quality\", \"external\")\n                .build();\n\n        RecordingRequester recordingRequester = provider.create(prompt, false);\n        assertThat(recordingRequester, instanceOf(ExternalAppRecordingRequester.class));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/StringRequesterImplTest.kt",
    "content": "package org.odk.collect.android.widgets.utilities\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.form.api.FormEntryPrompt\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.spy\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.R\nimport org.odk.collect.android.javarosawrapper.FormController\nimport org.odk.collect.android.utilities.ExternalAppIntentProvider\nimport org.odk.collect.androidshared.system.IntentLauncher\nimport org.robolectric.Robolectric\nimport java.lang.Exception\n\n@RunWith(AndroidJUnit4::class)\nclass StringRequesterImplTest {\n    private val intentLauncher = spy(FakeIntentLauncher())\n    private val requestCode = 99\n    private val externalAppIntentProvider = mock<ExternalAppIntentProvider>()\n    private val formEntryPrompt = mock<FormEntryPrompt>()\n    private val onError: (String) -> Unit = mock()\n    private val availableActionSendToIntent = Intent(Intent.ACTION_SENDTO)\n    private val unAvailableActionSendToIntent = Intent(Intent.ACTION_SENDTO).also {\n        it.putExtra(\"fail\", \"fail\")\n    }\n    private val availableIntent = Intent()\n    private val unAvailableIntent = Intent().also {\n        it.putExtra(\"fail\", \"fail\")\n    }\n\n    private val formController: FormController = mock()\n\n    private lateinit var activity: Activity\n    private lateinit var stringRequester: StringRequester\n\n    @Before\n    fun setup() {\n        activity = Robolectric.buildActivity(Activity::class.java).get()\n        stringRequester = StringRequesterImpl(intentLauncher, externalAppIntentProvider, formController)\n    }\n\n    @Test\n    fun `When exception is thrown by ExternalAppIntentProvider#getIntentToRunExternalApp onError should be called`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).then {\n            throw Exception(\"exception\")\n        }\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        verify(onError).invoke(\"exception\")\n    }\n\n    @Test\n    fun `When exception is thrown by ExternalAppIntentProvider#getIntentToRunExternalAppWithoutDefaultCategory onError should be called`() {\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).then {\n            throw Exception(\"exception\")\n        }\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        verify(onError).invoke(\"exception\")\n    }\n\n    @Test\n    fun `When error is thrown by ExternalAppIntentProvider#getIntentToRunExternalApp onError should be called`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).then {\n            throw Exception(\"error\")\n        }\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        verify(onError).invoke(\"error\")\n    }\n\n    @Test\n    fun `When error is thrown by ExternalAppIntentProvider#getIntentToRunExternalAppWithoutDefaultCategory onError should be called`() {\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).then {\n            throw Exception(\"error\")\n        }\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        verify(onError).invoke(\"error\")\n    }\n\n    @Test\n    fun `If the first attempt to start activity succeeded nothing else should happen`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            availableIntent\n        )\n\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        assertThat(intentLauncher.launchCallCounter, `is`(0))\n        assertThat(intentLauncher.launchForResultCallCounter, `is`(1))\n        assertThat(intentLauncher.errorCounter, `is`(0))\n    }\n\n    @Test\n    fun `If the first attempt to start activity succeeded for intent with ACTION_SENDTO nothing else should happen`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            availableActionSendToIntent\n        )\n\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        assertThat(intentLauncher.launchCallCounter, `is`(1))\n        assertThat(intentLauncher.launchForResultCallCounter, `is`(0))\n        assertThat(intentLauncher.errorCounter, `is`(0))\n    }\n\n    @Test\n    fun `If the first attempt to start activity failed there should be another one`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            unAvailableIntent\n        )\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).thenReturn(availableIntent)\n\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        assertThat(intentLauncher.launchCallCounter, `is`(0))\n        assertThat(intentLauncher.launchForResultCallCounter, `is`(2))\n        assertThat(intentLauncher.errorCounter, `is`(1))\n    }\n\n    @Test\n    fun `If the first attempt to start activity failed for intent with ACTION_SENDTO there should be another one`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            unAvailableActionSendToIntent\n        )\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).thenReturn(availableIntent)\n\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        assertThat(intentLauncher.launchCallCounter, `is`(2))\n        assertThat(intentLauncher.launchForResultCallCounter, `is`(0))\n        assertThat(intentLauncher.errorCounter, `is`(1))\n    }\n\n    @Test\n    fun `If both attempts to start activity failed onError should be called`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            unAvailableIntent\n        )\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).thenReturn(unAvailableIntent)\n\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        assertThat(intentLauncher.launchCallCounter, `is`(0))\n        assertThat(intentLauncher.launchForResultCallCounter, `is`(2))\n        assertThat(intentLauncher.errorCounter, `is`(2))\n\n        verify(onError).invoke(activity.getString(org.odk.collect.strings.R.string.no_app))\n    }\n\n    @Test\n    fun `If both attempts to start activity failed for intent with ACTION_SENDTO onError should be called`() {\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            unAvailableActionSendToIntent\n        )\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).thenReturn(unAvailableActionSendToIntent)\n\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        assertThat(intentLauncher.launchCallCounter, `is`(2))\n        assertThat(intentLauncher.launchForResultCallCounter, `is`(0))\n        assertThat(intentLauncher.errorCounter, `is`(2))\n\n        verify(onError).invoke(activity.getString(org.odk.collect.strings.R.string.no_app))\n    }\n\n    @Test\n    fun `If both attempts to start activity failed onError should be called with custom message if it is set`() {\n        whenever(formEntryPrompt.getSpecialFormQuestionText(\"noAppErrorString\")).thenReturn(\"Custom message\")\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            unAvailableIntent\n        )\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).thenReturn(unAvailableIntent)\n\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        verify(onError).invoke(\"Custom message\")\n    }\n\n    @Test\n    fun `If both attempts to start activity failed for intent with ACTION_SENDTO onError should be called with custom message if it is set`() {\n        whenever(formEntryPrompt.getSpecialFormQuestionText(\"noAppErrorString\")).thenReturn(\"Custom message\")\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            unAvailableActionSendToIntent\n        )\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).thenReturn(unAvailableActionSendToIntent)\n\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            null,\n            onError\n        )\n\n        verify(onError).invoke(\"Custom message\")\n    }\n\n    @Test\n    fun `Value should be added to intent`() {\n        whenever(formEntryPrompt.getSpecialFormQuestionText(\"noAppErrorString\")).thenReturn(\"Custom message\")\n        whenever(externalAppIntentProvider.getIntentToRunExternalApp(formController, formEntryPrompt)).thenReturn(\n            unAvailableIntent\n        )\n        whenever(\n            externalAppIntentProvider.getIntentToRunExternalAppWithoutDefaultCategory(\n                formController,\n                formEntryPrompt,\n                activity.packageManager\n            )\n        ).thenReturn(availableIntent)\n\n        stringRequester.launch(\n            activity,\n            requestCode,\n            formEntryPrompt,\n            \"123\",\n            onError\n        )\n\n        assertThat(unAvailableIntent.getSerializableExtra(\"value\"), `is`(\"123\"))\n        assertThat(availableIntent.getSerializableExtra(\"value\"), `is`(\"123\"))\n    }\n\n    class FakeIntentLauncher : IntentLauncher {\n        var launchCallCounter = 0\n        var launchForResultCallCounter = 0\n        var errorCounter = 0\n\n        override fun launch(context: Context, intent: Intent?, onError: () -> Unit) {\n            launchCallCounter++\n            if (intent!!.hasExtra(\"fail\")) {\n                errorCounter++\n                onError()\n            }\n        }\n\n        override fun launchForResult(\n            activity: Activity,\n            intent: Intent?,\n            requestCode: Int,\n            onError: () -> Unit\n        ) {\n            launchForResultCallCounter++\n            if (intent!!.hasExtra(\"fail\")) {\n                errorCounter++\n                onError()\n            }\n        }\n\n        override fun launchForResult(\n            resultLauncher: ActivityResultLauncher<Intent>,\n            intent: Intent?,\n            onError: () -> Unit\n        ) {\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/utilities/StringWidgetUtilsTest.java",
    "content": "package org.odk.collect.android.widgets.utilities;\n\nimport org.javarosa.core.model.data.DecimalData;\nimport org.javarosa.core.model.data.IntegerData;\nimport org.javarosa.core.model.data.StringData;\nimport org.javarosa.form.api.FormEntryPrompt;\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class StringWidgetUtilsTest {\n\n    private final FormEntryPrompt formEntryPrompt = mock(FormEntryPrompt.class);\n\n    @Test\n    public void getIntegerAnswerValueFromIAnswerDataTest() {\n        assertNull(StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(null));\n        assertEquals(Integer.valueOf(0), StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(new IntegerData(0)));\n        assertEquals(Integer.valueOf(-15), StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(new IntegerData(-15)));\n        assertEquals(Integer.valueOf(15), StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(new IntegerData(15)));\n        assertEquals(Integer.valueOf(15), StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(new DecimalData(15.0)));\n        assertEquals(Integer.valueOf(15), StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(new StringData(\"15\")));\n        assertNull(StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(new StringData(\"abc\")));\n    }\n\n    @Test\n    public void getDoubleAnswerValueFromIAnswerDataTest() {\n        assertNull(StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(null));\n        assertEquals(Double.valueOf(0), StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(new DecimalData(0)));\n        assertEquals(Double.valueOf(-15), StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(new DecimalData(-15)));\n        assertEquals(Double.valueOf(-15.123), StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(new DecimalData(-15.123)));\n        assertEquals(Double.valueOf(15), StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(new DecimalData(15)));\n        assertEquals(Double.valueOf(15.123), StringWidgetUtils.getDoubleAnswerValueFromIAnswerData(new DecimalData(15.123)));\n        assertEquals(Integer.valueOf(15), StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(new IntegerData(15)));\n        assertEquals(Integer.valueOf(15), StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(new StringData(\"15\")));\n        assertNull(StringWidgetUtils.getIntegerAnswerValueFromIAnswerData(new StringData(\"abc\")));\n    }\n\n    @Test\n    public void getIntegerDataTest() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(null);\n        assertNull(StringWidgetUtils.getIntegerData(\"\", formEntryPrompt));\n        assertNull(StringWidgetUtils.getIntegerData(\"5.5\", formEntryPrompt));\n        assertEquals(\"0\", StringWidgetUtils.getIntegerData(\"0\", formEntryPrompt).getDisplayText());\n        assertEquals(\"7\", StringWidgetUtils.getIntegerData(\"7\", formEntryPrompt).getDisplayText());\n        assertEquals(\"-22\", StringWidgetUtils.getIntegerData(\"-22\", formEntryPrompt).getDisplayText());\n        assertEquals(\"1000000\", StringWidgetUtils.getIntegerData(\"1000000\", formEntryPrompt).getDisplayText());\n    }\n\n    @Test\n    public void getDecimalDataTest() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(null);\n        assertNull(StringWidgetUtils.getDecimalData(\"\", formEntryPrompt));\n        assertEquals(\"0.0\", StringWidgetUtils.getDecimalData(\"0\", formEntryPrompt).getDisplayText());\n        assertEquals(\"50.0\", StringWidgetUtils.getDecimalData(\"50\", formEntryPrompt).getDisplayText());\n        assertEquals(\"7.75\", StringWidgetUtils.getDecimalData(\"7.75\", formEntryPrompt).getDisplayText());\n        assertEquals(\"-22.123\", StringWidgetUtils.getDecimalData(\"-22.123\", formEntryPrompt).getDisplayText());\n    }\n\n    @Test\n    public void getStringNumberDataTest() {\n        when(formEntryPrompt.getAppearanceHint()).thenReturn(null);\n        assertNull(StringWidgetUtils.getDecimalData(\"\", formEntryPrompt));\n        assertEquals(\"0\", StringWidgetUtils.getStringNumberData(\"0\", formEntryPrompt).getDisplayText());\n        assertEquals(\"50\", StringWidgetUtils.getStringNumberData(\"50\", formEntryPrompt).getDisplayText());\n        assertEquals(\"7.75\", StringWidgetUtils.getStringNumberData(\"7.75\", formEntryPrompt).getDisplayText());\n        assertEquals(\"-22.123\", StringWidgetUtils.getStringNumberData(\"-22.123\", formEntryPrompt).getDisplayText());\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/viewmodels/DateTimeViewModelTest.java",
    "content": "package org.odk.collect.android.widgets.viewmodels;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.androidtest.LiveDataTestUtilsKt.getOrAwaitValue;\n\nimport android.widget.DatePicker;\nimport android.widget.TimePicker;\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule;\nimport androidx.lifecycle.LiveData;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.joda.time.DateTime;\nimport org.joda.time.LocalDateTime;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.android.widgets.datetime.DateTimeUtils;\n\n@RunWith(AndroidJUnit4.class)\npublic class DateTimeViewModelTest {\n\n    @Rule\n    public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();\n\n    private DateTimeViewModel viewModel;\n    LiveData<LocalDateTime> selectedDate;\n    LiveData<DateTime> selectedTime;\n\n    @Before\n    public void setUp() {\n        viewModel = new DateTimeViewModel();\n        selectedDate = viewModel.getSelectedDate();\n        selectedTime = viewModel.getSelectedTime();\n    }\n\n    @Test\n    public void setSelectedDate_updatesDateReturnedByViewModel() {\n        viewModel.setSelectedDate(2012, 4, 12);\n        assertThat(getOrAwaitValue(selectedDate), is(DateTimeUtils.getSelectedDate(\n                new LocalDateTime().withDate(2012, 5, 12), LocalDateTime.now())));\n    }\n\n    @Test\n    public void setSelectedDateTime_updatesTimeReturnedByViewModel() {\n        viewModel.setSelectedTime(12, 10);\n        assertThat(getOrAwaitValue(selectedTime), is(DateTimeUtils.getSelectedTime(\n                new LocalDateTime().withTime(12, 10, 0, 0), LocalDateTime.now()).toDateTime()));\n    }\n\n    @Test\n    public void updatingDateInDateSetListener_updatesDateReturnedByViewModel() {\n        viewModel.getDateSetListener().onDateSet(mock(DatePicker.class), 2012, 4, 12);\n\n        assertThat(getOrAwaitValue(selectedDate), is(DateTimeUtils.getSelectedDate(\n                new LocalDateTime().withDate(2012, 5, 12), LocalDateTime.now())));\n    }\n\n    @Test\n    public void updatingTimeInTimeSetListener_updatesTimeReturnedByViewModel_andClearsFocus() {\n        TimePicker timePicker = mock(TimePicker.class);\n        when(timePicker.getCurrentHour()).thenReturn(12);\n        when(timePicker.getCurrentMinute()).thenReturn(10);\n        viewModel.getTimeSetListener().onTimeSet(timePicker, 0, 0);\n\n        verify(timePicker).clearFocus();\n        assertThat(getOrAwaitValue(selectedTime), is(DateTimeUtils.getSelectedTime(\n                new LocalDateTime().withTime(12, 10, 0, 0), LocalDateTime.now()).toDateTime()));\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/viewmodels/QuestionViewModelTest.kt",
    "content": "package org.odk.collect.android.widgets.viewmodels\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.core.model.instance.TreeReference\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.android.formentry.support.InMemFormSessionRepository\nimport org.odk.collect.android.javarosawrapper.FailedValidationResult\nimport org.odk.collect.android.javarosawrapper.FakeFormController\nimport org.odk.collect.android.support.MockFormEntryPromptBuilder\nimport org.odk.collect.strings.R\nimport org.odk.collect.testshared.FakeScheduler\nimport org.odk.collect.testshared.getOrAwaitValue\n\n@RunWith(AndroidJUnit4::class)\nclass QuestionViewModelTest {\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    private val scheduler: FakeScheduler = FakeScheduler()\n    private val startingIndex = FormIndex(null, 0, 0, TreeReference())\n    private val formController = FakeFormController(startingIndex, mock())\n    private val formSessionRepository = InMemFormSessionRepository().apply {\n        set(\"blah\", formController, mock())\n    }\n    private val viewModel = QuestionViewModel(scheduler, formSessionRepository, \"blah\")\n\n    @Test\n    fun `validate updates constraintValidationResult`() {\n        val formDef: FormDef = mock()\n        whenever(formDef.evaluateConstraint(any(), any())).thenReturn(false)\n        formController.setFormDef(formDef)\n\n        val reference = TreeReference()\n        reference.add(\"blah\", TreeReference.INDEX_UNBOUND)\n        val formIndex = FormIndex(null, 1, 1, reference)\n        val prompt = MockFormEntryPromptBuilder().build()\n        formController.setPrompt(formIndex, prompt)\n\n        val failedValidationResult =\n            FailedValidationResult(formIndex, 0, null, R.string.invalid_answer_error)\n        formController.setFailedConstraint(failedValidationResult)\n\n        viewModel.validate(formIndex, StringData(\"answer\"))\n        assertThat(\n            viewModel.constraintValidationResult.getOrAwaitValue(scheduler),\n            equalTo(failedValidationResult)\n        )\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/warnings/SpacesInUnderlyingValuesTest.java",
    "content": "package org.odk.collect.android.widgets.warnings;\n\nimport com.google.common.collect.Lists;\n\nimport org.javarosa.core.model.SelectChoice;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning.SpacesInUnderlyingValues;\nimport org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning.UnderlyingValuesChecker;\n\nimport java.util.List;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertTrue;\n\npublic class SpacesInUnderlyingValuesTest {\n\n    private UnderlyingValuesChecker subject;\n\n    @Before\n    public void setUp() {\n        subject = new SpacesInUnderlyingValues();\n    }\n\n    @Test\n    public void doesNotDetectErrorWhenThereIsNone() {\n        List<SelectChoice> items = Lists.newArrayList(\n                new SelectChoice(\"label\", \"no_space\")\n        );\n\n        subject.check(items);\n        assertFalse(subject.hasInvalidValues());\n    }\n\n    @Test\n    public void doesNotDetectErrorInEmptySet() {\n        List<SelectChoice> items = Lists.newArrayList();\n\n        subject.check(items);\n        assertFalse(subject.hasInvalidValues());\n    }\n\n    @Test\n    public void doesDetectSingleSpaceError() {\n        List<SelectChoice> items = Lists.newArrayList(\n                new SelectChoice(\"label\", \"with space\")\n        );\n\n        subject.check(items);\n        assertTrue(subject.hasInvalidValues());\n    }\n\n    @Test\n    public void detectsMultipleErrors() {\n        List<SelectChoice> items = Lists.newArrayList(\n                new SelectChoice(\"label\", \"with space\"),\n                new SelectChoice(\"label2\", \"with space2\")\n        );\n\n        subject.check(items);\n        assertTrue(subject.hasInvalidValues());\n    }\n\n    @Test\n    public void returnsInvalidValues() {\n        List<SelectChoice> items = Lists.newArrayList(\n                new SelectChoice(\"label\", \"with space\"),\n                new SelectChoice(\"label2\", \"with space2\")\n        );\n\n        subject.check(items);\n        assertEquals(subject.getInvalidValues().size(), 2);\n    }\n\n    @Test\n    public void spaceInTheBeginningOfUnderlyingValueIsTrimmed() {\n        List<SelectChoice> items = Lists.newArrayList(\n                new SelectChoice(\"label\", \" before\")\n        );\n\n        subject.check(items);\n        assertThat(items.get(0).getValue(), equalTo(\"before\"));\n        assertFalse(subject.hasInvalidValues());\n    }\n\n    @Test\n    public void spaceInTheTheEndOfUnderlyingValueIsTrimmed() {\n        List<SelectChoice> items = Lists.newArrayList(\n                new SelectChoice(\"label\", \"after \")\n        );\n\n        subject.check(items);\n        assertThat(items.get(0).getValue(), equalTo(\"after\"));\n        assertFalse(subject.hasInvalidValues());\n    }\n}"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/android/widgets/warnings/SpacesInUnderlyingValuesWarningTest.java",
    "content": "package org.odk.collect.android.widgets.warnings;\n\nimport com.google.common.collect.Lists;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\nimport org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning.UnderlyingValuesChecker;\nimport org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning.WarningRenderer;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\npublic class SpacesInUnderlyingValuesWarningTest {\n\n    SpacesInUnderlyingValuesWarning subject;\n\n    @Mock\n    UnderlyingValuesChecker checker;\n\n    @Mock\n    WarningRenderer renderer;\n\n    @Before\n    public void setUp() {\n        MockitoAnnotations.initMocks(this);\n        subject = new SpacesInUnderlyingValuesWarning(checker, renderer);\n    }\n\n    @Test\n    public void renderWarningWhenHasInvalidValues() {\n        when(checker.hasInvalidValues()).thenReturn(true);\n\n        subject.renderWarningIfNecessary(Lists.newArrayList());\n\n        verify(renderer, times(1)).render(any());\n    }\n\n    @Test\n    public void doesNotRenderWhenNoInvalidValuesDetected() {\n        when(checker.hasInvalidValues()).thenReturn(false);\n\n        subject.renderWarningIfNecessary(Lists.newArrayList());\n\n        verify(renderer, never()).render(any());\n    }\n}"
  },
  {
    "path": "collect_app/src/test/java/org/odk/collect/geo/javarosa/IntersectsFunctionHandlerTest.kt",
    "content": "package org.odk.collect.geo.javarosa\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.instanceOf\nimport org.javarosa.core.model.data.BooleanData\nimport org.javarosa.form.api.FormEntryController\nimport org.javarosa.form.api.FormEntryModel\nimport org.javarosa.test.BindBuilderXFormsElement.bind\nimport org.javarosa.test.Scenario\nimport org.javarosa.test.XFormsElement.body\nimport org.javarosa.test.XFormsElement.head\nimport org.javarosa.test.XFormsElement.html\nimport org.javarosa.test.XFormsElement.input\nimport org.javarosa.test.XFormsElement.mainInstance\nimport org.javarosa.test.XFormsElement.model\nimport org.javarosa.test.XFormsElement.t\nimport org.javarosa.test.XFormsElement.title\nimport org.javarosa.xpath.XPathTypeMismatchException\nimport org.junit.Assert.fail\nimport org.junit.Test\n\nclass IntersectsFunctionHandlerTest {\n\n    @Test\n    fun `returns false when input is empty`() {\n        val scenario = Scenario.init(\n            \"Intersects form\",\n            html(\n                head(\n                    title(\"Intersects form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"intersects-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        bind(\"/data/question\").type(\"geotrace\"),\n                        bind(\"/data/calculate\").type(\"boolean\")\n                            .calculate(\"intersects(/data/question)\")\n                    )\n                ),\n                body(\n                    input(\"/data/question\"),\n                    input(\"/data/calculate\")\n                )\n            )\n        ) { formDef ->\n            FormEntryController(FormEntryModel(formDef)).also {\n                it.addFunctionHandler(IntersectsFunctionHandler())\n            }\n        }\n\n        assertThat(scenario.answerOf<BooleanData>(\"/data/calculate\").value, equalTo(false))\n    }\n\n    @Test\n    fun `returns true when input is intersecting trace`() {\n        val scenario = Scenario.init(\n            \"Intersects form\",\n            html(\n                head(\n                    title(\"Intersects form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"intersects-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        bind(\"/data/question\").type(\"geotrace\"),\n                        bind(\"/data/calculate\").type(\"boolean\")\n                            .calculate(\"intersects(/data/question)\")\n                    )\n                ),\n                body(\n                    input(\"/data/question\"),\n                    input(\"/data/calculate\")\n                )\n            )\n        ) { formDef ->\n            FormEntryController(FormEntryModel(formDef)).also {\n                it.addFunctionHandler(IntersectsFunctionHandler())\n            }\n        }\n\n        scenario.answer(\n            \"/data/question\",\n            \"1.0 1.0 0.0 0.0; 1.0 3.0 0.0 0.0; 2.0 3.0 0.0 0.0; 2.0 2.0 0.0 0.0; 0.0 2.0 0.0 0.0\"\n        )\n        assertThat(scenario.answerOf<BooleanData>(\"/data/calculate\").value, equalTo(true))\n    }\n\n    @Test\n    fun `throws exception when input is a non-geo string`() {\n        val scenario = Scenario.init(\n            \"Intersects form\",\n            html(\n                head(\n                    title(\"Intersects form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"intersects-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"boolean\")\n                            .calculate(\"intersects(/data/question)\")\n                    )\n                ),\n                body(\n                    input(\"/data/question\"),\n                    input(\"/data/calculate\")\n                )\n            )\n        ) { formDef ->\n            FormEntryController(FormEntryModel(formDef)).also {\n                it.addFunctionHandler(IntersectsFunctionHandler())\n            }\n        }\n\n        val expected = XPathTypeMismatchException::class.java\n        try {\n            scenario.answer(\"/data/question\", \"blah\")\n            fail(\"Expected exception: $expected\")\n        } catch (e: Exception) {\n            assertThat(e.cause, instanceOf(expected))\n        }\n    }\n\n    @Test\n    fun `throws exception when passed too many args`() {\n        val expected = XPathTypeMismatchException::class.java\n        try {\n            Scenario.init(\n                \"Intersects form\",\n                html(\n                    head(\n                        title(\"Intersects form\"),\n                        model(\n                            mainInstance(\n                                t(\n                                    \"data id=\\\"intersects-form\\\"\",\n                                    t(\"question\"),\n                                    t(\"calculate\")\n                                )\n                            ),\n                            bind(\"/data/question\").type(\"geotrace\"),\n                            bind(\"/data/calculate\").type(\"boolean\")\n                                .calculate(\"intersects(/data/question,/data/question)\")\n                        )\n                    ),\n                    body(\n                        input(\"/data/question\"),\n                        input(\"/data/calculate\")\n                    )\n                )\n            ) { formDef ->\n                FormEntryController(FormEntryModel(formDef)).also {\n                    it.addFunctionHandler(IntersectsFunctionHandler())\n                }\n            }\n\n            fail(\"Expected exception: $expected\")\n        } catch (e: Exception) {\n            assertThat(e.cause, instanceOf(expected))\n        }\n    }\n}\n"
  },
  {
    "path": "collect_app/src/test/resources/forms/simple-search-external-csv.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>simple-search-external-csv</h:title>\n        <model>\n            <instance>\n                <simple-search-external-csv id=\"simple-search-external-csv\">\n                    <fruit1 />\n                    <note_fruit />\n                </simple-search-external-csv>\n            </instance>\n            <bind nodeset=\"/simple-search-external-csv/fruit1\" type=\"select1\" />\n            <bind nodeset=\"/simple-search-external-csv/note_fruit\" readonly=\"true()\" type=\"string\" />\n        </model>\n    </h:head>\n    <h:body>\n        <select1 appearance=\"search('simple-search-external-csv-fruits')\" ref=\"/simple-search-external-csv/fruit1\">\n            <label>Select from a CSV using search() appearance/function</label>\n            <item>\n                <label>name</label>\n                <value>name_key</value>\n            </item>\n        </select1>\n        <input ref=\"/simple-search-external-csv/note_fruit\">\n            <label>\n                The fruit <output value=\" /simple-search-external-csv/fruit1 \" /> pulled from csv\n            </label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "collect_app/src/test/resources/media/simple-search-external-csv-fruits.csv",
    "content": "name_key,name\nmango,Mango\noranges,Oranges\nstrawberries,Strawberries"
  },
  {
    "path": "collect_app/src/test/resources/robolectric.properties",
    "content": "application=org.odk.collect.android.application.RobolectricApplication\nsdk=33\n\n"
  },
  {
    "path": "config/checkstyle.xml",
    "content": "<?xml version=\"1.0\"?><!DOCTYPE module PUBLIC\n    \"-//Puppy Crawl//DTD Check Configuration 1.3//EN\"\n    \"http://www.puppycrawl.com/dtds/configuration_1_3.dtd\">\n\n<!--\n    Checkstyle configuration that checks the Google coding conventions from Google Java Style\n    that can be found at https://google.github.io/styleguide/javaguide.html.\n\n    Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.\n    Source: https://github.com/checkstyle/checkstyle/blob/master/src/main/resources/google_checks.xml\n -->\n\n<module name=\"Checker\">\n    <module name=\"SuppressWarningsFilter\" />\n    <module name=\"TreeWalker\">\n        <module name=\"SuppressWarningsHolder\" />\n    </module>\n    <property name=\"charset\" value=\"UTF-8\" />\n\n    <property name=\"fileExtensions\" value=\"java, properties, xml\" />\n\n    <module name=\"FileTabCharacter\">\n        <property name=\"eachLine\" value=\"true\" />\n    </module>\n\n    <!-- ODK-specific additions -->\n    <module name=\"RegexpMultiline\">\n        <property name=\"format\" value=\"\\r\\n\" />\n        <property name=\"message\" value=\"Do not use Windows line endings\" />\n    </module>\n\n    <module name=\"TreeWalker\">\n        <!-- ODK-specific additions -->\n        <module name=\"IllegalImport\" />\n        <module name=\"RedundantImport\" />\n        <module name=\"UnusedImports\" />\n\n        <module name=\"SimplifyBooleanExpression\" />\n        <module name=\"SimplifyBooleanReturn\" />\n\n        <module name=\"OuterTypeFilename\" />\n        <module name=\"IllegalTokenText\">\n            <property name=\"tokens\" value=\"STRING_LITERAL, CHAR_LITERAL\" />\n            <property name=\"format\"\n                value=\"\\\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\\\(0(10|11|12|14|15|42|47)|134)\" />\n            <property name=\"message\"\n                value=\"Consider using special escape sequence instead of octal value or Unicode escaped value.\" />\n        </module>\n        <module name=\"AvoidEscapedUnicodeCharacters\">\n            <property name=\"allowEscapesForControlCharacters\" value=\"true\" />\n            <property name=\"allowByTailComment\" value=\"true\" />\n            <property name=\"allowNonPrintableEscapes\" value=\"true\" />\n        </module>\n        <!-- <module name=\"LineLength\">\n            <property name=\"max\" value=\"100\"/>\n            <property name=\"ignorePattern\" value=\"^package.*|^import.*|a href|href|http://|https://|ftp://\"/>\n        </module> -->\n        <module name=\"AvoidStarImport\" />\n        <module name=\"OneTopLevelClass\" />\n        <module name=\"NoLineWrap\" />\n        <module name=\"EmptyBlock\">\n            <property name=\"option\" value=\"TEXT\" />\n            <property name=\"tokens\"\n                value=\"LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH, LITERAL_CATCH, LITERAL_WHILE\" />\n        </module>\n        <module name=\"NeedBraces\" />\n        <module name=\"LeftCurly\"/>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlySame\" />\n            <property name=\"tokens\"\n                value=\"LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_DO\" />\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlyAlone\" />\n            <property name=\"tokens\"\n                value=\"CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT, INSTANCE_INIT\" />\n        </module>\n        <module name=\"NoWhitespaceAfter\" />\n        <module name=\"NoWhitespaceBefore\" />\n        <module name=\"WhitespaceAround\">\n            <property name=\"allowEmptyConstructors\" value=\"true\" />\n            <property name=\"allowEmptyMethods\" value=\"true\" />\n            <property name=\"allowEmptyTypes\" value=\"true\" />\n            <property name=\"allowEmptyLoops\" value=\"true\" />\n            <property name=\"allowEmptyLambdas\" value=\"true\" />\n            <message key=\"ws.notFollowed\"\n                value=\"WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)\" />\n            <message key=\"ws.notPreceded\"\n                value=\"WhitespaceAround: ''{0}'' is not preceded with whitespace.\" />\n        </module>\n        <module name=\"OneStatementPerLine\" />\n        <module name=\"MultipleVariableDeclarations\" />\n        <module name=\"StaticVariableName\" />\n        <module name=\"ArrayTypeStyle\" />\n        <!-- <module name=\"MissingSwitchDefault\"/> -->\n        <module name=\"FallThrough\" />\n        <module name=\"UpperEll\" />\n        <module name=\"ModifierOrder\" />\n        <module name=\"EmptyLineSeparator\">\n            <property name=\"allowMultipleEmptyLines\" value=\"false\"/>\n            <property name=\"allowMultipleEmptyLinesInsideClassMembers\" value=\"false\"/>\n            <property name=\"allowNoEmptyLineBetweenFields\" value=\"true\" />\n            <property name=\"tokens\" value=\"STATIC_INIT\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapDot\" />\n            <property name=\"tokens\" value=\"DOT\" />\n            <property name=\"option\" value=\"nl\" />\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapComma\" />\n            <property name=\"tokens\" value=\"COMMA\" />\n            <property name=\"option\" value=\"EOL\" />\n        </module>\n        <module name=\"PackageName\">\n            <property name=\"format\" value=\"^[a-z]+(\\.[a-z][a-z0-9]*)*$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Package name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"TypeName\">\n            <message key=\"name.invalidPattern\"\n                value=\"Type name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"MemberName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9]*$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Member name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"ParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Parameter name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"CatchParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Catch parameter name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"LocalVariableName\">\n            <property name=\"tokens\" value=\"VARIABLE_DEF\" />\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Local variable name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"ClassTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Class type name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"MethodTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Method type name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"InterfaceTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Interface type name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"ConstantName\">\n            <property name=\"format\"\n                value=\"^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$\"/>\n            <message key=\"name.invalidPattern\"\n                value=\"Constant name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"NoFinalizer\" />\n        <module name=\"GenericWhitespace\">\n            <message key=\"ws.followed\"\n                value=\"GenericWhitespace ''{0}'' is followed by whitespace.\" />\n            <message key=\"ws.preceded\"\n                value=\"GenericWhitespace ''{0}'' is preceded with whitespace.\" />\n            <message key=\"ws.illegalFollow\"\n                value=\"GenericWhitespace ''{0}'' should followed by whitespace.\" />\n            <message key=\"ws.notPreceded\"\n                value=\"GenericWhitespace ''{0}'' is not preceded with whitespace.\" />\n        </module>\n        <!-- <module name=\"Indentation\">\n            <property name=\"basicOffset\" value=\"2\"/>\n            <property name=\"braceAdjustment\" value=\"0\"/>\n            <property name=\"caseIndent\" value=\"2\"/>\n            <property name=\"throwsIndent\" value=\"4\"/>\n            <property name=\"lineWrappingIndentation\" value=\"4\"/>\n            <property name=\"arrayInitIndent\" value=\"2\"/>\n        </module> -->\n        <!-- <module name=\"AbbreviationAsWordInName\">\n            <property name=\"ignoreFinal\" value=\"false\"/>\n            <property name=\"allowedAbbreviationLength\" value=\"1\"/>\n        </module> -->\n        <module name=\"OverloadMethodsDeclarationOrder\" />\n        <!-- <module name=\"VariableDeclarationUsageDistance\" /> -->\n        <!-- <module name=\"CustomImportOrder\">\n            <property name=\"sortImportsInGroupAlphabetically\" value=\"true\"/>\n            <property name=\"separateLineBetweenGroups\" value=\"true\"/>\n            <property name=\"customImportOrderRules\" value=\"STATIC###THIRD_PARTY_PACKAGE\"/>\n        </module> -->\n        <module name=\"MethodParamPad\" />\n        <module name=\"ParenPad\" />\n        <!--<module name=\"OperatorWrap\">-->\n        <!--<property name=\"option\" value=\"NL\"/>-->\n        <!--<property name=\"tokens\" value=\"BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF \"/>-->\n        <!--</module>-->\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationMostCases\" />\n            <property name=\"tokens\"\n                value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF\" />\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationVariables\" />\n            <property name=\"tokens\" value=\"VARIABLE_DEF\" />\n            <property name=\"allowSamelineMultipleAnnotations\" value=\"true\" />\n        </module>\n        <module name=\"NonEmptyAtclauseDescription\" />\n        <!-- <module name=\"JavadocTagContinuationIndentation\"/> -->\n        <!-- <module name=\"SummaryJavadoc\">\n            <property name=\"forbiddenSummaryFragments\" value=\"^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )\"/>\n        </module> -->\n        <!-- <module name=\"JavadocParagraph\"/> -->\n        <!-- <module name=\"AtclauseOrder\">\n            <property name=\"tagOrder\" value=\"@param, @return, @throws, @deprecated\"/>\n            <property name=\"target\" value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF\"/>\n        </module> -->\n        <!-- <module name=\"JavadocMethod\">\n            <property name=\"scope\" value=\"public\"/>\n            <property name=\"allowMissingParamTags\" value=\"true\"/>\n            <property name=\"allowMissingThrowsTags\" value=\"true\"/>\n            <property name=\"allowMissingReturnTag\" value=\"true\"/>\n            <property name=\"minLineCount\" value=\"2\"/>\n            <property name=\"allowedAnnotations\" value=\"Override, Test\"/>\n            <property name=\"allowThrowsTagsForSubclasses\" value=\"true\"/>\n        </module> -->\n        <module name=\"MethodName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9_]*$\" />\n            <message key=\"name.invalidPattern\"\n                value=\"Method name ''{0}'' must match pattern ''{1}''.\" />\n        </module>\n        <module name=\"SingleLineJavadoc\">\n            <property name=\"ignoreInlineTags\" value=\"false\" />\n        </module>\n        <module name=\"EmptyCatchBlock\">\n            <property name=\"exceptionVariableName\" value=\"expected\" />\n        </module>\n        <module name=\"EmptyForIteratorPad\" />\n        <module name=\"TypecastParenPad\" />\n        <module name=\"CovariantEquals\" />\n        <module name=\"EmptyStatement\" />\n        <module name=\"EqualsHashCode\" />\n        <module name=\"IllegalInstantiation\" />\n        <module name=\"InterfaceIsType\" />\n        <!--<module name=\"MethodLength\" />-->\n        <module name=\"WhitespaceAfter\" />\n        <!--<module name=\"UnnecessaryParentheses\" />--> <!-- Decided not to use in #1519 -->\n        <module name=\"RedundantModifier\" />\n        <module name=\"HideUtilityClassConstructor\" />\n    </module>\n</module>\n"
  },
  {
    "path": "config/lint.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<lint>\n    <!-- suppress newly failing checks -->\n    <!-- make sure classes referenced in the manifest or in a layout file are in the project -->\n    <issue id=\"MissingRegistered\" severity=\"warning\" />\n    <issue id=\"AppCompatResource\" severity=\"warning\" />\n    <issue id=\"StaticFieldLeak\" severity=\"warning\" />\n    <issue id=\"UnusedIds\" severity=\"error\" />\n    <issue id=\"AppLinkUrlError\" severity=\"warning\" />\n    <issue id=\"OutdatedLibrary\" severity=\"warning\" />\n\n    <!-- require inputType attribute on all text fields -->\n    <issue id=\"TextFields\" severity=\"error\" />\n\n    <!-- require 16KB alignment -->\n    <issue id=\"Aligned16KB\" severity=\"error\"/>\n\n    <issue id=\"GradleOverrides\" severity=\"error\" />\n    <issue id=\"FragmentTagUsage\" severity=\"error\" />\n    <issue id=\"UseCompatLoadingForDrawables\" severity=\"error\" />\n    <issue id=\"UselessParent\" severity=\"error\" />\n    <issue id=\"BinaryOperationInTimber\" severity=\"error\" />\n    <issue id=\"RelativeOverlap\" severity=\"error\" />\n    <issue id=\"Typos\" severity=\"error\" />\n    <issue id=\"SetTextI18n\" severity=\"error\" />\n    <issue id=\"HardcodedText\" severity=\"error\" />\n    <issue id=\"TypographyEllipsis\" severity=\"error\" />\n    <issue id=\"RtlHardcoded\" severity=\"error\" />\n    <issue id=\"Deprecated\" severity=\"error\" />\n    <issue id=\"DefaultLocale\" severity=\"error\" />\n    <issue id=\"SuspiciousImport\" severity=\"error\" />\n    <issue id=\"WifiManagerPotentialLeak\" severity=\"error\" />\n    <issue id=\"InefficientWeight\" severity=\"error\" />\n    <issue id=\"MergeRootFrame\" severity=\"error\" />\n    <issue id=\"LogNotTimber\" severity=\"error\" />\n    <issue id=\"StringFormatInTimber\" severity=\"error\" />\n    <issue id=\"ApplySharedPref\" severity=\"error\" />\n    <issue id=\"IconLocation\" severity=\"error\" />\n    <issue id=\"UnknownIdInLayout\" severity=\"error\" />\n    <issue id=\"IconDuplicates\" severity=\"error\" />\n    <issue id=\"PxUsage\" severity=\"error\" />\n    <issue id=\"UnusedNamespace\" severity=\"error\">\n        <ignore path=\"res/values*\" />\n    </issue>\n    <issue id=\"UnusedResources\" severity=\"error\">\n        <ignore path=\"res/values/dimens.xml\" />\n        <ignore path=\"res/values/leak_canary_config.xml\" />\n        <ignore regexp=\"ga_trackingId|google_crash_reporting_api_key|project_id|third_party_licenses|third_party_license_metadata|com.google.firebase.crashlytics.version_control_info\" />\n        <ignore regexp=\"sms_invalid_phone_number_description\" />\n        <ignore regexp=\"mapbox_access_token\" />\n    </issue>\n\n    <issue id=\"TimberExceptionLogging\" severity=\"error\" />\n    <issue id=\"UselessLeaf\" severity=\"warning\" />\n    <issue id=\"DuplicateIncludedIds\" severity=\"error\" />\n\n    <issue id=\"ContentDescription\" severity=\"ignore\" />\n    <issue id=\"HardwareIds\" severity=\"ignore\" />\n    <issue id=\"MissingPermission\" severity=\"error\" />\n    <issue id=\"RedundantNamespace\" severity=\"error\" />\n\n    <!-- This check crashes if request interval is passed as a param :S -->\n    <issue id=\"InvalidPeriodicWorkRequestInterval\" severity=\"ignore\" />\n    <issue id=\"AlwaysShowAction\" severity=\"ignore\" />\n\n    <issue id=\"InvalidFragmentVersionForActivityResult\" severity=\"ignore\" />\n\n    <!-- This flags IDs used via View Binding: https://issuetracker.google.com/issues/170658134 -->\n    <issue id=\"UnusedIds\" severity=\"ignore\" />\n\n    <!-- This flags calls to `getColumnIndex` incorrectly: https://issuetracker.google.com/issues/202193843 -->\n    <issue id=\"Range\" severity=\"ignore\" />\n</lint>\n"
  },
  {
    "path": "config/pmd-ruleset.xml",
    "content": "<?xml version=\"1.0\"?>\n<ruleset xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" name=\"Android Application Rules\"\n    xmlns=\"http://pmd.sf.net/ruleset/1.0.0\"\n    xsi:noNamespaceSchemaLocation=\"http://pmd.sf.net/ruleset_xml_schema.xsd\"\n    xsi:schemaLocation=\"http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd\">\n\n    <description>Custom rule-set for Android application</description>\n\n    <exclude-pattern>.*/R.java</exclude-pattern>\n    <exclude-pattern>.*/gen/.*</exclude-pattern>\n\n    <rule ref=\"category/java/bestpractices.xml\">\n        <exclude name=\"AbstractClassWithoutAbstractMethod\" />\n        <exclude name=\"ArrayIsStoredDirectly\" />\n        <exclude name=\"AvoidReassigningParameters\" />\n        <exclude name=\"AvoidStringBufferField\" />\n        <exclude name=\"ForLoopCanBeForeach\" />\n        <exclude name=\"ImplicitFunctionalInterface\" />\n        <exclude name=\"JUnitUseExpected\" /> <!-- Favor using fail explicitly to test correct exception -->\n        <exclude name=\"LiteralsFirstInComparisons\" />\n        <exclude name=\"LooseCoupling\" />\n        <exclude name=\"MissingOverride\" />\n        <exclude name=\"NonExhaustiveSwitch\" />\n        <exclude name=\"PreserveStackTrace\" />\n        <exclude name=\"RelianceOnDefaultCharset\" />\n        <exclude name=\"UnitTestContainsTooManyAsserts\" />\n        <exclude name=\"UnitTestShouldIncludeAssert\" />\n        <exclude name=\"UnitTestShouldUseBeforeAnnotation\" />\n        <exclude name=\"UnusedAssignment\" />\n        <exclude name=\"UnusedPrivateMethod\" />\n        <exclude name=\"UseVarargs\" />\n        <exclude name=\"UseTryWithResources\" />\n    </rule>\n\n    <rule ref=\"category/java/codestyle.xml\">\n        <exclude name=\"AtLeastOneConstructor\" />\n        <exclude name=\"CallSuperInConstructor\" />\n        <exclude name=\"ClassNamingConventions\" />\n        <exclude name=\"CommentDefaultAccessModifier\" />\n        <exclude name=\"ConfusingTernary\" />\n        <exclude name=\"EmptyControlStatement\" />\n        <exclude name=\"FieldDeclarationsShouldBeAtStartOfClass\" />\n        <exclude name=\"LongVariable\" />\n        <exclude name=\"LocalVariableCouldBeFinal\" />\n        <exclude name=\"MethodArgumentCouldBeFinal\" />\n        <exclude name=\"MethodNamingConventions\" />\n        <exclude name=\"OnlyOneReturn\" />\n        <exclude name=\"PrematureDeclaration\" />\n        <exclude name=\"ShortClassName\" />\n        <exclude name=\"ShortMethodName\" />\n        <exclude name=\"ShortVariable\" />\n        <exclude name=\"TooManyStaticImports\" />\n        <exclude name=\"UseUnderscoresInNumericLiterals\" />\n        <exclude name=\"LinguisticNaming\" />\n        <exclude name=\"UnnecessaryFullyQualifiedName\" />    <!-- Decided not to use in #6613 -->\n        <exclude name=\"UnnecessaryCast\" />\n        <exclude name=\"UselessParentheses\" />               <!-- Decided not to use in #1519 -->\n    </rule>\n\n    <rule ref=\"category/java/design.xml\">\n        <exclude name=\"AbstractClassWithoutAnyMethod\" />\n        <exclude name=\"AvoidDeeplyNestedIfStmts\" />\n        <exclude name=\"AvoidThrowingRawExceptionTypes\" />\n        <exclude name=\"ClassWithOnlyPrivateConstructorsShouldBeFinal\" />\n        <exclude name=\"CognitiveComplexity\" />\n        <exclude name=\"CollapsibleIfStatements\" />\n        <exclude name=\"CouplingBetweenObjects\" />\n        <exclude name=\"CyclomaticComplexity\" />\n        <exclude name=\"DataClass\" />\n        <exclude name=\"ExceptionAsFlowControl\" />\n        <exclude name=\"ExcessiveImports\" />\n        <exclude name=\"ExcessivePublicCount\" />\n        <exclude name=\"FinalFieldCouldBeStatic\" />\n        <exclude name=\"GodClass\" />\n        <exclude name=\"ImmutableField\" />\n        <exclude name=\"LawOfDemeter\" />\n        <exclude name=\"LoosePackageCoupling\" />\n        <exclude name=\"MutableStaticState\" />\n        <exclude name=\"NcssCount\" />\n        <exclude name=\"NPathComplexity\" />\n        <exclude name=\"PublicMemberInNonPublicType\" />\n        <exclude name=\"SignatureDeclareThrowsException\" />\n        <exclude name=\"SimplifyBooleanReturns\" />\n        <exclude name=\"SwitchDensity\" />\n        <exclude name=\"TooManyFields\" />\n        <exclude name=\"TooManyMethods\" />\n        <exclude name=\"UseObjectForClearerAPI\" />\n        <exclude name=\"AvoidUncheckedExceptionsInSignatures\" />\n        <exclude name=\"ExcessiveParameterList\" />\n    </rule>\n\n    <rule ref=\"category/java/documentation.xml\">\n        <exclude name=\"CommentSize\" />\n        <exclude name=\"CommentRequired\" />\n        <exclude name=\"UncommentedEmptyConstructor\" />\n        <exclude name=\"UncommentedEmptyMethodBody\" />\n        <exclude name=\"DanglingJavadoc\" />\n    </rule>\n\n    <rule ref=\"category/java/errorprone.xml\">\n        <exclude name=\"AssignmentInOperand\" />\n        <exclude name=\"AssignmentToNonFinalStatic\" />\n        <exclude name=\"AvoidAccessibilityAlteration\" />\n        <exclude name=\"AvoidBranchingStatementAsLastInLoop\" />\n        <exclude name=\"AvoidCatchingGenericException\" />\n        <exclude name=\"AvoidCatchingNPE\" />\n        <exclude name=\"AvoidCatchingThrowable\" />\n        <exclude name=\"AvoidDuplicateLiterals\" />\n        <exclude name=\"AvoidFieldNameMatchingMethodName\" />\n        <exclude name=\"AvoidInstanceofChecksInCatchClause\" />\n        <exclude name=\"AvoidLiteralsInIfCondition\" />\n        <exclude name=\"CloseResource\" />\n        <exclude name=\"ConfusingArgumentToVarargsMethod\" />\n        <exclude name=\"ConstructorCallsOverridableMethod\" />\n        <exclude name=\"DetachedTestCase\" />\n        <exclude name=\"EmptyCatchBlock\" />\n        <exclude name=\"ImplicitSwitchFallThrough\" />\n        <exclude name=\"JUnitSpelling\" />\n        <exclude name=\"MissingSerialVersionUID\" />\n        <exclude name=\"MissingStaticMethodInNonInstantiatableClass\" />\n        <exclude name=\"NullAssignment\" />\n        <exclude name=\"ReplaceJavaUtilCalendar\" />\n        <exclude name=\"ReplaceJavaUtilDate\" />\n        <exclude name=\"ReturnEmptyCollectionRatherThanNull\" />\n        <exclude name=\"TestClassWithoutTestCases\" />\n    </rule>\n\n    <rule ref=\"category/java/multithreading.xml\">\n        <exclude name=\"AvoidSynchronizedAtMethodLevel\" />\n        <exclude name=\"AvoidSynchronizedStatement\" />\n        <exclude name=\"DoNotUseThreads\" />\n        <exclude name=\"UseConcurrentHashMap\" />\n    </rule>\n\n    <rule ref=\"category/java/performance.xml\">\n        <exclude name=\"AvoidFileStream\" />\n        <exclude name=\"AvoidInstantiatingObjectsInLoops\" />\n        <exclude name=\"BigIntegerInstantiation\" />\n        <exclude name=\"InefficientEmptyStringCheck\" />\n        <exclude name=\"InefficientStringBuffering\" />\n        <exclude name=\"InsufficientStringBufferDeclaration\" />\n        <exclude name=\"TooFewBranchesForSwitch\" />\n        <exclude name=\"UseArraysAsList\" />\n        <exclude name=\"UseStringBufferForStringAppends\" />\n    </rule>\n\n    <rule ref=\"category/java/security.xml\"/>\n</ruleset>\n"
  },
  {
    "path": "config/quality.gradle",
    "content": "def configDir = \"${project.rootDir}/config\"\n\n//------------------------Checkstyle------------------------//\n\napply plugin: 'checkstyle'\n\ncheckstyle.toolVersion = '12.3.1' // Newer versions require JDK 21\n\ntasks.register(\"checkstyle\", Checkstyle) {\n    configFile file(\"$configDir/checkstyle.xml\")\n\n    ignoreFailures false // Fail early\n    showViolations true\n\n    source 'src'\n    include '**/*.java'\n    exclude '**/gen/**'\n    classpath = files()\n}\n\n//------------------------Pmd------------------------//\n\napply plugin: 'pmd'\n\npmd {\n    toolVersion = '7.21.0'\n}\n\ntasks.register(\"pmd\", Pmd) {\n    ignoreFailures = false // Fail early\n    ruleSetFiles = files(\"$configDir/pmd-ruleset.xml\")\n    ruleSets = []\n\n    source 'src'\n    include '**/*.java'\n    exclude '**/gen/**'\n\n    reports {\n        xml {\n            enabled false\n        }\n        html {\n            enabled true\n        }\n    }\n}\n\n//------------------------ktlint------------------------//\n\napply plugin: \"org.jlleitschuh.gradle.ktlint\"\n\nktlint {\n    version = \"1.8.0\"\n}\n"
  },
  {
    "path": "crash-handler/.gitignore",
    "content": "/build"
  },
  {
    "path": "crash-handler/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.crashhandler\"\n\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":androidshared\"))\n    implementation(project(\":strings\"))\n    implementation(libs.androidMaterial)\n    testImplementation(libs.junit)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.robolectric)\n}\n"
  },
  {
    "path": "crash-handler/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest>\n\n</manifest>\n"
  },
  {
    "path": "crash-handler/src/main/java/org/odk/collect/crashhandler/CrashHandler.kt",
    "content": "package org.odk.collect.crashhandler\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport org.odk.collect.androidshared.data.getState\nimport java.lang.Thread.UncaughtExceptionHandler\nimport kotlin.system.exitProcess\n\nclass CrashHandler(private val processKiller: Runnable = Runnable { exitProcess(0) }) {\n\n    var createMockViews = false\n\n    private var conditionFailure: String? = null\n\n    fun launchApp(conditionsCheck: Runnable, onSuccess: Runnable? = null) {\n        if (checkConditions(conditionsCheck)) {\n            onSuccess?.run()\n        }\n    }\n\n    fun registerCrash(context: Context, crash: Throwable) {\n        getPreferences(context).edit().putString(KEY_CRASH, crash.message ?: \"\").apply()\n    }\n\n    fun hasCrashed(context: Context): Boolean {\n        return getPreferences(context).contains(KEY_CRASH) || conditionFailure != null\n    }\n\n    @JvmOverloads\n    fun getCrashView(context: Context, onErrorDismissed: Runnable? = null): CrashView? {\n        val preferences = getPreferences(context)\n\n        return if (conditionFailure != null) {\n            val crashMessage = conditionFailure\n\n            createCrashView(context).also {\n                it.setCrash(context.getString(org.odk.collect.strings.R.string.cant_start_app), crashMessage) {\n                    processKiller.run()\n                }\n            }\n        } else if (preferences.contains(KEY_CRASH)) {\n            val crashMessage = preferences.getString(KEY_CRASH, null)\n\n            createCrashView(context).also {\n                it.setCrash(context.getString(org.odk.collect.strings.R.string.crash_last_run), crashMessage) {\n                    preferences.edit().remove(KEY_CRASH).apply()\n                    onErrorDismissed?.run()\n                }\n            }\n        } else {\n            null\n        }\n    }\n\n    private fun checkConditions(runnable: Runnable): Boolean {\n        return try {\n            runnable.run()\n            true\n        } catch (t: Throwable) {\n            conditionFailure = t.message ?: \"\"\n            false\n        }\n    }\n\n    private fun createCrashView(context: Context): CrashView {\n        return if (createMockViews) {\n            MockCrashView(context)\n        } else {\n            CrashView(context)\n        }\n    }\n\n    /**\n     * Uses raw [SharedPreferences] instead of [Settings] to keep dependencies to a minimum.\n     * [CrashHandler] might have to work in cases where dependency injection cannot be configured\n     * due to an early crash or conditions check failing.\n     */\n    private fun getPreferences(context: Context) =\n        context.getSharedPreferences(KEY_INSTANCE, Context.MODE_PRIVATE)\n\n    companion object {\n\n        private const val KEY_CRASH = \"crash\"\n        private const val KEY_INSTANCE = \"crash_handler\"\n\n        private var originalHandler: UncaughtExceptionHandler? = null\n\n        @JvmStatic\n        fun install(context: Context): CrashHandler {\n            return CrashHandler().also {\n                context.getState().set(\"crash_handler\", it)\n                wrapUncaughtExceptionHandler(it, context)\n            }\n        }\n\n        @JvmStatic\n        fun uninstall(context: Context) {\n            context.getState().set(\"crash_handler\", null)\n            unwrapUncaughtExceptionHandler()\n        }\n\n        @JvmStatic\n        fun getInstance(context: Context): CrashHandler? {\n            return context.getState().get<CrashHandler>(KEY_INSTANCE)\n        }\n\n        private fun wrapUncaughtExceptionHandler(\n            crashHandler: CrashHandler,\n            context: Context\n        ) {\n            if (originalHandler != null) {\n                throw IllegalStateException(\"install() should not be called multiple times without uninstall()!\")\n            }\n\n            val defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler().also {\n                originalHandler = it\n            }\n\n            Thread.setDefaultUncaughtExceptionHandler { t, e ->\n                crashHandler.registerCrash(context, e)\n                defaultUncaughtExceptionHandler?.uncaughtException(t, e)\n            }\n        }\n\n        private fun unwrapUncaughtExceptionHandler() {\n            Thread.setDefaultUncaughtExceptionHandler(originalHandler)\n            originalHandler = null\n        }\n    }\n}\n"
  },
  {
    "path": "crash-handler/src/main/java/org/odk/collect/crashhandler/CrashView.kt",
    "content": "package org.odk.collect.crashhandler\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport org.odk.collect.crashhandler.databinding.CrashLayoutBinding\n\nopen class CrashView(context: Context) : FrameLayout(context) {\n\n    private var onErrorDismissed: Runnable? = null\n    private val binding = CrashLayoutBinding.inflate(LayoutInflater.from(context), this, true)\n\n    init {\n        binding.okButton.setOnClickListener { dismiss() }\n    }\n\n    internal fun setCrash(title: String, message: String?, onErrorDismissed: Runnable? = null) {\n        binding.title.text = title\n        binding.message.text = message\n        this.onErrorDismissed = onErrorDismissed\n    }\n\n    open fun dismiss() {\n        onErrorDismissed?.run()\n    }\n}\n"
  },
  {
    "path": "crash-handler/src/main/java/org/odk/collect/crashhandler/MockCrashView.kt",
    "content": "package org.odk.collect.crashhandler\n\nimport android.content.Context\n\nclass MockCrashView(context: Context) : CrashView(context) {\n\n    var wasDismissed: Boolean = false\n        private set\n\n    override fun dismiss() {\n        wasDismissed = true\n    }\n}\n"
  },
  {
    "path": "crash-handler/src/main/res/layout/crash_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"@dimen/margin_standard\">\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/title\"\n        style=\"?textAppearanceTitleLarge\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginHorizontal=\"@dimen/margin_extra_large\"\n        android:gravity=\"center\"\n        android:textAlignment=\"center\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:text=\"There was a crash last time the app was running!\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/message\"\n        style=\"?textAppearanceBodyLarge\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginHorizontal=\"@dimen/margin_extra_large\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:gravity=\"center\"\n        android:textAlignment=\"center\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/title\"\n        tools:text=\"It exploded!\" />\n\n    <Button\n        android:id=\"@+id/ok_button\"\n        style=\"?materialButtonStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:text=\"@string/ok\"\n        app:layout_constraintEnd_toEndOf=\"@id/title\"\n        app:layout_constraintStart_toStartOf=\"@id/title\"\n        app:layout_constraintTop_toBottomOf=\"@id/message\" />\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:layout_width=\"64dp\"\n        android:layout_height=\"64dp\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:src=\"@drawable/ic_baseline_warning_24\"\n        app:layout_constraintBottom_toTopOf=\"@id/title\"\n        app:layout_constraintEnd_toEndOf=\"@id/title\"\n        app:layout_constraintStart_toStartOf=\"@id/title\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "crash-handler/src/test/java/org/odk/collect/crashhandler/CrashHandlerTest.kt",
    "content": "package org.odk.collect.crashhandler\n\nimport android.content.Context\nimport android.view.View\nimport android.widget.TextView\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.notNullValue\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.never\nimport org.mockito.kotlin.verify\n\n@RunWith(AndroidJUnit4::class)\nclass CrashHandlerTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>().also {\n        it.setTheme(com.google.android.material.R.style.Theme_MaterialComponents)\n    }\n\n    private val processKiller = mock<Runnable>()\n\n    @Test\n    fun hasCrashed_whenThereAreNoCrashesRegistered_andNoConditionsFailed_returnsFalse() {\n        val crashHandler = createCrashHandler()\n        crashHandler.launchApp({})\n        assertThat(crashHandler.hasCrashed(context), equalTo(false))\n    }\n\n    @Test\n    fun hasCrashed_whenThereHasBeenACrashRegistered_returnsTrue() {\n        createCrashHandler().registerCrash(context, RuntimeException())\n        assertThat(createCrashHandler().hasCrashed(context), equalTo(true))\n    }\n\n    @Test\n    fun hasCrashed_whenCheckingConditionsFails_returnsTrue() {\n        val crashHandler = createCrashHandler()\n        crashHandler.launchApp({ throw RuntimeException() })\n        assertThat(crashHandler.hasCrashed(context), equalTo(true))\n    }\n\n    @Test\n    fun getCrashView_whenThereAreNoCrashesRegistered_returnsNull() {\n        assertThat(createCrashHandler().getCrashView(context), equalTo(null))\n    }\n\n    @Test\n    fun getCrashView_whenThereHasBeenACrashRegistered_returnsViewWithCrashDetails() {\n        createCrashHandler().registerCrash(context, RuntimeException(\"crash!\"))\n\n        val view = createCrashHandler().getCrashView(context)\n        assertThat(\n            view!!.findViewById<TextView>(R.id.title).text,\n            equalTo(context.getString(org.odk.collect.strings.R.string.crash_last_run))\n        )\n        assertThat(view.findViewById<TextView>(R.id.message).text, equalTo(\"crash!\"))\n    }\n\n    @Test\n    fun getCrashView_whenThereHasBeenACrashRegistered_andTheOkButtonIsClickedOnTheView_runsTheOnErrorDismissedListener() {\n        createCrashHandler().registerCrash(context, RuntimeException())\n\n        val onErrorDismissed = mock<Runnable>()\n        val view = createCrashHandler().getCrashView(context, onErrorDismissed)\n        view!!.findViewById<View>(R.id.ok_button).performClick()\n        verify(onErrorDismissed).run()\n    }\n\n    @Test\n    fun getCrashView_whenThereHasBeenACrashRegistered_andTheOkButtonIsClickedOnTheView_doesNotRunProcessKiller() {\n        createCrashHandler().registerCrash(context, RuntimeException())\n\n        val onErrorDismissed = mock<Runnable>()\n        val view = createCrashHandler().getCrashView(context, onErrorDismissed)\n        view!!.findViewById<View>(R.id.ok_button).performClick()\n        verify(processKiller, never()).run()\n    }\n\n    @Test\n    fun getCrashView_whenThereHasBeenACrashRegistered_andTheOkButtonIsClickedOnTheView_clearsCrash() {\n        createCrashHandler().registerCrash(context, RuntimeException())\n\n        // Check crash view is returned for multiple instances (for rotates etc)\n        assertThat(createCrashHandler().getCrashView(context), notNullValue())\n\n        val onErrorDismissed = mock<Runnable>()\n        val view = createCrashHandler().getCrashView(context, onErrorDismissed)\n\n        view!!.findViewById<View>(R.id.ok_button).performClick()\n        assertThat(createCrashHandler().getCrashView(context), equalTo(null))\n    }\n\n    @Test\n    fun getCrashView_whenCheckingConditionsFails_returnsViewWithCrashDetails() {\n        val crashHandler = createCrashHandler()\n\n        crashHandler.launchApp({\n            throw RuntimeException(\"blah\")\n        })\n\n        val view = crashHandler.getCrashView(context)\n        assertThat(\n            view!!.findViewById<TextView>(R.id.title).text,\n            equalTo(context.getString(org.odk.collect.strings.R.string.cant_start_app))\n        )\n        assertThat(view.findViewById<TextView>(R.id.message).text, equalTo(\"blah\"))\n    }\n\n    @Test\n    fun getCrashView_whenCheckingConditionsFails_andTheOkButtonIsClickedOnTheView_killsTheProcess() {\n        val crashHandler = createCrashHandler()\n\n        crashHandler.launchApp({\n            throw RuntimeException(\"blah\")\n        })\n\n        val view = crashHandler.getCrashView(context)\n        view!!.findViewById<View>(R.id.ok_button).performClick()\n        verify(processKiller).run()\n    }\n\n    @Test\n    fun getCrashView_whenCheckingConditionsFails_andTheOkButtonIsClickedOnTheView_doesNotRunErrorDismissedListener() {\n        val crashHandler = createCrashHandler()\n\n        crashHandler.launchApp({\n            throw RuntimeException(\"blah\")\n        })\n\n        val onErrorDismissed = mock<Runnable>()\n        val view = crashHandler.getCrashView(context, onErrorDismissed)\n        view!!.findViewById<View>(R.id.ok_button).performClick()\n        verify(onErrorDismissed, never()).run()\n    }\n\n    @Test\n    fun getCrashView_whenCheckConditionFailedInDifferentInstance_returnsNull() {\n        val crashHandler = createCrashHandler()\n        crashHandler.launchApp({\n            throw RuntimeException(\"blah\")\n        })\n\n        val otherCrashHandler = createCrashHandler()\n        assertThat(otherCrashHandler.getCrashView(context), equalTo(null))\n    }\n\n    @Test\n    fun getCrashView_whenThereHasBeenACrashRegistered_andCheckConditionFailed_andTheOkButtonIsClickedOnTheView_runsProcessKiller() {\n        createCrashHandler().registerCrash(context, RuntimeException())\n\n        val crashHandler = createCrashHandler()\n        crashHandler.launchApp({\n            throw RuntimeException(\"blah\")\n        })\n\n        val view = crashHandler.getCrashView(context)\n        view!!.findViewById<View>(R.id.ok_button).performClick()\n        verify(processKiller).run()\n    }\n\n    @Test\n    fun launchApp_whenConditionsFail_runsOnSuccess() {\n        val crashHandler = createCrashHandler()\n\n        val onSuccess = mock<Runnable>()\n        crashHandler.launchApp(conditionsCheck = { }, onSuccess = onSuccess)\n        verify(onSuccess).run()\n    }\n\n    @Test\n    fun launchApp_whenSomethingFails_doesNotRunOnSuccess() {\n        val crashHandler = createCrashHandler()\n\n        val onSuccess = mock<Runnable>()\n        crashHandler.launchApp(\n            conditionsCheck = { throw RuntimeException() },\n            onSuccess = onSuccess\n        )\n        verify(onSuccess, never()).run()\n    }\n\n    private fun createCrashHandler() = CrashHandler(processKiller)\n}\n"
  },
  {
    "path": "create-release.sh",
    "content": "set -e\n\n[ \"$1\" = \"-h\" -o \"$1\" = \"--help\" ] && echo \"\n    Usage: `basename $0` lastReleasedVersionCode nextReleaseTag [additionalReleaseTag]\n\n    Creates up to two releases with versionCodes incremented from lastReleasedVersionCode. The last\n    released version code will typically be the last version code published on the Play Store (beta\n    or production). Two releases are needed when a patch is needed to the last production release\n    and a beta is already ongoing for the next release.\n\" && exit\n\nmkdir -p apks\nrm -f apks/*\n\nlast_version_code=$1\n\ngit checkout $2\n./gradlew clean\nrelease_version_code=$((last_version_code + 1))\n./gradlew assembleOdkCollectRelease -PversionCode=$release_version_code\ncp collect_app/build/outputs/apk/odkCollectRelease/*.apk apks\n\nif [[ $# -gt 2 ]]; then\n    git checkout $3\n    ./gradlew clean\n    beta_version_code=$((release_version_code + 1))\n    ./gradlew assembleOdkCollectRelease -PversionCode=$beta_version_code\n    cp collect_app/build/outputs/apk/odkCollectRelease/*.apk apks\nfi\n\nopen apks\n"
  },
  {
    "path": "db/.gitignore",
    "content": "/build"
  },
  {
    "path": "db/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.db\"\n\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":shared\"))\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.timber)\n\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.robolectric)\n}\n"
  },
  {
    "path": "db/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/AltDatabasePathContext.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport android.content.Context\nimport android.content.ContextWrapper\nimport android.database.DatabaseErrorHandler\nimport android.database.sqlite.SQLiteDatabase\nimport java.io.File\n\n/**\n * Allows creating an [SQLiteOpenHelper] that references a `.db` somewhere other than the standard\n * Android database path (by passing this as [Context] to the constructor).\n */\nclass AltDatabasePathContext(private val path: String, context: Context) : ContextWrapper(context) {\n\n    override fun getDatabasePath(name: String): File {\n        return File(path + File.separator + name)\n    }\n\n    override fun openOrCreateDatabase(\n        name: String,\n        mode: Int,\n        factory: SQLiteDatabase.CursorFactory?,\n        errorHandler: DatabaseErrorHandler?\n    ): SQLiteDatabase {\n        return openOrCreateDatabase(name, mode, factory)\n    }\n\n    override fun openOrCreateDatabase(\n        name: String,\n        mode: Int,\n        factory: SQLiteDatabase.CursorFactory?\n    ): SQLiteDatabase {\n        return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null)\n    }\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/CursorExt.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport android.database.Cursor\nimport androidx.core.database.getIntOrNull\nimport androidx.core.database.getLongOrNull\nimport androidx.core.database.getStringOrNull\nimport org.odk.collect.db.sqlite.CursorExt.getInt\nimport org.odk.collect.db.sqlite.CursorExt.getLong\n\nobject CursorExt {\n    fun <T> Cursor.foldAndClose(initial: T, operation: (T, Cursor) -> T): T {\n        return this.use {\n            it.moveToPosition(-1)\n\n            var accumulator = initial\n            while (it.moveToNext()) {\n                accumulator = operation(accumulator, it)\n            }\n\n            accumulator\n        }\n    }\n\n    fun <T> Cursor.foldAndClose(operation: (Cursor) -> T): List<T> {\n        return this.use {\n            it.moveToPosition(-1)\n\n            val accumulator = ArrayList<T>(it.count)\n            while (it.moveToNext()) {\n                accumulator.add(operation(it))\n            }\n\n            accumulator\n        }\n    }\n\n    fun <T> Cursor.first(map: (Cursor) -> T): T? {\n        return this.use {\n            if (it.moveToFirst()) {\n                map(it)\n            } else {\n                return null\n            }\n        }\n    }\n\n    fun Cursor.getString(column: String): String {\n        val columnIndex = this.getColumnIndex(column)\n        return this.getString(columnIndex)\n    }\n\n    fun Cursor.getStringOrNull(column: String): String? {\n        val columnIndex = this.getColumnIndex(column)\n        return this.getStringOrNull(columnIndex)\n    }\n\n    fun Cursor.getLong(column: String): Long {\n        val columnIndex = this.getColumnIndex(column)\n        return this.getLong(columnIndex)\n    }\n\n    fun Cursor.getLongOrNull(column: String): Long? {\n        val columnIndex = this.getColumnIndex(column)\n        return this.getLongOrNull(columnIndex)\n    }\n\n    fun Cursor.getInt(column: String): Int {\n        val columnIndex = this.getColumnIndex(column)\n        return this.getInt(columnIndex)\n    }\n\n    fun Cursor.getIntOrNull(column: String): Int? {\n        val columnIndex = this.getColumnIndex(column)\n        return this.getIntOrNull(columnIndex)\n    }\n\n    /**\n     * Translates Integer column to Boolean as described [here](https://sqlite.org/datatype3.html).\n     */\n    fun Cursor.getBoolean(column: String): Boolean {\n        val columnIndex = this.getColumnIndex(column)\n        return this.getInt(columnIndex) == 1\n    }\n\n    /**\n     * Prevents doing repeated [Cursor.getColumnIndex] lookups and also works around the lack of\n     * support for column names including a \".\" there (due to the mysterious bug 903852).\n     *\n     * @see [SQLiteCursor source](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/database/sqlite/SQLiteCursor.java;l=178?q=sqlitecursor)\n     */\n    fun Cursor.rowToMap(): Map<String, String> {\n        return this.columnNames.foldIndexed(mutableMapOf()) { index, map, column ->\n            map[column] = this.getString(index)\n            map\n        }\n    }\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/CustomSQLiteQueryBuilder.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.db.sqlite;\n\nimport org.odk.collect.shared.strings.StringUtils;\n\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class CustomSQLiteQueryBuilder {\n    protected static final String SPACE = \" \";\n    protected static final String LIST_SEPARATOR = \", \";\n    protected static final String SEMICOLON = \";\";\n\n    protected StringBuilder query;\n\n    CustomSQLiteQueryBuilder() {\n        query = new StringBuilder();\n    }\n\n    public void end() {}\n\n    public String getQueryString() {\n        return query.toString();\n    }\n\n    public static String quoteIdentifier(String unquoted) {\n        return \"\\\"\" + unquoted + \"\\\"\";\n    }\n\n    public static String quoteStringLiteral(String unquoted) {\n        return \"\\'\" + unquoted + \"\\'\";\n    }\n\n    public CustomSQLiteQueryBuilder select() {\n        query.append(\"SELECT\").append(SPACE);\n        return this;\n    }\n\n    @SuppressWarnings(\"PMD.ConsecutiveLiteralAppends\")\n    public CustomSQLiteQueryBuilder columnsForInsert(String... columns) {\n        query.append('(');\n        columnsForSelect(columns);\n        query.append(')').append(SPACE);\n        return this;\n    }\n\n    public CustomSQLiteQueryBuilder columnsForSelect(String... columns) {\n        for (String column : columns) {\n            query.append(column).append(',');\n        }\n        int lastCommaIndex = query.lastIndexOf(\",\");\n        query.deleteCharAt(lastCommaIndex).append(SPACE);\n        return this;\n    }\n\n    public CustomSQLiteQueryBuilder from(String table) {\n        query.append(\"FROM\").append(SPACE).append(table).append(SPACE);\n        return this;\n    }\n\n    public CustomSQLiteQueryBuilder where(String selectCriteria) {\n        query.append(\"WHERE\").append(SPACE).append(selectCriteria).append(SPACE);\n        return this;\n    }\n\n    public CustomSQLiteQueryBuilder where(String[] selectCriteria) {\n        return where(formatLogicalAnd(selectCriteria));\n    }\n\n    public static String formatCompareEquals(String left, String right) {\n        return left + \" = \" + right;\n    }\n\n    public static String formatLogicalAnd(String[] criteria) {\n        return StringUtils.join(\" AND \", Arrays.asList(criteria));\n    }\n\n    public CustomSQLiteQueryBuilder renameTable(String table) {\n        query.append(\"ALTER TABLE\").append(SPACE).append(table).append(SPACE).append(\"RENAME TO\").append(SPACE);\n        return this;\n    }\n\n    public CustomSQLiteQueryBuilder to(String table) {\n        query.append(table);\n        return this;\n    }\n\n    public CustomSQLiteQueryBuilder dropIfExists(String table) {\n        query.append(\"DROP TABLE IF EXISTS\").append(SPACE).append(table).append(SPACE);\n        return this;\n    }\n\n    public CustomSQLiteQueryBuilder insertInto(String table) {\n        query.append(\"INSERT INTO\").append(SPACE).append(table);\n        return this;\n    }\n\n    public CustomSQLiteQueryBuilder createTable(final String tableName) {\n        query.append(\"CREATE\").append(SPACE);\n        return table(tableName);\n    }\n\n    public CustomSQLiteQueryBuilder columnsForCreate(List<String> columnDefinitions) {\n        query.append('(').append(StringUtils.join(LIST_SEPARATOR, columnDefinitions)).append(')');\n        return this;\n    }\n\n    public static String formatColumnDefinition(String columnName, String columnType) {\n        return columnName + SPACE + columnType;\n    }\n\n    public CustomSQLiteQueryBuilder alter() {\n        query.append(\"ALTER\").append(SPACE);\n        return this;\n    }\n\n    public CustomSQLiteQueryBuilder table(final String table) {\n        query.append(\"TABLE\").append(SPACE).append(table).append(SPACE);\n        return this;\n    }\n\n    public CustomSQLiteQueryBuilder addColumn(final String columnName, final String columnType) {\n        query.append(\"ADD COLUMN\").append(SPACE).append(columnName).append(SPACE).append(columnType);\n        return this;\n    }\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/CustomSQLiteQueryExecutor.java",
    "content": "package org.odk.collect.db.sqlite;\n\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteException;\n\npublic final class CustomSQLiteQueryExecutor extends CustomSQLiteQueryBuilder {\n    private final SQLiteDatabase db;\n\n    private CustomSQLiteQueryExecutor(SQLiteDatabase db) {\n        super();\n        this.db = db;\n    }\n\n    public static CustomSQLiteQueryExecutor begin(SQLiteDatabase db) {\n        return new CustomSQLiteQueryExecutor(db);\n    }\n\n    @Override\n    public void end() throws SQLiteException {\n        query.append(SEMICOLON);\n        db.execSQL(query.toString());\n    }\n\n    public Cursor query()  throws SQLiteException {\n        query.append(SEMICOLON);\n        return db.rawQuery(query.toString(), null);\n    }\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/DatabaseConnection.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport android.content.Context\nimport android.database.Cursor\nimport android.database.sqlite.SQLiteDatabase\nimport android.database.sqlite.SQLiteDatabase.CursorFactory\nimport android.database.sqlite.SQLiteOpenHelper\nimport android.os.StrictMode\nimport timber.log.Timber\nimport java.io.File\n\n/**\n * Allows access to a database file. The actual underlying connection (an instance of\n * [SQLiteOpenHelper] to this database will be reused for different instances of\n * [DatabaseConnection] that refer to the same file.\n *\n * @param migrator used to migrate or create the database automatically before access\n */\nopen class DatabaseConnection @JvmOverloads constructor(\n    private val context: Context,\n    private val path: String,\n    private val name: String,\n    private val migrator: DatabaseMigrator,\n    private val databaseVersion: Int,\n    private val strict: Boolean = false\n) {\n\n    private val databasePath = path + File.separator + name\n\n    val writableDatabase: SQLiteDatabase\n        get() {\n            StrictMode.noteSlowCall(\"Accessing writable DB\")\n            return dbHelper.writableDatabase\n        }\n\n    val readableDatabase: SQLiteDatabase\n        get() {\n            if (strict) {\n                StrictMode.noteSlowCall(\"Accessing readable DB\")\n            }\n            return dbHelper.readableDatabase\n        }\n\n    private val dbHelper: SQLiteOpenHelper\n        get() {\n            return synchronized(openHelpers) {\n                if (openHelpers.containsKey(databasePath) && !File(databasePath).exists()) {\n                    /**\n                     * Ideally we should close the database here as well but it was causing crashes in\n                     * our tests as DB connections seem to be getting used after being closed. These\n                     * \"removed\" helpers will be closed in [closeAll] rather than when they are\n                     * replaced.\n                     */\n                    openHelpers.remove(databasePath)?.let {\n                        toClose.add(it)\n                    }\n                }\n\n                openHelpers.getOrPut(databasePath) {\n                    DatabaseMigratorSQLiteOpenHelper(\n                        AltDatabasePathContext(path, context),\n                        name,\n                        null,\n                        databaseVersion,\n                        migrator\n                    )\n                }\n            }\n        }\n\n    /**\n     * Closes the underlying connection and clears state so that subsequent accesses will create\n     * a new connection.\n     *\n     * This can be dangerous if the database is being access by multiple threads as the connection\n     * might be closed while a transaction is running or while another thread is using a\n     * [SQLiteDatabase] instance obtained via [writableDatabase] or [readableDatabase]. Using\n     * [SynchronizedDatabaseConnection] is recommended in those scenarios.\n     */\n    fun reset() {\n        openHelpers.remove(databasePath)?.close()\n    }\n\n    /**\n     * Access the database in a synchronized manner. This is not usually required, but should be\n     * used if a calls to [reset] will be made.\n     *\n     * Does not guarantee synchronized access if this or another [DatabaseConnection] for the\n     * same `.db` file uses [writableDatabase] or [readableDatabase].\n     * [SynchronizedDatabaseConnection] can be used to ensure synchronized writes/reads to the\n     * underlying DB.\n     */\n    fun <T> withSynchronizedConnection(block: DatabaseConnection.() -> T): T {\n        return synchronized(dbHelper) {\n            val result = block(this)\n\n            if (result !is Cursor) {\n                result\n            } else {\n                throw IllegalStateException(\"Returning a Cursor removes synchronized guarantees!\")\n            }\n        }\n    }\n\n    companion object {\n\n        private val openHelpers = mutableMapOf<String, SQLiteOpenHelper>()\n        private val toClose = mutableListOf<SQLiteOpenHelper>()\n\n        @JvmStatic\n        fun cleanUp() {\n            val openHelpersToClear = mutableListOf<String>()\n\n            openHelpers.forEach { (databasePath, openHelper) ->\n                if (!File(databasePath).exists()) {\n                    openHelper.close()\n                    openHelpersToClear.add(databasePath)\n                }\n            }\n\n            openHelpersToClear.forEach {\n                openHelpers.remove(it)\n            }\n        }\n\n        @JvmStatic\n        fun closeAll() {\n            openHelpers.forEach { (_, openHelper) -> openHelper.close() }\n            openHelpers.clear()\n\n            toClose.forEach(SQLiteOpenHelper::close)\n            toClose.clear()\n        }\n    }\n}\n\n/**\n * [SQLiteOpenHelper] that delegates `onCreate`, `onUpdate` to a [DatabaseMigrator].\n */\nprivate class DatabaseMigratorSQLiteOpenHelper(\n    context: Context,\n    name: String,\n    cursorFactory: CursorFactory?,\n    version: Int,\n    private val databaseMigrator: DatabaseMigrator\n) : SQLiteOpenHelper(context, name, cursorFactory, version) {\n\n    override fun onCreate(db: SQLiteDatabase?) {\n        databaseMigrator.onCreate(db)\n    }\n\n    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {\n        Timber.i(\"Upgrading database from version %d to %d\", oldVersion, newVersion)\n        databaseMigrator.onUpgrade(db, oldVersion)\n        Timber.i(\n            \"Upgrading database from version %d to %d completed with success.\",\n            oldVersion,\n            newVersion\n        )\n    }\n\n    override fun onConfigure(db: SQLiteDatabase?) {\n        super.onConfigure(db)\n        db?.setForeignKeyConstraintsEnabled(true)\n    }\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/DatabaseMigrator.java",
    "content": "package org.odk.collect.db.sqlite;\n\nimport android.database.sqlite.SQLiteDatabase;\n\npublic interface DatabaseMigrator {\n    void onCreate(SQLiteDatabase db);\n\n    void onUpgrade(SQLiteDatabase db, int oldVersion);\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/MigrationListDatabaseMigrator.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport android.database.sqlite.SQLiteDatabase\n\nabstract class MigrationListDatabaseMigrator(private val databaseVersion: Int, vararg migrations: ((SQLiteDatabase) -> Unit)) :\n    DatabaseMigrator {\n\n    private val migrations = migrations.toList()\n\n    override fun onCreate(db: SQLiteDatabase) {\n        createDbForVersion(db, databaseVersion)\n    }\n\n    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int) {\n        val migrationsForVersion = if (oldVersion <= 1) {\n            migrations\n        } else {\n            migrations.drop(oldVersion - 1)\n        }\n\n        migrationsForVersion.forEach { it(db) }\n    }\n\n    abstract fun createDbForVersion(db: SQLiteDatabase, version: Int)\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/RowNumbers.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport android.database.Cursor\nimport org.odk.collect.db.sqlite.SQLiteColumns.ROW_ID\nimport org.odk.collect.db.sqlite.SQLiteColumns.ROW_NUMBER\n\nobject RowNumbers {\n    fun <T> SynchronizedDatabaseConnection.rawQueryWithRowNumber(table: String, selection: String? = null, selectionArgs: Array<String>? = null, cursorMapper: (Cursor) -> T): T {\n        this.ensureRowIdTable(table)\n\n        return if (selection != null) {\n            this.withConnection {\n                val cursor = readableDatabase\n                    .rawQuery(\n                        \"\"\"\n                        SELECT *, i.$ROW_ID as $ROW_NUMBER\n                        FROM \"$table\" e, \"${getRowIdTableName(table)}\" i\n                        WHERE e._id = i._id AND $selection\n                        ORDER BY i.$ROW_ID\n                        \"\"\".trimIndent(),\n                        selectionArgs\n                    )\n\n                cursorMapper(cursor)\n            }\n        } else {\n            this.withConnection {\n                val cursor = readableDatabase\n                    .rawQuery(\n                        \"\"\"\n                        SELECT *, i.$ROW_ID as $ROW_NUMBER\n                        FROM \"$table\" e, \"${getRowIdTableName(table)}\" i\n                        WHERE e._id = i._id\n                        ORDER BY i.$ROW_ID\n                        \"\"\".trimIndent(),\n                        null\n                    )\n\n                cursorMapper(cursor)\n            }\n        }\n    }\n\n    fun SynchronizedDatabaseConnection.invalidateRowNumbers(table: String) {\n        this.resetTransaction {\n            execSQL(\n                \"\"\"\n                DROP TABLE IF EXISTS \"${getRowIdTableName(table)}\";\n                \"\"\".trimIndent()\n            )\n        }\n    }\n\n    private fun SynchronizedDatabaseConnection.ensureRowIdTable(table: String) {\n        resetTransaction {\n            execSQL(\n                \"\"\"\n                CREATE TABLE IF NOT EXISTS \"${getRowIdTableName(table)}\" AS SELECT _id FROM \"$table\" ORDER BY _id;\n                \"\"\".trimIndent()\n            )\n        }\n    }\n\n    private fun getRowIdTableName(it: String) = \"${it}_row_numbers\"\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/SQLiteColumns.kt",
    "content": "package org.odk.collect.db.sqlite\n\nobject SQLiteColumns {\n    const val ROW_ID = \"rowid\"\n    const val ROW_NUMBER = \"row_number\"\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/SQLiteDatabaseExt.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport android.database.Cursor\nimport android.database.sqlite.SQLiteDatabase\nimport android.database.sqlite.SQLiteQueryBuilder\n\nobject SQLiteDatabaseExt {\n    fun SQLiteDatabase.query(\n        table: String,\n        selection: String? = null,\n        selectionArgs: Array<String?>? = null\n    ): Cursor {\n        val qb = SQLiteQueryBuilder().apply {\n            tables = table\n        }\n\n        return qb.query(this, null, selection, selectionArgs, null, null, null)\n    }\n\n    fun SQLiteDatabase.delete(table: String) {\n        this.delete(table, null, null)\n    }\n\n    @JvmStatic\n    fun SQLiteDatabase.doesColumnExist(table: String, column: String): Boolean {\n        return this.getColumnNames(table).contains(column)\n    }\n\n    @JvmStatic\n    fun SQLiteDatabase.getColumnNames(table: String): List<String> {\n        var columnNames: Array<String>\n        this.query(table, null, null, null, null, null, null).use { c ->\n            columnNames = c.columnNames\n        }\n\n        return columnNames.toList()\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun SQLiteDatabase.addColumn(\n        table: String,\n        column: String,\n        type: String,\n        default: String? = null\n    ) {\n        if (default != null) {\n            this.execSQL(\n                \"\"\"\n                ALTER TABLE \"$table\" ADD \"$column\" $type DEFAULT $default;\n                \"\"\".trimIndent()\n            )\n        } else {\n            this.execSQL(\n                \"\"\"\n                ALTER TABLE \"$table\" ADD \"$column\" $type;\n                \"\"\".trimIndent()\n            )\n        }\n    }\n\n    fun SQLiteDatabase.dropTable(table: String) {\n        this.execSQL(\"\"\"DROP TABLE \"$table\";\"\"\")\n    }\n\n    fun SQLiteDatabase.renameTable(oldTable: String, newTable: String) {\n        this.execSQL(\"\"\"ALTER TABLE \"$oldTable\" RENAME TO \"$newTable\";\"\"\")\n    }\n\n    fun SQLiteDatabase.copyTableContent(oldTable: String, newTable: String, columns: List<String>) {\n        this.execSQL(\"\"\"INSERT INTO \"$newTable\" SELECT ${columns.joinToString { \"\\\"$it\\\"\" }} FROM \"$oldTable\";\"\"\")\n    }\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/SQLiteUtils.java",
    "content": "package org.odk.collect.db.sqlite;\n\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\n\n/**\n * @deprecated use {@link SQLiteDatabaseExt} instead.\n */\n@Deprecated\npublic final class SQLiteUtils {\n    private SQLiteUtils() {\n    }\n\n    public static boolean doesTableExist(SQLiteDatabase db, String tableName) {\n        final String sqliteSystemTable = CustomSQLiteQueryBuilder.quoteIdentifier(\"sqlite_master\");\n        final String nameColumn = CustomSQLiteQueryBuilder.quoteIdentifier(\"name\");\n        final String typeColumn = CustomSQLiteQueryBuilder.quoteIdentifier(\"type\");\n        final String tableLiteral = CustomSQLiteQueryBuilder.quoteStringLiteral(\"table\");\n        final String tableNameLiteral = CustomSQLiteQueryBuilder.quoteStringLiteral(tableName);\n\n        final String[] columnsToSelect = {nameColumn};\n        final String[] selectCriteria = {\n                CustomSQLiteQueryBuilder.formatCompareEquals(typeColumn, tableLiteral),\n                CustomSQLiteQueryBuilder.formatCompareEquals(nameColumn, tableNameLiteral)\n        };\n\n        Cursor cursor = db.query(sqliteSystemTable, columnsToSelect, CustomSQLiteQueryBuilder.formatLogicalAnd(selectCriteria), null, null, null, null);\n        boolean foundTable = cursor.getCount() == 1;\n        cursor.close();\n        return foundTable;\n    }\n\n    public static void renameTable(SQLiteDatabase db, String table, String newTable) {\n        CustomSQLiteQueryExecutor.begin(db)\n            .renameTable(table).to(newTable)\n            .end();\n    }\n\n    public static void copyRows(SQLiteDatabase db, String srcTable, String[] columns, String dstTable) {\n        CustomSQLiteQueryExecutor.begin(db)\n            .insertInto(dstTable).columnsForInsert(columns)\n                .select().columnsForSelect(columns).from(srcTable)\n            .end();\n    }\n\n    public static void dropTable(SQLiteDatabase db, String table) {\n        CustomSQLiteQueryExecutor.begin(db)\n            .dropIfExists(table)\n            .end();\n    }\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/SqlQuery.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport org.odk.collect.shared.Query\n\ndata class SqlQuery(\n    val selection: String,\n    val selectionArgs: Array<String>\n)\n\nfun Query.toSql(): SqlQuery {\n    return when (this) {\n        is Query.StringEq -> SqlQuery(\"$column = ?\", arrayOf(value))\n        is Query.StringNotEq -> SqlQuery(\"$column != ?\", arrayOf(value))\n        is Query.NumericEq -> SqlQuery(\"CAST($column AS REAL) = CAST(? AS REAL)\", arrayOf(value.toString()))\n        is Query.NumericNotEq -> SqlQuery(\"CAST($column AS REAL) != CAST(? AS REAL)\", arrayOf(value.toString()))\n        is Query.And -> {\n            val sqlA = queryA.toSql()\n            val sqlB = queryB.toSql()\n            SqlQuery(\n                \"(${sqlA.selection} AND ${sqlB.selection})\",\n                sqlA.selectionArgs + sqlB.selectionArgs\n            )\n        }\n        is Query.Or -> {\n            val sqlA = queryA.toSql()\n            val sqlB = queryB.toSql()\n            SqlQuery(\n                \"(${sqlA.selection} OR ${sqlB.selection})\",\n                sqlA.selectionArgs + sqlB.selectionArgs\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "db/src/main/java/org/odk/collect/db/sqlite/SynchronizedDatabaseConnection.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport android.content.Context\nimport android.database.sqlite.SQLiteDatabase\nimport androidx.core.database.sqlite.transaction\n\nclass SynchronizedDatabaseConnection(\n    context: Context,\n    path: String,\n    name: String,\n    migrator: DatabaseMigrator,\n    databaseVersion: Int\n) {\n    private val databaseConnection = DatabaseConnection(\n        context,\n        path,\n        name,\n        migrator,\n        databaseVersion,\n        true\n    )\n\n    fun <T> withConnection(block: DatabaseConnection.() -> T): T {\n        return databaseConnection.withSynchronizedConnection(block)\n    }\n\n    fun transaction(\n        body: SQLiteDatabase.() -> Unit\n    ) {\n        return withConnection {\n            writableDatabase.transaction {\n                body()\n            }\n        }\n    }\n\n    /**\n     * Runs a transaction and then calls [DatabaseConnection.reset]. Useful for transactions\n     * that will mutate the DB schema.\n     */\n    fun resetTransaction(\n        body: SQLiteDatabase.() -> Unit\n    ) {\n        transaction(body)\n        databaseConnection.reset()\n    }\n}\n"
  },
  {
    "path": "db/src/test/java/org/odk/collect/db/sqlite/CustomSQLiteQueryBuilderTest.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.db.sqlite;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class CustomSQLiteQueryBuilderTest {\n\n    private final String[] columns = {\"_id\", \"col1\", \"col2\", \"col3\"};\n\n    @Test\n    public void quoteIdentifierTest() {\n        assertEquals(\"\\\"identifier\\\"\", CustomSQLiteQueryBuilder.quoteIdentifier(\"identifier\"));\n    }\n\n    @Test\n    public void quoteStringLiteral() {\n        assertEquals(\"'literal'\", CustomSQLiteQueryBuilder.quoteStringLiteral(\"literal\"));\n    }\n\n    @Test\n    public void selectTest() {\n        assertEquals(\"SELECT \", new CustomSQLiteQueryBuilder().select().getQueryString());\n    }\n\n    @Test\n    public void columnsForInsertTest() {\n        assertEquals(\"(_id,col1,col2,col3 ) \", new CustomSQLiteQueryBuilder().columnsForInsert(columns).getQueryString());\n    }\n\n    @Test\n    public void columnsForSelectTest() {\n        assertEquals(\"_id,col1,col2,col3 \", new CustomSQLiteQueryBuilder().columnsForSelect(columns).getQueryString());\n    }\n\n    @Test\n    public void fromTest() {\n        assertEquals(\"FROM testTableName \", new CustomSQLiteQueryBuilder().from(\"testTableName\").getQueryString());\n    }\n\n    @Test\n    public void whereOneTest() {\n        assertEquals(\"WHERE foo = bar \", new CustomSQLiteQueryBuilder().where(\"foo = bar\").getQueryString());\n    }\n\n    @Test\n    public void whereMultipleTest() {\n        String[] criteria = {\"foo = 1\", \"bar = 2\"};\n        assertEquals(\"WHERE foo = 1 AND bar = 2 \", new CustomSQLiteQueryBuilder().where(criteria).getQueryString());\n    }\n\n    @Test\n    public void renameTableTest() {\n        assertEquals(\"ALTER TABLE testTableName RENAME TO \", new CustomSQLiteQueryBuilder().renameTable(\"testTableName\").getQueryString());\n    }\n\n    @Test\n    public void toTest() {\n        assertEquals(\"testTableName\", new CustomSQLiteQueryBuilder().to(\"testTableName\").getQueryString());\n    }\n\n    @Test\n    public void dropIfExistsTest() {\n        assertEquals(\"DROP TABLE IF EXISTS testTableName \", new CustomSQLiteQueryBuilder().dropIfExists(\"testTableName\").getQueryString());\n    }\n\n    @Test\n    public void insertIntoTest() {\n        assertEquals(\"INSERT INTO testTableName\", new CustomSQLiteQueryBuilder().insertInto(\"testTableName\").getQueryString());\n    }\n\n    @Test\n    public void alterTest() {\n        assertEquals(\"ALTER \", new CustomSQLiteQueryBuilder().alter().getQueryString());\n    }\n\n    @Test\n    public void tableTest() {\n        assertEquals(\"TABLE testTableName \", new CustomSQLiteQueryBuilder().table(\"testTableName\").getQueryString());\n    }\n\n    @Test\n    public void addColumnTest() {\n        assertEquals(\"ADD COLUMN Test text not null\", new CustomSQLiteQueryBuilder().addColumn(\"Test\", \"text not null\").getQueryString());\n    }\n\n    @Test\n    public void createTableTest() {\n        List<String> columnDefinitions = new ArrayList<>();\n        columnDefinitions.add(CustomSQLiteQueryBuilder.formatColumnDefinition(\"col1\", \"TEXT\"));\n        columnDefinitions.add(CustomSQLiteQueryBuilder.formatColumnDefinition(\"col2\", \"INTEGER\"));\n        String actualQuery = new CustomSQLiteQueryBuilder().createTable(\"testTable\").columnsForCreate(columnDefinitions).getQueryString();\n        assertEquals(\"CREATE TABLE testTable (col1 TEXT, col2 INTEGER)\", actualQuery);\n    }\n}\n"
  },
  {
    "path": "db/src/test/java/org/odk/collect/db/sqlite/DatabaseConnectionTest.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Assert.assertFalse\nimport org.junit.Assert.assertTrue\nimport org.junit.Assert.fail\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.db.sqlite.support.NoopMigrator\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\n@RunWith(AndroidJUnit4::class)\nclass DatabaseConnectionTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n\n    @Test\n    fun `#withSynchronizedConnection cannot return a Cursor`() {\n        val dbConnection = DatabaseConnection(\n            context,\n            TempFiles.createTempDir().absolutePath,\n            \"temp.db\",\n            NoopMigrator(),\n            1\n        )\n\n        dbConnection.writableDatabase.execSQL(\"CREATE TABLE blah (id integer);\")\n\n        try {\n            dbConnection.withSynchronizedConnection {\n                readableDatabase.query(\n                    \"blah\",\n                    null,\n                    null,\n                    null,\n                    null,\n                    null,\n                    null\n                )\n            }\n\n            fail()\n        } catch (_: IllegalStateException) {\n            // Expected\n        }\n    }\n\n    // https://github.com/getodk/collect/issues/5042\n    @Test\n    fun `database file should be recreated if removed between operations`() {\n        val dbDir = TempFiles.createTempDir()\n        val dbFileName = \"temp.db\"\n        val dbPath = dbDir.absolutePath + File.separator + dbFileName\n\n        DatabaseConnection(\n            context,\n            dbDir.absolutePath,\n            dbFileName,\n            NoopMigrator(),\n            1\n        ).also {\n            it.readableDatabase\n            assertTrue(File(dbPath).exists())\n\n            File(dbPath).delete()\n            assertFalse(File(dbPath).exists())\n\n            it.readableDatabase\n            assertTrue(File(dbPath).exists())\n        }\n    }\n}\n"
  },
  {
    "path": "db/src/test/java/org/odk/collect/db/sqlite/RowNumbersTest.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport android.content.ContentValues\nimport android.content.Context\nimport android.provider.BaseColumns._ID\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.db.sqlite.CursorExt.foldAndClose\nimport org.odk.collect.db.sqlite.CursorExt.rowToMap\nimport org.odk.collect.db.sqlite.RowNumbers.invalidateRowNumbers\nimport org.odk.collect.db.sqlite.RowNumbers.rawQueryWithRowNumber\nimport org.odk.collect.db.sqlite.SQLiteColumns.ROW_NUMBER\nimport org.odk.collect.db.sqlite.support.NoopMigrator\nimport org.odk.collect.shared.TempFiles\n\n@RunWith(AndroidJUnit4::class)\nclass RowNumbersTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n\n    @Test\n    fun `#rawQueryWithRowNumber returns results ordered by row_number column`() {\n        val dbConnection = SynchronizedDatabaseConnection(\n            context,\n            TempFiles.createTempDir().absolutePath,\n            \"temp.db\",\n            NoopMigrator(),\n            1\n        )\n\n        dbConnection.resetTransaction {\n            execSQL(\"CREATE TABLE test_table ($_ID integer PRIMARY KEY, position text)\")\n        }\n\n        dbConnection.transaction {\n            insertOrThrow(\"test_table\", null, ContentValues().also { it.put(\"position\", \"first\") })\n            insertOrThrow(\"test_table\", null, ContentValues().also { it.put(\"position\", \"second\") })\n        }\n\n        val rows = dbConnection.rawQueryWithRowNumber(\"test_table\") { cursor ->\n            cursor.foldAndClose { it.rowToMap() }\n        }\n        assertThat(rows.size, equalTo(2))\n\n        assertThat(rows[0][\"position\"], equalTo(\"first\"))\n        assertThat(rows[0][ROW_NUMBER], equalTo(\"1\"))\n\n        assertThat(rows[1][\"position\"], equalTo(\"second\"))\n        assertThat(rows[1][ROW_NUMBER], equalTo(\"2\"))\n    }\n\n    @Test\n    fun `#rawQueryWithRowNumber returns results ordered by updated row_number column after row deleted and invalidate`() {\n        val dbConnection = SynchronizedDatabaseConnection(\n            context,\n            TempFiles.createTempDir().absolutePath,\n            \"temp.db\",\n            NoopMigrator(),\n            1\n        )\n\n        dbConnection.resetTransaction {\n            execSQL(\"CREATE TABLE test_table ($_ID integer PRIMARY KEY, position text)\")\n        }\n\n        dbConnection.transaction {\n            insertOrThrow(\"test_table\", null, ContentValues().also { it.put(\"position\", \"first\") })\n            insertOrThrow(\"test_table\", null, ContentValues().also { it.put(\"position\", \"second\") })\n            insertOrThrow(\"test_table\", null, ContentValues().also { it.put(\"position\", \"third\") })\n        }\n\n        val beforeRows = dbConnection.rawQueryWithRowNumber(\"test_table\") { cursor ->\n            cursor.foldAndClose { it.rowToMap() }\n        }\n        assertThat(beforeRows.size, equalTo(3))\n\n        dbConnection.transaction {\n            delete(\"test_table\", \"position = ?\", arrayOf(\"second\"))\n        }\n\n        dbConnection.invalidateRowNumbers(\"test_table\")\n\n        val afterRows = dbConnection.rawQueryWithRowNumber(\"test_table\") { cursor ->\n            cursor.foldAndClose { it.rowToMap() }\n        }\n        assertThat(afterRows.size, equalTo(2))\n\n        assertThat(afterRows[0][\"position\"], equalTo(\"first\"))\n        assertThat(afterRows[0][ROW_NUMBER], equalTo(\"1\"))\n\n        assertThat(afterRows[1][\"position\"], equalTo(\"third\"))\n        assertThat(afterRows[1][ROW_NUMBER], equalTo(\"2\"))\n    }\n}\n"
  },
  {
    "path": "db/src/test/java/org/odk/collect/db/sqlite/SQLiteUtilsTest.java",
    "content": "package org.odk.collect.db.sqlite;\n\nimport static android.content.Context.MODE_PRIVATE;\nimport static junit.framework.TestCase.assertTrue;\n\nimport android.database.sqlite.SQLiteDatabase;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport junit.framework.TestCase;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@RunWith(AndroidJUnit4.class)\npublic class SQLiteUtilsTest {\n\n    @Test\n    public void doesTableExistTest() {\n        final String tableName = \"testTable\";\n        final String columnName = CustomSQLiteQueryBuilder.quoteIdentifier(\"col\");\n\n        SQLiteDatabase db = ApplicationProvider.getApplicationContext().openOrCreateDatabase(\"testDatabase\", MODE_PRIVATE, null);\n\n        TestCase.assertFalse(SQLiteUtils.doesTableExist(db, tableName));\n\n        // Create the table and check again\n        List<String> columnDefinitions = new ArrayList<>();\n        columnDefinitions.add(CustomSQLiteQueryBuilder.formatColumnDefinition(columnName, \"TEXT\"));\n        CustomSQLiteQueryExecutor.begin(db).createTable(tableName).columnsForCreate(columnDefinitions).end();\n\n        assertTrue(SQLiteUtils.doesTableExist(db, tableName));\n    }\n}\n"
  },
  {
    "path": "db/src/test/java/org/odk/collect/db/sqlite/SqlQueryTest.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.odk.collect.shared.Query\n\nclass SqlQueryTest {\n    @Test\n    fun `Eq query generates correct selection and arguments`() {\n        val query = Query.StringEq(\"name\", \"John\").toSql()\n\n        assertThat(query.selection, equalTo(\"name = ?\"))\n        assertThat(query.selectionArgs, equalTo(arrayOf(\"John\")))\n    }\n\n    @Test\n    fun `NotEq query generates correct selection and arguments`() {\n        val query = Query.StringNotEq(\"age\", \"30\").toSql()\n\n        assertThat(query.selection, equalTo(\"age != ?\"))\n        assertThat(query.selectionArgs, equalTo(arrayOf(\"30\")))\n    }\n\n    @Test\n    fun `And query generates correct selection and arguments`() {\n        val queryA = Query.StringEq(\"name\", \"John\")\n        val queryB = Query.StringNotEq(\"age\", \"30\")\n        val combinedQuery = Query.And(queryA, queryB).toSql()\n\n        assertThat(combinedQuery.selection, equalTo(\"(name = ? AND age != ?)\"))\n        assertThat(combinedQuery.selectionArgs, equalTo(arrayOf(\"John\", \"30\")))\n    }\n\n    @Test\n    fun `Or query generates correct selection and arguments`() {\n        val queryA = Query.StringEq(\"city\", \"New York\")\n        val queryB = Query.StringNotEq(\"country\", \"Canada\")\n        val combinedQuery = Query.Or(queryA, queryB).toSql()\n\n        assertThat(combinedQuery.selection, equalTo(\"(city = ? OR country != ?)\"))\n        assertThat(combinedQuery.selectionArgs, equalTo(arrayOf(\"New York\", \"Canada\")))\n    }\n\n    @Test\n    fun `nested And and Or queries generates correct selection and arguments`() {\n        val queryA = Query.StringEq(\"status\", \"active\")\n        val queryB = Query.StringNotEq(\"role\", \"admin\")\n        val queryC = Query.StringEq(\"team\", \"engineering\")\n\n        val combinedQuery = Query.And(Query.Or(queryA, queryB), queryC).toSql()\n\n        assertThat(combinedQuery.selection, equalTo(\"((status = ? OR role != ?) AND team = ?)\"))\n        assertThat(combinedQuery.selectionArgs, equalTo(arrayOf(\"active\", \"admin\", \"engineering\")))\n    }\n}\n"
  },
  {
    "path": "db/src/test/java/org/odk/collect/db/sqlite/SqliteDatabaseExtTest.kt",
    "content": "package org.odk.collect.db.sqlite\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.db.sqlite.SQLiteDatabaseExt.doesColumnExist\n\n@RunWith(AndroidJUnit4::class)\nclass SqliteDatabaseExtTest {\n\n    @Test\n    fun doesColumnExistTest() {\n        val tableName = \"testTable\"\n        val db = ApplicationProvider.getApplicationContext<Context>()\n            .openOrCreateDatabase(\"testDatabase\", Context.MODE_PRIVATE, null)\n        db.execSQL(\"CREATE TABLE $tableName (id integer, column1 text);\")\n\n        assertThat(db.doesColumnExist(tableName, \"id\"), equalTo(true))\n        assertThat(db.doesColumnExist(tableName, \"column1\"), equalTo(true))\n        assertThat(db.doesColumnExist(tableName, \"blah\"), equalTo(false))\n    }\n}\n"
  },
  {
    "path": "db/src/test/java/org/odk/collect/db/sqlite/support/NoopMigrator.kt",
    "content": "package org.odk.collect.db.sqlite.support\n\nimport android.database.sqlite.SQLiteDatabase\nimport org.odk.collect.db.sqlite.DatabaseMigrator\n\nclass NoopMigrator : DatabaseMigrator {\n    override fun onCreate(db: SQLiteDatabase?) {}\n    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int) {}\n}\n"
  },
  {
    "path": "docs/ANALYTICS-QUESTIONS.md",
    "content": "# Analytics questions\n\nA list of questions asked and answered via analytics already sectioned by the date (with a 90 day recording window).\n\n## June 2024\n\n- How often is high-res video disabled? 2% of users using video widget.\n- How often is `OSMWidget` used? Once.\n- How many users create shortcuts? 126.\n- How many user are using ADB to add forms/instances? 904 for Forms, 2728 for instances (500k events).\n- How many users are importing encrypted instances? 10.\n- How often is partial form finalization used? Never.\n- How many forms use auto send attribute? 200+ (max recordable) with 1.5k users.\n- How many forms use auto delete attribute? 200+ (max recordable) with 1k users.\n"
  },
  {
    "path": "docs/CODE-GUIDELINES.md",
    "content": "# Code style guidelines\n\n## Kotlin style guidelines\n\nFollow the [Kotlin code conventions](https://kotlinlang.org/docs/coding-conventions.html).\n\n## Java style guidelines\nFollow the [Android style rules](http://source.android.com/source/code-style.html) and the [Google Java style guide](https://google.github.io/styleguide/javaguide.html).\n\n## Testing style guidelines\nFavor [Hamcrest](http://hamcrest.org/JavaHamcrest/) asserts over JUnit asserts for readability.\n\nOld JUnit style:\n```java\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNull;\n...\nassertEquals(\"expected\", ClassToTest.methodToTest(\"input\"));\nassertNull(ClassToTest.methodReturnsNull());\n```\n\nPreferred style using Hamcrest:\n```java\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\n...\nassertThat(ClassToTest.methodToTest(\"input\"), equalTo(\"expected\"));\nassertThat(ClassToTest.methodReturnsNull(), equalTo(null));\n```\n\n### Backtick test naming\n\nTests written in Kotlin should use [backtick enclosed method names](https://kotlinlang.org/docs/coding-conventions.html#names-for-test-methods) and use `#` JavaDoc syntax to refer to an object member's in unit tests:\n\n```kotlin\n@Test\nfun `#getHello returns hello string`() {\n    assertThat(subject.getHello(), equalTo(\"hello\"))\n}\n```\n\nTest naming structure is hard to put strict rules around, but in general conditions (\"given\") should go after the action (\"when\") and assertion (\"then\") in this style of method naming:\n\n```kotlin\n@Test\nfun `#getHello returns goodbye string when subject is angry`() {\n    subject.setAngry(true)\n    assertThat(subject.getHello(), equalTo(\"goodbye\"))\n}\n```\n\nAlso, to keep test names short, it's best to avoid words like \"should\" or \"will\" (`#getHello should return hello string`) and instead use definitive statements with [third person singular verb conjugations](https://www.grammarly.com/blog/verb-forms/) (\"returns\", \"sets\", \"fetches\" etc).\n\n## XML style guidelines\n\nFollow these naming conventions in Android XML files:\n\n* Attributes (`attr`): `shouldBeCamelCased`\n* String, dimension and color names: `should_be_snake_cased`\n* Themes and Styles: `ShouldBePascalCased` and should also be qualified in a similar manner to Java package names like `<Type>.<Package>.<Name>...`. For instance:\n\n  ```xml\n  Theme.Collect.Light\n  TextAppearance.Collect.H1.Purple\n  Widget.Collect.Button\n  Widget.Collect.Button.BigRed\n  ```\n\n## UI components style guidelines\n\nInstead of building custom components, UI should be constructed from standard [Material components](https://material.io/components?platform=android). If a custom or platform component is needed, make sure it is compatible with both light and dark themes by using theme attributes for coloring instead of directly using color values (eg. #000000 or R.color.color_name). In XML these should come from the Material Theme attributes documented [here](https://material.io/develop/android/theming/color/). If you need to set colors in Java code there is a `ThemeUtils` helper class that will let you fetch these attributes.\n\n## Custom view guidelines\n\nWhen creating or refactoring views, keep in mind our vision of an \"ideal\" view:\n\n- Views should generally be as dumb and as stateless as possible\n- Views shouldn't interact with other layers of your application (repositories, network etc). They should be able to take in and render data (via setters) and emit \"meaningful\" events via listeners.\n- A view should have a single public `Listener` sub interface and `setListener` method. Methods on the interface should correspond to \"meaningful\" events. If the view is a button that could just be `onClick` but if it's a volume slider this might be something like `onVolumeChanged`.\n- Views would ideally have just one setter for data but more complex views (often that have many subviews) that take in data at different times may have more - fewer setters is better as it's less changing state in the view.\n- Views that render more than one kind of data (and that have more than one setter) might benefit from a `render` method that encapsulates all the logic around displaying the state of a view.\n\n## Strings\nAlways use [string resources](https://developer.android.com/guide/topics/resources/string-resource.html) instead of literal strings. This ensures wording consistency across the project and also enables full translation of the app. Only make changes to the base `res/values/strings.xml` English file (in the `strings` module) and not to the other language files. The translated files are generated from [Transifex](https://www.transifex.com/getodk/collect/) where translations can be submitted by the community. Names of software packages or other untranslatable strings should be placed in `res/values/untranslated.xml`.\n\nIf a new string is added that is used as a date format (with `SimpleDateFormat`), it should be added to `DateFormatsTest` to ensure that translations do not cause crashes.\n\nStrings that represent very rare failure cases or that are meant more for ODK developers to use for troubleshooting rather than directly for users may be written as literal strings. This reduces the burden on translators and makes it easier for developers to troubleshoot edge cases without having to look up translations.\n\n## Icons\n\nIcons (usually defined using Android XML drawables) should be placed in the `icons` module so they can be shared between other modules without being duplicated. Icons should usually come from the set of Material Icons which are easiest to acquire from Android Studio's \"Vector Asset\" tool.\n\n## Dependency injection\n\nAs much as possible to facilitate simpler, more modular and more testable components you should follow the Dependency Inversion principle in Collect Code. An example tutorial on this concept can be found [here](https://www.seadowg.com/dip-lesson/).\n\nBecause many Android components (Activity and Fragment for instance) don't allow us control over their constructors Collect uses [Dagger](https://google.github.io/dagger/) to 'inject' dependencies. The configuration for Dagger can be found in [AppDepdendencyComponent](collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java). For any normal objects it is probably best to avoid Dagger and use normal Java constructors.\n\nWhile it's important to read the Dagger [documentation](https://google.github.io/dagger/users-guide) we've provided some basic instructions on how to use Dagger within Collect below.\n\n### Providing dependencies\n\nTo declare a new dependency that objects can inject add a `@Provides` method to the `AppDependencyModule`:\n\n```java\n@Provides\npublic MyDependency providesMyDependency() {\n    return MyDependency();\n}\n```\n\nYou can also have Dagger return the same instance every time (i.e. a Singleton) by annotating the method with `@Singleton` as well.\n\n### Injecting dependencies into Activity/Fragment objects\n\nTo inject a dependency into the Activity you first need to make Dagger aware it's injecting into that Activity by adding an `inject` to the `AppDependencyComponent` (if it's not already there):\n\n```java\nvoid inject(MyActivity activity);\n```\n\nThen define a field with the `@Inject` annotation in your Activity:\n\n```java\n@Inject\nMyDependency dependency;\n```\n\nTo have Dagger inject the dependency you need to hook the injection into the Activity's `onCreate` (as this is the first part of the lifecycle we have access to):\n\n```java\n@Override\npublic void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    DaggerUtils.getComponent(this).inject(this);\n}\n```\n\nFor Fragment objects you should hook into the `onAttach` lifecycle method instead:\n\n```java\n@Override\npublic void onAttach(Context context) {\n    super.onAttach(context);\n    DaggerUtils.getComponent(context).inject(this);\n}\n```\n\n### Swapping out dependencies in tests\n\nTo swap out depdendencies in a Robolectric test you can override the module the Application object uses to inject objects using provided helpers:\n\n```java\n@Before\npublic void setup() {\n    MyDependency mocked = mock(MyDependency.class);\n    RobolectricHelpers.overrideAppDependencyModule(new AppDependencyModule() {\n      @Override\n      public MyDependency providesMyDependency() {\n        return mocked;\n      }\n    });\n}\n```\n\n## Code from external sources\nODK Collect is released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0). Please make sure that any code you include is an OSI-approved [permissive license](https://opensource.org/faq#permissive). **Please note that if no license is specified for a piece of code or if it has an incompatible license such as GPL, using it puts the project at legal risk**.\n\nSites with compatible licenses (including [StackOverflow](http://stackoverflow.com/)) will sometimes provide exactly the code snippet needed to solve a problem. You are encouraged to use such snippets in ODK Collect as long as you attribute them by including a direct link to the source. In addition to complying with the content license, this provides useful context for anyone reading the code.\n\n## Gradle sub modules\n\nCollect is a multi module Gradle project. Modules should have a focused feature or utility (like \"location\" or \"analytics\") rather than represent an architectural \"layer\" (like \"ui\" or \"backend\"). Collect has an `androidshared` and `shared` module for simple utilities that might be useful in any Android or Java application respectively. Modules names should be lowercase and be dash seperated (like `test-forms` rather than `testforms` or `test_forms`).\n\n### Adding a new module\n\nThere's no easy way to define exactly when a new module should be pulled out of an existing one or when new code calls for a new module - it's best to discuss that with the team before making any decisions. Once a structure has been agreed on, to add a new module:\n\n1. Click `File > New > New module...` in Android Studio\n2. Decide whether the new module should be an \"Android Library\" or \"Java or Kotlin Library\" - ideally as much code as possible could avoid relying on Android but a lot of features will require at least one Android Library module\n3. Review the generated `build.gradle` and remove any unnecessary dependencies or setup\n4. Add quality checks to the module's `build.gradle`:\n\n  ```\n  apply from: '../config/quality.gradle'\n  ```\n\n5. If the module will have tests, make sure they get run on CI by adding a line to `test_modules.txt` with `<module-name>` and if it's a non-Android module, registering the `testDebug` task in its `build.gradle` file:\n\n  ```\n  tasks.register(\"testDebug\") {\n    dependsOn(\"test\")\n  }\n  ```\n"
  },
  {
    "path": "docs/CONTRIBUTING.md",
    "content": "# Contributing code to ODK Collect\n\nThis is a living document. If you see something that could be improved, edit this document and submit a pull request following the instructions below!\n\n## Table of Contents\n* [Submitting a pull request](#submitting-a-pull-request)\n* [Making sure your pull request is accepted](#making-sure-your-pull-request-is-accepted)\n* [The review process](#the-review-process)\n* [Work in progress pull requests](#work-in-progress-pull-requests)\n* [Triage issues](#triage-issues-)\n\n## Submitting a pull request\n\nTo contribute code to ODK Collect, you will need to open a [pull request](https://help.github.com/articles/about-pull-requests/) which will be reviewed by the community and then merged into the core project. Generally, a pull request is submitted when a unit of work is considered complete but it can sometimes be helpful to share ideas through a work in progress (WIP) pull request ([learn more](#work-in-progress-pull-requests)).\n\n1. Read our [\"state of the union\"](STATE.md) to get a quick understanding of the codebase and its history. The \"Where we're going\" section may suggest efforts you can get involved in if you are having a hard time finding ways to contribute.\n\n1. Read our [code style guidelines](CODE-GUIDELINES.md).\n\n1. Read our documentation on [automated testing](TEST-GUIDELINES.md).\n\n1. If you're going to be working on adding or changing any `QuestionWidget` objects it's a good idea to read our docs on [them](WIDGETS.md).\n\n1. [Set up your development environment](https://github.com/getodk/collect#setting-up-your-development-environment).\n\n1. To make sure you have the latest version of the code, set up this repository as [a remote for your fork](https://help.github.com/articles/configuring-a-remote-for-a-fork/) and then [sync your fork](https://help.github.com/articles/syncing-a-fork/).\n\n1. Create a branch for the code you will be writing:\n\n        git checkout -b NAME_OF_YOUR_BRANCH\n\n1. If there is an [issue](https://github.com/getodk/collect/issues) corresponding to what you will work on, leave a comment on the issue asking to claim it. If there is no issue yet, create one to provide background on the problem you are solving. Issues that have been prioritised by the core team can be found in Collect's [backlog](https://github.com/orgs/getodk/projects/9/views/8).\n\n1. If you are an external contributor, make sure to discuss working on the issue with a maintainer who will then assign you to it. If you're on the core team you can either assign yourself to an issue in [the backlog](https://github.com/orgs/getodk/projects/9/views/8) and move it to \"in progress\" or discuss adding an item to the backlog with the rest of the team.\n\n1. Once you've made incremental progress towards you goal, commit your changes with a meaningful commit message. Use [keywords for closing issues](https://help.github.com/articles/closing-issues-via-commit-messages/) to refer to issues and have them automatically close when your changes are merged.\n\n        git commit -m \"Do a thing. Fix #1.\"\n\n1. Push changes to your fork at any time to make them publicly available:\n\n        git push\n\n1. Once you have completed your code changes, verify that you have followed the [style guidelines](CODE-GUIDELINES.md).\n\n1. When your changes are ready to be added to the core ODK Collect project, [open a pull request](https://help.github.com/articles/creating-a-pull-request/). Make sure to set the base fork to `getodk/collect`. Describe your changes in the comment, refer to any relevant issues using [keywords for closing issues](https://help.github.com/articles/closing-issues-via-commit-messages/) and tag any person you think might need to know about the changes.\n\n1. Pull requests will be reviewed when committers have time. If you haven't received a review in 10 days, you may notify committers by putting `@getodk/collect` in a comment.\n\n## Making sure your pull request is accepted\n\n1. Confirm that your code compiles.\n\n1. Verify the functionality. Ideally, include automated tests with each pull request. If that's not possible, describe in the pull request comment which cases you tried manually to confirm that your code works as expected. Attach a test form when appropriate. This form should only include questions which are useful for verifying your change.\n\n1. Make sure your changes don't cause any memory leaks (we use [LeakCanary](https://github.com/square/leakcanary) for this purpose)\n\n1. Make sure that there is an issue that corresponds to the pull request and that it has been discussed by the community as necessary.\n\n1. Keep your pull request focused on one narrow goal. This could mean addressing an issue with multiple, smaller pull requests. Small pull requests are easier to review and less likely to introduce bugs. If you would like to make stylistic changes to the code, create a separate pull request.\n\n1. Write clear code. Use descriptive names and create meaningful abstractions (methods, classes).\n\n1. Document your reasoning. Your commit messages should make it clear why each change has been made.\n\n1. Point out decisions you made and what alternatives you considered. If you're unsure about a particular approach, ask a question to make your own thinking clear and help the reviewer identify controversial parts of the proposed solution. For example: \"here I returned a result object to represent the status after the transaction. I also considered throwing an exception in case of error but I didn't like that it made it unclear where an error happened. Which do you prefer and why?\" This is particularly important for [work in progress pull requests](#work-in-progress-pull-requests).\n\n1. Follow the guidelines below.\n\n## The review process\n\nBug fixes, pull requests corresponding to issues with a clearly stated goal and pull requests with clear tests and/or process for manual verification are given priority. Pull requests that are unclear or controversial may be labelled as `needs discussion` and/or may take longer to review.\n\nWe encourage everyone to participate in the review process to get familiar with the code base and help ensure higher quality. Reviewers should ask themselves some or all of the following questions:\n- Was this change adequately discussed prior to implementation?\n- Is the intended behavior clear under all conditions?\n- What interesting cases should be verified?\n- Is the behavior as intended in all cases?\n- What other functionality could this PR affect? Does that functionality still work as intended?\n- Was the change verified with several different devices and Android versions?\n- Is the code easy to understand and to maintain?\n- Is there sufficient detail to inform any changes to documentation?\n\nA maintainer does a thorough code review and when satisfied with the code, accepts and merges. Typically, manual testing is a requirement and then the needs testing label should be added after merging. Only in rare cases do we merge pull requests that are deemed safe enough to skip additional testing.\n\n## Merging pull requests\n\nPull requests can be merged by maintainers once they are accepted unless [CI](https://app.circleci.com/pipelines/github/getodk/collect) is currently failing.\n\nIf a PR is being merged to a release branch rather than `master`, any strings added as part of the changes should also be added to `master` (with `tools:ignore=\"UnusedResources\"`) as a follow-up PR so that they can be translated.\n\n## Work in progress pull requests\n\nWork in progress (WIP) pull requests are useful to illustrate a proposed direction and get early feedback before committing to a solution direction. These should ideally ideally be created as [Draft Pull Requests](https://help.github.com/en/articles/about-pull-requests#draft-pull-requests).\n\n## Triage issues [![Open Source Helpers](https://www.codetriage.com/getodk/collect/badges/users.svg)](https://www.codetriage.com/getodk/collect)\n\nIn addition to contributing code, you can help to triage issues. This can include reproducing bug reports, or asking for vital information such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to getodk/collect on CodeTriage](https://www.codetriage.com/getodk/collect).\n"
  },
  {
    "path": "docs/STATE.md",
    "content": "# State of the union\n\nThe purpose of this document is to give anyone who reads it a quick overview  of both the current state and the direction of the code. The community should try and update this document as the code evolves.\n\n## How we got here\n\n* App originally built in Java for Android 1.0/T-Mobile G1\n* Written at Google by graduate student interns and then University of Washington\n* Designed as a survey application backed by [JavaRosa](https://github.com/getodk/javarosa/) (which deals with XForm forms) communicating with [OpenRosa](https://docs.getodk.org/openrosa/) servers\n* Many different contributors/styles/eras over 15+ year lifetime\n* App wasn't built with a TDD workflow or with automated testing\n* Lots of work in the last few years to add more tests and clean up code using coverage measurement and static checks\n\n## Where we are now\n\n* App written in a mixture of Java and Kotlin\n* App has mixture of unit tests, [\"Local tests\" and \"Instrumented tests\"](https://developer.android.com/training/testing/fundamentals#instrumented-vs-local) but coverage is far from complete\n* Local tests are a mixture of classic Robolectric and AndroidX Test/Espresso (backed by Robolectric)\n* UI is \"iconic\" (old) but with a lot of inconsistencies and quirks and is best adapted to small screens (although often used on tablets)\n* A lot of code lives in between one \"god\" Activity (`FormFillingActivity`) and a process singleton (`FormController`)\n* Core form entry flow uses custom side-to-side swipe view (in `FormFillingActivity` made up of `ODKView`)\n* Questions are rendered using a view \"framework\" of implementations inheriting from `QuestionWidget` (which is documented in [WIDGETS.MD](WIDGETS.md)) which are also used to store UI state during form entry\n* Almost all app data is stored in Android's user-accessible \"external\" rather than \"internal\" storage\n* App mostly stores state in multiple SQLite databases - this means there's no DB level integrity guarantees on some relationships (like between forms and instances for example)\n* Access to state in SQLite happens through repository objects which deal in data/domain objects (`FormsRepository` and `Form` for example)\n* Settings UIs for the app use Android's Preferences abstraction\n* App uses [Material 3 Theming](https://m3.material.io/foundations/customization) so [Material components](https://material.io/components?platform=android) are preferred over custom or platform ones.\n* Dagger2 is used to inject \"black box\" objects such as Activity and just uses a very basic setup\n* Http is handled using OkHttp3 and https client abstractions are generally wrapped in Android's AsyncTask\n* Geo activities use three engines (Mapbox, osmdroid, Google Maps) depending on the selected basemap\n* Code goes through static analysis using CheckStyle, PMD, ktlint and Android Lint\n* Forms get into the app from two different sources (Open Rosa servers and disk) but the logic for this is disparate and they don't sit behind a common interface\n* Instances are linked to the forms they are instances of through formid and version. However, the same formid and version combination could represent multiple forms in storage\n* `SharedPreferences` is wrapped in app's own `Settings` abstraction\n* The form hierarchy is rendered using `FormHierarchyFragment`, which hasn't been seriously touched (at a code or design) level for a few years\n\n## Where we're going\n\n* General effort to increase test coverage and quality while working on anything and enforcing tests for new code in PR review\n* Moving responsibilities out of `FormFillingActivity` into other components (like Fragments, ViewModels, use cases etc)\n* Writing all new code in Kotlin, and writing new UI using Jetpack Compose (utilizing `ComposeView` within existing Activity/Fragment structures)\n* Moving towards a [\"data services\"](data_services_architecture.pdf) oriented architecture that has emerged over time\n* Writing new code using a [multi-module approach](CODE-GUIDELINES.md#gradle-sub-modules) (feature modules, mini frameworks etc) and breaking old code out into modules when opportunities come up\n* Trying to remove technical debt flagged with `@Deprecated`\n* Replacing async work such as `AsyncTask` with `Flow` (converted to `LiveData` in UI code) + `Scheduler` abstraction\n* Gradually removing use of `CursorLoader` (all remaining uses are in `CursorLoaderFactory`)\n* Using AndroidX Test in new local tests and migrating other local tests as we touch them (from classic Robolectric)\n* Improving the `MapFragment` abstraction so more logic can be shared between the map engines\n"
  },
  {
    "path": "docs/TEST-GUIDELINES.md",
    "content": "# Test guidelines\n\nAutomated tests are very important for building software. They let us add, modify and understand the code in our app with confidence and can prevent us from making mistakes that could end up affecting people collecting data in the field. They can also force us to deal with our code's own design which reveals problems we might not have though about if no tests had been written.\n\nAlmost any Pull Request for Collect will require a set of tests along with its code changes. Otherwise there will be nothing to ensure that that code continues to work or exist in the future. We recommend when working on Collect code you follow a [TDD](https://en.wikipedia.org/wiki/Test-driven_development) style of development and write a test before each change/addition to the code. Writing tests can often be harder than writing implementations and this is especially true in a code base like Collect that wasn't written with testing in mind from the start. If you feel stuck it's always good to discuss testing strategies as part of a PR or on our [Slack](https://slack.getodk.org/).\n\n## Different kinds of tests\n\nAlthough testing different components/features will often require different kinds and styles of testing the tests in Collect can broadly be divided into five kinds:\n\n### JUnit tests\n\nPlain old standard JUnit tests. All the tests in Collect use JUnit in someway but these tests don't use Android specific frameworks (like Espresso/Robolectric) These live in `collect_app/src/test/java`. Often they'll be focused on testing one object and will mock/fake/stub its dependencies (referred to as \"unit testing\") but some will test internal \"APIs\" (such as our Open Rosa server abstraction). The key to writing these kinds of tests is that they should not involve or depend on the Android framework. JUnit tests are run on the JVM.\n\n### Robolectric tests\n\nRobolectric is a framework that sits on top of JUnit. It allows you to write tests for Android objects or anything that integrates with Android and run the tests on the JVM. Robolectric makes this possible by providing a fully working fake of the Android SDK. These tests will often check Android specific things like the cleanup on lifecycle for Activity/Fragment or detailed view rendering for example. Robolectric tests also live in `collect_app/src/test/java` but are annotated by `RunWith(RobolectricTestRunner.class)` or `RunWith(AndroidJUnit4.class)` (which is an alias for `RobolecricTestRunner` when running on the JVM). Documentation for Robolectric isn't particularly thorough but help can be found on Stack Overflow or on our Slack (#collect-code channel).\n\n### Feature tests\n\nThese tests live in `collect_app/src/androidTest/java/feature` and use the Android Espresso testing framework (which is similar to UI web testing frameworks like FluentLenium or Capybara). Espresso lets you write tests that carry out actions a user would like clicking on things, scrolling etc and then make assertions on what is on screen. The intention of these tests it \"drive out\" whole features in Collect. To make these tests easier to write and read we use a page objects (in the `org.odk.collect.android.support.pages` package) to abstract actions and assertions on different pages.\n\n### Regression tests\n\nThese look very similar to Collect's Feature tests but instead of being written by developers as part of development they were written by Collect's QA team with an aim of test cases they repeat frequently. These are incrementally being transitioned to feature tests.\n\n### Instrumented tests\n\nThese tests live in `collect_app/src/androidTest/java/instrumented`. They also run on a device (like the other tests in `androidTest`) but don't actually interact with UI - they're much more like unit or integration tests of objects. As much as possible we'd like to avoid these kinds of tests as they can usually be replaced by Robolectric or even plain JUnit tests which have much faster build and run times.\n\n## Testing practices and approaches\n\n* https://en.wikipedia.org/wiki/Test-driven_development\n* https://martinfowler.com/articles/mocksArentStubs.html\n* https://www.martinfowler.com/bliki/PageObject.html\n* https://www.destroyallsoftware.com/talks/boundaries\n"
  },
  {
    "path": "docs/WIDGETS.md",
    "content": "# Question Widgets\n\nCollect forms are defined using [XForms](https://getodk.github.io/xforms-spec/) which in the most simple sense are just a list of questions (which can be of various [types](https://xlsform.org/en/#question-types)). To render the form and let the enumerator fill it out Collect needs to be able to deal with each of these different question types. To do this Collect has a series of different `QuestionWidget` implementations - usually one for each type of question. The exact mapping between question types and widgets happens in `WidgetFactory`.\n\n## Implementing widgets\n\nThe `TriggerWiget` will be used here as an example of how to implement a widget. The `TriggerWidget` represents the `acknowledge` question type (its docs are [here](https://docs.getodk.org/form-question-types/#trigger-acknowledge-widget)).\n\n```java\npublic class TriggerWidget extends QuestionWidget {\n\n    private AppCompatCheckBox triggerButton;\n\n    public TriggerWidget(Context context, QuestionDetails prompt) {\n        super(context, prompt);\n        render();\n    }\n\n    @Override\n    protected View onCreateWidgetView(Context context, FormEntryPrompt prompt, int answerTextSize, int controlTextSize) {\n        ViewGroup answerView = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.trigger_widget_answer, null);\n\n        triggerButton = answerView.findViewById(R.id.check_box);\n        triggerButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerTextSize);\n        triggerButton.setEnabled(!prompt.isReadOnly());\n        triggerButton.setChecked(prompt.getAnswerText().equals(\"OK\"));\n        triggerButton.setOnCheckedChangeListener((buttonView, isChecked) -> widgetValueChanged());\n\n        return answerView;\n    }\n    \n    @Override\n    public IAnswerData getAnswer() {\n        return triggerButton.isChecked() ? new StringData(OK_TEXT) : null;\n    }\n\n    @Override\n    public void clearAnswer() {\n        triggerButton.setChecked(false);\n        widgetValueChanged();\n    }\n\n    @Override\n    public void setOnLongClickListener(OnLongClickListener l) {\n        triggerButton.setOnLongClickListener(l);\n    }\n\n    @Override\n    public void cancelLongPress() {\n        super.cancelLongPress();\n        triggerButton.cancelLongPress();\n    }\n}\n```\n\nTo create your own widget your class needs to override several methods:\n\n* `onCreateWidgetView` - Returns the `View` object that represents the interface for answering the question. This will be rendered underneath the question's `label`, `hint` and `guidance_hint`. This method is passed the question itself (as a `FormEntryPrompt`) which will often be needed in rendering the widget. It is also passed the size to be used for question text and controls (like buttons).\n* `getAnswer` - Returns the current answer for the question. Can be `null` if the question has not been answered yet.\n* `clearAnswer` - Called when the answer for this question needs to be cleared for some reason. The implementation of this method should reset the UI of the widget.\n* `setOnLongClickListener` - Used to make sure clickable views in the widget work with the long click feature (shows the \"Edit Prompt\" menu). The passed listener should be set as the long click listener on clickable views in the widget.\n* `cancelLongPress` - As above this is used to make sure long click features work. The `cancelLongPress` call should simply be forwarded to clickable views in the widget.\n\nAs you'll see from the example you need to call `widgetValueChanged` whenever the answer is changed in someway. This will make sure that any listeners attached to the widget will be called. `render` must be called in your widget's constructor to signal that it's safe to call `onCreateWidgetView` and show the widget on screen.\n\n### Handling rotation/configuration changes\n\nWhen the device is rotated during form entry, the on screen answers will be saved. Then, when it is is recreated, the `Widget` will be passed the correct answers in with `FormEntryPrompt` meaning no extra work is required to handle configuration changes. If a widget's answer layout contains any editable views (like an `EditText` for example) they should include `android:saveEnabled=\"false\"` to prevent Android from trying to load previous values in.\n\n## Testing widgets\n\nWidgets should have the majority of their behavior driven out by tests that treat them as an individual component. This means higher level feature tests for Collect won't have to be as concerned around the many types of question that exist and can be more focused on form entry, form management, settings etc as a whole.\n\nYou can use [Robolectric](https://robolectric.org) to write tests for widgets without having to run them on device or manually mock out the Android SDK:\n\n```java\n@RunWith(AndroidJUnit4.class)\npublic class TriggerWidgetTest {\n\n    @Test\n    public void getAnswer_whenPromptAnswerDoesNotHaveAnswer_returnsNull() {\n        assertThat(createWidget(promptWithAnswer(null)).getAnswer(), nullValue());\n    }\n\n    @Test\n    public void getAnswer_whenPromptHasAnswer_returnsAnswer() {\n        TriggerWidget widget = createWidget(promptWithAnswer(new StringData(\"OK\")));\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(\"OK\"));\n    }\n\n    @Test\n    public void clearAnswer_clearsWidgetAnswer() {\n        TriggerWidget widget = createWidget(promptWithAnswer(new StringData(\"OK\")));\n\n        widget.clearAnswer();\n        assertThat(widget.getAnswer(), nullValue());\n    }\n\n    @Test\n    public void clearAnswer_callsValueChangeListeners() {\n        TriggerWidget widget = createWidget(promptWithAnswer(null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n\n        widget.clearAnswer();\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    @Test\n    public void usingReadOnlyOption_makesAllClickableElementsDisabled() {\n        TriggerWidget widget = createWidget(promptWithReadOnly());\n        assertThat(widget.getCheckBox().getVisibility(), equalTo(View.VISIBLE));\n        assertThat(widget.getCheckBox().isEnabled(), equalTo(Boolean.FALSE));\n    }\n\n    @Test\n    public void whenPromptAnswerDoesNotHaveAnswer_checkboxIsUnchecked() {\n        TriggerWidget widget = createWidget(promptWithAnswer(null));\n        assertThat(widget.getCheckBox().isChecked(), equalTo(false));\n    }\n\n    @Test\n    public void whenPromptHasAnswer_checkboxIsChecked() {\n        TriggerWidget widget = createWidget(promptWithAnswer(new StringData(\"OK\")));\n        assertThat(widget.getCheckBox().isChecked(), equalTo(true));\n    }\n\n    @Test\n    public void checkingCheckbox_setsAnswer() {\n        TriggerWidget widget = createWidget(promptWithAnswer(null));\n        CheckBox triggerButton = widget.getCheckBox();\n\n        triggerButton.setChecked(true);\n        assertThat(widget.getAnswer().getDisplayText(), equalTo(\"OK\"));\n\n        triggerButton.setChecked(false);\n        assertThat(widget.getAnswer(), nullValue());\n    }\n\n    @Test\n    public void checkingCheckbox_callsValueChangeListeners() {\n        TriggerWidget widget = createWidget(promptWithAnswer(null));\n        WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);\n        CheckBox triggerButton = widget.getCheckBox();\n\n        triggerButton.setChecked(true);\n        verify(valueChangedListener).widgetValueChanged(widget);\n    }\n\n    private TriggerWidget createWidget(FormEntryPrompt prompt) {\n        return new TriggerWidget(widgetTestActivity(), new QuestionDetails(prompt, \"formAnalyticsID\"));\n    }\n}\n```\n\nThis example makes sure that the widget interface methods (`getAnswer`, `clearAnswer` etc) all behave as expected and also checks that the widget looks and behaves correctly. Widget test helpers make these tests easier to write and can be in `QuestionWidgetHelpers`.\n"
  },
  {
    "path": "docs/WINDOWS-DEV-SETUP.md",
    "content": "# Developing ODK Collect on Windows\n\n## Overview\nMost ODK devs are using OS X or Linux. Occasionally problems surface for Windows users due to code changes or tool changes. Such problems commonly include file path separators and file permissions differences from Linux/OSX. However in general it is possible to develop ODK using Android Studio on Windows.\n\n## Known Issues\nNone currently\n\n## Current Workarounds\nNone currently\n    \n## Configuring Android Studio for Windows\n* Change line endings to Unix. Settings > Editor > Code Style > General (tab) > Line Separator :: Unix and Mac OS (\\n)\n* Configure the Terminal environment settings for Java:\n  Settings > Tools > Terminal\n  `JAVA_HOME=C:/Program Files/Android/Android Studio/jre`\n* Accept SDK licenses if necessary. Run from the Terminal in Android Studio using your own username AppData path:\n  `\"C:\\Users\\<username>\\AppData\\Local\\Android\\Sdk\\tools\\bin\\sdkmanager.bat\" --licenses`\n* To be able to run / debug tests using Android Studio, edit the JUnit configuration to use a classpath file instead of a bloated classpath string (which maxes out the tiny Windows limit for environment variables):\n  Run > Edit Configurations... > (left pane)Templates > Android JUnit > (right pane)Shorten command line: *classpath file*\n\n\n## Configuring Git for Windows\n* To work around file permissions differences on Windows that appear as errant file edits, tell Git to ignore permissions differences by changing the git config:\n  `core.filemode = false`\n* More info: <https://community.atlassian.com/t5/Sourcetree-questions/Why-is-sourceTree-showing-changes-when-there-are-none/qaq-p/186916>"
  },
  {
    "path": "download-robolectric-deps.sh",
    "content": "set -e\n\nwget -nc https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/6.0.1_r3-robolectric-r1-i7/android-all-instrumented-6.0.1_r3-robolectric-r1-i7.jar -P robolectric-deps\nwget -nc https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/7.0.0_r1-robolectric-r1-i7/android-all-instrumented-7.0.0_r1-robolectric-r1-i7.jar -P robolectric-deps\nwget -nc https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/7.1.0_r7-robolectric-r1-i7/android-all-instrumented-7.1.0_r7-robolectric-r1-i7.jar -P robolectric-deps\nwget -nc https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/8.0.0_r4-robolectric-r1-i7/android-all-instrumented-8.0.0_r4-robolectric-r1-i7.jar -P robolectric-deps\nwget -nc https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/12.1-robolectric-8229987-i7/android-all-instrumented-12.1-robolectric-8229987-i7.jar -P robolectric-deps\nwget -nc https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/13-robolectric-9030017-i7/android-all-instrumented-13-robolectric-9030017-i7.jar -P robolectric-deps\nwget -nc https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/15-robolectric-12650502-i7/android-all-instrumented-15-robolectric-12650502-i7.jar -P robolectric-deps\nwget -nc https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/16-robolectric-13921718-i7/android-all-instrumented-16-robolectric-13921718-i7.jar -P robolectric-deps\n\ndest_dir=\"src/test/resources\"\nmkdir -p collect_app/$dest_dir && cp robolectric-deps.properties collect_app/$dest_dir\nmkdir -p audiorecorder/$dest_dir && cp robolectric-deps.properties audiorecorder/$dest_dir\nmkdir -p projects/$dest_dir && cp robolectric-deps.properties projects/$dest_dir\nmkdir -p location/$dest_dir && cp robolectric-deps.properties location/$dest_dir\nmkdir -p androidshared/$dest_dir && cp robolectric-deps.properties androidshared/$dest_dir\nmkdir -p geo/$dest_dir && cp robolectric-deps.properties geo/$dest_dir\nmkdir -p permissions/$dest_dir && cp robolectric-deps.properties permissions/$dest_dir\nmkdir -p settings/$dest_dir && cp robolectric-deps.properties settings/$dest_dir\nmkdir -p maps/$dest_dir && cp robolectric-deps.properties maps/$dest_dir\nmkdir -p errors/$dest_dir && cp robolectric-deps.properties errors/$dest_dir\nmkdir -p selfie-camera/$dest_dir && cp robolectric-deps.properties selfie-camera/$dest_dir\nmkdir -p qr-code/$dest_dir && cp robolectric-deps.properties qr-code/$dest_dir\nmkdir -p draw/$dest_dir && cp robolectric-deps.properties draw/$dest_dir\n"
  },
  {
    "path": "draw/.gitignore",
    "content": "/build"
  },
  {
    "path": "draw/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.draw\"\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":shared\"))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":async\"))\n    implementation(project(\":settings\"))\n    implementation(project(\":icons\"))\n\n    implementation(libs.rarepebbleColorpicker)\n    implementation(libs.androidxLifecycleLivedataKtx)\n    implementation(libs.androidxLifecycleViewmodelKtx)\n    implementation(libs.androidxFragmentKtx)\n    implementation(libs.androidMaterial)\n    implementation(libs.timber)\n\n    implementation(libs.dagger)\n    ksp(libs.daggerCompiler)\n\n    debugImplementation(project(\":fragments-test\"))\n\n    testImplementation(project(\":androidtest\"))\n    testImplementation(project(\":test-shared\"))\n\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.androidxTestEspressoCore)\n    testImplementation(libs.robolectric)\n}\n"
  },
  {
    "path": "draw/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <application>\n        <activity\n            android:name=\".DrawActivity\"\n            android:theme=\"@style/Theme.Material3.DayNight\"/>\n    </application>\n</manifest>\n"
  },
  {
    "path": "draw/src/main/java/org/odk/collect/draw/DaggerSetup.kt",
    "content": "package org.odk.collect.draw\n\nimport dagger.Component\nimport dagger.Module\nimport dagger.Provides\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.settings.SettingsProvider\nimport javax.inject.Singleton\n\ninterface DrawDependencyComponentProvider {\n    val drawDependencyComponent: DrawDependencyComponent\n}\n\n@Component(modules = [DrawDependencyModule::class])\n@Singleton\ninterface DrawDependencyComponent {\n    fun inject(drawActivity: DrawActivity)\n    fun inject(drawActivity: DrawView)\n}\n\n@Module\nopen class DrawDependencyModule {\n\n    @Provides\n    open fun providesScheduler(): Scheduler {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesSettingsProvider(): SettingsProvider {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesImagePath(): String {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n}\n"
  },
  {
    "path": "draw/src/main/java/org/odk/collect/draw/DrawActivity.java",
    "content": "/*\n * Copyright (C) 2012 University of Washington\n * Copyright (C) 2007 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.draw;\n\nimport static org.odk.collect.androidshared.ui.EdgeToEdge.setView;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.pm.ActivityInfo;\nimport android.graphics.Color;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.view.KeyEvent;\nimport android.view.View;\nimport android.view.WindowManager;\nimport android.view.animation.AccelerateDecelerateInterpolator;\nimport android.view.animation.Animation;\nimport android.view.animation.Interpolator;\nimport android.view.animation.OvershootInterpolator;\nimport android.view.animation.ScaleAnimation;\n\nimport androidx.activity.OnBackPressedCallback;\nimport androidx.annotation.NonNull;\nimport androidx.cardview.widget.CardView;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.lifecycle.viewmodel.CreationExtras;\n\nimport com.google.android.material.floatingactionbutton.FloatingActionButton;\n\nimport org.odk.collect.androidshared.bitmap.ImageFileUtils;\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.keys.MetaKeys;\nimport org.odk.collect.strings.localization.LocalizedActivity;\n\nimport java.io.File;\n\nimport javax.inject.Inject;\n\n/**\n * Modified from the FingerPaint example found in The Android Open Source\n * Project.\n *\n * @author BehrAtherton@gmail.com\n */\npublic class DrawActivity extends LocalizedActivity {\n    public static final String OPTION = \"option\";\n    public static final String OPTION_SIGNATURE = \"signature\";\n    public static final String OPTION_ANNOTATE = \"annotate\";\n    public static final String OPTION_DRAW = \"draw\";\n    public static final String REF_IMAGE = \"refImage\";\n    public static final String SCREEN_ORIENTATION = \"screenOrientation\";\n    public static final String EXTRA_OUTPUT = android.provider.MediaStore.EXTRA_OUTPUT;\n    // restore\n\n    private FloatingActionButton fabActions;\n\n    // incoming options...\n    private String loadOption;\n    private File refImage;\n    private File output;\n    private File savepointImage;\n\n    private DrawView drawView;\n\n    private DrawViewModel drawViewModel;\n\n    @Inject\n    Scheduler scheduler;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {\n        @Override\n        public void handleOnBackPressed() {\n            createQuitDrawDialog();\n        }\n    };\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        ((DrawDependencyComponentProvider) getApplicationContext()).getDrawDependencyComponent().inject(this);\n\n        PenColorPickerViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.Factory() {\n            @NonNull\n            @Override\n            public <T extends ViewModel> T create(@NonNull Class<T> modelClass, @NonNull CreationExtras extras) {\n                return (T) new PenColorPickerViewModel(settingsProvider.getMetaSettings(), MetaKeys.LAST_USED_PEN_COLOR);\n            }\n        }).get(PenColorPickerViewModel.class);\n\n        this.getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder()\n                .forClass(PenColorPickerDialog.class, () -> new PenColorPickerDialog(viewModel))\n                .build());\n\n        super.onCreate(savedInstanceState);\n        setView(this, R.layout.draw_layout, false);\n\n        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,\n                WindowManager.LayoutParams.FLAG_FULLSCREEN);\n\n        drawView = findViewById(R.id.drawView);\n\n        fabActions = findViewById(R.id.fab_actions);\n        final FloatingActionButton fabSetColor = findViewById(R.id.fab_set_color);\n        final CardView cardViewSetColor = findViewById(R.id.cv_set_color);\n        final FloatingActionButton fabSaveAndClose = findViewById(R.id.fab_save_and_close);\n        final CardView cardViewSaveAndClose = findViewById(R.id.cv_save_and_close);\n        final FloatingActionButton fabClear = findViewById(R.id.fab_clear);\n        final CardView cardViewClear = findViewById(R.id.cv_clear);\n\n        fabActions.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View view) {\n                int status = Integer.parseInt(view.getTag().toString());\n                if (status == 0) {\n                    status = 1;\n                    fabActions.animate().rotation(45).setInterpolator(new AccelerateDecelerateInterpolator())\n                            .setDuration(100).start();\n\n                    scaleInAnimation(fabSetColor, 50, 150, new OvershootInterpolator(), true);\n                    scaleInAnimation(cardViewSetColor, 50, 150, new OvershootInterpolator(), true);\n                    scaleInAnimation(fabSaveAndClose, 100, 150, new OvershootInterpolator(), true);\n                    scaleInAnimation(cardViewSaveAndClose, 100, 150, new OvershootInterpolator(), true);\n                    scaleInAnimation(fabClear, 150, 150, new OvershootInterpolator(), true);\n                    scaleInAnimation(cardViewClear, 150, 150, new OvershootInterpolator(), true);\n\n                    fabSetColor.show();\n                    cardViewSetColor.setVisibility(View.VISIBLE);\n                    fabSaveAndClose.show();\n                    cardViewSaveAndClose.setVisibility(View.VISIBLE);\n                    fabClear.show();\n                    cardViewClear.setVisibility(View.VISIBLE);\n                } else {\n                    status = 0;\n                    fabActions.animate().rotation(0).setInterpolator(new AccelerateDecelerateInterpolator())\n                            .setDuration(100).start();\n\n                    fabSetColor.hide();\n                    cardViewSetColor.setVisibility(View.INVISIBLE);\n                    fabSaveAndClose.hide();\n                    cardViewSaveAndClose.setVisibility(View.INVISIBLE);\n                    fabClear.hide();\n                    cardViewClear.setVisibility(View.INVISIBLE);\n                }\n                view.setTag(status);\n            }\n        });\n\n        cardViewClear.setOnClickListener(this::clear);\n        fabClear.setOnClickListener(this::clear);\n        cardViewSaveAndClose.setOnClickListener(this::close);\n        fabSaveAndClose.setOnClickListener(this::close);\n        cardViewSetColor.setOnClickListener(this::setColor);\n        fabSetColor.setOnClickListener(this::setColor);\n\n        Bundle extras = getIntent().getExtras();\n        String imagePath = drawView.getImagePath();\n\n        boolean portrait = extras.getInt(SCREEN_ORIENTATION) == 1;\n        setOrientation(portrait);\n\n        loadOption = extras.getString(OPTION);\n        if (loadOption == null) {\n            loadOption = OPTION_DRAW;\n        }\n        // refImage can also be present if resuming a drawing\n        Uri uri = (Uri) extras.get(REF_IMAGE);\n        if (uri != null) {\n            refImage = new File(uri.getPath());\n        }\n        savepointImage = new File(imagePath);\n        savepointImage.delete();\n        if (refImage != null && refImage.exists()) {\n            ImageFileUtils.copyImageAndApplyExifRotation(refImage, savepointImage);\n        }\n        uri = (Uri) extras.get(EXTRA_OUTPUT);\n        if (uri != null) {\n            output = new File(uri.getPath());\n        } else {\n            output = new File(imagePath);\n        }\n\n        // At this point, we have:\n        // loadOption -- type of activity (draw, signature, annotate)\n        // refImage -- original image to work with\n        // savepointImage -- drawing to use as a starting point (may be copy of\n        // original)\n        // output -- where the output should be written\n\n        drawView.setupView(OPTION_SIGNATURE.equals(loadOption));\n\n        viewModel.getPenColor().observe(this, penColor -> {\n            if (OPTION_SIGNATURE.equals(loadOption) && viewModel.isDefaultValue()) {\n                drawView.setColor(Color.BLACK);\n            } else {\n                drawView.setColor(penColor);\n            }\n        });\n\n        drawViewModel = new ViewModelProvider(this, new ViewModelProvider.Factory() {\n            @NonNull\n            @Override\n            public <T extends ViewModel> T create(@NonNull Class<T> modelClass, @NonNull CreationExtras extras) {\n                return (T) new DrawViewModel(output, scheduler);\n            }\n        }).get(DrawViewModel.class);\n\n        drawViewModel.getSaveResult().observe(this, (success) -> {\n            if (success) {\n                setResult(Activity.RESULT_OK);\n            } else {\n                setResult(Activity.RESULT_CANCELED);\n            }\n\n            finish();\n        });\n\n        getOnBackPressedDispatcher().addCallback(onBackPressedCallback);\n    }\n\n    /**\n     * Only lock orientation on screens that are smaller than 600dp (https://android-developers.googleblog.com/2025/01/orientation-and-resizability-changes-in-android-16.html)\n     */\n    @SuppressLint(\"SourceLockedOrientationActivity\")\n    private void setOrientation(boolean portrait) {\n        int smallestScreenWidthDp = getResources().getConfiguration().smallestScreenWidthDp;\n        if (smallestScreenWidthDp >= 600) {\n            if (portrait) {\n                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);\n            } else {\n                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);\n            }\n        }\n    }\n\n    private void reset() {\n        savepointImage.delete();\n        if (!OPTION_SIGNATURE.equals(loadOption) && refImage != null\n                && refImage.exists()) {\n            ImageFileUtils.copyImageAndApplyExifRotation(refImage, savepointImage);\n        }\n        drawView.reset();\n        drawView.invalidate();\n    }\n\n    private void cancelAndClose() {\n        setResult(Activity.RESULT_CANCELED);\n        this.finish();\n    }\n\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        switch (keyCode) {\n            case KeyEvent.KEYCODE_DPAD_RIGHT:\n            case KeyEvent.KEYCODE_DPAD_LEFT:\n                if (event.isAltPressed()) {\n                    createQuitDrawDialog();\n                    return true;\n                }\n                break;\n        }\n        return super.onKeyDown(keyCode, event);\n    }\n\n    /**\n     * Create a dialog with options to save and exit, save, or quit without\n     * saving\n     */\n    private void createQuitDrawDialog() {\n        QuitDrawingDialog.show(\n                this,\n                this::cancelAndClose,\n                () -> drawViewModel.save(drawView)\n        );\n    }\n\n    private void clear(View view) {\n        if (view.getVisibility() == View.VISIBLE) {\n            fabActions.performClick();\n            reset();\n        }\n    }\n\n    private void close(View view) {\n        if (view.getVisibility() == View.VISIBLE) {\n            fabActions.performClick();\n            drawViewModel.save(drawView);\n        }\n    }\n\n    private void setColor(View view) {\n        if (view.getVisibility() == View.VISIBLE) {\n            fabActions.performClick();\n\n            DialogFragmentUtils.showIfNotShowing(PenColorPickerDialog.class, getSupportFragmentManager());\n        }\n    }\n\n    private static void scaleInAnimation(final View view, int startOffset, int duration,\n                                         Interpolator interpolator, final boolean isInvisible) {\n        ScaleAnimation scaleInAnimation = new ScaleAnimation(0f, 1f, 0f, 1f, Animation.RELATIVE_TO_SELF,\n                0.5f, Animation.RELATIVE_TO_SELF, 0.5f);\n        scaleInAnimation.setInterpolator(interpolator);\n        scaleInAnimation.setDuration(duration);\n        scaleInAnimation.setStartOffset(startOffset);\n        scaleInAnimation.setAnimationListener(new Animation.AnimationListener() {\n            @Override\n            public void onAnimationStart(Animation animation) {\n                if (isInvisible) {\n                    view.setVisibility(View.VISIBLE);\n                }\n            }\n\n            @Override\n            public void onAnimationEnd(Animation animation) {\n            }\n\n            @Override\n            public void onAnimationRepeat(Animation animation) {\n            }\n        });\n        view.startAnimation(scaleInAnimation);\n    }\n}\n"
  },
  {
    "path": "draw/src/main/java/org/odk/collect/draw/DrawView.kt",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.draw\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.Paint\nimport android.graphics.Path\nimport android.util.AttributeSet\nimport android.view.MotionEvent\nimport android.view.View\nimport org.odk.collect.androidshared.bitmap.ImageFileUtils\nimport java.io.File\nimport javax.inject.Inject\n\nclass DrawView(context: Context, attrs: AttributeSet?) : View(context, attrs) {\n\n    init {\n        (context.applicationContext as DrawDependencyComponentProvider)\n            .drawDependencyComponent.inject(this)\n    }\n\n    private lateinit var bitmap: Bitmap\n\n    @Inject\n    lateinit var imagePath: String\n\n    private val paint = Paint().apply {\n        isAntiAlias = true\n        isDither = true\n        style = Paint.Style.STROKE\n        strokeJoin = Paint.Join.ROUND\n        strokeWidth = 10f\n    }\n\n    private var canvas = Canvas()\n    private var currentPath = Path()\n    private var offscreenPath = Path()\n    private var valueX = 0f\n    private var valueY = 0f\n\n    val bitmapHeight: Int\n        get() = bitmap.height\n\n    val bitmapWidth: Int\n        get() = bitmap.width\n\n    // Centered horizontally\n    private val bitmapLeft: Int\n        get() = (width - bitmap.width) / 2\n\n    // Centered vertically\n    private val bitmapTop: Int\n        get() = (height - bitmap.height) / 2\n\n    private var isSignature = false\n\n    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {\n        super.onSizeChanged(w, h, oldw, oldh)\n        resetImage(w, h)\n    }\n\n    override fun onDraw(canvas: Canvas) {\n        drawOnCanvas(canvas, bitmapLeft.toFloat(), bitmapTop.toFloat())\n    }\n\n    override fun onTouchEvent(event: MotionEvent): Boolean {\n        val x = event.x\n        val y = event.y\n        when (event.action) {\n            MotionEvent.ACTION_DOWN -> {\n                touchStart(x, y)\n                invalidate()\n            }\n\n            MotionEvent.ACTION_MOVE -> {\n                touchMove(x, y)\n                invalidate()\n            }\n\n            MotionEvent.ACTION_UP -> {\n                touchUp()\n                invalidate()\n            }\n        }\n        return true\n    }\n\n    fun setupView(isSignature: Boolean) {\n        this.isSignature = isSignature\n    }\n\n    fun reset() {\n        val metrics = resources.displayMetrics\n        resetImage(metrics.widthPixels, metrics.heightPixels)\n    }\n\n    fun setColor(color: Int) {\n        paint.color = color\n    }\n\n    fun drawOnCanvas(canvas: Canvas, left: Float, top: Float) {\n        canvas.drawColor(0xFFAAAAAA.toInt())\n        canvas.drawBitmap(bitmap, left, top, Paint(Paint.DITHER_FLAG))\n        canvas.drawPath(currentPath, paint)\n    }\n\n    private fun touchStart(x: Float, y: Float) {\n        currentPath.reset()\n        currentPath.moveTo(x, y)\n        offscreenPath.reset()\n        offscreenPath.moveTo(x - bitmapLeft, y - bitmapTop)\n        valueX = x\n        valueY = y\n    }\n\n    private fun touchMove(x: Float, y: Float) {\n        currentPath.quadTo(valueX, valueY, (x + valueX) / 2, (y + valueY) / 2)\n        offscreenPath.quadTo(\n            valueX - bitmapLeft,\n            valueY - bitmapTop,\n            (x + valueX) / 2 - bitmapLeft,\n            (y + valueY) / 2 - bitmapTop\n        )\n        valueX = x\n        valueY = y\n    }\n\n    private fun touchUp() {\n        if (currentPath.isEmpty) {\n            canvas.drawPoint(valueX, valueY, paint)\n        } else {\n            currentPath.lineTo(valueX, valueY)\n            offscreenPath.lineTo(valueX - bitmapLeft, valueY - bitmapTop)\n\n            // commit the path to our offscreen\n            canvas.drawPath(offscreenPath, paint)\n        }\n        // kill this so we don't double draw\n        currentPath.reset()\n    }\n\n    private fun drawLine() {\n        val originalColor = paint.color\n        paint.color = Color.BLACK\n        canvas.drawLine(\n            0f,\n            (canvas.height * .7).toFloat(),\n            canvas.width.toFloat(),\n            (canvas.height * .7).toFloat(),\n            paint\n        )\n        paint.color = originalColor\n    }\n\n    private fun resetImage(w: Int, h: Int) {\n        val backgroundBitmapFile = File(imagePath)\n        if (backgroundBitmapFile.exists()) {\n            bitmap = ImageFileUtils.getBitmapScaledToDisplay(backgroundBitmapFile, h, w, true)!!\n                .copy(Bitmap.Config.ARGB_8888, true)\n            canvas = Canvas(bitmap)\n        } else {\n            bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)\n            canvas = Canvas(bitmap)\n            canvas.drawColor(Color.WHITE)\n            if (isSignature) {\n                drawLine()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "draw/src/main/java/org/odk/collect/draw/DrawViewModel.kt",
    "content": "package org.odk.collect.draw\n\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport org.odk.collect.async.Scheduler\nimport timber.log.Timber\nimport java.io.File\nimport java.io.FileNotFoundException\nimport java.io.FileOutputStream\n\ninternal class DrawViewModel(\n    private val output: File,\n    private val scheduler: Scheduler\n) : ViewModel() {\n\n    private val _saveResult = MutableLiveData<Boolean>()\n    val saveResult = _saveResult\n\n    fun save(drawView: DrawView) {\n        scheduler.immediate(\n            background = {\n                try {\n                    saveImageFromDrawView(drawView)\n                    true\n                } catch (e: FileNotFoundException) {\n                    false\n                }\n            },\n            foreground = { success ->\n                _saveResult.value = success\n            }\n        )\n    }\n\n    private fun saveImageFromDrawView(drawView: DrawView) {\n        if (drawView.width == 0 || drawView.height == 0) {\n            // apparently on 4.x, the orientation change notification can occur\n            // sometime before the view is rendered. In that case, the view\n            // dimensions will not be known.\n            Timber.e(Error(\"View has zero width or zero height\"))\n        } else {\n            val fos = FileOutputStream(output)\n            val bitmap = Bitmap.createBitmap(\n                drawView.bitmapWidth,\n                drawView.bitmapHeight,\n                Bitmap.Config.ARGB_8888\n            )\n            val canvas = Canvas(bitmap)\n            drawView.drawOnCanvas(canvas, 0f, 0f)\n            bitmap.compress(Bitmap.CompressFormat.JPEG, 70, fos)\n            try {\n                fos.flush()\n                fos.close()\n            } catch (e: Exception) {\n                Timber.e(e)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "draw/src/main/java/org/odk/collect/draw/PenColorPickerDialog.kt",
    "content": "package org.odk.collect.draw\n\nimport android.app.Dialog\nimport android.os.Bundle\nimport androidx.fragment.app.DialogFragment\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.rarepebble.colorpicker.ColorPickerView\n\ninternal class PenColorPickerDialog(private val model: PenColorPickerViewModel) : DialogFragment() {\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        val picker = ColorPickerView(requireContext()).apply {\n            color = model.penColor.value\n            showAlpha(false)\n            showHex(false)\n        }\n\n        return MaterialAlertDialogBuilder(requireContext())\n            .setView(picker)\n            .setTitle(org.odk.collect.strings.R.string.project_color)\n            .setPositiveButton(org.odk.collect.strings.R.string.ok) { _, _ -> model.setPenColor(picker.color) }\n            .create()\n    }\n}\n"
  },
  {
    "path": "draw/src/main/java/org/odk/collect/draw/PenColorPickerViewModel.kt",
    "content": "package org.odk.collect.draw\n\nimport android.graphics.Color\nimport androidx.lifecycle.ViewModel\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\nimport org.odk.collect.shared.settings.Settings\n\ninternal class PenColorPickerViewModel(private val metaSettings: Settings, private val lastUsedKey: String) : ViewModel() {\n    private val lastUsedPenColor: Int\n        get() {\n            return if (metaSettings.contains(lastUsedKey)) {\n                metaSettings.getInt(lastUsedKey)\n            } else {\n                Color.BLACK\n            }\n        }\n\n    var isDefaultValue = true\n        private set\n\n    private val _penColor = MutableNonNullLiveData(lastUsedPenColor)\n    val penColor: NonNullLiveData<Int> = _penColor\n\n    fun setPenColor(color: Int) {\n        isDefaultValue = false\n        metaSettings.save(lastUsedKey, color)\n        _penColor.value = color\n    }\n}\n"
  },
  {
    "path": "draw/src/main/java/org/odk/collect/draw/QuitDrawingDialog.kt",
    "content": "package org.odk.collect.draw\n\nimport android.app.Activity\nimport androidx.appcompat.app.AlertDialog\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.draw.databinding.QuitDrawingDialogLayoutBinding\n\nobject QuitDrawingDialog {\n\n    @JvmStatic\n    fun show(\n        activity: Activity,\n        onDiscardChanges: Runnable?,\n        onSaveChanges: Runnable?\n    ): AlertDialog {\n        return create(\n            activity,\n            onDiscardChanges,\n            onSaveChanges\n        ).also {\n            it.show()\n        }\n    }\n\n    private fun create(\n        activity: Activity,\n        onDiscardChanges: Runnable?,\n        onSaveChanges: Runnable?\n    ): AlertDialog {\n        val binding = QuitDrawingDialogLayoutBinding.inflate(activity.layoutInflater)\n        val dialog = MaterialAlertDialogBuilder(activity)\n            .setTitle(org.odk.collect.strings.R.string.save_drawing_title)\n            .setView(binding.root)\n            .create()\n\n        binding.discardChanges.setOnClickListener {\n            onDiscardChanges?.run()\n        }\n\n        binding.keepEditing.setOnClickListener {\n            dialog.dismiss()\n        }\n\n        binding.saveChanges.setOnClickListener {\n            onSaveChanges?.run()\n        }\n\n        return dialog\n    }\n}\n"
  },
  {
    "path": "draw/src/main/java/org/odk/collect/draw/RobolectricApplication.kt",
    "content": "package org.odk.collect.draw\n\nimport android.app.Application\n/**\n * Used as the Application in tests in in the `test/src` root. This is setup in `robolectric.properties`\n */\ninternal class RobolectricApplication : Application(), DrawDependencyComponentProvider {\n\n    override lateinit var drawDependencyComponent: DrawDependencyComponent\n\n    fun setupDependencies(dependencyModule: DrawDependencyModule) {\n        drawDependencyComponent = DaggerDrawDependencyComponent.builder()\n            .drawDependencyModule(dependencyModule)\n            .build()\n    }\n}\n"
  },
  {
    "path": "draw/src/main/res/layout/draw_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\">\n\n    <org.odk.collect.draw.DrawView\n        android:id=\"@+id/drawView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" >\n    </org.odk.collect.draw.DrawView>\n\n    <androidx.cardview.widget.CardView\n        android:id=\"@+id/cv_clear\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|end\"\n        android:layout_marginStart=\"72dp\"\n        android:layout_marginLeft=\"72dp\"\n        android:layout_marginEnd=\"72dp\"\n        android:layout_marginRight=\"72dp\"\n        android:layout_marginBottom=\"192dp\"\n        android:visibility=\"invisible\"\n        app:cardCornerRadius=\"4dp\"\n        app:cardElevation=\"6dp\"\n        app:cardPreventCornerOverlap=\"true\"\n        app:cardUseCompatPadding=\"true\"\n        tools:visibility=\"visible\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"6dp\"\n            android:text=\"@string/reset_image\"\n            android:textSize=\"14sp\" />\n\n    </androidx.cardview.widget.CardView>\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/fab_clear\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|end\"\n        android:layout_marginBottom=\"198dp\"\n        android:layout_marginEnd=\"24dp\"\n        android:layout_marginStart=\"24dp\"\n        android:layout_marginRight=\"24dp\"\n        android:layout_marginLeft=\"24dp\"\n        android:visibility=\"invisible\"\n        app:elevation=\"6dp\"\n        style=\"?attr/floatingActionButtonSmallStyle\"\n        app:srcCompat=\"@drawable/ic_clear_white\"\n        tools:visibility=\"visible\"/>\n\n    <androidx.cardview.widget.CardView\n        android:id=\"@+id/cv_save_and_close\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|end\"\n        android:layout_marginBottom=\"142dp\"\n        android:layout_marginEnd=\"72dp\"\n        android:layout_marginStart=\"72dp\"\n        android:layout_marginRight=\"72dp\"\n        android:layout_marginLeft=\"72dp\"\n        android:visibility=\"invisible\"\n        app:cardCornerRadius=\"4dp\"\n        app:cardElevation=\"6dp\"\n        app:cardPreventCornerOverlap=\"true\"\n        app:cardUseCompatPadding=\"true\"\n        tools:visibility=\"visible\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"6dp\"\n            android:text=\"@string/save_and_close\"\n            android:textSize=\"14sp\" />\n\n    </androidx.cardview.widget.CardView>\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/fab_save_and_close\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|end\"\n        android:layout_marginBottom=\"147dp\"\n        android:layout_marginEnd=\"24dp\"\n        android:layout_marginStart=\"24dp\"\n        android:layout_marginRight=\"24dp\"\n        android:layout_marginLeft=\"24dp\"\n        android:visibility=\"invisible\"\n        app:elevation=\"6dp\"\n        style=\"?attr/floatingActionButtonSmallStyle\"\n        app:srcCompat=\"@drawable/ic_save_white\"\n        tools:visibility=\"visible\"/>\n\n    <androidx.cardview.widget.CardView\n        android:id=\"@+id/cv_set_color\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|end\"\n        android:layout_marginBottom=\"92dp\"\n        android:layout_marginEnd=\"72dp\"\n        android:layout_marginStart=\"72dp\"\n        android:layout_marginRight=\"72dp\"\n        android:layout_marginLeft=\"72dp\"\n        android:visibility=\"invisible\"\n        app:cardCornerRadius=\"4dp\"\n        app:cardElevation=\"6dp\"\n        app:cardPreventCornerOverlap=\"true\"\n        app:cardUseCompatPadding=\"true\"\n        tools:visibility=\"visible\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"6dp\"\n            android:text=\"@string/set_color\"\n            android:textSize=\"14sp\" />\n\n    </androidx.cardview.widget.CardView>\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/fab_set_color\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|end\"\n        android:layout_marginBottom=\"96dp\"\n        android:layout_marginEnd=\"24dp\"\n        android:layout_marginStart=\"24dp\"\n        android:layout_marginRight=\"24dp\"\n        android:layout_marginLeft=\"24dp\"\n        android:visibility=\"invisible\"\n        app:elevation=\"6dp\"\n        style=\"?attr/floatingActionButtonSmallStyle\"\n        app:srcCompat=\"@drawable/ic_color_lens_white\"\n        tools:visibility=\"visible\"/>\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/fab_actions\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|end\"\n        android:layout_marginBottom=\"30dp\"\n        android:layout_marginEnd=\"16dp\"\n        android:layout_marginStart=\"16dp\"\n        android:layout_marginRight=\"16dp\"\n        android:layout_marginLeft=\"16dp\"\n        android:tag=\"0\"\n        app:srcCompat=\"@drawable/ic_add_white_24\"/>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "draw/src/main/res/layout/quit_drawing_dialog_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"@dimen/margin_large\">\n\n        <com.google.android.material.textview.MaterialTextView\n            android:id=\"@+id/save_drawing_explanation\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/save_drawing_explanation\"\n            android:textAppearance=\"?textAppearanceBodyMedium\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/discard_changes\"\n            style=\"?materialButtonOutlinedIconStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginTop=\"@dimen/margin_standard\"\n            android:text=\"@string/discard_changes\"\n            app:icon=\"@drawable/ic_delete\"\n            app:iconGravity=\"textStart\"\n            app:iconSize=\"18dp\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/save_drawing_explanation\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/keep_editing\"\n            style=\"?materialButtonOutlinedIconStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:text=\"@string/keep_editing\"\n            app:icon=\"@drawable/ic_edit\"\n            app:iconGravity=\"textStart\"\n            app:iconSize=\"18dp\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/discard_changes\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/saveChanges\"\n            style=\"?materialButtonIconStyle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:text=\"@string/keep_changes\"\n            app:icon=\"@drawable/ic_save\"\n            app:iconGravity=\"textStart\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"@id/discard_changes\"\n            app:layout_constraintStart_toStartOf=\"@id/discard_changes\"\n            app:layout_constraintTop_toBottomOf=\"@id/keep_editing\" />\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</androidx.core.widget.NestedScrollView>\n"
  },
  {
    "path": "draw/src/test/java/org/odk/collect/draw/DrawActivityTest.kt",
    "content": "package org.odk.collect.draw\n\nimport android.app.Activity\nimport android.content.Intent\nimport androidx.test.core.app.ApplicationProvider.getApplicationContext\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidtest.ActivityScenarioExtensions.isFinishing\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.strings.R\nimport org.odk.collect.testshared.FakeScheduler\nimport org.odk.collect.testshared.EspressoInteractions\n\n@RunWith(AndroidJUnit4::class)\ninternal class DrawActivityTest {\n\n    @get:Rule\n    val launcherRule = ActivityScenarioLauncherRule()\n\n    private val application: RobolectricApplication by lazy { getApplicationContext() }\n    private val scheduler = FakeScheduler()\n\n    @Before\n    fun setup() {\n        application.setupDependencies(\n            object : DrawDependencyModule() {\n                override fun providesScheduler(): Scheduler {\n                    return scheduler\n                }\n\n                override fun providesSettingsProvider(): SettingsProvider {\n                    return InMemSettingsProvider()\n                }\n\n                override fun providesImagePath(): String {\n                    return TempFiles.createTempFile().absolutePath\n                }\n            }\n        )\n    }\n\n    @Test\n    fun `discarding changes closes the activity with canceled result`() {\n        val intent = Intent(getApplicationContext(), DrawActivity::class.java)\n\n        intent.putExtra(DrawActivity.SCREEN_ORIENTATION, 0)\n        val scenario = launcherRule.launchForResult<DrawActivity>(intent)\n\n        Espresso.pressBack()\n        EspressoInteractions.clickOn(withText(R.string.discard_changes), isDialog())\n        assertThat(scenario.isFinishing, equalTo(true))\n        assertThat(scenario.result.resultCode, equalTo(Activity.RESULT_CANCELED))\n    }\n\n    @Test\n    fun `choosing to keep editing does not close the activity`() {\n        val intent = Intent(getApplicationContext(), DrawActivity::class.java)\n\n        intent.putExtra(DrawActivity.SCREEN_ORIENTATION, 0)\n        val scenario = launcherRule.launchForResult<DrawActivity>(intent)\n\n        Espresso.pressBack()\n        EspressoInteractions.clickOn(withText(R.string.keep_editing), isDialog())\n        assertThat(scenario.isFinishing, equalTo(false))\n    }\n\n    @Test\n    fun `saving changes closes the activity with ok result`() {\n        val intent = Intent(getApplicationContext(), DrawActivity::class.java)\n\n        intent.putExtra(DrawActivity.SCREEN_ORIENTATION, 0)\n        val scenario = launcherRule.launchForResult<DrawActivity>(intent)\n\n        Espresso.pressBack()\n        EspressoInteractions.clickOn(withText(R.string.keep_changes), isDialog())\n        scheduler.flush()\n        assertThat(scenario.isFinishing, equalTo(true))\n        assertThat(scenario.result.resultCode, equalTo(Activity.RESULT_OK))\n    }\n}\n"
  },
  {
    "path": "draw/src/test/java/org/odk/collect/draw/PenColorPickerDialogTest.kt",
    "content": "package org.odk.collect.draw\n\nimport android.graphics.Color\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\n\n@RunWith(AndroidJUnit4::class)\nclass PenColorPickerDialogTest {\n\n    private val viewModel = mock<PenColorPickerViewModel>().also {\n        whenever(it.penColor).thenReturn(MutableNonNullLiveData(Color.BLACK))\n    }\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule(\n        defaultFactory = FragmentFactoryBuilder()\n            .forClass(PenColorPickerDialog::class) {\n                PenColorPickerDialog(\n                    viewModel\n                )\n            }\n            .build()\n    )\n\n    @Test\n    fun `dialog should be cancelable`() {\n        val scenario = launcherRule.launch(PenColorPickerDialog::class.java)\n        scenario.onFragment {\n            assertThat(it.isCancelable, `is`(true))\n        }\n    }\n\n    @Test\n    fun `pen color in view model should be set after clicking ok`() {\n        launcherRule.launch(PenColorPickerDialog::class.java)\n\n        onView(withText(org.odk.collect.strings.R.string.ok)).inRoot(isDialog()).perform(click())\n\n        verify(viewModel).setPenColor(Color.BLACK)\n    }\n}\n"
  },
  {
    "path": "draw/src/test/java/org/odk/collect/draw/PenColorPickerViewModelTest.kt",
    "content": "package org.odk.collect.draw\n\nimport android.graphics.Color\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.`is`\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.shared.settings.InMemSettings\nimport org.odk.collect.shared.settings.Settings\n\n@RunWith(AndroidJUnit4::class)\nclass PenColorPickerViewModelTest {\n    private lateinit var settings: Settings\n    private lateinit var viewModel: PenColorPickerViewModel\n\n    @Before\n    fun setup() {\n        settings = InMemSettings()\n        viewModel = PenColorPickerViewModel(settings, \"penColor\")\n    }\n\n    @Test\n    fun `default pen color should be black`() {\n        assertThat(viewModel.penColor.value, `is`(Color.BLACK))\n    }\n\n    @Test\n    fun `setPenColor sets penColor`() {\n        viewModel.setPenColor(Color.RED)\n        assertThat(viewModel.penColor.value, `is`(Color.RED))\n    }\n\n    @Test\n    fun `color saved in meta settings should be used as default if exists`() {\n        settings.save(\"penColor\", Color.RED)\n        viewModel = PenColorPickerViewModel(settings, \"penColor\")\n        assertThat(viewModel.penColor.value, `is`(Color.RED))\n    }\n}\n"
  },
  {
    "path": "draw/src/test/resources/robolectric.properties",
    "content": "application=org.odk.collect.draw.RobolectricApplication\nsdk=33\n"
  },
  {
    "path": "entities/.gitignore",
    "content": "/build"
  },
  {
    "path": "entities/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n    alias(libs.plugins.safeargsKotlin)\n    alias(libs.plugins.composeCompiler)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.entities\"\n\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n        buildConfig = true\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":strings\"))\n    implementation(project(\":shared\"))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":material\"))\n    implementation(project(\":async\"))\n    implementation(project(\":lists\"))\n    implementation(project(\":forms\"))\n    implementation(project(\":icons\"))\n\n    implementation(libs.kotlinStdlib)\n    implementation(libs.javarosa) {\n        exclude(group = \"joda-time\")\n        exclude(group = \"org.hamcrest\", module = \"hamcrest-all\")\n    }\n    implementation(libs.androidxAppcompat)\n    implementation(libs.androidMaterial)\n    implementation(libs.androidxNavigationFragmentKtx)\n    implementation(libs.androidxNavigationUi)\n    implementation(libs.dagger)\n    ksp(libs.daggerCompiler)\n\n    val composeBom = platform(libs.androidxComposeBom)\n    implementation(composeBom)\n    implementation(libs.androidXComposeMaterial)\n    implementation(libs.androidXConstraintLayoutCompose)\n    implementation(libs.runtime.livedata)\n    implementation(libs.androidXComposePreview)\n    debugImplementation(libs.androidXComposeTooling)\n\n    testImplementation(project(\":forms-test\"))\n    testImplementation(libs.junit)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.javarosa) // Include with all dependencies\n    testImplementation(libs.androidXComposeUiTestJunit4)\n    debugImplementation(libs.androidXComposeUiTestManifest)\n}\n"
  },
  {
    "path": "entities/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <application>\n        <activity\n            android:name=\".browser.EntityBrowserActivity\"\n            android:theme=\"@style/Theme.Material3.DayNight\"/>\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/DaggerSetup.kt",
    "content": "package org.odk.collect.entities\n\nimport dagger.Component\nimport dagger.Module\nimport dagger.Provides\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.entities.browser.EntityBrowserActivity\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport javax.inject.Singleton\n\ninterface EntitiesDependencyComponentProvider {\n    val entitiesDependencyComponent: EntitiesDependencyComponent\n}\n\n@Component(modules = [EntitiesDependencyModule::class])\n@Singleton\ninterface EntitiesDependencyComponent {\n\n    @Component.Builder\n    interface Builder {\n\n        fun entitiesDependencyModule(entitiesDependencyModule: EntitiesDependencyModule): Builder\n\n        fun build(): EntitiesDependencyComponent\n    }\n\n    fun inject(entityBrowserActivity: EntityBrowserActivity)\n}\n\n@Module\nopen class EntitiesDependencyModule {\n\n    @Provides\n    open fun providesEntitiesRepository(): EntitiesRepository {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesScheduler(): Scheduler {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesComposeThemeProvider(): ComposeThemeProvider {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt",
    "content": "package org.odk.collect.entities\n\nimport org.apache.commons.csv.CSVRecord\nimport org.javarosa.core.model.instance.SecondaryInstanceCSVParserBuilder\nimport org.odk.collect.entities.javarosa.finalization.EntitiesExtra\nimport org.odk.collect.entities.javarosa.parse.EntitySchema\nimport org.odk.collect.entities.javarosa.spec.EntityAction\nimport org.odk.collect.entities.server.EntitySource\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.forms.MediaFile\nimport org.odk.collect.shared.DebugLogger\nimport org.odk.collect.shared.Query\nimport java.io.File\nimport java.util.UUID\n\nobject LocalEntityUseCases {\n\n    @JvmStatic\n    fun updateLocalEntitiesFromForm(\n        formEntities: EntitiesExtra?,\n        entitiesRepository: EntitiesRepository,\n        debugLogger: DebugLogger? = null\n    ) {\n        formEntities?.entities?.forEach { formEntity ->\n            val id = formEntity.id\n            val label = formEntity.label\n            when (formEntity.action) {\n                EntityAction.CREATE -> {\n                    val list = entitiesRepository.getList(formEntity.dataset)\n                    if (list != null && !list.needsApproval) {\n                        val entity = Entity.New(\n                            id,\n                            label,\n                            1,\n                            formEntity.properties,\n                            branchId = UUID.randomUUID().toString()\n                        )\n\n                        entitiesRepository.save(formEntity.dataset, entity)\n                    }\n                }\n\n                EntityAction.UPDATE -> {\n                    val existing = entitiesRepository.query(\n                        formEntity.dataset,\n                        Query.StringEq(EntitySchema.ID, formEntity.id)\n                    ).firstOrNull()\n\n                    if (existing != null) {\n                        entitiesRepository.save(\n                            formEntity.dataset,\n                            existing.copy(\n                                label = label.ifBlank { existing.label },\n                                properties = formEntity.properties,\n                                version = existing.version + 1\n                            )\n                        )\n                    }\n                }\n            }\n        }\n\n        formEntities?.invalidEntities?.forEach {\n            debugLogger?.log(\n                \"Entities\",\n                \"Failed to create/update dataset=${it.dataset}, id=${it.id}, label=${it.label}\"\n            )\n        }\n    }\n\n    fun updateLocalEntitiesFromServer(\n        list: String,\n        serverList: File,\n        entitiesRepository: EntitiesRepository,\n        mediaFile: MediaFile\n    ) {\n        val existingListHash = entitiesRepository.getList(list)?.hash\n        if (mediaFile.hash == existingListHash) {\n            return\n        }\n\n        val csvParser = try {\n            SecondaryInstanceCSVParserBuilder()\n                .path(serverList.absolutePath)\n                .build()\n        } catch (_: Exception) {\n            return\n        }\n\n        val localEntities = entitiesRepository.query(list)\n\n        val missingFromServer = localEntities.associateBy { it.id }.toMutableMap()\n        val newAndUpdated = ArrayList<Entity>()\n        csvParser.use {\n            it.forEach { record ->\n                val serverEntity = parseEntityFromRecord(record) ?: return\n                val existing = missingFromServer.remove(serverEntity.id)\n\n                if (existing == null) {\n                    newAndUpdated.add(\n                        Entity.New(\n                            serverEntity.id,\n                            serverEntity.label,\n                            serverEntity.version,\n                            serverEntity.properties.toList(),\n                            state = Entity.State.ONLINE,\n                            trunkVersion = serverEntity.version,\n                            branchId = UUID.randomUUID().toString()\n                        )\n                    )\n                } else if (existing.version < serverEntity.version) {\n                    newAndUpdated.add(serverEntity.updateLocal(existing))\n                } else if (existing.version == serverEntity.version) {\n                    if (existing.isDirty()) {\n                        newAndUpdated.add(serverEntity.updateLocal(existing))\n                    }\n                } else if (existing.state == Entity.State.OFFLINE) {\n                    val update = existing.copy(state = Entity.State.ONLINE)\n                    newAndUpdated.add(update)\n                }\n            }\n        }\n\n        missingFromServer.values\n            .filter { it.state == Entity.State.ONLINE }\n            .forEach {\n                entitiesRepository.delete(list, it.id)\n            }\n\n        entitiesRepository.save(list, *newAndUpdated.toTypedArray())\n        entitiesRepository.updateList(\n            list,\n            mediaFile.hash,\n            mediaFile.type == MediaFile.Type.APPROVAL_ENTITY_LIST\n        )\n    }\n\n    fun cleanUpDeletedOfflineEntities(\n        list: String,\n        entitiesRepository: EntitiesRepository,\n        entitySource: EntitySource,\n        mediaFile: MediaFile\n    ) {\n        val offlineLocalEntities = entitiesRepository\n            .query(list)\n            .filter { it.state == Entity.State.OFFLINE }\n\n        val integrityUrl = mediaFile.integrityUrl\n        if (integrityUrl != null && offlineLocalEntities.isNotEmpty()) {\n            entitySource.fetchDeletedStates(integrityUrl, offlineLocalEntities.map { it.id })\n                .forEach {\n                    if (it.second) {\n                        entitiesRepository.delete(list, it.first)\n                    }\n                }\n        }\n    }\n\n    private fun parseEntityFromRecord(record: CSVRecord): ServerEntity? {\n        val map = record.toMap()\n\n        val id = map.remove(EntitySchema.ID)\n        val label = map.remove(EntitySchema.LABEL)\n        val version = map.remove(EntitySchema.VERSION)?.toInt()\n        if (id == null || label == null || version == null) {\n            return null\n        }\n\n        return ServerEntity(\n            id,\n            label,\n            version,\n            map\n        )\n    }\n}\n\nprivate data class ServerEntity(\n    val id: String,\n    val label: String,\n    val version: Int,\n    val properties: Map<String, String>\n) {\n\n    fun updateLocal(local: Entity.Saved): Entity.Saved {\n        return local.copy(\n            label = this.label,\n            version = this.version,\n            properties = this.properties.toList(),\n            state = Entity.State.ONLINE,\n            branchId = UUID.randomUUID().toString(),\n            trunkVersion = this.version\n        )\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/browser/EntitiesFragment.kt",
    "content": "package org.odk.collect.entities.browser\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.livedata.observeAsState\nimport androidx.compose.ui.platform.ComposeView\nimport androidx.compose.ui.platform.ViewCompositionStrategy\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\n\nclass EntitiesFragment(private val viewModelFactory: ViewModelProvider.Factory) : Fragment() {\n\n    private val entitiesViewModel by viewModels<EntitiesViewModel> { viewModelFactory }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return ComposeView(requireContext())\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        val composeView = view as ComposeView\n        val list = EntitiesFragmentArgs.fromBundle(requireArguments()).list\n        composeView.setContextThemedContent(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) {\n            Surface {\n                val entities by entitiesViewModel.getEntities(list).observeAsState(emptyList())\n                LazyColumn {\n                    items(entities) {\n                        EntityItem(entity = it)\n                        HorizontalDivider()\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/browser/EntitiesViewModel.kt",
    "content": "package org.odk.collect.entities.browser\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.entities.storage.getListNames\n\nclass EntitiesViewModel(\n    private val scheduler: Scheduler,\n    private val entitiesRepository: EntitiesRepository\n) : ViewModel() {\n\n    private val _lists = MutableLiveData<List<String>>(emptyList())\n    val lists: LiveData<List<String>> = _lists\n\n    init {\n        scheduler.immediate {\n            _lists.postValue(entitiesRepository.getListNames().toList())\n        }\n    }\n\n    fun getEntities(list: String): LiveData<List<Entity.Saved>> {\n        val result = MutableLiveData<List<Entity.Saved>>(emptyList())\n        scheduler.immediate {\n            result.postValue(entitiesRepository.query(list))\n        }\n\n        return result\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/browser/EntityBrowserActivity.kt",
    "content": "package org.odk.collect.entities.browser\n\nimport android.os.Bundle\nimport androidx.appcompat.widget.Toolbar\nimport androidx.compose.runtime.Composable\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport androidx.navigation.fragment.NavHostFragment\nimport androidx.navigation.ui.AppBarConfiguration\nimport androidx.navigation.ui.setupWithNavController\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.entities.EntitiesDependencyComponentProvider\nimport org.odk.collect.entities.R\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport javax.inject.Inject\n\nclass EntityBrowserActivity : LocalizedActivity(), ComposeThemeProvider {\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    @Inject\n    lateinit var entitiesRepository: EntitiesRepository\n\n    @Inject\n    lateinit var composeThemeProvider: ComposeThemeProvider\n\n    val viewModelFactory = viewModelFactory {\n        addInitializer(EntitiesViewModel::class) {\n            EntitiesViewModel(scheduler, entitiesRepository)\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        supportFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(EntityListsFragment::class) {\n                EntityListsFragment(\n                    viewModelFactory,\n                    ::getToolbar\n                )\n            }\n            .forClass(EntitiesFragment::class) { EntitiesFragment(viewModelFactory) }\n            .build()\n\n        super.onCreate(savedInstanceState)\n        (applicationContext as EntitiesDependencyComponentProvider)\n            .entitiesDependencyComponent.inject(this)\n\n        setView(R.layout.entities_layout, false)\n\n        val navHostFragment =\n            supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment\n        val navController = navHostFragment.navController\n\n        val appBarConfiguration = AppBarConfiguration(navController.graph)\n        getToolbar().setupWithNavController(navController, appBarConfiguration)\n    }\n\n    private fun getToolbar() = findViewById<Toolbar>(org.odk.collect.androidshared.R.id.toolbar)\n\n    @Composable\n    override fun Theme(content: @Composable (() -> Unit)) {\n        composeThemeProvider.Theme { content() }\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/browser/EntityItem.kt",
    "content": "package org.odk.collect.entities.browser\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.material.Pill\n\n@Composable\nfun EntityItem(modifier: Modifier = Modifier, entity: Entity.Saved) {\n    val marginStandard =\n        dimensionResource(org.odk.collect.androidshared.R.dimen.margin_standard)\n\n    Column(\n        modifier\n            .fillMaxWidth()\n            .padding(marginStandard)\n    ) {\n        val marginSmall = dimensionResource(org.odk.collect.androidshared.R.dimen.margin_small)\n        val marginExtraSmall =\n            dimensionResource(org.odk.collect.androidshared.R.dimen.margin_extra_small)\n\n        val offline = entity.state == Entity.State.OFFLINE\n        if (offline) {\n            Pill(\n                text = stringResource(org.odk.collect.strings.R.string.offline),\n                icon = org.odk.collect.icons.R.drawable.ic_baseline_wifi_off_24,\n                backgroundColor = MaterialTheme.colorScheme.surfaceContainerLowest\n            )\n        }\n\n        Text(\n            text = entity.label ?: \"\",\n            style = MaterialTheme.typography.titleMedium,\n            modifier = Modifier.padding(top = if (offline) marginSmall else 0.dp)\n        )\n\n        Text(\n            text = \"${entity.id} (${entity.version})\",\n            style = MaterialTheme.typography.labelMedium,\n            modifier = Modifier.padding(top = marginExtraSmall)\n        )\n\n        Text(\n            text = entity.properties\n                .sortedBy { it.first }\n                .joinToString(separator = \"\\n\") { \"${it.first}: ${it.second}\" },\n            style = MaterialTheme.typography.bodyMedium,\n            modifier = Modifier.padding(top = marginExtraSmall)\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun OfflinePreview() {\n    MaterialTheme {\n        EntityItem(\n            entity = Entity.Saved(\n                id = \"1\",\n                label = \"Entity 1\",\n                version = 1,\n                state = Entity.State.OFFLINE,\n                properties = listOf(\"Property 1\" to \"Value 1\", \"Property 2\" to \"Value 2\"),\n                index = 0\n            )\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun OnlinePreview() {\n    MaterialTheme {\n        EntityItem(\n            entity = Entity.Saved(\n                id = \"1\",\n                label = \"Entity 1\",\n                version = 1,\n                state = Entity.State.ONLINE,\n                properties = listOf(\"Property 1\" to \"Value 1\", \"Property 2\" to \"Value 2\"),\n                index = 0\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/browser/EntityListsFragment.kt",
    "content": "package org.odk.collect.entities.browser\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.view.MenuHost\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.navigation.NavController\nimport androidx.navigation.fragment.findNavController\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.RecyclerView.ViewHolder\nimport org.odk.collect.entities.R\nimport org.odk.collect.entities.databinding.EntityListItemLayoutBinding\nimport org.odk.collect.entities.databinding.ListLayoutBinding\nimport org.odk.collect.lists.RecyclerViewUtils\n\nclass EntityListsFragment(\n    private val viewModelFactory: ViewModelProvider.Factory,\n    private val menuHost: () -> MenuHost\n) : Fragment() {\n\n    private val entitiesViewModel by viewModels<EntitiesViewModel> { viewModelFactory }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return ListLayoutBinding.inflate(inflater, container, false).root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        val binding = ListLayoutBinding.bind(view)\n        binding.list.layoutManager = LinearLayoutManager(requireContext())\n        binding.list.addItemDecoration(RecyclerViewUtils.verticalLineDivider(requireContext()))\n\n        entitiesViewModel.lists.observe(viewLifecycleOwner) {\n            binding.list.adapter = ListsAdapter(it, findNavController())\n        }\n    }\n}\n\nprivate class ListsAdapter(\n    private val data: List<String>,\n    private val navController: NavController\n) : RecyclerView.Adapter<ListViewHolder>() {\n\n    override fun onCreateViewHolder(parent: ViewGroup, position: Int): ListViewHolder {\n        return ListViewHolder(parent)\n    }\n\n    override fun getItemCount(): Int {\n        return data.size\n    }\n\n    override fun onBindViewHolder(viewHolder: ListViewHolder, position: Int) {\n        val list = data[position]\n        viewHolder.setList(list)\n        viewHolder.itemView.setOnClickListener {\n            navController.navigate(\n                R.id.lists_to_entities,\n                EntitiesFragmentArgs(list).toBundle()\n            )\n        }\n    }\n}\n\nprivate class ListViewHolder(parent: ViewGroup) : ViewHolder(\n    EntityListItemLayoutBinding.inflate(\n        LayoutInflater.from(parent.context),\n        parent,\n        false\n    ).root\n) {\n\n    fun setList(list: String) {\n        EntityListItemLayoutBinding.bind(itemView).name.text = list\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/filter/LocalEntitiesFilterStrategy.kt",
    "content": "package org.odk.collect.entities.javarosa.filter\n\nimport org.javarosa.core.model.condition.EvaluationContext\nimport org.javarosa.core.model.condition.FilterStrategy\nimport org.javarosa.core.model.instance.DataInstance\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.xpath.expr.XPathExpression\nimport org.odk.collect.entities.javarosa.intance.LocalEntitiesInstanceAdapter\nimport org.odk.collect.entities.javarosa.intance.LocalEntitiesInstanceProvider\nimport org.odk.collect.entities.javarosa.parse.XPathExpressionExt.toQuery\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.entities.storage.QueryException\nimport org.odk.collect.shared.Query\nimport java.util.function.Supplier\n\n/**\n * A JavaRosa [FilterStrategy] that will use an [EntitiesRepository] to perform filters. For\n * supported expressions, this prevents JavaRosa from using it's standard [FilterStrategy] chain\n * which requires loading the whole secondary instance into memory (assuming that\n * [LocalEntitiesInstanceProvider] or similar is used to take advantage of JavaRosa's partial\n * parsing).\n */\nclass LocalEntitiesFilterStrategy(entitiesRepository: EntitiesRepository) :\n    FilterStrategy {\n\n    private val instanceAdapter = LocalEntitiesInstanceAdapter(entitiesRepository)\n\n    override fun filter(\n        sourceInstance: DataInstance<*>,\n        nodeSet: TreeReference,\n        predicate: XPathExpression,\n        children: MutableList<TreeReference>,\n        evaluationContext: EvaluationContext,\n        next: Supplier<MutableList<TreeReference>>\n    ): List<TreeReference> {\n        if (sourceInstance.instanceId == null || !instanceAdapter.supportsInstance(sourceInstance.instanceId)) {\n            return next.get()\n        }\n\n        val query = predicate.toQuery(sourceInstance, evaluationContext)\n        return if (query != null) {\n            try {\n                queryToTreeReferences(query, sourceInstance)\n            } catch (e: QueryException) {\n                next.get()\n            }\n        } else {\n            next.get()\n        }\n    }\n\n    private fun queryToTreeReferences(\n        query: Query,\n        sourceInstance: DataInstance<*>\n    ): List<TreeReference> {\n        val results = instanceAdapter.query(sourceInstance.instanceId, query)\n        sourceInstance.replacePartialElements(results)\n        return results.map {\n            it.parent = sourceInstance.root\n            it.ref\n        }\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/filter/PullDataFunctionHandler.kt",
    "content": "package org.odk.collect.entities.javarosa.filter\n\nimport org.javarosa.core.model.condition.EvaluationContext\nimport org.javarosa.core.model.condition.IFunctionHandler\nimport org.javarosa.xpath.expr.XPathFuncExpr\nimport org.odk.collect.entities.javarosa.intance.LocalEntitiesInstanceAdapter\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.entities.storage.QueryException\nimport org.odk.collect.shared.Query\n\nclass PullDataFunctionHandler(\n    entitiesRepository: EntitiesRepository,\n    private val fallback: IFunctionHandler? = null\n) : IFunctionHandler {\n\n    private val instanceAdapter = LocalEntitiesInstanceAdapter(entitiesRepository)\n\n    override fun getName(): String {\n        return NAME\n    }\n\n    override fun getPrototypes(): List<Array<Class<Any>>> {\n        return emptyList()\n    }\n\n    override fun rawArgs(): Boolean {\n        return true\n    }\n\n    override fun realTime(): Boolean {\n        return false\n    }\n\n    override fun eval(args: Array<Any>, ec: EvaluationContext): Any {\n        val instanceId = XPathFuncExpr.toString(args[0])\n\n        return if (instanceAdapter.supportsInstance(instanceId)) {\n            val child = XPathFuncExpr.toString(args[1])\n            val filterChild = XPathFuncExpr.toString(args[2])\n            val filterValue = XPathFuncExpr.toString(args[3])\n\n            try {\n                instanceAdapter.query(instanceId, Query.StringEq(filterChild, filterValue)).firstOrNull()\n                    ?.getFirstChild(child)?.value?.value ?: \"\"\n            } catch (e: QueryException) {\n                \"\"\n            }\n        } else {\n            fallback?.eval(args, ec) ?: \"\"\n        }\n    }\n\n    companion object {\n        private const val NAME = \"pulldata\"\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntitiesExtra.kt",
    "content": "package org.odk.collect.entities.javarosa.finalization\n\ndata class EntitiesExtra(\n    val entities: List<FormEntity> = emptyList(),\n    val invalidEntities: List<InvalidEntity> = emptyList()\n)\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.kt",
    "content": "package org.odk.collect.entities.javarosa.finalization\n\nimport org.javarosa.core.model.instance.FormInstance\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.form.api.FormEntryFinalizationProcessor\nimport org.javarosa.form.api.FormEntryModel\nimport org.odk.collect.entities.javarosa.parse.EntityFormExtra\nimport org.odk.collect.entities.javarosa.parse.SaveTo\nimport org.odk.collect.entities.javarosa.parse.isV4UUID\nimport org.odk.collect.entities.javarosa.spec.EntityAction\nimport org.odk.collect.entities.javarosa.spec.EntityFormParser\n\nclass EntityFormFinalizationProcessor : FormEntryFinalizationProcessor {\n    override fun processForm(formEntryModel: FormEntryModel) {\n        val formDef = formEntryModel.form\n        val mainInstance = formDef.mainInstance\n\n        val entityFormExtra = formDef.extras.get(EntityFormExtra::class.java)\n        if (entityFormExtra != null) {\n            val saveTos = entityFormExtra.saveTos\n\n            val entityElements = EntityFormParser.getEntityElements(mainInstance.getRoot())\n            val entitiesExtra = entityElements.fold(EntitiesExtra()) { extra, element ->\n                val action = EntityFormParser.parseAction(element)\n                val dataset = EntityFormParser.parseDataset(element)!!\n                val id = EntityFormParser.parseId(element)\n                val label = EntityFormParser.parseLabel(element)\n\n                if (action == EntityAction.CREATE || action == EntityAction.UPDATE) {\n                    val entity = createEntity(\n                        dataset,\n                        id,\n                        label,\n                        element.ref,\n                        saveTos,\n                        action,\n                        mainInstance\n                    )\n\n                    if (entity != null) {\n                        extra.copy(entities = extra.entities + entity)\n                    } else {\n                        val invalidEntity = InvalidEntity(dataset, id, label)\n                        extra.copy(invalidEntities = extra.invalidEntities + invalidEntity)\n                    }\n                } else {\n                    extra\n                }\n            }\n\n            formEntryModel.extras.put(entitiesExtra)\n        }\n    }\n\n    private fun createEntity(\n        dataset: String,\n        id: String?,\n        label: String,\n        elementRef: TreeReference,\n        saveTos: List<SaveTo>,\n        action: EntityAction,\n        mainInstance: FormInstance\n    ): FormEntity? {\n        val entityGroupRef = elementRef.getParentRef().getParentRef()\n        val fields = saveTos.mapNotNull { saveTo ->\n            if (!entityGroupRef.genericize().equals(saveTo.entityGroupReference)) {\n                null\n            } else {\n                val entityBindRef = saveTo.reference\n                val entityFieldRef = entityBindRef.contextualize(entityGroupRef)\n\n                val element = mainInstance.resolveReference(entityFieldRef)\n                if (element.isRelevant) {\n                    val value = element.value?.uncast()?.string ?: \"\"\n                    saveTo.value to value\n                } else {\n                    null\n                }\n            }\n        }\n\n        return if (id.isV4UUID() && (action == EntityAction.UPDATE || label.isNotBlank())) {\n            FormEntity(action, dataset, id, label, fields)\n        } else {\n            null\n        }\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/finalization/FormEntity.kt",
    "content": "package org.odk.collect.entities.javarosa.finalization\n\nimport org.odk.collect.entities.javarosa.spec.EntityAction\n\ndata class FormEntity(\n    @JvmField val action: EntityAction,\n    @JvmField val dataset: String,\n    @JvmField val id: String,\n    @JvmField val label: String,\n    @JvmField val properties: List<Pair<String, String>>\n)\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/finalization/InvalidEntity.kt",
    "content": "package org.odk.collect.entities.javarosa.finalization\n\ndata class InvalidEntity(\n    val dataset: String,\n    val id: String?,\n    val label: String?\n)\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesExternalInstanceParserFactory.kt",
    "content": "package org.odk.collect.entities.javarosa.intance\n\nimport org.javarosa.xform.parse.ExternalInstanceParser\nimport org.javarosa.xform.parse.ExternalInstanceParserFactory\nimport org.odk.collect.entities.storage.EntitiesRepository\n\nclass LocalEntitiesExternalInstanceParserFactory(\n    private val entitiesRepositoryProvider: () -> EntitiesRepository,\n    private val enabled: () -> Boolean\n) : ExternalInstanceParserFactory {\n    override fun getExternalInstanceParser(): ExternalInstanceParser {\n        val parser = ExternalInstanceParser()\n\n        if (enabled()) {\n            parser.addInstanceProvider(LocalEntitiesInstanceProvider(entitiesRepositoryProvider))\n        }\n\n        return parser\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesInstanceAdapter.kt",
    "content": "package org.odk.collect.entities.javarosa.intance\n\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.core.model.instance.TreeElement\nimport org.odk.collect.entities.javarosa.parse.EntitySchema\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.entities.storage.getListNames\nimport org.odk.collect.shared.Query\n\nclass LocalEntitiesInstanceAdapter(private val entitiesRepository: EntitiesRepository) {\n\n    private val lists = entitiesRepository.getListNames()\n\n    fun supportsInstance(instanceId: String): Boolean {\n        return lists.contains(instanceId)\n    }\n\n    fun getAll(instanceId: String, partial: Boolean): List<TreeElement> {\n        return if (partial) {\n            val count = entitiesRepository.getCount(instanceId)\n\n            if (count > 0) {\n                val first = entitiesRepository.getByIndex(instanceId, 0)!!\n\n                0.until(count).map {\n                    if (it == 0) {\n                        convertToElement(first)\n                    } else {\n                        TreeElement(\"item\", it, true)\n                    }\n                }\n            } else {\n                emptyList()\n            }\n        } else {\n            entitiesRepository.query(instanceId).map { entity ->\n                convertToElement(entity)\n            }\n        }\n    }\n\n    fun query(list: String, query: Query): List<TreeElement> {\n        return entitiesRepository\n            .query(list, query)\n            .map { convertToElement(it) }\n    }\n\n    private fun convertToElement(entity: Entity.Saved): TreeElement {\n        val name = TreeElement(EntitySchema.ID)\n        val label = TreeElement(EntitySchema.LABEL)\n        val version = TreeElement(EntitySchema.VERSION)\n        val trunkVersion = TreeElement(EntitySchema.TRUNK_VERSION)\n        val branchId = TreeElement(EntitySchema.BRANCH_ID)\n\n        name.value = StringData(entity.id)\n        version.value = StringData(entity.version.toString())\n        branchId.value = StringData(entity.branchId)\n\n        if (entity.label != null) {\n            label.value = StringData(entity.label)\n        }\n\n        if (entity.trunkVersion != null) {\n            trunkVersion.value = StringData(entity.trunkVersion.toString())\n        }\n\n        val item = TreeElement(\"item\", entity.index, false)\n        item.addChild(name)\n        item.addChild(label)\n        item.addChild(version)\n        item.addChild(trunkVersion)\n        item.addChild(branchId)\n\n        entity.properties.forEach { property ->\n            val propertyElement = TreeElement(property.first)\n            propertyElement.value = StringData(property.second)\n            item.addChild(propertyElement)\n        }\n\n        return item\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesInstanceProvider.kt",
    "content": "package org.odk.collect.entities.javarosa.intance\n\nimport org.javarosa.core.model.instance.TreeElement\nimport org.javarosa.xform.parse.ExternalInstanceParser\nimport org.odk.collect.entities.storage.EntitiesRepository\n\ninternal class LocalEntitiesInstanceProvider(private val entitiesRepositoryProvider: () -> EntitiesRepository) :\n    ExternalInstanceParser.InstanceProvider {\n\n    override fun get(instanceId: String, instanceSrc: String): TreeElement {\n        return get(instanceId, instanceSrc, false)\n    }\n\n    override fun get(instanceId: String, instanceSrc: String, partial: Boolean): TreeElement {\n        val root = TreeElement(\"root\", 0)\n        createDataAdapter().getAll(instanceId, partial).forEach { root.addChild(it) }\n        return root\n    }\n\n    override fun isSupported(instanceId: String, instanceSrc: String): Boolean {\n        return createDataAdapter().supportsInstance(instanceId)\n    }\n\n    private fun createDataAdapter() = LocalEntitiesInstanceAdapter(entitiesRepositoryProvider())\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityFormExtra.kt",
    "content": "package org.odk.collect.entities.javarosa.parse\n\nimport org.javarosa.core.util.externalizable.ExtUtil\nimport org.javarosa.core.util.externalizable.Externalizable\nimport org.javarosa.core.util.externalizable.PrototypeFactory\nimport java.io.DataInputStream\nimport java.io.DataOutputStream\n\nclass EntityFormExtra @JvmOverloads constructor(\n    saveTos: List<SaveTo> = emptyList()\n) : Externalizable {\n    private val _saveTos: MutableList<SaveTo> = saveTos.toMutableList()\n\n    val saveTos: List<SaveTo>\n        get() = _saveTos\n\n    override fun readExternal(input: DataInputStream, pf: PrototypeFactory) {\n        val size = ExtUtil.readInt(input)\n        _saveTos.clear()\n        repeat(size) {\n            val entry = SaveTo()\n            entry.readExternal(input, pf)\n            _saveTos.add(entry)\n        }\n    }\n\n    override fun writeExternal(output: DataOutputStream) {\n        ExtUtil.writeNumeric(output, _saveTos.size.toLong())\n        _saveTos.forEach { it.writeExternal(output) }\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityFormParseProcessor.kt",
    "content": "package org.odk.collect.entities.javarosa.parse\n\nimport org.javarosa.core.model.DataBinding\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.instance.TreeElement\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.model.xform.XPathReference\nimport org.javarosa.xform.parse.XFormParser\nimport org.javarosa.xform.parse.XFormParser.BindAttributeProcessor\nimport org.javarosa.xform.parse.XFormParser.FormDefProcessor\nimport org.javarosa.xform.parse.XFormParser.MissingModelAttributeException\nimport org.javarosa.xform.parse.XFormParser.ModelAttributeProcessor\nimport org.odk.collect.entities.javarosa.parse.EntityFormParseProcessor.Companion.VERSIONS.V2022_1\nimport org.odk.collect.entities.javarosa.parse.EntityFormParseProcessor.Companion.VERSIONS.V2023_1\nimport org.odk.collect.entities.javarosa.parse.EntityFormParseProcessor.Companion.VERSIONS.V2024_1\nimport org.odk.collect.entities.javarosa.parse.EntityFormParseProcessor.Companion.VERSIONS.V2025_1\nimport org.odk.collect.entities.javarosa.spec.EntityFormParser\nimport org.odk.collect.entities.javarosa.spec.UnrecognizedEntityVersionException\n\nclass EntityFormParseProcessor() : BindAttributeProcessor, FormDefProcessor, ModelAttributeProcessor {\n    private val bindAttributes = mutableListOf<Pair<TreeReference, String>>()\n    private var version: String? = null\n\n    override fun getModelAttributes(): Set<Pair<String, String>> {\n        return setOf(Pair(ENTITIES_NAMESPACE, \"entities-version\"))\n    }\n\n    @Throws(XFormParser.ParseException::class)\n    override fun processModelAttribute(name: String, value: String) {\n        version = value\n\n        if (SUPPORTED_VERSIONS.none { value.startsWith(it) }) {\n            throw UnrecognizedEntityVersionException(value)\n        }\n    }\n\n    override fun getBindAttributes(): Set<Pair<String, String>> {\n        return setOf(Pair(ENTITIES_NAMESPACE, \"saveto\"))\n    }\n\n    override fun processBindAttribute(name: String, value: String, binding: DataBinding) {\n        val reference = (binding.reference as XPathReference).reference as TreeReference\n        bindAttributes.add(Pair(reference, value))\n    }\n\n    @Throws(XFormParser.ParseException::class)\n    override fun processFormDef(formDef: FormDef) {\n        if (isEntityForm(formDef)) {\n            version.let {\n                if (it == null) {\n                    throw MissingModelAttributeException(ENTITIES_NAMESPACE, \"entities-version\")\n                } else if (LOCAL_ENTITY_VERSIONS.any { prefix -> it.startsWith(prefix) }) {\n                    val saveTos = bindAttributes.mapNotNull { (ref, value) ->\n                        val parentElement = formDef.mainInstance.resolveReference(ref).parent as TreeElement\n                        findNearestEntityGroupElement(parentElement)?.let { entityGroup ->\n                            SaveTo(ref, value, entityGroup.ref.genericize())\n                        }\n                    }\n                    val entityFormExtra = EntityFormExtra(saveTos)\n                    formDef.extras.put(entityFormExtra)\n                }\n            }\n        }\n    }\n\n    private fun findNearestEntityGroupElement(element: TreeElement?): TreeElement? {\n        if (element == null) {\n            return null\n        }\n        if (EntityFormParser.hasEntityElement(element)) {\n            return element\n        }\n        return findNearestEntityGroupElement(element.parent as? TreeElement)\n    }\n\n    companion object {\n\n        private object VERSIONS {\n            const val V2022_1 = \"2022.1\"\n            const val V2023_1 = \"2023.1\"\n            const val V2024_1 = \"2024.1\"\n            const val V2025_1 = \"2025.1\"\n        }\n\n        private const val ENTITIES_NAMESPACE = \"http://www.opendatakit.org/xforms/entities\"\n        private val SUPPORTED_VERSIONS = arrayOf(V2022_1, V2023_1, V2024_1, V2025_1)\n        private val LOCAL_ENTITY_VERSIONS = arrayOf(V2024_1, V2025_1)\n\n        private fun isEntityForm(formDef: FormDef): Boolean {\n            return EntityFormParser.hasEntityElement(formDef.mainInstance.root)\n        }\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntitySchema.kt",
    "content": "package org.odk.collect.entities.javarosa.parse\n\nobject EntitySchema {\n    const val ID = \"name\"\n    const val LABEL = \"label\"\n    const val VERSION = \"__version\"\n    const val TRUNK_VERSION = \"__trunkVersion\"\n    const val BRANCH_ID = \"__branchId\"\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.kt",
    "content": "package org.odk.collect.entities.javarosa.parse\n\nimport org.javarosa.xform.parse.IXFormParserFactory\nimport org.javarosa.xform.parse.XFormParser\n\nclass EntityXFormParserFactory(base: IXFormParserFactory) : IXFormParserFactory.Wrapper(base) {\n    override fun apply(parser: XFormParser): XFormParser {\n        val processor = EntityFormParseProcessor()\n        parser.addProcessor(processor)\n\n        return parser\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/parse/SaveTo.kt",
    "content": "package org.odk.collect.entities.javarosa.parse\n\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.core.util.externalizable.ExtUtil\nimport org.javarosa.core.util.externalizable.Externalizable\nimport org.javarosa.core.util.externalizable.PrototypeFactory\nimport java.io.DataInputStream\nimport java.io.DataOutputStream\n\nclass SaveTo() : Externalizable {\n    lateinit var reference: TreeReference\n        private set\n    lateinit var value: String\n        private set\n    lateinit var entityGroupReference: TreeReference\n        private set\n\n    constructor(\n        reference: TreeReference,\n        value: String,\n        entityGroupReference: TreeReference\n    ) : this() {\n        this.reference = reference\n        this.value = value\n        this.entityGroupReference = entityGroupReference\n    }\n\n    override fun readExternal(input: DataInputStream, pf: PrototypeFactory) {\n        reference = ExtUtil.read(input, TreeReference::class.java, pf) as TreeReference\n        value = ExtUtil.read(input, String::class.java, pf) as String\n        entityGroupReference = ExtUtil.read(input, TreeReference::class.java, pf) as TreeReference\n    }\n\n    override fun writeExternal(output: DataOutputStream) {\n        ExtUtil.write(output, reference)\n        ExtUtil.write(output, value)\n        ExtUtil.write(output, entityGroupReference)\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/parse/StringExt.kt",
    "content": "package org.odk.collect.entities.javarosa.parse\n\nimport java.util.UUID\nimport kotlin.contracts.ExperimentalContracts\nimport kotlin.contracts.contract\n\n@OptIn(ExperimentalContracts::class)\nfun String?.isV4UUID(): Boolean {\n    contract {\n        returns(true) implies (this@isV4UUID != null)\n    }\n\n    return if (this != null) {\n        try {\n            UUID.fromString(this).version() == 4\n        } catch (_: IllegalArgumentException) {\n            false\n        }\n    } else {\n        false\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/parse/XPathExpressionExt.kt",
    "content": "package org.odk.collect.entities.javarosa.parse\n\nimport org.javarosa.core.model.CompareToNodeExpression\nimport org.javarosa.core.model.condition.EvaluationContext\nimport org.javarosa.core.model.instance.DataInstance\nimport org.javarosa.xpath.expr.XPathBoolExpr\nimport org.javarosa.xpath.expr.XPathEqExpr\nimport org.javarosa.xpath.expr.XPathExpression\nimport org.javarosa.xpath.expr.XPathStep\nimport org.javarosa.xpath.expr.XPathStep.AXIS_CHILD\nimport org.javarosa.xpath.expr.XPathStep.AXIS_SELF\nimport org.javarosa.xpath.expr.XPathStep.TEST_TYPE_NODE\nimport org.odk.collect.shared.Query\n\nobject XPathExpressionExt {\n\n    /**\n     * Converts an XPath expression to a [Query]. For example:\n     * - `label = 'blah'` will be converted to `Query.StringEq(\"label\", \"blah\")`\n     * - `label` = /some/string/ref will be converted to `Query.StringEq(\"label\", \"blah\")`\n     * (where `/some/string/ref` evaluates to `\"blah\"` within the context of the passed\n     * `DataInstance` and `EvaluationContext`)\n     *\n     * `and`, `or`, `=` and `!=` are all supported. If an expression cannot be converted to a\n     * `Query`, `null` will be returned.\n     */\n    fun XPathExpression.toQuery(\n        sourceInstance: DataInstance<*>,\n        evaluationContext: EvaluationContext\n    ): Query? {\n        return when (this) {\n            is XPathBoolExpr -> xPathBoolExprToQuery(this, sourceInstance, evaluationContext)\n            is XPathEqExpr -> xPathEqExprToQuery(this, sourceInstance, evaluationContext)\n            else -> null\n        }\n    }\n\n    private fun xPathBoolExprToQuery(\n        predicate: XPathBoolExpr,\n        sourceInstance: DataInstance<*>,\n        evaluationContext: EvaluationContext\n    ): Query? {\n        val queryA = predicate.a.toQuery(sourceInstance, evaluationContext)\n        val queryB = predicate.b.toQuery(sourceInstance, evaluationContext)\n\n        return if (queryA != null && queryB != null) {\n            if (predicate.op == XPathBoolExpr.AND) {\n                Query.And(queryA, queryB)\n            } else {\n                Query.Or(queryA, queryB)\n            }\n        } else {\n            null\n        }\n    }\n\n    private fun xPathEqExprToQuery(\n        predicate: XPathEqExpr,\n        sourceInstance: DataInstance<*>,\n        evaluationContext: EvaluationContext\n    ): Query? {\n        val candidate = CompareToNodeExpression.parse(predicate)\n\n        return if (candidate != null) {\n            val steps = candidate.nodeSide.steps\n            val child = if (steps.size == 1) {\n                steps[0].name.name\n            } else if (isNodeRelativeExpression(steps)) {\n                steps[1].name.name\n            } else {\n                return null\n            }\n\n            val value = candidate.evalContextSide(sourceInstance, evaluationContext)\n\n            if (predicate.isEqual) {\n                if (value is Double) {\n                    Query.NumericEq(child, value)\n                } else {\n                    Query.StringEq(child, value.toString())\n                }\n            } else {\n                if (value is Double) {\n                    Query.NumericNotEq(child, value)\n                } else {\n                    Query.StringNotEq(child, value.toString())\n                }\n            }\n        } else {\n            null\n        }\n    }\n\n    private fun isNodeRelativeExpression(steps: Array<XPathStep>): Boolean {\n        return if (steps.size == 2 && steps[0].test == TEST_TYPE_NODE) {\n            return steps[0].axis == AXIS_SELF || steps[0].axis == AXIS_CHILD\n        } else {\n            false\n        }\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityAction.kt",
    "content": "package org.odk.collect.entities.javarosa.spec\n\nenum class EntityAction {\n    CREATE,\n    UPDATE\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityFormParser.kt",
    "content": "package org.odk.collect.entities.javarosa.spec\n\nimport org.javarosa.core.model.instance.TreeElement\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.xpath.expr.XPathFuncExpr\nimport org.odk.collect.entities.javarosa.spec.FormEntityElement.ATTRIBUTE_CREATE\nimport org.odk.collect.entities.javarosa.spec.FormEntityElement.ATTRIBUTE_DATASET\nimport org.odk.collect.entities.javarosa.spec.FormEntityElement.ATTRIBUTE_ID\nimport org.odk.collect.entities.javarosa.spec.FormEntityElement.ATTRIBUTE_UPDATE\nimport org.odk.collect.entities.javarosa.spec.FormEntityElement.ELEMENT_ENTITY\nimport org.odk.collect.entities.javarosa.spec.FormEntityElement.ELEMENT_LABEL\n\nobject EntityFormParser {\n    fun parseDataset(entity: TreeElement): String? {\n        return entity.getAttributeValue(null, ATTRIBUTE_DATASET)\n    }\n\n    @JvmStatic\n    fun parseLabel(entity: TreeElement): String {\n        val labelElement = entity.getFirstChild(ELEMENT_LABEL)\n        return labelElement?.value?.uncast()?.string ?: \"\"\n    }\n\n    fun parseId(entity: TreeElement): String? {\n        return entity.getAttributeValue(\"\", ATTRIBUTE_ID)\n    }\n\n    fun getEntityElements(treeElement: TreeElement): List<TreeElement> {\n        val entityElements = mutableListOf<TreeElement>()\n\n        val numOfChildren = treeElement.numChildren\n        for (i in 0..<numOfChildren) {\n            val childTreeElement = treeElement.getChildAt(i)\n            if (\"meta\" == childTreeElement.name) {\n                val entity = childTreeElement.getFirstChild(ELEMENT_ENTITY)\n                if (entity != null) {\n                    entityElements.add(entity)\n                }\n            } else if (childTreeElement.hasChildren() && childTreeElement.mult != TreeReference.INDEX_TEMPLATE) {\n                entityElements.addAll(getEntityElements(childTreeElement))\n            }\n        }\n\n        return entityElements\n    }\n\n    fun hasEntityElement(treeElement: TreeElement): Boolean {\n        return getEntityElements(treeElement).isNotEmpty()\n    }\n\n    @JvmStatic\n    fun parseAction(entity: TreeElement): EntityAction? {\n        val create = entity.getAttributeValue(null, ATTRIBUTE_CREATE)\n        val update = entity.getAttributeValue(null, ATTRIBUTE_UPDATE)\n\n        var shouldCreate = false\n        if (create != null) {\n            if (XPathFuncExpr.boolStr(create)) {\n                shouldCreate = true\n            }\n        }\n\n        var shouldUpdate = false\n        if (update != null) {\n            if (XPathFuncExpr.boolStr(update)) {\n                shouldUpdate = true\n            }\n        }\n\n        return if (shouldCreate && shouldUpdate) {\n            null\n        } else if (shouldCreate) {\n            EntityAction.CREATE\n        } else if (shouldUpdate) {\n            EntityAction.UPDATE\n        } else {\n            null\n        }\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/spec/FormEntityElement.kt",
    "content": "package org.odk.collect.entities.javarosa.spec\n\ninternal object FormEntityElement {\n    const val ELEMENT_ENTITY = \"entity\"\n    const val ELEMENT_LABEL = \"label\"\n\n    const val ATTRIBUTE_DATASET = \"dataset\"\n    const val ATTRIBUTE_ID = \"id\"\n    const val ATTRIBUTE_BASE_VERSION = \"baseVersion\"\n    const val ATTRIBUTE_CREATE = \"create\"\n    const val ATTRIBUTE_UPDATE = \"update\"\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/javarosa/spec/UnrecognizedEntityVersionException.kt",
    "content": "package org.odk.collect.entities.javarosa.spec\n\nimport org.javarosa.xform.parse.XFormParser\n\nclass UnrecognizedEntityVersionException(val entityVersion: String) : XFormParser.ParseException()\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/server/EntitySource.kt",
    "content": "package org.odk.collect.entities.server\n\ninterface EntitySource {\n    fun fetchDeletedStates(integrityUrl: String, ids: List<String>): List<Pair<String, Boolean>>\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/storage/EntitiesRepository.kt",
    "content": "package org.odk.collect.entities.storage\n\nimport org.odk.collect.shared.Query\n\ninterface EntitiesRepository {\n    fun save(list: String, vararg entities: Entity)\n    fun getLists(): List<EntityList>\n    fun getCount(list: String): Int\n    fun addList(list: String)\n    fun delete(list: String, id: String)\n    fun query(list: String, query: Query? = null): List<Entity.Saved>\n    fun getByIndex(list: String, index: Int): Entity.Saved?\n    fun updateList(list: String, hash: String, needsApproval: Boolean)\n    fun getList(list: String): EntityList?\n}\n\nfun EntitiesRepository.getListNames(): List<String> {\n    return getLists().map { it.name }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/storage/Entity.kt",
    "content": "package org.odk.collect.entities.storage\n\nsealed interface Entity {\n    val id: String\n    val label: String?\n    val version: Int\n    val properties: List<Pair<String, String>>\n    val state: State\n\n    /**\n     * The server version (from an entity list CSV) this is based on. This should only be updated\n     * when updating an entity from the server where as [version] should be incremented whenever\n     * there is a local change.\n     */\n    val trunkVersion: Int?\n\n    /**\n     * The offline \"branch\" identifier. Should be updated whenever the local version is modified\n     * from the latest server version.\n     */\n    val branchId: String\n\n    fun isDirty(): Boolean {\n        return version != trunkVersion\n    }\n\n    data class New(\n        override val id: String,\n        override val label: String?,\n        override val version: Int = 1,\n        override val properties: List<Pair<String, String>> = emptyList(),\n        override val state: State = State.OFFLINE,\n        override val trunkVersion: Int? = null,\n        override val branchId: String = \"\"\n    ) : Entity\n\n    data class Saved(\n        override val id: String,\n        override val label: String?,\n        override val version: Int = 1,\n        override val properties: List<Pair<String, String>> = emptyList(),\n        override val state: State = State.OFFLINE,\n        val index: Int,\n        override val trunkVersion: Int? = null,\n        override val branchId: String = \"\"\n    ) : Entity\n\n    enum class State {\n        OFFLINE,\n        ONLINE\n    }\n\n    fun sameAs(entity: Entity): Boolean {\n        val a = convertToNew(this)\n        val b = convertToNew(entity)\n        return a == b\n    }\n\n    private fun convertToNew(entity: Entity): New {\n        return New(\n            entity.id,\n            entity.label,\n            entity.version,\n            entity.properties,\n            entity.state,\n            entity.trunkVersion,\n            entity.branchId\n        )\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/storage/EntityList.kt",
    "content": "package org.odk.collect.entities.storage\n\ndata class EntityList(val name: String, val hash: String? = null, val needsApproval: Boolean = false, val lastUpdated: Long? = null)\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/storage/InMemEntitiesRepository.kt",
    "content": "package org.odk.collect.entities.storage\n\nimport org.odk.collect.entities.javarosa.parse.EntitySchema\nimport org.odk.collect.shared.Query\n\nclass InMemEntitiesRepository(private val clock: () -> Long = { 0 }) : EntitiesRepository {\n\n    private val lists = mutableListOf<EntityList>()\n    private val listProperties = mutableMapOf<String, MutableSet<String>>()\n    private val entities = mutableMapOf<String, MutableList<Entity.New>>()\n\n    override fun getLists(): List<EntityList> {\n        return lists\n    }\n\n    override fun getCount(list: String): Int {\n        return query(list).count()\n    }\n\n    override fun addList(list: String) {\n        if (lists.none { it.name == list }) {\n            lists.add(EntityList(list))\n        }\n    }\n\n    override fun delete(list: String, id: String) {\n        entities[list]?.removeIf { it.id == id }\n    }\n\n    override fun query(list: String, query: Query?): List<Entity.Saved> {\n        val entities = (entities[list] ?: emptyList()).mapIndexed { index, entity ->\n            Entity.Saved(\n                entity.id,\n                entity.label,\n                entity.version,\n                buildProperties(list, entity),\n                entity.state,\n                index,\n                entity.trunkVersion,\n                entity.branchId\n            )\n        }\n\n        fun Entity.getFieldValue(column: String): String = when (column) {\n            EntitySchema.ID -> id\n            EntitySchema.LABEL -> label!!\n            EntitySchema.VERSION -> version.toString()\n            else -> properties.find { it.first == column }?.second\n                ?: throw QueryException(\"No such column: $column\")\n        }\n\n        return when (query) {\n            is Query.StringEq -> entities.filter { it.getFieldValue(query.column) == query.value }\n            is Query.StringNotEq -> entities.filter { it.getFieldValue(query.column) != query.value }\n            is Query.NumericEq -> entities.filter { it.getFieldValue(query.column).toDoubleOrNull() == query.value }\n            is Query.NumericNotEq -> entities.filter { it.getFieldValue(query.column).toDoubleOrNull() != query.value }\n            is Query.And -> query(list, query.queryA).intersect(query(list, query.queryB)).toList()\n            is Query.Or -> query(list, query.queryA).union(query(list, query.queryB)).toList()\n            null -> entities\n        }\n    }\n\n    override fun getByIndex(list: String, index: Int): Entity.Saved? {\n        return query(list).firstOrNull { it.index == index }\n    }\n\n    override fun updateList(list: String, hash: String, needsApproval: Boolean) {\n        val existing = lists.firstOrNull { it.name == list }\n        if (existing != null) {\n            val update =\n                existing.copy(hash = hash, needsApproval = needsApproval, lastUpdated = clock())\n            lists.remove(existing)\n            lists.add(update)\n        } else {\n            lists.add(EntityList(list, hash, needsApproval, clock()))\n        }\n    }\n\n    override fun getList(list: String): EntityList? {\n        return lists.firstOrNull { it.name == list }\n    }\n\n    override fun save(list: String, vararg entities: Entity) {\n        val entityList = this.entities.getOrPut(list) { mutableListOf() }\n\n        entities.forEach { entity ->\n            updateLists(list, entity)\n            val existing = entityList.find { it.id == entity.id }\n\n            if (existing != null) {\n                val state = when (existing.state) {\n                    Entity.State.OFFLINE -> entity.state\n                    Entity.State.ONLINE -> Entity.State.ONLINE\n                }\n\n                entityList.remove(existing)\n                entityList.add(\n                    Entity.New(\n                        entity.id,\n                        entity.label ?: existing.label,\n                        version = entity.version,\n                        properties = mergeProperties(existing, entity),\n                        state = state,\n                        trunkVersion = entity.trunkVersion,\n                        branchId = entity.branchId\n                    )\n                )\n            } else {\n                entityList.add(\n                    Entity.New(\n                        entity.id,\n                        entity.label,\n                        entity.version,\n                        entity.properties,\n                        entity.state,\n                        entity.trunkVersion,\n                        entity.branchId\n                    )\n                )\n            }\n        }\n    }\n\n    private fun updateLists(list: String, entity: Entity) {\n        addList(list)\n\n        val properties = listProperties.getOrPut(list) {\n            mutableSetOf()\n        }\n\n        val expectedProperties = entity\n            .properties\n            .map { it.first }\n            .distinctBy { it.lowercase() }\n\n        val newProperties = expectedProperties\n            .filterNot { properties.any { property -> property.equals(it, ignoreCase = true) } }\n        properties.addAll(newProperties)\n\n        val removedProperties = properties\n            .filter { property -> expectedProperties.none { it.equals(property, ignoreCase = true) } }\n        properties.removeAll(removedProperties.toSet())\n    }\n\n    private fun mergeProperties(\n        existing: Entity,\n        new: Entity\n    ): List<Pair<String, String>> {\n        val existingProperties = mutableMapOf(*existing.properties.toTypedArray())\n        new.properties.forEach {\n            existingProperties[it.first] = it.second\n        }\n\n        return existingProperties.map { Pair(it.key, it.value) }\n    }\n\n    private fun buildProperties(list: String, entity: Entity.New): List<Pair<String, String>> {\n        return listProperties[list]?.map { property ->\n            Pair(\n                property,\n                entity.properties.find { it.first == property }?.second ?: \"\"\n            )\n        } ?: emptyList()\n    }\n}\n"
  },
  {
    "path": "entities/src/main/java/org/odk/collect/entities/storage/QueryException.kt",
    "content": "package org.odk.collect.entities.storage\n\nclass QueryException(message: String?) : RuntimeException(message)\n"
  },
  {
    "path": "entities/src/main/res/layout/entities_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/nav_host\"\n        android:name=\"androidx.navigation.fragment.NavHostFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:defaultNavHost=\"true\"\n        app:navGraph=\"@navigation/entities_nav\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "entities/src/main/res/layout/entity_list_item_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"@dimen/margin_standard\"\n    android:background=\"?android:attr/selectableItemBackground\">\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/name\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"?textAppearanceBodyLarge\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:text=\"content\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "entities/src/main/res/layout/list_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_marginBottom=\"?attr/actionBarSize\">\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "entities/src/main/res/navigation/entities_nav.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/main\"\n    app:startDestination=\"@id/lists\">\n\n    <fragment\n        android:id=\"@+id/lists\"\n        android:name=\"org.odk.collect.entities.browser.EntityListsFragment\"\n        android:label=\"@string/entities_title\">\n        <action\n            android:id=\"@+id/lists_to_entities\"\n            app:destination=\"@id/entities\" />\n    </fragment>\n\n    <fragment\n        android:id=\"@+id/entities\"\n        android:name=\"org.odk.collect.entities.browser.EntitiesFragment\"\n        android:label=\"{list}\">\n        <argument\n            android:name=\"list\"\n            app:argType=\"string\" />\n    </fragment>\n</navigation>\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/LocalEntityUseCasesTest.kt",
    "content": "package org.odk.collect.entities\n\nimport org.apache.commons.csv.CSVFormat\nimport org.apache.commons.csv.CSVPrinter\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.containsInAnyOrder\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.hamcrest.text.IsBlankString.blankOrNullString\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.entities.javarosa.finalization.EntitiesExtra\nimport org.odk.collect.entities.javarosa.finalization.FormEntity\nimport org.odk.collect.entities.javarosa.finalization.InvalidEntity\nimport org.odk.collect.entities.javarosa.parse.EntitySchema\nimport org.odk.collect.entities.javarosa.spec.EntityAction\nimport org.odk.collect.entities.server.EntitySource\nimport org.odk.collect.entities.storage.EntitiesRepository\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.entities.storage.EntityList\nimport org.odk.collect.entities.storage.InMemEntitiesRepository\nimport org.odk.collect.formstest.FormFixtures\nimport org.odk.collect.shared.DebugLogger\nimport org.odk.collect.shared.Query\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\nimport java.util.UUID\n\nclass LocalEntityUseCasesTest {\n\n    private val entitiesRepository = InMemEntitiesRepository()\n    private val entitySource = FakeEntitySource()\n\n    @Test\n    fun `#updateLocalEntitiesFromForm saves a new entity on create`() {\n        entitiesRepository.addList(\"things\")\n\n        val formEntity =\n            FormEntity(EntityAction.CREATE, \"things\", \"id\", \"label\", listOf(\"property\" to \"value\"))\n        val formEntities = EntitiesExtra(listOf(formEntity))\n        LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository)\n\n        val entities = entitiesRepository.query(\"things\")\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].id, equalTo(formEntity.id))\n        assertThat(entities[0].label, equalTo(formEntity.label))\n        assertThat(entities[0].properties, equalTo(formEntity.properties))\n        assertThat(entities[0].branchId, not(blankOrNullString()))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromForm does not save a new entity on create if the list doesn't already exist`() {\n        val formEntity =\n            FormEntity(EntityAction.CREATE, \"things\", \"id\", \"label\", listOf(\"property\" to \"value\"))\n        val formEntities = EntitiesExtra(listOf(formEntity))\n        LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository)\n\n        val entities = entitiesRepository.query(\"things\")\n        assertThat(entities.size, equalTo(0))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromForm increments version on update`() {\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\n                \"id\",\n                \"label\",\n                version = 1\n            )\n        )\n\n        val formEntity =\n            FormEntity(EntityAction.UPDATE, \"things\", \"id\", \"label\", emptyList())\n        val formEntities = EntitiesExtra(listOf(formEntity))\n\n        LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository)\n        val entities = entitiesRepository.query(\"things\")\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].version, equalTo(2))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromForm updates properties on update`() {\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\n                \"id\",\n                \"label\",\n                version = 1,\n                properties = listOf(\"prop\" to \"value\")\n            )\n        )\n\n        val formEntity =\n            FormEntity(EntityAction.UPDATE, \"things\", \"id\", \"label\", listOf(\"prop\" to \"value 2\"))\n        val formEntities = EntitiesExtra(listOf(formEntity))\n\n        LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository)\n        val entities = entitiesRepository.query(\"things\")\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].properties.size, equalTo(1))\n        assertThat(entities[0].properties[0], equalTo(\"prop\" to \"value 2\"))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromForm updates properties and does not change label on update if label is blank`() {\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\n                \"id\",\n                \"label\",\n                version = 1,\n                properties = listOf(\"prop\" to \"value\")\n            )\n        )\n\n        val formEntity =\n            FormEntity(EntityAction.UPDATE, \"things\", \"id\", \" \", listOf(\"prop\" to \"value 2\"))\n        val formEntities = EntitiesExtra(listOf(formEntity))\n\n        LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository)\n        val entities = entitiesRepository.query(\"things\")\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].label, equalTo(\"label\"))\n        assertThat(entities[0].properties.size, equalTo(1))\n        assertThat(entities[0].properties[0], equalTo(\"prop\" to \"value 2\"))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromForm does not override trunk version or branchId on update`() {\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\n                \"id\",\n                \"label\",\n                version = 1,\n                trunkVersion = 1,\n                branchId = \"branch-1\"\n            )\n        )\n\n        val formEntity =\n            FormEntity(EntityAction.UPDATE, \"things\", \"id\", \"label\", emptyList())\n        val formEntities = EntitiesExtra(listOf(formEntity))\n\n        LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository)\n        val entities = entitiesRepository.query(\"things\")\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].trunkVersion, equalTo(1))\n        assertThat(entities[0].branchId, equalTo(\"branch-1\"))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromForm does not save updated entity that doesn't already exist`() {\n        val formEntity =\n            FormEntity(EntityAction.UPDATE, \"things\", \"1\", \"1\", emptyList())\n        val formEntities = EntitiesExtra(listOf(formEntity))\n        entitiesRepository.addList(\"things\")\n\n        LocalEntityUseCases.updateLocalEntitiesFromForm(formEntities, entitiesRepository)\n        assertThat(entitiesRepository.query(\"things\").size, equalTo(0))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromForm logs invalid entities`() {\n        val debugLogger = mock<DebugLogger>()\n        val formEntities =\n            EntitiesExtra(emptyList(), listOf(InvalidEntity(\"things\", \"id\", \"label\")))\n\n        LocalEntityUseCases.updateLocalEntitiesFromForm(\n            formEntities,\n            entitiesRepository,\n            debugLogger\n        )\n\n        verify(debugLogger).log(\n            \"Entities\",\n            \"Failed to create/update dataset=things, id=id, label=label\"\n        )\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer saves entity from server`() {\n        val csv = createEntityList(\n            Entity.New(\n                \"noah\",\n                \"Noah\",\n                2,\n                properties = listOf(\"property\" to \"value\")\n            )\n        )\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(1))\n        assertThat(songs[0].label, equalTo(\"Noah\"))\n        assertThat(songs[0].version, equalTo(2))\n        assertThat(songs[0].properties, equalTo(listOf(\"property\" to \"value\")))\n        assertThat(songs[0].state, equalTo(Entity.State.ONLINE))\n        assertThat(songs[0].trunkVersion, equalTo(2))\n        assertThat(songs[0].branchId, not(blankOrNullString()))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer overrides offline version if the online version is newer`() {\n        val offline = Entity.New(\"noah\", \"Noa\", 1, trunkVersion = 1)\n        entitiesRepository.save(\"songs\", offline)\n        val csv = createEntityList(Entity.New(\"noah\", \"Noah\", 2))\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(1))\n        assertThat(songs[0].label, equalTo(\"Noah\"))\n        assertThat(songs[0].version, equalTo(2))\n        assertThat(songs[0].state, equalTo(Entity.State.ONLINE))\n        assertThat(songs[0].trunkVersion, equalTo(2))\n        assertThat(songs[0].branchId, not(blankOrNullString()))\n        assertThat(songs[0].branchId, not(equalTo(offline.branchId)))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer overrides offline version if the online version is newer and the offline version is dirty`() {\n        val offline = Entity.New(\"noah\", \"Noa\", 1, trunkVersion = null)\n        entitiesRepository.save(\"songs\", offline)\n        val csv = createEntityList(Entity.New(\"noah\", \"Noah\", 2))\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(1))\n        assertThat(songs[0].label, equalTo(\"Noah\"))\n        assertThat(songs[0].version, equalTo(2))\n        assertThat(songs[0].state, equalTo(Entity.State.ONLINE))\n        assertThat(songs[0].trunkVersion, equalTo(2))\n        assertThat(songs[0].branchId, not(blankOrNullString()))\n        assertThat(songs[0].branchId, not(equalTo(offline.branchId)))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer updates state if the online version is older`() {\n        val offline = Entity.New(\"noah\", \"Noah\", 2)\n        entitiesRepository.save(\"songs\", offline)\n        val csv = createEntityList(Entity.New(\"noah\", \"Noa\", 1))\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(1))\n        assertThat(songs[0].label, equalTo(\"Noah\"))\n        assertThat(songs[0].version, equalTo(2))\n        assertThat(songs[0].state, equalTo(Entity.State.ONLINE))\n        assertThat(songs[0].trunkVersion, equalTo(null))\n        assertThat(songs[0].branchId, equalTo(offline.branchId))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer does not write to repository if online and local are exactly the same`() {\n        val entitiesRepository = MeasurableEntitiesRepository(entitiesRepository)\n\n        val local = Entity.New(\"noah\", \"Noah\", 2, properties = listOf(\"length\" to \"4:33\"))\n        val csv1 = createEntityList(local)\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv1,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        assertThat(entitiesRepository.savedEntities, equalTo(1))\n\n        val csv2 = createEntityList(local, Entity.New(\"perception\", \"Perception\"))\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv2,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        assertThat(entitiesRepository.savedEntities, equalTo(2))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer updates trunkVersion, branchId and state if the online version catches up to an offline branch`() {\n        val offline = Entity.New(\"noah\", \"Noah\", 2)\n        entitiesRepository.save(\"songs\", offline)\n\n        val csv1 = createEntityList(Entity.New(\"noah\", \"Noah\", 2))\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv1,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n\n        val onlineBranched = Entity.New(\"noah\", \"Noah\", 3)\n        entitiesRepository.save(\"songs\", onlineBranched)\n        val csv2 = createEntityList(Entity.New(\"noah\", \"Noah\", 3))\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv2,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(1))\n        assertThat(songs[0].version, equalTo(3))\n        assertThat(songs[0].state, equalTo(Entity.State.ONLINE))\n        assertThat(songs[0].trunkVersion, equalTo(3))\n        assertThat(songs[0].branchId, not(blankOrNullString()))\n        assertThat(songs[0].branchId, not(equalTo(onlineBranched.branchId)))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer overrides offline version if the online version is the same`() {\n        val offline = Entity.New(\"noah\", \"Noah\", 2)\n        entitiesRepository.save(\"songs\", offline)\n        val csv = createEntityList(\n            Entity.New(\n                \"noah\",\n                \"Noa\",\n                2,\n                properties = listOf(\"length\" to \"4:33\")\n            )\n        )\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(1))\n        assertThat(songs[0].label, equalTo(\"Noa\"))\n        assertThat(songs[0].properties, containsInAnyOrder(\"length\" to \"4:33\"))\n        assertThat(songs[0].version, equalTo(2))\n        assertThat(songs[0].state, equalTo(Entity.State.ONLINE))\n        assertThat(songs[0].trunkVersion, equalTo(2))\n        assertThat(songs[0].branchId, not(blankOrNullString()))\n        assertThat(songs[0].branchId, not(equalTo(offline.branchId)))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer ignores properties not in offline version from older online version`() {\n        entitiesRepository.save(\"songs\", Entity.New(\"noah\", \"Noah\", 3))\n        val csv =\n            createEntityList(Entity.New(\"noah\", \"Noah\", 2, listOf(Pair(\"length\", \"6:38\"))))\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(1))\n        assertThat(songs[0].properties, equalTo(emptyList()))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer overrides properties in offline version from newer list version`() {\n        entitiesRepository.save(\n            \"songs\",\n            Entity.New(\"noah\", \"Noah\", 1, listOf(Pair(\"length\", \"6:38\")))\n        )\n        val csv =\n            createEntityList(Entity.New(\"noah\", \"Noah\", 2, listOf(Pair(\"length\", \"4:58\"))))\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(1))\n        assertThat(songs[0].version, equalTo(2))\n        assertThat(songs[0].properties, equalTo(listOf(Pair(\"length\", \"4:58\"))))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer does nothing if version does not exist in online entities`() {\n        val csv =\n            createCsv(\n                listOf(\"name\", \"label\"),\n                listOf(\"grisaille\", \"Grisaille\")\n            )\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        assertThat(entitiesRepository.getLists().size, equalTo(0))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer does nothing if name does not exist in online entities`() {\n        val csv =\n            createCsv(\n                listOf(\"label\", \"__version\"),\n                listOf(\"Grisaille\", \"2\")\n            )\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        assertThat(entitiesRepository.getLists().size, equalTo(0))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer does nothing if label does not exist in online entities`() {\n        val csv =\n            createCsv(\n                listOf(\"name\", \"__version\"),\n                listOf(\"grisaille\", \"2\")\n            )\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        assertThat(entitiesRepository.getLists().size, equalTo(0))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer adds online entity when its label is blank`() {\n        val csv = createEntityList(Entity.New(\"cathedrals\", label = \"\"))\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(1))\n        assertThat(songs[0].label, equalTo(\"\"))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer does nothing if passed a non-CSV file`() {\n        val file = TempFiles.createTempFile(\".xml\")\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            file,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        assertThat(entitiesRepository.getLists().size, equalTo(0))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer does not remove offline entities that are not in online entities`() {\n        entitiesRepository.save(\"songs\", Entity.New(\"noah\", \"Noah\"))\n        val csv = createEntityList(Entity.New(\"cathedrals\", \"Cathedrals\"))\n\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(2))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer does not check for deletions with the entity source if it does not need to`() {\n        val csv = createEntityList()\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile(integrityUrl = entitySource.integrityUrl)\n        )\n\n        assertThat(entitySource.accesses, equalTo(0))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer removes offline entity that was in online list, but isn't any longer`() {\n        entitiesRepository.save(\"songs\", Entity.New(\"cathedrals\", \"Cathedrals\"))\n\n        val firstCsv = createEntityList(Entity.New(\"cathedrals\", \"Cathedrals\"))\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            firstCsv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n\n        val secondCsv = createEntityList(Entity.New(\"noah\", \"Noah\"))\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            secondCsv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(1))\n        assertThat(songs[0].id, equalTo(\"noah\"))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer removes offline entity that was updated in online list, but isn't any longer`() {\n        entitiesRepository.save(\"songs\", Entity.New(\"cathedrals\", \"Cathedrals\", version = 1))\n\n        val firstCsv =\n            createEntityList(Entity.New(\"cathedrals\", \"Cathedrals (A Song)\", version = 2))\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            firstCsv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n\n        val secondCsv = createEntityList()\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            secondCsv,\n            entitiesRepository,\n            FormFixtures.mediaFile()\n        )\n\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun `#updateLocalEntitiesFromServer updates the list hash`() {\n        val csv = createEntityList(Entity.New(\"cathedrals\", \"Cathedrals\"))\n        LocalEntityUseCases.updateLocalEntitiesFromServer(\n            \"songs\",\n            csv,\n            entitiesRepository,\n            FormFixtures.mediaFile(hash = \"hash\")\n        )\n\n        val hash = entitiesRepository.getList(\"songs\")?.hash\n        assertThat(hash, equalTo(\"hash\"))\n    }\n\n    private fun createEntityList(vararg entities: Entity): File {\n        if (entities.isNotEmpty()) {\n            val header = listOf(\n                EntitySchema.ID,\n                EntitySchema.LABEL,\n                EntitySchema.VERSION\n            ) + entities[0].properties.map { it.first }\n\n            val rows = entities.map { entity ->\n                listOf(\n                    entity.id,\n                    entity.label,\n                    entity.version.toString()\n                ) + entity.properties.map { it.second }\n            }.toTypedArray()\n\n            return createCsv(header, *rows)\n        } else {\n            val header = listOf(\n                EntitySchema.ID,\n                EntitySchema.LABEL,\n                EntitySchema.VERSION\n            )\n\n            return createCsv(header)\n        }\n    }\n\n    @Test\n    fun `#updateOfflineLocalEntitiesFromServer removes offline entities that are deleted according to the entity source`() {\n        entitiesRepository.save(\n            \"songs\",\n            Entity.New(\"cathedrals\", \"Cathedrals\", state = Entity.State.OFFLINE)\n        )\n        entitiesRepository.save(\"songs\", Entity.New(\"noah\", \"Noah\", state = Entity.State.ONLINE))\n        entitiesRepository.save(\n            \"songs\",\n            Entity.New(\"midnightCity\", \"Midnight City\", state = Entity.State.OFFLINE)\n        )\n\n        entitySource.delete(\"cathedrals\")\n\n        LocalEntityUseCases.cleanUpDeletedOfflineEntities(\n            \"songs\",\n            entitiesRepository,\n            entitySource,\n            FormFixtures.mediaFile(integrityUrl = entitySource.integrityUrl)\n        )\n\n        val songs = entitiesRepository.query(\"songs\")\n        assertThat(songs.size, equalTo(2))\n        assertThat(songs[0].label, equalTo(\"Noah\"))\n        assertThat(songs[1].label, equalTo(\"Midnight City\"))\n    }\n\n    private fun createCsv(header: List<String>, vararg rows: List<String?>): File {\n        val csv = TempFiles.createTempFile()\n        csv.writer().use { it ->\n            val csvPrinter = CSVPrinter(it, CSVFormat.DEFAULT)\n            csvPrinter.printRecord(header)\n\n            rows.forEach {\n                csvPrinter.printRecord(it)\n            }\n        }\n\n        return csv\n    }\n}\n\nprivate class MeasurableEntitiesRepository(private val wrapped: EntitiesRepository) :\n    EntitiesRepository {\n\n    var accesses: Int = 0\n        private set\n\n    var savedEntities: Int = 0\n        private set\n\n    override fun save(list: String, vararg entities: Entity) {\n        accesses += 1\n        savedEntities += entities.size\n        wrapped.save(list, *entities)\n    }\n\n    override fun getLists(): List<EntityList> {\n        accesses += 1\n        return wrapped.getLists()\n    }\n\n    override fun getCount(list: String): Int {\n        return wrapped.getCount(list)\n    }\n\n    override fun addList(list: String) {\n        accesses += 1\n        wrapped.addList(list)\n    }\n\n    override fun delete(list: String, id: String) {\n        accesses += 1\n        wrapped.delete(list, id)\n    }\n\n    override fun query(list: String, query: Query?): List<Entity.Saved> {\n        accesses += 1\n        return wrapped.query(list, query)\n    }\n\n    override fun getByIndex(list: String, index: Int): Entity.Saved? {\n        accesses += 1\n        return wrapped.getByIndex(list, index)\n    }\n\n    override fun updateList(list: String, hash: String, needsApproval: Boolean) {\n        accesses += 1\n        wrapped.updateList(list, hash, false)\n    }\n\n    override fun getList(list: String): EntityList? {\n        accesses += 1\n        return wrapped.getList(list)\n    }\n}\n\nprivate class FakeEntitySource : EntitySource {\n\n    val integrityUrl = \"http://example.com/${UUID.randomUUID()}\"\n    var accesses: Int = 0\n        private set\n\n    private val deleted = mutableListOf<String>()\n\n    override fun fetchDeletedStates(\n        integrityUrl: String,\n        ids: List<String>\n    ): List<Pair<String, Boolean>> {\n        accesses += 1\n\n        if (integrityUrl == this.integrityUrl) {\n            return ids.map {\n                Pair(it, deleted.contains(it))\n            }\n        } else {\n            throw IllegalArgumentException()\n        }\n    }\n\n    fun delete(id: String) {\n        deleted.add(id)\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/browser/EntityItemTest.kt",
    "content": "package org.odk.collect.entities.browser\n\nimport android.app.Application\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.assertIsNotDisplayed\nimport androidx.compose.ui.test.junit4.createComposeRule\nimport androidx.compose.ui.test.onNodeWithContentDescription\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.entities.storage.Entity\n\n@RunWith(AndroidJUnit4::class)\nclass EntityItemTest {\n\n    @get:Rule\n    val composeTestRule = createComposeRule()\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n\n    @Test\n    fun `sorts order of properties`() {\n        composeTestRule.setContent {\n            EntityItem(\n                entity = Entity.Saved(\n                    \"1\",\n                    \"S.D.O.S\",\n                    properties = listOf(Pair(\"name\", \"S.D.O.S\"), Pair(\"length\", \"2:50\")),\n                    index = 0\n                )\n            )\n        }\n\n        composeTestRule.onNodeWithText(\"length: 2:50\\nname: S.D.O.S\").assertIsDisplayed()\n    }\n\n    @Test\n    fun `shows offline pill when entity is offline`() {\n        composeTestRule.setContent {\n            EntityItem(\n                entity = Entity.Saved(\"1\", \"S.D.O.S\", index = 0, state = Entity.State.OFFLINE)\n            )\n        }\n\n        composeTestRule\n            .onNodeWithContentDescription(context.getString(org.odk.collect.strings.R.string.offline))\n            .assertIsDisplayed()\n    }\n\n    @Test\n    fun `does not show offline pill when entity is online`() {\n        composeTestRule.setContent {\n            EntityItem(\n                entity = Entity.Saved(\"1\", \"S.D.O.S\", index = 0, state = Entity.State.ONLINE)\n            )\n        }\n\n        composeTestRule\n            .onNodeWithContentDescription(context.getString(org.odk.collect.strings.R.string.offline))\n            .assertIsNotDisplayed()\n    }\n\n    @Test\n    fun `shows id and version`() {\n        composeTestRule.setContent {\n            EntityItem(\n                entity = Entity.Saved(\"1\", \"S.D.O.S\", version = 11, index = 0)\n            )\n        }\n\n        composeTestRule.onNodeWithText(\"1 (11)\").assertIsDisplayed()\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/javarosa/EntitiesTest.kt",
    "content": "package org.odk.collect.entities.javarosa\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.containsInAnyOrder\nimport org.hamcrest.Matchers.notNullValue\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.core.model.data.UncastData\nimport org.javarosa.core.model.instance.TreeElement\nimport org.javarosa.test.BindBuilderXFormsElement.bind\nimport org.javarosa.test.Scenario\nimport org.javarosa.test.XFormsElement.body\nimport org.javarosa.test.XFormsElement.group\nimport org.javarosa.test.XFormsElement.head\nimport org.javarosa.test.XFormsElement.html\nimport org.javarosa.test.XFormsElement.input\nimport org.javarosa.test.XFormsElement.item\nimport org.javarosa.test.XFormsElement.mainInstance\nimport org.javarosa.test.XFormsElement.model\nimport org.javarosa.test.XFormsElement.repeat\nimport org.javarosa.test.XFormsElement.select1\nimport org.javarosa.test.XFormsElement.setvalue\nimport org.javarosa.test.XFormsElement.t\nimport org.javarosa.test.XFormsElement.title\nimport org.javarosa.xform.parse.XFormParserFactory\nimport org.javarosa.xform.util.XFormUtils\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport org.odk.collect.entities.javarosa.finalization.EntitiesExtra\nimport org.odk.collect.entities.javarosa.finalization.EntityFormFinalizationProcessor\nimport org.odk.collect.entities.javarosa.finalization.FormEntity\nimport org.odk.collect.entities.javarosa.parse.EntityXFormParserFactory\nimport org.odk.collect.entities.javarosa.spec.EntityAction\nimport org.odk.collect.entities.javarosa.spec.EntityAction.CREATE\nimport org.odk.collect.entities.javarosa.support.EntityXFormsElement.entityIdBind\nimport org.odk.collect.entities.javarosa.support.EntityXFormsElement.entityIdSetValue\nimport org.odk.collect.entities.javarosa.support.EntityXFormsElement.entityLabelBind\nimport org.odk.collect.entities.javarosa.support.EntityXFormsElement.entityNode\nimport org.odk.collect.entities.javarosa.support.EntityXFormsElement.withSaveTo\n\nclass EntitiesTest {\n    private val entityXFormParserFactory = EntityXFormParserFactory(XFormParserFactory())\n\n    @Before\n    fun setup() {\n        XFormUtils.setXFormParserFactory(entityXFormParserFactory)\n    }\n\n    @After\n    fun teardown() {\n        XFormUtils.setXFormParserFactory(XFormParserFactory())\n    }\n\n    @Test\n    fun `filling form without create does not create any entities`() {\n        val scenario = Scenario.init(\n            \"Entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"entity-form\\\"\",\n                                t(\"name\"),\n                                t(\n                                    \"meta\",\n                                    t(\"entity dataset=\\\"people\\\"\")\n                                )\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\").withSaveTo(\"name\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.answer(\"/data/name\", \"Tom Wambsgans\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(0))\n    }\n\n    @Test\n    fun `filling form with create makes entity available`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\n                                    \"meta\",\n                                    t(\"entity dataset=\\\"people\\\" create=\\\"1\\\" id=\\\"\\\"\",\n                                        t(\"label\")\n                                    )\n                                )\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\").withSaveTo(\"name\"),\n                        entityIdBind(),\n                        entityIdSetValue(),\n                        entityLabelBind(\"/data/name\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.answer(\"/data/name\", \"Tom Wambsgans\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].dataset, equalTo(\"people\"))\n        assertThat(entities[0].id, equalTo(scenario.answerOf<StringData>(\"/data/meta/entity/@id\").value))\n        assertThat(entities[0].label, equalTo(\"Tom Wambsgans\"))\n        assertThat(entities[0].properties, equalTo(listOf(Pair(\"name\", \"Tom Wambsgans\"))))\n        assertThat(entities[0].action, equalTo(CREATE))\n    }\n\n    @Test\n    fun `filling form with create in multiple groups makes entities available`() {\n        val scenario = Scenario.init(\n            \"Create entities from multiple groups form\",\n            html(\n                listOf(\"entities\" to \"http://www.opendatakit.org/xforms/entities\"),\n                head(\n                    title(\"Create entities from multiple groups form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2025.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entities-from-multiple-groups-form\\\"\",\n                                t(\n                                    \"people\",\n                                    t(\"name\"),\n                                    t(\n                                        \"meta\",\n                                        t(\n                                            \"entity dataset=\\\"people\\\" create=\\\"1\\\" id=\\\"\\\"\",\n                                            t(\"label\")\n                                        )\n                                    )\n                                ),\n                                t(\n                                    \"cars\",\n                                    t(\"model\"),\n                                    t(\n                                        \"meta\",\n                                        t(\n                                            \"entity dataset=\\\"cars\\\" create=\\\"1\\\" id=\\\"\\\"\",\n                                            t(\"label\")\n                                        )\n                                    )\n                                )\n                            )\n                        ),\n                        bind(\"/data/people/name\").type(\"string\").withSaveTo(\"name\"),\n                        bind(\"/data/people/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/people/meta/entity/label\").type(\"string\").calculate(\"/data/people/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/people/meta/entity/@id\", \"uuid()\"),\n                        bind(\"/data/cars/model\").type(\"string\").withSaveTo(\"car_model\"),\n                        bind(\"/data/cars/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/cars/meta/entity/label\").type(\"string\").calculate(\"/data/cars/model\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/cars/meta/entity/@id\", \"uuid()\"),\n                    )\n                ),\n                body(\n                    group(\n                        \"/data/people\",\n                        input(\"/data/people/name\"),\n                    ),\n                    group(\n                        \"/data/cars\",\n                        input(\"/data/cars/model\"),\n                    )\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.answer(\"/data/people/name\", \"Tom Wambsgans\")\n        scenario.answer(\"/data/cars/model\", \"Range Rover\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(2))\n\n        assertThat(\n            entities,\n            containsInAnyOrder(\n                FormEntity(\n                    CREATE,\n                    \"people\",\n                    scenario.answerOf<StringData>(\"/data/people/meta/entity/@id\").value as String,\n                    \"Tom Wambsgans\",\n                    listOf(Pair(\"name\", \"Tom Wambsgans\"))\n                ),\n                FormEntity(\n                    CREATE,\n                    \"cars\",\n                    scenario.answerOf<StringData>(\"/data/cars/meta/entity/@id\").value as String,\n                    \"Range Rover\",\n                    listOf(Pair(\"car_model\", \"Range Rover\"))\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `filling form with update in multiple groups makes entities available`() {\n        val scenario = Scenario.init(\n            \"Update entities from multiple groups form\",\n            html(\n                listOf(\"entities\" to \"http://www.opendatakit.org/xforms/entities\"),\n                head(\n                    title(\"Update entities from multiple groups form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2025.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"update-entities-from-multiple-groups-form\\\"\",\n                                t(\n                                    \"people\",\n                                    t(\"name\"),\n                                    t(\n                                        \"meta\",\n                                        t(\n                                            \"entity dataset=\\\"people\\\" update=\\\"1\\\" id=\\\"123\\\" baseVersion=\\\"1\\\"\",\n                                            t(\"label\")\n                                        )\n                                    )\n                                ),\n                                t(\n                                    \"cars\",\n                                    t(\"model\"),\n                                    t(\n                                        \"meta\",\n                                        t(\n                                            \"entity dataset=\\\"cars\\\" update=\\\"1\\\" id=\\\"123\\\" baseVersion=\\\"1\\\"\",\n                                            t(\"label\")\n                                        )\n                                    )\n                                )\n                            )\n                        ),\n                        bind(\"/data/people/name\").type(\"string\").withSaveTo(\"name\"),\n                        bind(\"/data/people/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/people/meta/entity/label\").type(\"string\").calculate(\"/data/people/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/people/meta/entity/@id\", \"uuid()\"),\n                        bind(\"/data/cars/model\").type(\"string\").withSaveTo(\"car_model\"),\n                        bind(\"/data/cars/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/cars/meta/entity/label\").type(\"string\").calculate(\"/data/cars/model\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/cars/meta/entity/@id\", \"uuid()\"),\n                    )\n                ),\n                body(\n                    group(\n                        \"/data/people\",\n                        input(\"/data/people/name\"),\n                        setvalue(\"odk-new-repeat\", \"/data/people/meta/entity/@id\", \"uuid()\")\n                    ),\n                    group(\n                        \"/data/cars\",\n                        input(\"/data/cars/model\"),\n                        setvalue(\"odk-new-repeat\", \"/data/cars/meta/entity/@id\", \"uuid()\")\n                    )\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.answer(\"/data/people/name\", \"Tom Wambsgans\")\n        scenario.answer(\"/data/cars/model\", \"Range Rover\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(2))\n\n        assertThat(\n            entities,\n            containsInAnyOrder(\n                FormEntity(\n                    EntityAction.UPDATE,\n                    \"people\",\n                    scenario.answerOf<StringData>(\"/data/people/meta/entity/@id\").value as String,\n                    \"Tom Wambsgans\",\n                    listOf(Pair(\"name\", \"Tom Wambsgans\"))\n                ),\n                FormEntity(\n                    EntityAction.UPDATE,\n                    \"cars\",\n                    scenario.answerOf<StringData>(\"/data/cars/meta/entity/@id\").value as String,\n                    \"Range Rover\",\n                    listOf(Pair(\"car_model\", \"Range Rover\"))\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `filling form with create in repeats makes entities available`() {\n        val scenario = Scenario.init(\n            \"Create entities from repeats form\",\n            html(\n                listOf(\"entities\" to \"http://www.opendatakit.org/xforms/entities\"),\n                head(\n                    title(\"Create entities from repeats form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2025.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entities-from-repeats-form\\\"\",\n                                t(\n                                    \"people\",\n                                    t(\"name\"),\n                                    t(\n                                        \"meta\",\n                                        t(\n                                            \"entity dataset=\\\"people\\\" create=\\\"1\\\" id=\\\"\\\"\",\n                                            t(\"label\")\n                                        )\n                                    )\n                                )\n                            )\n                        ),\n                        bind(\"/data/people/name\").type(\"string\").withSaveTo(\"name\"),\n                        bind(\"/data/people/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/people/meta/entity/label\").type(\"string\").calculate(\"/data/people/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/people/meta/entity/@id\", \"uuid()\"),\n                    )\n                ),\n                body(\n                    repeat(\n                        \"/data/people\",\n                        input(\"/data/people/name\"),\n                        setvalue(\"odk-new-repeat\", \"/data/people/meta/entity/@id\", \"uuid()\")\n                    )\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.answer(\"/data/people[1]/name\", \"Tom Wambsgans\")\n        scenario.createNewRepeat(\"/data/people\")\n        scenario.answer(\"/data/people[2]/name\", \"Shiv Roy\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(2))\n\n        assertThat(\n            entities,\n            containsInAnyOrder(\n                FormEntity(\n                    CREATE,\n                    \"people\",\n                    scenario.answerOf<StringData>(\"/data/people[1]/meta/entity/@id\").value as String,\n                    \"Tom Wambsgans\",\n                    listOf(Pair(\"name\", \"Tom Wambsgans\"))\n                ),\n                FormEntity(\n                    CREATE,\n                    \"people\",\n                    scenario.answerOf<UncastData>(\"/data/people[2]/meta/entity/@id\").value as String,\n                    \"Shiv Roy\",\n                    listOf(Pair(\"name\", \"Shiv Roy\"))\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `filling form with update in repeats makes entities available`() {\n        val scenario = Scenario.init(\n            \"Update entities from repeats form\",\n            html(\n                listOf(\"entities\" to \"http://www.opendatakit.org/xforms/entities\"),\n                head(\n                    title(\"Update entities from repeats form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2025.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"update-entities-from-repeats-form\\\"\",\n                                t(\n                                    \"people\",\n                                    t(\"name\"),\n                                    t(\n                                        \"meta\",\n                                        t(\n                                            \"entity dataset=\\\"people\\\" update=\\\"1\\\" id=\\\"123\\\" baseVersion=\\\"1\\\"\",\n                                            t(\"label\")\n                                        )\n                                    )\n                                )\n                            )\n                        ),\n                        bind(\"/data/people/name\").type(\"string\").withSaveTo(\"name\"),\n                        bind(\"/data/people/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/people/meta/entity/label\").type(\"string\").calculate(\"/data/people/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/people/meta/entity/@id\", \"uuid()\"),\n                    )\n                ),\n                body(\n                    repeat(\n                        \"/data/people\",\n                        input(\"/data/people/name\"),\n                        setvalue(\"odk-new-repeat\", \"/data/people/meta/entity/@id\", \"uuid()\")\n                    )\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.answer(\"/data/people[1]/name\", \"Tom Wambsgans\")\n        scenario.createNewRepeat(\"/data/people\")\n        scenario.answer(\"/data/people[2]/name\", \"Shiv Roy\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(2))\n\n        assertThat(\n            entities,\n            containsInAnyOrder(\n                FormEntity(\n                    EntityAction.UPDATE,\n                    \"people\",\n                    scenario.answerOf<StringData>(\"/data/people[1]/meta/entity/@id\").value as String,\n                    \"Tom Wambsgans\",\n                    listOf(Pair(\"name\", \"Tom Wambsgans\"))\n                ),\n                FormEntity(\n                    EntityAction.UPDATE,\n                    \"people\",\n                    scenario.answerOf<UncastData>(\"/data/people[2]/meta/entity/@id\").value as String,\n                    \"Shiv Roy\",\n                    listOf(Pair(\"name\", \"Shiv Roy\"))\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `filling form with create in nested repeats makes entities available`() {\n        val scenario = Scenario.init(\n            \"Create entities from nested repeats form\",\n            html(\n                listOf(\"entities\" to \"http://www.opendatakit.org/xforms/entities\"),\n                head(\n                    title(\"Create entities from nested repeats form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2025.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entities-from-nested-repeats-form\\\"\",\n                                t(\n                                    \"people\",\n                                    t(\"name\"),\n                                    t(\n                                        \"cars\",\n                                        t(\"model\"),\n                                        t(\n                                            \"meta\",\n                                            t(\n                                                \"entity dataset=\\\"cars\\\" create=\\\"1\\\" id=\\\"\\\"\",\n                                                t(\"label\")\n                                            )\n                                        )\n                                    ),\n                                    t(\n                                        \"meta\",\n                                        t(\n                                            \"entity dataset=\\\"people\\\" create=\\\"1\\\" id=\\\"\\\"\",\n                                            t(\"label\")\n                                        )\n                                    )\n                                )\n                            )\n                        ),\n                        bind(\"/data/people/name\").type(\"string\").withSaveTo(\"name\"),\n                        bind(\"/data/people/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/people/meta/entity/label\").type(\"string\").calculate(\"/data/people/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/people/meta/entity/@id\", \"uuid()\"),\n                        bind(\"/data/people/cars/model\").type(\"string\").withSaveTo(\"car_model\"),\n                        bind(\"/data/people/cars/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/people/cars/meta/entity/label\").type(\"string\").calculate(\"/data/people/cars/model\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/people/cars/meta/entity/@id\", \"uuid()\"),\n                    )\n                ),\n                body(\n                    repeat(\n                        \"/data/people\",\n                        input(\"/data/people/name\"),\n                        setvalue(\"odk-new-repeat\", \"/data/people/meta/entity/@id\", \"uuid()\"),\n                        repeat(\n                            \"/data/people/cars\",\n                            input(\"/data/people/cars/model\"),\n                            setvalue(\"odk-new-repeat\", \"/data/people/cars/meta/entity/@id\", \"uuid()\")\n                        )\n                    )\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.answer(\"/data/people[1]/name\", \"Tom Wambsgans\")\n        scenario.answer(\"/data/people[1]/cars[1]/model\", \"Range Rover\")\n        scenario.createNewRepeat(\"/data/people\")\n        scenario.answer(\"/data/people[2]/name\", \"Shiv Roy\")\n        scenario.createNewRepeat(\"/data/people[2]/cars\")\n        scenario.answer(\"/data/people[2]/cars[1]/model\", \"Audi A8\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(4))\n\n        assertThat(\n            entities,\n            containsInAnyOrder(\n                FormEntity(\n                    CREATE,\n                    \"people\",\n                    scenario.answerOf<StringData>(\"/data/people[1]/meta/entity/@id\").value as String,\n                    \"Tom Wambsgans\",\n                    listOf(Pair(\"name\", \"Tom Wambsgans\"))\n                ),\n                FormEntity(\n                    CREATE,\n                    \"people\",\n                    scenario.answerOf<UncastData>(\"/data/people[2]/meta/entity/@id\").value as String,\n                    \"Shiv Roy\",\n                    listOf(Pair(\"name\", \"Shiv Roy\"))\n                ),\n                FormEntity(\n                    CREATE,\n                    \"cars\",\n                    scenario.answerOf<StringData>(\"/data/people[1]/cars[1]/meta/entity/@id\").value as String,\n                    \"Range Rover\",\n                    listOf(Pair(\"car_model\", \"Range Rover\"))\n                ),\n                FormEntity(\n                    CREATE,\n                    \"cars\",\n                    scenario.answerOf<UncastData>(\"/data/people[2]/cars[1]/meta/entity/@id\").value as String,\n                    \"Audi A8\",\n                    listOf(Pair(\"car_model\", \"Audi A8\"))\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `filling form with update in nested repeats makes entities available`() {\n        val scenario = Scenario.init(\n            \"Update entities from nested repeats form\",\n            html(\n                listOf(\"entities\" to \"http://www.opendatakit.org/xforms/entities\"),\n                head(\n                    title(\"Update entities from nested repeats form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2025.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"update-entities-from-nested-repeats-form\\\"\",\n                                t(\n                                    \"people\",\n                                    t(\"name\"),\n                                    t(\n                                        \"cars\",\n                                        t(\"model\"),\n                                        t(\n                                            \"meta\",\n                                            t(\n                                                \"entity dataset=\\\"cars\\\" update=\\\"1\\\" id=\\\"123\\\" baseVersion=\\\"1\\\"\",\n                                                t(\"label\")\n                                            )\n                                        )\n                                    ),\n                                    t(\n                                        \"meta\",\n                                        t(\n                                            \"entity dataset=\\\"people\\\" update=\\\"1\\\" id=\\\"123\\\" baseVersion=\\\"1\\\"\",\n                                            t(\"label\")\n                                        )\n                                    )\n                                )\n                            )\n                        ),\n                        bind(\"/data/people/name\").type(\"string\").withSaveTo(\"name\"),\n                        bind(\"/data/people/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/people/meta/entity/label\").type(\"string\").calculate(\"/data/people/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/people/meta/entity/@id\", \"uuid()\"),\n                        bind(\"/data/people/cars/model\").type(\"string\").withSaveTo(\"car_model\"),\n                        bind(\"/data/people/cars/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/people/cars/meta/entity/label\").type(\"string\").calculate(\"/data/people/cars/model\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/people/cars/meta/entity/@id\", \"uuid()\"),\n                    )\n                ),\n                body(\n                    repeat(\n                        \"/data/people\",\n                        input(\"/data/people/name\"),\n                        setvalue(\"odk-new-repeat\", \"/data/people/meta/entity/@id\", \"uuid()\"),\n                        repeat(\n                            \"/data/people/cars\",\n                            input(\"/data/people/cars/model\"),\n                            setvalue(\"odk-new-repeat\", \"/data/people/cars/meta/entity/@id\", \"uuid()\")\n                        )\n                    )\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.answer(\"/data/people[1]/name\", \"Tom Wambsgans\")\n        scenario.answer(\"/data/people[1]/cars[1]/model\", \"Range Rover\")\n        scenario.createNewRepeat(\"/data/people\")\n        scenario.answer(\"/data/people[2]/name\", \"Shiv Roy\")\n        scenario.createNewRepeat(\"/data/people[2]/cars\")\n        scenario.answer(\"/data/people[2]/cars[1]/model\", \"Audi A8\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(4))\n\n        assertThat(\n            entities,\n            containsInAnyOrder(\n                FormEntity(\n                    EntityAction.UPDATE,\n                    \"people\",\n                    scenario.answerOf<StringData>(\"/data/people[1]/meta/entity/@id\").value as String,\n                    \"Tom Wambsgans\",\n                    listOf(Pair(\"name\", \"Tom Wambsgans\"))\n                ),\n                FormEntity(\n                    EntityAction.UPDATE,\n                    \"people\",\n                    scenario.answerOf<UncastData>(\"/data/people[2]/meta/entity/@id\").value as String,\n                    \"Shiv Roy\",\n                    listOf(Pair(\"name\", \"Shiv Roy\"))\n                ),\n                FormEntity(\n                    EntityAction.UPDATE,\n                    \"cars\",\n                    scenario.answerOf<StringData>(\"/data/people[1]/cars[1]/meta/entity/@id\").value as String,\n                    \"Range Rover\",\n                    listOf(Pair(\"car_model\", \"Range Rover\"))\n                ),\n                FormEntity(\n                    EntityAction.UPDATE,\n                    \"cars\",\n                    scenario.answerOf<UncastData>(\"/data/people[2]/cars[1]/meta/entity/@id\").value as String,\n                    \"Audi A8\",\n                    listOf(Pair(\"car_model\", \"Audi A8\"))\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `filling form with create without an id makes invalid entity available`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"id\"),\n                                t(\"name\"),\n                                t(\n                                    \"meta\",\n                                    t(\"entity dataset=\\\"people\\\" create=\\\"1\\\" id=\\\"\\\"\",\n                                        t(\"label\")\n                                    )\n                                )\n                            )\n                        ),\n                        bind(\"/data/id\").type(\"string\"),\n                        bind(\"/data/meta/entity/@id\").type(\"string\").calculate(\"/data/id\"),\n                        bind(\"/data/meta/entity/label\").type(\"string\").calculate(\"/data/name\")\n                    )\n                ),\n                body(\n                    input(\"/data/id\"),\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n        scenario.finalizeInstance()\n\n        val entitiesExtra = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java)\n        val (entities, invalidEntities) = entitiesExtra\n        assertThat(entities.size, equalTo(0))\n        assertThat(invalidEntities.size, equalTo(1))\n        assertThat(invalidEntities[0].dataset, equalTo(\"people\"))\n        assertThat(invalidEntities[0].id, equalTo(null))\n    }\n\n    @Test\n    fun `filling form with blank label makes invalid entity available`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\"meta\", entityNode(\"people\", CREATE))\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\").withSaveTo(\"name\"),\n                        entityLabelBind(\"/data/name\"),\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n        scenario.answer(\"/data/name\", \" \")\n        scenario.finalizeInstance()\n\n        val entitiesExtra = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java)\n        val (entities, invalidEntities) = entitiesExtra\n        assertThat(entities.size, equalTo(0))\n        assertThat(invalidEntities.size, equalTo(1))\n        assertThat(invalidEntities[0].dataset, equalTo(\"people\"))\n        assertThat(invalidEntities[0].label, equalTo(\" \"))\n    }\n\n    @Test\n    fun `filling fom with non-UUID id makes invalid entity available`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\"meta\", entityNode(\"people\", CREATE))\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\").withSaveTo(\"name\"),\n                        entityLabelBind(\"/data/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/meta/entity/@id\", \"1\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n        scenario.answer(\"/data/name\", \"Dylan\")\n        scenario.finalizeInstance()\n\n        val entitiesExtra = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java)\n        val (entities, invalidEntities) = entitiesExtra\n        assertThat(entities.size, equalTo(0))\n        assertThat(invalidEntities.size, equalTo(1))\n        assertThat(invalidEntities[0].dataset, equalTo(\"people\"))\n        assertThat(invalidEntities[0].id, equalTo(\"1\"))\n    }\n\n    @Test\n    fun `filling form with update makes entity available`() {\n        val scenario = Scenario.init(\n            \"Update entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Update entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"update-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\n                                    \"meta\",\n                                    t(\"entity dataset=\\\"people\\\" update=\\\"1\\\" id=\\\"123\\\" baseVersion=\\\"1\\\"\",\n                                        t(\"label\")\n                                    )\n                                )\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\").withSaveTo(\"name\"),\n                        entityIdBind(),\n                        entityIdSetValue(),\n                        entityLabelBind(\"/data/name\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n        scenario.answer(\"/data/name\", \"Tom Wambsgans\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].dataset, equalTo(\"people\"))\n        assertThat(entities[0].id, notNullValue())\n        assertThat(entities[0].label, equalTo(\"Tom Wambsgans\"))\n        assertThat(entities[0].properties, equalTo(listOf(Pair(\"name\", \"Tom Wambsgans\"))))\n        assertThat(entities[0].action, equalTo(EntityAction.UPDATE))\n    }\n\n    @Test\n    fun `filling form with update without an id does not make entity available`() {\n        val scenario = Scenario.init(\n            \"Update entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Update entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"update-entity-form\\\"\",\n                                t(\"id\"),\n                                t(\n                                    \"meta\",\n                                    t(\"entity dataset=\\\"people\\\" update=\\\"1\\\" id=\\\"\\\" baseVersion=\\\"\\\"\")\n                                )\n                            )\n                        ),\n                        bind(\"/data/id\").type(\"string\"),\n                        bind(\"/data/meta/entity/@id\").type(\"string\").calculate(\"/data/id\").readonly()\n                    )\n                ),\n                body(\n                    input(\"/data/id\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(0))\n    }\n\n    @Test\n    fun `filling form with create and update does not make entity available`() {\n        val scenario = Scenario.init(\n            \"Upsert entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Upsert entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"upsert-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\n                                    \"meta\",\n                                    t(\"entity dataset=\\\"people\\\" create=\\\"1\\\" update=\\\"1\\\" id=\\\"123\\\" baseVersion=\\\"1\\\"\",\n                                        t(\"label\")\n                                    )\n                                )\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\").withSaveTo(\"name\"),\n                        bind(\"/data/meta/entity/label\").type(\"string\").calculate(\"/data/name\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n        scenario.answer(\"/data/name\", \"Tom Wambsgans\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(0))\n    }\n\n    @Test\n    fun `filling form with dynamic create expression conditionally creates entities`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\"join\"),\n                                t(\n                                    \"meta\",\n                                    entityNode(\"members\", CREATE)\n                                )\n                            )\n                        ),\n                        bind(\"/data/meta/entity/@create\").calculate(\"/data/join = 'yes'\"),\n                        bind(\"/data/name\").type(\"string\").withSaveTo(\"name\"),\n                        entityIdBind(),\n                        entityIdSetValue(),\n                        entityLabelBind(\"/data/name\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\"),\n                    select1(\"/data/join\", item(\"yes\", \"Yes\"), item(\"no\", \"No\"))\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.next()\n        scenario.answer(\"Roman Roy\")\n        scenario.next()\n        scenario.answer(scenario.choicesOf(\"/data/join\")[0])\n        scenario.finalizeInstance()\n\n        var entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(1))\n\n        scenario.newInstance()\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n        scenario.next()\n        scenario.answer(\"Roman Roy\")\n        scenario.next()\n        scenario.answer(scenario.choicesOf(\"/data/join\")[1])\n        scenario.finalizeInstance()\n\n        entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(0))\n    }\n\n    @Test\n    fun `entity form can be serialized`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\n                                    \"meta\",\n                                    entityNode(\"people\", CREATE)\n                                )\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\").withSaveTo(\"name\"),\n                        entityIdBind(),\n                        entityIdSetValue(),\n                        entityLabelBind(\"/data/name\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n        val deserializedScenario = scenario.serializeAndDeserializeForm()\n        deserializedScenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        deserializedScenario.next()\n        deserializedScenario.answer(\"Shiv Roy\")\n        deserializedScenario.finalizeInstance()\n\n        val entities = deserializedScenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].dataset, equalTo(\"people\"))\n        assertThat(entities[0].properties, equalTo(listOf(Pair(\"name\", \"Shiv Roy\"))))\n    }\n\n    @Test\n    fun `entities namespace works regardless of name`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"blah\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"blah:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\"meta\", entityNode(\"people\", CREATE))\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\").withAttribute(\"blah\", \"saveto\", \"name\"),\n                        entityIdBind(),\n                        entityIdSetValue(),\n                        entityLabelBind(\"/data/name\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.answer(\"/data/name\", \"Tom Wambsgans\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].properties, equalTo(listOf(Pair(\"name\", \"Tom Wambsgans\"))))\n    }\n\n    @Test\n    fun `filling form with select saveto and with create saves values correctly to entity`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"team\"),\n                                t(\"meta\", entityNode(\"people\", CREATE))\n                            )\n                        ),\n                        bind(\"/data/team\").type(\"string\").withSaveTo(\"team\"),\n                        entityIdBind(),\n                        entityIdSetValue(),\n                        entityLabelBind(\"/data/team\")\n                    )\n                ),\n                body(\n                    select1(\"/data/team\", item(\"kendall\", \"Kendall\"),\n                        item(\"logan\", \"Logan\")\n                    )\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n\n        scenario.next()\n        scenario.answer(scenario.choicesOf(\"/data/team\")[0])\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].properties, equalTo(listOf(Pair(\"team\", \"kendall\"))))\n    }\n\n    @Test\n    fun `when saveto question is not answered, entity property is empty string`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\"age\"),\n                                t(\"meta\", entityNode(\"people\", CREATE))\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\"),\n                        bind(\"/data/age\").withSaveTo(\"age\"),\n                        entityIdBind(),\n                        entityIdSetValue(),\n                        entityLabelBind(\"/data/name\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.formEntryController.addPostProcessor(EntityFormFinalizationProcessor())\n        scenario.answer(\"/data/name\", \"James\")\n        scenario.finalizeInstance()\n\n        val entities = scenario.formEntryController.model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].properties, equalTo(listOf(Pair(\"age\", \"\"))))\n    }\n\n    @Test\n    fun `saveto is removed from bind attributes for clients`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\n                                    \"meta\",\n                                    t(\"entity dataset=\\\"people\\\" create=\\\"1\\\"\")\n                                )\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\").withSaveTo(\"name\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.next()\n        val bindAttributes: List<TreeElement> = scenario.formEntryPromptAtIndex.bindAttributes\n        val containsSaveTo = bindAttributes.any { it.name == \"saveto\" }\n        assertThat(containsSaveTo, equalTo(false))\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.kt",
    "content": "package org.odk.collect.entities.javarosa\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.data.DateData\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.test.BindBuilderXFormsElement.bind\nimport org.javarosa.test.Scenario\nimport org.javarosa.test.XFormsElement.body\nimport org.javarosa.test.XFormsElement.group\nimport org.javarosa.test.XFormsElement.head\nimport org.javarosa.test.XFormsElement.html\nimport org.javarosa.test.XFormsElement.input\nimport org.javarosa.test.XFormsElement.mainInstance\nimport org.javarosa.test.XFormsElement.model\nimport org.javarosa.test.XFormsElement.setvalue\nimport org.javarosa.test.XFormsElement.t\nimport org.javarosa.test.XFormsElement.title\nimport org.javarosa.xform.parse.XFormParserFactory\nimport org.javarosa.xform.util.XFormUtils\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport org.odk.collect.entities.javarosa.finalization.EntitiesExtra\nimport org.odk.collect.entities.javarosa.finalization.EntityFormFinalizationProcessor\nimport org.odk.collect.entities.javarosa.finalization.FormEntity\nimport org.odk.collect.entities.javarosa.parse.EntityXFormParserFactory\nimport org.odk.collect.entities.javarosa.spec.EntityAction.CREATE\nimport org.odk.collect.entities.javarosa.support.EntityXFormsElement.entityNode\nimport org.odk.collect.entities.javarosa.support.EntityXFormsElement.withSaveTo\nimport java.sql.Date\n\nclass EntityFormFinalizationProcessorTest {\n\n    private val entityXFormParserFactory = EntityXFormParserFactory(XFormParserFactory())\n\n    @Before\n    fun setup() {\n        XFormUtils.setXFormParserFactory(entityXFormParserFactory)\n    }\n\n    @After\n    fun teardown() {\n        XFormUtils.setXFormParserFactory(XFormParserFactory())\n    }\n\n    @Test\n    fun `when form does not have entity element, adds no entities to extras`() {\n        val scenario = Scenario.init(\n            \"Normal form\",\n            html(\n                head(\n                    title(\"Normal form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"normal\\\"\",\n                                t(\"name\")\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        val processor = EntityFormFinalizationProcessor()\n        val model = scenario.formEntryController.model\n        processor.processForm(model)\n        assertThat(model.extras.get(EntitiesExtra::class.java), equalTo(null))\n    }\n\n    @Test\n    fun `when saveTo is not relevant, it is not included in entity`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\"age\"),\n                                t(\"meta\", entityNode(\"people\", CREATE))\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"string\"),\n                        bind(\"/data/age\").type(\"string\")\n                            .withSaveTo(\"age\")\n                            .relevant(\"false()\"),\n                        bind(\"/data/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/meta/entity/label\").type(\"string\").calculate(\"/data/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/meta/entity/@id\", \"uuid()\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        scenario.answer(\"/data/name\", \"Johannes\")\n\n        val processor = EntityFormFinalizationProcessor()\n        val model = scenario.formEntryController.model\n        processor.processForm(model)\n\n        val entities: List<FormEntity> =\n            model.extras.get(EntitiesExtra::class.java).entities\n\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].properties, equalTo(emptyList()))\n    }\n\n    @Test\n    fun `creates entity with values treated as opaque strings`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"birthday\"),\n                                t(\"meta\", entityNode(\"people\", CREATE))\n                            )\n                        ),\n                        bind(\"/data/birthday\").type(\"date\")\n                            .withSaveTo(\"birthday\"),\n                        bind(\"/data/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/meta/entity/label\").type(\"string\")\n                            .calculate(\"/data/birthday\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/meta/entity/@id\", \"uuid()\")\n                    )\n                ),\n                body(\n                    input(\"/data/birthday\")\n                )\n            )\n        )\n\n        val processor = EntityFormFinalizationProcessor()\n        val model = scenario.formEntryController.model\n\n        scenario.next()\n        scenario.formEntryController.answerQuestion(DateData(Date.valueOf(\"2024-11-15\")), true)\n\n        processor.processForm(model)\n\n        val entities = model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(1))\n        assertThat(\n            entities[0].properties[0],\n            equalTo(Pair(\"birthday\", \"2024-11-15\"))\n        )\n    }\n\n    @Test\n    fun `does not create entity with blank labels`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\"meta\", entityNode(\"people\", CREATE))\n                            )\n                        ),\n                        bind(\"/data/name\").type(\"date\")\n                            .withSaveTo(\"name\"),\n                        bind(\"/data/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/meta/entity/label\").type(\"string\")\n                            .calculate(\"/data/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/meta/entity/@id\", \"uuid()\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\")\n                )\n            )\n        )\n\n        val processor = EntityFormFinalizationProcessor()\n        val model = scenario.formEntryController.model\n        processor.processForm(model)\n\n        val entities = model.extras.get(EntitiesExtra::class.java).entities\n        assertThat(entities.size, equalTo(0))\n    }\n\n    @Test\n    fun `when saveTo is in not relevant group, it is not included in entity`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"name\"),\n                                t(\n                                    \"group\",\n                                    t(\"age\")\n                                ),\n                                t(\"meta\", entityNode(\"people\", CREATE))\n                            )\n                        ),\n                        bind(\"/data/name\"),\n                        bind(\"/data/group\").relevant(\"false()\"),\n                        bind(\"/data/group/age\").type(\"string\")\n                            .withSaveTo(\"age\"),\n                        bind(\"/data/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/meta/entity/label\").type(\"string\").calculate(\"/data/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/meta/entity/@id\", \"uuid()\")\n                    )\n                ),\n                body(\n                    input(\"/data/name\"),\n                    group(\n                        \"/data/group\",\n                        input(\"/data/group/age\")\n                    )\n                )\n            )\n        )\n\n        scenario.answer(\"/data/name\", \"Thomas\")\n\n        val processor = EntityFormFinalizationProcessor()\n        val model = scenario.formEntryController.model\n        processor.processForm(model)\n\n        val entities = model.extras.get(EntitiesExtra::class.java).entities\n\n        assertThat(entities.size, equalTo(1))\n        assertThat(entities[0].properties, equalTo(emptyList()))\n    }\n\n    @Test\n    fun `when saveTo is nested in an extra group, creates entity with values`() {\n        val scenario = Scenario.init(\n            \"Create entity form\",\n            html(\n                listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n                head(\n                    title(\"Create entity form\"),\n                    model(\n                        listOf(Pair(\"entities:entities-version\", \"2024.1.0\")),\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\n                                    \"group\",\n                                    t(\"name\")\n                                ),\n                                t(\"meta\", entityNode(\"people\", CREATE))\n                            )\n                        ),\n                        bind(\"/data/group\"),\n                        bind(\"/data/group/name\").type(\"string\")\n                            .withSaveTo(\"name\"),\n                        bind(\"/data/meta/entity/@id\").type(\"string\"),\n                        bind(\"/data/meta/entity/label\").type(\"string\")\n                            .calculate(\"/data/group/name\"),\n                        setvalue(\"odk-instance-first-load\", \"/data/meta/entity/@id\", \"uuid()\")\n                    )\n                ),\n                body(\n                    group(\n                        \"/data/group\",\n                        input(\"/data/group/name\")\n                    )\n                )\n            )\n        )\n\n        val processor = EntityFormFinalizationProcessor()\n        val model = scenario.formEntryController.model\n        scenario.next()\n        scenario.next()\n        scenario.formEntryController.answerQuestion(StringData(\"John\"), true)\n        processor.processForm(model)\n\n        val entities = model.extras.get(EntitiesExtra::class.java).entities\n\n        assertThat(entities.size, equalTo(1))\n        assertThat(\n            entities[0].properties[0],\n            equalTo(Pair(\"name\", \"John\"))\n        )\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParseProcessorTest.kt",
    "content": "package org.odk.collect.entities.javarosa\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.instanceOf\nimport org.hamcrest.CoreMatchers.notNullValue\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.test.BindBuilderXFormsElement.bind\nimport org.javarosa.test.XFormsElement.body\nimport org.javarosa.test.XFormsElement.head\nimport org.javarosa.test.XFormsElement.html\nimport org.javarosa.test.XFormsElement.input\nimport org.javarosa.test.XFormsElement.mainInstance\nimport org.javarosa.test.XFormsElement.model\nimport org.javarosa.test.XFormsElement.t\nimport org.javarosa.test.XFormsElement.title\nimport org.javarosa.xform.parse.XFormParser\nimport org.javarosa.xform.parse.XFormParser.MissingModelAttributeException\nimport org.junit.Assert.fail\nimport org.junit.Test\nimport org.odk.collect.entities.javarosa.parse.EntityFormExtra\nimport org.odk.collect.entities.javarosa.parse.EntityFormParseProcessor\nimport org.odk.collect.entities.javarosa.spec.UnrecognizedEntityVersionException\nimport org.odk.collect.entities.javarosa.support.EntityXFormsElement.withSaveTo\nimport java.io.ByteArrayInputStream\nimport java.io.InputStreamReader\n\nclass EntityFormParseProcessorTest {\n    @Test\n    fun `when version is missing parses without error`() {\n        val form = html(\n            head(\n                title(\"Non entity form\"),\n                model(\n                    mainInstance(\n                        t(\"data id=\\\"create-entity-form\\\"\",\n                            t(\"name\"),\n                            t(\"meta\")\n                        )\n                    ),\n                    bind(\"/data/name\").type(\"string\")\n                )\n            ),\n            body(\n                input(\"/data/name\")\n            )\n        )\n\n        val processor = EntityFormParseProcessor()\n        val parser = XFormParser(InputStreamReader(ByteArrayInputStream(form.asXml().toByteArray())))\n        parser.addProcessor(processor)\n        parser.parse(null)\n    }\n\n    @Test\n    fun `when version is missing and there is an entity element throws exception`() {\n        val form = html(\n            listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n            head(\n                title(\"Create entity form\"),\n                model(\n                    mainInstance(\n                        t(\"data id=\\\"create-entity-form\\\"\",\n                            t(\"name\"),\n                            t(\"meta\",\n                                t(\"entity dataset=\\\"people\\\"\")\n                            )\n                        )\n                    ),\n                    bind(\"/data/name\").type(\"string\").withSaveTo(\"name\")\n                )\n            ),\n            body(\n                input(\"/data/name\")\n            )\n        )\n\n        val processor = EntityFormParseProcessor()\n        val parser = XFormParser(InputStreamReader(ByteArrayInputStream(form.asXml().toByteArray())))\n        parser.addProcessor(processor)\n\n        try {\n            parser.parse(null)\n            fail(\"Expected exception!\")\n        } catch (e: Exception) {\n            assertThat(e, instanceOf(MissingModelAttributeException::class.java))\n            val ex = e as MissingModelAttributeException\n            assertThat(ex.namespace, equalTo(\"http://www.opendatakit.org/xforms/entities\"))\n            assertThat(ex.name, equalTo(\"entities-version\"))\n        }\n    }\n\n    @Test(expected = UnrecognizedEntityVersionException::class)\n    fun `when version is not recognized throws exception`() {\n        val form = html(\n            listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n            head(\n                title(\"Create entity form\"),\n                model(\n                    listOf(Pair(\"entities:entities-version\", \"somethingElse\")),\n                    mainInstance(\n                        t(\"data id=\\\"create-entity-form\\\"\",\n                            t(\"name\"),\n                            t(\"meta\",\n                                t(\"entity dataset=\\\"people\\\"\")\n                            )\n                        )\n                    ),\n                    bind(\"/data/name\").type(\"string\").withSaveTo(\"name\")\n                )\n            ),\n            body(\n                input(\"/data/name\")\n            )\n        )\n\n        val processor = EntityFormParseProcessor()\n        val parser = XFormParser(InputStreamReader(ByteArrayInputStream(form.asXml().toByteArray())))\n        parser.addProcessor(processor)\n        parser.parse(null)\n    }\n\n    @Test\n    fun `when version is new patch parses correctly`() {\n        val newPatchVersion = \"2022.1.12\"\n        val form = html(\n            listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n            head(\n                title(\"Create entity form\"),\n                model(\n                    listOf(Pair(\"entities:entities-version\", newPatchVersion)),\n                    mainInstance(\n                        t(\"data id=\\\"create-entity-form\\\"\",\n                            t(\"name\"),\n                            t(\"meta\",\n                                t(\"entity dataset=\\\"people\\\"\")\n                            )\n                        )\n                    ),\n                    bind(\"/data/name\").type(\"string\").withSaveTo(\"name\")\n                )\n            ),\n            body(\n                input(\"/data/name\")\n            )\n        )\n\n        val processor = EntityFormParseProcessor()\n        val parser = XFormParser(InputStreamReader(ByteArrayInputStream(form.asXml().toByteArray())))\n        parser.addProcessor(processor)\n\n        val formDef: FormDef = parser.parse(null)\n        assertThat(formDef, notNullValue())\n    }\n\n    @Test\n    fun `when version is new version with updates parses correctly`() {\n        val updateVersion = \"2023.1.0\"\n        val form = html(\n            listOf(Pair(\"entities\", \"http://www.opendatakit.org/xforms/entities\")),\n            head(\n                title(\"Create entity form\"),\n                model(\n                    listOf(Pair(\"entities:entities-version\", updateVersion)),\n                    mainInstance(\n                        t(\"data id=\\\"update-entity-form\\\"\",\n                            t(\"name\"),\n                            t(\"meta\",\n                                t(\"entity dataset=\\\"people\\\" update=\\\"1\\\" id=\\\"17\\\"\")\n                            )\n                        )\n                    ),\n                    bind(\"/data/name\").type(\"string\").withSaveTo(\"name\")\n                )\n            ),\n            body(\n                input(\"/data/name\")\n            )\n        )\n\n        val processor = EntityFormParseProcessor()\n        val parser = XFormParser(InputStreamReader(ByteArrayInputStream(form.asXml().toByteArray())))\n        parser.addProcessor(processor)\n\n        val formDef: FormDef = parser.parse(null)\n        assertThat(formDef, notNullValue())\n    }\n\n    @Test\n    fun `saveTos with incorrect namespace are ignored`() {\n        val form = html(\n            listOf(\n                Pair(\"correct\", \"http://www.opendatakit.org/xforms/entities\"),\n                Pair(\"incorrect\", \"blah\")\n            ),\n            head(\n                title(\"Create entity form\"),\n                model(\n                    listOf(Pair(\"correct:entities-version\", \"2024.1.0\")),\n                    mainInstance(\n                        t(\"data id=\\\"create-entity-form\\\"\",\n                            t(\"name\"),\n                            t(\"meta\",\n                                t(\"entity dataset=\\\"people\\\"\")\n                            )\n                        )\n                    ),\n                    bind(\"/data/name\").type(\"string\").withAttribute(\"incorrect\", \"saveto\", \"name\")\n                )\n            ),\n            body(\n                input(\"/data/name\")\n            )\n        )\n\n        val processor = EntityFormParseProcessor()\n        val parser = XFormParser(InputStreamReader(ByteArrayInputStream(form.asXml().toByteArray())))\n        parser.addProcessor(processor)\n\n        val formDef: FormDef = parser.parse(null)\n        assertThat(formDef.extras.get(EntityFormExtra::class.java).saveTos.isEmpty(), equalTo(true))\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParserTest.kt",
    "content": "package org.odk.collect.entities.javarosa\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.javarosa.core.model.data.IntegerData\nimport org.javarosa.core.model.instance.TreeElement\nimport org.junit.Test\nimport org.odk.collect.entities.javarosa.spec.EntityAction\nimport org.odk.collect.entities.javarosa.spec.EntityFormParser.parseAction\nimport org.odk.collect.entities.javarosa.spec.EntityFormParser.parseLabel\nimport org.odk.collect.entities.javarosa.spec.FormEntityElement.ATTRIBUTE_CREATE\nimport org.odk.collect.entities.javarosa.spec.FormEntityElement.ATTRIBUTE_UPDATE\nimport org.odk.collect.entities.javarosa.spec.FormEntityElement.ELEMENT_ENTITY\nimport org.odk.collect.entities.javarosa.spec.FormEntityElement.ELEMENT_LABEL\n\nclass EntityFormParserTest {\n    @Test\n    fun `parse action finds create with true string`() {\n        val entityElement = TreeElement(ELEMENT_ENTITY)\n        entityElement.setAttribute(null, ATTRIBUTE_CREATE, \"true\")\n\n        val action = parseAction(entityElement)\n        assertThat(action, equalTo(EntityAction.CREATE))\n    }\n\n    @Test\n    fun `parse action finds update with true string`() {\n        val entityElement = TreeElement(ELEMENT_ENTITY)\n        entityElement.setAttribute(null, ATTRIBUTE_UPDATE, \"true\")\n\n        val dataset = parseAction(entityElement)\n        assertThat(dataset, equalTo(EntityAction.UPDATE))\n    }\n\n    @Test\n    fun `parse label when label is an int, converts to string`() {\n        val labelElement = TreeElement(ELEMENT_LABEL)\n        labelElement.setAnswer(IntegerData(0))\n        val entityElement = TreeElement(ELEMENT_ENTITY)\n        entityElement.addChild(labelElement)\n\n        val label = parseLabel(entityElement)\n        assertThat(label, equalTo(\"0\"))\n    }\n\n    @Test\n    fun `parse label when label is null, returns an empty string`() {\n        val labelElement = TreeElement(ELEMENT_LABEL)\n        val entityElement = TreeElement(ELEMENT_ENTITY)\n        entityElement.addChild(labelElement)\n\n        val label = parseLabel(entityElement)\n        assertThat(label, equalTo(\"\"))\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/javarosa/LocalEntitiesInstanceProviderTest.kt",
    "content": "package org.odk.collect.entities.javarosa\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.odk.collect.entities.javarosa.intance.LocalEntitiesInstanceProvider\nimport org.odk.collect.entities.javarosa.parse.EntitySchema\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.entities.storage.InMemEntitiesRepository\n\nclass LocalEntitiesInstanceProviderTest {\n\n    private val entitiesRepository = InMemEntitiesRepository()\n\n    @Test\n    fun `includes properties in local entity elements`() {\n        val entity =\n            Entity.New(\n                \"1\",\n                \"Shiv Roy\",\n                properties = listOf(Pair(\"age\", \"35\"), Pair(\"born\", \"England\"))\n            )\n        entitiesRepository.save(\"people\", entity)\n\n        val parser = LocalEntitiesInstanceProvider { entitiesRepository }\n        val instance = parser.get(\"people\", \"people.csv\")\n        assertThat(instance.numChildren, equalTo(1))\n\n        val item = instance.getChildAt(0)!!\n        assertThat(item.numChildren, equalTo(7))\n        assertThat(item.getFirstChild(\"age\")?.value?.value, equalTo(\"35\"))\n        assertThat(item.getFirstChild(\"born\")?.value?.value, equalTo(\"England\"))\n    }\n\n    @Test\n    fun `includes version in local entity elements`() {\n        val entity =\n            Entity.New(\n                \"1\",\n                \"Shiv Roy\",\n                version = 1\n            )\n        entitiesRepository.save(\"people\", entity)\n\n        val parser = LocalEntitiesInstanceProvider { entitiesRepository }\n        val instance = parser.get(\"people\", \"people.csv\")\n        assertThat(instance.numChildren, equalTo(1))\n\n        val item = instance.getChildAt(0)!!\n        assertThat(item.numChildren, equalTo(5))\n        assertThat(item.getFirstChild(EntitySchema.VERSION)?.value?.value, equalTo(\"1\"))\n    }\n\n    @Test\n    fun `includes trunk version in local entity elements`() {\n        val entity =\n            Entity.New(\n                \"1\",\n                \"Shiv Roy\",\n                trunkVersion = 1\n            )\n        entitiesRepository.save(\"people\", entity)\n\n        val parser = LocalEntitiesInstanceProvider { entitiesRepository }\n        val instance = parser.get(\"people\", \"people.csv\")\n        assertThat(instance.numChildren, equalTo(1))\n\n        val item = instance.getChildAt(0)!!\n        assertThat(item.numChildren, equalTo(5))\n        assertThat(item.getFirstChild(EntitySchema.TRUNK_VERSION)?.value?.value, equalTo(\"1\"))\n    }\n\n    @Test\n    fun `includes branch id in local entity elements`() {\n        val entity =\n            Entity.New(\n                \"1\",\n                \"Shiv Roy\",\n                branchId = \"branch-1\"\n            )\n        entitiesRepository.save(\"people\", entity)\n\n        val parser = LocalEntitiesInstanceProvider { entitiesRepository }\n        val instance = parser.get(\"people\", \"people.csv\")\n        assertThat(instance.numChildren, equalTo(1))\n\n        val item = instance.getChildAt(0)!!\n        assertThat(item.numChildren, equalTo(5))\n        assertThat(\n            item.getFirstChild(EntitySchema.BRANCH_ID)?.value?.value,\n            equalTo(\"branch-1\")\n        )\n    }\n\n    @Test\n    fun `includes blank trunk version when it is null`() {\n        val entity =\n            Entity.New(\n                \"1\",\n                \"Shiv Roy\",\n                trunkVersion = null\n            )\n        entitiesRepository.save(\"people\", entity)\n\n        val parser = LocalEntitiesInstanceProvider { entitiesRepository }\n        val instance = parser.get(\"people\", \"people.csv\")\n        assertThat(instance.numChildren, equalTo(1))\n\n        val item = instance.getChildAt(0)!!\n        assertThat(item.getFirstChild(EntitySchema.TRUNK_VERSION)?.value, equalTo(null))\n    }\n\n    @Test\n    fun `partial parse returns the full first item and just item for others`() {\n        val entity = arrayOf(\n            Entity.New(\n                \"1\",\n                \"Shiv Roy\",\n                properties = listOf(Pair(\"age\", \"35\")),\n                version = 1\n            ),\n            Entity.New(\n                \"2\",\n                \"Kendall Roy\",\n                properties = listOf(Pair(\"age\", \"40\")),\n                version = 1\n            )\n        )\n        entitiesRepository.save(\"people\", *entity)\n\n        val parser = LocalEntitiesInstanceProvider { entitiesRepository }\n        val instance = parser.get(\"people\", \"people.csv\", true)\n        assertThat(instance.numChildren, equalTo(2))\n\n        val item1 = instance.getChildAt(0)!!\n        assertThat(item1.isPartial, equalTo(false))\n        assertThat(item1.numChildren, equalTo(6))\n        assertThat(item1.getFirstChild(\"name\")!!.value!!.value, equalTo(\"1\"))\n\n        val item2 = instance.getChildAt(1)!!\n        assertThat(item2.isPartial, equalTo(true))\n        assertThat(item2.numChildren, equalTo(0))\n    }\n\n    @Test\n    fun `uses entity index for multiplicity`() {\n        val entities = arrayOf(\n            Entity.New(\n                \"1\",\n                \"Shiv Roy\"\n            ),\n            Entity.New(\n                \"2\",\n                \"Kendall Roy\"\n            )\n        )\n\n        val repository = InMemEntitiesRepository()\n        repository.save(\"people\", *entities)\n\n        val parser = LocalEntitiesInstanceProvider { repository }\n        val instance = parser.get(\"people\", \"people.csv\", false)\n\n        val first = instance.getChildAt(0)!!\n        assertThat(first.getFirstChild(\"name\")!!.value!!.value, equalTo(\"1\"))\n        assertThat(first.multiplicity, equalTo(0))\n\n        val second = instance.getChildAt(1)!!\n        assertThat(second.getFirstChild(\"name\")!!.value!!.value, equalTo(\"2\"))\n        assertThat(second.multiplicity, equalTo(1))\n    }\n\n    @Test\n    fun `includes blank label version when it is null`() {\n        val entity =\n            Entity.New(\n                \"1\",\n                label = null\n            )\n        entitiesRepository.save(\"people\", entity)\n\n        val parser = LocalEntitiesInstanceProvider { entitiesRepository }\n        val instance = parser.get(\"people\", \"people.csv\")\n        assertThat(instance.numChildren, equalTo(1))\n\n        val item = instance.getChildAt(0)!!\n        assertThat(item.getFirstChild(EntitySchema.LABEL)?.value, equalTo(null))\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/javarosa/filter/LocalEntitiesFilterStrategyTest.kt",
    "content": "package org.odk.collect.entities.javarosa.filter\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.containsInAnyOrder\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.FormDef\nimport org.javarosa.core.model.condition.EvaluationContext\nimport org.javarosa.core.model.condition.FilterStrategy\nimport org.javarosa.core.model.data.IntegerData\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.core.model.instance.DataInstance\nimport org.javarosa.core.model.instance.TreeElement\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.form.api.FormEntryController\nimport org.javarosa.form.api.FormEntryModel\nimport org.javarosa.test.BindBuilderXFormsElement.bind\nimport org.javarosa.test.Scenario\nimport org.javarosa.test.XFormsElement.body\nimport org.javarosa.test.XFormsElement.head\nimport org.javarosa.test.XFormsElement.html\nimport org.javarosa.test.XFormsElement.input\nimport org.javarosa.test.XFormsElement.instance\nimport org.javarosa.test.XFormsElement.mainInstance\nimport org.javarosa.test.XFormsElement.model\nimport org.javarosa.test.XFormsElement.repeat\nimport org.javarosa.test.XFormsElement.select1Dynamic\nimport org.javarosa.test.XFormsElement.t\nimport org.javarosa.test.XFormsElement.title\nimport org.javarosa.xform.parse.ExternalInstanceParser\nimport org.javarosa.xform.parse.ExternalInstanceParser.InstanceProvider\nimport org.javarosa.xform.util.XFormUtils\nimport org.javarosa.xpath.expr.XPathExpression\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport org.odk.collect.entities.javarosa.intance.LocalEntitiesInstanceProvider\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.entities.storage.InMemEntitiesRepository\nimport java.util.function.Supplier\n\nclass LocalEntitiesFilterStrategyTest {\n\n    private val entitiesRepository = InMemEntitiesRepository()\n    private val fallthroughFilterStrategy = FallthroughFilterStrategy()\n    private val instanceProvider =\n        SpyInstanceProvider(LocalEntitiesInstanceProvider(::entitiesRepository))\n\n    private val controllerSupplier: (FormDef) -> FormEntryController = { formDef ->\n        FormEntryController(FormEntryModel(formDef)).also {\n            it.addFilterStrategy(LocalEntitiesFilterStrategy(entitiesRepository))\n            it.addFilterStrategy(fallthroughFilterStrategy)\n        }\n    }\n\n    @Before\n    fun setup() {\n        XFormUtils.setExternalInstanceParserFactory {\n            ExternalInstanceParser().also {\n                it.addInstanceProvider(instanceProvider)\n            }\n        }\n    }\n\n    @After\n    fun teardown() {\n        XFormUtils.setExternalInstanceParserFactory { ExternalInstanceParser() }\n    }\n\n    @Test\n    fun `returns matching nodes in the optimized way when id = value`() {\n        entitiesRepository.save(\"things\", Entity.New(\"thing1\", \"Thing 1\"))\n        entitiesRepository.save(\"things\", Entity.New(\"thing2\", \"Thing 2\"))\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"string\")\n                            .calculate(\"instance('things')/root/item[name='thing1']/label\")\n                    )\n                ),\n                body(\n                    input(\"/data/calculate\")\n                )\n            ),\n            controllerSupplier\n        )\n\n        assertThat(scenario.answerOf<StringData>(\"/data/calculate\").value, equalTo(\"Thing 1\"))\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(false))\n    }\n\n    @Test\n    fun `returns matching nodes in the optimized way when id != value`() {\n        entitiesRepository.save(\"things\", Entity.New(\"thing1\", \"Thing 1\"))\n        entitiesRepository.save(\"things\", Entity.New(\"thing2\", \"Thing 2\"))\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"string\")\n                            .calculate(\"instance('things')/root/item[name!='thing1']/label\")\n                    )\n                ),\n                body(\n                    input(\"/data/calculate\")\n                )\n            ),\n            controllerSupplier\n        )\n\n        assertThat(scenario.answerOf<StringData>(\"/data/calculate\").value, equalTo(\"Thing 2\"))\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(false))\n    }\n\n    @Test\n    fun `replaces partial elements when entity matches name`() {\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\"thing\", \"Thing\"),\n            Entity.New(\"other\", \"Other\")\n        )\n\n        Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"string\")\n                            .calculate(\"instance('things')/root/item[name='thing']/label\")\n                    )\n                ),\n                body(\n                    input(\"/data/calculate\")\n                )\n            ),\n            controllerSupplier\n        )\n\n        assertThat(instanceProvider.fullParsePerformed, equalTo(false))\n    }\n\n    @Test\n    fun `returns empty nodeset in the optimized way when no entity matches name`() {\n        entitiesRepository.addList(\"things\")\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"string\")\n                            .calculate(\"instance('things')/root/item[name='other']/label\")\n                    )\n                ),\n                body(\n                    input(\"/data/calculate\")\n                )\n            ),\n            controllerSupplier\n        )\n\n        assertThat(scenario.answerOf<StringData>(\"/data/calculate\"), equalTo(null))\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(false))\n    }\n\n    @Test\n    fun `works correctly but not in the optimized way with non eq name expressions`() {\n        entitiesRepository.save(\"things\", Entity.New(\"thing\", \"Thing\"))\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"string\")\n                            .calculate(\"instance('things')/root/item[starts-with(name, 'thing')]/label\")\n                    )\n                ),\n                body(\n                    input(\"/data/calculate\")\n                )\n            ),\n            controllerSupplier\n        )\n\n        assertThat(scenario.answerOf<StringData>(\"/data/calculate\").value, equalTo(\"Thing\"))\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(true))\n    }\n\n    @Test\n    fun `does not effect name queries on non entity instances`() {\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        instance(\n                            \"secondary\",\n                            t(\"item\", t(\"label\", \"Thing\"), t(\"name\", \"thing\"))\n                        ),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"string\")\n                            .calculate(\"instance('secondary')/root/item[name='thing']/label\")\n                    )\n                ),\n                body(\n                    input(\"/data/calculate\")\n                )\n            ),\n            controllerSupplier\n        )\n\n        assertThat(scenario.answerOf<StringData>(\"/data/calculate\").value, equalTo(\"Thing\"))\n    }\n\n    @Test\n    fun `works correctly with filtering on a repeat`() {\n        val scenario = Scenario.init(\n            \"Count people underage\",\n            html(\n                head(\n                    title(\"Count people underage\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"count_people_underage\\\"\",\n                                t(\"people\",\n                                    t(\"name\"),\n                                    t(\"age\")\n                                ),\n                                t(\"total_underage\")\n                            )\n                        ),\n                        bind(\"/data/people/name\").type(\"string\"),\n                        bind(\"/data/people/age\").type(\"int\"),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/total_underage\").type(\"string\").calculate(\"count( /data/people [age&lt;18])\")\n                    )\n                ),\n                body(\n                    repeat(\"/data/people\",\n                        input(\"/data/people/name\"),\n                        input(\"/data/people/age\")\n                    ),\n                    input(\"/data/total_underage\")\n                )\n            ),\n            controllerSupplier\n        )\n\n        assertThat(scenario.answerOf<IntegerData>(\"/data/total_underage\").value, equalTo(0))\n    }\n\n    @Test\n    fun `works correctly in the optimized way with property = expressions`() {\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\n                \"thing1\",\n                \"Thing1\",\n                properties = listOf(\"property\" to \"value\")\n            ),\n            Entity.New(\n                \"thing2\",\n                \"Thing2\",\n                properties = listOf(\"property\" to \"value\")\n            ),\n            Entity.New(\n                \"other\",\n                \"Other\",\n                properties = listOf(\"property\" to \"other\")\n            )\n        )\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\")\n                    )\n                ),\n                body(\n                    select1Dynamic(\n                        \"/data/question\",\n                        \"instance('things')/root/item[property='value']\",\n                        \"name\",\n                        \"label\"\n                    )\n                )\n            ),\n            controllerSupplier\n        )\n\n        val choices = scenario.choicesOf(\"/data/question\").map { it.value }\n        assertThat(choices, containsInAnyOrder(\"thing1\", \"thing2\"))\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(false))\n    }\n\n    @Test\n    fun `works correctly in the optimized way with property = number`() {\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\n                \"thing1\",\n                \"Thing1\",\n                properties = listOf(\"age\" to \"25\")\n            )\n        )\n\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\n                \"thing2\",\n                \"Thing2\",\n                properties = listOf(\"age\" to \"30\")\n            )\n        )\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\")\n                    )\n                ),\n                body(\n                    select1Dynamic(\n                        \"/data/question\",\n                        \"instance('things')/root/item[age=25]\",\n                        \"name\",\n                        \"label\"\n                    )\n                )\n            ),\n            controllerSupplier\n        )\n\n        val choices = scenario.choicesOf(\"/data/question\").map { it.value }\n        assertThat(choices, containsInAnyOrder(\"thing1\"))\n        assertThat(instanceProvider.fullParsePerformed, equalTo(false))\n    }\n\n    @Test\n    fun `replaces partial elements when entity matches property`() {\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\n                \"thing1\",\n                \"Thing1\",\n                properties = listOf(\"property\" to \"value\")\n            )\n        )\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\")\n                    )\n                ),\n                body(\n                    select1Dynamic(\n                        \"/data/question\",\n                        \"instance('things')/root/item[property='value']\",\n                        \"name\",\n                        \"label\"\n                    )\n                )\n            ),\n            controllerSupplier\n        )\n\n        scenario.choicesOf(\"/data/question\") // Calculate choices\n        assertThat(instanceProvider.fullParsePerformed, equalTo(false))\n    }\n\n    @Test\n    fun `works correctly in the optimized way with label = expressions`() {\n        entitiesRepository.save(\"things\", Entity.New(\"thing1\", \"Thing1\"))\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\")\n                    )\n                ),\n                body(\n                    select1Dynamic(\n                        \"/data/question\",\n                        \"instance('things')/root/item[label='Thing1']\",\n                        \"name\",\n                        \"label\"\n                    )\n                )\n            ),\n            controllerSupplier\n        )\n\n        val choices = scenario.choicesOf(\"/data/question\").map { it.value }\n        assertThat(choices, containsInAnyOrder(\"thing1\"))\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(false))\n    }\n\n    @Test\n    fun `works correctly in the optimized way with version = expressions`() {\n        entitiesRepository.save(\"things\", Entity.New(\"thing1\", \"Thing1\", version = 2))\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\")\n                    )\n                ),\n                body(\n                    select1Dynamic(\n                        \"/data/question\",\n                        \"instance('things')/root/item[__version='2']\",\n                        \"name\",\n                        \"label\"\n                    )\n                )\n            ),\n            controllerSupplier\n        )\n\n        val choices = scenario.choicesOf(\"/data/question\").map { it.value }\n        assertThat(choices, containsInAnyOrder(\"thing1\"))\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(false))\n    }\n\n    @Test\n    fun `works correctly in the optimized way with combined eq expressions`() {\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\n                \"thing1\",\n                \"Thing1\",\n                version = 1,\n                properties = listOf(\"property\" to \"value1\")\n            ),\n            Entity.New(\n                \"thing2\",\n                \"Thing2\",\n                version = 2,\n                properties = listOf(\"property\" to \"value2\")\n            ),\n            Entity.New(\n                \"thing3\",\n                \"Thing3\",\n                version = 2,\n                properties = listOf(\"property\" to \"value3\")\n            ),\n        )\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\"),\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\")\n                    )\n                ),\n                body(\n                    select1Dynamic(\n                        \"/data/question\",\n                        \"instance('things')/root/item[label='Thing1' or (__version='2' and property='value3')]\",\n                        \"name\",\n                        \"label\"\n                    )\n                )\n            ),\n            controllerSupplier\n        )\n\n        val choices = scenario.choicesOf(\"/data/question\").map { it.value }\n        assertThat(choices, containsInAnyOrder(\"thing1\", \"thing3\"))\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(false))\n        assertThat(instanceProvider.fullParsePerformed, equalTo(false))\n    }\n\n    @Test\n    fun `works correctly but not in the optimized way with unanswered question = ''`() {\n        entitiesRepository.save(\"things\", Entity.New(\"thing1\", \"Thing1\"))\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"ref_question\"),\n                                t(\"question\")\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/ref_question\").type(\"string\"),\n                        bind(\"/data/question\").type(\"string\")\n                    )\n                ),\n                body(\n                    input(\"/data/ref_question\"),\n                    select1Dynamic(\n                        \"/data/question\",\n                        \"instance('things')/root/item[/data/ref_question='']\",\n                        \"name\",\n                        \"label\"\n                    )\n                )\n            ),\n            controllerSupplier\n        )\n\n        val choices = scenario.choicesOf(\"/data/question\").map { it.value }\n        assertThat(choices, containsInAnyOrder(\"thing1\"))\n\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(true))\n    }\n\n    @Test\n    fun `works correctly but not in the optimized way with answered question = value`() {\n        entitiesRepository.save(\"things\", Entity.New(\"thing1\", \"Thing1\"))\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"ref_question\"),\n                                t(\"question\")\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/ref_question\").type(\"string\"),\n                        bind(\"/data/question\").type(\"string\")\n                    )\n                ),\n                body(\n                    input(\"/data/ref_question\"),\n                    select1Dynamic(\n                        \"/data/question\",\n                        \"instance('things')/root/item[/data/ref_question='']\",\n                        \"name\",\n                        \"label\"\n                    )\n                )\n            ),\n            controllerSupplier\n        )\n        scenario.next()\n        scenario.answer(\"blah\")\n\n        val choices = scenario.choicesOf(\"/data/question\").map { it.value }\n        assertThat(choices.isEmpty(), equalTo(true))\n\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(true))\n    }\n\n    @Test\n    fun `works correctly but not in the optimized way with non existing property = ''`() {\n        entitiesRepository.save(\"things\", Entity.New(\"thing1\", \"Thing1\"))\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\")\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\")\n                    )\n                ),\n                body(\n                    select1Dynamic(\n                        \"/data/question\",\n                        \"instance('things')/root/item[not_existing_property='']\",\n                        \"name\",\n                        \"label\"\n                    )\n                )\n            ),\n            controllerSupplier\n        )\n\n        val choices = scenario.choicesOf(\"/data/question\").map { it.value }\n        assertThat(choices, containsInAnyOrder(\"thing1\"))\n\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(true))\n    }\n\n    @Test\n    fun `works correctly but not in the optimized way with non existing property = value`() {\n        entitiesRepository.save(\"things\", Entity.New(\"thing1\", \"Thing1\"))\n\n        val scenario = Scenario.init(\n            \"Secondary instance form\",\n            html(\n                head(\n                    title(\"Secondary instance form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"create-entity-form\\\"\",\n                                t(\"question\")\n                            )\n                        ),\n                        t(\"instance id=\\\"things\\\" src=\\\"jr://file-csv/things.csv\\\"\"),\n                        bind(\"/data/question\").type(\"string\")\n                    )\n                ),\n                body(\n                    select1Dynamic(\n                        \"/data/question\",\n                        \"instance('things')/root/item[not_existing_property='value']\",\n                        \"name\",\n                        \"label\"\n                    )\n                )\n            ),\n            controllerSupplier\n        )\n\n        val choices = scenario.choicesOf(\"/data/question\").map { it.value }\n        assertThat(choices.isEmpty(), equalTo(true))\n\n        assertThat(fallthroughFilterStrategy.fellThrough, equalTo(true))\n    }\n}\n\nprivate class FallthroughFilterStrategy : FilterStrategy {\n\n    var fellThrough = false\n        private set\n\n    override fun filter(\n        sourceInstance: DataInstance<*>,\n        nodeSet: TreeReference,\n        predicate: XPathExpression,\n        children: MutableList<TreeReference>,\n        evaluationContext: EvaluationContext,\n        next: Supplier<MutableList<TreeReference>>\n    ): MutableList<TreeReference> {\n        fellThrough = true\n        return next.get()\n    }\n}\n\nprivate class SpyInstanceProvider(private val wrapped: InstanceProvider) : InstanceProvider {\n    var fullParsePerformed = false\n        private set\n\n    override fun get(instanceId: String, instanceSrc: String): TreeElement {\n        return wrapped.get(instanceId, instanceSrc)\n    }\n\n    override fun get(instanceId: String, instanceSrc: String, partial: Boolean): TreeElement {\n        if (!partial) {\n            fullParsePerformed = true\n        }\n\n        return wrapped.get(instanceId, instanceSrc, partial)\n    }\n\n    override fun isSupported(instanceId: String, instanceSrc: String): Boolean {\n        return wrapped.isSupported(instanceId, instanceSrc)\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/javarosa/filter/PullDataFunctionHandlerTest.kt",
    "content": "package org.odk.collect.entities.javarosa.filter\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.form.api.FormEntryController\nimport org.javarosa.form.api.FormEntryModel\nimport org.javarosa.test.BindBuilderXFormsElement.bind\nimport org.javarosa.test.Scenario\nimport org.javarosa.test.XFormsElement.body\nimport org.javarosa.test.XFormsElement.head\nimport org.javarosa.test.XFormsElement.html\nimport org.javarosa.test.XFormsElement.input\nimport org.javarosa.test.XFormsElement.mainInstance\nimport org.javarosa.test.XFormsElement.model\nimport org.javarosa.test.XFormsElement.t\nimport org.javarosa.test.XFormsElement.title\nimport org.junit.Test\nimport org.odk.collect.entities.storage.Entity\nimport org.odk.collect.entities.storage.InMemEntitiesRepository\n\nclass PullDataFunctionHandlerTest {\n\n    @Test\n    fun `returns empty string when there are no matching results`() {\n        val entitiesRepository = InMemEntitiesRepository()\n        entitiesRepository.addList(\"things\")\n\n        val scenario = Scenario.init(\n            \"Pull data form\",\n            html(\n                head(\n                    title(\"Pull data form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"pull-data-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"string\")\n                            .calculate(\"pulldata('things', 'label', 'name', 'blah')\")\n                    )\n                ),\n                body(\n                    input(\"/data/question\"),\n                    input(\"/data/calculate\")\n                )\n            )\n        ) { formDef ->\n            FormEntryController(FormEntryModel(formDef)).also {\n                it.addFunctionHandler(PullDataFunctionHandler(entitiesRepository))\n            }\n        }\n\n        assertThat(scenario.answerOf<StringData>(\"/data/calculate\"), equalTo(null))\n    }\n\n    @Test\n    fun `returns first match when there are multiple`() {\n        val entitiesRepository = InMemEntitiesRepository()\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\"one\", \"One\", properties = listOf(Pair(\"property\", \"value\")))\n        )\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\"two\", \"Two\", properties = listOf(Pair(\"property\", \"value\")))\n        )\n\n        val scenario = Scenario.init(\n            \"Pull data form\",\n            html(\n                head(\n                    title(\"Pull data form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"pull-data-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"string\")\n                            .calculate(\"pulldata('things', 'label', 'property', 'value')\")\n                    )\n                ),\n                body(\n                    input(\"/data/question\"),\n                    input(\"/data/calculate\")\n                )\n            )\n        ) { formDef ->\n            FormEntryController(FormEntryModel(formDef)).also {\n                it.addFunctionHandler(PullDataFunctionHandler(entitiesRepository))\n            }\n        }\n\n        assertThat(scenario.answerOf<StringData>(\"/data/calculate\").value, equalTo(\"One\"))\n    }\n\n    @Test\n    fun `returns empty string when uses not existing property`() {\n        val entitiesRepository = InMemEntitiesRepository()\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\"one\", \"One\")\n        )\n\n        val scenario = Scenario.init(\n            \"Pull data form\",\n            html(\n                head(\n                    title(\"Pull data form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"pull-data-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"string\")\n                            .calculate(\"pulldata('things', 'label', 'property', 'value')\")\n                    )\n                ),\n                body(\n                    input(\"/data/question\"),\n                    input(\"/data/calculate\")\n                )\n            )\n        ) { formDef ->\n            FormEntryController(FormEntryModel(formDef)).also {\n                it.addFunctionHandler(PullDataFunctionHandler(entitiesRepository))\n            }\n        }\n\n        assertThat(scenario.answerOf<StringData>(\"/data/calculate\"), equalTo(null))\n    }\n\n    @Test\n    fun `returns empty string when uses not existing property with no value`() {\n        val entitiesRepository = InMemEntitiesRepository()\n        entitiesRepository.save(\n            \"things\",\n            Entity.New(\"one\", \"One\")\n        )\n\n        val scenario = Scenario.init(\n            \"Pull data form\",\n            html(\n                head(\n                    title(\"Pull data form\"),\n                    model(\n                        mainInstance(\n                            t(\n                                \"data id=\\\"pull-data-form\\\"\",\n                                t(\"question\"),\n                                t(\"calculate\")\n                            )\n                        ),\n                        bind(\"/data/question\").type(\"string\"),\n                        bind(\"/data/calculate\").type(\"string\")\n                            .calculate(\"pulldata('things', 'label', 'property', '')\")\n                    )\n                ),\n                body(\n                    input(\"/data/question\"),\n                    input(\"/data/calculate\")\n                )\n            )\n        ) { formDef ->\n            FormEntryController(FormEntryModel(formDef)).also {\n                it.addFunctionHandler(PullDataFunctionHandler(entitiesRepository))\n            }\n        }\n\n        assertThat(scenario.answerOf<StringData>(\"/data/calculate\"), equalTo(null))\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/javarosa/parse/StringExtTest.kt",
    "content": "package org.odk.collect.entities.javarosa.parse\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport java.util.UUID\n\nclass StringExtTest {\n\n    @Test\n    fun `#isV4UUID is false for version 1 UUIDs`() {\n        val v1UUID = \"6b5ea8de-9565-11e8-9eb6-529269fb1459\"\n        assertThat(UUID.fromString(v1UUID).version(), equalTo(1))\n        assertThat(v1UUID.isV4UUID(), equalTo(false))\n    }\n\n    @Test\n    fun `#isV4UUID is false for version 2 UUIDs`() {\n        val v2UUID = \"00000000-0000-2000-8000-000000000000\"\n        assertThat(UUID.fromString(v2UUID).version(), equalTo(2))\n        assertThat(v2UUID.isV4UUID(), equalTo(false))\n    }\n\n    @Test\n    fun `#isV4UUID is false for version 3 UUIDs`() {\n        val v3UUID = UUID.nameUUIDFromBytes(\"blah\".toByteArray()).toString()\n        assertThat(v3UUID.isV4UUID(), equalTo(false))\n    }\n\n    /**\n     * We just test up to version 4 here because Java only natively supports those versions (in\n     * [UUID]).\n     */\n    @Test\n    fun `#isV4UUID is true for version 4 UUIDs`() {\n        val v4UUID = UUID.randomUUID().toString()\n        assertThat(v4UUID.isV4UUID(), equalTo(true))\n    }\n\n    @Test\n    fun `#isV4UUID is false for invalid UUID string`() {\n        val invalid = \"not-a-uuid\"\n        assertThat(invalid.isV4UUID(), equalTo(false))\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/javarosa/parse/XPathExpressionExtTest.kt",
    "content": "package org.odk.collect.entities.javarosa.parse\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.core.model.condition.EvaluationContext\nimport org.javarosa.core.model.instance.ExternalDataInstance\nimport org.javarosa.xpath.XPathParseTool\nimport org.junit.Test\nimport org.odk.collect.entities.javarosa.parse.XPathExpressionExt.toQuery\nimport org.odk.collect.shared.Query\n\nclass XPathExpressionExtTest {\n\n    @Test\n    fun `#toQuery returns Query when node side is qualified relative expression`() {\n        val sourceInstance = ExternalDataInstance()\n        val evaluationContext = EvaluationContext(sourceInstance)\n\n        val expression = XPathParseTool.parseXPath(\"./label = 'blah'\")\n        assertThat(\n            expression.toQuery(sourceInstance, evaluationContext),\n            equalTo(Query.StringEq(\"label\", \"blah\"))\n        )\n    }\n\n    @Test\n    fun `#toQuery returns Query when node side is node() expression`() {\n        val sourceInstance = ExternalDataInstance()\n        val evaluationContext = EvaluationContext(sourceInstance)\n\n        val expression = XPathParseTool.parseXPath(\"node()/label = 'blah'\")\n        assertThat(\n            expression.toQuery(sourceInstance, evaluationContext),\n            equalTo(Query.StringEq(\"label\", \"blah\"))\n        )\n    }\n\n    @Test\n    fun `#toQuery returns null when node side is multiple levels`() {\n        val sourceInstance = ExternalDataInstance()\n        val evaluationContext = EvaluationContext(sourceInstance)\n\n        val expression = XPathParseTool.parseXPath(\"one/two = 'blah'\")\n        assertThat(\n            expression.toQuery(sourceInstance, evaluationContext),\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun `#toQuery returns null when node side is multiple levels and contains level called node`() {\n        val sourceInstance = ExternalDataInstance()\n        val evaluationContext = EvaluationContext(sourceInstance)\n\n        val expression = XPathParseTool.parseXPath(\"node/two = 'blah'\")\n        assertThat(\n            expression.toQuery(sourceInstance, evaluationContext),\n            equalTo(null)\n        )\n    }\n}\n"
  },
  {
    "path": "entities/src/test/java/org/odk/collect/entities/javarosa/support/EntityXFormsElement.kt",
    "content": "package org.odk.collect.entities.javarosa.support\n\nimport org.javarosa.test.BindBuilderXFormsElement\nimport org.javarosa.test.BindBuilderXFormsElement.bind\nimport org.javarosa.test.XFormsElement\nimport org.javarosa.test.XFormsElement.setvalue\nimport org.javarosa.test.XFormsElement.t\nimport org.odk.collect.entities.javarosa.spec.EntityAction\n\nobject EntityXFormsElement {\n    fun entityNode(\n        dataset: String,\n        action: EntityAction,\n        optionalAction: Boolean = false\n    ): XFormsElement {\n        val actionValue = if (optionalAction) {\n            \"\"\n        } else {\n            \"1\"\n        }\n\n        return when (action) {\n            EntityAction.CREATE -> t(\n                \"entity dataset=\\\"$dataset\\\" create=\\\"$actionValue\\\" id=\\\"\\\"\",\n                t(\"label\")\n            )\n\n            EntityAction.UPDATE -> t(\n                \"entity dataset=\\\"$dataset\\\" update=\\\"$actionValue\\\" id=\\\"\\\"\",\n                t(\"label\")\n            )\n        }\n    }\n\n    fun entityLabelBind(ref: String): BindBuilderXFormsElement {\n        return bind(\"/data/meta/entity/label\").type(\"string\").calculate(ref)\n    }\n\n    fun entityIdBind(): BindBuilderXFormsElement {\n        return bind(\"/data/meta/entity/@id\").type(\"string\")\n    }\n\n    fun entityIdSetValue(): XFormsElement {\n        return setvalue(\"odk-instance-first-load\", \"/data/meta/entity/@id\", \"uuid()\")\n    }\n\n    fun BindBuilderXFormsElement.withSaveTo(property: String): BindBuilderXFormsElement {\n        return withAttribute(\"entities\", \"saveto\", property)\n    }\n}\n"
  },
  {
    "path": "errors/.gitignore",
    "content": "/build"
  },
  {
    "path": "errors/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n\n    namespace = \"org.odk.collect.errors\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":strings\"))\n    implementation(project(\":androidshared\"))\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.androidxAppcompat)\n    implementation(libs.androidxRecyclerview)\n    implementation(libs.androidMaterial)\n\n    testImplementation(project(\":androidtest\"))\n    testImplementation(project(\":test-shared\"))\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.androidxTestEspressoCore)\n}\n"
  },
  {
    "path": "errors/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <application>\n        <activity\n            android:name=\".ErrorActivity\"\n            android:theme=\"@style/Theme.MaterialComponents.NoActionBar\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "errors/src/main/java/org/odk/collect/errors/ErrorActivity.kt",
    "content": "package org.odk.collect.errors\n\nimport android.app.NotificationManager\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.View\nimport androidx.appcompat.widget.Toolbar\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport org.odk.collect.strings.localization.getLocalizedString\n\nclass ErrorActivity : LocalizedActivity() {\n    companion object {\n        const val EXTRA_ERRORS = \"ERRORS\"\n        const val EXTRA_NOTIFICATION_ID = \"NOTIFICATION_ID\"\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setView(R.layout.activity_error, false)\n        title = getLocalizedString(org.odk.collect.strings.R.string.errors)\n        val toolbar = findViewById<View>(org.odk.collect.androidshared.R.id.toolbar) as Toolbar\n        setSupportActionBar(toolbar)\n        supportActionBar!!.setDisplayHomeAsUpEnabled(true)\n        findViewById<Toolbar>(org.odk.collect.androidshared.R.id.toolbar).setNavigationOnClickListener { finish() }\n\n        val failures = intent.getSerializableExtra(EXTRA_ERRORS) as? List<ErrorItem>\n        if (failures != null) {\n            findViewById<RecyclerView>(R.id.errors).apply {\n                adapter = ErrorAdapter(failures)\n                layoutManager = LinearLayoutManager(context)\n            }\n        } else {\n            finish()\n        }\n\n        val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)\n        if (notificationId != -1) {\n            val notificationManager =\n                getSystemService(NOTIFICATION_SERVICE) as NotificationManager\n            notificationManager.cancel(notificationId)\n        }\n    }\n}\n"
  },
  {
    "path": "errors/src/main/java/org/odk/collect/errors/ErrorAdapter.kt",
    "content": "package org.odk.collect.errors\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.recyclerview.widget.RecyclerView\nimport org.odk.collect.errors.databinding.ErrorItemBinding\n\nclass ErrorAdapter(private val errors: List<ErrorItem>) : RecyclerView.Adapter<ErrorAdapter.ViewHolder>() {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        val binding = ErrorItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        return ViewHolder(binding)\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n        with(holder) {\n            with(errors[position]) {\n                binding.title.text = this.title\n                binding.secondaryText.text = this.secondaryText\n                binding.supportingText.text = this.supportingText\n            }\n        }\n    }\n\n    override fun getItemCount() = errors.size\n\n    inner class ViewHolder(val binding: ErrorItemBinding) : RecyclerView.ViewHolder(binding.root)\n}\n"
  },
  {
    "path": "errors/src/main/java/org/odk/collect/errors/ErrorItem.kt",
    "content": "package org.odk.collect.errors\n\nimport java.io.Serializable\n\ndata class ErrorItem(\n    val title: String,\n    val secondaryText: String,\n    val supportingText: String\n) : Serializable\n"
  },
  {
    "path": "errors/src/main/res/layout/activity_error.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include layout=\"@layout/app_bar_layout\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/errors\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "errors/src/main/res/layout/error_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_margin=\"@dimen/margin_extra_small\"\n    app:cardElevation=\"2dp\"\n    xmlns:tool=\"http://schemas.android.com/tools\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:padding=\"@dimen/margin_standard\">\n\n        <TextView\n            android:id=\"@+id/title\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"?attr/textAppearanceSubtitle1\"\n            tool:text=\"Form 1\"/>\n\n        <TextView\n            android:id=\"@+id/secondary_text\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:textAppearance=\"?attr/textAppearanceSubtitle2\"\n            android:textColor=\"?android:attr/textColorSecondary\"\n            tool:text=\"ID: form-1 Version: 202122\"/>\n\n        <TextView\n            android:id=\"@+id/supporting_text\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_standard\"\n            android:textAppearance=\"?attr/textAppearanceBody2\"\n            android:textColor=\"?android:attr/textColorSecondary\"\n            tool:text=\"The form has invalid submission.\"/>\n    </LinearLayout>\n</com.google.android.material.card.MaterialCardView>\n"
  },
  {
    "path": "errors/src/test/java/org/odk/collect/errors/ErrorActivityTest.kt",
    "content": "package org.odk.collect.errors\n\nimport android.content.Intent\nimport androidx.lifecycle.Lifecycle\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport org.odk.collect.testshared.RecyclerViewMatcher.Companion.withListSize\nimport org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView\nimport java.io.Serializable\n\n@RunWith(AndroidJUnit4::class)\nclass ErrorActivityTest {\n\n    @get:Rule\n    val launcherRule = ActivityScenarioLauncherRule()\n\n    @Test\n    fun `list of errors should be displayed`() {\n        val errors = listOf(\n            ErrorItem(\"Title 1\", \"Secondary text 1\", \"Supporting text 1\"),\n            ErrorItem(\"Title 2\", \"Secondary text 2\", \"Supporting text 2\")\n        )\n\n        val intent =\n            Intent(ApplicationProvider.getApplicationContext(), ErrorActivity::class.java).apply {\n                putExtra(ErrorActivity.EXTRA_ERRORS, errors as Serializable)\n            }\n\n        launcherRule.launch<ErrorActivity>(intent)\n\n        onView(withId(R.id.errors)).check(matches(withListSize(2)))\n\n        onView(withRecyclerView(R.id.errors).atPositionOnView(0, R.id.title))\n            .check(matches(withText(\"Title 1\")))\n\n        onView(withRecyclerView(R.id.errors).atPositionOnView(0, R.id.secondary_text))\n            .check(matches(withText(\"Secondary text 1\")))\n\n        onView(withRecyclerView(R.id.errors).atPositionOnView(0, R.id.supporting_text))\n            .check(matches(withText(\"Supporting text 1\")))\n\n        onView(withRecyclerView(R.id.errors).atPositionOnView(1, R.id.title))\n            .check(matches(withText(\"Title 2\")))\n\n        onView(withRecyclerView(R.id.errors).atPositionOnView(1, R.id.secondary_text))\n            .check(matches(withText(\"Secondary text 2\")))\n\n        onView(withRecyclerView(R.id.errors).atPositionOnView(1, R.id.supporting_text))\n            .check(matches(withText(\"Supporting text 2\")))\n    }\n\n    @Test\n    fun `finishes when passed no errors`() {\n        val intent = Intent(ApplicationProvider.getApplicationContext(), ErrorActivity::class.java)\n\n        val scenario = launcherRule.launch<ErrorActivity>(intent)\n        assertThat(scenario.state, equalTo(Lifecycle.State.DESTROYED))\n    }\n}\n"
  },
  {
    "path": "errors/src/test/resources/robolectric.properties",
    "content": "sdk=33\n"
  },
  {
    "path": "external-app/.gitignore",
    "content": "/build"
  },
  {
    "path": "external-app/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n    namespace = \"org.odk.collect.externalapp\"\n}\n\ndependencies {\n    implementation(libs.androidxCoreKtx)\n\n    testImplementation(libs.junit)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.mockitoKotlin)\n}\n"
  },
  {
    "path": "external-app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "external-app/src/main/java/org/odk/collect/externalapp/ExternalAppUtils.kt",
    "content": "package org.odk.collect.externalapp\n\nimport android.app.Activity\nimport android.content.Intent\n\n/**\n * Helpers for returning answers to [FormFillingActivity] via [Activity.onActivityResult] and also\n * for dealing with those answers in [Activity.onActivityResult] itself.\n */\nobject ExternalAppUtils {\n\n    private const val VALUE_KEY = \"value\"\n\n    @JvmStatic\n    fun returnSingleValue(activity: Activity, value: String) {\n        activity.setResult(Activity.RESULT_OK, getReturnIntent(value))\n        activity.finish()\n    }\n\n    @JvmStatic\n    fun getReturnIntent(value: String): Intent {\n        return Intent().also {\n            it.putExtra(VALUE_KEY, value)\n        }\n    }\n\n    @JvmStatic\n    fun getReturnedSingleValue(data: Intent): Any? {\n        return data.extras?.get(VALUE_KEY)\n    }\n}\n"
  },
  {
    "path": "external-app/src/test/java/org/odk/collect/externalapp/ExternalAppUtilsTest.kt",
    "content": "package org.odk.collect.externalapp\n\nimport android.app.Activity\nimport android.content.Intent\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.argThat\nimport org.mockito.kotlin.eq\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\n\n@RunWith(AndroidJUnit4::class)\nclass ExternalAppUtilsTest {\n    @Test\n    fun `returnSingleValue should finish activity and pass expected value`() {\n        val activity = mock<Activity>()\n        val intent = Intent().also {\n            it.putExtra(\"value\", \"blah\")\n        }\n\n        ExternalAppUtils.returnSingleValue(activity, \"blah\")\n\n        verify(activity).setResult(\n            eq(Activity.RESULT_OK),\n            argThat {\n                intent.extras?.get(\"value\") == \"blah\"\n            }\n        )\n        verify(activity).finish()\n    }\n\n    @Test\n    fun `getReturnIntent should return correct intent with string value`() {\n        val intent = ExternalAppUtils.getReturnIntent(\"blah\")\n\n        assertThat(intent.extras?.get(\"value\"), `is`(\"blah\"))\n    }\n\n    @Test\n    fun `getReturnedSingleValue should return correct integer value`() {\n        val intent = Intent().apply {\n            putExtra(\"value\", 5)\n        }\n\n        assertThat(ExternalAppUtils.getReturnedSingleValue(intent), `is`(5))\n    }\n\n    @Test\n    fun `getReturnedSingleValue should return correct decimal value`() {\n        val intent = Intent().apply {\n            putExtra(\"value\", 5.5)\n        }\n\n        assertThat(ExternalAppUtils.getReturnedSingleValue(intent), `is`(5.5))\n    }\n\n    @Test\n    fun `getReturnedSingleValue should return correct string value`() {\n        val intent = Intent().apply {\n            putExtra(\"value\", \"blah\")\n        }\n\n        assertThat(ExternalAppUtils.getReturnedSingleValue(intent), `is`(\"blah\"))\n    }\n}\n"
  },
  {
    "path": "forms/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "forms/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.javaLibrary)\n    alias(libs.plugins.kotlinLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\njava {\n    sourceCompatibility = JavaVersion.VERSION_17\n    targetCompatibility = JavaVersion.VERSION_17\n}\n\ndependencies {\n    implementation(libs.kotlinStdlib)\n    testImplementation(libs.junit)\n    testImplementation(libs.hamcrest)\n}\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/Form.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.forms;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Objects;\n\n/**\n * A form definition stored on the device.\n * <p>\n * Objects of this class are created using the builder pattern: https://en.wikipedia.org/wiki/Builder_pattern\n */\npublic final class Form {\n\n    private final Long dbId;\n    private final String displayName;\n    private final String description;\n    private final String formId;\n    private final String version;\n    private final String formFilePath;\n    private final String submissionUri;\n    private final String base64RSAPublicKey;\n    private final String md5Hash;\n    private final Long date;\n    private final String jrCacheFilePath;\n    private final String formMediaPath;\n    private final String language;\n    private final String autoSend;\n    private final String autoDelete;\n    private final String geometryXPath;\n    private final boolean deleted;\n    private final Long lastDetectedAttachmentsUpdateDate;\n    private final boolean usesEntities;\n\n    private Form(Form.Builder builder) {\n        dbId = builder.dbId;\n        displayName = builder.displayName;\n        description = builder.description;\n        formId = builder.formId;\n        version = builder.version;\n        formFilePath = builder.formFilePath;\n        submissionUri = builder.submissionUri;\n        base64RSAPublicKey = builder.base64RSAPublicKey;\n        md5Hash = builder.md5Hash;\n        date = builder.date;\n        jrCacheFilePath = builder.jrCacheFilePath;\n        formMediaPath = builder.formMediaPath;\n        language = builder.language;\n        autoSend = builder.autoSend;\n        autoDelete = builder.autoDelete;\n        geometryXPath = builder.geometryXpath;\n        deleted = builder.deleted;\n        lastDetectedAttachmentsUpdateDate = builder.lastDetectedAttachmentsUpdateDate;\n        usesEntities = builder.usesEntities;\n    }\n\n    public static class Builder {\n        private Long dbId;\n        private String displayName;\n        private String description;\n        private String formId;\n        private String version;\n        private String formFilePath;\n        private String submissionUri;\n        private String base64RSAPublicKey;\n        private String md5Hash;\n        private Long date;\n        private String jrCacheFilePath;\n        private String formMediaPath;\n        private String language;\n        private String autoSend;\n        private String autoDelete;\n        private String geometryXpath;\n        private boolean deleted;\n        private Long lastDetectedAttachmentsUpdateDate;\n        private boolean usesEntities;\n\n        public Builder() {\n        }\n\n        public Builder(@NotNull Form form) {\n            dbId = form.dbId;\n            displayName = form.displayName;\n            description = form.description;\n            formId = form.formId;\n            version = form.version;\n            formFilePath = form.formFilePath;\n            submissionUri = form.submissionUri;\n            base64RSAPublicKey = form.base64RSAPublicKey;\n            md5Hash = form.md5Hash;\n            date = form.date;\n            jrCacheFilePath = form.jrCacheFilePath;\n            formMediaPath = form.formMediaPath;\n            language = form.language;\n            autoSend = form.autoSend;\n            autoDelete = form.autoDelete;\n            geometryXpath = form.geometryXPath;\n            deleted = form.deleted;\n            lastDetectedAttachmentsUpdateDate = form.lastDetectedAttachmentsUpdateDate;\n            usesEntities = form.usesEntities;\n        }\n\n        public Builder dbId(Long id) {\n            this.dbId = id;\n            return this;\n        }\n\n        public Builder displayName(String displayName) {\n            this.displayName = displayName;\n            return this;\n        }\n\n        public Builder description(String description) {\n            this.description = description;\n            return this;\n        }\n\n        public Builder formId(String formId) {\n            this.formId = formId;\n            return this;\n        }\n\n        public Builder version(String version) {\n            this.version = version;\n            return this;\n        }\n\n        public Builder formFilePath(String formFilePath) {\n            this.formFilePath = formFilePath;\n            return this;\n        }\n\n        public Builder submissionUri(String submissionUri) {\n            this.submissionUri = submissionUri;\n            return this;\n        }\n\n        public Builder base64RSAPublicKey(String base64RSAPublicKey) {\n            this.base64RSAPublicKey = base64RSAPublicKey;\n            return this;\n        }\n\n        public Builder md5Hash(String md5Hash) {\n            this.md5Hash = md5Hash;\n            return this;\n        }\n\n        public Builder date(Long date) {\n            this.date = date;\n            return this;\n        }\n\n        public Builder jrCacheFilePath(String jrCacheFilePath) {\n            this.jrCacheFilePath = jrCacheFilePath;\n            return this;\n        }\n\n        public Builder formMediaPath(String formMediaPath) {\n            this.formMediaPath = formMediaPath;\n            return this;\n        }\n\n        public Builder language(String language) {\n            this.language = language;\n            return this;\n        }\n\n        public Builder autoSend(String autoSend) {\n            this.autoSend = autoSend;\n            return this;\n        }\n\n        public Builder autoDelete(String autoDelete) {\n            this.autoDelete = autoDelete;\n            return this;\n        }\n\n        public Builder geometryXpath(String geometryXpath) {\n            this.geometryXpath = geometryXpath;\n            return this;\n        }\n\n        public Builder deleted(boolean deleted) {\n            this.deleted = deleted;\n            return this;\n        }\n\n        public Builder lastDetectedAttachmentsUpdateDate(Long lastDetectedAttachmentsUpdateDate) {\n            this.lastDetectedAttachmentsUpdateDate = lastDetectedAttachmentsUpdateDate;\n            return this;\n        }\n\n        public Builder usesEntities(boolean usesEntities) {\n            this.usesEntities = usesEntities;\n            return this;\n        }\n\n        public Form build() {\n            return new Form(this);\n        }\n    }\n\n    public Long getDbId() {\n        return dbId;\n    }\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public String getFormId() {\n        return formId;\n    }\n\n    @Nullable\n    public String getVersion() {\n        return version;\n    }\n\n    public String getFormFilePath() {\n        return formFilePath;\n    }\n\n    public String getSubmissionUri() {\n        return submissionUri;\n    }\n\n    @Nullable\n    public String getBASE64RSAPublicKey() {\n        return base64RSAPublicKey;\n    }\n\n    public String getMD5Hash() {\n        return md5Hash;\n    }\n\n    public Long getDate() {\n        return date;\n    }\n\n    public String getJrCacheFilePath() {\n        return jrCacheFilePath;\n    }\n\n    @Nullable\n    public String getFormMediaPath() {\n        return formMediaPath;\n    }\n\n    public String getLanguage() {\n        return language;\n    }\n\n    @Nullable\n    public String getAutoSend() {\n        return autoSend;\n    }\n\n    public String getAutoDelete() {\n        return autoDelete;\n    }\n\n    public String getGeometryXpath() {\n        return geometryXPath;\n    }\n\n    public boolean isDeleted() {\n        return deleted;\n    }\n\n    public Long getLastDetectedAttachmentsUpdateDate() {\n        return lastDetectedAttachmentsUpdateDate;\n    }\n\n    public boolean usesEntities() {\n        return usesEntities;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        \n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        Form form = (Form) o;\n        return deleted == form.deleted &&\n                Objects.equals(dbId, form.dbId) &&\n                Objects.equals(displayName, form.displayName) &&\n                Objects.equals(description, form.description) &&\n                Objects.equals(formId, form.formId) &&\n                Objects.equals(version, form.version) &&\n                Objects.equals(formFilePath, form.formFilePath) &&\n                Objects.equals(submissionUri, form.submissionUri) &&\n                Objects.equals(base64RSAPublicKey, form.base64RSAPublicKey) &&\n                Objects.equals(md5Hash, form.md5Hash) &&\n                Objects.equals(date, form.date) &&\n                Objects.equals(jrCacheFilePath, form.jrCacheFilePath) &&\n                Objects.equals(formMediaPath, form.formMediaPath) &&\n                Objects.equals(language, form.language) &&\n                Objects.equals(autoSend, form.autoSend) &&\n                Objects.equals(autoDelete, form.autoDelete) &&\n                Objects.equals(geometryXPath, form.geometryXPath) &&\n                Objects.equals(lastDetectedAttachmentsUpdateDate, form.lastDetectedAttachmentsUpdateDate) &&\n                Objects.equals(usesEntities, form.usesEntities);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(dbId, displayName, description, formId, version, formFilePath,\n                submissionUri, base64RSAPublicKey, md5Hash, date, jrCacheFilePath, formMediaPath,\n                language, autoSend, autoDelete, geometryXPath, deleted, lastDetectedAttachmentsUpdateDate,\n                usesEntities);\n    }\n\n    @Override\n    public String toString() {\n        return \"Form{\" +\n                \"formId='\" + formId + '\\'' +\n                \"version='\" + version + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/FormListItem.kt",
    "content": "package org.odk.collect.forms\n\ndata class FormListItem(\n    val downloadURL: String,\n    val formID: String,\n    val version: String?,\n    val hash: String?,\n    val name: String,\n    val manifestURL: String?\n)\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/FormSource.java",
    "content": "package org.odk.collect.forms;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.InputStream;\nimport java.util.List;\n\n/**\n * A place where forms live (outside the app's storage). Ideally in future this would be\n * a common interface for getting forms from a server, Google Drive or even the disk.\n */\npublic interface FormSource {\n\n    List<FormListItem> fetchFormList() throws FormSourceException;\n\n    ManifestFile fetchManifest(String manifestURL) throws FormSourceException;\n\n    @NotNull\n    InputStream fetchForm(String formURL) throws FormSourceException;\n\n    @NotNull\n    InputStream fetchMediaFile(String mediaFileURL) throws FormSourceException;\n}\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/FormSourceException.kt",
    "content": "package org.odk.collect.forms\n\nsealed class FormSourceException : Exception() {\n    class Unreachable(val serverUrl: String) : FormSourceException()\n    class AuthRequired : FormSourceException()\n    class FetchError : FormSourceException()\n    class SecurityError(val serverUrl: String) : FormSourceException()\n    class ServerError(val statusCode: Int, val serverUrl: String) : FormSourceException()\n    class ParseError(val serverUrl: String) : FormSourceException()\n\n    // Aggregate 0.9 and prior used a custom API before the OpenRosa standard was in place. Aggregate continued\n    // to provide this response to HTTP requests so some custom servers tried to implement it.\n    class ServerNotOpenRosaError : FormSourceException()\n}\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/FormsRepository.java",
    "content": "package org.odk.collect.forms;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\n\npublic interface FormsRepository {\n\n    @Nullable\n    Form get(Long id);\n\n    @Nullable\n    Form getLatestByFormIdAndVersion(String formId, @Nullable String version);\n\n    @Nullable\n    Form getOneByPath(String path);\n\n    @Nullable\n    Form getOneByMd5Hash(@NotNull String hash);\n\n    List<Form> getAll();\n\n    List<Form> getAllByFormIdAndVersion(String formId, @Nullable String version);\n\n    List<Form> getAllByFormId(String formId);\n\n    List<Form> getAllNotDeletedByFormId(String formId);\n\n    List<Form> getAllNotDeletedByFormIdAndVersion(String formId, @Nullable String version);\n\n    Form save(@NotNull Form form);\n\n    void delete(Long id);\n\n    void softDelete(Long id);\n\n    void deleteByMd5Hash(@NotNull String md5Hash);\n\n    void deleteAll();\n\n    void restore(Long id);\n}\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/ManifestFile.kt",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.forms\n\nimport java.io.Serializable\n\ndata class ManifestFile(\n    val hash: String?,\n    val mediaFiles: List<MediaFile>\n) : Serializable\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/MediaFile.kt",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.forms\n\nimport java.io.Serializable\n\ndata class MediaFile @JvmOverloads constructor(\n    val filename: String,\n    val hash: String,\n    val downloadUrl: String,\n    val type: Type? = null,\n    val integrityUrl: String? = null\n) : Serializable {\n\n    enum class Type {\n        ENTITY_LIST,\n        APPROVAL_ENTITY_LIST\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/instances/Instance.java",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.forms.instances;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Objects;\n\n/**\n * A filled form stored on the device.\n * <p>\n * Objects of this class are created using the builder pattern: https://en.wikipedia.org/wiki/Builder_pattern\n */\npublic final class Instance {\n    // status for instances\n    public static final String STATUS_INCOMPLETE = \"incomplete\";\n    public static final String STATUS_INVALID = \"invalid\";\n    public static final String STATUS_VALID = \"valid\";\n    public static final String STATUS_NEW_EDIT = \"newEdit\";\n    public static final String STATUS_COMPLETE = \"complete\";\n    public static final String STATUS_SUBMITTED = \"submitted\";\n    public static final String STATUS_SUBMISSION_FAILED = \"submissionFailed\";\n\n    public static final String GEOMETRY_TYPE_POINT = \"Point\";\n\n    private final String displayName;\n    private final String submissionUri;\n    private final boolean canEditWhenComplete;\n    private final String instanceFilePath;\n    private final String formId;\n    private final String formVersion;\n    private final String status;\n    private final Long lastStatusChangeDate;\n    private final Long finalizationDate;\n    private final Long deletedDate;\n    private final String geometryType;\n    private final String geometry;\n    private final Long editOf;\n    private final Long editNumber;\n\n    private final Long dbId;\n    private final boolean canDeleteBeforeSend;\n\n    private Instance(Builder builder) {\n        displayName = builder.displayName;\n        submissionUri = builder.submissionUri;\n        canEditWhenComplete = builder.canEditWhenComplete;\n        instanceFilePath = builder.instanceFilePath;\n        formId = builder.formId;\n        formVersion = builder.formVersion;\n        status = builder.status;\n        lastStatusChangeDate = builder.lastStatusChangeDate;\n        finalizationDate = builder.finalizationDate;\n        deletedDate = builder.deletedDate;\n        geometryType = builder.geometryType;\n        geometry = builder.geometry;\n        editOf = builder.editOf;\n        editNumber = builder.editNumber;\n        canDeleteBeforeSend = builder.canDeleteBeforeSend;\n\n        dbId = builder.dbId;\n    }\n\n    public static class Builder {\n        private String displayName;\n        private String submissionUri;\n        private boolean canEditWhenComplete = true;\n        private String instanceFilePath;\n        private String formId;\n        private String formVersion;\n        private String status;\n        private Long lastStatusChangeDate;\n        private Long finalizationDate;\n        private Long deletedDate;\n        private String geometryType;\n        private String geometry;\n        private Long editOf;\n        private Long editNumber;\n\n        private Long dbId;\n        private boolean canDeleteBeforeSend = true;\n\n        public Builder() {\n\n        }\n\n        public Builder(Instance instance) {\n            dbId = instance.dbId;\n            displayName = instance.displayName;\n            submissionUri = instance.submissionUri;\n            canEditWhenComplete = instance.canEditWhenComplete;\n            instanceFilePath = instance.instanceFilePath;\n            formId = instance.formId;\n            formVersion = instance.formVersion;\n            status = instance.status;\n            lastStatusChangeDate = instance.lastStatusChangeDate;\n            finalizationDate = instance.finalizationDate;\n            deletedDate = instance.deletedDate;\n            geometryType = instance.geometryType;\n            geometry = instance.geometry;\n            editOf = instance.editOf;\n            editNumber = instance.editNumber;\n            canDeleteBeforeSend = instance.canDeleteBeforeSend;\n        }\n\n        public Builder displayName(String displayName) {\n            this.displayName = displayName;\n            return this;\n        }\n\n        public Builder submissionUri(String submissionUri) {\n            this.submissionUri = submissionUri;\n            return this;\n        }\n\n        public Builder canEditWhenComplete(boolean canEditWhenComplete) {\n            this.canEditWhenComplete = canEditWhenComplete;\n            return this;\n        }\n\n        public Builder instanceFilePath(String instanceFilePath) {\n            this.instanceFilePath = instanceFilePath;\n            return this;\n        }\n\n        public Builder formId(String formId) {\n            this.formId = formId;\n            return this;\n        }\n\n        public Builder formVersion(String jrVersion) {\n            this.formVersion = jrVersion;\n            return this;\n        }\n\n        public Builder status(String status) {\n            this.status = status;\n            return this;\n        }\n\n        public Builder lastStatusChangeDate(Long lastStatusChangeDate) {\n            this.lastStatusChangeDate = lastStatusChangeDate;\n            return this;\n        }\n\n        public Builder finalizationDate(Long finalizationDate) {\n            this.finalizationDate = finalizationDate;\n            return this;\n        }\n\n        public Builder deletedDate(Long deletedDate) {\n            this.deletedDate = deletedDate;\n            return this;\n        }\n\n        public Builder geometryType(String geometryType) {\n            this.geometryType = geometryType;\n            return this;\n        }\n\n        public Builder geometry(String geometry) {\n            this.geometry = geometry;\n            return this;\n        }\n\n        public Builder editOf(Long editOf) {\n            this.editOf = editOf;\n            return this;\n        }\n\n        public Builder editNumber(Long editNumber) {\n            this.editNumber = editNumber;\n            return this;\n        }\n\n        public Builder dbId(Long dbId) {\n            this.dbId = dbId;\n            return this;\n        }\n\n        @NotNull\n        public Builder canDeleteBeforeSend(boolean canDeleteBeforeSend) {\n            this.canDeleteBeforeSend = canDeleteBeforeSend;\n            return this;\n        }\n\n        public Instance build() {\n            return new Instance(this);\n        }\n    }\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public String getSubmissionUri() {\n        return submissionUri;\n    }\n\n    public boolean canEditWhenComplete() {\n        return canEditWhenComplete;\n    }\n\n    public String getInstanceFilePath() {\n        return instanceFilePath;\n    }\n\n    public String getFormId() {\n        return formId;\n    }\n\n    public String getFormVersion() {\n        return formVersion;\n    }\n\n    public String getStatus() {\n        return status;\n    }\n\n    public Long getLastStatusChangeDate() {\n        return lastStatusChangeDate;\n    }\n\n    public Long getFinalizationDate() {\n        return finalizationDate;\n    }\n\n    @Nullable\n    public Long getDeletedDate() {\n        return deletedDate;\n    }\n\n    public String getGeometryType() {\n        return geometryType;\n    }\n\n    public String getGeometry() {\n        return geometry;\n    }\n\n    @Nullable\n    public Long getEditOf() {\n        return editOf;\n    }\n\n    @Nullable\n    public Long getEditNumber() {\n        return editNumber;\n    }\n\n\n    public Long getDbId() {\n        return dbId;\n    }\n\n    public boolean canDeleteBeforeSend() {\n        return canDeleteBeforeSend;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        Instance instance = (Instance) o;\n        return canEditWhenComplete == instance.canEditWhenComplete && Objects.equals(displayName, instance.displayName) && Objects.equals(submissionUri, instance.submissionUri) && Objects.equals(instanceFilePath, instance.instanceFilePath) && Objects.equals(formId, instance.formId) && Objects.equals(formVersion, instance.formVersion) && Objects.equals(status, instance.status) && Objects.equals(lastStatusChangeDate, instance.lastStatusChangeDate) && Objects.equals(finalizationDate, instance.finalizationDate) && Objects.equals(deletedDate, instance.deletedDate) && Objects.equals(geometryType, instance.geometryType) && Objects.equals(geometry, instance.geometry) && Objects.equals(editOf, instance.editOf) && Objects.equals(editNumber, instance.editNumber) && Objects.equals(dbId, instance.dbId);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(displayName, submissionUri, canEditWhenComplete, instanceFilePath, formId, formVersion, status, lastStatusChangeDate, finalizationDate, deletedDate, geometryType, geometry, editOf, editNumber, dbId);\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/instances/InstancesRepository.java",
    "content": "package org.odk.collect.forms.instances;\n\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\n\n/**\n * Gives access to {@link Instance} objects representing filled form instances on the device.\n *\n * The goal of this layer is to separate domain objects from data fetching concerns to facilitate\n * testing and reduce duplicated code to access those domain objects. To start, it has specific\n * query methods. Over time, we should expand it to mediate addition and removal. If there ends up\n * being may specific query methods, we may want to introduce a way to query more generically\n * without introducing new specialized methods (e.g. get(Specification s) instead of getBy(XYZ).\n */\npublic interface InstancesRepository {\n\n    @Nullable\n    Instance get(Long id);\n\n    /**\n     * Null if not exactly one instance matches.\n     */\n    @Nullable\n    Instance getOneByPath(String instancePath);\n\n    List<Instance> getAll();\n\n    List<Instance> getAllNotDeleted();\n\n    List<Instance> getAllByStatus(String... status);\n\n    int getCountByStatus(String... status);\n\n    List<Instance> getAllByFormId(String formId);\n\n    List<Instance> getAllNotDeletedByFormIdAndVersion(String formId, String version);\n\n    void delete(Long id);\n\n    void deleteAll();\n\n    Instance save(Instance instance);\n\n    /**\n     * Deletes any sensitive data (the submission file and geometry data) but leaves the Instance\n     * record in place.\n     */\n    void deleteWithLogging(Long id);\n\n    class IntegrityException extends RuntimeException {\n        public IntegrityException(String message) {\n            super(message);\n        }\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/savepoints/Savepoint.kt",
    "content": "package org.odk.collect.forms.savepoints\n\ndata class Savepoint(\n    val formDbId: Long,\n    val instanceDbId: Long?,\n    val savepointFilePath: String,\n    val instanceFilePath: String\n)\n"
  },
  {
    "path": "forms/src/main/java/org/odk/collect/forms/savepoints/SavepointsRepository.kt",
    "content": "package org.odk.collect.forms.savepoints\n\ninterface SavepointsRepository {\n    fun get(formDbId: Long, instanceDbId: Long?): Savepoint?\n\n    fun getAll(): List<Savepoint>\n\n    fun save(savepoint: Savepoint)\n\n    fun delete(formDbId: Long, instanceDbId: Long?)\n\n    fun deleteAll()\n}\n"
  },
  {
    "path": "forms/src/test/java/org/odk/collect/forms/instances/InstanceTest.kt",
    "content": "package org.odk.collect.forms.instances\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\n\nclass InstanceTest {\n\n    @Test\n    fun `canDeleteBeforeSend is true by default`() {\n        val instance = Instance.Builder().build()\n        assertThat(instance.canDeleteBeforeSend(), equalTo(true))\n    }\n}\n"
  },
  {
    "path": "forms-test/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "forms-test/README.md",
    "content": "# formstest\n\nProvides helpers and classes for writing tests in modules that depend on `forms`.\n"
  },
  {
    "path": "forms-test/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.javaLibrary)\n    alias(libs.plugins.kotlinLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\njava {\n    sourceCompatibility = JavaVersion.VERSION_17\n    targetCompatibility = JavaVersion.VERSION_17\n}\n\ndependencies {\n    implementation(project(\":forms\"))\n    implementation(project(\":shared\"))\n    implementation(libs.kotlinStdlib)\n    implementation(libs.commonsIo)\n    implementation(libs.junit)\n    implementation(libs.mockitoKotlin)\n    implementation(libs.hamcrest)\n}\n\ntasks.register(\"testDebug\") {\n    dependsOn(\"test\")\n}\n"
  },
  {
    "path": "forms-test/src/main/java/org/odk/collect/formstest/FormFixtures.kt",
    "content": "package org.odk.collect.formstest\n\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.MediaFile\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\nimport java.util.UUID\n\nobject FormFixtures {\n    // If you set the date here, it might be overridden with the current date during saving to the database if dbId is not set too\n    fun form(\n        formId: String = \"formId\",\n        version: String = \"1\",\n        formFilePath: String? = null,\n        mediaFiles: List<Pair<String, String>> = emptyList(),\n        autoSend: String? = null\n    ): Form {\n        val formFilesPath = TempFiles.createTempDir().absolutePath\n        val mediaFilePath = TempFiles.createTempDir().absolutePath\n\n        mediaFiles.forEach { (name, contents) ->\n            File(mediaFilePath, name).also { it.writeBytes(contents.toByteArray()) }\n        }\n\n        return Form.Builder()\n            .displayName(\"Test Form\")\n            .formId(formId)\n            .version(version)\n            .formFilePath(\n                formFilePath ?: FormUtils.createFormFixtureFile(\n                    formId,\n                    version,\n                    formFilesPath\n                )\n            )\n            .formMediaPath(mediaFilePath)\n            .autoSend(autoSend)\n            .build()\n    }\n\n    fun mediaFile(\n        integrityUrl: String? = null,\n        hash: String = UUID.randomUUID().toString()\n    ): MediaFile {\n        return MediaFile(\n            \"file\",\n            hash,\n            \"http://example.com/download\",\n            type = null,\n            integrityUrl\n        )\n    }\n}\n"
  },
  {
    "path": "forms-test/src/main/java/org/odk/collect/formstest/FormUtils.kt",
    "content": "package org.odk.collect.formstest\n\nimport org.apache.commons.io.FileUtils\nimport org.odk.collect.forms.Form\nimport java.io.File\nimport java.io.IOException\nimport java.nio.charset.Charset\n\nobject FormUtils {\n    @JvmStatic\n    @JvmOverloads\n    fun createXFormBody(formId: String, version: String?, title: String = \"Test Form\", entitiesVersion: String? = null): String {\n        val entitiesVersionAttribute = entitiesVersion?.let { \"\"\"entities:entities-version=\"$it\"\"\"\" } ?: \"\"\n\n        return \"\"\"<?xml version=\"1.0\"?>\n                    <h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n                        <h:head>\n                            <h:title>$title</h:title>\n                            <model $entitiesVersionAttribute>\n                                <instance>\n                                    <data id=\"$formId\" orx:version=\"$version\">\n                                        <question/>\n                                    </data>\n                                </instance>\n                                <bind nodeset=\"/data/question\" type=\"string\"/>\n                            </model>\n                        </h:head>\n                        <h:body>\n                            <input ref=\"/data/question\">\n                                <label>question label</label>\n                            </input>\n                        </h:body>\n                    </h:html>\"\"\"\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun createXFormFile(formId: String, version: String?, title: String = \"Form\"): File {\n        val body = createXFormBody(formId, version, title)\n        return try {\n            val file = File.createTempFile(\"$formId-$version\", \".xml\")\n            FileUtils.writeStringToFile(file, body, Charset.defaultCharset())\n            file\n        } catch (e: IOException) {\n            throw RuntimeException(e)\n        }\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun buildForm(\n        formId: String,\n        version: String?,\n        formFilesPath: String,\n        xform: String = createXFormBody(formId, version),\n        autosend: String? = null,\n        usesEntities: Boolean = false\n    ): Form.Builder {\n        val formFilePath = createFormFixtureFile(formId, version, formFilesPath, xform)\n\n        return Form.Builder()\n            .displayName(\"Test Form\")\n            .formId(formId)\n            .version(version)\n            .formFilePath(formFilePath)\n            .autoSend(autosend)\n            .usesEntities(usesEntities)\n    }\n\n    fun createFormFixtureFile(\n        formId: String,\n        version: String?,\n        formFilesPath: String,\n        xform: String = createXFormBody(formId, version)\n    ): String {\n        val fileName = formId + \"-\" + version + \"-\" + Math.random()\n        val formFile = File(\"$formFilesPath/$fileName.xml\")\n\n        try {\n            FileUtils.writeByteArrayToFile(formFile, xform.toByteArray())\n        } catch (e: IOException) {\n            throw RuntimeException(e)\n        }\n\n        return formFile.absolutePath\n    }\n}\n"
  },
  {
    "path": "forms-test/src/main/java/org/odk/collect/formstest/FormsRepositoryTest.java",
    "content": "package org.odk.collect.formstest;\n\nimport org.apache.commons.io.FileUtils;\nimport org.junit.Test;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.forms.savepoints.Savepoint;\nimport org.odk.collect.forms.savepoints.SavepointsRepository;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.function.Supplier;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.contains;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.assertNotNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.formstest.FormUtils.createXFormBody;\n\npublic abstract class FormsRepositoryTest {\n    protected final SavepointsRepository savepointsRepository = new InMemSavepointsRepository();\n\n    public abstract FormsRepository buildSubject();\n\n    public abstract FormsRepository buildSubject(Supplier<Long> clock);\n\n    public abstract String getFormFilesPath();\n\n    @Test\n    public void getLatestByFormIdAndVersion_whenFormHasNullVersion_returnsForm() {\n        FormsRepository formsRepository = buildSubject();\n        formsRepository.save(FormUtils.buildForm(\"1\", null, getFormFilesPath())\n                .build());\n\n        Form form = formsRepository.getLatestByFormIdAndVersion(\"1\", null);\n        assertThat(form, notNullValue());\n        assertThat(form.getDbId(), is(1L));\n    }\n\n    @Test\n    public void getLatestByFormIdAndVersion_whenMultipleExist_returnsLatest() {\n        Supplier<Long> mockClock = mock(Supplier.class);\n        when(mockClock.get()).thenReturn(2L, 3L, 1L);\n\n        FormsRepository formsRepository = buildSubject(mockClock);\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", getFormFilesPath(), createXFormBody(\"1\", \"1\", \"Form1\"))\n                .build());\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", getFormFilesPath(), createXFormBody(\"1\", \"1\", \"Form2\"))\n                .build());\n        formsRepository.save(FormUtils.buildForm(\"1\", \"1\", getFormFilesPath(), createXFormBody(\"1\", \"1\", \"Form3\"))\n                .build());\n\n        Form form = formsRepository.getLatestByFormIdAndVersion(\"1\", \"1\");\n        assertThat(form, notNullValue());\n        assertThat(form.getDbId(), is(2L));\n    }\n\n    @Test\n    public void getAllByFormIdAndVersion_whenFormHasNullVersion_returnsAllMatchingForms() {\n        FormsRepository formsRepository = buildSubject();\n        formsRepository.save(FormUtils.buildForm(\"1\", null, getFormFilesPath(), createXFormBody(\"1\", null, \"Form1\"))\n                .build());\n\n        formsRepository.save(FormUtils.buildForm(\"1\", null, getFormFilesPath(), createXFormBody(\"1\", null, \"Form2\"))\n                .build());\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"7\", getFormFilesPath(), createXFormBody(\"1\", \"7\", \"Form3\"))\n                .build());\n\n        List<Form> forms = formsRepository.getAllByFormIdAndVersion(\"1\", null);\n        assertThat(forms.size(), is(2));\n        assertThat(forms.get(0).getVersion(), is(nullValue()));\n        assertThat(forms.get(1).getVersion(), is(nullValue()));\n    }\n\n    @Test\n    public void getAllNotDeletedByFormId_doesNotReturnDeletedForms() {\n        FormsRepository formsRepository = buildSubject();\n        formsRepository.save(FormUtils.buildForm(\"1\", \"deleted\", getFormFilesPath())\n                .deleted(true)\n                .build()\n        );\n\n        formsRepository.save(FormUtils.buildForm(\"1\", \"not-deleted\", getFormFilesPath())\n                .deleted(false)\n                .build()\n        );\n\n        List<Form> forms = formsRepository.getAllNotDeletedByFormId(\"1\");\n        assertThat(forms.size(), is(1));\n        assertThat(forms.get(0).getVersion(), equalTo(\"not-deleted\"));\n    }\n\n    @Test\n    public void getAllNotDeletedByFormIdAndVersion_onlyReturnsNotDeletedFormsThatMatchVersion() {\n        FormsRepository formsRepository = buildSubject();\n        formsRepository.save(FormUtils.buildForm(\"id\", \"1\", getFormFilesPath(), createXFormBody(\"id\", \"1\", \"Form1\"))\n                .deleted(true)\n                .build()\n        );\n        formsRepository.save(FormUtils.buildForm(\"id\", \"1\", getFormFilesPath(), createXFormBody(\"id\", \"1\", \"Form2\"))\n                .deleted(false)\n                .build()\n        );\n\n        formsRepository.save(FormUtils.buildForm(\"id\", \"2\", getFormFilesPath(), createXFormBody(\"id\", \"2\", \"Form3\"))\n                .deleted(true)\n                .build()\n        );\n        formsRepository.save(FormUtils.buildForm(\"id\", \"2\", getFormFilesPath(), createXFormBody(\"id\", \"2\", \"Form4\"))\n                .deleted(false)\n                .build()\n        );\n\n        List<Form> forms = formsRepository.getAllNotDeletedByFormIdAndVersion(\"id\", \"2\");\n        assertThat(forms.size(), is(1));\n        assertThat(forms.get(0).getVersion(), equalTo(\"2\"));\n    }\n\n    @Test\n    public void softDelete_marksDeletedAsTrue() {\n        FormsRepository formsRepository = buildSubject();\n        formsRepository.save(FormUtils.buildForm(\"1\", null, getFormFilesPath())\n                .build());\n\n        formsRepository.softDelete(1L);\n        assertThat(formsRepository.get(1L).isDeleted(), is(true));\n    }\n\n    @Test\n    public void softDelete_deletesTheSavepointThatBelongsToTheFormThatShouldBeDeleted() {\n        FormsRepository formsRepository = buildSubject();\n        Form form1 = formsRepository.save(FormUtils.buildForm(\"id1\", \"version\", getFormFilesPath()).build());\n        Form form2 = formsRepository.save(FormUtils.buildForm(\"id2\", \"version\", getFormFilesPath()).build());\n\n        Savepoint savepoint1 = new Savepoint(form1.getDbId(), null, \"\", \"\");\n        Savepoint savepoint2 = new Savepoint(form2.getDbId(), null, \"\", \"\");\n        savepointsRepository.save(savepoint1);\n        savepointsRepository.save(savepoint2);\n\n        formsRepository.softDelete(form1.getDbId());\n\n        assertThat(savepointsRepository.getAll(), contains(savepoint2));\n    }\n\n    @Test\n    public void restore_marksDeletedAsFalse() {\n        FormsRepository formsRepository = buildSubject();\n        formsRepository.save(FormUtils.buildForm(\"1\", null, getFormFilesPath())\n                .deleted(true)\n                .build());\n\n        formsRepository.restore(1L);\n        assertThat(formsRepository.get(1L).isDeleted(), is(false));\n    }\n\n    @Test\n    public void save_multipleFormsWithTheSameHashIgnoresDuplicatesAndReturnsTheExistingForm() {\n        FormsRepository formsRepository = buildSubject();\n        Form form = FormUtils.buildForm(\"id\", \"version\", getFormFilesPath()).build();\n\n        Form form1 = formsRepository.save(form);\n        Form form2 = formsRepository.save(form);\n\n        assertThat(formsRepository.getAll().size(), is(1));\n        assertNotNull(form1);\n        assertNotNull(form2);\n        assertEquals(form1, form2);\n    }\n\n    @Test\n    public void save_addsId() {\n        FormsRepository formsRepository = buildSubject();\n        Form form = FormUtils.buildForm(\"id\", \"version\", getFormFilesPath()).build();\n\n        formsRepository.save(form);\n        assertThat(formsRepository.getAll().get(0).getDbId(), notNullValue());\n    }\n\n    @Test\n    public void save_addsMediaPath_whereMediaDirCanBeCreated() {\n        FormsRepository formsRepository = buildSubject();\n        Form form = FormUtils.buildForm(\"id\", \"version\", getFormFilesPath()).build();\n        assertThat(form.getFormMediaPath(), equalTo(null));\n\n        Form savedForm = formsRepository.save(form);\n        assertThat(new File(savedForm.getFormMediaPath()).mkdir(), is(true));\n    }\n\n    @Test\n    public void save_addsHashBasedOnFormFile() {\n        FormsRepository formsRepository = buildSubject();\n        Form form = FormUtils.buildForm(\"id\", \"version\", getFormFilesPath()).build();\n        assertThat(form.getMD5Hash(), equalTo(null));\n\n        formsRepository.save(form);\n\n        String expectedHash = Md5.getMd5Hash(new File(form.getFormFilePath()));\n        assertThat(formsRepository.get(1L).getMD5Hash(), equalTo(expectedHash));\n    }\n\n    @Test(expected = Exception.class)\n    public void save_whenNoFormFilePath_explodes() {\n        FormsRepository formsRepository = buildSubject();\n        Form form = FormUtils.buildForm(\"id\", \"version\", getFormFilesPath()).build();\n        form = new Form.Builder(form)\n                .formFilePath(null)\n                .build();\n\n        formsRepository.save(form);\n    }\n\n    @Test\n    public void save_whenFormHasId_updatesExisting() {\n        FormsRepository formsRepository = buildSubject();\n        Form originalForm = formsRepository.save(FormUtils.buildForm(\"id\", \"version\", getFormFilesPath())\n                .displayName(\"original\")\n                .build());\n\n        formsRepository.save(new Form.Builder(originalForm)\n                .displayName(\"changed\")\n                .build());\n\n        assertThat(formsRepository.get(originalForm.getDbId()).getDisplayName(), is(\"changed\"));\n    }\n\n    @Test\n    public void save_whenFormHasId_updatesHash() throws IOException {\n        FormsRepository formsRepository = buildSubject();\n        Form originalForm = formsRepository.save(FormUtils.buildForm(\"id\", \"version\", getFormFilesPath())\n                .displayName(\"original\")\n                .build());\n\n        String newFormBody = FormUtils.createXFormBody(\"id\", \"version\", \"A different title\");\n        File formFile = new File(originalForm.getFormFilePath());\n        FileUtils.writeByteArrayToFile(formFile, newFormBody.getBytes());\n\n        formsRepository.save(new Form.Builder(originalForm)\n                .displayName(\"changed\")\n                .build());\n\n        String expectedHash = Md5.getMd5Hash(formFile);\n        assertThat(formsRepository.get(originalForm.getDbId()).getMD5Hash(), is(expectedHash));\n    }\n\n    @Test\n    public void save_withNewFormVersion_copiesLanguageFromLatestFormVersion() {\n        FormsRepository formsRepository = buildSubject();\n\n        Form formV1 = formsRepository.save(FormUtils.buildForm(\"id\", \"1\", getFormFilesPath()).build());\n        formsRepository.save(new Form.Builder(formV1)\n                .language(\"Spanish\")\n                .build());\n\n        Form formV2 = formsRepository.save(FormUtils.buildForm(formV1.getFormId(), \"2\", getFormFilesPath()).build());\n        formsRepository.save(new Form.Builder(formV2)\n                .language(\"Polish\")\n                .build());\n\n        Form formV3 = formsRepository.save(FormUtils.buildForm(formV1.getFormId(), \"3\", getFormFilesPath()).build());\n\n        assertThat(formsRepository.get(formV3.getDbId()).getLanguage(), is(\"Polish\"));\n    }\n\n    @Test\n    public void delete_deletesFiles() throws Exception {\n        FormsRepository formsRepository = buildSubject();\n        Form form = formsRepository.save(FormUtils.buildForm(\"id\", \"version\", getFormFilesPath()).build());\n\n        // FormRepository doesn't automatically create all form files\n        File mediaDir = new File(form.getFormMediaPath());\n        mediaDir.mkdir();\n        File cacheFile = new File(form.getJrCacheFilePath());\n        cacheFile.createNewFile();\n\n        File formFile = new File(form.getFormFilePath());\n        assertThat(formFile.exists(), is(true));\n        assertThat(mediaDir.exists(), is(true));\n        assertThat(cacheFile.exists(), is(true));\n\n        formsRepository.delete(form.getDbId());\n        assertThat(formFile.exists(), is(false));\n        assertThat(mediaDir.exists(), is(false));\n        assertThat(cacheFile.exists(), is(false));\n    }\n\n    @Test\n    public void delete_deletesTheSavepointThatBelongsToTheFormThatShouldBeDeleted() {\n        FormsRepository formsRepository = buildSubject();\n        Form form1 = formsRepository.save(FormUtils.buildForm(\"id1\", \"version\", getFormFilesPath()).build());\n        Form form2 = formsRepository.save(FormUtils.buildForm(\"id2\", \"version\", getFormFilesPath()).build());\n\n        Savepoint savepoint1 = new Savepoint(form1.getDbId(), null, \"\", \"\");\n        Savepoint savepoint2 = new Savepoint(form2.getDbId(), null, \"\", \"\");\n        savepointsRepository.save(savepoint1);\n        savepointsRepository.save(savepoint2);\n\n        formsRepository.delete(form1.getDbId());\n\n        assertThat(savepointsRepository.getAll(), contains(savepoint2));\n    }\n\n    @Test\n    public void delete_whenMediaPathIsFile_deletesFiles() throws Exception {\n        FormsRepository formsRepository = buildSubject();\n        Form form = formsRepository.save(FormUtils.buildForm(\"id\", \"version\", getFormFilesPath()).build());\n\n        // FormRepository currently doesn't manage media file path other than deleting it\n        String mediaPath = form.getFormMediaPath();\n        new File(mediaPath).createNewFile();\n\n        File formFile = new File(form.getFormFilePath());\n        File mediaDir = new File(form.getFormMediaPath());\n        assertThat(formFile.exists(), is(true));\n        assertThat(mediaDir.exists(), is(true));\n\n        formsRepository.delete(1L);\n        assertThat(formFile.exists(), is(false));\n        assertThat(mediaDir.exists(), is(false));\n    }\n\n    @Test\n    public void deleteAll_deletesAllForms() {\n        FormsRepository formsRepository = buildSubject();\n        formsRepository.save(FormUtils.buildForm(\"id1\", \"version\", getFormFilesPath()).build());\n        formsRepository.save(FormUtils.buildForm(\"id2\", \"version\", getFormFilesPath()).build());\n\n        List<Form> forms = formsRepository.getAll();\n\n        formsRepository.deleteAll();\n        assertThat(formsRepository.getAll().size(), is(0));\n\n        for (Form form : forms) {\n            assertThat(new File(form.getFormFilePath()).exists(), is(false));\n            assertThat(new File(form.getFormMediaPath()).exists(), is(false));\n        }\n    }\n\n    @Test\n    public void deleteAll_deletesAllSavepointsThatBelongToFormsThatShouldBeDeleted() {\n        FormsRepository formsRepository = buildSubject();\n        Form form1 = formsRepository.save(FormUtils.buildForm(\"id1\", \"version\", getFormFilesPath()).build());\n        Form form2 = formsRepository.save(FormUtils.buildForm(\"id2\", \"version\", getFormFilesPath()).build());\n\n        Savepoint savepoint1 = new Savepoint(form1.getDbId(), null, \"\", \"\");\n        Savepoint savepoint2 = new Savepoint(form2.getDbId(), null, \"\", \"\");\n        savepointsRepository.save(savepoint1);\n        savepointsRepository.save(savepoint2);\n\n        formsRepository.deleteAll();\n\n        assertThat(savepointsRepository.getAll().isEmpty(), equalTo(true));\n    }\n\n    @Test\n    public void deleteByMd5Hash_deletesFormsWithMatchingHash() {\n        FormsRepository formsRepository = buildSubject();\n        formsRepository.save(FormUtils.buildForm(\"id1\", \"version\", getFormFilesPath(), createXFormBody(\"id1\", \"version\", \"Form1\")).build());\n        formsRepository.save(FormUtils.buildForm(\"id2\", \"version\", getFormFilesPath(), createXFormBody(\"id2\", \"version\", \"Form2\")).build());\n\n        List<Form> id1Forms = formsRepository.getAllByFormIdAndVersion(\"id1\", \"version\");\n        formsRepository.deleteByMd5Hash(id1Forms.get(0).getMD5Hash());\n\n        assertThat(formsRepository.getAll().size(), is(1));\n        assertThat(formsRepository.getAll().get(0).getFormId(), is(\"id2\"));\n    }\n\n    @Test\n    public void deleteByMd5Hash_deletesTheSavepointThatBelongsToTheFormThatShouldBeDeleted() {\n        FormsRepository formsRepository = buildSubject();\n        Form form1 = formsRepository.save(FormUtils.buildForm(\"id1\", \"version\", getFormFilesPath(), createXFormBody(\"id1\", \"version\", \"Form1\")).build());\n        Form form2 = formsRepository.save(FormUtils.buildForm(\"id2\", \"version\", getFormFilesPath(), createXFormBody(\"id2\", \"version\", \"Form2\")).build());\n\n        Savepoint savepoint1 = new Savepoint(form1.getDbId(), null, \"\", \"\");\n        Savepoint savepoint2 = new Savepoint(form2.getDbId(), null, \"\", \"\");\n        savepointsRepository.save(savepoint1);\n        savepointsRepository.save(savepoint2);\n\n        List<Form> id1Forms = formsRepository.getAllByFormIdAndVersion(\"id1\", \"version\");\n        formsRepository.deleteByMd5Hash(id1Forms.get(0).getMD5Hash());\n\n        assertThat(savepointsRepository.getAll(), contains(savepoint2));\n    }\n\n    @Test(expected = Exception.class)\n    public void getOneByMd5Hash_whenHashIsNull_explodes() {\n        buildSubject().getOneByMd5Hash(null);\n    }\n\n    @Test\n    public void getOneByMd5Hash_returnsMatchingForm() {\n        FormsRepository formsRepository = buildSubject();\n        formsRepository.save(FormUtils.buildForm(\"id1\", \"version\", getFormFilesPath()).build());\n        Form form2 = formsRepository.save(FormUtils.buildForm(\"id2\", \"version\", getFormFilesPath()).build());\n\n        assertThat(formsRepository.getOneByMd5Hash(form2.getMD5Hash()), is(form2));\n    }\n\n    @Test\n    public void getOneByPath_returnsMatchingForm() {\n        FormsRepository formsRepository = buildSubject();\n        formsRepository.save(FormUtils.buildForm(\"id1\", \"version\", getFormFilesPath()).build());\n\n        Form form2 = FormUtils.buildForm(\"id2\", \"version\", getFormFilesPath()).build();\n        formsRepository.save(form2);\n\n        assertThat(formsRepository.getOneByPath(form2.getFormFilePath()).getFormId(), is(\"id2\"));\n    }\n\n    @Test\n    public void getAllFormId_returnsMatchingForms() {\n        FormsRepository formsRepository = buildSubject();\n        Form form1 = formsRepository.save(FormUtils.buildForm(\"id1\", \"version\", getFormFilesPath()).build());\n        Form form2 = formsRepository.save(FormUtils.buildForm(\"id1\", \"other_version\", getFormFilesPath()).build());\n        formsRepository.save(FormUtils.buildForm(\"id2\", \"version\", getFormFilesPath()).build());\n\n        List<Form> forms = formsRepository.getAllByFormId(\"id1\");\n        assertThat(forms.size(), is(2));\n        assertThat(forms, contains(form1, form2));\n    }\n}\n"
  },
  {
    "path": "forms-test/src/main/java/org/odk/collect/formstest/InMemFormsRepository.java",
    "content": "package org.odk.collect.formstest;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.odk.collect.forms.Form;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.forms.savepoints.SavepointsRepository;\nimport org.odk.collect.shared.files.FileExt;\nimport org.odk.collect.shared.strings.Md5;\nimport org.odk.collect.shared.TempFiles;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\nimport static java.util.stream.Collectors.toList;\n\npublic class InMemFormsRepository implements FormsRepository {\n\n    private final List<Form> forms = new ArrayList<>();\n    private long idCounter = 1L;\n\n    private final Supplier<Long> clock;\n    private final SavepointsRepository savepointsRepository;\n\n    public InMemFormsRepository() {\n        this(System::currentTimeMillis, new InMemSavepointsRepository());\n    }\n\n    public InMemFormsRepository(Supplier<Long> clock) {\n        this(clock, new InMemSavepointsRepository());\n    }\n\n    public InMemFormsRepository(SavepointsRepository savepointsRepository) {\n        this(System::currentTimeMillis, savepointsRepository);\n    }\n\n    public InMemFormsRepository(Supplier<Long> clock, SavepointsRepository savepointsRepository) {\n        this.clock = clock;\n        this.savepointsRepository = savepointsRepository;\n    }\n\n    @Nullable\n    @Override\n    public Form get(Long id) {\n        return forms.stream().filter(f -> f.getDbId().equals(id)).findFirst().orElse(null);\n    }\n\n    @Nullable\n    @Override\n    public Form getLatestByFormIdAndVersion(String formId, @Nullable String version) {\n        List<Form> candidates = getAllByFormIdAndVersion(formId, version);\n\n        if (!candidates.isEmpty()) {\n            return candidates.stream().max(Comparator.comparingLong(Form::getDate)).get();\n        } else {\n            return null;\n        }\n    }\n\n    @Nullable\n    @Override\n    public Form getOneByMd5Hash(@NotNull String hash) {\n        if (hash == null) {\n            throw new IllegalArgumentException(\"null hash\");\n        }\n\n        return forms.stream().filter(f -> f.getMD5Hash().equals(hash)).findFirst().orElse(null);\n    }\n\n    @Nullable\n    @Override\n    public Form getOneByPath(String path) {\n        return forms.stream().filter(f -> f.getFormFilePath().equals(path)).findFirst().orElse(null);\n    }\n\n    @Override\n    public List<Form> getAll() {\n        return new ArrayList<>(forms); // Avoid anything  mutating the list externally\n    }\n\n    @Override\n    public List<Form> getAllByFormIdAndVersion(String jrFormId, @Nullable String jrVersion) {\n        return forms.stream().filter(f -> f.getFormId().equals(jrFormId) && Objects.equals(f.getVersion(), jrVersion)).collect(toList());\n    }\n\n    @Override\n    public List<Form> getAllByFormId(String formId) {\n        return forms.stream().filter(f -> f.getFormId().equals(formId)).collect(toList());\n    }\n\n    @Override\n    public List<Form> getAllNotDeletedByFormId(String jrFormId) {\n        return forms.stream().filter(f -> f.getFormId().equals(jrFormId) && !f.isDeleted()).collect(toList());\n    }\n\n    public List<Form> getAllNotDeletedByFormIdAndVersion(String jrFormId, @Nullable String jrVersion) {\n        return forms.stream().filter(f -> f.getFormId().equals(jrFormId) && Objects.equals(f.getVersion(), jrVersion) && !f.isDeleted()).collect(toList());\n    }\n\n    @Override\n    public Form save(@NotNull Form form) {\n        Form.Builder builder = new Form.Builder(form);\n\n        if (form.getFormMediaPath() == null) {\n            builder.formMediaPath(TempFiles.getPathInTempDir());\n        }\n\n        if (form.getDbId() != null) {\n            String formFilePath = form.getFormFilePath();\n            String hash = Md5.getMd5Hash(new File(formFilePath));\n            builder.md5Hash(hash);\n\n            forms.removeIf(f -> f.getDbId().equals(form.getDbId()));\n            forms.add(builder.build());\n            return form;\n        } else {\n            builder.dbId(idCounter++)\n                    .date(clock.get());\n\n            // Allows tests to override hash\n            String hash;\n            if (form.getMD5Hash() == null) {\n                String formFilePath = form.getFormFilePath();\n                hash = Md5.getMd5Hash(new File(formFilePath));\n                builder.md5Hash(hash);\n            } else {\n                hash = form.getMD5Hash();\n            }\n\n            Form existingFormWithSameHash = forms.stream().filter(f -> f.getMD5Hash().equals(hash)).findFirst().orElse(null);\n            if (existingFormWithSameHash != null) {\n                return existingFormWithSameHash;\n            }\n\n            if (form.getJrCacheFilePath() == null) {\n                builder.jrCacheFilePath(TempFiles.getPathInTempDir(hash, \".formdef\"));\n            }\n\n            List<Form> existingFormsWithSameFormId = getAllByFormId(form.getFormId());\n            if (!existingFormsWithSameFormId.isEmpty()) {\n                Form latestFormWithSameFormId = Collections.max(existingFormsWithSameFormId, Comparator.comparing(Form::getDate));\n                builder.language(latestFormWithSameFormId.getLanguage());\n            }\n\n            Form formToSave = builder.build();\n            forms.add(formToSave);\n            return formToSave;\n        }\n    }\n\n    @Override\n    public void delete(Long id) {\n        Optional<Form> formToRemove = forms.stream().filter(f -> f.getDbId().equals(id)).findFirst();\n        if (formToRemove.isPresent()) {\n            Form form = formToRemove.get();\n            deleteFilesForForm(form);\n            forms.remove(form);\n        }\n    }\n\n    @Override\n    public void softDelete(Long id) {\n        Form form = forms.stream().filter(f -> f.getDbId().equals(id)).findFirst().orElse(null);\n\n        if (form != null) {\n            forms.remove(form);\n            forms.add(new Form.Builder(form)\n                    .deleted(true)\n                    .build());\n            savepointsRepository.delete(id, null);\n        }\n    }\n\n    @Override\n    public void deleteByMd5Hash(@NotNull String md5Hash) {\n        Form form = forms.stream().filter(f -> f.getMD5Hash().equals(md5Hash)).findFirst().orElse(null);\n        if (form != null) {\n            forms.remove(form);\n            savepointsRepository.delete(form.getDbId(), null);\n        }\n    }\n\n    @Override\n    public void deleteAll() {\n        for (Form form : forms) {\n            deleteFilesForForm(form);\n        }\n\n        forms.clear();\n    }\n\n    @Override\n    public void restore(Long id) {\n        Form form = forms.stream().filter(f -> f.getDbId().equals(id)).findFirst().orElse(null);\n\n        if (form != null) {\n            forms.remove(form);\n            forms.add(new Form.Builder(form)\n                    .deleted(false)\n                    .build());\n        }\n    }\n\n    private void deleteFilesForForm(Form form) {\n        // Delete form file\n        if (form.getFormFilePath() != null) {\n            new File(form.getFormFilePath()).delete();\n        }\n\n        // Delete cache file\n        if (form.getJrCacheFilePath() != null) {\n            new File(form.getJrCacheFilePath()).delete();\n        }\n\n        // Delete media files\n        if (form.getFormMediaPath() != null) {\n            File mediaDir = new File(form.getFormMediaPath());\n\n            if (mediaDir.isDirectory()) {\n                FileExt.deleteDirectory(mediaDir);\n            } else {\n                mediaDir.delete();\n            }\n        }\n\n        savepointsRepository.delete(form.getDbId(), null);\n    }\n}\n"
  },
  {
    "path": "forms-test/src/main/java/org/odk/collect/formstest/InMemInstancesRepository.java",
    "content": "package org.odk.collect.formstest;\n\nimport org.odk.collect.forms.instances.Instance;\nimport org.odk.collect.forms.instances.InstancesRepository;\nimport org.odk.collect.shared.files.FileExt;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic final class InMemInstancesRepository implements InstancesRepository {\n\n    private final List<Instance> instances;\n    private final Supplier<Long> clock;\n\n    private long idCounter = 1L;\n\n    public InMemInstancesRepository() {\n        this(System::currentTimeMillis, new ArrayList<>());\n    }\n\n    public InMemInstancesRepository(List<Instance> instances) {\n        this(System::currentTimeMillis, instances);\n    }\n\n    public InMemInstancesRepository(Supplier<Long> clock) {\n        this(clock, new ArrayList<>());\n    }\n\n    public InMemInstancesRepository(Supplier<Long> clock, List<Instance> instances) {\n        this.clock = clock;\n        this.instances = new ArrayList<>(instances);\n    }\n\n    @Override\n    public Instance get(Long databaseId) {\n        for (Instance instance : instances) {\n            if (instance.getDbId().equals(databaseId)) {\n                return instance;\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public Instance getOneByPath(String instancePath) {\n        List<Instance> result = new ArrayList<>();\n\n        for (Instance instance : instances) {\n            if (instance.getInstanceFilePath().equals(instancePath)) {\n                result.add(instance);\n            }\n        }\n\n        if (result.size() == 1) {\n            return result.get(0);\n        } else {\n            return null;\n        }\n    }\n\n    @Override\n    public List<Instance> getAll() {\n        return new ArrayList<>(instances);\n    }\n\n    @Override\n    public List<Instance> getAllNotDeleted() {\n        return instances.stream()\n                .filter(instance -> instance.getDeletedDate() == null)\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public List<Instance> getAllByStatus(String... status) {\n        List<String> statuses = Arrays.asList(status);\n        List<Instance> result = new ArrayList<>();\n\n        for (Instance instance : instances) {\n            if (statuses.contains(instance.getStatus())) {\n                result.add(instance);\n            }\n        }\n\n        return result;\n    }\n\n    @Override\n    public int getCountByStatus(String... status) {\n        return getAllByStatus(status).size();\n    }\n\n    @Override\n    public List<Instance> getAllByFormId(String formId) {\n        List<Instance> result = new ArrayList<>();\n\n        for (Instance instance : instances) {\n            if (instance.getFormId().equals(formId)) {\n                result.add(instance);\n            }\n        }\n\n        return result;\n    }\n\n    @Override\n    public List<Instance> getAllNotDeletedByFormIdAndVersion(String formId, String version) {\n        return instances.stream().filter(instance -> {\n            return Objects.equals(instance.getFormId(), formId)\n                    && Objects.equals(instance.getFormVersion(), version)\n                    && instance.getDeletedDate() == null;\n        }).collect(Collectors.toList());\n    }\n\n    @Override\n    public void delete(Long id) {\n        Instance instance = get(id);\n\n        boolean hasEdits = instances.stream()\n                .anyMatch(it -> instance.getDbId().equals(it.getEditOf()));\n        if (hasEdits) {\n            throw new IntegrityException(\"\");\n        }\n\n        deleteInstanceFiles(instance);\n\n        instances.remove(instance);\n    }\n\n    @Override\n    public void deleteAll() {\n        for (Instance instance : instances) {\n            deleteInstanceFiles(instance);\n        }\n\n        instances.clear();\n    }\n\n    @Override\n    public Instance save(Instance instance) {\n        if (instance.getStatus() == null) {\n            instance = new Instance.Builder(instance)\n                    .status(Instance.STATUS_INCOMPLETE)\n                    .build();\n        }\n\n        Long currentTime = clock.get();\n\n        if (instance.getStatus().equals(Instance.STATUS_COMPLETE) && instance.getFinalizationDate() == null) {\n            instance = new Instance.Builder(instance)\n                    .finalizationDate(currentTime)\n                    .build();\n        }\n\n        Long id = instance.getDbId();\n        if (id == null) {\n            if (instance.getLastStatusChangeDate() == null) {\n                instance = new Instance.Builder(instance)\n                        .lastStatusChangeDate(currentTime)\n                        .build();\n            }\n\n            Instance newInstance = new Instance.Builder(instance)\n                    .dbId(idCounter++)\n                    .build();\n\n            saveInstance(newInstance);\n            return newInstance;\n        } else {\n            if (instance.getDeletedDate() == null) {\n                instance = new Instance.Builder(instance)\n                        .lastStatusChangeDate(currentTime)\n                        .build();\n            }\n\n            instances.removeIf(i -> i.getDbId().equals(id));\n            saveInstance(instance);\n            return instance;\n        }\n    }\n\n    @Override\n    public void deleteWithLogging(Long id) {\n        Instance instance = new Instance.Builder(get(id))\n                .geometry(null)\n                .geometryType(null)\n                .deletedDate(clock.get())\n                .build();\n\n        instances.removeIf(i -> i.getDbId().equals(id));\n        saveInstance(instance);\n        deleteInstanceFiles(instance);\n    }\n\n    public void removeInstanceById(Long databaseId) {\n        for (int i = 0; i < instances.size(); i++) {\n            if (instances.get(i).getDbId().equals(databaseId)) {\n                instances.remove(i);\n                return;\n            }\n        }\n    }\n\n    private void deleteInstanceFiles(Instance instance) {\n        if (instance.getInstanceFilePath() != null) {\n            FileExt.deleteDirectory(new File(instance.getInstanceFilePath()).getParentFile());\n        }\n    }\n\n    private void saveInstance(Instance instance) {\n        if (instance.getEditOf() == null ^ instance.getEditNumber() == null) {\n            throw new IntegrityException(\"\");\n        }\n        if (instance.getDbId().equals(instance.getEditOf())) {\n            throw new IntegrityException(\"\");\n        }\n        if (instance.getEditOf() != null) {\n            boolean referenceInstance = instances.stream()\n                    .anyMatch(it -> it.getDbId().equals(instance.getEditOf()));\n\n            if (!referenceInstance) {\n                throw new IntegrityException(\"\");\n            }\n        }\n        instances.add(instance);\n    }\n}\n"
  },
  {
    "path": "forms-test/src/main/java/org/odk/collect/formstest/InMemSavepointsRepository.kt",
    "content": "package org.odk.collect.formstest\n\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport java.io.File\n\nclass InMemSavepointsRepository : SavepointsRepository {\n    private val savepoints = mutableListOf<Savepoint>()\n\n    override fun get(formDbId: Long, instanceDbId: Long?): Savepoint? {\n        return savepoints.find { savepoint -> savepoint.formDbId == formDbId && savepoint.instanceDbId == instanceDbId }\n    }\n\n    override fun getAll(): List<Savepoint> {\n        return savepoints\n    }\n\n    override fun save(savepoint: Savepoint) {\n        if (savepoints.any { it.formDbId == savepoint.formDbId && it.instanceDbId == savepoint.instanceDbId }) {\n            return\n        }\n        savepoints.add(savepoint)\n        savepoints.indexOf(savepoint).toLong()\n    }\n\n    override fun delete(formDbId: Long, instanceDbId: Long?) {\n        val savepoint = get(formDbId, instanceDbId)\n        if (savepoint != null) {\n            File(savepoint.savepointFilePath).delete()\n            savepoints.remove(get(formDbId, instanceDbId))\n        }\n    }\n\n    override fun deleteAll() {\n        savepoints.forEach {\n            File(it.savepointFilePath).delete()\n        }\n        savepoints.clear()\n    }\n}\n"
  },
  {
    "path": "forms-test/src/main/java/org/odk/collect/formstest/InstanceFixtures.kt",
    "content": "package org.odk.collect.formstest\n\nimport org.odk.collect.forms.Form\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\nobject InstanceFixtures {\n\n    fun instance(\n        status: String = Instance.STATUS_INCOMPLETE,\n        lastStatusChangeDate: Long = 0,\n        displayName: String = \"Form\",\n        dbId: Long? = null,\n        form: Form? = null,\n        deletedDate: Long? = null,\n        canDeleteBeforeSend: Boolean = true,\n        instancesDir: File = TempFiles.createTempDir(),\n        formId: String = \"formId\",\n        formVersion: String = \"version\",\n        editOf: Long? = null,\n        editNumber: Long? = null\n    ): Instance {\n        return InstanceUtils.buildInstance(formId, formVersion, instancesDir.absolutePath)\n            .status(status)\n            .lastStatusChangeDate(lastStatusChangeDate)\n            .displayName(displayName)\n            .dbId(dbId).also {\n                if (form != null) {\n                    it.formId(form.formId)\n                    it.formVersion(form.version)\n                }\n            }\n            .deletedDate(deletedDate)\n            .canDeleteBeforeSend(canDeleteBeforeSend)\n            .editOf(editOf)\n            .editNumber(editNumber)\n            .build()\n    }\n}\n"
  },
  {
    "path": "forms-test/src/main/java/org/odk/collect/formstest/InstanceUtils.kt",
    "content": "package org.odk.collect.formstest\n\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.shared.TempFiles.createTempFile\nimport org.odk.collect.shared.strings.RandomString\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\nobject InstanceUtils {\n\n    @JvmStatic\n    fun buildInstance(formId: String?, version: String?, instancesDir: String): Instance.Builder {\n        return buildInstance(\n            formId,\n            version,\n            \"display name\",\n            Instance.STATUS_INCOMPLETE,\n            null,\n            instancesDir\n        )\n    }\n\n    @JvmStatic\n    fun buildInstance(\n        formId: String?,\n        version: String?,\n        displayName: String?,\n        status: String?,\n        deletedDate: Long?,\n        instancesDir: String\n    ): Instance.Builder {\n        val instanceFile = createInstanceDirAndFile(instancesDir)\n\n        return Instance.Builder()\n            .formId(formId)\n            .formVersion(version)\n            .displayName(displayName)\n            .instanceFilePath(instanceFile.absolutePath)\n            .status(status)\n            .deletedDate(deletedDate)\n    }\n\n    @JvmStatic\n    fun createInstanceDirAndFile(instancesDir: String): File {\n        val timestamp = SimpleDateFormat(\"yyyy-MM-dd_HH-mm-ss\", Locale.ENGLISH)\n            .format(Date((100_000_000_0000L..999_999_999_9999L).random()))\n\n        val instanceDir = File(instancesDir + File.separator + RandomString.randomString(5) + \"_\" + timestamp)\n        instanceDir.mkdir()\n\n        return createTempFile(instanceDir, instanceDir.name, \".xml\").also {\n            it.writeText(RandomString.randomString(10))\n        }\n    }\n}\n"
  },
  {
    "path": "forms-test/src/main/java/org/odk/collect/formstest/InstancesRepositoryTest.kt",
    "content": "package org.odk.collect.formstest\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.hamcrest.Matchers.notNullValue\nimport org.junit.Assert.assertThrows\nimport org.junit.Test\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.forms.instances.Instance\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.formstest.InstanceUtils.buildInstance\nimport java.io.File\nimport java.util.function.Supplier\n\nabstract class InstancesRepositoryTest {\n    abstract fun buildSubject(): InstancesRepository\n\n    abstract fun buildSubject(clock: Supplier<Long>): InstancesRepository\n\n    abstract val instancesDir: File\n\n    @Test\n    fun allNotDeleted_returnsUndeletedInstances() {\n        val instancesRepository = buildSubject()\n\n        instancesRepository.save(\n            buildInstance(\"deleted\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .deletedDate(System.currentTimeMillis())\n                .build()\n        )\n        instancesRepository.save(\n            buildInstance(\"undeleted\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n\n        val allNotDeleted = instancesRepository.allNotDeleted\n        assertThat(allNotDeleted.size, equalTo(1))\n        assertThat(allNotDeleted[0].formId, equalTo(\"undeleted\"))\n    }\n\n    @Test\n    fun allByStatus_withOneStatus_returnsMatchingInstances() {\n        val instancesRepository = buildSubject()\n\n        instancesRepository.save(\n            buildInstance(\"incomplete\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n        instancesRepository.save(\n            buildInstance(\"incomplete\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        instancesRepository.save(\n            buildInstance(\"complete\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n        instancesRepository.save(\n            buildInstance(\"complete\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n\n        val incomplete = instancesRepository.getAllByStatus(Instance.STATUS_INCOMPLETE)\n        assertThat(incomplete.size, equalTo(2))\n        assertThat(incomplete[0].formId, equalTo(\"incomplete\"))\n        assertThat(incomplete[1].status, equalTo(\"incomplete\"))\n\n        // Check corresponding count method is also correct\n        assertThat(\n            instancesRepository.getCountByStatus(Instance.STATUS_INCOMPLETE),\n            equalTo(2)\n        )\n    }\n\n    @Test\n    fun allByStatus_withMultipleStatus_returnsMatchingInstances() {\n        val instancesRepository = buildSubject()\n\n        instancesRepository.save(\n            buildInstance(\"incomplete\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n        instancesRepository.save(\n            buildInstance(\"incomplete\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_INCOMPLETE)\n                .build()\n        )\n\n        instancesRepository.save(\n            buildInstance(\"complete\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n        instancesRepository.save(\n            buildInstance(\"complete\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n\n        instancesRepository.save(\n            buildInstance(\"submitted\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_SUBMITTED)\n                .build()\n        )\n        instancesRepository.save(\n            buildInstance(\"submitted\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_SUBMITTED)\n                .build()\n        )\n\n        val incomplete =\n            instancesRepository.getAllByStatus(\n                Instance.STATUS_INCOMPLETE,\n                Instance.STATUS_SUBMITTED\n            )\n        assertThat(incomplete.size, equalTo(4))\n        assertThat(incomplete[0].formId, not(\"complete\"))\n        assertThat(incomplete[1].formId, not(\"complete\"))\n        assertThat(incomplete[2].formId, not(\"complete\"))\n        assertThat(incomplete[3].status, not(\"complete\"))\n\n        // Check corresponding count method is also correct\n        assertThat(\n            instancesRepository.getCountByStatus(Instance.STATUS_INCOMPLETE, Instance.STATUS_SUBMITTED),\n            equalTo(4)\n        )\n    }\n\n    @Test\n    fun allByFormId_includesAllVersionsForFormId() {\n        val instancesRepository = buildSubject()\n\n        instancesRepository.save(\n            buildInstance(\"formid\", \"1\", instancesDir.absolutePath).build()\n        )\n        instancesRepository.save(\n            buildInstance(\n                \"formid\",\n                \"2\",\n                \"display\",\n                Instance.STATUS_COMPLETE,\n                null,\n                instancesDir.absolutePath\n            ).build()\n        )\n        instancesRepository.save(\n            buildInstance(\"formid\", \"3\", instancesDir.absolutePath).build()\n        )\n        instancesRepository.save(\n            buildInstance(\n                \"formid\",\n                \"4\",\n                \"display\",\n                Instance.STATUS_COMPLETE,\n                System.currentTimeMillis(),\n                instancesDir.absolutePath\n            ).build()\n        )\n        instancesRepository.save(\n            buildInstance(\n                \"formid2\",\n                \"1\",\n                \"display\",\n                Instance.STATUS_COMPLETE,\n                null,\n                instancesDir.absolutePath\n            ).build()\n        )\n\n        val instances = instancesRepository.getAllByFormId(\"formid\")\n        assertThat(instances.size, equalTo(4))\n    }\n\n    @Test\n    fun allByFormIdAndVersionNotDeleted_excludesDeleted() {\n        val instancesRepository = buildSubject()\n\n        instancesRepository.save(\n            buildInstance(\"formid\", \"1\", instancesDir.absolutePath).build()\n        )\n        instancesRepository.save(\n            buildInstance(\n                \"formid\",\n                \"1\",\n                \"display\",\n                Instance.STATUS_COMPLETE,\n                null,\n                instancesDir.absolutePath\n            )\n                .build()\n        )\n        instancesRepository.save(\n            buildInstance(\"formid\", \"1\", instancesDir.absolutePath).build()\n        )\n        instancesRepository.save(\n            buildInstance(\n                \"formid\",\n                \"1\",\n                \"display\",\n                Instance.STATUS_COMPLETE,\n                System.currentTimeMillis(),\n                instancesDir.absolutePath\n            )\n                .build()\n        )\n        instancesRepository.save(\n            buildInstance(\n                \"formid2\",\n                \"1\",\n                \"display\",\n                Instance.STATUS_COMPLETE,\n                null,\n                instancesDir.absolutePath\n            )\n                .build()\n        )\n\n        val instances = instancesRepository.getAllNotDeletedByFormIdAndVersion(\"formid\", \"1\")\n        assertThat(instances.size, equalTo(3))\n    }\n\n    @Test\n    fun deleteAll_deletesAllInstances() {\n        val instancesRepository = buildSubject()\n\n        instancesRepository.save(buildInstance(\"formid\", \"1\", instancesDir.absolutePath).build())\n        instancesRepository.save(buildInstance(\"formid\", \"1\", instancesDir.absolutePath).build())\n\n        instancesRepository.deleteAll()\n        assertThat(instancesRepository.all.size, equalTo(0))\n    }\n\n    @Test\n    fun deleteAll_deletesInstanceFiles() {\n        val instancesRepository = buildSubject()\n\n        val instance1 = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            ).build()\n        )\n        val instance2 = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            ).build()\n        )\n\n        instancesRepository.deleteAll()\n        assertThat(File(instance1.instanceFilePath).exists(), equalTo(false))\n        assertThat(File(instance2.instanceFilePath).exists(), equalTo(false))\n    }\n\n    @Test\n    fun save_addsUniqueId() {\n        val instancesRepository = buildSubject()\n\n        instancesRepository.save(buildInstance(\"formid\", \"1\", instancesDir.absolutePath).build())\n        instancesRepository.save(buildInstance(\"formid\", \"1\", instancesDir.absolutePath).build())\n\n        val id1 = instancesRepository.all[0].dbId\n        val id2 = instancesRepository.all[1].dbId\n        assertThat(id1, notNullValue())\n        assertThat(id2, notNullValue())\n        assertThat(id1, not(equalTo(id2)))\n    }\n\n    @Test\n    fun save_returnsInstanceWithId() {\n        val instancesRepository = buildSubject()\n\n        val instance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            ).build()\n        )\n        assertThat(instancesRepository[instance.dbId], equalTo(instance))\n    }\n\n    @Test\n    fun save_whenInstanceHasId_updatesExisting() {\n        val instancesRepository = buildSubject()\n\n        val originalInstance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            )\n                .displayName(\"Blah\")\n                .build()\n        )\n\n        instancesRepository.save(\n            Instance.Builder(originalInstance)\n                .displayName(\"A different blah\")\n                .build()\n        )\n\n        assertThat(\n            instancesRepository[originalInstance.dbId]!!.displayName,\n            equalTo(\"A different blah\")\n        )\n    }\n\n    @Test\n    fun save_whenInstanceHasId_updatesLastStatusChangeDate() {\n        val clock = mock<Supplier<Long>>()\n        whenever(clock.get()).thenReturn(123L)\n\n        val instancesRepository = buildSubject(clock)\n\n        val instance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            ).build()\n        )\n\n        instancesRepository.save(instance)\n        assertThat(\n            instancesRepository[instance.dbId]!!.lastStatusChangeDate, equalTo(123L)\n        )\n    }\n\n    @Test\n    fun save_whenStatusIsFinalized_populatesFinalizationDate() {\n        val clock = mock<Supplier<Long>>()\n        whenever(clock.get()).thenReturn(123L)\n\n        val instancesRepository = buildSubject(clock)\n\n        val instance = instancesRepository.save(\n            buildInstance(\"formid\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n        assertThat(\n            instancesRepository[instance.dbId]!!.finalizationDate,\n            equalTo(123L)\n        )\n    }\n\n    @Test\n    fun save_whenStatusIsFinalizedAndFinalizationDateIsAlreadyPopulated_doesNotUpdateFinalizationDate() {\n        val clock = mock<Supplier<Long>>()\n        whenever(clock.get()).thenReturn(123L)\n\n        val instancesRepository = buildSubject(clock)\n\n        val instance = instancesRepository.save(\n            buildInstance(\"formid\", \"1\", instancesDir.absolutePath)\n                .status(Instance.STATUS_COMPLETE)\n                .build()\n        )\n        whenever(clock.get()).thenReturn(456L)\n        instancesRepository.save(instance)\n        assertThat(\n            instancesRepository[instance.dbId]!!.finalizationDate,\n            equalTo(123L)\n        )\n    }\n\n    @Test\n    fun save_whenStatusIsNotFinalized_doesNotPopulateFinalizationDate() {\n        val clock = mock<Supplier<Long>>()\n        whenever(clock.get()).thenReturn(123L)\n\n        val instancesRepository = buildSubject(clock)\n\n        val nonFinalizedStatuses = listOf(\n            Instance.STATUS_VALID,\n            Instance.STATUS_INVALID,\n            Instance.STATUS_NEW_EDIT,\n            Instance.STATUS_SUBMISSION_FAILED,\n            Instance.STATUS_SUBMITTED\n        )\n\n        for (status in nonFinalizedStatuses) {\n            val instance = instancesRepository.save(\n                buildInstance(\"formid\", \"1\", instancesDir.absolutePath)\n                    .status(status)\n                    .build()\n            )\n\n            assertThat(\n                instancesRepository[instance.dbId]!!.finalizationDate,\n                equalTo(null)\n            )\n        }\n    }\n\n    @Test\n    fun save_whenStatusIsNull_usesIncomplete() {\n        val instancesRepository = buildSubject()\n\n        val instance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            )\n                .status(null)\n                .build()\n        )\n        assertThat(\n            instancesRepository[instance.dbId]!!.status, equalTo(Instance.STATUS_INCOMPLETE)\n        )\n    }\n\n    @Test\n    fun save_whenLastStatusChangeDateIsNull_setsIt() {\n        val instancesRepository = buildSubject()\n\n        val instance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            )\n                .lastStatusChangeDate(null)\n                .build()\n        )\n        assertThat(\n            instancesRepository[instance.dbId]!!.lastStatusChangeDate,\n            notNullValue()\n        )\n    }\n\n    @Test\n    fun save_whenInstanceHasDeletedDate_doesNotUpdateLastChangesStatusDate() {\n        val clock = mock<Supplier<Long>>()\n        whenever(clock.get()).thenReturn(123L)\n\n        val instancesRepository = buildSubject(clock)\n\n        val originalInstance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            ).build()\n        )\n        val originalInstanceDbId = originalInstance.dbId\n\n        whenever(clock.get()).thenReturn(456L)\n        instancesRepository.deleteWithLogging(originalInstanceDbId)\n        instancesRepository.save(instancesRepository[originalInstanceDbId])\n\n        assertThat(\n            instancesRepository[originalInstanceDbId]!!.lastStatusChangeDate, equalTo(123L)\n        )\n    }\n\n    @Test\n    fun deleteWithLogging_setsDeletedDate() {\n        val instancesRepository = buildSubject()\n        val instance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            ).build()\n        )\n\n        instancesRepository.deleteWithLogging(instance.dbId)\n        assertThat(\n            instancesRepository[instance.dbId]!!.deletedDate, notNullValue()\n        )\n    }\n\n    @Test\n    fun deleteWithLogging_deletesInstanceDir() {\n        val instancesRepository = buildSubject()\n        val instance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            ).build()\n        )\n\n        val instanceDir = File(instance.instanceFilePath).parentFile\n        assertThat(instanceDir.exists(), equalTo(true))\n        assertThat(instanceDir.isDirectory, equalTo(true))\n\n        instancesRepository.deleteWithLogging(instance.dbId)\n        assertThat(instanceDir.exists(), equalTo(false))\n    }\n\n    @Test\n    fun deleteWithLogging_clearsGeometryData() {\n        val instancesRepository = buildSubject()\n        val instance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            )\n                .geometry(\"blah\")\n                .geometryType(\"blah\")\n                .build()\n        )\n\n        instancesRepository.deleteWithLogging(instance.dbId)\n        assertThat(\n            instancesRepository[instance.dbId]!!.geometry, equalTo(null)\n        )\n        assertThat(\n            instancesRepository[instance.dbId]!!.geometryType, equalTo(null)\n        )\n    }\n\n    @Test\n    fun delete_deletesInstance() {\n        val instancesRepository = buildSubject()\n        val instance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            ).build()\n        )\n\n        instancesRepository.delete(instance.dbId)\n        assertThat(instancesRepository.all.size, equalTo(0))\n    }\n\n    @Test\n    fun delete_deletesInstanceDir() {\n        val instancesRepository = buildSubject()\n        val instance = instancesRepository.save(\n            buildInstance(\n                \"formid\", \"1\",\n                instancesDir.absolutePath\n            ).build()\n        )\n\n        // The repo assumes the parent of the file also contains other instance files\n        val instanceDir = File(instance.instanceFilePath).parentFile\n        assertThat(instanceDir.exists(), equalTo(true))\n        assertThat(instanceDir.isDirectory, equalTo(true))\n\n        instancesRepository.delete(instance.dbId)\n        assertThat(instanceDir.exists(), equalTo(false))\n    }\n\n    @Test\n    fun delete_failsWhenDeletingInstanceWithEdits() {\n        val instancesRepository = buildSubject()\n\n        val originalInstance = InstanceFixtures.instance(displayName = \"Form1\", instancesDir = instancesDir)\n        val originalInstanceDbId = instancesRepository.save(originalInstance).dbId\n\n        val editedInstance = InstanceFixtures.instance(displayName = \"Form1\", instancesDir = instancesDir, editOf = originalInstanceDbId, editNumber = 1)\n        instancesRepository.save(editedInstance)\n\n        assertThrows(InstancesRepository.IntegrityException::class.java) {\n            instancesRepository.delete(originalInstanceDbId)\n        }\n    }\n\n    @Test\n    fun save_failsWhenEditOfPointsAtItsOwnDbId() {\n        val instancesRepository = buildSubject()\n\n        val originalInstance = InstanceFixtures.instance(displayName = \"Form1\", instancesDir = instancesDir)\n        val originalInstanceDbId = instancesRepository.save(originalInstance).dbId\n\n        val editedInstance = InstanceFixtures.instance(displayName = \"Form1\", instancesDir = instancesDir, editOf = originalInstanceDbId + 1, editNumber = 1)\n\n        assertThrows(InstancesRepository.IntegrityException::class.java) {\n            instancesRepository.save(editedInstance)\n        }\n    }\n\n    @Test\n    fun save_failsWhenEditOfPointsAtNonExistingDbId() {\n        val instancesRepository = buildSubject()\n\n        val originalInstance = InstanceFixtures.instance(displayName = \"Form1\", instancesDir = instancesDir)\n        val originalInstanceDbId = instancesRepository.save(originalInstance).dbId\n\n        val editedInstance = InstanceFixtures.instance(displayName = \"Form1\", instancesDir = instancesDir, editOf = originalInstanceDbId + 100, editNumber = 1)\n\n        assertThrows(InstancesRepository.IntegrityException::class.java) {\n            instancesRepository.save(editedInstance)\n        }\n    }\n\n    @Test\n    fun save_failsWhenEditOfIsNullButEditNumberIsNotNull() {\n        val instancesRepository = buildSubject()\n\n        val originalInstance = InstanceFixtures.instance(displayName = \"Form1\", instancesDir = instancesDir)\n        instancesRepository.save(originalInstance)\n\n        val editedInstance = InstanceFixtures.instance(displayName = \"Form1\", instancesDir = instancesDir, editNumber = 1)\n\n        assertThrows(InstancesRepository.IntegrityException::class.java) {\n            instancesRepository.save(editedInstance)\n        }\n    }\n\n    @Test\n    fun save_failsWhenEditOfIsNotNullButEditNumberIsNull() {\n        val instancesRepository = buildSubject()\n\n        val originalInstance = InstanceFixtures.instance(displayName = \"Form1\", instancesDir = instancesDir)\n        val originalInstanceDbId = instancesRepository.save(originalInstance).dbId\n\n        val editedInstance = InstanceFixtures.instance(displayName = \"Form1\", instancesDir = instancesDir, editOf = originalInstanceDbId)\n\n        assertThrows(InstancesRepository.IntegrityException::class.java) {\n            instancesRepository.save(editedInstance)\n        }\n    }\n}\n"
  },
  {
    "path": "forms-test/src/main/java/org/odk/collect/formstest/SavepointsRepositoryTest.kt",
    "content": "package org.odk.collect.formstest\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.contains\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.forms.savepoints.Savepoint\nimport org.odk.collect.forms.savepoints.SavepointsRepository\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\nabstract class SavepointsRepositoryTest {\n    private val cacheDirPath = TempFiles.createTempDir().absolutePath\n    private val instancesDirPath = TempFiles.createTempDir().absolutePath\n\n    abstract fun buildSubject(cacheDirPath: String, instancesDirPath: String): SavepointsRepository\n\n    private fun getSavepointFile(relativeFilePath: String): File {\n        return File(cacheDirPath, relativeFilePath)\n    }\n\n    private fun getInstanceFile(relativeFilePath: String): File {\n        return File(instancesDirPath, relativeFilePath)\n    }\n\n    @Test\n    fun `get returns null if the database is empty`() {\n        val savepointsRepository = buildSubject(cacheDirPath, instancesDirPath)\n\n        assertThat(savepointsRepository.get(1, null), equalTo(null))\n    }\n\n    @Test\n    fun `get returns null if there is no savepoint for give formDbId and instanceDbId`() {\n        val savepointsRepository = buildSubject(cacheDirPath, instancesDirPath)\n\n        savepointsRepository.save(Savepoint(1, 1, getSavepointFile(\"foo\").absolutePath, getInstanceFile(\"foo\").absolutePath))\n\n        assertThat(savepointsRepository.get(1, 2), equalTo(null))\n    }\n\n    @Test\n    fun `get returns savepoint if one with given formDbId and instanceDbId exists`() {\n        val savepointsRepository = buildSubject(cacheDirPath, instancesDirPath)\n\n        val savepoint1 = Savepoint(1, 1, getSavepointFile(\"foo\").absolutePath, getInstanceFile(\"foo\").absolutePath)\n        val savepoint2 = Savepoint(1, 2, getSavepointFile(\"bar\").absolutePath, getInstanceFile(\"bar\").absolutePath)\n        savepointsRepository.save(savepoint1)\n        savepointsRepository.save(savepoint2)\n\n        assertThat(savepointsRepository.get(1, 1), equalTo(savepoint1))\n    }\n\n    @Test\n    fun `getAll returns an empty list if the database is empty`() {\n        val savepointsRepository = buildSubject(cacheDirPath, instancesDirPath)\n\n        assertThat(savepointsRepository.getAll().isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun `getAll returns all savepoints stored in the database`() {\n        val savepointsRepository = buildSubject(cacheDirPath, instancesDirPath)\n\n        val savepoint1 = Savepoint(1, null, getSavepointFile(\"foo\").absolutePath, getInstanceFile(\"foo\").absolutePath)\n        val savepoint2 = Savepoint(1, 1, getSavepointFile(\"bar\").absolutePath, getInstanceFile(\"bar\").absolutePath)\n        savepointsRepository.save(savepoint1)\n        savepointsRepository.save(savepoint2)\n\n        assertThat(savepointsRepository.getAll(), contains(savepoint1, savepoint2))\n    }\n\n    @Test\n    fun `save does not save two savepoints with the same formDbId and instanceDbId`() {\n        val savepointsRepository = buildSubject(cacheDirPath, instancesDirPath)\n\n        val savepoint1 = Savepoint(1, null, getSavepointFile(\"foo\").absolutePath, getInstanceFile(\"foo\").absolutePath)\n        savepointsRepository.save(savepoint1)\n        savepointsRepository.save(Savepoint(1, null, getSavepointFile(\"bar2\").absolutePath, getInstanceFile(\"bar2\").absolutePath))\n\n        assertThat(savepointsRepository.getAll(), contains(savepoint1))\n    }\n\n    @Test\n    fun `delete removes savepoint from the database and its savepoint file for given formDbId and instanceDbId if instanceDbId is null`() {\n        val savepointsRepository = buildSubject(cacheDirPath, instancesDirPath)\n\n        val savepointFile1 = getSavepointFile(\"foo\")\n        savepointFile1.createNewFile()\n        val savepoint1 = Savepoint(1, null, savepointFile1.absolutePath, getInstanceFile(\"foo\").absolutePath)\n\n        val savepointFile2 = getSavepointFile(\"bar\")\n        savepointFile2.createNewFile()\n        val savepoint2 = Savepoint(2, null, savepointFile2.absolutePath, getInstanceFile(\"bar\").absolutePath)\n\n        savepointsRepository.save(savepoint1)\n        savepointsRepository.save(savepoint2)\n\n        savepointsRepository.delete(savepoint1.formDbId, savepoint1.instanceDbId)\n\n        assertThat(savepointsRepository.getAll(), contains(savepoint2))\n        assertThat(savepointFile1.exists(), equalTo(false))\n        assertThat(savepointFile2.exists(), equalTo(true))\n    }\n\n    @Test\n    fun `delete removes savepoint from the database and its savepoint file for given formDbId and instanceDbId if instanceDbId is not null`() {\n        val savepointsRepository = buildSubject(cacheDirPath, instancesDirPath)\n\n        val savepointFile1 = getSavepointFile(\"foo\")\n        savepointFile1.createNewFile()\n        val savepoint1 = Savepoint(1, 1, savepointFile1.absolutePath, getInstanceFile(\"foo\").absolutePath)\n\n        val savepointFile2 = getSavepointFile(\"bar\")\n        savepointFile2.createNewFile()\n        val savepoint2 = Savepoint(2, 1, savepointFile2.absolutePath, getInstanceFile(\"bar\").absolutePath)\n\n        savepointsRepository.save(savepoint1)\n        savepointsRepository.save(savepoint2)\n\n        savepointsRepository.delete(savepoint1.formDbId, savepoint1.instanceDbId)\n\n        assertThat(savepointsRepository.getAll().size, equalTo(1))\n        assertThat(savepointsRepository.getAll()[0].formDbId, equalTo(savepoint2.formDbId))\n        assertThat(savepointFile1.exists(), equalTo(false))\n        assertThat(savepointFile2.exists(), equalTo(true))\n    }\n\n    @Test\n    fun `deleteAll removes all savepoints and all savepoint files`() {\n        val savepointsRepository = buildSubject(cacheDirPath, instancesDirPath)\n\n        val savepointFile1 = getSavepointFile(\"foo\")\n        savepointFile1.createNewFile()\n        val savepoint1 = Savepoint(1, null, savepointFile1.absolutePath, \"\")\n\n        val savepointFile2 = getSavepointFile(\"bar\")\n        savepointFile2.createNewFile()\n        val savepoint2 = Savepoint(2, 1, savepointFile2.absolutePath, \"\")\n\n        savepointsRepository.save(savepoint1)\n        savepointsRepository.save(savepoint2)\n\n        savepointsRepository.deleteAll()\n\n        assertThat(savepointsRepository.getAll().isEmpty(), equalTo(true))\n        assertThat(savepointFile1.exists(), equalTo(false))\n        assertThat(savepointFile2.exists(), equalTo(false))\n    }\n}\n"
  },
  {
    "path": "forms-test/src/test/java/org/odk/collect/formstest/InMemFormsRepositoryTest.java",
    "content": "package org.odk.collect.formstest;\n\nimport org.junit.Before;\nimport org.odk.collect.forms.FormsRepository;\nimport org.odk.collect.shared.TempFiles;\n\nimport java.util.function.Supplier;\n\npublic class InMemFormsRepositoryTest extends FormsRepositoryTest {\n\n    private String tempDirectory;\n\n    @Before\n    public void setup() {\n        tempDirectory = TempFiles.createTempDir().getAbsolutePath();\n    }\n\n    @Override\n    public FormsRepository buildSubject() {\n        return new InMemFormsRepository(savepointsRepository);\n    }\n\n    @Override\n    public FormsRepository buildSubject(Supplier<Long> clock) {\n        return new InMemFormsRepository(clock, savepointsRepository);\n    }\n\n    @Override\n    public String getFormFilesPath() {\n        return tempDirectory;\n    }\n}\n"
  },
  {
    "path": "forms-test/src/test/java/org/odk/collect/formstest/InMemInstancesRepositoryTest.kt",
    "content": "package org.odk.collect.formstest\n\nimport org.odk.collect.forms.instances.InstancesRepository\nimport org.odk.collect.shared.TempFiles.createTempDir\nimport java.io.File\nimport java.util.function.Supplier\n\nclass InMemInstancesRepositoryTest : InstancesRepositoryTest() {\n    override val instancesDir: File = createTempDir()\n\n    override fun buildSubject(): InstancesRepository {\n        return InMemInstancesRepository { System.currentTimeMillis() }\n    }\n\n    override fun buildSubject(clock: Supplier<Long>): InstancesRepository {\n        return InMemInstancesRepository(clock)\n    }\n}\n"
  },
  {
    "path": "forms-test/src/test/java/org/odk/collect/formstest/InMemSavepointsRepositoryTest.kt",
    "content": "package org.odk.collect.formstest\n\nimport org.odk.collect.forms.savepoints.SavepointsRepository\n\nclass InMemSavepointsRepositoryTest : SavepointsRepositoryTest() {\n    override fun buildSubject(cacheDirPath: String, instancesDirPath: String): SavepointsRepository {\n        return InMemSavepointsRepository()\n    }\n}\n"
  },
  {
    "path": "fragments-test/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "fragments-test/README.md",
    "content": "# fragmentstest\n\nIncludes helpers (arguably missing from `androidx.fragment:fragment-testing`) for writing unit tests for Fragments. This includes (and extends) `androidx.fragment:fragment-testing`. `fragmentstest` needs to be included as a `debugImplementation` dependency (because of the underlying Android testing package):\n\n```gradle\ndebugImplementation project(':fragmentstest')\n```\n"
  },
  {
    "path": "fragments-test/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    namespace = \"org.odk.collect.fragmentstest\"\n}\n\ndependencies {\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxTestEspressoCore)\n    implementation(libs.androidMaterial)\n\n    //noinspection FragmentGradleConfiguration\n    debugApi(libs.androidxFragmentTesting) {\n        exclude(group = \"androidx.test\", module = \"monitor\") // fixes issue https://github.com/android/android-test/issues/731\n    }\n}\n"
  },
  {
    "path": "fragments-test/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "fragments-test/src/main/java/org/odk/collect/fragmentstest/FragmentScenarioLauncherRule.kt",
    "content": "package org.odk.collect.fragmentstest\n\nimport android.os.Bundle\nimport androidx.annotation.StyleRes\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentFactory\nimport androidx.fragment.app.testing.FragmentScenario\nimport androidx.lifecycle.Lifecycle\nimport com.google.android.material.R\nimport org.junit.rules.ExternalResource\nimport kotlin.reflect.KClass\n\n/**\n * Alternative to [FragmentScenario] that allows tests to do work before launching the [Fragment]\n * (like switch out dependencies, construct intents etc) and also allows creation of multiple\n * scenarios in a test.\n */\nclass FragmentScenarioLauncherRule @JvmOverloads constructor(\n    private val defaultFactory: FragmentFactory? = null,\n    @StyleRes private val defaultThemeResId: Int = R.style.Theme_Material3_Light\n) : ExternalResource() {\n\n    private val scenarios = mutableListOf<FragmentScenario<*>>()\n\n    @JvmOverloads\n    fun <F : Fragment> launchInContainer(\n        fragmentClass: Class<F>,\n        args: Bundle? = null,\n        @StyleRes themResId: Int? = defaultThemeResId,\n        factory: FragmentFactory? = defaultFactory,\n        initialState: Lifecycle.State = Lifecycle.State.RESUMED\n    ): FragmentScenario<F> {\n        val scenario = if (themResId != null) {\n            FragmentScenario.launchInContainer(\n                fragmentClass = fragmentClass,\n                fragmentArgs = args,\n                themeResId = themResId,\n                factory = factory,\n                initialState = initialState\n            )\n        } else {\n            FragmentScenario.launchInContainer(\n                fragmentClass = fragmentClass,\n                fragmentArgs = args,\n                factory = factory,\n                initialState = initialState\n            )\n        }\n\n        return scenario.also {\n            scenarios.add(it)\n        }\n    }\n\n    inline fun <reified F : Fragment> launchInContainer(crossinline factory: () -> F): FragmentScenario<F> {\n        val fragmentFactory = object : FragmentFactory() {\n            override fun instantiate(\n                classLoader: ClassLoader,\n                className: String\n            ): Fragment {\n                val fragmentClass = loadFragmentClass(classLoader, className)\n\n                return if (F::class.java.isAssignableFrom(fragmentClass)) {\n                    factory()\n                } else {\n                    super.instantiate(classLoader, className)\n                }\n            }\n        }\n\n        return launchInContainer(F::class.java, factory = fragmentFactory)\n    }\n\n    @JvmOverloads\n    fun <F : Fragment> launch(\n        fragmentClass: Class<F>,\n        fragmentArgs: Bundle? = null,\n        initialState: Lifecycle.State = Lifecycle.State.RESUMED\n    ): FragmentScenario<F> {\n        val scenario = if (defaultThemeResId != null) {\n            FragmentScenario.launch(\n                fragmentClass = fragmentClass,\n                fragmentArgs = fragmentArgs,\n                themeResId = defaultThemeResId,\n                factory = defaultFactory,\n                initialState = initialState\n            )\n        } else {\n            FragmentScenario.launch(\n                fragmentClass = fragmentClass,\n                fragmentArgs = fragmentArgs,\n                factory = defaultFactory,\n                initialState = initialState\n            )\n        }\n\n        return scenario.also {\n            scenarios.add(it)\n        }\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <Child : Fragment> launchAndAssertOnChild(\n        fragment: KClass<out Fragment>,\n        args: Bundle,\n        assertion: (Child) -> Unit\n    ) {\n        launch(\n            fragment.java,\n            args\n        ).onFragment {\n            assertion(it.childFragmentManager.fragments[0] as Child)\n        }\n    }\n\n    override fun after() {\n        scenarios.forEach(FragmentScenario<*>::close)\n    }\n}\n"
  },
  {
    "path": "geo/.gitignore",
    "content": "/build"
  },
  {
    "path": "geo/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n    alias(libs.plugins.composeCompiler)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n    namespace = \"org.odk.collect.geo\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":shared\"))\n    implementation(project(\":icons\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":location\"))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":external-app\"))\n    implementation(project(\":async\"))\n    implementation(project(\":analytics\"))\n    implementation(project(\":permissions\"))\n    implementation(project(\":settings\"))\n    implementation(project(\":maps\"))\n    implementation(project(\":material\"))\n    implementation(project(\":web-page\"))\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxAppcompat)\n    implementation(libs.androidxLifecycleLivedataKtx)\n    implementation(libs.timber)\n    implementation(libs.playServicesLocation)\n    implementation(libs.androidxFragmentKtx)\n    implementation(libs.dagger)\n    ksp(libs.daggerCompiler)\n    implementation(libs.javarosa) {\n        exclude(group = \"joda-time\")\n        exclude(group = \"org.hamcrest\", module = \"hamcrest-all\")\n    }\n\n    val composeBom = platform(libs.androidxComposeBom)\n    implementation(composeBom)\n    implementation(libs.androidXComposeMaterial)\n    implementation(libs.androidXComposeMaterialIcons)\n    implementation(libs.androidXComposePreview)\n    debugImplementation(libs.androidXComposeTooling)\n\n    debugImplementation(project(\":fragments-test\"))\n\n    testImplementation(project(\":androidtest\"))\n    testImplementation(project(\":settings\"))\n    testImplementation(project(\":test-shared\"))\n    testImplementation(libs.junit)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.androidxTestEspressoCore)\n    testImplementation(libs.androidxArchCoreTesting)\n    testImplementation(libs.androidXComposeUiTestJunit4)\n    debugImplementation(libs.androidXComposeUiTestManifest)\n}\n"
  },
  {
    "path": "geo/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <application>\n        <activity\n            android:name=\".geopoint.GeoPointMapActivity\"\n            android:theme=\"@style/Theme.MaterialComponents\" />\n\n        <activity\n            android:name=\".geopoint.GeoPointActivity\"\n            android:theme=\"@style/Theme.MaterialComponents\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/Constants.kt",
    "content": "package org.odk.collect.geo\n\nobject Constants {\n    const val EXTRA_READ_ONLY = \"readOnly\"\n    const val EXTRA_DRAGGABLE_ONLY = \"draggable\"\n    const val EXTRA_RETAIN_MOCK_ACCURACY = \"retainMockAccuracy\"\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/DaggerSetup.kt",
    "content": "package org.odk.collect.geo\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.lifecycle.ViewModel\nimport dagger.BindsInstance\nimport dagger.Component\nimport dagger.Module\nimport dagger.Provides\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.geo.geopoint.GeoPointActivity\nimport org.odk.collect.geo.geopoint.GeoPointDialogFragment\nimport org.odk.collect.geo.geopoint.GeoPointMapActivity\nimport org.odk.collect.geo.geopoint.GeoPointViewModelFactory\nimport org.odk.collect.geo.geopoint.LocationTrackerGeoPointViewModel\nimport org.odk.collect.geo.geopoly.GeoPolyFragment\nimport org.odk.collect.geo.selection.SelectionMapFragment\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.location.satellites.SatelliteInfoClient\nimport org.odk.collect.location.tracker.LocationTracker\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.webpage.WebPageService\nimport javax.inject.Singleton\n\ninterface GeoDependencyComponentProvider {\n    val geoDependencyComponent: GeoDependencyComponent\n}\n\n@Component(modules = [GeoDependencyModule::class])\n@Singleton\ninterface GeoDependencyComponent {\n\n    @Component.Builder\n    interface Builder {\n\n        @BindsInstance\n        fun application(application: Application): Builder\n\n        fun geoDependencyModule(geoDependencyModule: GeoDependencyModule): Builder\n\n        fun build(): GeoDependencyComponent\n    }\n\n    fun inject(geoPointMapActivity: GeoPointMapActivity)\n    fun inject(geoPointDialogFragment: GeoPointDialogFragment)\n    fun inject(geoPointActivity: GeoPointActivity)\n    fun inject(selectionMapFragment: SelectionMapFragment)\n    fun inject(geoPolyFragment: GeoPolyFragment)\n\n    val scheduler: Scheduler\n    val locationTracker: LocationTracker\n    val satelliteInfoClient: SatelliteInfoClient\n}\n\n@Module\nopen class GeoDependencyModule {\n\n    @Provides\n    open fun context(application: Application): Context {\n        return application\n    }\n\n    @Provides\n    open fun providesMapFragmentFactory(): MapFragmentFactory {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesLocationTracker(application: Application): LocationTracker {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesLocationClient(): LocationClient {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesScheduler(): Scheduler {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesSatelliteInfoClient(context: Context): SatelliteInfoClient {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesPermissionChecker(context: Context): PermissionsChecker {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    internal open fun providesGeoPointViewModelFactory(application: Application): GeoPointViewModelFactory {\n        return object : GeoPointViewModelFactory {\n            @Suppress(\"UNCHECKED_CAST\")\n            override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                val componentProvider = application as GeoDependencyComponentProvider\n                val component = componentProvider.geoDependencyComponent\n                return LocationTrackerGeoPointViewModel(\n                    component.locationTracker,\n                    component.satelliteInfoClient,\n                    System::currentTimeMillis,\n                    component.scheduler\n                ) as T\n            }\n        }\n    }\n\n    @Provides\n    open fun providesReferenceLayerRepository(): ReferenceLayerRepository {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesSettingsProvider(): SettingsProvider {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesWebPageService(): WebPageService {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/GeoActivityUtils.kt",
    "content": "package org.odk.collect.geo\n\nimport android.Manifest\nimport android.app.Activity\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.permissions.ContextCompatPermissionChecker\n\ninternal object GeoActivityUtils {\n\n    @JvmStatic\n    fun requireLocationPermissions(activity: Activity) {\n        val permissionGranted = ContextCompatPermissionChecker(activity).isPermissionGranted(\n            Manifest.permission.ACCESS_FINE_LOCATION,\n            Manifest.permission.ACCESS_COARSE_LOCATION\n        )\n\n        if (!permissionGranted) {\n            ToastUtils.showLongToast(org.odk.collect.strings.R.string.not_granted_permission)\n            activity.finish()\n        }\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/GeoUtils.kt",
    "content": "package org.odk.collect.geo\n\nimport android.content.Context\nimport android.location.Location\nimport org.javarosa.core.model.data.GeoPointData\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.shared.strings.StringUtils.removeEnd\nimport org.odk.collect.strings.R\nimport java.text.DecimalFormat\nimport java.util.Locale\n\nobject GeoUtils {\n\n    /**\n     * Serializes a list of vertices into a string, in the format\n     * appropriate for storing as the result of the form question.\n     */\n    @JvmStatic\n    fun formatPointsResultString(points: MutableList<MapPoint>, isShape: Boolean): String? {\n        if (isShape) {\n            // Polygons are stored with a last point that duplicates the\n            // first point.  Add this extra point if it's not already present.\n            val count = points.size\n            if (count > 1 && points[0] != points[count - 1]) {\n                points.add(points[0])\n            }\n        }\n        val result = StringBuilder()\n        for (point in points) {\n            // TODO(ping): Remove excess precision when we're ready for the output to change.\n            result.append(\n                String.format(\n                    Locale.US, \"%s %s %s %s;\",\n                    point.latitude.toString(), point.longitude.toString(),\n                    point.altitude.toString(), point.accuracy.toFloat().toString()\n                )\n            )\n        }\n\n        return removeEnd(result.toString().trim(), \";\")\n    }\n\n    @JvmStatic\n    fun formatLocationResultString(location: Location): String {\n        return formatLocationResultString(\n            org.odk.collect.location.Location(\n                location.latitude,\n                location.longitude,\n                location.altitude,\n                location.accuracy\n            )\n        )\n    }\n\n    fun formatLocationResultString(location: org.odk.collect.location.Location): String {\n        return String.format(\n            \"%s %s %s %s\", location.latitude, location.longitude,\n            location.altitude, location.accuracy\n        )\n    }\n\n    fun formatAccuracy(context: Context, accuracy: Float): String {\n        val formattedValue = DecimalFormat(\"#.##\").format(accuracy.toDouble())\n        return context.getString(R.string.accuracy_m, formattedValue)\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun parseGeometryPoint(answer: String?, strict: Boolean = false): DoubleArray? {\n        if (!answer.isNullOrEmpty()) {\n            val sa = answer.trim().split(\" \")\n            return try {\n                doubleArrayOf(\n                    sa[0].toDouble(),\n                    if (sa.size > 1) sa[1].toDouble() else 0.0,\n                    if (sa.size > 2) sa[2].toDouble() else 0.0,\n                    if (sa.size > 3) sa[3].toDouble() else 0.0\n                )\n            } catch (_: Throwable) {\n                if (strict) {\n                    throw IllegalArgumentException()\n                } else {\n                    null\n                }\n            }\n        } else {\n            return null\n        }\n    }\n\n    @JvmStatic\n    fun GeoPointData.toMapPoint(): MapPoint {\n        return MapPoint(this.getPart(0), this.getPart(1), this.getPart(2), this.getPart(3))\n    }\n\n    fun org.odk.collect.location.Location.toMapPoint(): MapPoint {\n        return MapPoint(this.latitude, this.longitude, this.altitude, this.accuracy.toDouble())\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/analytics/AnalyticsEvents.kt",
    "content": "package org.odk.collect.geo.analytics\n\ninternal object AnalyticsEvents {\n\n    /**\n     * Compare different ways of saving geopoints: are users saving without looking at accuracy,\n     * are they waiting for the threshold autosave etc. Each event should include `accuracy` param\n     * with either `unacceptable` (over 100m), `poor` (over 10m) or `acceptable` (less than 10m).\n     */\n    const val SAVE_POINT_AUTO = \"SavePointAuto\"\n    const val SAVE_POINT_MANUAL = \"SavePointManual\"\n    const val SAVE_POINT_IMMEDIATE = \"SavePointImmediate\"\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoint/AccuracyProgressView.kt",
    "content": "package org.odk.collect.geo.geopoint\n\nimport android.content.Context\nimport android.graphics.drawable.ColorDrawable\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport org.odk.collect.androidshared.system.ContextExt.getThemeAttributeValue\nimport org.odk.collect.geo.GeoUtils.formatAccuracy\nimport org.odk.collect.geo.databinding.AccuracyProgressLayoutBinding\n\ninternal class AccuracyProgressView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyle: Int = 0\n) : FrameLayout(context, attrs, defStyle) {\n\n    val binding = AccuracyProgressLayoutBinding.inflate(LayoutInflater.from(context), this, true)\n\n    var accuracy: LocationAccuracy? = null\n        set(value) {\n            field = value\n            if (value != null) {\n                render(value)\n            }\n        }\n\n    private fun render(accuracy: LocationAccuracy) {\n        val (backgroundColor, textColor) = getBackgroundAndTextColor(accuracy)\n        binding.root.background = ColorDrawable(backgroundColor)\n        binding.title.setTextColor(textColor)\n        binding.text.setTextColor(textColor)\n        binding.currentAccuracy.setTextColor(textColor)\n        binding.strength.setIndicatorColor(textColor)\n\n        binding.currentAccuracy.text = formatAccuracy(context, accuracy.value)\n\n        val (text, strength) = getTextAndStrength(accuracy)\n        binding.text.setText(text)\n\n        binding.strength.setProgress(strength, true)\n    }\n\n    private fun getBackgroundAndTextColor(accuracy: LocationAccuracy): Pair<Int, Int> {\n        return if (accuracy is LocationAccuracy.Unacceptable) {\n            Pair(\n                getThemeAttributeValue(context, androidx.appcompat.R.attr.colorError),\n                getThemeAttributeValue(context, com.google.android.material.R.attr.colorOnError)\n            )\n        } else {\n            Pair(\n                getThemeAttributeValue(context, androidx.appcompat.R.attr.colorPrimary),\n                getThemeAttributeValue(context, com.google.android.material.R.attr.colorOnPrimary)\n            )\n        }\n    }\n\n    private fun getTextAndStrength(accuracy: LocationAccuracy): Pair<Int, Int> {\n        return when (accuracy) {\n            is LocationAccuracy.Improving -> Pair(org.odk.collect.strings.R.string.improving_accuracy, 80)\n            is LocationAccuracy.Poor -> Pair(org.odk.collect.strings.R.string.poor_accuracy, 60)\n            is LocationAccuracy.Unacceptable -> Pair(org.odk.collect.strings.R.string.unacceptable_accuracy, 40)\n        }\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoint/AccuracyStatusView.kt",
    "content": "package org.odk.collect.geo.geopoint\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport androidx.core.content.withStyledAttributes\nimport androidx.core.view.isGone\nimport org.odk.collect.geo.GeoUtils\nimport org.odk.collect.geo.databinding.AccuracyStatusLayoutBinding\nimport org.odk.collect.strings.R\n\ninternal class AccuracyStatusView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyle: Int = 0\n) : FrameLayout(context, attrs, defStyle) {\n\n    val binding =\n        AccuracyStatusLayoutBinding.inflate(LayoutInflater.from(context), this, true)\n\n    var title: String = \"\"\n        set(value) {\n            field = value\n            render()\n        }\n\n    var accuracy: LocationAccuracy? = null\n        set(value) {\n            field = value\n            render()\n        }\n\n    init {\n        context.withStyledAttributes(attrs, org.odk.collect.geo.R.styleable.AccuracyStatusView) {\n            title = getString(org.odk.collect.geo.R.styleable.AccuracyStatusView_title) ?: \"\"\n        }\n    }\n\n    private fun render() {\n        binding.title.text = title\n        binding.title.isGone = title.isBlank()\n\n        accuracy?.let {\n            binding.locationStatus.text = formatLocationStatus(it)\n        }\n    }\n\n    private fun formatLocationStatus(accuracy: LocationAccuracy): String {\n        val formattedAccuracy = GeoUtils.formatAccuracy(context, accuracy.value)\n        return when (accuracy) {\n            is LocationAccuracy.Unacceptable -> {\n                context.getString(R.string.location_accuracy_unacceptable, formattedAccuracy)\n            }\n\n            else -> context.getString(R.string.location_accuracy, formattedAccuracy)\n        }\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointActivity.kt",
    "content": "package org.odk.collect.geo.geopoint\n\nimport android.os.Bundle\nimport androidx.lifecycle.ViewModelProvider\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.externalapp.ExternalAppUtils\nimport org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY\nimport org.odk.collect.geo.GeoDependencyComponentProvider\nimport org.odk.collect.geo.GeoUtils\nimport org.odk.collect.geo.analytics.AnalyticsEvents\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport javax.inject.Inject\n\nclass GeoPointActivity : LocalizedActivity(), GeoPointDialogFragment.Listener {\n\n    @Inject\n    internal lateinit var geoPointViewModelFactory: GeoPointViewModelFactory\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        (application as GeoDependencyComponentProvider).geoDependencyComponent.inject(this)\n\n        val viewModel =\n            ViewModelProvider(this, geoPointViewModelFactory).get(GeoPointViewModel::class.java)\n\n        viewModel.acceptedLocation.observe(this) {\n            if (it != null) {\n                Analytics.log(AnalyticsEvents.SAVE_POINT_AUTO)\n                ExternalAppUtils.returnSingleValue(this, GeoUtils.formatLocationResultString(it))\n            }\n        }\n\n        DialogFragmentUtils.showIfNotShowing(\n            GeoPointDialogFragment::class.java,\n            supportFragmentManager\n        )\n\n        viewModel.start(\n            retainMockAccuracy = this.intent.getBooleanExtra(\n                EXTRA_RETAIN_MOCK_ACCURACY,\n                false\n            ),\n            accuracyThreshold = (this.intent.extras?.get(EXTRA_ACCURACY_THRESHOLD) as? Float),\n            unacceptableAccuracyThreshold = (\n                this.intent.extras?.get(\n                    EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD\n                ) as? Float\n            )\n        )\n    }\n\n    override fun onCancel() {\n        finish()\n    }\n\n    companion object {\n        const val EXTRA_ACCURACY_THRESHOLD = \"extra_accuracy_threshold\"\n        const val EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD = \"extra_unacceptable_accuracy_threshold\"\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointDialogFragment.kt",
    "content": "package org.odk.collect.geo.geopoint\n\nimport android.app.Dialog\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport android.text.format.DateUtils\nimport android.view.LayoutInflater\nimport androidx.activity.ComponentDialog\nimport androidx.activity.OnBackPressedCallback\nimport androidx.fragment.app.DialogFragment\nimport androidx.lifecycle.ViewModelProvider\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.geo.GeoDependencyComponentProvider\nimport org.odk.collect.geo.GeoUtils.formatAccuracy\nimport org.odk.collect.geo.databinding.GeopointDialogBinding\nimport javax.inject.Inject\n\nclass GeoPointDialogFragment : DialogFragment() {\n\n    @Inject\n    internal lateinit var geoPointViewModelFactory: GeoPointViewModelFactory\n\n    var listener: Listener? = null\n\n    lateinit var binding: GeopointDialogBinding\n    private lateinit var viewModel: GeoPointViewModel\n\n    private val onBackPressedCallback: OnBackPressedCallback =\n        object : OnBackPressedCallback(true) {\n            override fun handleOnBackPressed() {\n                listener?.onCancel()\n            }\n        }\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        val component =\n            (context.applicationContext as GeoDependencyComponentProvider).geoDependencyComponent\n        component.inject(this)\n\n        listener = context as? Listener\n\n        viewModel =\n            ViewModelProvider(\n                requireActivity(),\n                geoPointViewModelFactory\n            ).get(GeoPointViewModel::class.java)\n    }\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        binding = GeopointDialogBinding.inflate(LayoutInflater.from(context))\n\n        val accuracyThreshold = viewModel.accuracyThreshold\n\n        viewModel.currentAccuracy.observe(this) {\n            binding.accuracyStatus.accuracy = it\n        }\n\n        binding.threshold.text =\n            getString(org.odk.collect.strings.R.string.point_will_be_saved, formatAccuracy(requireContext(), accuracyThreshold))\n\n        viewModel.timeElapsed.observe(this) {\n            binding.time.text =\n                getString(org.odk.collect.strings.R.string.time_elapsed, DateUtils.formatElapsedTime(it / 1000))\n        }\n\n        viewModel.satellites.observe(this) {\n            binding.satellites.text = getString(org.odk.collect.strings.R.string.satellites, it.toString())\n        }\n\n        val dialog = MaterialAlertDialogBuilder(requireContext())\n            .setView(binding.root)\n            .setPositiveButton(org.odk.collect.strings.R.string.save) { _, _ -> viewModel.forceLocation() }\n            .setNegativeButton(org.odk.collect.strings.R.string.cancel) { _, _ -> listener?.onCancel() }\n            .create()\n\n        dialog.setOnShowListener {\n            viewModel.currentAccuracy.observe(this) {\n                dialog.getButton(DialogInterface.BUTTON_POSITIVE).isEnabled = it != null\n            }\n        }\n\n        isCancelable = false\n\n        (dialog as ComponentDialog).onBackPressedDispatcher.addCallback(onBackPressedCallback)\n        return dialog\n    }\n\n    interface Listener {\n        fun onCancel()\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.geo.geopoint;\n\nimport static org.odk.collect.androidshared.ui.EdgeToEdge.setView;\nimport static org.odk.collect.geo.Constants.EXTRA_DRAGGABLE_ONLY;\nimport static org.odk.collect.geo.Constants.EXTRA_READ_ONLY;\nimport static org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY;\nimport static org.odk.collect.geo.GeoActivityUtils.requireLocationPermissions;\n\nimport android.annotation.SuppressLint;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.view.Window;\nimport android.widget.ImageButton;\n\nimport androidx.annotation.NonNull;\nimport androidx.fragment.app.Fragment;\nimport androidx.fragment.app.FragmentContainerView;\n\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.externalapp.ExternalAppUtils;\nimport org.odk.collect.geo.GeoDependencyComponentProvider;\nimport org.odk.collect.geo.R;\nimport org.odk.collect.maps.MapFragment;\nimport org.odk.collect.maps.MapFragmentFactory;\nimport org.odk.collect.maps.MapPoint;\nimport org.odk.collect.maps.layers.OfflineMapLayersPickerBottomSheetDialogFragment;\nimport org.odk.collect.maps.layers.ReferenceLayerRepository;\nimport org.odk.collect.maps.markers.MarkerDescription;\nimport org.odk.collect.maps.markers.MarkerIconDescription;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.strings.localization.LocalizedActivity;\nimport org.odk.collect.webpage.WebPageService;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\n/**\n * Allow the user to indicate a location by placing a marker on a map, either\n * by touching a point on the map or by tapping a button to place the marker\n * at the current location (obtained from GPS or other location sensors).\n */\npublic class GeoPointMapActivity extends LocalizedActivity {\n\n    public static final String POINT_KEY = \"point\";\n\n    public static final String IS_DRAGGED_KEY = \"is_dragged\";\n    public static final String CAPTURE_LOCATION_KEY = \"capture_location\";\n    public static final String FOUND_FIRST_LOCATION_KEY = \"found_first_location\";\n    public static final String SET_CLEAR_KEY = \"set_clear\";\n    public static final String POINT_FROM_INTENT_KEY = \"point_from_intent\";\n    public static final String INTENT_READ_ONLY_KEY = \"intent_read_only\";\n    public static final String INTENT_DRAGGABLE_KEY = \"intent_draggable\";\n    public static final String IS_POINT_LOCKED_KEY = \"is_point_locked\";\n\n    public static final String PLACE_MARKER_BUTTON_ENABLED_KEY = \"place_marker_button_enabled\";\n    public static final String ZOOM_BUTTON_ENABLED_KEY = \"zoom_button_enabled\";\n    public static final String CLEAR_BUTTON_ENABLED_KEY = \"clear_button_enabled\";\n    public static final String LOCATION_STATUS_VISIBILITY_KEY = \"location_status_visibility\";\n    public static final String LOCATION_INFO_VISIBILITY_KEY = \"location_info_visibility\";\n\n    public static final String EXTRA_LOCATION = \"gp\";\n\n    protected Bundle previousState;\n\n    @Inject\n    MapFragmentFactory mapFragmentFactory;\n\n    @Inject\n    ReferenceLayerRepository referenceLayerRepository;\n\n    @Inject\n    Scheduler scheduler;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    @Inject\n    WebPageService webPageService;\n\n    private MapFragment map;\n    private int featureId = -1;  // will be a positive featureId once map is ready\n\n    private AccuracyStatusView locationStatus;\n\n    private MapPoint location;\n    private ImageButton placeMarkerButton;\n\n    private boolean isDragged;\n\n    private ImageButton zoomButton;\n    private ImageButton clearButton;\n\n    private boolean captureLocation;\n    private boolean foundFirstLocation;\n\n    /**\n     * True if a tap on the clear button removed an existing marker and\n     * no new marker has been placed.\n     */\n    private boolean setClear;\n\n    /** True if the current point came from the intent. */\n    private boolean pointFromIntent;\n\n    /** True if the intent requested for the point to be read-only. */\n    private boolean intentReadOnly;\n\n    /** True if the intent requested for the marker to be draggable. */\n    private boolean intentDraggable;\n\n    /** While true, the point cannot be moved by dragging or long-pressing. */\n    private boolean isPointLocked;\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        ((GeoDependencyComponentProvider) getApplication()).getGeoDependencyComponent().inject(this);\n        getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder()\n                .forClass(MapFragment.class, () -> (Fragment) mapFragmentFactory.createMapFragment())\n                .forClass(OfflineMapLayersPickerBottomSheetDialogFragment.class, () -> new OfflineMapLayersPickerBottomSheetDialogFragment(getActivityResultRegistry(), referenceLayerRepository, scheduler, settingsProvider, webPageService))\n                .build()\n        );\n        super.onCreate(savedInstanceState);\n\n        requireLocationPermissions(this);\n\n        previousState = savedInstanceState;\n\n        requestWindowFeature(Window.FEATURE_NO_TITLE);\n        try {\n            setView(this, R.layout.geopoint_layout, false);\n        } catch (NoClassDefFoundError e) {\n            Timber.e(e, \"Google maps not accessible due to: %s \", e.getMessage());\n            ToastUtils.showShortToast(org.odk.collect.strings.R.string.google_play_services_error_occured);\n            finish();\n            return;\n        }\n\n        locationStatus = findViewById(R.id.status_section);\n        placeMarkerButton = findViewById(R.id.place_marker);\n        zoomButton = findViewById(R.id.zoom);\n\n        MapFragment mapFragment = ((FragmentContainerView) findViewById(R.id.map_container)).getFragment();\n        mapFragment.init(this::initMap, this::finish);\n    }\n\n    @Override protected void onSaveInstanceState(Bundle state) {\n        super.onSaveInstanceState(state);\n        if (map == null) {\n            // initMap() is called asynchronously, so map can be null if the activity\n            // is stopped (e.g. by screen rotation) before initMap() gets to run.\n            // In this case, preserve any provided instance state.\n            if (previousState != null) {\n                state.putAll(previousState);\n            }\n            return;\n        }\n\n        state.putParcelable(POINT_KEY, map.getMarkerPoint(featureId));\n\n        // Flags\n        state.putBoolean(IS_DRAGGED_KEY, isDragged);\n        state.putBoolean(CAPTURE_LOCATION_KEY, captureLocation);\n        state.putBoolean(FOUND_FIRST_LOCATION_KEY, foundFirstLocation);\n        state.putBoolean(SET_CLEAR_KEY, setClear);\n        state.putBoolean(POINT_FROM_INTENT_KEY, pointFromIntent);\n        state.putBoolean(INTENT_READ_ONLY_KEY, intentReadOnly);\n        state.putBoolean(INTENT_DRAGGABLE_KEY, intentDraggable);\n        state.putBoolean(IS_POINT_LOCKED_KEY, isPointLocked);\n\n        // UI state\n        state.putBoolean(PLACE_MARKER_BUTTON_ENABLED_KEY, placeMarkerButton.isEnabled());\n        state.putBoolean(ZOOM_BUTTON_ENABLED_KEY, zoomButton.isEnabled());\n        state.putBoolean(CLEAR_BUTTON_ENABLED_KEY, clearButton.isEnabled());\n        state.putInt(LOCATION_STATUS_VISIBILITY_KEY, locationStatus.getVisibility());\n    }\n\n    public void returnLocation() {\n        String result = null;\n\n        if (setClear || (intentReadOnly && featureId == -1)) {\n            result = \"\";\n        } else if (isDragged || intentReadOnly || pointFromIntent) {\n            result = formatResult(map.getMarkerPoint(featureId));\n        } else if (location != null) {\n            result = formatResult(location);\n        }\n\n        if (result != null) {\n            ExternalAppUtils.returnSingleValue(this, result);\n        } else {\n            finish();\n        }\n    }\n\n    @SuppressLint(\"MissingPermission\") // Permission handled in Constructor\n    public void initMap(MapFragment newMapFragment) {\n        map = newMapFragment;\n        map.setDragEndListener(this::onDragEnd);\n        map.setLongPressListener(this::onLongPress);\n\n        ImageButton acceptLocation = findViewById(R.id.accept_location);\n        acceptLocation.setOnClickListener(v -> returnLocation());\n\n        placeMarkerButton.setEnabled(false);\n        placeMarkerButton.setOnClickListener(v -> {\n            MapPoint mapPoint = map.getGpsLocation();\n            if (mapPoint != null) {\n                placeMarker(mapPoint);\n                zoomToMarker(true);\n            }\n        });\n\n        // Focuses on marked location\n        zoomButton.setEnabled(false);\n        zoomButton.setOnClickListener(v -> map.zoomToCurrentLocation(map.getGpsLocation()));\n\n        // Menu Layer Toggle\n        findViewById(R.id.layer_menu).setOnClickListener(v -> {\n            DialogFragmentUtils.showIfNotShowing(OfflineMapLayersPickerBottomSheetDialogFragment.class, getSupportFragmentManager());\n        });\n\n        clearButton = findViewById(R.id.clear);\n        clearButton.setEnabled(false);\n        clearButton.setOnClickListener(v -> {\n            clear();\n            if (map.getGpsLocation() != null) {\n                placeMarkerButton.setEnabled(true);\n                // locationStatus.setVisibility(View.VISIBLE);\n            }\n            // placeMarkerButton.setEnabled(true);\n            locationStatus.setVisibility(View.VISIBLE);\n            pointFromIntent = false;\n        });\n\n        Intent intent = getIntent();\n        if (intent != null && intent.getExtras() != null) {\n            intentDraggable = intent.getBooleanExtra(EXTRA_DRAGGABLE_ONLY, false);\n            if (!intentDraggable) {\n                // Not Draggable, set text for Map else leave as placement-map text\n                locationStatus.setTitle(getString(org.odk.collect.strings.R.string.geopoint_no_draggable_instruction));\n            }\n\n            intentReadOnly = intent.getBooleanExtra(EXTRA_READ_ONLY, false);\n            if (intentReadOnly) {\n                captureLocation = true;\n                clearButton.setEnabled(false);\n            }\n\n            if (intent.hasExtra(EXTRA_LOCATION)) {\n                MapPoint point = intent.getParcelableExtra(EXTRA_LOCATION);\n\n                // If the point is initially set from the intent, the \"place marker\"\n                // button, dragging, and long-pressing are all initially disabled.\n                // To enable them, the user must clear the marker and add a new one.\n                isPointLocked = true;\n                placeMarker(point);\n                placeMarkerButton.setEnabled(false);\n\n                captureLocation = true;\n                pointFromIntent = true;\n                locationStatus.setVisibility(View.GONE);\n                zoomButton.setEnabled(true);\n                foundFirstLocation = true;\n                zoomToMarker(false);\n            }\n        }\n\n        map.setRetainMockAccuracy(intent.getBooleanExtra(EXTRA_RETAIN_MOCK_ACCURACY, false));\n        map.setGpsLocationListener(this::onLocationChanged);\n        map.setGpsLocationEnabled(true);\n\n        if (previousState != null) {\n            restoreFromInstanceState(previousState);\n        }\n    }\n\n    protected void restoreFromInstanceState(Bundle state) {\n        isDragged = state.getBoolean(IS_DRAGGED_KEY, false);\n        captureLocation = state.getBoolean(CAPTURE_LOCATION_KEY, false);\n        foundFirstLocation = state.getBoolean(FOUND_FIRST_LOCATION_KEY, false);\n        setClear = state.getBoolean(SET_CLEAR_KEY, false);\n        pointFromIntent = state.getBoolean(POINT_FROM_INTENT_KEY, false);\n        intentReadOnly = state.getBoolean(INTENT_READ_ONLY_KEY, false);\n        intentDraggable = state.getBoolean(INTENT_DRAGGABLE_KEY, false);\n        isPointLocked = state.getBoolean(IS_POINT_LOCKED_KEY, false);\n\n        // Restore the marker and dialog after the flags, because they use some of them.\n        MapPoint point = state.getParcelable(POINT_KEY);\n        if (point != null) {\n            placeMarker(point);\n        } else {\n            clear();\n        }\n\n        // Restore the flags again, because placeMarker() and clear() modify some of them.\n        isDragged = state.getBoolean(IS_DRAGGED_KEY, false);\n        captureLocation = state.getBoolean(CAPTURE_LOCATION_KEY, false);\n        foundFirstLocation = state.getBoolean(FOUND_FIRST_LOCATION_KEY, false);\n        setClear = state.getBoolean(SET_CLEAR_KEY, false);\n        pointFromIntent = state.getBoolean(POINT_FROM_INTENT_KEY, false);\n        intentReadOnly = state.getBoolean(INTENT_READ_ONLY_KEY, false);\n        intentDraggable = state.getBoolean(INTENT_DRAGGABLE_KEY, false);\n        isPointLocked = state.getBoolean(IS_POINT_LOCKED_KEY, false);\n\n        placeMarkerButton.setEnabled(state.getBoolean(PLACE_MARKER_BUTTON_ENABLED_KEY, false));\n        zoomButton.setEnabled(state.getBoolean(ZOOM_BUTTON_ENABLED_KEY, false));\n        clearButton.setEnabled(state.getBoolean(CLEAR_BUTTON_ENABLED_KEY, false));\n\n        locationStatus.setVisibility(state.getInt(LOCATION_STATUS_VISIBILITY_KEY, View.GONE));\n    }\n\n    public void onLocationChanged(MapPoint point) {\n        if (setClear) {\n            placeMarkerButton.setEnabled(true);\n        }\n\n        this.location = point;\n\n        if (point != null) {\n            enableZoomButton(true);\n\n            if (!captureLocation && !setClear) {\n                placeMarker(point);\n                placeMarkerButton.setEnabled(true);\n            }\n\n            if (!foundFirstLocation) {\n                map.zoomToCurrentLocation(map.getGpsLocation());\n                foundFirstLocation = true;\n            }\n\n            locationStatus.setAccuracy(new LocationAccuracy.Improving((float) point.accuracy));\n        }\n    }\n\n    public String formatResult(MapPoint point) {\n        return String.format(\"%s %s %s %s\", point.latitude, point.longitude, point.altitude, point.accuracy);\n    }\n\n    public void onDragEnd(int draggedFeatureId) {\n        if (draggedFeatureId == featureId) {\n            isDragged = true;\n            captureLocation = true;\n            setClear = false;\n            map.setCenter(map.getMarkerPoint(featureId), true);\n        }\n    }\n\n    public void onLongPress(MapPoint point) {\n        if (intentDraggable && !intentReadOnly && !isPointLocked) {\n            placeMarker(point);\n            enableZoomButton(true);\n            isDragged = true;\n        }\n    }\n\n    private void enableZoomButton(boolean shouldEnable) {\n        if (zoomButton != null) {\n            zoomButton.setEnabled(shouldEnable);\n        }\n    }\n\n    public void zoomToMarker(boolean animate) {\n        map.zoomToPoint(map.getMarkerPoint(featureId), animate);\n    }\n\n    private void clear() {\n        map.clearFeatures();\n        featureId = -1;\n        clearButton.setEnabled(false);\n\n        isPointLocked = false;\n        isDragged = false;\n        captureLocation = false;\n        setClear = true;\n    }\n\n    /** Places the marker and enables the button to remove it. */\n    private void placeMarker(@NonNull MapPoint point) {\n        map.clearFeatures();\n        featureId = map.addMarker(new MarkerDescription(point, intentDraggable && !intentReadOnly && !isPointLocked, MapFragment.IconAnchor.CENTER, new MarkerIconDescription.DrawableResource(org.odk.collect.icons.R.drawable.ic_map_point)));\n        if (!intentReadOnly) {\n            clearButton.setEnabled(true);\n        }\n        captureLocation = true;\n        setClear = false;\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointViewModel.kt",
    "content": "package org.odk.collect.geo.geopoint\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.map\nimport org.odk.collect.analytics.Analytics.Companion.log\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.geo.analytics.AnalyticsEvents\nimport org.odk.collect.location.Location\nimport org.odk.collect.location.satellites.SatelliteInfoClient\nimport org.odk.collect.location.tracker.LocationTracker\nimport org.odk.collect.location.tracker.getCurrentLocation\n\ninternal abstract class GeoPointViewModel : ViewModel() {\n\n    abstract val accuracyThreshold: Float\n\n    abstract val acceptedLocation: LiveData<Location?>\n    abstract val currentAccuracy: LiveData<LocationAccuracy?>\n    abstract val timeElapsed: NonNullLiveData<Long>\n    abstract val satellites: NonNullLiveData<Int>\n\n    abstract fun start(\n        retainMockAccuracy: Boolean = false,\n        accuracyThreshold: Float? = null,\n        unacceptableAccuracyThreshold: Float? = null\n    )\n\n    abstract fun forceLocation()\n}\n\ninternal class LocationTrackerGeoPointViewModel(\n    private val locationTracker: LocationTracker,\n    private val satelliteInfoClient: SatelliteInfoClient,\n    private val clock: () -> Long,\n    scheduler: Scheduler\n) : GeoPointViewModel() {\n\n    private val startTime = clock()\n    private val repeat = scheduler.repeat(\n        {\n            timeElapsed.value = clock() - startTime\n            updateLocation()\n        },\n        1000L\n    )\n\n    override var accuracyThreshold: Float = Float.MAX_VALUE\n        private set\n\n    private var unacceptableAccuracyThreshold: Float = Float.MAX_VALUE\n\n    private val trackerLocation = MutableLiveData<Location?>(null)\n    override val acceptedLocation: MutableLiveData<Location?> = MutableLiveData<Location?>(null)\n\n    override val currentAccuracy = trackerLocation.map {\n        if (it != null) {\n            when {\n                it.accuracy > unacceptableAccuracyThreshold -> LocationAccuracy.Unacceptable(it.accuracy)\n                it.accuracy > (accuracyThreshold + 5) -> LocationAccuracy.Poor(it.accuracy)\n                else -> LocationAccuracy.Improving(it.accuracy)\n            }\n        } else {\n            null\n        }\n    }\n\n    override val timeElapsed: MutableNonNullLiveData<Long> = MutableNonNullLiveData(0)\n    override val satellites: NonNullLiveData<Int> = satelliteInfoClient.satellitesUsedInLastFix\n\n    override fun start(\n        retainMockAccuracy: Boolean,\n        accuracyThreshold: Float?,\n        unacceptableAccuracyThreshold: Float?\n    ) {\n        if (accuracyThreshold != null) {\n            this.accuracyThreshold = accuracyThreshold\n        }\n\n        if (unacceptableAccuracyThreshold != null) {\n            this.unacceptableAccuracyThreshold = unacceptableAccuracyThreshold\n        }\n\n        locationTracker.start(retainMockAccuracy, 1000L)\n    }\n\n    override fun forceLocation() {\n        acceptLocation(trackerLocation.value!!, true)\n    }\n\n    public override fun onCleared() {\n        repeat.cancel()\n        locationTracker.stop()\n    }\n\n    private fun updateLocation() {\n        locationTracker.getCurrentLocation().let {\n            trackerLocation.value = it\n\n            if (it != null && it.accuracy <= accuracyThreshold) {\n                acceptLocation(it, false)\n            }\n        }\n    }\n\n    private fun acceptLocation(location: Location, isManual: Boolean) {\n        if (acceptedLocation.value == null) {\n            acceptedLocation.value = location\n\n            if (isManual) {\n                logSavePointManual(location)\n            } else {\n                log(AnalyticsEvents.SAVE_POINT_AUTO)\n            }\n        }\n    }\n\n    private fun logSavePointManual(location: Location) {\n        val event = if (System.currentTimeMillis() - startTime < 2000) {\n            AnalyticsEvents.SAVE_POINT_IMMEDIATE\n        } else {\n            AnalyticsEvents.SAVE_POINT_MANUAL\n        }\n\n        if (location.accuracy > 100) {\n            log(event, \"accuracy\", \"unacceptable\")\n        } else if (location.accuracy > 10) {\n            log(event, \"accuracy\", \"poor\")\n        } else {\n            log(event, \"accuracy\", \"acceptable\")\n        }\n    }\n}\n\ninternal interface GeoPointViewModelFactory : ViewModelProvider.Factory\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoint/LocationAccuracy.kt",
    "content": "package org.odk.collect.geo.geopoint\n\ninternal sealed class LocationAccuracy {\n\n    abstract val value: Float\n\n    data class Improving(override val value: Float) : LocationAccuracy()\n    data class Poor(override val value: Float) : LocationAccuracy()\n    data class Unacceptable(override val value: Float) : LocationAccuracy()\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt",
    "content": "package org.odk.collect.geo.geopoly\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.View\nimport androidx.activity.OnBackPressedCallback\nimport androidx.activity.OnBackPressedDispatcher\nimport androidx.core.os.bundleOf\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.asLiveData\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.google.android.material.snackbar.Snackbar\nimport org.odk.collect.androidshared.livedata.LiveDataExt.combine\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing\nimport org.odk.collect.androidshared.ui.DisplayString\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.androidshared.ui.SnackbarUtils\nimport org.odk.collect.androidshared.ui.SnackbarUtils.showSnackbar\nimport org.odk.collect.androidshared.ui.ToastUtils.showShortToastInMiddle\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.geo.GeoActivityUtils.requireLocationPermissions\nimport org.odk.collect.geo.GeoDependencyComponentProvider\nimport org.odk.collect.geo.GeoUtils\nimport org.odk.collect.geo.GeoUtils.toMapPoint\nimport org.odk.collect.geo.R\nimport org.odk.collect.geo.databinding.GeopolyLayoutBinding\nimport org.odk.collect.geo.geopoint.LocationAccuracy.Improving\nimport org.odk.collect.geo.geopoint.LocationAccuracy.Unacceptable\nimport org.odk.collect.geo.geopoly.GeoPolySettingsDialogFragment.SettingsDialogCallback\nimport org.odk.collect.location.Location\nimport org.odk.collect.location.tracker.LocationTracker\nimport org.odk.collect.maps.traces.LineDescription\nimport org.odk.collect.maps.MapConsts\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.circles.CurrentLocationDelegate\nimport org.odk.collect.maps.traces.PolygonDescription\nimport org.odk.collect.maps.layers.OfflineMapLayersPickerBottomSheetDialogFragment\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.webpage.WebPageService\nimport javax.inject.Inject\n\nclass GeoPolyFragment @JvmOverloads constructor(\n    val onBackPressedDispatcher: () -> OnBackPressedDispatcher,\n    val outputMode: OutputMode = OutputMode.GEOTRACE,\n    val readOnly: Boolean = false,\n    val retainMockAccuracy: Boolean = false,\n    val inputPolygon: List<MapPoint> = emptyList(),\n    val invalidMessage: LiveData<DisplayString?> = MutableLiveData(null)\n) : Fragment(R.layout.geopoly_layout), SettingsDialogCallback {\n\n    @Inject\n    lateinit var mapFragmentFactory: MapFragmentFactory\n\n    @Inject\n    lateinit var locationTracker: LocationTracker\n\n    @Inject\n    lateinit var referenceLayerRepository: ReferenceLayerRepository\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    @Inject\n    lateinit var webPageService: WebPageService\n\n    private var previousState: Bundle? = null\n\n    private var featureId = -1 // will be a positive featureId once map is ready\n    private var originalPoly: List<MapPoint>? = null\n    private var intervalIndex: Int = DEFAULT_INTERVAL_INDEX\n    private var accuracyThresholdIndex: Int = DEFAULT_ACCURACY_THRESHOLD_INDEX\n    private var mapInitialized = false\n\n    private val onBackPressedCallback: OnBackPressedCallback =\n        object : OnBackPressedCallback(true) {\n            override fun handleOnBackPressed() {\n                if (!readOnly && originalPoly != viewModel.points.value) {\n                    showBackDialog()\n                } else {\n                    cancel()\n                }\n            }\n        }\n\n    private val viewModel: GeoPolyViewModel by viewModels {\n        viewModelFactory {\n            addInitializer(GeoPolyViewModel::class) {\n                GeoPolyViewModel(\n                    outputMode,\n                    inputPolygon,\n                    retainMockAccuracy,\n                    locationTracker,\n                    scheduler,\n                    invalidMessage\n                )\n            }\n        }\n    }\n\n    private val currentLocationDelegate = CurrentLocationDelegate()\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        (context.applicationContext as GeoDependencyComponentProvider)\n            .geoDependencyComponent.inject(this)\n\n        getChildFragmentManager().fragmentFactory = FragmentFactoryBuilder()\n            .forClass(MapFragment::class.java) { mapFragmentFactory.createMapFragment() as Fragment }\n            .forClass(OfflineMapLayersPickerBottomSheetDialogFragment::class.java) {\n                OfflineMapLayersPickerBottomSheetDialogFragment(\n                    requireActivity().activityResultRegistry,\n                    referenceLayerRepository,\n                    scheduler,\n                    settingsProvider,\n                    webPageService\n                )\n            }\n            .forClass(GeoPolySettingsDialogFragment::class.java) {\n                GeoPolySettingsDialogFragment(\n                    this\n                )\n            }\n            .build()\n\n        requireLocationPermissions(requireActivity())\n\n        viewModel.points.asLiveData().observe(this) {\n            setChangeResult(it)\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        previousState = savedInstanceState\n\n        if (savedInstanceState != null) {\n            intervalIndex = savedInstanceState.getInt(INTERVAL_INDEX_KEY, DEFAULT_INTERVAL_INDEX)\n            accuracyThresholdIndex = savedInstanceState.getInt(\n                ACCURACY_THRESHOLD_INDEX_KEY, DEFAULT_ACCURACY_THRESHOLD_INDEX\n            )\n        }\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        val binding = GeopolyLayoutBinding.bind(view)\n        val mapFragment: MapFragment = binding.mapContainer.getFragment()\n        mapFragment.init({ initMap(it, binding) }, { this.cancel() })\n\n        onBackPressedDispatcher().addCallback(viewLifecycleOwner, onBackPressedCallback)\n\n        viewModel.fixedAlerts.showSnackbar(viewLifecycleOwner, view) {\n            SnackbarUtils.SnackbarDetails(getString(string.error_fixed))\n        }\n    }\n\n    override fun onSaveInstanceState(state: Bundle) {\n        super.onSaveInstanceState(state)\n        if (!mapInitialized) {\n            // initMap() is called asynchronously, so map can be null if the activity\n            // is stopped (e.g. by screen rotation) before initMap() gets to run.\n            // In this case, preserve any provided instance state.\n            if (previousState != null) {\n                state.putAll(previousState)\n            }\n            return\n        }\n        state.putInt(INTERVAL_INDEX_KEY, intervalIndex)\n        state.putInt(ACCURACY_THRESHOLD_INDEX_KEY, accuracyThresholdIndex)\n    }\n\n    fun initMap(map: MapFragment, binding: GeopolyLayoutBinding) {\n        mapInitialized = true\n\n        binding.info.setOnClickListener { showInfoDialog(false) }\n        binding.clear.setOnClickListener { showClearDialog() }\n        binding.pause.setOnClickListener {\n            viewModel.stopRecording()\n            updateUi()\n        }\n\n        binding.backspace.setOnClickListener { removeLastPoint() }\n        binding.save.setOnClickListener {\n            if (!viewModel.points.value.isEmpty()) {\n                if (outputMode == OutputMode.GEOTRACE) {\n                    saveAsPolyline()\n                } else {\n                    saveAsPolygon()\n                }\n            } else {\n                setResult()\n            }\n        }\n\n        binding.play.setOnClickListener {\n            if (viewModel.points.value.isEmpty()) {\n                showIfNotShowing<GeoPolySettingsDialogFragment>(\n                    GeoPolySettingsDialogFragment::class.java,\n                    getChildFragmentManager()\n                )\n            } else {\n                startInput()\n            }\n        }\n\n        binding.recordButton.setOnClickListener {\n            viewModel.recordPoint()\n        }\n\n        binding.layers.setOnClickListener {\n            showIfNotShowing(\n                OfflineMapLayersPickerBottomSheetDialogFragment::class.java,\n                getChildFragmentManager()\n            )\n        }\n\n        binding.zoom.setOnClickListener {\n            currentLocationDelegate.zoomToCurrentLocation(map)\n        }\n\n        originalPoly = inputPolygon\n\n        map.setClickListener(this::onClick)\n        // Also allow long press to place point to match prior versions\n        map.setLongPressListener(this::onClick)\n        map.setDragEndListener {\n            viewModel.update(map.getPolyPoints(it))\n        }\n\n        if (!map.hasCenter()) {\n            if (viewModel.points.value.isNotEmpty()) {\n                map.zoomToBoundingBox(viewModel.points.value, 0.6, false)\n            }\n        }\n\n        val snackbar = SnackbarUtils.make(\n            requireView(),\n            \"\",\n            Snackbar.LENGTH_INDEFINITE,\n            action = SnackbarUtils.Action(getString(string.how_to_modify)) {\n                showInfoDialog(true)\n            },\n            displayDismissButton = true\n        )\n\n        viewModel.currentLocation.asLiveData()\n            .combine(viewModel.inputActive.asLiveData())\n            .observe(viewLifecycleOwner) { (location, inputActive) ->\n                binding.zoom.isEnabled = location != null\n\n                if (location != null) {\n                    val shouldFollowLocation =\n                        (inputActive ?: false) && viewModel.recordingMode != GeoPolyViewModel.RecordingMode.PLACEMENT\n                    currentLocationDelegate.update(map, location.toMapPoint(), shouldFollowLocation)\n\n                    binding.locationStatus.accuracy = if (isLocationAcceptable(location)) {\n                        Improving(location.accuracy)\n                    } else {\n                        Unacceptable(location.accuracy)\n                    }\n                }\n            }\n\n        viewModel.invalidMessage.observe(viewLifecycleOwner) {\n            val isValid = it == null\n            if (!isValid) {\n                snackbar.setText(it.getString(requireContext()))\n                SnackbarUtils.show(snackbar)\n            } else {\n                snackbar.dismiss()\n            }\n        }\n\n        viewModel.geoPoly.observe(viewLifecycleOwner) { (points, isValid) ->\n            binding.save.isEnabled = !readOnly && isValid\n\n            val color = if (isValid) {\n                MapConsts.DEFAULT_STROKE_COLOR\n            } else {\n                MapConsts.DEFAULT_ERROR_COLOR\n            }\n\n            if (outputMode == OutputMode.GEOSHAPE) {\n                val polygonDescription = PolygonDescription(\n                    points,\n                    draggable = !readOnly,\n                    strokeColor = color,\n                    fillColor = color,\n                    highlightLastPoint = isValid\n                )\n\n                if (featureId == -1) {\n                    featureId = map.addPolygon(polygonDescription)\n                } else {\n                    map.updatePolygon(featureId, polygonDescription)\n                }\n            } else {\n                val lineDescription = LineDescription(\n                    points,\n                    draggable = !readOnly,\n                    strokeColor = color,\n                    highlightLastPoint = isValid\n                )\n\n                if (featureId == -1) {\n                    featureId = map.addPolyLine(lineDescription)\n                } else {\n                    map.updatePolyLine(featureId, lineDescription)\n                }\n            }\n\n            updateUi()\n        }\n    }\n\n    private fun saveAsPolyline() {\n        if (viewModel.points.value.size > 1) {\n            setResult()\n        } else {\n            showShortToastInMiddle(\n                requireActivity(),\n                getString(org.odk.collect.strings.R.string.polyline_validator)\n            )\n        }\n    }\n\n    private fun saveAsPolygon() {\n        if (viewModel.points.value.size > 2) {\n            setResult()\n        } else {\n            showShortToastInMiddle(\n                requireActivity(),\n                getString(org.odk.collect.strings.R.string.polygon_validator)\n            )\n        }\n    }\n\n    private fun setChangeResult(points: List<MapPoint>) {\n        val geoString = if (outputMode == OutputMode.GEOSHAPE && points.size < 3) {\n            \"\"\n        } else if (points.size < 2) {\n            \"\"\n        } else {\n            getGeoString(points)\n        }\n\n        getParentFragmentManager().setFragmentResult(\n            REQUEST_GEOPOLY,\n            bundleOf(RESULT_GEOPOLY_CHANGE to geoString)\n        )\n    }\n\n    private fun setResult() {\n        val points = viewModel.points.value\n        getParentFragmentManager().setFragmentResult(\n            REQUEST_GEOPOLY,\n            bundleOf(RESULT_GEOPOLY to getGeoString(points))\n        )\n    }\n\n    private fun getGeoString(points: List<MapPoint>): String? {\n        return GeoUtils.formatPointsResultString(\n            points.toMutableList(),\n            outputMode == OutputMode.GEOSHAPE\n        )\n    }\n\n    override fun startInput() {\n        viewModel.enableInput()\n        if (viewModel.recordingMode == GeoPolyViewModel.RecordingMode.AUTOMATIC) {\n            viewModel.startRecording(\n                ACCURACY_THRESHOLD_OPTIONS[accuracyThresholdIndex],\n                INTERVAL_OPTIONS[intervalIndex].toLong() * 1000\n            )\n        }\n        updateUi()\n    }\n\n    override fun updateRecordingMode(id: Int) {\n        when (id) {\n            R.id.placement_mode -> viewModel.setRecordingMode(GeoPolyViewModel.RecordingMode.PLACEMENT)\n            R.id.manual_mode -> viewModel.setRecordingMode(GeoPolyViewModel.RecordingMode.MANUAL)\n            R.id.automatic_mode -> viewModel.setRecordingMode(GeoPolyViewModel.RecordingMode.AUTOMATIC)\n        }\n    }\n\n    override fun getCheckedId(): Int {\n        return when (viewModel.recordingMode) {\n            GeoPolyViewModel.RecordingMode.PLACEMENT -> R.id.placement_mode\n            GeoPolyViewModel.RecordingMode.MANUAL -> R.id.manual_mode\n            GeoPolyViewModel.RecordingMode.AUTOMATIC -> R.id.automatic_mode\n        }\n    }\n\n    override fun getIntervalIndex(): Int {\n        return intervalIndex\n    }\n\n    override fun getAccuracyThresholdIndex(): Int {\n        return accuracyThresholdIndex\n    }\n\n    override fun setIntervalIndex(intervalIndex: Int) {\n        this.intervalIndex = intervalIndex\n    }\n\n    override fun setAccuracyThresholdIndex(accuracyThresholdIndex: Int) {\n        this.accuracyThresholdIndex = accuracyThresholdIndex\n    }\n\n    private fun cancel() {\n        getParentFragmentManager().setFragmentResult(REQUEST_GEOPOLY, Bundle.EMPTY)\n    }\n\n    private fun discard() {\n        val geoString = GeoUtils.formatPointsResultString(\n            (originalPoly ?: emptyList()).toMutableList(),\n            outputMode == OutputMode.GEOSHAPE\n        )\n\n        val bundle = Bundle()\n        bundle.putString(RESULT_GEOPOLY, geoString)\n        getParentFragmentManager().setFragmentResult(REQUEST_GEOPOLY, bundle)\n    }\n\n    private fun onClick(point: MapPoint) {\n        if (viewModel.inputActive.value && viewModel.recordingMode == GeoPolyViewModel.RecordingMode.PLACEMENT) {\n            viewModel.add(point)\n        }\n    }\n\n    private fun isLocationAcceptable(location: Location): Boolean {\n        if (!this.isAccuracyThresholdActive) {\n            return true\n        }\n\n        return location.accuracy <= ACCURACY_THRESHOLD_OPTIONS[accuracyThresholdIndex]\n    }\n\n    private val isAccuracyThresholdActive: Boolean\n        get() {\n            val meters: Int =\n                ACCURACY_THRESHOLD_OPTIONS[accuracyThresholdIndex]\n            return viewModel.recordingMode == GeoPolyViewModel.RecordingMode.AUTOMATIC && meters > 0\n        }\n\n    private fun removeLastPoint() {\n        if (featureId != -1) {\n            viewModel.removeLast()\n        }\n    }\n\n    private fun clear() {\n        viewModel.update(emptyList())\n    }\n\n    /** Updates the state of various UI widgets to reflect internal state.  */\n    private fun updateUi() {\n        val binding = GeopolyLayoutBinding.bind(requireView())\n\n        val numPoints = viewModel.points.value.size\n\n        // Visibility state\n        binding.play.isVisible = !viewModel.inputActive.value\n        binding.pause.isVisible = viewModel.inputActive.value\n        binding.recordButton.isVisible =\n            viewModel.inputActive.value && viewModel.recordingMode == GeoPolyViewModel.RecordingMode.MANUAL\n\n        // Enabled state\n        binding.backspace.isEnabled = numPoints > 0\n        binding.clear.isEnabled = !viewModel.inputActive.value && numPoints > 0\n\n        if (readOnly) {\n            binding.play.isEnabled = false\n            binding.backspace.isEnabled = false\n            binding.clear.isEnabled = false\n        }\n\n        // GPS status\n        val usingThreshold = this.isAccuracyThresholdActive\n        val seconds: Int = INTERVAL_OPTIONS[intervalIndex]\n        val minutes = seconds / 60\n        val meters: Int = ACCURACY_THRESHOLD_OPTIONS[accuracyThresholdIndex]\n\n        binding.collectionStatus.text = if (!viewModel.inputActive.value) {\n            getString(org.odk.collect.strings.R.string.collection_status_paused, numPoints)\n        } else {\n            if (viewModel.recordingMode == GeoPolyViewModel.RecordingMode.PLACEMENT) {\n                getString(\n                    org.odk.collect.strings.R.string.collection_status_placement,\n                    numPoints\n                )\n            } else {\n                if (viewModel.recordingMode == GeoPolyViewModel.RecordingMode.MANUAL) {\n                    getString(\n                        org.odk.collect.strings.R.string.collection_status_manual,\n                        numPoints\n                    )\n                } else {\n                    if (!usingThreshold) {\n                        if (minutes > 0) {\n                            getString(\n                                org.odk.collect.strings.R.string.collection_status_auto_minutes,\n                                numPoints,\n                                minutes\n                            )\n                        } else {\n                            getString(\n                                org.odk.collect.strings.R.string.collection_status_auto_seconds,\n                                numPoints,\n                                seconds\n                            )\n                        }\n                    } else {\n                        if (minutes > 0) {\n                            getString(\n                                org.odk.collect.strings.R.string.collection_status_auto_minutes_accuracy,\n                                numPoints,\n                                minutes,\n                                meters\n                            )\n                        } else {\n                            getString(\n                                org.odk.collect.strings.R.string.collection_status_auto_seconds_accuracy,\n                                numPoints,\n                                seconds,\n                                meters\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private fun showInfoDialog(fromSnackbar: Boolean) {\n        InfoDialog.show(requireContext(), viewModel, fromSnackbar)\n    }\n\n    private fun showClearDialog() {\n        if (!viewModel.points.value.isEmpty()) {\n            MaterialAlertDialogBuilder(requireContext())\n                .setMessage(org.odk.collect.strings.R.string.geo_clear_warning)\n                .setPositiveButton(org.odk.collect.strings.R.string.clear) { _, _ -> clear() }\n                .setNegativeButton(org.odk.collect.strings.R.string.cancel, null)\n                .show()\n        }\n    }\n\n    private fun showBackDialog() {\n        MaterialAlertDialogBuilder(requireContext())\n            .setMessage(getString(org.odk.collect.strings.R.string.geo_exit_warning))\n            .setPositiveButton(org.odk.collect.strings.R.string.discard) { _, _ -> discard() }\n            .setNegativeButton(org.odk.collect.strings.R.string.cancel, null)\n            .show()\n    }\n\n    enum class OutputMode {\n        GEOTRACE, GEOSHAPE\n    }\n\n    companion object {\n        const val REQUEST_GEOPOLY: String = \"geopoly\"\n        const val RESULT_GEOPOLY: String = \"geopoly\"\n        const val RESULT_GEOPOLY_CHANGE: String = \"geopoly_change\"\n        const val INTERVAL_INDEX_KEY: String = \"interval_index\"\n        const val ACCURACY_THRESHOLD_INDEX_KEY: String = \"accuracy_threshold_index\"\n        val INTERVAL_OPTIONS = intArrayOf(\n            1, 5, 10, 20, 30, 60, 300, 600, 1200, 1800\n        )\n        const val DEFAULT_INTERVAL_INDEX = 3 // default is 20 seconds\n\n        private val ACCURACY_THRESHOLD_OPTIONS = intArrayOf(\n            0, 3, 5, 10, 15, 20\n        )\n        private const val DEFAULT_ACCURACY_THRESHOLD_INDEX = 3 // default is 10 meters\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolySettingsDialogFragment.java",
    "content": "package org.odk.collect.geo.geopoly;\n\nimport android.app.Dialog;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.AdapterView;\nimport android.widget.ArrayAdapter;\nimport android.widget.RadioGroup;\nimport android.widget.Spinner;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.odk.collect.geo.R;\n\npublic class GeoPolySettingsDialogFragment extends DialogFragment {\n\n    private static final int[] INTERVAL_OPTIONS = {\n            1, 5, 10, 20, 30, 60, 300, 600, 1200, 1800\n    };\n\n    private static final int[] ACCURACY_THRESHOLD_OPTIONS = {\n            0, 3, 5, 10, 15, 20\n    };\n\n    private View autoOptions;\n    private RadioGroup radioGroup;\n    private final SettingsDialogCallback callback;\n\n    private int checkedRadioButtonId = -1;\n    private int intervalIndex = -1;\n    private int accuracyThresholdIndex = -1;\n\n    public GeoPolySettingsDialogFragment(SettingsDialogCallback callback) {\n        this.callback = callback;\n    }\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        super.onCreateDialog(savedInstanceState);\n\n        View settingsView = getActivity().getLayoutInflater().inflate(R.layout.geopoly_dialog, null);\n        radioGroup = settingsView.findViewById(R.id.radio_group);\n        radioGroup.setOnCheckedChangeListener((group, checkedId) -> {\n            checkedRadioButtonId = checkedId;\n            autoOptions.setVisibility(checkedId == R.id.automatic_mode ? View.VISIBLE : View.GONE);\n        });\n\n        autoOptions = settingsView.findViewById(R.id.auto_options);\n        Spinner autoInterval = settingsView.findViewById(R.id.auto_interval);\n        autoInterval.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {\n            @Override\n            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {\n                intervalIndex = position;\n            }\n\n            @Override\n            public void onNothingSelected(AdapterView<?> parent) {\n\n            }\n        });\n\n        String[] options = new String[INTERVAL_OPTIONS.length];\n        for (int i = 0; i < INTERVAL_OPTIONS.length; i++) {\n            options[i] = formatInterval(INTERVAL_OPTIONS[i]);\n        }\n        populateSpinner(autoInterval, options);\n\n        Spinner accuracyThreshold = settingsView.findViewById(R.id.accuracy_threshold);\n        accuracyThreshold.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {\n            @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {\n                accuracyThresholdIndex = position;\n            }\n\n            @Override public void onNothingSelected(AdapterView<?> parent) { }\n        });\n\n        options = new String[ACCURACY_THRESHOLD_OPTIONS.length];\n        for (int i = 0; i < ACCURACY_THRESHOLD_OPTIONS.length; i++) {\n            options[i] = formatAccuracyThreshold(ACCURACY_THRESHOLD_OPTIONS[i]);\n        }\n        populateSpinner(accuracyThreshold, options);\n\n        if (checkedRadioButtonId == -1) {\n            checkedRadioButtonId = callback.getCheckedId();\n            intervalIndex = callback.getIntervalIndex();\n            accuracyThresholdIndex = callback.getAccuracyThresholdIndex();\n\n            radioGroup.check(checkedRadioButtonId);\n            autoInterval.setSelection(intervalIndex);\n            accuracyThreshold.setSelection(accuracyThresholdIndex);\n        }\n\n        return new MaterialAlertDialogBuilder(getActivity())\n                .setTitle(getString(org.odk.collect.strings.R.string.input_method))\n                .setView(settingsView)\n                .setPositiveButton(getString(org.odk.collect.strings.R.string.start), (dialog, id) -> {\n                    callback.updateRecordingMode(radioGroup.getCheckedRadioButtonId());\n                    callback.setIntervalIndex(intervalIndex);\n                    callback.setAccuracyThresholdIndex(accuracyThresholdIndex);\n                    callback.startInput();\n                    dismiss();\n                })\n                .setNegativeButton(org.odk.collect.strings.R.string.cancel, (dialog, id) -> {\n                    dismiss();\n                })\n                .create();\n    }\n\n    /** Formats a time interval as a whole number of seconds or minutes. */\n    private String formatInterval(int seconds) {\n        int minutes = seconds / 60;\n        return minutes > 0\n                ? getResources().getQuantityString(org.odk.collect.strings.R.plurals.number_of_minutes, minutes, minutes)\n                : getResources().getQuantityString(org.odk.collect.strings.R.plurals.number_of_seconds, seconds, seconds);\n    }\n\n    /** Populates a Spinner with the option labels in the given array. */\n    private void populateSpinner(Spinner spinner, String[] options) {\n        ArrayAdapter<String> adapter = new ArrayAdapter<>(\n                getActivity(), android.R.layout.simple_spinner_item, options);\n        adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item);\n        spinner.setAdapter(adapter);\n    }\n\n    /** Formats an entry in the accuracy threshold dropdown. */\n    private String formatAccuracyThreshold(int meters) {\n        return meters > 0\n                ? getResources().getQuantityString(org.odk.collect.strings.R.plurals.number_of_meters, meters, meters)\n                : getString(org.odk.collect.strings.R.string.none);\n    }\n\n    public interface SettingsDialogCallback {\n        void startInput();\n        void updateRecordingMode(int checkedId);\n\n        int getCheckedId();\n        int getIntervalIndex();\n        int getAccuracyThresholdIndex();\n\n        void setIntervalIndex(int intervalIndex);\n        void setAccuracyThresholdIndex(int accuracyThresholdIndex);\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyUtils.kt",
    "content": "package org.odk.collect.geo.geopoly\n\nimport org.odk.collect.geo.GeoUtils.parseGeometryPoint\nimport org.odk.collect.maps.MapPoint\n\nobject GeoPolyUtils {\n\n    fun parseGeometry(geometry: String?, strict: Boolean = false): List<MapPoint> {\n        val points = ArrayList<MapPoint>()\n\n        for (vertex in (geometry ?: \"\").split(\";\")) {\n            val point = parseGeometryPoint(vertex, strict)\n            if (point != null) {\n                points.add(MapPoint(point[0], point[1], point[2], point[3]))\n            } else {\n                return ArrayList()\n            }\n        }\n\n        return points\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt",
    "content": "package org.odk.collect.geo.geopoly\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.asLiveData\nimport androidx.lifecycle.map\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.androidshared.livedata.LiveDataExt.combine\nimport org.odk.collect.androidshared.livedata.LiveDataExt.withLast\nimport org.odk.collect.androidshared.ui.DisplayString\nimport org.odk.collect.async.Cancellable\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.geo.geopoly.GeoPolyFragment.OutputMode\nimport org.odk.collect.location.tracker.LocationTracker\nimport org.odk.collect.maps.MapPoint\n\nclass GeoPolyViewModel(\n    outputMode: OutputMode,\n    points: List<MapPoint>,\n    retainMockAccuracy: Boolean,\n    private val locationTracker: LocationTracker,\n    private val scheduler: Scheduler,\n    val invalidMessage: LiveData<DisplayString?>\n) : ViewModel() {\n\n    enum class RecordingMode {\n        PLACEMENT, MANUAL, AUTOMATIC\n    }\n\n    var recordingMode: RecordingMode = RecordingMode.PLACEMENT\n        private set\n\n    private val _inputActive = MutableStateFlow(false)\n    var inputActive: StateFlow<Boolean> = _inputActive\n        private set\n\n    private val _points = MutableStateFlow(\n        if (points.isNotEmpty()) {\n            if (outputMode == OutputMode.GEOSHAPE) {\n                points.subList(0, points.size - 1)\n            } else {\n                points\n            }\n        } else {\n            points\n        }\n    )\n    val points: StateFlow<List<MapPoint>> = _points\n\n    val fixedAlerts = invalidMessage.withLast().map {\n        if (it.second == null && it.first != null) {\n            Consumable(Unit)\n        } else {\n            null\n        }\n    }\n\n    val geoPoly = _points.asLiveData()\n        .combine(invalidMessage)\n        .map {\n            GeoPoly(it.first ?: emptyList(), it.second == null)\n        }\n\n    val currentLocation = locationTracker.getLocation()\n\n    private var accuracyThreshold: Int = 0\n    private var recording: Cancellable? = null\n\n    init {\n        locationTracker.start(retainMockAccuracy)\n    }\n\n    fun add(point: MapPoint) {\n        if (invalidMessage.value == null) {\n            val points = _points.value\n            if (points.isEmpty() || point != points.last()) {\n                _points.value = points + point\n            }\n        }\n    }\n\n    fun removeLast() {\n        _points.value = _points.value.dropLast(1)\n    }\n\n    fun update(points: List<MapPoint>) {\n        _points.value = points\n    }\n\n    fun startRecording(accuracyThreshold: Int, interval: Long) {\n        this.accuracyThreshold = accuracyThreshold\n        recording = scheduler.repeat({ recordPoint(accuracyThreshold) }, interval)\n    }\n\n    fun recordPoint(accuracyThreshold: Int = 0) {\n        locationTracker.getLocation().value?.let {\n            if (accuracyThreshold == 0 || it.accuracy <= accuracyThreshold) {\n                add(\n                    MapPoint(\n                        it.latitude,\n                        it.longitude,\n                        it.altitude,\n                        it.accuracy.toDouble()\n                    )\n                )\n            }\n        }\n    }\n\n    fun stopRecording() {\n        disableInput()\n        recording?.cancel()\n    }\n\n    fun setRecordingMode(mode: RecordingMode) {\n        recordingMode = mode\n    }\n\n    fun enableInput() {\n        _inputActive.value = true\n    }\n\n    fun disableInput() {\n        _inputActive.value = false\n    }\n\n    public override fun onCleared() {\n        stopRecording()\n        locationTracker.stop()\n    }\n\n    data class GeoPoly(val points: List<MapPoint>, val isValid: Boolean)\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/geopoly/InfoDialog.kt",
    "content": "package org.odk.collect.geo.geopoly\n\nimport android.content.Context\nimport androidx.appcompat.app.AlertDialog\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.Backspace\nimport androidx.compose.material.icons.filled.AddLocation\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.TouchApp\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.platform.ComposeView\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.androidshared.R.dimen\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\nimport org.odk.collect.androidshared.ui.compose.marginSmall\nimport org.odk.collect.androidshared.ui.compose.marginStandard\nimport org.odk.collect.strings.R.string\n\nobject InfoDialog {\n    data class InfoItem(\n        val icon: ImageVector,\n        val text: String\n    )\n\n    fun show(context: Context, viewModel: GeoPolyViewModel, fromSnackbar: Boolean) {\n        var dialog: AlertDialog? = null\n\n        val info = ComposeView(context).apply {\n            setContextThemedContent {\n                InfoContent(viewModel.recordingMode, fromSnackbar) { dialog?.dismiss() }\n            }\n        }\n\n        dialog = MaterialAlertDialogBuilder(context)\n            .setView(info)\n            .show()\n    }\n}\n\n@Composable\nfun InfoContent(\n    recordingMode: GeoPolyViewModel.RecordingMode,\n    fromSnackbar: Boolean,\n    onDone: () -> Unit\n) {\n    val items = when (recordingMode) {\n        GeoPolyViewModel.RecordingMode.PLACEMENT -> {\n            if (fromSnackbar) {\n                listOf(\n                    InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),\n                    InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),\n                    InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_shape_to_start_over_info_item)),\n                    InfoDialog.InfoItem(Icons.Filled.AddLocation, stringResource(string.add_point_info_item))\n                )\n            } else {\n                listOf(\n                    InfoDialog.InfoItem(Icons.Filled.AddLocation, stringResource(string.tap_to_add_a_point_info_item)),\n                    InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),\n                    InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),\n                    InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_entire_shape_info_item))\n                )\n            }\n        }\n        GeoPolyViewModel.RecordingMode.MANUAL, GeoPolyViewModel.RecordingMode.AUTOMATIC -> {\n            if (fromSnackbar) {\n                listOf(\n                    InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),\n                    InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),\n                    InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_entire_shape_info_item)),\n                )\n            } else {\n                listOf(\n                    InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),\n                    InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),\n                    InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_entire_shape_info_item))\n                )\n            }\n        }\n    }\n\n    val scrollState = rememberScrollState()\n\n    Surface {\n        Column(\n            modifier = Modifier\n                .padding(marginStandard())\n                .verticalScroll(scrollState)\n        ) {\n            Title()\n            items.forEachIndexed { index, item ->\n                Info(item.icon, item.text)\n                if (index < items.lastIndex) {\n                    HorizontalDivider(Modifier.padding(horizontal = marginSmall()))\n                }\n            }\n            DoneButton(onDone)\n        }\n    }\n}\n\n@Composable\nprivate fun Title() {\n    Text(\n        modifier = Modifier.padding(\n            start = marginStandard(),\n            top = dimensionResource(id = dimen.margin_extra_small),\n            bottom = marginStandard()\n        ),\n        text = stringResource(string.how_to_modify_map),\n        style = MaterialTheme.typography.titleLarge\n    )\n}\n\n@Composable\nprivate fun Info(icon: ImageVector, text: String) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(marginStandard()),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Icon(\n            imageVector = icon,\n            contentDescription = null,\n        )\n        Text(\n            modifier = Modifier.padding(start = marginSmall()),\n            text = text,\n            style = MaterialTheme.typography.bodyLarge\n        )\n    }\n}\n\n@Composable\nprivate fun DoneButton(onDone: () -> Unit) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(top = marginStandard()),\n        horizontalArrangement = Arrangement.End\n    ) {\n        TextButton(onClick = onDone) {\n            Text(stringResource(string.done))\n        }\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/javarosa/IntersectsFunctionHandler.kt",
    "content": "package org.odk.collect.geo.javarosa\n\nimport org.javarosa.core.model.condition.EvaluationContext\nimport org.javarosa.core.model.condition.IFunctionHandler\nimport org.javarosa.xpath.XPathTypeMismatchException\nimport org.odk.collect.geo.geopoly.GeoPolyUtils.parseGeometry\nimport org.odk.collect.maps.toPoint\nimport org.odk.collect.shared.geometry.Trace\nimport org.odk.collect.shared.geometry.intersects\n\nclass IntersectsFunctionHandler : IFunctionHandler {\n    override fun getName(): String {\n        return \"intersects\"\n    }\n\n    override fun getPrototypes(): List<Array<out Class<*>>> {\n        return listOf(arrayOf(String::class.java))\n    }\n\n    override fun eval(args: Array<out Any>, ec: EvaluationContext): Any {\n        try {\n            val mapPoints = parseGeometry(args[0] as String, strict = true)\n            val trace = Trace(mapPoints.map { it.toPoint() })\n            return trace.intersects()\n        } catch (_: IllegalArgumentException) {\n            throw XPathTypeMismatchException()\n        }\n    }\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/selection/MappableSelectItem.kt",
    "content": "package org.odk.collect.geo.selection\n\nimport org.odk.collect.maps.MapPoint\n\nsealed class MappableSelectItem {\n    abstract val id: Long\n    abstract val name: String\n    abstract val properties: List<IconifiedText>\n    abstract val selected: Boolean\n    abstract val info: String?\n    abstract val action: IconifiedText?\n    abstract val status: Status?\n\n    data class MappableSelectPoint(\n        override val id: Long,\n        override val name: String,\n        override val properties: List<IconifiedText> = emptyList(),\n        override val selected: Boolean = false,\n        override val info: String? = null,\n        override val action: IconifiedText? = null,\n        override val status: Status? = null,\n        val point: MapPoint,\n        val smallIcon: Int,\n        val largeIcon: Int,\n        val color: String? = null,\n        val symbol: String? = null\n    ) : MappableSelectItem()\n\n    data class MappableSelectLine(\n        override val id: Long,\n        override val name: String,\n        override val properties: List<IconifiedText> = emptyList(),\n        override val selected: Boolean = false,\n        override val info: String? = null,\n        override val action: IconifiedText? = null,\n        override val status: Status? = null,\n        val points: List<MapPoint>,\n        val strokeWidth: String? = null,\n        val strokeColor: String? = null\n    ) : MappableSelectItem()\n\n    data class MappableSelectPolygon(\n        override val id: Long,\n        override val name: String,\n        override val properties: List<IconifiedText> = emptyList(),\n        override val selected: Boolean = false,\n        override val info: String? = null,\n        override val action: IconifiedText? = null,\n        override val status: Status? = null,\n        val points: List<MapPoint>,\n        val strokeWidth: String? = null,\n        val strokeColor: String? = null,\n        val fillColor: String? = null\n    ) : MappableSelectItem()\n}\n\ndata class IconifiedText(val icon: Int?, val text: String)\n\nenum class Status { ERRORS, NO_ERRORS }\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt",
    "content": "package org.odk.collect.geo.selection\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.ViewTreeObserver\nimport androidx.activity.OnBackPressedCallback\nimport androidx.activity.OnBackPressedDispatcher\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.ViewModel\nimport com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback\nimport com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED\nimport com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.androidshared.ui.multiclicksafe.setMultiClickSafeOnClickListener\nimport org.odk.collect.androidshared.utils.sanitizeToColorInt\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.geo.GeoDependencyComponentProvider\nimport org.odk.collect.geo.databinding.SelectionMapLayoutBinding\nimport org.odk.collect.maps.traces.LineDescription\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.traces.PolygonDescription\nimport org.odk.collect.maps.layers.OfflineMapLayersPickerBottomSheetDialogFragment\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.maps.markers.MarkerDescription\nimport org.odk.collect.maps.markers.MarkerIconDescription\nimport org.odk.collect.material.BottomSheetBehavior\nimport org.odk.collect.material.MaterialProgressDialogFragment\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.webpage.WebPageService\nimport javax.inject.Inject\n\n/**\n * Can be used to allow an item to be selected from a map. Items can be provided using an\n * implementation of [SelectionMapData].\n */\nclass SelectionMapFragment(\n    val selectionMapData: SelectionMapData,\n    val skipSummary: Boolean = false,\n    val zoomToFitItems: Boolean = true,\n    val showNewItemButton: Boolean = true,\n    val onBackPressedDispatcher: (() -> OnBackPressedDispatcher)? = null\n) : Fragment() {\n\n    @Inject\n    lateinit var mapFragmentFactory: MapFragmentFactory\n\n    @Inject\n    lateinit var permissionsChecker: PermissionsChecker\n\n    @Inject\n    lateinit var referenceLayerRepository: ReferenceLayerRepository\n\n    @Inject\n    lateinit var scheduler: Scheduler\n\n    @Inject\n    lateinit var settingsProvider: SettingsProvider\n\n    @Inject\n    lateinit var webPageService: WebPageService\n\n    private val selectedItemViewModel by viewModels<SelectedItemViewModel>()\n\n    private lateinit var map: MapFragment\n    private lateinit var summarySheetBehavior: BottomSheetBehavior<*>\n    private lateinit var summarySheet: SelectionSummarySheet\n    private lateinit var bottomSheetCallback: BottomSheetCallback\n\n    private val itemsByFeatureId: MutableMap<Int, MappableSelectItem> = mutableMapOf()\n    private val featureIdsByItemId: MutableMap<Long, Int> = mutableMapOf()\n\n    /**\n     * Points to be mapped. Note: kept separately from [.itemsByFeatureId] so we can\n     * quickly zoom to bounding box.\n     */\n    private val points: MutableList<MapPoint> = mutableListOf()\n    private var itemCount: Int = 0\n    private var featureCount: Int = 0\n\n    private var previousState: Bundle? = null\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        childFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(MapFragment::class.java) {\n                mapFragmentFactory.createMapFragment() as Fragment\n            }\n            .forClass(OfflineMapLayersPickerBottomSheetDialogFragment::class) {\n                OfflineMapLayersPickerBottomSheetDialogFragment(requireActivity().activityResultRegistry, referenceLayerRepository, scheduler, settingsProvider, webPageService)\n            }\n            .build()\n\n        super.onCreate(savedInstanceState)\n        previousState = savedInstanceState\n    }\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n\n        val component =\n            (context.applicationContext as GeoDependencyComponentProvider).geoDependencyComponent\n        component.inject(this)\n\n        if (!permissionsChecker.isPermissionGranted(\n                Manifest.permission.ACCESS_FINE_LOCATION,\n                Manifest.permission.ACCESS_COARSE_LOCATION\n            )\n        ) {\n            ToastUtils.showLongToast(org.odk.collect.strings.R.string.not_granted_permission)\n            requireActivity().finish()\n        }\n\n        MaterialProgressDialogFragment.showOn(\n            this,\n            selectionMapData.isLoading(),\n            childFragmentManager\n        ) {\n            MaterialProgressDialogFragment().also { dialog ->\n                dialog.message = getString(org.odk.collect.strings.R.string.loading)\n            }\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return SelectionMapLayoutBinding.inflate(inflater).root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        val binding = SelectionMapLayoutBinding.bind(view)\n\n        val mapFragment = binding.mapContainer.getFragment<Fragment?>() as MapFragment\n        mapFragment.init(\n            { newMapFragment -> initMap(newMapFragment, binding) },\n            { requireActivity().finish() }\n        )\n\n        selectionMapData.getMapTitle().observe(viewLifecycleOwner) {\n            binding.title.text = it\n        }\n\n        selectionMapData.getItemCount().observe(viewLifecycleOwner) {\n            itemCount = it\n            updateCounts(binding)\n        }\n\n        setUpSummarySheet(binding)\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n\n        if (!::map.isInitialized) {\n            // initMap() is called asynchronously, so map can be null if the activity\n            // is stopped (e.g. by screen rotation) before initMap() gets to run.\n            // In this case, preserve any provided instance state.\n            if (previousState != null) {\n                outState.putAll(previousState)\n            }\n\n            return\n        }\n    }\n\n    override fun onDestroy() {\n        if (this::summarySheetBehavior.isInitialized) {\n            summarySheetBehavior.removeBottomSheetCallback(bottomSheetCallback)\n        }\n\n        super.onDestroy()\n    }\n\n    @SuppressLint(\"MissingPermission\") // Permission handled in Constructor\n    private fun initMap(newMapFragment: MapFragment, binding: SelectionMapLayoutBinding) {\n        map = newMapFragment\n\n        binding.zoomToLocation.setMultiClickSafeOnClickListener {\n            map.zoomToCurrentLocation(map.getGpsLocation())\n        }\n\n        binding.zoomToBounds.setMultiClickSafeOnClickListener {\n            map.zoomToBoundingBox(points, 0.8, false)\n        }\n\n        binding.layerMenu.setMultiClickSafeOnClickListener {\n            DialogFragmentUtils.showIfNotShowing(\n                OfflineMapLayersPickerBottomSheetDialogFragment::class.java,\n                childFragmentManager\n            )\n        }\n\n        if (showNewItemButton) {\n            binding.newItem.setMultiClickSafeOnClickListener {\n                parentFragmentManager.setFragmentResult(\n                    REQUEST_SELECT_ITEM,\n                    Bundle().also {\n                        it.putBoolean(RESULT_CREATE_NEW_ITEM, true)\n                    }\n                )\n            }\n        } else {\n            binding.newItem.visibility = View.GONE\n        }\n\n        map.setGpsLocationEnabled(true)\n\n        map.setFeatureClickListener(::onFeatureSelected)\n        map.setClickListener { onClick() }\n\n        selectionMapData.getMappableItems().observe(viewLifecycleOwner) {\n            if (it != null) {\n                updateItems(it)\n                updateCounts(binding)\n            }\n        }\n    }\n\n    private fun updateCounts(binding: SelectionMapLayoutBinding) {\n        binding.geometryStatus.text = getString(\n            org.odk.collect.strings.R.string.select_item_count,\n            selectionMapData.getItemType(),\n            itemCount,\n            featureCount\n        )\n    }\n\n    private fun setUpSummarySheet(binding: SelectionMapLayoutBinding) {\n        summarySheet = binding.summarySheet\n        summarySheetBehavior = BottomSheetBehavior.from(summarySheet)\n        summarySheetBehavior.state = STATE_HIDDEN\n\n        val closeSummarySheet = object : OnBackPressedCallback(false) {\n            override fun handleOnBackPressed() {\n                summarySheetBehavior.state = STATE_HIDDEN\n            }\n        }\n\n        (onBackPressedDispatcher?.invoke() ?: requireActivity().onBackPressedDispatcher).addCallback(\n            viewLifecycleOwner,\n            closeSummarySheet\n        )\n\n        bottomSheetCallback = object : BottomSheetCallback() {\n            override fun onStateChanged(onStateChangedbottomSheet: View, newState: Int) {\n                val selectedItem = selectedItemViewModel.getSelectedItem()\n                if (newState == STATE_HIDDEN && selectedItem != null) {\n                    selectedItemViewModel.setSelectedItem(null)\n                    if (selectedItem is MappableSelectItem.MappableSelectPoint) {\n                        resetIcon(selectedItem)\n                    }\n\n                    closeSummarySheet.isEnabled = false\n                } else {\n                    closeSummarySheet.isEnabled = true\n                }\n            }\n\n            override fun onSlide(bottomSheet: View, slideOffset: Float) {}\n        }\n\n        summarySheetBehavior.addBottomSheetCallback(bottomSheetCallback)\n\n        summarySheet.listener = object : SelectionSummarySheet.Listener {\n            override fun selectionAction(id: Long) {\n                summarySheetBehavior.state = STATE_HIDDEN\n\n                parentFragmentManager.setFragmentResult(\n                    REQUEST_SELECT_ITEM,\n                    Bundle().also {\n                        it.putLong(RESULT_SELECTED_ITEM, id)\n                    }\n                )\n            }\n        }\n    }\n\n    private fun onFeatureSelected(featureId: Int, maintainZoom: Boolean = true, selectedByUser: Boolean = true) {\n        val item = itemsByFeatureId[featureId]\n        val selectedItem = selectedItemViewModel.getSelectedItem()\n\n        if (item != null) {\n            if (selectedItem != null && selectedItem.id != item.id && selectedItem is MappableSelectItem.MappableSelectPoint) {\n                resetIcon(selectedItem)\n            }\n\n            if (skipSummary && selectedByUser) {\n                parentFragmentManager.setFragmentResult(\n                    REQUEST_SELECT_ITEM,\n                    Bundle().also {\n                        it.putLong(RESULT_SELECTED_ITEM, item.id)\n                    }\n                )\n            } else {\n                when (item) {\n                    is MappableSelectItem.MappableSelectLine -> map.zoomToBoundingBox(item.points, 0.8, true)\n                    is MappableSelectItem.MappableSelectPolygon -> map.zoomToBoundingBox(item.points, 0.8, true)\n                    is MappableSelectItem.MappableSelectPoint -> {\n                        val point = item.point\n\n                        if (maintainZoom) {\n                            map.zoomToPoint(MapPoint(point.latitude, point.longitude), map.getZoom(), true)\n                        } else {\n                            map.zoomToPoint(MapPoint(point.latitude, point.longitude), true)\n                        }\n\n                        map.setMarkerIcon(\n                            featureId,\n                            MarkerIconDescription.DrawableResource(item.largeIcon, item.color, item.symbol)\n                        )\n                    }\n                }\n\n                summarySheet.setItem(item)\n\n                summarySheetBehavior.state = STATE_COLLAPSED\n                summarySheet.viewTreeObserver.addOnGlobalLayoutListener(\n                    object : ViewTreeObserver.OnGlobalLayoutListener {\n                        override fun onGlobalLayout() {\n                            summarySheet.viewTreeObserver.removeOnGlobalLayoutListener(this)\n                            summarySheetBehavior.peekHeight = summarySheet.peekHeight\n                        }\n                    }\n                )\n\n                selectedItemViewModel.setSelectedItem(item)\n            }\n        }\n    }\n\n    private fun onClick() {\n        summarySheetBehavior.state = STATE_HIDDEN\n    }\n\n    private fun updateItems(items: List<MappableSelectItem>) {\n        if (!::map.isInitialized) {\n            return\n        }\n\n        updateFeatures(items)\n\n        val previouslySelectedItem =\n            itemsByFeatureId.filter { it.value.selected }.map { it.key }.firstOrNull()\n        val selectedItem = selectedItemViewModel.getSelectedItem()\n\n        if (selectedItem != null) {\n            val featureId = featureIdsByItemId[selectedItem.id]\n            if (featureId != null) {\n                onFeatureSelected(featureId, selectedByUser = false)\n            }\n        } else if (previouslySelectedItem != null) {\n            onFeatureSelected(previouslySelectedItem, maintainZoom = false, selectedByUser = false)\n        } else if (!map.hasCenter()) {\n            if (zoomToFitItems && points.isNotEmpty()) {\n                map.zoomToBoundingBox(points, 0.8, false)\n            } else {\n                map.setGpsLocationListener { point ->\n                    map.zoomToCurrentLocation(point)\n                    map.setGpsLocationListener(null)\n                }\n            }\n        }\n    }\n\n    private fun resetIcon(selectedItem: MappableSelectItem.MappableSelectPoint) {\n        val featureId = featureIdsByItemId[selectedItem.id]\n        if (featureId != null) {\n            map.setMarkerIcon(\n                featureId,\n                MarkerIconDescription.DrawableResource(selectedItem.smallIcon, selectedItem.color, selectedItem.symbol)\n            )\n        }\n    }\n\n    /**\n     * Clears the existing features on the map and places features for the current form's instances.\n     */\n    private fun updateFeatures(items: List<MappableSelectItem>) {\n        points.clear()\n        map.clearFeatures()\n        itemsByFeatureId.clear()\n\n        val singlePoints = items.filterIsInstance<MappableSelectItem.MappableSelectPoint>()\n        val lines = items.filterIsInstance<MappableSelectItem.MappableSelectLine>()\n        val polygons = items.filterIsInstance<MappableSelectItem.MappableSelectPolygon>()\n\n        val markerDescriptions = singlePoints.map {\n            MarkerDescription(\n                MapPoint(it.point.latitude, it.point.longitude),\n                false,\n                MapFragment.IconAnchor.BOTTOM,\n                MarkerIconDescription.DrawableResource(it.smallIcon, it.color, it.symbol)\n            )\n        }\n\n        val pointIds = map.addMarkers(markerDescriptions)\n        val lineIds = lines.fold(listOf<Int>()) { ids, item ->\n            ids + map.addPolyLine(\n                LineDescription(\n                    item.points,\n                    item.strokeWidth,\n                    item.strokeColor?.sanitizeToColorInt()\n                )\n            )\n        }\n        val polygonIds = polygons.fold(listOf<Int>()) { ids, item ->\n            ids + map.addPolygon(\n                PolygonDescription(\n                    item.points,\n                    item.strokeWidth,\n                    item.strokeColor?.sanitizeToColorInt(),\n                    item.fillColor?.sanitizeToColorInt()\n                )\n            )\n        }\n\n        (singlePoints + lines + polygons).zip(pointIds + lineIds + polygonIds).forEach { (item, featureId) ->\n            itemsByFeatureId[featureId] = item\n            featureIdsByItemId[item.id] = featureId\n            when (item) {\n                is MappableSelectItem.MappableSelectPoint -> points.add(item.point)\n                is MappableSelectItem.MappableSelectLine -> points.addAll(item.points)\n                is MappableSelectItem.MappableSelectPolygon -> points.addAll(item.points)\n            }\n        }\n\n        featureCount = items.size\n    }\n\n    companion object {\n        const val REQUEST_SELECT_ITEM = \"select_item\"\n        const val RESULT_SELECTED_ITEM = \"selected_item\"\n        const val RESULT_CREATE_NEW_ITEM = \"create_new_item\"\n    }\n}\n\ninternal class SelectedItemViewModel : ViewModel() {\n\n    private var selectedItem: MappableSelectItem? = null\n\n    fun getSelectedItem(): MappableSelectItem? {\n        return selectedItem\n    }\n\n    fun setSelectedItem(item: MappableSelectItem?) {\n        selectedItem = item\n    }\n}\n\ninterface SelectionMapData {\n    fun isLoading(): NonNullLiveData<Boolean>\n    fun getMapTitle(): LiveData<String?>\n    fun getItemType(): String\n    fun getItemCount(): NonNullLiveData<Int>\n    fun getMappableItems(): LiveData<List<MappableSelectItem>?>\n}\n"
  },
  {
    "path": "geo/src/main/java/org/odk/collect/geo/selection/SelectionSummarySheet.kt",
    "content": "package org.odk.collect.geo.selection\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.FrameLayout\nimport androidx.core.content.ContextCompat\nimport androidx.core.view.get\nimport androidx.core.view.size\nimport org.odk.collect.geo.R\nimport org.odk.collect.geo.databinding.PropertyBinding\nimport org.odk.collect.geo.databinding.SelectionSummarySheetLayoutBinding\n\ninternal class SelectionSummarySheet(context: Context, attrs: AttributeSet?) :\n    FrameLayout(context, attrs) {\n\n    constructor(context: Context) : this(context, null)\n\n    val binding =\n        SelectionSummarySheetLayoutBinding.inflate(LayoutInflater.from(context), this, true)\n\n    var listener: Listener? = null\n\n    val peekHeight: Int\n        get() {\n            return when (binding.properties.size) {\n                0 -> binding.properties.top\n                1 -> binding.properties.top + binding.properties[0].bottom\n                else -> {\n                    val bottomOfFirstProp = binding.properties.top + binding.properties[0].bottom\n                    val secondPropHeight = binding.properties[1].bottom - binding.properties[1].top\n                    val enoughOfSecondPropToImplyMoreProps = (secondPropHeight / 12) * 7\n                    bottomOfFirstProp + enoughOfSecondPropToImplyMoreProps\n                }\n            }\n        }\n\n    private var itemId: Long? = null\n\n    init {\n        binding.action.setOnClickListener {\n            itemId?.let { listener?.selectionAction(it) }\n        }\n    }\n\n    fun setItem(item: MappableSelectItem) {\n        itemId = item.id\n        when (item.status) {\n            Status.ERRORS -> binding.statusChip.errors = true\n            Status.NO_ERRORS -> binding.statusChip.errors = false\n            else -> binding.statusChip.visibility = View.GONE\n        }\n\n        binding.name.text = item.name\n\n        binding.properties.removeAllViews()\n        item.properties.forEach {\n            val property = PropertyBinding.bind(\n                LayoutInflater.from(context).inflate(R.layout.property, binding.properties, false)\n            )\n\n            property.text.text = it.text\n\n            if (it.icon != null) {\n                property.icon.setImageDrawable(ContextCompat.getDrawable(context, it.icon))\n                property.icon.background = null\n            } else {\n                property.icon.visibility = View.GONE\n            }\n\n            binding.properties.addView(property.root)\n        }\n\n        item.action?.let {\n            binding.action.text = it.text\n\n            if (it.icon != null) {\n                binding.action.icon = ContextCompat.getDrawable(context, it.icon)\n            }\n\n            binding.action.visibility = View.VISIBLE\n        }\n\n        item.info?.let {\n            binding.info.text = item.info\n            binding.info.visibility = View.VISIBLE\n        }\n    }\n\n    interface Listener {\n        fun selectionAction(id: Long)\n    }\n}\n"
  },
  {
    "path": "geo/src/main/res/color/fab_surface_background_color_less_transparent_disabled.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"?attr/colorSurface\" android:state_enabled=\"true\"/>\n    <item android:alpha=\"@dimen/medium_emphasis\" android:color=\"?attr/colorSurface\"/>\n</selector>"
  },
  {
    "path": "geo/src/main/res/drawable/ic_add_location.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"36\"\n    android:viewportHeight=\"36\"\n    android:tint=\"?colorOnSurface\">\n\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M 18.303977,0.22161146 c 6.84699,0 12.416893,5.56990284 12.416893,12.41689354 0,9.312691 -12.416893,23.059967 -12.416893,23.059967 0,0 -12.4168929,-13.747276 -12.4168929,-23.059967 C 5.887001,5.7915143 11.456904,0.22161146 18.303977,0.22161146 Z m -7.095439,14.19079354 5.321537,0 0,5.321536 3.54772,0 0,-5.321536 5.321537,0 0,-3.547717 -5.321537,0 0,-5.3215407 -3.54772,0 0,5.3215407 -5.321537,0 0,3.547717 z\" />\n</vector>"
  },
  {
    "path": "geo/src/main/res/drawable/ic_backspace.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M22,3L7,3c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM19,15.59L17.59,17 14,13.41 10.41,17 9,15.59 12.59,12 9,8.41 10.41,7 14,10.59 17.59,7 19,8.41 15.41,12 19,15.59z\"/>\n</vector>\n"
  },
  {
    "path": "geo/src/main/res/drawable/ic_crop_frame.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"36dp\"\n        android:height=\"36dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\"\n        android:tint=\"@android:color/black\">\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M3,5v4h2L5,5h4L9,3L5,3c-1.1,0 -2,0.9 -2,2zM5,15L3,15v4c0,1.1 0.9,2 2,2h4v-2L5,19v-4zM19,19h-4v2h4c1.1,0 2,-0.9 2,-2v-4h-2v4zM19,3h-4v2h4v4h2L21,5c0,-1.1 -0.9,-2 -2,-2z\"/>\n</vector>\n"
  },
  {
    "path": "geo/src/main/res/drawable/ic_distance.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"33.78818\"\n    android:viewportHeight=\"33.78818\"\n    android:tint=\"#179588\"\n    android:autoMirrored=\"true\">\n  <path\n      android:pathData=\"m16.0701,19.3663c0,-3.1721 -2.5807,-5.7527 -5.7528,-5.7527 -3.1721,0 -5.7527,2.5807 -5.7527,5.7527 0,0.9296 0.2218,1.8082 0.615,2.586l5.1376,10.5949 5.1386,-10.5965c0.3927,-0.7775 0.6143,-1.6555 0.6143,-2.5844zM10.3173,22.5499c-1.7554,0 -3.1836,-1.4282 -3.1836,-3.1836 0,-1.7554 1.4282,-3.1836 3.1836,-3.1836 1.7554,0 3.1836,1.4282 3.1836,3.1836 0,1.7554 -1.4281,3.1836 -3.1836,3.1836z\"\n      android:fillColor=\"#179588\"/>\n  <path\n      android:pathData=\"m26.6181,1.209c-1.6062,0 -2.9131,1.3068 -2.9131,2.913 0,0.4707 0.1124,0.9156 0.3115,1.3096l2.6016,5.365 2.602,-5.3658c0.1988,-0.3937 0.3111,-0.8383 0.3111,-1.3087C29.5311,2.5158 28.2242,1.209 26.6181,1.209ZM26.6181,5.7341c-0.8889,0 -1.6121,-0.7232 -1.6121,-1.6121 0,-0.8889 0.7232,-1.6121 1.6121,-1.6121 0.8888,0 1.612,0.7232 1.612,1.6121 0,0.8889 -0.7232,1.6121 -1.612,1.6121z\"\n      android:fillColor=\"#179588\"/>\n  <path\n      android:pathData=\"m24.0809,20.1072c-0.1182,-0.5558 -0.2717,-1.1005 -0.465,-1.6352 -0.2592,-0.7165 -0.57,-1.4167 -0.8081,-2.1414 -0.445,-1.3546 0.3227,-2.485 0.4669,-2.6905 0.2528,-0.3598 0.5483,-0.69 0.8602,-0.9992 0.6004,-0.5953 1.2626,-1.1274 1.9554,-1.6175 -0.0858,-0.133 -0.169,-0.2674 -0.2481,-0.4043 -0.9964,0.3906 -6.1206,2.1815 -5.9957,6.138 0.025,0.7931 0.3315,1.548 0.5203,2.3168 0.091,0.3705 0.1699,0.7442 0.2181,1.1229 0.0208,0.1705 0.0336,0.3419 0.0475,0.5132 0.0004,-0.0013 0.0003,-0.0022 -0.0005,-0.0029 0.0005,0.005 0.0132,0.543 -0.0107,0.776 -0.0748,0.7297 -0.1825,1.4669 -0.4901,2.1363 -0.2861,0.6223 -0.7177,1.1636 -1.2517,1.5914 -1.2205,0.9778 -2.8377,1.3687 -4.3637,1.5259 -0.0726,0.0076 -0.1453,0.0142 -0.2179,0.0205l-2.7246,5.6183c1.3443,-0.0835 2.6852,-0.2675 3.9934,-0.5895 1.5395,-0.379 3.0398,-0.9513 4.3842,-1.7998 1.3432,-0.8475 2.4982,-1.9941 3.2672,-3.3901 0.8044,-1.4602 1.1267,-3.1328 1.0581,-4.7901 -0.0233,-0.5661 -0.077,-1.1432 -0.1952,-1.6988z\"\n      android:fillColor=\"#179588\"/>\n</vector>\n"
  },
  {
    "path": "geo/src/main/res/drawable/ic_info.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n      \n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z\"/>\n</vector>\n"
  },
  {
    "path": "geo/src/main/res/drawable/ic_layers.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z\"/>\n</vector>\n"
  },
  {
    "path": "geo/src/main/res/drawable/ic_my_location.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z\"/>\n</vector>\n"
  },
  {
    "path": "geo/src/main/res/drawable/ic_note_add.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"36dp\"\n        android:height=\"36dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\"\n        android:tint=\"@android:color/black\"\n        android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,16h-3v3h-2v-3L8,16v-2h3v-3h2v3h3v2zM13,9L13,3.5L18.5,9L13,9z\"/>\n</vector>\n"
  },
  {
    "path": "geo/src/main/res/drawable/ic_pause_36.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z\" />\n</vector>\n"
  },
  {
    "path": "geo/src/main/res/drawable/property_divider.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <!-- Material Design reference: https://material.io/design/components/dividers.html#specs -->\n\n    <item>\n        <shape android:shape=\"rectangle\">\n            <size android:height=\"1dp\" />\n            <solid android:color=\"@color/color_on_surface_low_emphasis\"/>\n        </shape>\n    </item>\n\n</layer-list>\n"
  },
  {
    "path": "geo/src/main/res/layout/accuracy_progress_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?colorPrimary\"\n    android:paddingHorizontal=\"@dimen/margin_standard\"\n    android:paddingTop=\"@dimen/margin_standard\"\n    android:paddingBottom=\"@dimen/margin_large\">\n\n    <TextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"start\"\n        android:text=\"@string/accuracy_status_title\"\n        android:textAppearance=\"?textAppearanceOverline\"\n        android:textColor=\"?colorOnPrimary\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <TextView\n        android:id=\"@+id/current_accuracy\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:text=\"@string/empty_accuracy\"\n        android:textAppearance=\"?textAppearanceHeadline3\"\n        android:textColor=\"?colorOnPrimary\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/title\" />\n\n    <TextView\n        android:id=\"@+id/text\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:gravity=\"center\"\n        android:text=\"@string/waiting_for_location\"\n        android:textAppearance=\"?textAppearanceSubtitle1\"\n        android:textColor=\"?colorOnPrimary\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/current_accuracy\" />\n\n    <com.google.android.material.progressindicator.LinearProgressIndicator\n        android:id=\"@+id/strength\"\n        android:layout_width=\"240dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n        android:progress=\"20\"\n        app:indicatorColor=\"?colorOnPrimary\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/text\"\n        app:trackThickness=\"16dp\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n\n"
  },
  {
    "path": "geo/src/main/res/layout/accuracy_status_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\"\n    android:paddingHorizontal=\"@dimen/margin_extra_small\"\n    android:paddingVertical=\"@dimen/margin_extra_small\">\n\n    <TextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginBottom=\"@dimen/margin_extra_extra_small\"\n        android:textAppearance=\"?textAppearanceLabelLarge\" />\n\n    <TextView\n        android:id=\"@+id/location_status\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/please_wait_long\"\n        android:textAppearance=\"?textAppearanceBodyMedium\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "geo/src/main/res/layout/geopoint_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/guideline_start\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_begin=\"@dimen/margin_standard\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/guideline_end\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_end=\"@dimen/margin_standard\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/guideline_top\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            app:layout_constraintGuide_begin=\"@dimen/margin_standard\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/guideline_bottom\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            app:layout_constraintGuide_end=\"@dimen/margin_standard\" />\n\n        <org.odk.collect.geo.geopoint.AccuracyProgressView\n            android:id=\"@+id/accuracy_status\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n        <TextView\n            android:id=\"@+id/threshold\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_standard\"\n            android:textAppearance=\"?textAppearanceBody2\"\n            app:layout_constraintStart_toStartOf=\"@id/guideline_start\"\n            app:layout_constraintTop_toBottomOf=\"@id/accuracy_status\"\n            tools:text=\"Point will be saved at 1m\" />\n\n        <TextView\n            android:id=\"@+id/time\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:textAppearance=\"?textAppearanceBody2\"\n            app:layout_constraintStart_toStartOf=\"@id/guideline_start\"\n            app:layout_constraintTop_toBottomOf=\"@id/threshold\"\n            tools:text=\"Time elapsed: 03:52\" />\n\n        <TextView\n            android:id=\"@+id/satellites\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:textAppearance=\"?textAppearanceBody2\"\n            app:layout_constraintBottom_toTopOf=\"@id/guideline_bottom\"\n            app:layout_constraintStart_toStartOf=\"@id/guideline_start\"\n            app:layout_constraintTop_toBottomOf=\"@id/time\"\n            tools:text=\"Satellites available: 3/24\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n</androidx.core.widget.NestedScrollView>\n"
  },
  {
    "path": "geo/src/main/res/layout/geopoint_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.geo.geopoint.AccuracyStatusView\n        android:id=\"@+id/status_section\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:title=\"@string/geopoint_instruction\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/map_container\"\n        android:name=\"org.odk.collect.maps.MapFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toBottomOf=\"@id/status_section\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/zoom\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/show_my_location\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/status_section\"\n        app:srcCompat=\"@drawable/ic_my_location\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/layer_menu\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/layer_data_file\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/zoom\"\n        app:srcCompat=\"@drawable/ic_layers\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/place_marker\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/record_geopoint\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toTopOf=\"@id/clear\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_add_location\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/clear\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/clear\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toTopOf=\"@id/accept_location\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_delete\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/accept_location\"\n        style=\"@style/Widget.Material3.FloatingActionButton.Primary\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/save\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_save\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "geo/src/main/res/layout/geopoly_dialog.xml",
    "content": "<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_height=\"wrap_content\"\n    android:layout_width=\"wrap_content\">\n\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <RadioGroup\n            android:id=\"@+id/radio_group\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\">\n\n            <RadioButton\n                android:id=\"@+id/placement_mode\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"10dp\"\n                android:layout_marginStart=\"10dp\"\n                android:layout_marginTop=\"10dp\"\n                android:text=\"@string/placement_mode\"/>\n\n            <RadioButton\n                android:id=\"@+id/manual_mode\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"10dp\"\n                android:layout_marginStart=\"10dp\"\n                android:text=\"@string/manual_mode\"/>\n\n            <RadioButton\n                android:id=\"@+id/automatic_mode\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"10dp\"\n                android:layout_marginStart=\"10dp\"\n                android:text=\"@string/automatic_mode\"/>\n\n        </RadioGroup>\n\n        <LinearLayout\n            android:id=\"@+id/auto_options\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\">\n\n            <LinearLayout\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"10dp\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:paddingStart=\"42dp\"\n                    android:text=\"@string/recording_interval\"/>\n\n                <Spinner\n                    android:id=\"@+id/auto_interval\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"/>\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:paddingStart=\"42dp\"\n                    android:text=\"@string/accuracy_requirement\"/>\n\n                <Spinner\n                    android:id=\"@+id/accuracy_threshold\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"/>\n            </LinearLayout>\n\n        </LinearLayout>\n\n    </LinearLayout>\n\n</ScrollView>"
  },
  {
    "path": "geo/src/main/res/layout/geopoly_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.geo.geopoint.AccuracyStatusView\n        android:id=\"@+id/location_status\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/map_container\"\n        android:name=\"org.odk.collect.maps.MapFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintBottom_toTopOf=\"@id/collection_status\"\n        app:layout_constraintTop_toBottomOf=\"@id/location_status\" />\n\n    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton\n        android:id=\"@+id/record_button\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/record_geopoint\"\n        android:text=\"@string/record_geopoint\"\n        android:visibility=\"gone\"\n        app:icon=\"@drawable/ic_distance\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/location_status\"\n        tools:visibility=\"visible\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/zoom\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/show_my_location\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/location_status\"\n        app:srcCompat=\"@drawable/ic_my_location\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/layers\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/layer_data_file\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/zoom\"\n        app:srcCompat=\"@drawable/ic_layers\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/info\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/show_how_to_modify_map\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/layers\"\n        app:srcCompat=\"@drawable/ic_info\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/play\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/input_method\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toTopOf=\"@id/pause\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_add_location\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/pause\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/pause_location_recording\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        android:visibility=\"gone\"\n        app:layout_constraintBottom_toTopOf=\"@id/backspace\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_pause_36\"\n        tools:visibility=\"visible\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/backspace\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/remove_last_point\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toTopOf=\"@id/clear\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_backspace\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/clear\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/clear\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toTopOf=\"@id/save\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_delete\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/save\"\n        style=\"@style/Widget.Material3.FloatingActionButton.Primary\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/save\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toTopOf=\"@id/collection_status\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_save\" />\n\n    <TextView\n        android:id=\"@+id/collection_status\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"#BF000000\"\n        android:paddingLeft=\"5dip\"\n        android:paddingTop=\"5dip\"\n        android:paddingRight=\"5dip\"\n        android:paddingBottom=\"5dip\"\n        android:text=\"\"\n        android:textColor=\"#FFFFFF\"\n        app:layout_constraintBottom_toBottomOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "geo/src/main/res/layout/property.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:minHeight=\"46dp\"\n    android:paddingVertical=\"@dimen/margin_extra_small\"\n    android:paddingHorizontal=\"@dimen/margin_standard\">\n\n    <ImageButton\n        android:id=\"@+id/icon\"\n        android:layout_width=\"30dp\"\n        android:layout_height=\"30dp\"\n        android:layout_marginEnd=\"@dimen/margin_extra_small\"\n        app:layout_constraintEnd_toStartOf=\"@id/text\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:tint=\"?colorPrimary\" />\n\n    <TextView\n        android:id=\"@+id/text\"\n        style=\"?textAppearanceBodyLarge\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:textColor=\"?colorOnSurface\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintStart_toEndOf=\"@id/icon\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:text=\"Status\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "geo/src/main/res/layout/selection_map_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <TextView\n            android:id=\"@+id/title\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"#BF000000\"\n            android:gravity=\"start\"\n            android:padding=\"4dp\"\n            android:textColor=\"#FFFFFF\" />\n\n        <TextView\n            android:id=\"@+id/geometry_status\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentBottom=\"true\"\n            android:background=\"#BF000000\"\n            android:gravity=\"start\"\n            android:padding=\"4dp\"\n            android:textColor=\"#FFFFFF\" />\n\n        <androidx.fragment.app.FragmentContainerView\n            android:id=\"@+id/map_container\"\n            android:name=\"org.odk.collect.maps.MapFragment\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:layout_above=\"@id/geometry_status\"\n            android:layout_below=\"@id/title\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/new_item\"\n            style=\"@style/Widget.Material3.FloatingActionButton.Primary\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@id/geometry_status\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_marginHorizontal=\"@dimen/margin_standard\"\n            android:layout_marginBottom=\"@dimen/margin_standard\"\n            android:contentDescription=\"@string/new_item\"\n            android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n            app:srcCompat=\"@drawable/ic_note_add\" />\n\n        <ScrollView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@+id/title\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_marginBottom=\"100dp\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\">\n\n                <com.google.android.material.floatingactionbutton.FloatingActionButton\n                    android:id=\"@+id/zoom_to_location\"\n                    style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_marginHorizontal=\"@dimen/margin_standard\"\n                    android:layout_marginTop=\"@dimen/margin_standard\"\n                    android:contentDescription=\"@string/show_my_location\"\n                    android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n                    app:srcCompat=\"@drawable/ic_my_location\" />\n\n                <com.google.android.material.floatingactionbutton.FloatingActionButton\n                    android:id=\"@+id/zoom_to_bounds\"\n                    style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_marginHorizontal=\"@dimen/margin_standard\"\n                    android:layout_marginTop=\"@dimen/margin_standard\"\n                    android:contentDescription=\"@string/zoom_to_fit_all\"\n                    android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n                    app:srcCompat=\"@drawable/ic_crop_frame\" />\n\n                <com.google.android.material.floatingactionbutton.FloatingActionButton\n                    android:id=\"@+id/layer_menu\"\n                    style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_marginHorizontal=\"@dimen/margin_standard\"\n                    android:layout_marginTop=\"@dimen/margin_standard\"\n                    android:contentDescription=\"@string/layer_data_file\"\n                    android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n                    app:srcCompat=\"@drawable/ic_layers\" />\n            </LinearLayout>\n        </ScrollView>\n    </RelativeLayout>\n\n    <org.odk.collect.geo.selection.SelectionSummarySheet\n        android:id=\"@+id/summary_sheet\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"?colorSurface\"\n        app:behavior_hideable=\"true\"\n        android:maxWidth=\"@null\"\n        app:layout_behavior=\"com.google.android.material.bottomsheet.BottomSheetBehavior\"\n        style=\"@style/Widget.Material3.BottomSheet\"\n        tools:visibility=\"gone\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "geo/src/main/res/layout/selection_summary_sheet_layout.xml",
    "content": "<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingBottom=\"@dimen/margin_standard\">\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/guideline_start\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_begin=\"@dimen/margin_standard\" />\n\n        <androidx.constraintlayout.widget.Guideline\n            android:id=\"@+id/guideline_end\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:layout_constraintGuide_end=\"@dimen/margin_standard\" />\n\n        <com.google.android.material.bottomsheet.BottomSheetDragHandleView\n            android:id=\"@+id/drag_handle\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintStart_toEndOf=\"@id/guideline_start\"\n            app:layout_constraintEnd_toStartOf=\"@id/guideline_end\"\n            app:layout_constraintTop_toTopOf=\"parent\"/>\n\n        <org.odk.collect.material.ErrorsPill\n            android:id=\"@+id/status_chip\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:layout_constraintStart_toEndOf=\"@id/guideline_start\"\n            app:layout_constraintTop_toBottomOf=\"@id/drag_handle\" />\n\n        <TextView\n            android:id=\"@+id/name\"\n            style=\"?textAppearanceHeadline6\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:textColor=\"?colorOnSurface\"\n            android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n            app:layout_constraintStart_toEndOf=\"@id/guideline_start\"\n            app:layout_constraintEnd_toStartOf=\"@id/guideline_end\"\n            app:layout_constraintTop_toBottomOf=\"@id/status_chip\"\n            tools:text=\"Name\" />\n\n        <TextView\n            android:id=\"@+id/info\"\n            style=\"?textAppearanceBody2\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:textColor=\"@color/color_on_surface_medium_emphasis\"\n            android:visibility=\"gone\"\n            app:layout_constraintStart_toEndOf=\"@id/guideline_start\"\n            app:layout_constraintEnd_toStartOf=\"@id/guideline_end\"\n            app:layout_constraintTop_toBottomOf=\"@id/name\"\n            tools:text=\"Info\"\n            tools:visibility=\"visible\" />\n\n        <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n            android:id=\"@+id/action\"\n            style=\"?materialButtonIconStyle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:visibility=\"gone\"\n            app:layout_constraintStart_toEndOf=\"@id/guideline_start\"\n            app:layout_constraintTop_toBottomOf=\"@id/info\"\n            tools:icon=\"@drawable/ic_delete\"\n            tools:text=\"Action\"\n            tools:visibility=\"visible\" />\n\n        <View\n            android:id=\"@+id/divider\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"1dp\"\n            android:layout_marginTop=\"@dimen/margin_extra_small\"\n            android:background=\"@drawable/property_divider\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/action\" />\n\n        <androidx.appcompat.widget.LinearLayoutCompat\n            android:id=\"@+id/properties\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            app:divider=\"@drawable/property_divider\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@id/divider\"\n            app:showDividers=\"middle\" />\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n</androidx.core.widget.NestedScrollView>\n"
  },
  {
    "path": "geo/src/main/res/layout/simple_spinner_dropdown_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@android:id/text1\"\n    style=\"?android:attr/spinnerDropDownItemStyle\"\n    android:singleLine=\"true\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"45dp\"\n    android:ellipsize=\"marquee\"/>"
  },
  {
    "path": "geo/src/main/res/layout-land/geopoint_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.geo.geopoint.AccuracyStatusView\n        android:id=\"@+id/status_section\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:title=\"@string/geopoint_instruction\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/map_container\"\n        android:name=\"org.odk.collect.maps.MapFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toBottomOf=\"@id/status_section\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/zoom\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/show_my_location\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/status_section\"\n        app:srcCompat=\"@drawable/ic_my_location\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/layer_menu\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/layer_data_file\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/zoom\"\n        app:srcCompat=\"@drawable/ic_layers\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/place_marker\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/record_geopoint\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toStartOf=\"@id/clear\"\n        app:srcCompat=\"@drawable/ic_add_location\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/clear\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/clear\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toStartOf=\"@id/accept_location\"\n        app:srcCompat=\"@drawable/ic_delete\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/accept_location\"\n        style=\"@style/Widget.Material3.FloatingActionButton.Primary\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/save\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_save\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "geo/src/main/res/layout-land/geopoly_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <org.odk.collect.geo.geopoint.AccuracyStatusView\n        android:id=\"@+id/location_status\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/map_container\"\n        android:name=\"org.odk.collect.maps.MapFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintBottom_toTopOf=\"@id/collection_status\"\n        app:layout_constraintTop_toBottomOf=\"@id/location_status\" />\n\n    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton\n        android:id=\"@+id/record_button\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:text=\"@string/record_geopoint\"\n        android:visibility=\"gone\"\n        app:icon=\"@drawable/ic_distance\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/location_status\"\n        tools:visibility=\"visible\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/zoom\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/show_my_location\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/location_status\"\n        app:srcCompat=\"@drawable/ic_my_location\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/layers\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/layer_data_file\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/zoom\"\n        app:srcCompat=\"@drawable/ic_layers\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/info\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Small.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/show_how_to_modify_map\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/layers\"\n        app:srcCompat=\"@drawable/ic_info\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/play\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        android:contentDescription=\"@string/input_method\"\n        app:layout_constraintBottom_toTopOf=\"@id/collection_status\"\n        app:layout_constraintEnd_toStartOf=\"@id/pause\"\n        app:srcCompat=\"@drawable/ic_add_location\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/pause\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/pause_location_recording\"\n        android:visibility=\"gone\"\n        app:layout_constraintBottom_toTopOf=\"@id/collection_status\"\n        app:layout_constraintEnd_toStartOf=\"@id/backspace\"\n        app:srcCompat=\"@drawable/ic_pause_36\"\n        tools:visibility=\"visible\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/backspace\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/remove_last_point\"\n        app:layout_constraintBottom_toTopOf=\"@id/collection_status\"\n        app:layout_constraintEnd_toStartOf=\"@id/clear\"\n        app:srcCompat=\"@drawable/ic_backspace\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/clear\"\n        style=\"@style/Widget.Geo.FloatingActionButton.Map.Surface\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        android:contentDescription=\"@string/clear\"\n        app:layout_constraintBottom_toTopOf=\"@id/collection_status\"\n        app:layout_constraintEnd_toStartOf=\"@id/save\"\n        app:srcCompat=\"@drawable/ic_delete\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/save\"\n        style=\"@style/Widget.Material3.FloatingActionButton.Primary\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_marginBottom=\"@dimen/margin_standard\"\n        android:contentDescription=\"@string/save\"\n        android:theme=\"@style/Theme.Geo.ForceLightSurface.Overlay\"\n        app:layout_constraintBottom_toTopOf=\"@id/collection_status\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_save\" />\n\n    <TextView\n        android:id=\"@+id/collection_status\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"#BF000000\"\n        android:paddingLeft=\"5dip\"\n        android:paddingTop=\"5dip\"\n        android:paddingRight=\"5dip\"\n        android:paddingBottom=\"5dip\"\n        android:text=\"\"\n        android:textColor=\"#FFFFFF\"\n        app:layout_constraintBottom_toBottomOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "geo/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"AccuracyStatusView\">\n        <attr name=\"title\" format=\"string\" />\n    </declare-styleable>\n</resources>\n"
  },
  {
    "path": "geo/src/main/res/values/fab_surface.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Uses a higher alpha value when disabled to make sure buttons are still visible over darker\nparts of the map (often a problem in satellite modes). -->\n<resources>\n    <style name=\"Widget.Geo.FloatingActionButton.Map.Small.Surface\" parent=\"Widget.Material3.FloatingActionButton.Small.Surface\">\n        <item name=\"backgroundTint\">@color/fab_surface_background_color_less_transparent_disabled</item>\n    </style>\n\n    <style name=\"Widget.Geo.FloatingActionButton.Map.Surface\" parent=\"Widget.Material3.FloatingActionButton.Surface\">\n        <item name=\"backgroundTint\">@color/fab_surface_background_color_less_transparent_disabled</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "geo/src/main/res/values/force_light_surface_overlay.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"Theme.Geo.ForceLightSurface.Overlay\" parent=\"\">\n        <item name=\"colorSurface\">#FFFFFF</item>\n        <item name=\"colorOnSurface\">#000000</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/GeoUtilsTest.kt",
    "content": "package org.odk.collect.geo\n\nimport android.content.Context\nimport android.location.Location\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.geo.GeoUtils.formatPointsResultString\nimport org.odk.collect.geo.GeoUtils.parseGeometryPoint\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.testshared.LocationTestUtils.createLocation\n\n@RunWith(AndroidJUnit4::class)\nclass GeoUtilsTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n    private val points = mutableListOf(\n        MapPoint(11.0, 12.0, 13.0, 14.0),\n        MapPoint(21.0, 22.0, 23.0, 24.0),\n        MapPoint(31.0, 32.0, 33.0, 34.0)\n    )\n\n    @Test\n    fun whenPointsAreNull_formatPoints_returnsEmptyString() {\n        assertThat(formatPointsResultString(mutableListOf(), true), equalTo(\"\"))\n        assertThat(formatPointsResultString(mutableListOf(), false), equalTo(\"\"))\n    }\n\n    @Test\n    fun geotraces_areSeparatedBySemicolon_withoutTrialingSemicolon() {\n        assertThat(\n            formatPointsResultString(points, false),\n            equalTo(\"11.0 12.0 13.0 14.0;21.0 22.0 23.0 24.0;31.0 32.0 33.0 34.0\")\n        )\n    }\n\n    @Test\n    fun geoshapes_areSeparatedBySemicolon_withoutTrialingSemicolon_andHaveMatchingFirstAndLastPoints() {\n        assertThat(\n            formatPointsResultString(points, true),\n            equalTo(\"11.0 12.0 13.0 14.0;21.0 22.0 23.0 24.0;31.0 32.0 33.0 34.0;11.0 12.0 13.0 14.0\")\n        )\n    }\n\n    @Test\n    fun test_formatLocationResultString() {\n        val location: Location = createLocation(\"GPS\", 1.0, 2.0, 3.0, 4f)\n        assertThat(GeoUtils.formatLocationResultString(location), equalTo(\"1.0 2.0 3.0 4.0\"))\n    }\n\n    @Test\n    fun `#formatAccuracy formats accuracy in meters to 2 decimal places`() {\n        assertThat(GeoUtils.formatAccuracy(context, 0.000f), equalTo(\"0 m\"))\n        assertThat(GeoUtils.formatAccuracy(context, 0.001f), equalTo(\"0 m\"))\n        assertThat(GeoUtils.formatAccuracy(context, 0.01f), equalTo(\"0.01 m\"))\n        assertThat(GeoUtils.formatAccuracy(context, 0.10f), equalTo(\"0.1 m\"))\n        assertThat(GeoUtils.formatAccuracy(context, 1.1f), equalTo(\"1.1 m\"))\n    }\n\n    @Test\n    fun parseGeometryPointTest() {\n        var gp =\n            parseGeometryPoint(\"37.45153333333334 -122.15539166666667 0.0 20.0\")!!\n        assertThat(37.45153333333334, equalTo(gp[0]))\n        assertThat(-122.15539166666667, equalTo(gp[1]))\n        assertThat(0.0, equalTo(gp[2]))\n        assertThat(20.0, equalTo(gp[3]))\n\n        gp = parseGeometryPoint(\"37.45153333333334\")!!\n        assertThat(37.45153333333334, equalTo(gp[0]))\n        assertThat(0.0, equalTo(gp[1]))\n        assertThat(0.0, equalTo(gp[2]))\n        assertThat(0.0, equalTo(gp[3]))\n\n        gp = parseGeometryPoint(\" 37.45153333333334 -122.15539166666667 0.0 \")!!\n        assertThat(37.45153333333334, equalTo(gp[0]))\n        assertThat(-122.15539166666667, equalTo(gp[1]))\n        assertThat(0.0, equalTo(gp[2]))\n        assertThat(0.0, equalTo(gp[3]))\n\n        assertThat(\n            null,\n            equalTo(parseGeometryPoint(\"37.45153333333334 -122.15539166666667 0.0 qwerty\"))\n        )\n        assertThat(null, equalTo(parseGeometryPoint(\"\")))\n        assertThat(null, equalTo(parseGeometryPoint(null)))\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoint/AccuracyProgressViewTest.kt",
    "content": "package org.odk.collect.geo.geopoint\n\nimport android.app.Application\nimport android.graphics.drawable.ColorDrawable\nimport androidx.test.core.app.ApplicationProvider.getApplicationContext\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidshared.system.ContextExt.getThemeAttributeValue\n\n@RunWith(AndroidJUnit4::class)\nclass AccuracyProgressViewTest {\n\n    private val context = getApplicationContext<Application>().also {\n        // Need a theme where primary and secondary are different for tests\n        it.setTheme(com.google.android.material.R.style.Theme_MaterialComponents)\n    }\n\n    private val colorPrimary = getThemeAttributeValue(context, androidx.appcompat.R.attr.colorPrimary)\n    private val colorOnPrimary = getThemeAttributeValue(context, com.google.android.material.R.attr.colorOnPrimary)\n    private val colorError = getThemeAttributeValue(context, androidx.appcompat.R.attr.colorError)\n    private val colorOnError = getThemeAttributeValue(context, com.google.android.material.R.attr.colorOnError)\n\n    @Test\n    fun `updates current accuracy`() {\n        val view = AccuracyProgressView(context)\n        assertThat(\n            view.binding.currentAccuracy.text,\n            equalTo(context.getString(org.odk.collect.strings.R.string.empty_accuracy))\n        )\n\n        view.accuracy = LocationAccuracy.Improving(52f)\n        assertThat(\n            view.binding.currentAccuracy.text,\n            equalTo(context.getString(org.odk.collect.strings.R.string.accuracy_m, \"52\"))\n        )\n    }\n\n    @Test\n    fun `updates text and strength based on accuracy`() {\n        val view = AccuracyProgressView(context)\n\n        assertThat(\n            view.binding.text.text,\n            equalTo(context.getString(org.odk.collect.strings.R.string.waiting_for_location))\n        )\n        assertThat(view.binding.strength.progress, equalTo(20))\n\n        view.accuracy = LocationAccuracy.Unacceptable(10f)\n        assertThat(\n            view.binding.text.text,\n            equalTo(context.getString(org.odk.collect.strings.R.string.unacceptable_accuracy))\n        )\n        assertThat(view.binding.strength.progress, equalTo(40))\n\n        view.accuracy = LocationAccuracy.Poor(10f)\n        assertThat(view.binding.text.text, equalTo(context.getString(org.odk.collect.strings.R.string.poor_accuracy)))\n        assertThat(view.binding.strength.progress, equalTo(60))\n\n        view.accuracy = LocationAccuracy.Improving(10f)\n        assertThat(view.binding.text.text, equalTo(context.getString(org.odk.collect.strings.R.string.improving_accuracy)))\n        assertThat(view.binding.strength.progress, equalTo(80))\n    }\n\n    @Test\n    fun `has primary background when accuracy is poor`() {\n        val view = AccuracyProgressView(context)\n        view.accuracy = LocationAccuracy.Poor(10f)\n\n        val backgroundColor = (view.binding.root.background as ColorDrawable).color\n        assertThat(backgroundColor, equalTo(colorPrimary))\n\n        val titleColor = view.binding.title.currentTextColor\n        assertThat(titleColor, equalTo(colorOnPrimary))\n\n        val currentAccuracyColor = view.binding.currentAccuracy.currentTextColor\n        assertThat(currentAccuracyColor, equalTo(colorOnPrimary))\n\n        val qualitativeColor = view.binding.text.currentTextColor\n        assertThat(qualitativeColor, equalTo(colorOnPrimary))\n\n        val strengthColor = view.binding.strength.indicatorColor[0]\n        assertThat(strengthColor, equalTo(colorOnPrimary))\n    }\n\n    @Test\n    fun `has error background when accuracy is unacceptable`() {\n        val view = AccuracyProgressView(context)\n        view.accuracy = LocationAccuracy.Unacceptable(10f)\n\n        val backgroundColor = (view.binding.root.background as ColorDrawable).color\n        assertThat(backgroundColor, equalTo(colorError))\n\n        val titleColor = view.binding.title.currentTextColor\n        assertThat(titleColor, equalTo(colorOnError))\n\n        val currentAccuracyColor = view.binding.currentAccuracy.currentTextColor\n        assertThat(currentAccuracyColor, equalTo(colorOnError))\n\n        val qualitativeColor = view.binding.text.currentTextColor\n        assertThat(qualitativeColor, equalTo(colorOnError))\n\n        val strengthColor = view.binding.strength.indicatorColor[0]\n        assertThat(strengthColor, equalTo(colorOnError))\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoint/AccuracyStatusViewTest.kt",
    "content": "package org.odk.collect.geo.geopoint\n\nimport android.app.Application\nimport android.view.View\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.strings.R\n\n@RunWith(AndroidJUnit4::class)\nclass AccuracyStatusViewTest {\n\n    private val application = ApplicationProvider.getApplicationContext<Application>().also {\n        it.setTheme(com.google.android.material.R.style.Theme_MaterialComponents)\n    }\n\n    @Test\n    fun `hides title when text is blank`() {\n        val view = AccuracyStatusView(application)\n        assertThat(view.binding.title.visibility, equalTo(View.GONE))\n    }\n\n    @Test\n    fun `shows title when text is not blank`() {\n        val view = AccuracyStatusView(application)\n        view.title = \"SOMETHING!\"\n        assertThat(view.binding.title.visibility, equalTo(View.VISIBLE))\n    }\n\n    @Test\n    fun `shows warning when accuracy is Unacceptable`() {\n        val view = AccuracyStatusView(application)\n\n        view.accuracy = LocationAccuracy.Unacceptable(0.0f)\n        assertThat(\n            view.binding.locationStatus.text,\n            equalTo(application.getString(R.string.location_accuracy_unacceptable, \"0 m\"))\n        )\n\n        view.accuracy = LocationAccuracy.Improving(0.0f)\n        assertThat(\n            view.binding.locationStatus.text,\n            equalTo(application.getString(R.string.location_accuracy, \"0 m\"))\n        )\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointActivityTest.kt",
    "content": "package org.odk.collect.geo.geopoint\n\nimport android.app.Activity\nimport android.app.Application\nimport android.content.Intent\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.test.core.app.ApplicationProvider.getApplicationContext\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidtest.ActivityScenarioExtensions.isFinishing\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport org.odk.collect.externalapp.ExternalAppUtils\nimport org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY\nimport org.odk.collect.geo.DaggerGeoDependencyComponent\nimport org.odk.collect.geo.GeoDependencyModule\nimport org.odk.collect.geo.GeoUtils\nimport org.odk.collect.geo.support.RobolectricApplication\nimport org.odk.collect.location.Location\nimport org.odk.collect.testshared.FakeScheduler\n\n@RunWith(AndroidJUnit4::class)\nclass GeoPointActivityTest {\n\n    private val locationLiveData: MutableLiveData<Location?> = MutableLiveData(null)\n    private val viewModel = mock<GeoPointViewModel> {\n        on { acceptedLocation } doReturn locationLiveData\n        on { currentAccuracy } doReturn MutableLiveData(null)\n        on { timeElapsed } doReturn MutableNonNullLiveData(0)\n        on { satellites } doReturn MutableNonNullLiveData(0)\n    }\n\n    private val scheduler = FakeScheduler()\n\n    @get:Rule\n    val launcherRule = ActivityScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        val application = getApplicationContext<RobolectricApplication>()\n        application.geoDependencyComponent = DaggerGeoDependencyComponent.builder()\n            .application(application)\n            .geoDependencyModule(object : GeoDependencyModule() {\n                override fun providesScheduler() = scheduler\n\n                override fun providesGeoPointViewModelFactory(application: Application) =\n                    object : GeoPointViewModelFactory {\n                        @Suppress(\"UNCHECKED_CAST\")\n                        override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                            return viewModel as T\n                        }\n                    }\n            })\n            .build()\n    }\n\n    @Test\n    fun `starts view model`() {\n        val intent = Intent(getApplicationContext(), GeoPointActivity::class.java)\n\n        launcherRule.launch<GeoPointActivity>(intent)\n        verify(viewModel).start(retainMockAccuracy = false)\n    }\n\n    @Test\n    fun `shows dialog`() {\n        val scenario = launcherRule.launch(GeoPointActivity::class.java)\n        scenario.onActivity {\n            val fragments = it.supportFragmentManager.fragments\n            assertThat(fragments[0].javaClass, equalTo(GeoPointDialogFragment::class.java))\n        }\n    }\n\n    @Test\n    fun `finishes with location when available`() {\n        val scenario = launcherRule.launchForResult(GeoPointActivity::class.java)\n\n        val location = Location(0.0, 0.0, 0.0, 0.0f)\n        locationLiveData.value = location\n\n        assertThat(scenario.isFinishing, equalTo(true))\n        assertThat(scenario.result.resultCode, equalTo(Activity.RESULT_OK))\n        val resultIntent = scenario.result.resultData\n        val returnedValue = ExternalAppUtils.getReturnedSingleValue(resultIntent)\n        assertThat(\n            returnedValue,\n            equalTo(GeoUtils.formatLocationResultString(location))\n        )\n    }\n\n    @Test\n    fun `finishes when dialog is cancelled`() {\n        val scenario = launcherRule.launchForResult(GeoPointActivity::class.java)\n        scenario.onActivity {\n            it.onCancel()\n        }\n\n        assertThat(scenario.isFinishing, equalTo(true))\n        assertThat(scenario.result.resultCode, equalTo(Activity.RESULT_CANCELED))\n    }\n\n    @Test\n    fun `passes retain mock accuracy extra to view model`() {\n        val intent = Intent(getApplicationContext(), GeoPointActivity::class.java)\n\n        intent.putExtra(EXTRA_RETAIN_MOCK_ACCURACY, true)\n        launcherRule.launch<GeoPointActivity>(intent)\n        verify(viewModel).start(retainMockAccuracy = true)\n    }\n\n    @Test\n    fun `passes threshold extra to view model`() {\n        val intent = Intent(getApplicationContext(), GeoPointActivity::class.java)\n        intent.putExtra(GeoPointActivity.EXTRA_ACCURACY_THRESHOLD, 5.0f)\n\n        launcherRule.launch<GeoPointActivity>(intent)\n        verify(viewModel).start(retainMockAccuracy = false, accuracyThreshold = 5.0f)\n    }\n\n    @Test\n    fun `passes unacceptable threshold extra to view model`() {\n        val intent = Intent(getApplicationContext(), GeoPointActivity::class.java)\n        intent.putExtra(GeoPointActivity.EXTRA_UNACCEPTABLE_ACCURACY_THRESHOLD, 10.0f)\n\n        launcherRule.launch<GeoPointActivity>(intent)\n        verify(viewModel).start(retainMockAccuracy = false, unacceptableAccuracyThreshold = 10.0f)\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointDialogFragmentTest.kt",
    "content": "package org.odk.collect.geo.geopoint\n\nimport android.app.Application\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.test.core.app.ApplicationProvider.getApplicationContext\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.scrollTo\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.isEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.geo.DaggerGeoDependencyComponent\nimport org.odk.collect.geo.GeoDependencyModule\nimport org.odk.collect.geo.GeoUtils\nimport org.odk.collect.geo.support.RobolectricApplication\nimport org.odk.collect.strings.localization.getLocalizedString\n\n@RunWith(AndroidJUnit4::class)\nclass GeoPointDialogFragmentTest {\n\n    private val application = getApplicationContext<RobolectricApplication>()\n\n    private val currentAccuracyLiveData: MutableLiveData<LocationAccuracy?> = MutableLiveData(null)\n    private val timeElapsedLiveData: MutableNonNullLiveData<Long> = MutableNonNullLiveData(0)\n    private val satellitesLiveData = MutableNonNullLiveData(0)\n    private val viewModel = mock<GeoPointViewModel> {\n        on { currentAccuracy } doReturn currentAccuracyLiveData\n        on { timeElapsed } doReturn timeElapsedLiveData\n        on { satellites } doReturn satellitesLiveData\n    }\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        application.geoDependencyComponent = DaggerGeoDependencyComponent.builder()\n            .application(application)\n            .geoDependencyModule(object : GeoDependencyModule() {\n                override fun providesGeoPointViewModelFactory(application: Application) =\n                    object : GeoPointViewModelFactory {\n                        @Suppress(\"UNCHECKED_CAST\")\n                        override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                            return viewModel as T\n                        }\n                    }\n            })\n            .build()\n    }\n\n    @Test\n    fun `disables save until location is available`() {\n        launcherRule.launch(GeoPointDialogFragment::class.java)\n        onView(withText(org.odk.collect.strings.R.string.save)).inRoot(isDialog()).check(matches(not(isEnabled())))\n\n        currentAccuracyLiveData.value = LocationAccuracy.Improving(5.0f)\n        onView(withText(org.odk.collect.strings.R.string.save)).inRoot(isDialog()).check(matches(isEnabled()))\n    }\n\n    @Test\n    fun `shows accuracy threshold`() {\n        whenever(viewModel.accuracyThreshold).thenReturn(5.0f)\n        launcherRule.launch(GeoPointDialogFragment::class.java)\n\n        onView(withText(application.getLocalizedString(org.odk.collect.strings.R.string.point_will_be_saved, GeoUtils.formatAccuracy(application, 5.0f))))\n            .inRoot(isDialog())\n            .perform(scrollTo())\n            .check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `shows and updates current accuracy`() {\n        val scenario = launcherRule.launch(GeoPointDialogFragment::class.java)\n\n        currentAccuracyLiveData.value = LocationAccuracy.Improving(50.2f)\n        scenario.onFragment {\n            assertThat(it.binding.accuracyStatus.accuracy, equalTo(LocationAccuracy.Improving(50.2f)))\n        }\n\n        currentAccuracyLiveData.value = LocationAccuracy.Improving(15.65f)\n        onView(withText(GeoUtils.formatAccuracy(application, 15.65f))).inRoot(isDialog())\n            .perform(scrollTo()).check(matches(isDisplayed()))\n        scenario.onFragment {\n            assertThat(it.binding.accuracyStatus.accuracy, equalTo(LocationAccuracy.Improving(15.65f)))\n        }\n    }\n\n    @Test\n    fun `shows and updates time elapsed`() {\n        launcherRule.launch(GeoPointDialogFragment::class.java)\n\n        timeElapsedLiveData.value = 0\n        onView(withText(application.getLocalizedString(org.odk.collect.strings.R.string.time_elapsed, \"00:00\")))\n            .inRoot(isDialog())\n            .perform(scrollTo()).check(matches(isDisplayed()))\n\n        timeElapsedLiveData.value = 62000\n        onView(withText(application.getLocalizedString(org.odk.collect.strings.R.string.time_elapsed, \"01:02\")))\n            .inRoot(isDialog())\n            .perform(scrollTo()).check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `shows and updates satellites`() {\n        launcherRule.launch(GeoPointDialogFragment::class.java)\n\n        onView(withText(application.getLocalizedString(org.odk.collect.strings.R.string.satellites, 0)))\n            .inRoot(isDialog())\n            .perform(scrollTo())\n            .check(matches(isDisplayed()))\n\n        satellitesLiveData.value = 5\n\n        onView(withText(application.getLocalizedString(org.odk.collect.strings.R.string.satellites, 5)))\n            .inRoot(isDialog())\n            .perform(scrollTo())\n            .check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `clicking cancel calls listener`() {\n        val scenario = launcherRule.launch(GeoPointDialogFragment::class.java)\n\n        val listener = mock<GeoPointDialogFragment.Listener>()\n        scenario.onFragment {\n            it.listener = listener\n        }\n\n        onView(withText(org.odk.collect.strings.R.string.cancel)).inRoot(isDialog()).perform(click())\n        verify(listener).onCancel()\n    }\n\n    @Test\n    fun `pressing back calls listener`() {\n        val scenario = launcherRule.launch(GeoPointDialogFragment::class.java)\n\n        val listener = mock<GeoPointDialogFragment.Listener>()\n        scenario.onFragment {\n            it.listener = listener\n        }\n\n        Espresso.pressBack()\n        verify(listener).onCancel()\n    }\n\n    @Test\n    fun `clicking save calls forceLocation() on view model`() {\n        launcherRule.launch(GeoPointDialogFragment::class.java)\n        currentAccuracyLiveData.value = LocationAccuracy.Improving(5.0f)\n\n        onView(withText(org.odk.collect.strings.R.string.save)).inRoot(isDialog()).perform(click())\n        verify(viewModel).forceLocation()\n    }\n\n    @Test\n    fun `dialog is not cancellable`() {\n        launcherRule.launch(GeoPointDialogFragment::class.java).onFragment {\n            assertThat(it.isCancelable, equalTo(false))\n        }\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoint/GeoPointMapActivityTest.java",
    "content": "package org.odk.collect.geo.geopoint;\n\nimport static android.app.Activity.RESULT_OK;\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.junit.Assert.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.odk.collect.geo.Constants.EXTRA_RETAIN_MOCK_ACCURACY;\nimport static org.robolectric.Shadows.shadowOf;\n\nimport android.app.Activity;\nimport android.app.Application;\nimport android.content.Intent;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.test.core.app.ActivityScenario;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule;\nimport org.odk.collect.async.Scheduler;\nimport org.odk.collect.externalapp.ExternalAppUtils;\nimport org.odk.collect.geo.DaggerGeoDependencyComponent;\nimport org.odk.collect.geo.GeoDependencyModule;\nimport org.odk.collect.geo.R;\nimport org.odk.collect.geo.support.FakeMapFragment;\nimport org.odk.collect.geo.support.RobolectricApplication;\nimport org.odk.collect.maps.MapFragmentFactory;\nimport org.odk.collect.maps.MapPoint;\nimport org.odk.collect.maps.layers.ReferenceLayerRepository;\nimport org.odk.collect.settings.InMemSettingsProvider;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.webpage.WebPageService;\nimport org.robolectric.shadows.ShadowApplication;\n\nimport java.util.List;\n\n@RunWith(AndroidJUnit4.class)\npublic class GeoPointMapActivityTest {\n\n    private final FakeMapFragment mapFragment = new FakeMapFragment();\n\n    @Rule\n    public ActivityScenarioLauncherRule launcherRule = new ActivityScenarioLauncherRule();\n\n    @Before\n    public void setUp() {\n        ShadowApplication shadowApplication = shadowOf(ApplicationProvider.<Application>getApplicationContext());\n        shadowApplication.grantPermissions(\"android.permission.ACCESS_FINE_LOCATION\");\n        shadowApplication.grantPermissions(\"android.permission.ACCESS_COARSE_LOCATION\");\n\n        RobolectricApplication application = ApplicationProvider.getApplicationContext();\n        application.geoDependencyComponent = DaggerGeoDependencyComponent.builder()\n                .application(application)\n                .geoDependencyModule(new GeoDependencyModule() {\n                    @NonNull\n                    @Override\n                    public MapFragmentFactory providesMapFragmentFactory() {\n                        return () -> mapFragment;\n                    }\n\n                    @NonNull\n                    @Override\n                    public ReferenceLayerRepository providesReferenceLayerRepository() {\n                        return mock();\n                    }\n\n                    @NonNull\n                    @Override\n                    public Scheduler providesScheduler() {\n                        return mock();\n                    }\n\n                    @NonNull\n                    @Override\n                    public SettingsProvider providesSettingsProvider() {\n                        return new InMemSettingsProvider();\n                    }\n\n                    @NonNull\n                    @Override\n                    public WebPageService providesWebPageService() {\n                        return mock();\n                    }\n                })\n                .build();\n    }\n\n    @Test\n    public void whenLocationNotSetShouldDisplayPleaseWaitMessage() {\n        ActivityScenario<GeoPointMapActivity> scenario = launcherRule.launchForResult(GeoPointMapActivity.class);\n        mapFragment.ready();\n\n        scenario.onActivity(activity -> assertEquals(activity.getString(org.odk.collect.strings.R.string.please_wait_long), getLocationStatus(activity)));\n    }\n\n    @Test\n    public void whenLocationSetShouldDisplayStatusMessage() {\n        ActivityScenario<GeoPointMapActivity> scenario = launcherRule.launchForResult(GeoPointMapActivity.class);\n        mapFragment.ready();\n        mapFragment.setLocationProvider(\"GPS\");\n        mapFragment.setLocation(new MapPoint(1, 2, 3, 4f));\n\n        scenario.onActivity(activity -> assertEquals(\"Accuracy: 4 m\", getLocationStatus(activity)));\n    }\n\n    @Test\n    public void shouldReturnPointFromLastLocationFix() {\n        ActivityScenario<GeoPointMapActivity> scenario = launcherRule.launchForResult(GeoPointMapActivity.class);\n        mapFragment.ready();\n        mapFragment.setLocationProvider(\"GPS\");\n\n        // First location\n        mapFragment.setLocation(new MapPoint(1, 2, 3, 4f));\n\n        // Second location\n        mapFragment.setLocation(new MapPoint(5, 6, 7, 8f));\n\n        // When the user clicks the \"Save\" button, the fix location should be returned.\n        scenario.onActivity(activity -> activity.findViewById(R.id.accept_location).performClick());\n\n        assertThat(scenario.getResult().getResultCode(), is(RESULT_OK));\n        scenario.onActivity(activity -> {\n            Intent resultData = scenario.getResult().getResultData();\n            assertThat(ExternalAppUtils.getReturnedSingleValue(resultData), is(activity.formatResult(new MapPoint(5, 6, 7, 8))));\n        });\n    }\n\n    @Test\n    public void whenLocationExtraIncluded_showsMarker() {\n        Intent intent = new Intent(ApplicationProvider.getApplicationContext(), GeoPointMapActivity.class);\n        intent.putExtra(GeoPointMapActivity.EXTRA_LOCATION, new MapPoint(1.0, 2.0));\n        launcherRule.launch(intent);\n        mapFragment.ready();\n\n        List<MapPoint> markers = mapFragment.getMarkers();\n        assertThat(markers.size(), equalTo(1));\n        assertThat(markers.get(0).latitude, equalTo(1.0));\n        assertThat(markers.get(0).longitude, equalTo(2.0));\n    }\n\n    @Test\n    public void mapFragmentRetainMockAccuracy_isFalse() {\n        launcherRule.launch(GeoPointMapActivity.class);\n        mapFragment.ready();\n\n        assertThat(mapFragment.isRetainMockAccuracy(), is(false));\n    }\n\n    @Test\n    public void passingRetainMockAccuracyExtra_updatesMapFragmentState() {\n        Intent intent = new Intent(ApplicationProvider.getApplicationContext(), GeoPointMapActivity.class);\n        intent.putExtra(EXTRA_RETAIN_MOCK_ACCURACY, true);\n        launcherRule.launch(intent);\n        mapFragment.ready();\n\n        assertThat(mapFragment.isRetainMockAccuracy(), is(true));\n\n        intent.putExtra(EXTRA_RETAIN_MOCK_ACCURACY, false);\n        launcherRule.launch(intent);\n        mapFragment.ready();\n\n        assertThat(mapFragment.isRetainMockAccuracy(), is(false));\n    }\n\n    @Test\n    public void recreatingTheActivityWithTheLayersDialogDisplayedDoesNotCrashTheApp() {\n        ActivityScenario<GeoPointMapActivity> scenario = launcherRule.launch(GeoPointMapActivity.class);\n        mapFragment.ready();\n\n        onView(withId(R.id.layer_menu)).perform(click());\n\n        scenario.recreate();\n    }\n\n    private String getLocationStatus(Activity activity) {\n        return activity\n                .findViewById(R.id.status_section)\n                .<TextView>findViewById(R.id.location_status)\n                .getText().toString();\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoint/LocationTrackerGeoPointViewModelTest.kt",
    "content": "package org.odk.collect.geo.geopoint\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidtest.getOrAwaitValue\nimport org.odk.collect.location.Location\nimport org.odk.collect.location.satellites.SatelliteInfoClient\nimport org.odk.collect.location.tracker.LocationTracker\nimport org.odk.collect.testshared.FakeScheduler\n\nclass LocationTrackerGeoPointViewModelTest {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    val currentLocation = MutableStateFlow<Location?>(null)\n    private val locationTracker = mock<LocationTracker> {\n        on { getLocation() } doReturn currentLocation\n    }\n\n    private val satelliteInfoClient = mock<SatelliteInfoClient>()\n    private val scheduler = FakeScheduler()\n\n    @Test\n    fun `start() starts LocationTracker with false retain mock accuracy and 1s update interval`() {\n        val viewModel = createViewModel()\n        viewModel.start()\n\n        verify(locationTracker).start(retainMockAccuracy = false, updateInterval = 1000L)\n    }\n\n    @Test\n    fun `start() starts LocationTracker with with retain mock accuracy value when set`() {\n        val viewModel = createViewModel()\n        viewModel.start(retainMockAccuracy = true)\n\n        verify(locationTracker).start(true, 1000L)\n    }\n\n    @Test\n    fun `acceptedLocation is null when no location`() {\n        val viewModel = createViewModel()\n        viewModel.start(accuracyThreshold = 0.0f)\n\n        val location = viewModel.acceptedLocation\n        currentLocation.value = null\n        scheduler.runForeground()\n        assertThat(location.getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun `acceptedLocation is null when accuracy is higher than threshold value`() {\n        val viewModel = createViewModel()\n        viewModel.start(accuracyThreshold = 1.0f)\n\n        val location = viewModel.acceptedLocation\n        currentLocation.value = Location(\n            0.0,\n            0.0,\n            0.0,\n            1.1f\n        )\n        scheduler.runForeground()\n        assertThat(location.getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun `acceptedLocation is tracker location when accuracy is equal to threshold value`() {\n        val viewModel = createViewModel()\n        viewModel.start(accuracyThreshold = 1.0f)\n\n        val location = viewModel.acceptedLocation\n        val locationTrackerLocation = Location(0.0, 0.0, 0.0, 1.0f)\n        currentLocation.value = locationTrackerLocation\n        scheduler.runForeground()\n        assertThat(location.getOrAwaitValue(), equalTo(locationTrackerLocation))\n    }\n\n    @Test\n    fun `acceptedLocation is tracker location when accuracy is lower than threshold value`() {\n        val viewModel = createViewModel()\n        viewModel.start(accuracyThreshold = 1.0f)\n\n        val location = viewModel.acceptedLocation\n        val locationTrackerLocation = Location(0.0, 0.0, 0.0, 0.9f)\n        currentLocation.value = locationTrackerLocation\n        scheduler.runForeground()\n        assertThat(location.getOrAwaitValue(), equalTo(locationTrackerLocation))\n    }\n\n    @Test\n    fun `acceptedLocation does not update after it has met the threshold`() {\n        val viewModel = createViewModel()\n        viewModel.start(accuracyThreshold = 1.0f)\n\n        val location = viewModel.acceptedLocation\n        val locationTrackerLocation = Location(0.0, 0.0, 0.0, 1.0f)\n        currentLocation.value = locationTrackerLocation\n        scheduler.runForeground()\n        assertThat(location.getOrAwaitValue(), equalTo(locationTrackerLocation))\n\n        currentLocation.value = Location(\n            1.0,\n            1.0,\n            1.0,\n            1.0f\n        )\n        scheduler.runForeground()\n        assertThat(location.getOrAwaitValue(), equalTo(locationTrackerLocation))\n    }\n\n    @Test\n    fun `currentAccuracy is null when no location`() {\n        val viewModel = createViewModel()\n        viewModel.start()\n\n        val currentAccuracy = viewModel.currentAccuracy\n        currentLocation.value = null\n        scheduler.runForeground()\n        assertThat(currentAccuracy.getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun `currentAccuracy updates with location accuracy`() {\n        val viewModel = createViewModel()\n        viewModel.start(accuracyThreshold = 5.0f, unacceptableAccuracyThreshold = 20f)\n\n        val currentAccuracy = viewModel.currentAccuracy\n\n        currentLocation.value = Location(\n            0.0,\n            0.0,\n            0.0,\n            6.1f\n        )\n        scheduler.runForeground()\n        assertThat(currentAccuracy.getOrAwaitValue(), equalTo(LocationAccuracy.Improving(6.1f)))\n\n        currentLocation.value = Location(\n            0.0,\n            0.0,\n            0.0,\n            5.0f + 5.1f\n        )\n        scheduler.runForeground()\n        assertThat(currentAccuracy.getOrAwaitValue(), equalTo(LocationAccuracy.Poor(5.0f + 5.1f)))\n\n        currentLocation.value = Location(\n            0.0,\n            0.0,\n            0.0,\n            20.1f\n        )\n        scheduler.runForeground()\n        assertThat(currentAccuracy.getOrAwaitValue(), equalTo(LocationAccuracy.Unacceptable(20.1f)))\n    }\n\n    @Test\n    fun `currentAccuracy is never Poor when unacceptableAccuracyThreshold is equal to accuracyThreshold + 5`() {\n        val viewModel = createViewModel()\n        viewModel.start(accuracyThreshold = 5f, unacceptableAccuracyThreshold = 10f)\n\n        val currentAccuracy = viewModel.currentAccuracy\n\n        currentLocation.value = Location(\n            0.0,\n            0.0,\n            0.0,\n            11f\n        )\n        scheduler.runForeground()\n        assertThat(currentAccuracy.getOrAwaitValue(), equalTo(LocationAccuracy.Unacceptable(11f)))\n\n        currentLocation.value = Location(\n            0.0,\n            0.0,\n            0.0,\n            10f\n        )\n        scheduler.runForeground()\n        assertThat(currentAccuracy.getOrAwaitValue(), equalTo(LocationAccuracy.Improving(10f)))\n    }\n\n    @Test\n    fun `currentAccuracy is never Poor when unacceptableAccuracyThreshold is less than accuracyThreshold + 5`() {\n        val viewModel = createViewModel()\n        viewModel.start(accuracyThreshold = 5f, unacceptableAccuracyThreshold = 9f)\n\n        val currentAccuracy = viewModel.currentAccuracy\n\n        currentLocation.value = Location(\n            0.0,\n            0.0,\n            0.0,\n            10f\n        )\n        scheduler.runForeground()\n        assertThat(currentAccuracy.getOrAwaitValue(), equalTo(LocationAccuracy.Unacceptable(10f)))\n\n        currentLocation.value = Location(\n            0.0,\n            0.0,\n            0.0,\n            9f\n        )\n        scheduler.runForeground()\n        assertThat(currentAccuracy.getOrAwaitValue(), equalTo(LocationAccuracy.Improving(9f)))\n    }\n\n    @Test\n    fun `onCleared() stops LocationTracker`() {\n        val viewModel = createViewModel()\n        viewModel.start()\n        viewModel.onCleared()\n    }\n\n    @Test\n    fun `onCleared() cancels repeat`() {\n        val viewModel = createViewModel()\n        viewModel.start()\n        viewModel.onCleared()\n        assertThat(scheduler.isRepeatRunning(), equalTo(false))\n    }\n\n    @Test\n    fun `forceLocation() sets acceptedLocation to location tracker location regardless of threshold`() {\n        val viewModel = createViewModel()\n        viewModel.start(accuracyThreshold = 1.0f)\n\n        val location = viewModel.acceptedLocation\n        val locationTrackerLocation = Location(0.0, 0.0, 0.0, 2.5f)\n        currentLocation.value = locationTrackerLocation\n        scheduler.runForeground()\n        assertThat(location.getOrAwaitValue(), equalTo(null))\n\n        viewModel.forceLocation()\n        assertThat(location.getOrAwaitValue(), equalTo(locationTrackerLocation))\n    }\n\n    /**\n     * We want to avoid timing issues where an update happens right after a click and the user\n     * ends up with a location fix that was never on screen.\n     */\n    @Test\n    fun `forceLocation() locks acceptedLocation to current one`() {\n        val viewModel = createViewModel()\n        viewModel.start()\n\n        val location = viewModel.acceptedLocation\n        val locationTrackerLocation = Location(0.0, 0.0, 0.0, 2.5f)\n        currentLocation.value = locationTrackerLocation\n        scheduler.runForeground()\n        viewModel.forceLocation()\n        assertThat(location.getOrAwaitValue(), equalTo(locationTrackerLocation))\n\n        currentLocation.value = Location(\n            0.0,\n            0.0,\n            0.0,\n            5.5f\n        )\n        scheduler.runForeground()\n        assertThat(location.getOrAwaitValue(), equalTo(locationTrackerLocation))\n    }\n\n    @Test\n    fun `timeElapsed updates with time since creation`() {\n        var timeElapsed = 0L\n        val viewModel = createViewModel(clock = { timeElapsed })\n        viewModel.start()\n\n        val timeElapsedLiveData = viewModel.timeElapsed\n\n        timeElapsed = 5L\n        scheduler.runForeground()\n        assertThat(timeElapsedLiveData.getOrAwaitValue(), equalTo(5L))\n\n        timeElapsed = 60L\n        scheduler.runForeground()\n        assertThat(timeElapsedLiveData.getOrAwaitValue(), equalTo(60L))\n    }\n\n    @Test\n    fun `satellites updates with satellitesUsedInLastFix`() {\n        val satellitesUsedInLastFix = MutableNonNullLiveData(0)\n        whenever(satelliteInfoClient.satellitesUsedInLastFix).thenReturn(satellitesUsedInLastFix)\n\n        val viewModel = createViewModel()\n        assertThat(viewModel.satellites.value, equalTo(0))\n\n        satellitesUsedInLastFix.value = 6\n        assertThat(viewModel.satellites.value, equalTo(6))\n    }\n\n    private fun createViewModel(clock: () -> Long = { 0 }) =\n        LocationTrackerGeoPointViewModel(locationTracker, satelliteInfoClient, clock, scheduler)\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt",
    "content": "package org.odk.collect.geo.geopoly\n\nimport android.app.Application\nimport androidx.activity.OnBackPressedDispatcher\nimport androidx.lifecycle.MutableLiveData\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withContentDescription\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.not\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.odk.collect.androidshared.ui.DisplayString\nimport org.odk.collect.androidshared.ui.SnackbarUtils\nimport org.odk.collect.androidshared.utils.opaque\nimport org.odk.collect.androidtest.FragmentScenarioExtensions.setFragmentResultListener\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.geo.DaggerGeoDependencyComponent\nimport org.odk.collect.geo.GeoDependencyModule\nimport org.odk.collect.geo.GeoUtils.toMapPoint\nimport org.odk.collect.geo.R\nimport org.odk.collect.geo.geopoint.AccuracyStatusView\nimport org.odk.collect.geo.geopoint.LocationAccuracy\nimport org.odk.collect.geo.geopoly.GeoPolyFragment.Companion.INTERVAL_OPTIONS\nimport org.odk.collect.geo.geopoly.GeoPolyFragment.OutputMode\nimport org.odk.collect.geo.support.AccuracyStatusViewMatcher.Companion.hasAccuracy\nimport org.odk.collect.geo.support.FakeLocationTracker\nimport org.odk.collect.geo.support.FakeMapFragment\nimport org.odk.collect.geo.support.RobolectricApplication\nimport org.odk.collect.location.Location\nimport org.odk.collect.location.tracker.LocationTracker\nimport org.odk.collect.maps.MapConsts\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.testshared.EspressoAssertions\nimport org.odk.collect.testshared.EspressoAssertions.assertNotVisible\nimport org.odk.collect.testshared.EspressoAssertions.assertVisible\nimport org.odk.collect.testshared.FakeScheduler\nimport org.odk.collect.testshared.FragmentResultRecorder\nimport org.odk.collect.testshared.EspressoInteractions\nimport org.odk.collect.webpage.WebPageService\nimport org.robolectric.Shadows\n\n@RunWith(AndroidJUnit4::class)\nclass GeoPolyFragmentTest {\n    private val mapFragment = FakeMapFragment(ready = true)\n\n    private val locationTracker = FakeLocationTracker()\n\n    @get:Rule\n    val fragmentLauncherRule = FragmentScenarioLauncherRule()\n\n    private val application = ApplicationProvider.getApplicationContext<Application>()\n    private val scheduler = FakeScheduler()\n\n    @Before\n    fun setUp() {\n        val shadowApplication =\n            Shadows.shadowOf(application)\n        shadowApplication.grantPermissions(\"android.permission.ACCESS_FINE_LOCATION\")\n        shadowApplication.grantPermissions(\"android.permission.ACCESS_COARSE_LOCATION\")\n        overrideDependencies(mapFragment)\n\n        SnackbarUtils.alertStore.enabled = true\n    }\n\n    @After\n    fun teardown() {\n        SnackbarUtils.alertStore.enabled = false\n    }\n\n    @Test\n    fun zoomsToCurrentLocation() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        locationTracker.currentLocation = Location(2.0, 2.0)\n        assertThat(mapFragment.getCenter(), equalTo(MapPoint(2.0, 2.0)))\n        assertThat(mapFragment.getZoom(), not(equalTo(0.0)))\n    }\n\n    @Test\n    fun marksCurrentLocation() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        locationTracker.currentLocation = Location(2.0, 2.0, accuracy = 5.2f)\n        assertThat(\n            mapFragment.getMarkers(),\n            equalTo(listOf(locationTracker.currentLocation!!.toMapPoint()))\n        )\n        mapFragment.getCircles().let {\n            assertThat(it.size, equalTo(1))\n            assertThat(it[0].center, equalTo(locationTracker.currentLocation!!.toMapPoint()))\n            assertThat(it[0].radius, equalTo(5.2f))\n        }\n\n        locationTracker.currentLocation = Location(3.0, 2.0, accuracy = 2.1f)\n        assertThat(\n            mapFragment.getMarkers(),\n            equalTo(listOf(locationTracker.currentLocation!!.toMapPoint()))\n        )\n        mapFragment.getCircles().let {\n            assertThat(it.size, equalTo(1))\n            assertThat(it[0].center, equalTo(locationTracker.currentLocation!!.toMapPoint()))\n            assertThat(it[0].radius, equalTo(2.1f))\n        }\n    }\n\n    @Test\n    fun showsCurrentLocationAccuracy() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        locationTracker.currentLocation = Location(2.0, 2.0, accuracy = 3.1f)\n        val accuracy = LocationAccuracy.Improving(3.1f)\n        onView(isAssignableFrom(AccuracyStatusView::class.java)).check(matches(hasAccuracy(accuracy)))\n    }\n\n    @Test\n    fun recordButton_should_beHiddenForAutomaticMode() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        startInput(R.id.automatic_mode)\n        onView(withId(R.id.record_button)).check(matches(not(isDisplayed())))\n    }\n\n    @Test\n    fun recordButton_should_beVisibleForManualMode() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        startInput(R.id.manual_mode)\n        onView(withId(R.id.record_button)).check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun placingPoint_updatesCollectionStatus() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        startInput(R.id.placement_mode)\n        mapFragment.click(MapPoint(1.0, 1.0))\n        assertVisible(withText(application.getString(string.collection_status_placement, 1)))\n    }\n\n    @Test\n    fun recordingPointsAutomatically_updatesCollectionStatus() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        startInput(R.id.automatic_mode)\n        assertVisible(\n            withText(\n                application.getString(string.collection_status_auto_seconds_accuracy, 0, 20, 10)\n            )\n        )\n\n        locationTracker.currentLocation = Location(1.0, 1.0)\n        scheduler.runForeground(0)\n        assertVisible(\n            withText(\n                application.getString(string.collection_status_auto_seconds_accuracy, 1, 20, 10)\n            )\n        )\n\n        locationTracker.currentLocation = Location(2.0, 2.0)\n        scheduler.runForeground(DEFAULT_RECORDING_INTERVAL)\n        assertVisible(\n            withText(\n                application.getString(string.collection_status_auto_seconds_accuracy, 2, 20, 10)\n            )\n        )\n    }\n\n    @Test\n    fun recordingPointsAutomatically_updatesAccuracyBasedOnThreshold() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        val unacceptable = LocationAccuracy.Unacceptable(11.0f)\n        locationTracker.currentLocation = Location(1.0, 1.0, accuracy = unacceptable.value)\n        startInput(R.id.automatic_mode)\n        onView(isAssignableFrom(AccuracyStatusView::class.java)).check(matches(hasAccuracy(unacceptable)))\n\n        val improving = LocationAccuracy.Improving(9.0f)\n        locationTracker.currentLocation = Location(1.0, 1.0, accuracy = improving.value)\n        onView(isAssignableFrom(AccuracyStatusView::class.java)).check(matches(hasAccuracy(improving)))\n    }\n\n    @Test\n    fun recordingPointsAutomatically_doesNotRecordFasterThanInterval() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        startInput(R.id.automatic_mode)\n        locationTracker.currentLocation = Location(1.0, 1.0)\n        assertVisible(\n            withText(\n                application.getString(string.collection_status_auto_seconds_accuracy, 0, 20, 10)\n            )\n        )\n    }\n\n    @Test\n    fun recordingPointsAutomatically_andClickingPause_stopsRecording() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        startInput(R.id.automatic_mode)\n        assertVisible(\n            withText(\n                application.getString(string.collection_status_auto_seconds_accuracy, 0, 20, 10)\n            )\n        )\n\n        locationTracker.currentLocation = Location(1.0, 1.0)\n        scheduler.runForeground(0)\n        assertVisible(\n            withText(\n                application.getString(string.collection_status_auto_seconds_accuracy, 1, 20, 10)\n            )\n        )\n\n        EspressoInteractions.clickOn(withContentDescription(string.pause_location_recording))\n        locationTracker.currentLocation = Location(2.0, 2.0)\n        scheduler.runForeground(DEFAULT_RECORDING_INTERVAL)\n        assertVisible(\n            withText(\n                application.getString(string.collection_status_paused, 1)\n            )\n        )\n    }\n\n    @Test\n    fun recordingPointsAutomatically_usesCurrentLocationWhenThereIsOne() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        locationTracker.currentLocation = Location(1.0, 1.0)\n        startInput(R.id.automatic_mode)\n        scheduler.runForeground(0)\n        assertVisible(\n            withText(\n                application.getString(string.collection_status_auto_seconds_accuracy, 1, 20, 10)\n            )\n        )\n    }\n\n    @Test\n    fun whenPolygonExtraPresent_showsPoly() {\n        val polygon = ArrayList<MapPoint>()\n        polygon.add(MapPoint(1.0, 2.0, 3.0, 4.0))\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() }, inputPolygon = polygon)\n        }\n\n        val polys = mapFragment.getPolyLines()\n        assertThat(polys.size, equalTo(1))\n        assertThat(polys[0].points, equalTo(polygon))\n    }\n\n    @Test\n    fun whenPolygonExtraPresent_andOutputModeIsShape_showsClosedPoly() {\n        val polygon = ArrayList<MapPoint>()\n        polygon.add(MapPoint(1.0, 2.0, 3.0, 4.0))\n        polygon.add(MapPoint(2.0, 3.0, 3.0, 4.0))\n        polygon.add(MapPoint(1.0, 2.0, 3.0, 4.0))\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                OutputMode.GEOSHAPE,\n                inputPolygon = polygon\n            )\n        }\n\n        val polys = mapFragment.getPolygons()\n        assertThat(polys.size, equalTo(1))\n        val expectedPolygon = ArrayList<MapPoint>()\n        expectedPolygon.add(MapPoint(1.0, 2.0, 3.0, 4.0))\n        expectedPolygon.add(MapPoint(2.0, 3.0, 3.0, 4.0))\n        assertThat(polys[0].points, equalTo(expectedPolygon))\n    }\n\n    @Test\n    fun whenPolygonExtraPresent_andPolyIsEmpty_andOutputModeIsShape_doesNotShowPoly() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                OutputMode.GEOSHAPE,\n                inputPolygon = emptyList()\n            )\n        }\n\n        val polys = mapFragment.getPolygons()\n        assertThat(polys.size, equalTo(1))\n        assertThat(polys[0].points.isEmpty(), equalTo(true))\n    }\n\n    @Test\n    fun pressingBack_setsCancelledResult() {\n        val onBackPressedDispatcher = OnBackPressedDispatcher()\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ onBackPressedDispatcher })\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        onBackPressedDispatcher.onBackPressed()\n        val result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(result.second.isEmpty, equalTo(true))\n    }\n\n    @Test\n    fun whenInputPolyIsNotEmpty_pressingBack_setsCancelledResult() {\n        val onBackPressedDispatcher = OnBackPressedDispatcher()\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { onBackPressedDispatcher },\n                inputPolygon = listOf(MapPoint(1.0, 1.0))\n            )\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        onBackPressedDispatcher.onBackPressed()\n        val result = resultListener.getAll().lastOrNull()\n        assertThat(result!!.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(result.second.isEmpty, equalTo(true))\n    }\n\n    @Test\n    fun whenPolygonHasBeenModified_pressingBack_andClickingCancel_setsNoResult() {\n        val onBackPressedDispatcher = OnBackPressedDispatcher()\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ onBackPressedDispatcher })\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        startInput(R.id.placement_mode)\n        mapFragment.click(MapPoint(1.0, 1.0))\n        resultListener.clear()\n\n        onBackPressedDispatcher.onBackPressed()\n        EspressoInteractions.clickOn(withText(string.cancel), root = isDialog())\n\n        val result = resultListener.getAll().lastOrNull()\n        assertThat(result, equalTo(null))\n    }\n\n    @Test\n    fun whenPolygonHasBeenCreated_pressingBack_andClickingDiscard_setsEmptyResult() {\n        val onBackPressedDispatcher = OnBackPressedDispatcher()\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ onBackPressedDispatcher })\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        startInput(R.id.placement_mode)\n        mapFragment.click(MapPoint(1.0, 1.0))\n        mapFragment.click(MapPoint(2.0, 2.0))\n        resultListener.clear()\n\n        onBackPressedDispatcher.onBackPressed()\n        EspressoInteractions.clickOn(withText(string.discard), root = isDialog())\n\n        val result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(result.second.getString(GeoPolyFragment.RESULT_GEOPOLY), equalTo(\"\"))\n    }\n\n    @Test\n    fun whenPolygonHasBeenModified_pressingBack_andClickingDiscard_setsOriginalAsResult() {\n        val onBackPressedDispatcher = OnBackPressedDispatcher()\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { onBackPressedDispatcher },\n                inputPolygon = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 1.0))\n            )\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        startInput()\n        mapFragment.click(MapPoint(2.0, 2.0))\n        resultListener.clear()\n\n        onBackPressedDispatcher.onBackPressed()\n        EspressoInteractions.clickOn(withText(string.discard), root = isDialog())\n\n        val result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY),\n            equalTo(\"0.0 0.0 0.0 0.0;1.0 1.0 0.0 0.0\")\n        )\n    }\n\n    @Test\n    fun whenInputPolygonIsOnlyOnePoint_andHasBeenModified_pressingBack_andClickingDiscard_setsOriginalAsResult() {\n        val onBackPressedDispatcher = OnBackPressedDispatcher()\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { onBackPressedDispatcher },\n                inputPolygon = listOf(MapPoint(0.0, 0.0))\n            )\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        startInput()\n        mapFragment.click(MapPoint(2.0, 2.0))\n        resultListener.clear()\n\n        onBackPressedDispatcher.onBackPressed()\n        EspressoInteractions.clickOn(withText(string.discard), root = isDialog())\n\n        val result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY),\n            equalTo(\"0.0 0.0 0.0 0.0\")\n        )\n    }\n\n    @Test\n    fun whenPolygonHasBeenModified_recreating_andPressingBack_andClickingDiscard_setsOriginalAsResult() {\n        val onBackPressedDispatcher = OnBackPressedDispatcher()\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { onBackPressedDispatcher },\n                inputPolygon = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 1.0))\n            )\n        }\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        startInput()\n        mapFragment.click(MapPoint(2.0, 2.0))\n        resultListener.clear()\n\n        scenario.recreate()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        onBackPressedDispatcher.onBackPressed()\n        EspressoInteractions.clickOn(withText(string.discard), root = isDialog())\n\n        val result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY),\n            equalTo(\"0.0 0.0 0.0 0.0;1.0 1.0 0.0 0.0\")\n        )\n    }\n\n    @Test\n    fun recordingPointsAutomatically_usesRetainMockAccuracyTrueToStartLocationTracker() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                OutputMode.GEOTRACE,\n                false,\n                true,\n                emptyList()\n            )\n        }\n\n        startInput(R.id.automatic_mode)\n        assertThat(locationTracker.retainMockAccuracy, equalTo(true))\n    }\n\n    @Test\n    fun recordingPointsAutomatically_usesRetainMockAccuracyFalseToStartLocationTracker() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                OutputMode.GEOTRACE,\n                false,\n                false,\n                emptyList()\n            )\n        }\n\n        startInput(R.id.automatic_mode)\n        assertThat(locationTracker.retainMockAccuracy, equalTo(false))\n    }\n\n    @Test\n    fun recordingPointManually_whenPointIsADuplicateOfTheLastPoint_skipsPoint() {\n        fragmentLauncherRule.launchInContainer { GeoPolyFragment({ OnBackPressedDispatcher() }) }\n\n        startInput(R.id.manual_mode)\n        locationTracker.currentLocation = Location(1.0, 1.0)\n        onView(withId(R.id.record_button)).perform(click())\n        onView(withId(R.id.record_button)).perform(click())\n        assertThat(mapFragment.getPolyLines()[0].points.size, equalTo(1))\n    }\n\n    @Test\n    fun placingPoint_whenPointIsADuplicateOfTheLastPoint_skipsPoint() {\n        fragmentLauncherRule.launchInContainer { GeoPolyFragment({ OnBackPressedDispatcher() }) }\n\n        startInput(R.id.placement_mode)\n        mapFragment.click(MapPoint(1.0, 1.0))\n        mapFragment.click(MapPoint(1.0, 1.0))\n        assertThat(mapFragment.getPolyLines()[0].points.size, equalTo(1))\n    }\n\n    @Test\n    fun buttonsShouldBeEnabledInEditableMode() {\n        val polyline = ArrayList<MapPoint>()\n        polyline.add(MapPoint(1.0, 2.0, 3.0, 4.0))\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                OutputMode.GEOTRACE,\n                false,\n                false,\n                polyline\n            )\n        }\n\n        EspressoAssertions.assertEnabled(withContentDescription(string.input_method))\n        EspressoAssertions.assertEnabled(withContentDescription(string.remove_last_point))\n        EspressoAssertions.assertEnabled(withContentDescription(string.clear))\n        EspressoAssertions.assertEnabled(withContentDescription(string.save))\n    }\n\n    @Test\n    fun buttonsShouldBeDisabledInReadOnlyMode() {\n        val polygon = ArrayList<MapPoint>()\n        polygon.add(MapPoint(1.0, 2.0, 3.0, 4.0))\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                OutputMode.GEOTRACE,\n                true,\n                false,\n                polygon\n            )\n        }\n\n        EspressoAssertions.assertDisabled(withContentDescription(string.input_method))\n        EspressoAssertions.assertDisabled(withContentDescription(string.remove_last_point))\n        EspressoAssertions.assertDisabled(withContentDescription(string.clear))\n        EspressoAssertions.assertDisabled(withContentDescription(string.save))\n    }\n\n    @Test\n    fun polyShouldBeDraggableInEditableMode() {\n        val polyline = ArrayList<MapPoint>()\n        polyline.add(MapPoint(1.0, 2.0, 3.0, 4.0))\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                OutputMode.GEOTRACE,\n                false,\n                false,\n                polyline\n            )\n        }\n\n        assertThat(mapFragment.isPolyDraggable(0), equalTo(true))\n    }\n\n    @Test\n    fun polyShouldNotBeDraggableInReadOnlyMode() {\n        val polygon = ArrayList<MapPoint>()\n        polygon.add(MapPoint(1.0, 2.0, 3.0, 4.0))\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                OutputMode.GEOTRACE,\n                true,\n                false,\n                polygon\n            )\n        }\n\n        assertThat(mapFragment.isPolyDraggable(0), equalTo(false))\n    }\n\n    @Test\n    fun recreatingTheFragmentWithTheLayersDialogDisplayedDoesNotCrashTheApp() {\n        val scenario =\n            fragmentLauncherRule.launchInContainer { GeoPolyFragment({ OnBackPressedDispatcher() }) }\n\n        onView(withId(R.id.layers)).perform(click())\n\n        scenario.recreate()\n    }\n\n    @Test\n    fun whenInvalidMessageIsNotNull_pointsCannotBeAddedByClicking() {\n        val invalidMessage = MutableLiveData<DisplayString?>(null)\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                invalidMessage = invalidMessage\n            )\n        }\n\n        startInput(R.id.placement_mode)\n\n        invalidMessage.value = DisplayString.Raw(\"Blah\")\n        mapFragment.click(MapPoint(0.0, 0.0))\n        assertThat(mapFragment.getPolyLines()[0].points.size, equalTo(0))\n    }\n\n    @Test\n    fun whenInvalidMessageIsNotNull_pointsCannotBeAddedManually() {\n        val invalidMessage = MutableLiveData<DisplayString?>(null)\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                invalidMessage = invalidMessage\n            )\n        }\n\n        startInput(R.id.manual_mode)\n\n        invalidMessage.value = DisplayString.Raw(\"Blah\")\n        EspressoInteractions.clickOn(withContentDescription(string.record_geopoint))\n        assertThat(mapFragment.getPolyLines()[0].points.size, equalTo(0))\n    }\n\n    @Test\n    fun whenInvalidMessageIsNotNull_automaticRecordingStops() {\n        val invalidMessage = MutableLiveData<DisplayString?>(null)\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                invalidMessage = invalidMessage\n            )\n        }\n\n        startInput(R.id.automatic_mode)\n\n        invalidMessage.value = DisplayString.Raw(\"Blah\")\n        locationTracker.currentLocation = Location(1.0, 1.0, 1.0, 1f)\n        scheduler.runForeground(0)\n        assertThat(mapFragment.getPolyLines()[0].points.size, equalTo(0))\n    }\n\n    @Test\n    fun showsAndHidesInvalidMessageSnackbarBasedOnValue() {\n        val invalidMessage = MutableLiveData<DisplayString?>(null)\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                invalidMessage = invalidMessage\n            )\n        }\n\n        val message = \"Something is wrong\"\n        invalidMessage.value = DisplayString.Raw(message)\n        assertVisible(withText(message))\n\n        invalidMessage.value = null\n        assertNotVisible(withText(message))\n        EspressoAssertions.assertAlert(\n            SnackbarUtils.alertStore,\n            application.getString(string.error_fixed),\n            \"No error fixed message shown!\"\n        )\n    }\n\n    @Test\n    fun invalidSnackbarCanBeDismissed() {\n        val invalidMessage = MutableLiveData<DisplayString?>(null)\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                invalidMessage = invalidMessage\n            )\n        }\n\n        val message = \"Something is wrong\"\n        invalidMessage.value = DisplayString.Raw(message)\n        EspressoInteractions.clickOn(withContentDescription(string.close_snackbar))\n        assertNotVisible(withText(message))\n    }\n\n    @Test\n    fun changesPolyLineColorBasedOnInvalidMessage() {\n        val invalidMessage = MutableLiveData<DisplayString?>(null)\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                invalidMessage = invalidMessage\n            )\n        }\n\n        startInput(R.id.placement_mode)\n        mapFragment.click(MapPoint(0.0, 0.0))\n\n        invalidMessage.value = DisplayString.Raw(\"blah\")\n        val errorPolyLine = mapFragment.getPolyLines()[0]\n        assertThat(errorPolyLine.getStrokeColor(), equalTo(MapConsts.DEFAULT_ERROR_COLOR))\n        assertThat(errorPolyLine.highlightLastPoint, equalTo(false))\n\n        invalidMessage.value = null\n        val normalPolyLine = mapFragment.getPolyLines()[0]\n        assertThat(normalPolyLine.getStrokeColor(), equalTo(MapConsts.DEFAULT_STROKE_COLOR))\n        assertThat(normalPolyLine.highlightLastPoint, equalTo(true))\n    }\n\n    @Test\n    fun changesPolygonColorBasedOnInvalidMessage() {\n        val invalidMessage = MutableLiveData<DisplayString?>(null)\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                outputMode = OutputMode.GEOSHAPE,\n                invalidMessage = invalidMessage\n            )\n        }\n\n        startInput(R.id.placement_mode)\n        mapFragment.click(MapPoint(0.0, 0.0))\n\n        invalidMessage.value = DisplayString.Raw(\"blah\")\n        val errorPolyLine = mapFragment.getPolygons()[0]\n        assertThat(errorPolyLine.getStrokeColor(), equalTo(MapConsts.DEFAULT_ERROR_COLOR))\n        assertThat(errorPolyLine.highlightLastPoint, equalTo(false))\n        assertThat(\n            errorPolyLine.getFillColor().opaque(),\n            equalTo(MapConsts.DEFAULT_ERROR_COLOR.opaque())\n        )\n\n        invalidMessage.value = null\n        val normalPolyLine = mapFragment.getPolygons()[0]\n        assertThat(normalPolyLine.getStrokeColor(), equalTo(MapConsts.DEFAULT_STROKE_COLOR))\n        assertThat(normalPolyLine.highlightLastPoint, equalTo(true))\n        assertThat(\n            normalPolyLine.getFillColor().opaque(),\n            equalTo(MapConsts.DEFAULT_STROKE_COLOR.opaque())\n        )\n    }\n\n    @Test\n    fun disablesSaveButtonWhenInvalid() {\n        val invalidMessage = MutableLiveData<DisplayString?>(null)\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                invalidMessage = invalidMessage\n            )\n        }\n\n        invalidMessage.value = DisplayString.Raw(\"Blah\")\n        EspressoAssertions.assertDisabled(withContentDescription(string.save))\n\n        invalidMessage.value = null\n        EspressoAssertions.assertEnabled(withContentDescription(string.save))\n    }\n\n    @Test\n    fun whenOutputModeIsGeoTrace_setsChangeResultWheneverAPointIsAddedAfterTheFirst() {\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                OutputMode.GEOTRACE\n            )\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        startInput(R.id.placement_mode)\n\n        mapFragment.click(MapPoint(0.0, 0.0))\n        var result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE),\n            equalTo(\"\")\n        )\n\n        mapFragment.click(MapPoint(1.0, 1.0))\n        result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE),\n            equalTo(\"0.0 0.0 0.0 0.0;1.0 1.0 0.0 0.0\")\n        )\n    }\n\n    @Test\n    fun whenOutputModeIsGeoShape_doesNotSetChangeResultUntilThereAre3Points() {\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                OutputMode.GEOSHAPE\n            )\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        startInput(R.id.placement_mode)\n\n        mapFragment.click(MapPoint(0.0, 0.0))\n        var result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE),\n            equalTo(\"\")\n        )\n\n        mapFragment.click(MapPoint(1.0, 0.0))\n        result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE),\n            equalTo(\"\")\n        )\n\n        mapFragment.click(MapPoint(1.0, 1.0))\n        result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE),\n            equalTo(\"0.0 0.0 0.0 0.0;1.0 0.0 0.0 0.0;1.0 1.0 0.0 0.0;0.0 0.0 0.0 0.0\")\n        )\n    }\n\n    @Test\n    fun setsChangeResultWheneverAPointIsRemoved() {\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment(\n                { OnBackPressedDispatcher() },\n                inputPolygon =\n                    listOf(\n                        MapPoint(0.0, 0.0),\n                        MapPoint(1.0, 0.0),\n                        MapPoint(1.0, 1.0)\n                    )\n            )\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        EspressoInteractions.clickOn(withContentDescription(string.remove_last_point))\n        val result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE),\n            equalTo(\"0.0 0.0 0.0 0.0;1.0 0.0 0.0 0.0\")\n        )\n    }\n\n    @Test\n    fun setsChangeResultWheneverAPointIsMoved() {\n        val inputPolygon = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 0.0), MapPoint(1.0, 1.0))\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() }, inputPolygon = inputPolygon)\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        val lineId = mapFragment.getFeatureId(inputPolygon)\n        mapFragment.dragPolyLine(lineId, inputPolygon.dropLast(1) + MapPoint(2.0, 2.0))\n\n        val result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE),\n            equalTo(\"0.0 0.0 0.0 0.0;1.0 0.0 0.0 0.0;2.0 2.0 0.0 0.0\")\n        )\n    }\n\n    @Test\n    fun clickingClear_clearsPoints() {\n        val inputPolygon = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 0.0), MapPoint(1.0, 1.0))\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() }, inputPolygon = inputPolygon)\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        EspressoInteractions.clickOn(withContentDescription(string.clear))\n        EspressoInteractions.clickOn(withText(string.clear), root = isDialog())\n        assertThat(mapFragment.getPolyLines().first().points.size, equalTo(0))\n    }\n\n    @Test\n    fun clickingClear_setsChangeResult() {\n        val inputPolygon = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 0.0), MapPoint(1.0, 1.0))\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() }, inputPolygon = inputPolygon)\n        }\n\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        EspressoInteractions.clickOn(withContentDescription(string.clear))\n        EspressoInteractions.clickOn(withText(string.clear), root = isDialog())\n\n        val result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE),\n            equalTo(\"\")\n        )\n    }\n\n    @Test\n    fun recordingPointsAutomatically_setsChangeResult() {\n        val scenario = fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n        val resultListener = FragmentResultRecorder()\n        scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener)\n\n        startInput(R.id.automatic_mode)\n        locationTracker.currentLocation = Location(1.0, 1.0, 1.0, 1f)\n        scheduler.runForeground(0)\n        locationTracker.currentLocation = Location(2.0, 2.0, 1.0, 1f)\n        scheduler.runForeground(DEFAULT_RECORDING_INTERVAL)\n\n        val result = resultListener.getAll().last()\n        assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY))\n        assertThat(\n            result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE),\n            equalTo(\"1.0 1.0 1.0 1.0;2.0 2.0 1.0 1.0\")\n        )\n    }\n\n    @Test\n    fun clickingZoom_zoomsToCurrentLocation() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        locationTracker.currentLocation = Location(5.0, 5.0)\n        EspressoInteractions.clickOn(withContentDescription(string.show_my_location))\n        assertThat(mapFragment.getCenter(), equalTo(MapPoint(5.0, 5.0)))\n    }\n\n    @Test\n    fun whenAutomaticallyRecordingLocation_mapCenterUpdates() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        startInput(R.id.automatic_mode)\n        locationTracker.currentLocation = Location(5.0, 5.0)\n        assertThat(mapFragment.getCenter(), equalTo(MapPoint(5.0, 5.0)))\n\n        locationTracker.currentLocation = Location(1.0, 1.0)\n        assertThat(mapFragment.getCenter(), equalTo(MapPoint(1.0, 1.0)))\n    }\n\n    @Test\n    fun whenNotRecordingLocation_mapCenterDoesNoUpdate() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        locationTracker.currentLocation = Location(5.0, 5.0)\n        assertThat(mapFragment.getCenter(), equalTo(MapPoint(5.0, 5.0)))\n\n        locationTracker.currentLocation = Location(1.0, 1.0)\n        assertThat(mapFragment.getCenter(), equalTo(MapPoint(5.0, 5.0)))\n    }\n\n    @Test\n    fun whenRecordingIsPaused_mapCenterDoesNoUpdate() {\n        fragmentLauncherRule.launchInContainer {\n            GeoPolyFragment({ OnBackPressedDispatcher() })\n        }\n\n        startInput(R.id.automatic_mode)\n        locationTracker.currentLocation = Location(5.0, 5.0)\n        assertThat(mapFragment.getCenter(), equalTo(MapPoint(5.0, 5.0)))\n\n        EspressoInteractions.clickOn(withContentDescription(string.pause_location_recording))\n        locationTracker.currentLocation = Location(1.0, 1.0)\n        assertThat(mapFragment.getCenter(), equalTo(MapPoint(5.0, 5.0)))\n    }\n\n    private fun overrideDependencies(mapFragment: MapFragment) {\n        val application = ApplicationProvider.getApplicationContext<RobolectricApplication>()\n        application.geoDependencyComponent = DaggerGeoDependencyComponent.builder()\n            .application(application)\n            .geoDependencyModule(object : GeoDependencyModule() {\n                override fun providesMapFragmentFactory(): MapFragmentFactory {\n                    return object : MapFragmentFactory {\n                        override fun createMapFragment(): MapFragment {\n                            return mapFragment\n                        }\n                    }\n                }\n\n                override fun providesLocationTracker(application: Application): LocationTracker {\n                    return locationTracker\n                }\n\n                override fun providesReferenceLayerRepository(): ReferenceLayerRepository {\n                    return mock()\n                }\n\n                override fun providesScheduler(): Scheduler {\n                    return scheduler\n                }\n\n                override fun providesSettingsProvider(): SettingsProvider {\n                    return InMemSettingsProvider()\n                }\n\n                override fun providesWebPageService(): WebPageService {\n                    return mock()\n                }\n            })\n            .build()\n    }\n\n    companion object {\n        private val DEFAULT_RECORDING_INTERVAL =\n            INTERVAL_OPTIONS[GeoPolyFragment.DEFAULT_INTERVAL_INDEX].toLong() * 1000\n\n        private fun startInput(mode: Int? = null) {\n            onView(withId(R.id.play)).perform(click())\n\n            if (mode != null) {\n                onView(withId(mode)).inRoot(isDialog()).perform(click())\n                onView(withId(android.R.id.button1)).inRoot(isDialog()).perform(click())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolySettingsDialogFragmentTest.java",
    "content": "package org.odk.collect.geo.geopoly;\n\nimport static android.view.View.GONE;\nimport static android.view.View.VISIBLE;\nimport static junit.framework.TestCase.assertTrue;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.junit.Assert.assertFalse;\nimport static org.mockito.Mockito.when;\nimport static org.robolectric.Shadows.shadowOf;\n\nimport android.content.DialogInterface;\nimport android.view.View;\nimport android.widget.RadioGroup;\nimport android.widget.Spinner;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.InOrder;\nimport org.mockito.Mockito;\nimport org.odk.collect.geo.R;\nimport org.odk.collect.testshared.RobolectricHelpers;\nimport org.robolectric.shadows.ShadowDialog;\n\n@RunWith(AndroidJUnit4.class)\npublic class GeoPolySettingsDialogFragmentTest {\n\n    private final int sampleId = R.id.automatic_mode;\n\n    private FragmentManager fragmentManager;\n    private GeoPolySettingsDialogFragment dialogFragment;\n    private GeoPolySettingsDialogFragment.SettingsDialogCallback callback;\n\n    @Before\n    public void setup() {\n        FragmentActivity activity = RobolectricHelpers.createThemedActivity(FragmentActivity.class);\n        fragmentManager = activity.getSupportFragmentManager();\n        callback = Mockito.mock(GeoPolySettingsDialogFragment.SettingsDialogCallback.class);\n        dialogFragment = new GeoPolySettingsDialogFragment(callback);\n    }\n\n    @Test\n    public void dialogIsCancellable() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        assertThat(shadowOf(dialogFragment.getDialog()).isCancelable(), equalTo(true));\n    }\n\n    @Test\n    public void clickingStart_shouldDismissTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n        assertTrue(dialog.isShowing());\n\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();\n        RobolectricHelpers.runLooper();\n\n        assertFalse(dialog.isShowing());\n        assertTrue(shadowOf(dialog).hasBeenDismissed());\n    }\n\n    @Test\n    public void clickingCancel_shouldDismissTheDialog() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n        assertTrue(dialog.isShowing());\n\n        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick();\n        RobolectricHelpers.runLooper();\n\n        assertFalse(dialog.isShowing());\n        assertTrue(shadowOf(dialog).hasBeenDismissed());\n    }\n\n    @Test\n    public void clickingStart_callsCorrectMethodsInCorrectOrder() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n        RadioGroup radioGroup = dialog.findViewById(R.id.radio_group);\n        Spinner autoInterval = dialog.findViewById(R.id.auto_interval);\n        Spinner accuracyThreshold = dialog.findViewById(R.id.accuracy_threshold);\n\n        radioGroup.check(sampleId);\n        autoInterval.setSelection(2);\n        accuracyThreshold.setSelection(2);\n\n        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick();\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();\n        RobolectricHelpers.runLooper();\n\n        InOrder orderVerifier = Mockito.inOrder(callback);\n        orderVerifier.verify(callback).updateRecordingMode(sampleId);\n        orderVerifier.verify(callback).setIntervalIndex(2);\n        orderVerifier.verify(callback).setAccuracyThresholdIndex(2);\n        orderVerifier.verify(callback).startInput();\n    }\n\n    @Test\n    public void selectingAutomaticMode_displaysIntervalAndAccuracyOptions() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n\n        RadioGroup radioGroup = dialog.findViewById(R.id.radio_group);\n        View autoOptions = dialog.findViewById(R.id.auto_options);\n        assertThat(autoOptions.getVisibility(), equalTo(GONE));\n\n        radioGroup.check(sampleId);\n        assertThat(autoOptions.getVisibility(), equalTo(VISIBLE));\n    }\n\n    @Test\n    public void notSelectingAutomaticMode_doesNotDisplayIntervalAndAccuracyOptions() {\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n        RadioGroup radioGroup = dialog.findViewById(R.id.radio_group);\n        View autoOptions = dialog.findViewById(R.id.auto_options);\n\n        radioGroup.check(R.id.manual_mode);\n        assertThat(autoOptions.getVisibility(), equalTo(GONE));\n    }\n\n    @Test\n    public void creatingDialog_showsCorrectView() {\n        when(callback.getCheckedId()).thenReturn(sampleId);\n        when(callback.getIntervalIndex()).thenReturn(2);\n        when(callback.getAccuracyThresholdIndex()).thenReturn(2);\n\n        dialogFragment.show(fragmentManager, \"TAG\");\n        RobolectricHelpers.runLooper();\n\n        AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();\n\n        assertThat(dialog.findViewById(R.id.auto_options).getVisibility(), equalTo(VISIBLE));\n        assertThat(((RadioGroup) dialog.findViewById(R.id.radio_group)).getCheckedRadioButtonId(), equalTo(sampleId));\n        assertThat(((Spinner) dialog.findViewById(R.id.auto_interval)).getSelectedItemPosition(), equalTo(2));\n        assertThat(((Spinner) dialog.findViewById(R.id.accuracy_threshold)).getSelectedItemPosition(), equalTo(2));\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyUtilsTest.kt",
    "content": "package org.odk.collect.geo.geopoly\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.geo.geopoly.GeoPolyUtils.parseGeometry\nimport org.odk.collect.maps.MapPoint\n\nclass GeoPolyUtilsTest {\n\n    @Test\n    fun parseGeometryTest() {\n        assertThat(parseGeometry(\"1.0 2.0 3 4\"), equalTo(listOf(MapPoint(1.0, 2.0, 3.0, 4.0))))\n        assertThat(\n            parseGeometry(\"1.0 2.0 3 4; 5.0 6.0 7 8\"),\n            equalTo(listOf(MapPoint(1.0, 2.0, 3.0, 4.0), MapPoint(5.0, 6.0, 7.0, 8.0)))\n        )\n\n        assertThat(parseGeometry(\"blah\"), equalTo(emptyList()))\n        assertThat(parseGeometry(\"1.0 2.0 3 4; blah\"), equalTo(emptyList()))\n        assertThat(\n            parseGeometry(\"37.45153333333334 -122.15539166666667 0.0 qwerty\"),\n            equalTo(emptyList())\n        )\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyViewModelTest.kt",
    "content": "package org.odk.collect.geo.geopoly\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.lifecycle.MutableLiveData\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.mockito.Mockito.mock\nimport org.odk.collect.androidshared.ui.DisplayString\nimport org.odk.collect.androidtest.MainDispatcherRule\nimport org.odk.collect.geo.support.FakeLocationTracker\nimport org.odk.collect.testshared.getOrAwaitValue\n\nclass GeoPolyViewModelTest {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    @get:Rule\n    val mainDispatcherRule = MainDispatcherRule()\n\n    @Test\n    fun `#fixedAlerts is null until after invalid message is non-null`() {\n        val invalidMessage = MutableLiveData<DisplayString?>(null)\n        val viewModel = GeoPolyViewModel(\n            GeoPolyFragment.OutputMode.GEOTRACE,\n            emptyList(),\n            false,\n            FakeLocationTracker(),\n            mock(),\n            invalidMessage\n        )\n\n        assertThat(viewModel.fixedAlerts.getOrAwaitValue(), equalTo(null))\n\n        invalidMessage.value = DisplayString.Raw(\"OH NO\")\n        assertThat(viewModel.fixedAlerts.getOrAwaitValue(), equalTo(null))\n\n        invalidMessage.value = null\n        assertThat(viewModel.fixedAlerts.getOrAwaitValue()!!.value, equalTo(Unit))\n    }\n\n    @Test\n    fun `#fixedAlerts is null after repeated invalid message nulls after a non-null`() {\n        val invalidMessage = MutableLiveData<DisplayString?>(null)\n        val viewModel = GeoPolyViewModel(\n            GeoPolyFragment.OutputMode.GEOTRACE,\n            emptyList(),\n            false,\n            FakeLocationTracker(),\n            mock(),\n            invalidMessage\n        )\n\n        invalidMessage.value = DisplayString.Raw(\"OH NO\")\n        invalidMessage.value = null\n        invalidMessage.value = null\n        assertThat(viewModel.fixedAlerts.getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun `#onCleared stops LocationTracker`() {\n        val locationTracker = FakeLocationTracker()\n        val viewModel = GeoPolyViewModel(\n            GeoPolyFragment.OutputMode.GEOTRACE,\n            emptyList(),\n            false,\n            locationTracker,\n            mock(),\n            MutableLiveData(null)\n        )\n\n        assertThat(locationTracker.isStarted, equalTo(true))\n\n        viewModel.onCleared()\n        assertThat(locationTracker.isStarted, equalTo(false))\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/geopoly/InfoContentTest.kt",
    "content": "package org.odk.collect.geo.geopoly\n\nimport android.app.Application\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.junit4.createComposeRule\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport androidx.compose.ui.test.performScrollTo\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\n\n@RunWith(AndroidJUnit4::class)\nclass InfoContentTest {\n\n    @get:Rule\n    val composeTestRule = createComposeRule()\n\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n\n    @Test\n    fun `shows dialog content from snackbar in PLACEMENT mode`() {\n        composeTestRule.setContent {\n            InfoContent(GeoPolyViewModel.RecordingMode.PLACEMENT, fromSnackbar = true) {}\n        }\n\n        assertInfo(\n            listOf(\n                org.odk.collect.strings.R.string.long_press_to_move_point_info_item,\n                org.odk.collect.strings.R.string.remove_last_point_info_item,\n                org.odk.collect.strings.R.string.delete_shape_to_start_over_info_item,\n                org.odk.collect.strings.R.string.add_point_info_item,\n            )\n        )\n    }\n\n    @Test\n    fun `shows dialog content from info button in PLACEMENT mode`() {\n        composeTestRule.setContent {\n            InfoContent(GeoPolyViewModel.RecordingMode.PLACEMENT, fromSnackbar = false) {}\n        }\n\n        assertInfo(\n            listOf(\n                org.odk.collect.strings.R.string.tap_to_add_a_point_info_item,\n                org.odk.collect.strings.R.string.long_press_to_move_point_info_item,\n                org.odk.collect.strings.R.string.remove_last_point_info_item,\n                org.odk.collect.strings.R.string.delete_entire_shape_info_item,\n            )\n        )\n    }\n\n    @Test\n    fun `shows dialog content from snackbar in MANUAL mode`() {\n        composeTestRule.setContent {\n            InfoContent(GeoPolyViewModel.RecordingMode.MANUAL, fromSnackbar = true) {}\n        }\n\n        assertInfo(\n            listOf(\n                org.odk.collect.strings.R.string.long_press_to_move_point_info_item,\n                org.odk.collect.strings.R.string.remove_last_point_info_item,\n                org.odk.collect.strings.R.string.delete_entire_shape_info_item,\n            )\n        )\n    }\n\n    @Test\n    fun `shows dialog content from info button in MANUAL mode`() {\n        composeTestRule.setContent {\n            InfoContent(GeoPolyViewModel.RecordingMode.MANUAL, fromSnackbar = false) {}\n        }\n\n        assertInfo(\n            listOf(\n                org.odk.collect.strings.R.string.long_press_to_move_point_info_item,\n                org.odk.collect.strings.R.string.remove_last_point_info_item,\n                org.odk.collect.strings.R.string.delete_entire_shape_info_item,\n            )\n        )\n    }\n\n    @Test\n    fun `shows dialog content from snackbar in AUTOMATIC mode`() {\n        composeTestRule.setContent {\n            InfoContent(GeoPolyViewModel.RecordingMode.AUTOMATIC, fromSnackbar = true) {}\n        }\n\n        assertInfo(\n            listOf(\n                org.odk.collect.strings.R.string.long_press_to_move_point_info_item,\n                org.odk.collect.strings.R.string.remove_last_point_info_item,\n                org.odk.collect.strings.R.string.delete_entire_shape_info_item,\n            )\n        )\n    }\n\n    @Test\n    fun `shows dialog content from info button in AUTOMATIC mode`() {\n        composeTestRule.setContent {\n            InfoContent(GeoPolyViewModel.RecordingMode.AUTOMATIC, fromSnackbar = false) {}\n        }\n\n        assertInfo(\n            listOf(\n                org.odk.collect.strings.R.string.long_press_to_move_point_info_item,\n                org.odk.collect.strings.R.string.remove_last_point_info_item,\n                org.odk.collect.strings.R.string.delete_entire_shape_info_item,\n            )\n        )\n    }\n\n    @Test\n    fun `calls onDone when Done button is clicked`() {\n        var onDoneCalled = false\n\n        composeTestRule.setContent {\n            InfoContent(GeoPolyViewModel.RecordingMode.PLACEMENT, false) { onDoneCalled = true }\n        }\n\n        composeTestRule\n            .onNodeWithText(context.getString(org.odk.collect.strings.R.string.done))\n            .assertIsDisplayed()\n            .performClick()\n\n        assertThat(onDoneCalled, equalTo(true))\n    }\n\n    private fun assertInfo(items: List<Int>) {\n        items.forEach {\n            composeTestRule\n                .onNodeWithText(context.getString(it))\n                .performScrollTo()\n                .assertIsDisplayed()\n        }\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt",
    "content": "package org.odk.collect.geo.selection\n\nimport android.content.Context\nimport android.graphics.Color\nimport android.os.Bundle\nimport androidx.activity.OnBackPressedDispatcher\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentFactory\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.MutableLiveData\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.doesNotExist\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withContentDescription\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.allOf\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.instanceOf\nimport org.hamcrest.Matchers.not\nimport org.hamcrest.Matchers.notNullValue\nimport org.hamcrest.Matchers.nullValue\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.geo.DaggerGeoDependencyComponent\nimport org.odk.collect.geo.GeoDependencyModule\nimport org.odk.collect.geo.R\nimport org.odk.collect.geo.support.FakeMapFragment\nimport org.odk.collect.geo.support.Fixtures\nimport org.odk.collect.geo.support.RobolectricApplication\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapFragmentFactory\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.layers.OfflineMapLayersPickerBottomSheetDialogFragment\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.maps.markers.MarkerIconDescription\nimport org.odk.collect.material.BottomSheetBehavior\nimport org.odk.collect.material.MaterialProgressDialogFragment\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.testshared.RobolectricHelpers.getFragmentByClass\nimport org.odk.collect.webpage.WebPageService\n\n@RunWith(AndroidJUnit4::class)\nclass SelectionMapFragmentTest {\n\n    private val application = ApplicationProvider.getApplicationContext<RobolectricApplication>()\n\n    private lateinit var map: FakeMapFragment\n    private val data = mock<SelectionMapData> {\n        on { isLoading() } doReturn MutableNonNullLiveData(false)\n        on { getMapTitle() } doReturn MutableLiveData(\"\")\n        on { getItemType() } doReturn \"Things\"\n        on { getItemCount() } doReturn MutableNonNullLiveData(0)\n        on { getMappableItems() } doReturn MutableLiveData(emptyList())\n    }\n\n    private val onBackPressedDispatcher = OnBackPressedDispatcher()\n\n    @get:Rule\n    val launcherRule = FragmentScenarioLauncherRule(\n        object : FragmentFactory() {\n            override fun instantiate(classLoader: ClassLoader, className: String): Fragment {\n                return SelectionMapFragment(data, onBackPressedDispatcher = { onBackPressedDispatcher })\n            }\n        }\n    )\n\n    @Before\n    fun setup() {\n        application.geoDependencyComponent = DaggerGeoDependencyComponent.builder()\n            .application(application)\n            .geoDependencyModule(object : GeoDependencyModule() {\n                override fun providesMapFragmentFactory(): MapFragmentFactory {\n                    return object : MapFragmentFactory {\n                        override fun createMapFragment(): MapFragment {\n                            return FakeMapFragment().also {\n                                map = it\n                            }\n                        }\n                    }\n                }\n\n                override fun providesPermissionChecker(context: Context): PermissionsChecker {\n                    return object : PermissionsChecker {\n                        override fun isPermissionGranted(vararg permissions: String): Boolean {\n                            return true\n                        }\n                    }\n                }\n\n                override fun providesReferenceLayerRepository(): ReferenceLayerRepository {\n                    return mock()\n                }\n\n                override fun providesScheduler(): Scheduler {\n                    return mock()\n                }\n\n                override fun providesSettingsProvider(): SettingsProvider {\n                    return InMemSettingsProvider()\n                }\n\n                override fun providesWebPageService(): WebPageService {\n                    return mock()\n                }\n            }).build()\n\n        BottomSheetBehavior.DRAGGING_ENABLED = false\n    }\n\n    @Test\n    fun `summary sheet is hidden initially`() {\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        onView(withId(R.id.summary_sheet)).check(matches(not(isDisplayed())))\n    }\n\n    @Test\n    fun `does not have enabled back callbacks`() {\n        val scenario = launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        scenario.onFragment {\n            assertThat(onBackPressedDispatcher.hasEnabledCallbacks(), equalTo(false))\n        }\n    }\n\n    @Test\n    fun `updates markers when items update`() {\n        val items: List<MappableSelectItem> = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0))\n        )\n        val itemsLiveData = MutableLiveData(items)\n        whenever(data.getMappableItems()).thenReturn(itemsLiveData)\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        assertThat(map.getMarkers(), equalTo(itemsLiveData.value?.map { (it as MappableSelectItem.MappableSelectPoint).toMapPoint() }))\n\n        itemsLiveData.value = emptyList()\n        assertThat(map.getMarkers(), equalTo(emptyList()))\n    }\n\n    @Test\n    fun `updates item count when items update`() {\n        val items: List<MappableSelectItem> = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0),\n            Fixtures.actionMappableSelectPoint().copy(id = 1)\n        )\n\n        val itemsLiveData = MutableLiveData(items)\n        whenever(data.getMappableItems()).thenReturn(itemsLiveData)\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        onView(withText(application.getString(org.odk.collect.strings.R.string.select_item_count, \"Things\", 0, 2)))\n            .check(matches(isDisplayed()))\n\n        itemsLiveData.value = emptyList()\n        onView(withText(application.getString(org.odk.collect.strings.R.string.select_item_count, \"Things\", 0, 0)))\n            .check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `shows polyline when item has multiple points`() {\n        val items: List<MappableSelectItem> = listOf(\n            Fixtures.actionMappableSelectLine().copy(\n                id = 0,\n                points = listOf(\n                    MapPoint(40.0, 0.0),\n                    MapPoint(41.0, 0.0)\n                ),\n                strokeWidth = \"10\",\n                strokeColor = \"#ffffff\"\n            )\n        )\n\n        val itemsLiveData = MutableLiveData(items)\n        whenever(data.getMappableItems()).thenReturn(itemsLiveData)\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n        assertThat(map.getPolyLines()[0].points, equalTo(itemsLiveData.value?.map { (it as MappableSelectItem.MappableSelectLine).points }?.first()))\n        assertThat(map.getPolyLines()[0].getStrokeWidth(), equalTo(10f))\n        assertThat(map.getPolyLines()[0].getStrokeColor(), equalTo(-1))\n        onView(withText(application.getString(org.odk.collect.strings.R.string.select_item_count, \"Things\", 0, 1)))\n            .check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `shows polygon when item has multiple closed points`() {\n        val items: List<MappableSelectItem> = listOf(\n            Fixtures.actionMappableSelectPolygon().copy(\n                id = 0,\n                points = listOf(\n                    MapPoint(40.0, 0.0),\n                    MapPoint(41.0, 0.0),\n                    MapPoint(40.0, 0.0)\n                ),\n                strokeWidth = \"10\",\n                strokeColor = \"#aaccee\",\n                fillColor = \"#ffffff\"\n            )\n        )\n\n        val itemsLiveData = MutableLiveData(items)\n        whenever(data.getMappableItems()).thenReturn(itemsLiveData)\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        assertThat(map.getPolygons()[0].points, equalTo(itemsLiveData.value?.map { (it as MappableSelectItem.MappableSelectPolygon).points }?.first()))\n        assertThat(map.getPolygons()[0].getStrokeWidth(), equalTo(10f))\n        assertThat(map.getPolygons()[0].getStrokeColor(), equalTo(-5583634))\n        assertThat(map.getPolygons()[0].getFillColor(), equalTo(1157627903))\n        onView(withText(application.getString(org.odk.collect.strings.R.string.select_item_count, \"Things\", 0, 1)))\n            .check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `zooms to fit all items`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0))\n        )\n\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        val points = items.map { it.toMapPoint() }\n        assertThat(map.getZoomBoundingBox(), equalTo(Pair(points, 0.8)))\n    }\n\n    @Test\n    fun `zooms to fit all points in for item with multiple points`() {\n        val points = listOf(MapPoint(40.0, 0.0), MapPoint(41.0, 0.0))\n        val items: List<MappableSelectItem> = listOf(\n            Fixtures.actionMappableSelectLine().copy(id = 0, points = points)\n        )\n\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        assertThat(map.getZoomBoundingBox(), equalTo(Pair(points, 0.8)))\n    }\n\n    @Test\n    fun `does not zoom to fit all items again when they change`() {\n        val originalItems =\n            listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)))\n        val itemsLiveData: MutableLiveData<List<MappableSelectItem>?> =\n            MutableLiveData(originalItems)\n        whenever(data.getMappableItems()).thenReturn(itemsLiveData)\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        itemsLiveData.value =\n            listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(52.0, 0.0)))\n\n        val points = originalItems.map { it.toMapPoint() }\n        assertThat(map.getZoomBoundingBox(), equalTo(Pair(points, 0.8)))\n    }\n\n    @Test\n    fun `does not zoom to fit all items if map already has center`() {\n        val items = listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)))\n        val itemsLiveData: MutableLiveData<List<MappableSelectItem>?> =\n            MutableLiveData(items)\n        whenever(data.getMappableItems()).thenReturn(itemsLiveData)\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.setCenter(MapPoint(12.3, 45.6), false)\n        map.ready()\n\n        assertThat(map.getCenter(), equalTo(MapPoint(12.3, 45.6)))\n    }\n\n    @Test\n    fun `zooms to current location when zoomToFitItems is false`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0))\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(\n            SelectionMapFragment::class.java,\n            factory = FragmentFactoryBuilder()\n                .forClass(SelectionMapFragment::class.java) {\n                    SelectionMapFragment(\n                        data,\n                        zoomToFitItems = false,\n                        onBackPressedDispatcher = { onBackPressedDispatcher }\n                    )\n                }.build()\n        )\n        map.ready()\n\n        assertThat(map.hasCenter(), equalTo(false))\n\n        map.setLocation(MapPoint(1.0, 2.0))\n        assertThat(map.getCenter(), equalTo(MapPoint(1.0, 2.0)))\n    }\n\n    @Test\n    fun `zooms to current location with default zoom level when there are no items and there is no zoom level saved`() {\n        whenever(data.getMappableItems()).doReturn(MutableLiveData(emptyList()))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        assertThat(map.hasCenter(), equalTo(false))\n\n        map.setLocation(MapPoint(1.0, 2.0))\n        assertThat(map.getCenter(), equalTo(MapPoint(1.0, 2.0)))\n        assertThat(map.getZoom(), equalTo(MapFragment.POINT_ZOOM.toDouble()))\n    }\n\n    @Test\n    fun `zooms to current location with saved zoom level when there are no items and there is last zoom level set by user saved`() {\n        whenever(data.getMappableItems()).doReturn(MutableLiveData(emptyList()))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.setZoomLevel(10f)\n        map.ready()\n\n        assertThat(map.hasCenter(), equalTo(false))\n\n        map.setLocation(MapPoint(1.0, 2.0))\n        assertThat(map.getCenter(), equalTo(MapPoint(1.0, 2.0)))\n        assertThat(map.getZoom(), equalTo(10.0))\n    }\n\n    @Test\n    fun `does not zoom to current location when it changes`() {\n        whenever(data.getMappableItems()).doReturn(MutableLiveData(emptyList()))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        assertThat(map.hasCenter(), equalTo(false))\n\n        map.setLocation(MapPoint(1.0, 2.0))\n        assertThat(map.getCenter(), equalTo(MapPoint(1.0, 2.0)))\n\n        map.setLocation(MapPoint(3.0, 4.0))\n        assertThat(map.getCenter(), equalTo(MapPoint(1.0, 2.0)))\n    }\n\n    @Test\n    fun `does not zoom to current location when items change`() {\n        val originalItems =\n            listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)))\n        val itemsLiveData: MutableLiveData<List<MappableSelectItem>?> =\n            MutableLiveData(originalItems)\n        whenever(data.getMappableItems()).thenReturn(itemsLiveData)\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n        map.setLocation(MapPoint(67.0, 48.0))\n\n        itemsLiveData.value =\n            listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(52.0, 0.0)))\n\n        val points = originalItems.map { it.toMapPoint() }\n        assertThat(map.getZoomBoundingBox(), equalTo(Pair(points, 0.8)))\n    }\n\n    @Test\n    fun `clicking current location button zooms to gps location`() {\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.setLocation(MapPoint(40.181389, 44.514444))\n        onView(withId(R.id.zoom_to_location)).perform(click())\n\n        assertThat(map.getCenter(), equalTo(MapPoint(40.181389, 44.514444)))\n        assertThat(map.getZoom(), equalTo(MapFragment.POINT_ZOOM.toDouble()))\n    }\n\n    @Test\n    fun `clicking zoom to fit button zooms to fit all items`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0))\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        onView(withId(R.id.zoom_to_bounds)).perform(click())\n\n        val points = items.map { it.toMapPoint() }\n        assertThat(map.getZoomBoundingBox(), equalTo(Pair(points, 0.8)))\n    }\n\n    @Test\n    fun `clicking zoom to fit button zooms to fit all polys`() {\n        val points = listOf(MapPoint(40.0, 0.0), MapPoint(41.0, 0.0))\n        val items: List<MappableSelectItem> = listOf(\n            Fixtures.actionMappableSelectLine().copy(id = 0, points = points)\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        onView(withId(R.id.zoom_to_bounds)).perform(click())\n        assertThat(map.getZoomBoundingBox(), equalTo(Pair(points, 0.8)))\n    }\n\n    @Test\n    fun `clicking layers button navigates to layers settings`() {\n        val scenario = launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        onView(withId(R.id.layer_menu)).perform(click())\n        scenario.onFragment {\n            assertThat(\n                it.childFragmentManager.findFragmentByTag(OfflineMapLayersPickerBottomSheetDialogFragment::class.java.name),\n                instanceOf(OfflineMapLayersPickerBottomSheetDialogFragment::class.java)\n            )\n        }\n    }\n\n    @Test\n    fun `clicking on item centers on that item with current zoom level`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0))\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.zoomToPoint(MapPoint(55.0, 66.0), 2.0, false)\n\n        map.clickOnFeature(1)\n        assertThat(map.getCenter(), equalTo(items[1].toMapPoint()))\n        assertThat(map.getZoom(), equalTo(2.0))\n    }\n\n    @Test\n    fun `clicking on item with multiple points zooms to fit all item points`() {\n        val itemPoints = listOf(MapPoint(40.0, 0.0), MapPoint(41.0, 0.0))\n        val items = listOf(\n            Fixtures.actionMappableSelectLine().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))),\n            Fixtures.actionMappableSelectLine().copy(id = 1, points = itemPoints)\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(1)\n        assertThat(map.getZoomBoundingBox(), equalTo(Pair(itemPoints, 0.8)))\n    }\n\n    /**\n     * This looks like a duplicated test, but it's easy to write an implementation that will work\n     * for everything else and break for interleaved points and traces.\n     */\n    @Test\n    fun `clicking on item always selects correct item`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectLine().copy(id = 0, points = listOf(MapPoint(40.0, 0.0), MapPoint(41.0, 0.0))),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(45.0, 0.0))\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeatureId(map.getFeatureId(listOf((items[1] as MappableSelectItem.MappableSelectPoint).point)))\n        assertThat(map.getCenter(), equalTo((items[1] as MappableSelectItem.MappableSelectPoint).point))\n    }\n\n    @Test\n    fun `clicking on item switches item marker to large icon`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(\n                id = 0,\n                smallIcon = android.R.drawable.ic_lock_idle_charging,\n                largeIcon = android.R.drawable.ic_lock_idle_alarm,\n                symbol = \"A\",\n                color = \"#ffffff\"\n            ),\n            Fixtures.actionMappableSelectPoint().copy(\n                id = 1,\n                smallIcon = android.R.drawable.ic_lock_idle_charging,\n                largeIcon = android.R.drawable.ic_lock_idle_alarm,\n                symbol = \"B\",\n                color = \"#000000\"\n            )\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(1)\n\n        val firstIcon = map.getMarkerIcons()[0]!! as MarkerIconDescription.DrawableResource\n        assertThat(firstIcon.drawable, equalTo(items[0].smallIcon))\n        assertThat(firstIcon.getSymbol(), equalTo(\"A\"))\n        assertThat(firstIcon.getColor(), equalTo(Color.parseColor(\"#ffffff\")))\n\n        val secondIcon = map.getMarkerIcons()[1]!! as MarkerIconDescription.DrawableResource\n        assertThat(secondIcon.drawable, equalTo(items[1].largeIcon))\n        assertThat(secondIcon.getSymbol(), equalTo(\"B\"))\n        assertThat(secondIcon.getColor(), equalTo(Color.parseColor(\"#000000\")))\n    }\n\n    @Test\n    fun `clicking on item when another has been tapped switches the first one back to its small icon`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(\n                id = 0,\n                smallIcon = android.R.drawable.ic_lock_idle_charging,\n                largeIcon = android.R.drawable.ic_lock_idle_alarm,\n                symbol = \"A\",\n                color = \"#ffffff\"\n            ),\n            Fixtures.actionMappableSelectPoint().copy(\n                id = 1,\n                smallIcon = android.R.drawable.ic_lock_idle_charging,\n                largeIcon = android.R.drawable.ic_lock_idle_alarm,\n                symbol = \"B\",\n                color = \"#000000\"\n            )\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(0)\n        map.clickOnFeature(1)\n\n        val firstIcon = map.getMarkerIcons()[0]!! as MarkerIconDescription.DrawableResource\n        assertThat(firstIcon.drawable, equalTo(items[0].smallIcon))\n        assertThat(firstIcon.getSymbol(), equalTo(\"A\"))\n        assertThat(firstIcon.getColor(), equalTo(Color.parseColor(\"#ffffff\")))\n\n        val secondIcon = map.getMarkerIcons()[1]!! as MarkerIconDescription.DrawableResource\n        assertThat(secondIcon.drawable, equalTo(items[1].largeIcon))\n        assertThat(secondIcon.getSymbol(), equalTo(\"B\"))\n        assertThat(secondIcon.getColor(), equalTo(Color.parseColor(\"#000000\")))\n    }\n\n    @Test\n    fun `clicking on item sets item on summary sheet`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, name = \"Blah1\"),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, name = \"Blah2\")\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(0)\n        onView(allOf(isDescendantOfA(withId(R.id.summary_sheet)), withText(\"Blah1\")))\n            .check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `clicking on item returns item ID as result when skipSummary is true`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0),\n            Fixtures.actionMappableSelectPoint().copy(id = 1)\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        val scenario = launcherRule.launchInContainer(\n            SelectionMapFragment::class.java,\n            factory = FragmentFactoryBuilder()\n                .forClass(SelectionMapFragment::class.java) {\n                    SelectionMapFragment(\n                        data,\n                        skipSummary = true,\n                        onBackPressedDispatcher = { onBackPressedDispatcher }\n                    )\n                }.build()\n        )\n        map.ready()\n\n        var actualResult: Bundle? = null\n        scenario.onFragment {\n            it.parentFragmentManager.setFragmentResultListener(\n                SelectionMapFragment.REQUEST_SELECT_ITEM,\n                it\n            ) { _: String?, result: Bundle ->\n                actualResult = result\n            }\n        }\n\n        map.clickOnFeature(0)\n        assertThat(\n            actualResult!!.getLong(SelectionMapFragment.RESULT_SELECTED_ITEM),\n            equalTo(items[0].id)\n        )\n    }\n\n    @Test\n    fun `clicking map with an item selected deselects it`() {\n        val item = Fixtures.actionMappableSelectPoint().copy(id = 0, name = \"Blah1\")\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(listOf(item)))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(0)\n        map.click(MapPoint(0.0, 0.0))\n\n        onView(allOf(isDescendantOfA(withId(R.id.summary_sheet)), withText(\"Blah1\")))\n            .check(matches(not(isDisplayed())))\n        assertThat((map.getMarkerIcons()[0]!! as MarkerIconDescription.DrawableResource).drawable, equalTo(item.smallIcon))\n    }\n\n    @Test\n    fun `pressing back with an item selected deselects it`() {\n        val item = Fixtures.actionMappableSelectPoint()\n            .copy(id = 0, name = \"Blah1\", symbol = \"A\", color = \"#ffffff\")\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(listOf(item)))\n\n        val scenario = launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(0)\n        scenario.onFragment {\n            onBackPressedDispatcher.onBackPressed()\n        }\n\n        onView(allOf(isDescendantOfA(withId(R.id.summary_sheet)), withText(\"Blah1\")))\n            .check(matches(not(isDisplayed())))\n\n        val icon = map.getMarkerIcons()[0]!! as MarkerIconDescription.DrawableResource\n        assertThat(icon.drawable, equalTo(item.smallIcon))\n        assertThat(icon.getSymbol(), equalTo(\"A\"))\n        assertThat(icon.getColor(), equalTo(Color.parseColor(\"#ffffff\")))\n    }\n\n    @Test\n    fun `pressing back after deselecting item disables back callbacks`() {\n        val item = Fixtures.actionMappableSelectPoint().copy(id = 0, name = \"Blah1\")\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(listOf(item)))\n\n        val scenario = launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(0)\n        scenario.onFragment {\n            onBackPressedDispatcher.onBackPressed()\n            assertThat(onBackPressedDispatcher.hasEnabledCallbacks(), equalTo(false))\n        }\n    }\n\n    @Test\n    fun `recreating after deselecting item has no item selected`() {\n        val items = listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, name = \"Point1\"))\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        val scenario = launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(0)\n        scenario.onFragment {\n            onBackPressedDispatcher.onBackPressed()\n        }\n\n        scenario.recreate()\n        map.ready()\n\n        onView(allOf(isDescendantOfA(withId(R.id.summary_sheet)), withText(\"Point1\")))\n            .check(doesNotExist())\n    }\n\n    @Test\n    fun `clicking action hides summary sheet`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(\n                id = 0,\n                name = \"Item\",\n                action = IconifiedText(null, \"Action\")\n            )\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(0)\n        onView(withText(\"Action\")).perform(click())\n        onView(withText(\"Item\")).check(matches(not(isDisplayed())))\n    }\n\n    @Test\n    fun `centers on already selected item`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(40.1, 0.0), selected = true)\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        assertThat(map.getCenter(), equalTo(items[1].toMapPoint()))\n        assertThat(map.getZoom(), equalTo(MapFragment.POINT_ZOOM.toDouble()))\n    }\n\n    @Test\n    fun `does not move when location changes when centered on already selected item`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), selected = true)\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.setLocation(MapPoint(1.0, 2.0))\n        assertThat(map.getCenter(), equalTo(items[1].toMapPoint()))\n    }\n\n    @Test\n    fun `hides new item button when showNewItemButton is false`() {\n        launcherRule.launchInContainer(\n            SelectionMapFragment::class.java,\n            factory = FragmentFactoryBuilder()\n                .forClass(SelectionMapFragment::class.java) {\n                    SelectionMapFragment(\n                        data,\n                        showNewItemButton = false,\n                        onBackPressedDispatcher = { onBackPressedDispatcher }\n                    )\n                }.build()\n        )\n        map.ready()\n\n        onView(withContentDescription(org.odk.collect.strings.R.string.new_item)).check(matches(not(isDisplayed())))\n    }\n\n    @Test\n    fun `ignores feature clicks for IDs that are not item features`() {\n        launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeatureId(-1)\n        map.clickOnFeatureId(-2) // First click is fine but second could use the ID and crash\n    }\n\n    @Test\n    fun `recreating maintains selection`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0), name = \"Point1\"),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), name = \"Point2\")\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        val scenario = launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(1)\n\n        scenario.recreate()\n        map.ready()\n\n        onView(allOf(isDescendantOfA(withId(R.id.summary_sheet)), withText(\"Point2\")))\n            .check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `recreating with initial selection maintains new selection`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint()\n                .copy(id = 0, point = MapPoint(40.0, 0.0), name = \"Point1\", selected = true),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), name = \"Point2\")\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        val scenario = launcherRule.launchInContainer(SelectionMapFragment::class.java)\n        map.ready()\n\n        map.clickOnFeature(1)\n\n        scenario.recreate()\n        map.ready()\n\n        onView(allOf(isDescendantOfA(withId(R.id.summary_sheet)), withText(\"Point2\")))\n            .check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `shows progress dialog when loading`() {\n        val loadingLiveData = MutableNonNullLiveData(false)\n        whenever(data.isLoading()).thenReturn(loadingLiveData)\n\n        launcherRule.launchInContainer(SelectionMapFragment::class.java).onFragment {\n            val dialogClass = MaterialProgressDialogFragment::class.java\n            assertThat(getFragmentByClass(it.childFragmentManager, dialogClass), nullValue())\n\n            loadingLiveData.value = true\n            assertThat(getFragmentByClass(it.childFragmentManager, dialogClass), notNullValue())\n\n            loadingLiveData.value = false\n            assertThat(getFragmentByClass(it.childFragmentManager, dialogClass), nullValue())\n        }\n    }\n\n    @Test\n    fun `onDestroy works if the view was never created`() {\n        val scenario = launcherRule.launchInContainer(\n            SelectionMapFragment::class.java,\n            initialState = Lifecycle.State.CREATED // `onCreateView` is called at `RESUMED`\n        )\n\n        scenario.moveToState(Lifecycle.State.DESTROYED)\n    }\n\n    @Test\n    fun `opening the map with already selected item when skipSummary is true does not close the map`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), selected = true)\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        val scenario = launcherRule.launchInContainer(\n            SelectionMapFragment::class.java,\n            factory = FragmentFactoryBuilder()\n                .forClass(SelectionMapFragment::class.java) {\n                    SelectionMapFragment(\n                        data,\n                        skipSummary = true,\n                        onBackPressedDispatcher = { onBackPressedDispatcher }\n                    )\n                }.build()\n        )\n        var actualResult: Bundle? = null\n        scenario.onFragment {\n            it.parentFragmentManager.setFragmentResultListener(\n                SelectionMapFragment.REQUEST_SELECT_ITEM,\n                it\n            ) { _: String?, result: Bundle ->\n                actualResult = result\n            }\n        }\n\n        map.ready()\n\n        assertThat(actualResult, equalTo(null))\n    }\n\n    @Test\n    fun `recreating the map with already selected item when skipSummary is true does not close the map`() {\n        val items = listOf(\n            Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)),\n            Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), selected = true)\n        )\n        whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))\n\n        val scenario = launcherRule.launchInContainer(\n            SelectionMapFragment::class.java,\n            factory = FragmentFactoryBuilder()\n                .forClass(SelectionMapFragment::class.java) {\n                    SelectionMapFragment(\n                        data,\n                        skipSummary = true,\n                        onBackPressedDispatcher = { onBackPressedDispatcher }\n                    )\n                }.build()\n        )\n        map.ready()\n        scenario.recreate()\n        var actualResult: Bundle? = null\n        scenario.onFragment {\n            it.parentFragmentManager.setFragmentResultListener(\n                SelectionMapFragment.REQUEST_SELECT_ITEM,\n                it\n            ) { _: String?, result: Bundle ->\n                actualResult = result\n            }\n        }\n        map.ready()\n\n        assertThat(actualResult, equalTo(null))\n    }\n\n    private fun MappableSelectItem.MappableSelectPoint.toMapPoint(): MapPoint {\n        return MapPoint(this.point.latitude, this.point.longitude)\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/selection/SelectionSummarySheetTest.kt",
    "content": "package org.odk.collect.geo.selection\n\nimport android.app.Application\nimport android.view.View\nimport androidx.test.core.app.ApplicationProvider.getApplicationContext\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.geo.databinding.PropertyBinding\nimport org.odk.collect.geo.support.Fixtures\nimport org.odk.collect.testshared.RobolectricHelpers.getCreatedFromResId\n\n@RunWith(AndroidJUnit4::class)\nclass SelectionSummarySheetTest {\n\n    private val application = getApplicationContext<Application>().also {\n        it.setTheme(com.google.android.material.R.style.Theme_Material3_Light)\n    }\n\n    @Test\n    fun `setItem shows an error chip when the status is ERRORS`() {\n        val selectionSummarySheet = SelectionSummarySheet(application)\n        selectionSummarySheet.setItem(\n            Fixtures.actionMappableSelectPoint().copy(\n                status = Status.ERRORS\n            )\n        )\n\n        assertThat(selectionSummarySheet.binding.statusChip.visibility, equalTo(View.VISIBLE))\n        assertThat(selectionSummarySheet.binding.statusChip.errors, equalTo(true))\n    }\n\n    @Test\n    fun `setItem shows a no-error chip when the status is NO_ERRORS`() {\n        val selectionSummarySheet = SelectionSummarySheet(application)\n        selectionSummarySheet.setItem(\n            Fixtures.actionMappableSelectPoint().copy(\n                status = Status.NO_ERRORS\n            )\n        )\n\n        assertThat(selectionSummarySheet.binding.statusChip.visibility, equalTo(View.VISIBLE))\n        assertThat(selectionSummarySheet.binding.statusChip.errors, equalTo(false))\n    }\n\n    @Test\n    fun `setItem does not show a chip if the status is null`() {\n        val selectionSummarySheet = SelectionSummarySheet(application)\n        selectionSummarySheet.setItem(Fixtures.actionMappableSelectPoint())\n\n        assertThat(selectionSummarySheet.binding.statusChip.visibility, equalTo(View.GONE))\n    }\n\n    @Test\n    fun `setItem shows name`() {\n        val selectionSummarySheet = SelectionSummarySheet(application)\n        selectionSummarySheet.setItem(\n            Fixtures.actionMappableSelectPoint().copy(\n                name = \"Cosmic Dread\"\n            )\n        )\n\n        assertThat(selectionSummarySheet.binding.name.text, equalTo(\"Cosmic Dread\"))\n    }\n\n    @Test\n    fun `setItem shows properties`() {\n        val selectionSummarySheet = SelectionSummarySheet(application)\n        selectionSummarySheet.setItem(\n            Fixtures.actionMappableSelectPoint().copy(\n                properties = listOf(\n                    IconifiedText(\n                        android.R.drawable.ic_btn_speak_now,\n                        \"Emotion\"\n                    ),\n                    IconifiedText(\n                        android.R.drawable.ic_dialog_info,\n                        \"Mystery\"\n                    )\n                )\n            )\n        )\n\n        val firstProperty =\n            PropertyBinding.bind(selectionSummarySheet.binding.properties.getChildAt(0))\n        assertThat(firstProperty.text.text, equalTo(\"Emotion\"))\n        val firstIcon = firstProperty.icon.drawable\n        assertThat(getCreatedFromResId(firstIcon), equalTo(android.R.drawable.ic_btn_speak_now))\n\n        val secondProperty =\n            PropertyBinding.bind(selectionSummarySheet.binding.properties.getChildAt(1))\n        assertThat(secondProperty.text.text, equalTo(\"Mystery\"))\n        val secondIcon = secondProperty.icon.drawable\n        assertThat(getCreatedFromResId(secondIcon), equalTo(android.R.drawable.ic_dialog_info))\n    }\n\n    @Test\n    fun `properties without icons have hidden icon view`() {\n        val selectionSummarySheet = SelectionSummarySheet(application)\n        selectionSummarySheet.setItem(\n            Fixtures.actionMappableSelectPoint().copy(\n                properties = listOf(\n                    IconifiedText(\n                        null,\n                        \"Emotion\"\n                    )\n                )\n            )\n        )\n\n        val property = PropertyBinding.bind(selectionSummarySheet.binding.properties.getChildAt(0))\n        assertThat(property.text.text, equalTo(\"Emotion\"))\n        assertThat(property.icon.visibility, equalTo(View.GONE))\n    }\n\n    @Test\n    fun `properties reset between items`() {\n        val selectionSummarySheet = SelectionSummarySheet(application)\n        selectionSummarySheet.setItem(\n            Fixtures.actionMappableSelectPoint().copy(\n                properties = listOf(\n                    IconifiedText(\n                        android.R.drawable.ic_btn_speak_now,\n                        \"Emotion\"\n                    )\n                )\n            )\n        )\n\n        selectionSummarySheet.setItem(\n            Fixtures.actionMappableSelectPoint().copy(\n                properties = listOf(\n                    IconifiedText(\n                        android.R.drawable.ic_dialog_info,\n                        \"Mystery\"\n                    )\n                )\n            )\n        )\n\n        assertThat(selectionSummarySheet.binding.properties.childCount, equalTo(1))\n\n        val property =\n            PropertyBinding.bind(selectionSummarySheet.binding.properties.getChildAt(0))\n        assertThat(property.text.text, equalTo(\"Mystery\"))\n        val firstIcon = property.icon.drawable\n        assertThat(getCreatedFromResId(firstIcon), equalTo(android.R.drawable.ic_dialog_info))\n    }\n\n    @Test\n    fun `setItem shows info and hides action when it is non-null`() {\n        val selectionSummarySheet = SelectionSummarySheet(application)\n        selectionSummarySheet.setItem(\n            Fixtures.infoMappableSelectPoint().copy(\n                info = \"Don't even bother looking\"\n            )\n        )\n\n        assertThat(selectionSummarySheet.binding.info.visibility, equalTo(View.VISIBLE))\n        assertThat(selectionSummarySheet.binding.action.visibility, equalTo(View.GONE))\n        assertThat(selectionSummarySheet.binding.info.text, equalTo(\"Don't even bother looking\"))\n    }\n\n    @Test\n    fun `setItem shows action and hides info when it is non-null`() {\n        val selectionSummarySheet = SelectionSummarySheet(application)\n        selectionSummarySheet.setItem(\n            Fixtures.actionMappableSelectPoint().copy(\n                action = IconifiedText(\n                    android.R.drawable.ic_btn_speak_now,\n                    \"Come on in\"\n                )\n            )\n        )\n\n        assertThat(selectionSummarySheet.binding.action.visibility, equalTo(View.VISIBLE))\n        assertThat(selectionSummarySheet.binding.info.visibility, equalTo(View.GONE))\n\n        assertThat(selectionSummarySheet.binding.action.text, equalTo(\"Come on in\"))\n        val iconDrawable = selectionSummarySheet.binding.action.icon\n        assertThat(\n            getCreatedFromResId(iconDrawable!!),\n            equalTo(android.R.drawable.ic_btn_speak_now)\n        )\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/support/AccuracyStatusViewMatcher.kt",
    "content": "package org.odk.collect.geo.support\n\nimport android.view.View\nimport org.hamcrest.Description\nimport org.hamcrest.TypeSafeMatcher\nimport org.odk.collect.geo.geopoint.AccuracyStatusView\nimport org.odk.collect.geo.geopoint.LocationAccuracy\n\ninternal class AccuracyStatusViewMatcher(private val accuracy: LocationAccuracy) :\n    TypeSafeMatcher<View>() {\n    override fun matchesSafely(view: View): Boolean {\n        return view is AccuracyStatusView && view.accuracy == accuracy\n    }\n\n    override fun describeTo(description: Description) {\n        description.appendText(\"AccuracyStatusView with an accuracy=$accuracy\")\n    }\n\n    companion object {\n        fun hasAccuracy(accuracy: LocationAccuracy): AccuracyStatusViewMatcher {\n            return AccuracyStatusViewMatcher(accuracy)\n        }\n    }\n}"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/support/FakeLocationTracker.kt",
    "content": "package org.odk.collect.geo.support\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport org.odk.collect.location.Location\nimport org.odk.collect.location.tracker.LocationTracker\n\nclass FakeLocationTracker : LocationTracker {\n\n    var currentLocation: Location? = null\n        set(value) {\n            _currentLocation.value = value\n            field = value\n        }\n\n    var retainMockAccuracy: Boolean = false\n        private set\n\n    private val _currentLocation: MutableStateFlow<Location?> = MutableStateFlow(currentLocation)\n\n    var isStarted = false\n        private set\n\n    override fun getLocation(): StateFlow<Location?> {\n        return _currentLocation\n    }\n\n    override fun start(retainMockAccuracy: Boolean, updateInterval: Long?) {\n        this.retainMockAccuracy = retainMockAccuracy\n\n        isStarted = true\n        _currentLocation.value = null\n    }\n\n    override fun stop() {\n        isStarted = false\n        _currentLocation.value = null\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt",
    "content": "package org.odk.collect.geo.support\n\nimport androidx.fragment.app.Fragment\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapFragment.FeatureListener\nimport org.odk.collect.maps.MapFragment.PointListener\nimport org.odk.collect.maps.MapFragment.ReadyListener\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.circles.CircleDescription\nimport org.odk.collect.maps.markers.MarkerDescription\nimport org.odk.collect.maps.markers.MarkerIconDescription\nimport org.odk.collect.maps.traces.LineDescription\nimport org.odk.collect.maps.traces.PolygonDescription\nimport kotlin.random.Random\n\nclass FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragment {\n    private var clickListener: PointListener? = null\n    private var gpsLocationListener: PointListener? = null\n    private var locationProvider: String? = null\n    private var retainMockAccuracy = false\n    private var center: MapPoint? = null\n    private var zoom = 0.0\n    private var zoomBoundingBox: Pair<Iterable<MapPoint>, Double>? = null\n    private var readyListener: ReadyListener? = null\n    private var gpsLocation: MapPoint? = null\n    private var featureClickListener: FeatureListener? = null\n    private var dragListener: FeatureListener? = null\n    private val markers = mutableMapOf<Int, MapPoint>()\n    private val markerIcons = mutableMapOf<Int, MarkerIconDescription?>()\n    private val polyLines = mutableMapOf<Int, LineDescription>()\n    private val polygons = mutableMapOf<Int, PolygonDescription>()\n\n    private val circles = mutableMapOf<Int, CircleDescription>()\n    private var hasCenter = false\n    private val featureIds = mutableListOf<Int>()\n    private var zoomLevelSetByUser: Float? = null\n\n    override fun init(\n        readyListener: ReadyListener?,\n        errorListener: MapFragment.ErrorListener?\n    ) {\n        this.readyListener = readyListener\n\n        if (ready) {\n            ready()\n        }\n    }\n\n    fun ready() {\n        readyListener?.onReady(this)\n    }\n\n    override fun getCenter(): MapPoint {\n        return center ?: MapFragment.INITIAL_CENTER\n    }\n\n    override fun getZoom(): Double {\n        return zoom\n    }\n\n    override fun setCenter(center: MapPoint?, animate: Boolean) {\n        if (center == null) {\n            return\n        }\n\n        this.center = center\n        hasCenter = true\n    }\n\n    override fun zoomToCurrentLocation(center: MapPoint?) {\n        this.center = center\n        this.hasCenter = true\n        this.zoom = (zoomLevelSetByUser ?: MapFragment.POINT_ZOOM).toDouble()\n    }\n\n    override fun zoomToPoint(center: MapPoint?, animate: Boolean) {\n        zoomBoundingBox = null\n        this.center = center\n        this.zoom = MapFragment.POINT_ZOOM.toDouble()\n        hasCenter = true\n    }\n\n    override fun zoomToPoint(center: MapPoint?, zoom: Double, animate: Boolean) {\n        zoomBoundingBox = null\n        this.center = center\n        this.zoom = zoom\n        hasCenter = true\n    }\n\n    override fun zoomToBoundingBox(\n        points: Iterable<MapPoint>,\n        scaleFactor: Double,\n        animate: Boolean\n    ) {\n        points.let {\n            center = null\n            zoom = 0.0\n            zoomBoundingBox = Pair(\n                it.toList(), // Clone list to prevent original changing captured values\n                scaleFactor\n            )\n            hasCenter = true\n        }\n    }\n\n    override fun addMarker(markerDescription: MarkerDescription): Int {\n        val featureId = generateFeatureId()\n\n        markers[featureId] = markerDescription.point\n        markerIcons[featureId] = markerDescription.iconDescription\n\n        featureIds.add(featureId)\n        return featureId\n    }\n\n    override fun updateMarker(\n        featureId: Int,\n        markerDescription: MarkerDescription\n    ) {\n        markers[featureId] = markerDescription.point\n        markerIcons[featureId] = markerDescription.iconDescription\n    }\n\n    override fun addMarkers(markers: List<MarkerDescription>): List<Int> {\n        return markers.map {\n            addMarker(it)\n        }\n    }\n\n    override fun setMarkerIcon(featureId: Int, markerIconDescription: MarkerIconDescription) {\n        markerIcons[featureId] = markerIconDescription\n    }\n\n    override fun getMarkerPoint(featureId: Int): MapPoint? {\n        return markers[featureId]\n    }\n\n    override fun addPolyLine(lineDescription: LineDescription): Int {\n        val featureId = generateFeatureId()\n\n        polyLines[featureId] = lineDescription\n        featureIds.add(featureId)\n        return featureId\n    }\n\n    override fun updatePolyLine(\n        featureId: Int,\n        lineDescription: LineDescription\n    ) {\n        polyLines[featureId] = lineDescription\n    }\n\n    override fun addPolygon(polygonDescription: PolygonDescription): Int {\n        val featureId = generateFeatureId()\n        polygons[featureId] = polygonDescription\n        featureIds.add(featureId)\n        return featureId\n    }\n\n    override fun updatePolygon(\n        featureId: Int,\n        polygonDescription: PolygonDescription\n    ) {\n        polygons[featureId] = polygonDescription\n    }\n\n    override fun addCircle(circleDescription: CircleDescription): Int {\n        val featureId = generateFeatureId()\n        circles[featureId] = circleDescription\n        return featureId\n    }\n\n    override fun updateCircle(\n        featureId: Int,\n        circleDescription: CircleDescription\n    ) {\n        circles[featureId] = circleDescription\n    }\n\n    fun getCircles(): List<CircleDescription> {\n        return circles.values.toList()\n    }\n\n    override fun getPolyPoints(featureId: Int): List<MapPoint> {\n        return polyLines[featureId]?.points ?: polygons[featureId]?.points ?: emptyList()\n    }\n\n    override fun clearFeatures() {\n        markers.clear()\n        markerIcons.clear()\n        polyLines.clear()\n        polygons.clear()\n    }\n\n    override fun setClickListener(listener: PointListener?) {\n        this.clickListener = listener\n    }\n\n    fun click(point: MapPoint) {\n        clickListener?.onPoint(point)\n    }\n\n    override fun setLongPressListener(listener: PointListener?) {}\n    override fun setFeatureClickListener(listener: FeatureListener?) {\n        featureClickListener = listener\n    }\n\n    override fun setDragEndListener(listener: FeatureListener?) {\n        dragListener = listener\n    }\n\n    override fun setGpsLocationEnabled(enabled: Boolean) {}\n    override fun getGpsLocation(): MapPoint? {\n        return gpsLocation\n    }\n\n    override fun setGpsLocationListener(listener: PointListener?) {\n        gpsLocationListener = listener\n\n        gpsLocation?.let {\n            listener?.onPoint(it)\n        }\n    }\n\n    override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) {\n        this.retainMockAccuracy = retainMockAccuracy\n    }\n\n    override fun hasCenter(): Boolean {\n        return hasCenter\n    }\n\n    fun setLocation(mapPoint: MapPoint?) {\n        gpsLocation = mapPoint\n        if (gpsLocationListener != null) {\n            gpsLocationListener!!.onPoint(mapPoint!!)\n        }\n    }\n\n    fun setLocationProvider(locationProvider: String?) {\n        this.locationProvider = locationProvider\n    }\n\n    fun isRetainMockAccuracy(): Boolean {\n        return retainMockAccuracy\n    }\n\n    fun clickOnFeature(index: Int) {\n        featureClickListener!!.onFeature(featureIds[index])\n    }\n\n    fun clickOnFeatureId(featureId: Int) {\n        featureClickListener!!.onFeature(featureId)\n    }\n\n    fun getMarkers(): List<MapPoint> {\n        return markers.values.toList()\n    }\n\n    fun getMarkerIcons(): List<MarkerIconDescription?> {\n        return markerIcons.values.toList()\n    }\n\n    fun getZoomBoundingBox(): Pair<Iterable<MapPoint>, Double>? {\n        return zoomBoundingBox\n    }\n\n    fun getPolyLines(): List<LineDescription> {\n        return polyLines.values.toList()\n    }\n\n    fun isPolyDraggable(index: Int): Boolean {\n        return polyLines[featureIds[index]]!!.draggable\n    }\n\n    fun getFeatureId(points: List<MapPoint>): Int {\n        return if (points.size == 1) {\n            markers.entries.find {\n                it.value == points[0]\n            }!!.key\n        } else {\n            polyLines.entries.find {\n                it.value.points == points\n            }!!.key\n        }\n    }\n\n    private fun generateFeatureId(): Int {\n        var featureId = Random.nextInt()\n        while (featureIds.contains(featureId)) {\n            featureId = Random.nextInt()\n        }\n\n        return featureId\n    }\n\n    fun getPolygons(): List<PolygonDescription> {\n        return polygons.values.toList()\n    }\n\n    fun setZoomLevel(zoomLevel: Float?) {\n        zoomLevelSetByUser = zoomLevel\n    }\n\n    fun dragPolyLine(featureId: Int, new: List<MapPoint>) {\n        polyLines[featureId] = polyLines[featureId]!!.copy(points = new)\n        dragListener?.onFeature(featureId)\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/support/Fixtures.kt",
    "content": "package org.odk.collect.geo.support\n\nimport android.R\nimport org.odk.collect.geo.selection.IconifiedText\nimport org.odk.collect.geo.selection.MappableSelectItem\nimport org.odk.collect.maps.MapPoint\n\nobject Fixtures {\n    fun actionMappableSelectPoint(): MappableSelectItem.MappableSelectPoint {\n        return MappableSelectItem.MappableSelectPoint(\n            0,\n            \"0\",\n            listOf(IconifiedText(R.drawable.ic_lock_idle_charging, \"An item\")),\n            point = MapPoint(0.0, 0.0),\n            smallIcon = R.drawable.ic_lock_power_off,\n            largeIcon = R.drawable.ic_lock_idle_charging,\n            action = IconifiedText(R.drawable.ic_delete, \"Action\")\n        )\n    }\n\n    fun infoMappableSelectPoint(): MappableSelectItem.MappableSelectPoint {\n        return MappableSelectItem.MappableSelectPoint(\n            0,\n            \"0\",\n            listOf(IconifiedText(R.drawable.ic_lock_idle_charging, \"An item\")),\n            point = MapPoint(0.0, 0.0),\n            smallIcon = R.drawable.ic_lock_power_off,\n            largeIcon = R.drawable.ic_lock_idle_charging,\n            info = \"Info\"\n        )\n    }\n\n    fun actionMappableSelectLine(): MappableSelectItem.MappableSelectLine {\n        return MappableSelectItem.MappableSelectLine(\n            0,\n            \"0\",\n            listOf(IconifiedText(R.drawable.ic_lock_idle_charging, \"An item\")),\n            points = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 1.0)),\n            action = IconifiedText(R.drawable.ic_delete, \"Action\")\n        )\n    }\n\n    fun actionMappableSelectPolygon(): MappableSelectItem.MappableSelectPolygon {\n        return MappableSelectItem.MappableSelectPolygon(\n            0,\n            \"0\",\n            listOf(IconifiedText(R.drawable.ic_lock_idle_charging, \"An item\")),\n            points = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 1.0)),\n            action = IconifiedText(R.drawable.ic_delete, \"Action\")\n        )\n    }\n}\n"
  },
  {
    "path": "geo/src/test/java/org/odk/collect/geo/support/RobolectricApplication.kt",
    "content": "package org.odk.collect.geo.support\n\nimport android.app.Application\nimport org.odk.collect.androidshared.ui.Animations\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\nimport org.odk.collect.geo.GeoDependencyComponent\nimport org.odk.collect.geo.GeoDependencyComponentProvider\n\nclass RobolectricApplication : Application(), GeoDependencyComponentProvider {\n\n    override lateinit var geoDependencyComponent: GeoDependencyComponent\n\n    override fun onCreate() {\n        super.onCreate()\n        Animations.DISABLE_ANIMATIONS = true\n\n        // We don't want any clicks to be blocked\n        MultiClickGuard.test = true\n    }\n}\n"
  },
  {
    "path": "geo/src/test/resources/robolectric.properties",
    "content": "application=org.odk.collect.geo.support.RobolectricApplication\nsdk=33"
  },
  {
    "path": "google-maps/.gitignore",
    "content": "/build"
  },
  {
    "path": "google-maps/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.googlemaps\"\n\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":shared\"))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":maps\"))\n    implementation(project(\":settings\"))\n    implementation(project(\":location\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":icons\"))\n\n    implementation(libs.androidxPreferenceKtx)\n    implementation(libs.playServicesMaps)\n    implementation(libs.playServicesLocation)\n    implementation(libs.timber)\n    implementation(libs.androidMaterial)\n\n    implementation(libs.dagger)\n    ksp(libs.daggerCompiler)\n}\n"
  },
  {
    "path": "google-maps/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt",
    "content": "package org.odk.collect.googlemaps\n\nimport android.content.Context\nimport android.util.LruCache\nimport com.google.android.gms.maps.model.BitmapDescriptor\nimport com.google.android.gms.maps.model.BitmapDescriptorFactory\nimport org.odk.collect.maps.markers.MarkerIconCreator\nimport org.odk.collect.maps.markers.MarkerIconCreator.toBitmap\nimport org.odk.collect.maps.markers.MarkerIconDescription\n\nobject BitmapDescriptorCache {\n    /**\n     * We use different markers in different features and in normal cases we should not need to use\n     * more than 10 different types of markers at the same time.\n     * The case when markers have symbols is an exception and then it doesn't make sense to cache\n     * bitmaps because every marker will most likely be unique.\n     */\n    private val cache = LruCache<Int, BitmapDescriptor>(10)\n\n    @JvmStatic\n    fun getBitmapDescriptor(context: Context, markerIconDescription: MarkerIconDescription): BitmapDescriptor {\n        val drawableId = markerIconDescription.hashCode()\n\n        if (cache[drawableId] == null) {\n            BitmapDescriptorFactory.fromBitmap(markerIconDescription.toBitmap(context)).also {\n                cache.put(drawableId, it)\n            }\n        }\n        return cache[drawableId]\n    }\n\n    @JvmStatic\n    fun clearCache() {\n        MarkerIconCreator.clearCache()\n        cache.evictAll()\n    }\n}\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/DaggerSetup.kt",
    "content": "package org.odk.collect.googlemaps\n\nimport dagger.Component\nimport dagger.Module\nimport dagger.Provides\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.settings.SettingsProvider\nimport javax.inject.Singleton\n\ninterface GoogleMapsDependencyComponentProvider {\n    val googleMapsDependencyComponent: GoogleMapsDependencyComponent\n}\n\n@Component(modules = [GoogleMapsDependencyModule::class])\n@Singleton\ninterface GoogleMapsDependencyComponent {\n    fun inject(osmDroidMapFragment: GoogleMapFragment)\n}\n\n@Module\nopen class GoogleMapsDependencyModule {\n\n    @Provides\n    open fun providesReferenceLayerRepository(): ReferenceLayerRepository {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesLocationClient(): LocationClient {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesSettingsProvider(): SettingsProvider {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n}\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapConfigurator.java",
    "content": "package org.odk.collect.googlemaps;\n\nimport static org.odk.collect.androidshared.ui.PrefUtils.createListPref;\nimport static org.odk.collect.androidshared.ui.PrefUtils.getInt;\nimport static kotlin.collections.SetsKt.setOf;\n\nimport android.content.Context;\nimport android.content.pm.ApplicationInfo;\nimport android.content.pm.PackageManager;\nimport android.os.Bundle;\n\nimport androidx.preference.Preference;\n\nimport com.google.android.gms.maps.GoogleMap;\n\nimport org.odk.collect.androidshared.system.OpenGLVersionChecker;\nimport org.odk.collect.androidshared.system.PlayServicesChecker;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.maps.MapConfigurator;\nimport org.odk.collect.maps.layers.MbtilesFile;\nimport org.odk.collect.maps.layers.MbtilesFile.LayerType;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.io.File;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\n\npublic class GoogleMapConfigurator implements MapConfigurator {\n    private final String prefKey;\n    private final int sourceLabelId;\n    private final GoogleMapTypeOption[] options;\n\n    /** Constructs a configurator with a few Google map type options to choose from. */\n    public GoogleMapConfigurator(String prefKey, int sourceLabelId, GoogleMapTypeOption... options) {\n        this.prefKey = prefKey;\n        this.sourceLabelId = sourceLabelId;\n        this.options = options;\n    }\n\n    @Override public boolean isAvailable(Context context) {\n        try {\n            ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);\n            String apiKey = applicationInfo.metaData.getString(\"com.google.android.geo.API_KEY\");\n\n            return isGoogleMapsSdkAvailable(context) && isGooglePlayServicesAvailable(context) && !apiKey.equals(\"\");\n        } catch (PackageManager.NameNotFoundException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static boolean isGooglePlayServicesAvailable(Context context) {\n        return new PlayServicesChecker().isGooglePlayServicesAvailable(context);\n    }\n\n    private static boolean isGoogleMapsSdkAvailable(Context context) {\n        /*\n         * The Google Maps SDK for Android requires OpenGL ES version 2.\n         * See: https://developers.google.com/maps/documentation/android-sdk/config\n         */\n        return OpenGLVersionChecker.isOpenGLv2Supported(context);\n    }\n\n    @Override public void showUnavailableMessage(Context context) {\n        if (!isGoogleMapsSdkAvailable(context)) {\n            ToastUtils.showLongToast(context.getString(\n                org.odk.collect.strings.R.string.basemap_source_unavailable, context.getString(sourceLabelId)));\n        }\n\n        PlayServicesChecker playServicesChecker = new PlayServicesChecker();\n        if (!playServicesChecker.isGooglePlayServicesAvailable(context)) {\n            playServicesChecker.showGooglePlayServicesAvailabilityErrorDialog(context);\n        }\n    }\n\n    @Override public List<Preference> createPrefs(Context context, Settings settings) {\n        int[] labelIds = new int[options.length];\n        String[] values = new String[options.length];\n        for (int i = 0; i < options.length; i++) {\n            labelIds[i] = options[i].labelId;\n            values[i] = Integer.toString(options[i].mapType);\n        }\n        String prefTitle = context.getString(\n            org.odk.collect.strings.R.string.map_style_label, context.getString(sourceLabelId));\n        return Collections.singletonList(createListPref(\n            context, prefKey, prefTitle, labelIds, values, settings\n        ));\n    }\n\n    @Override public Set<String> getPrefKeys() {\n        return prefKey.isEmpty() ? setOf(ProjectKeys.KEY_REFERENCE_LAYER) :\n                setOf(prefKey, ProjectKeys.KEY_REFERENCE_LAYER);\n    }\n\n    @Override public Bundle buildConfig(Settings prefs) {\n        Bundle config = new Bundle();\n        config.putInt(GoogleMapFragment.KEY_MAP_TYPE,\n            getInt(ProjectKeys.KEY_GOOGLE_MAP_STYLE, GoogleMap.MAP_TYPE_NORMAL, prefs));\n        config.putString(GoogleMapFragment.KEY_REFERENCE_LAYER,\n            prefs.getString(ProjectKeys.KEY_REFERENCE_LAYER));\n        return config;\n    }\n\n    @Override public boolean supportsLayer(File file) {\n        // GoogleMapFragment supports only raster tiles.\n        return MbtilesFile.readLayerType(file) == LayerType.RASTER;\n    }\n\n    @Override public String getDisplayName(File file) {\n        String name = MbtilesFile.readName(file);\n        return name != null ? name : file.getName();\n    }\n\n    public static class GoogleMapTypeOption {\n        final int mapType;\n        final int labelId;\n\n        public GoogleMapTypeOption(int mapType, int labelId) {\n            this.mapType = mapType;\n            this.labelId = labelId;\n        }\n    }\n}\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java",
    "content": "/*\n * Copyright (C) 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.googlemaps;\n\nimport static org.odk.collect.googlemaps.MapPointExt.toLatLng;\nimport static org.odk.collect.maps.traces.TraceDescriptionKt.getMarkersForPoints;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.location.Location;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport com.google.android.gms.location.LocationListener;\nimport com.google.android.gms.maps.CameraUpdate;\nimport com.google.android.gms.maps.CameraUpdateFactory;\nimport com.google.android.gms.maps.GoogleMap;\nimport com.google.android.gms.maps.SupportMapFragment;\nimport com.google.android.gms.maps.model.BitmapDescriptor;\nimport com.google.android.gms.maps.model.CameraPosition;\nimport com.google.android.gms.maps.model.Circle;\nimport com.google.android.gms.maps.model.CircleOptions;\nimport com.google.android.gms.maps.model.LatLng;\nimport com.google.android.gms.maps.model.LatLngBounds;\nimport com.google.android.gms.maps.model.MapStyleOptions;\nimport com.google.android.gms.maps.model.Marker;\nimport com.google.android.gms.maps.model.MarkerOptions;\nimport com.google.android.gms.maps.model.Polygon;\nimport com.google.android.gms.maps.model.PolygonOptions;\nimport com.google.android.gms.maps.model.Polyline;\nimport com.google.android.gms.maps.model.PolylineOptions;\nimport com.google.android.gms.maps.model.TileOverlay;\nimport com.google.android.gms.maps.model.TileOverlayOptions;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.androidshared.system.ContextExt;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.googlemaps.GoogleMapConfigurator.GoogleMapTypeOption;\nimport org.odk.collect.googlemaps.circles.CircleFeature;\nimport org.odk.collect.googlemaps.scaleview.MapScaleView;\nimport org.odk.collect.location.LocationClient;\nimport org.odk.collect.maps.MapConfigurator;\nimport org.odk.collect.maps.MapFragment;\nimport org.odk.collect.maps.MapPoint;\nimport org.odk.collect.maps.MapViewModel;\nimport org.odk.collect.maps.MapViewModelMapFragment;\nimport org.odk.collect.maps.Zoom;\nimport org.odk.collect.maps.ZoomObserver;\nimport org.odk.collect.maps.circles.CircleDescription;\nimport org.odk.collect.maps.layers.MapFragmentReferenceLayerUtils;\nimport org.odk.collect.maps.layers.ReferenceLayerRepository;\nimport org.odk.collect.maps.markers.MarkerDescription;\nimport org.odk.collect.maps.markers.MarkerIconDescription;\nimport org.odk.collect.maps.traces.LineDescription;\nimport org.odk.collect.maps.traces.PolygonDescription;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.odk.collect.settings.keys.ProjectKeys;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport java.util.stream.StreamSupport;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\npublic class GoogleMapFragment extends MapViewModelMapFragment implements\n        LocationListener, LocationClient.LocationClientListener,\n        GoogleMap.OnMapClickListener, GoogleMap.OnMapLongClickListener,\n        GoogleMap.OnMarkerClickListener, GoogleMap.OnMarkerDragListener,\n        GoogleMap.OnPolylineClickListener, GoogleMap.OnPolygonClickListener {\n\n    // Bundle keys understood by applyConfig().\n    static final String KEY_MAP_TYPE = \"MAP_TYPE\";\n\n    @Inject\n    ReferenceLayerRepository referenceLayerRepository;\n\n    @Inject\n    LocationClient locationClient;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    private GoogleMap map;\n    private MapScaleView scaleView;\n    private ReadyListener readyListener;\n    private ErrorListener errorListener;\n    private Marker locationCrosshairs;\n    private Circle accuracyCircle;\n    private final List<ReadyListener> gpsLocationReadyListeners = new ArrayList<>();\n    private PointListener clickListener;\n    private PointListener longPressListener;\n    private PointListener gpsLocationListener;\n    private FeatureListener featureClickListener;\n    private FeatureListener dragEndListener;\n\n    private boolean clientWantsLocationUpdates;\n    private MapPoint lastLocationFix;\n\n    private int nextFeatureId = 1;\n    private final Map<Integer, MapFeature> features = new HashMap<>();\n    private int mapType;\n    private File referenceLayerFile;\n    private TileOverlay referenceOverlay;\n\n    @Override\n    public void init(@Nullable ReadyListener readyListener, @Nullable ErrorListener errorListener) {\n        this.readyListener = readyListener;\n        this.errorListener = errorListener;\n    }\n\n    @Nullable\n    @Override\n    @SuppressLint(\"MissingPermission\") // Permission checks for location services handled in widgets\n    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        View view = inflater.inflate(R.layout.map_layout, container, false);\n\n        scaleView = view.findViewById(R.id.scale_view);\n\n        SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.map);\n        mapFragment.getMapAsync((GoogleMap googleMap) -> {\n            if (googleMap == null) {\n                ToastUtils.showShortToast(org.odk.collect.strings.R.string.google_play_services_error_occured);\n                if (errorListener != null) {\n                    errorListener.onError();\n                }\n                return;\n            }\n            this.map = googleMap;\n            googleMap.setMapType(mapType);\n            googleMap.setOnMapClickListener(this);\n            googleMap.setOnMapLongClickListener(this);\n            googleMap.setOnMarkerClickListener(this);\n            googleMap.setOnPolylineClickListener(this);\n            googleMap.setOnPolygonClickListener(this);\n            googleMap.setOnMarkerDragListener(this);\n            googleMap.getUiSettings().setCompassEnabled(true);\n            // Don't show the blue dot on the map; we'll draw crosshairs instead.\n            googleMap.setMyLocationEnabled(false);\n            googleMap.setMinZoomPreference(1);\n            googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(\n                    toLatLng(MapFragment.Companion.getINITIAL_CENTER()), INITIAL_ZOOM));\n            googleMap.setOnCameraMoveListener(() -> scaleView.update(googleMap.getCameraPosition().zoom, googleMap.getCameraPosition().target.latitude));\n            CameraListener cameraListener = new CameraListener(googleMap) {\n                @Override\n                void onCameraMoveFinished(boolean isUser, boolean isZoom) {\n                    CameraPosition cameraPosition = googleMap.getCameraPosition();\n                    LatLng target = cameraPosition.target;\n                    scaleView.update(cameraPosition.zoom, target.latitude);\n                    if (isUser) {\n                        float newZoomLevel = cameraPosition.zoom;\n                        MapPoint center = new MapPoint(target.latitude, target.longitude);\n                        if (isZoom) {\n                            getMapViewModel().onUserZoom(center, newZoomLevel);\n                        } else {\n                            getMapViewModel().onUserMove(center, newZoomLevel);\n                        }\n                    }\n                }\n            };\n            googleMap.setOnCameraMoveStartedListener(cameraListener);\n            googleMap.setOnCameraIdleListener(cameraListener);\n\n\n            loadReferenceOverlay();\n\n            MapConfigurator configurator = createConfigurator();\n            getMapViewModel().getSettings(configurator.getPrefKeys()).observe(getViewLifecycleOwner(), settings -> {\n                Bundle newConfig = configurator.buildConfig(settings);\n                onConfigChanged(newConfig);\n            });\n\n            getMapViewModel().getZoom().observe(getViewLifecycleOwner(), new ZoomObserver() {\n                @Override\n                public void onZoomToPoint(@NonNull Zoom.Point zoom) {\n                    MapPoint point = zoom.getPoint();\n                    moveOrAnimateCamera(\n                            CameraUpdateFactory.newLatLngZoom(toLatLng(point), (float) zoom.getLevel()), zoom.getAnimate());\n                }\n\n                @Override\n                public void onZoomToBox(@NonNull Zoom.Box zoom) {\n                    List<MapPoint> points = zoom.getBox();\n                    int count = 0;\n                    LatLngBounds.Builder builder = new LatLngBounds.Builder();\n                    MapPoint lastPoint = null;\n                    for (MapPoint point : points) {\n                        lastPoint = point;\n                        builder.include(toLatLng(point));\n                        count++;\n                    }\n                    if (count == 1) {\n                        zoomToPoint(lastPoint, zoom.getAnimate());\n                    } else if (count > 1) {\n                        final LatLngBounds bounds = expandBounds(builder.build(), 1 / zoom.getScaleFactor());\n                        new Handler().postDelayed(() -> {\n                            try {\n                                moveOrAnimateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0), zoom.getAnimate());\n                            } catch (IllegalArgumentException\n                                     //https://github.com/getodk/collect/issues/5379\n                                     |\n                                     IllegalStateException e) { // https://github.com/getodk/collect/issues/5634\n                                LatLng boxCenter = bounds.getCenter();\n                                zoomToPoint(new MapPoint(boxCenter.latitude, boxCenter.longitude), map.getMinZoomLevel(), false);\n                            }\n                        }, 100);\n                    }\n                }\n            });\n\n            // If the screen is rotated before the map is ready, this fragment\n            // could already be detached, which makes it unsafe to use.  Only\n            // call the ReadyListener if this fragment is still attached.\n            if (readyListener != null && getActivity() != null) {\n                readyListener.onReady(this);\n            }\n        });\n\n        return view;\n    }\n\n    @Override public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        GoogleMapsDependencyComponent component = ((GoogleMapsDependencyComponentProvider) context.getApplicationContext()).getGoogleMapsDependencyComponent();\n        component.inject(this);\n    }\n\n    @Override public void onResume() {\n        super.onResume();\n        enableLocationUpdates(clientWantsLocationUpdates);\n    }\n\n    @Override public void onPause() {\n        super.onPause();\n        enableLocationUpdates(false);\n    }\n\n    @Override public void onDestroy() {\n        BitmapDescriptorCache.clearCache();\n        super.onDestroy();\n    }\n\n    @Override public @NonNull MapPoint getCenter() {\n        if (map == null) {  // during Robolectric tests, map will be null\n            return MapFragment.Companion.getINITIAL_CENTER();\n        }\n        LatLng target = map.getCameraPosition().target;\n        return new MapPoint(target.latitude, target.longitude);\n    }\n\n    @Override public double getZoom() {\n        if (map == null) {  // during Robolectric tests, map will be null\n            return INITIAL_ZOOM;\n        }\n        return map.getCameraPosition().zoom;\n    }\n\n    @Override public int addMarker(MarkerDescription markerDescription) {\n        int featureId = nextFeatureId++;\n        return addMarker(featureId, markerDescription);\n    }\n\n    private int addMarker(int featureId, MarkerDescription markerDescription) {\n        features.put(featureId, new MarkerFeature(getActivity(), markerDescription, map));\n        return featureId;\n    }\n\n    @Override\n    public List<Integer> addMarkers(List<MarkerDescription> markers) {\n        List<Integer> featureIds = new ArrayList<>();\n        for (MarkerDescription markerDescription : markers) {\n            int featureId = addMarker(markerDescription);\n            featureIds.add(featureId);\n        }\n\n        return featureIds;\n    }\n\n    @Override public void setMarkerIcon(int featureId, MarkerIconDescription markerIconDescription) {\n        MapFeature feature = features.get(featureId);\n        if (feature instanceof MarkerFeature) {\n            ((MarkerFeature) feature).setIcon(markerIconDescription);\n        }\n    }\n\n    @Override public @Nullable MapPoint getMarkerPoint(int featureId) {\n        MapFeature feature = features.get(featureId);\n        return feature instanceof MarkerFeature ? ((MarkerFeature) feature).getPoint() : null;\n    }\n\n    @Override public int addPolyLine(LineDescription lineDescription) {\n        int featureId = nextFeatureId++;\n        addPolyLine(featureId, lineDescription);\n        return featureId;\n    }\n\n    private void addPolyLine(int featureId, LineDescription lineDescription) {\n        if (lineDescription.getDraggable()) {\n            features.put(featureId, new DynamicPolyLineFeature(getActivity(), lineDescription, map));\n        } else {\n            features.put(featureId, new StaticPolyLineFeature(lineDescription, map));\n        }\n    }\n\n    @Override\n    public void updatePolyLine(int featureId, @NotNull LineDescription lineDescription) {\n        features.get(featureId).dispose();\n        addPolyLine(featureId, lineDescription);\n    }\n\n    @Override\n    public int addPolygon(PolygonDescription polygonDescription) {\n        int featureId = nextFeatureId++;\n        addPolygon(featureId, polygonDescription);\n        return featureId;\n    }\n\n    private void addPolygon(int featureId, PolygonDescription polygonDescription) {\n        if (polygonDescription.getDraggable()) {\n            features.put(featureId, new DynamicPolygonFeature(map, polygonDescription));\n        } else {\n            features.put(featureId, new StaticPolygonFeature(map, polygonDescription));\n        }\n    }\n\n    @Override\n    public void updatePolygon(int featureId, @NotNull PolygonDescription polygonDescription) {\n        features.get(featureId).dispose();\n        addPolygon(featureId, polygonDescription);\n    }\n\n    @Override public @NonNull List<MapPoint> getPolyPoints(int featureId) {\n        MapFeature feature = features.get(featureId);\n        if (feature instanceof LineFeature) {\n            return ((LineFeature) feature).getPoints();\n        }\n\n        return new ArrayList<>();\n    }\n\n    @Override public void clearFeatures() {\n        if (map != null) {  // during Robolectric tests, map will be null\n            for (MapFeature feature : features.values()) {\n                feature.dispose();\n            }\n        }\n        features.clear();\n        nextFeatureId = 1;\n    }\n\n    @Override public void setClickListener(@Nullable PointListener listener) {\n        clickListener = listener;\n    }\n\n    @Override public void setLongPressListener(@Nullable PointListener listener) {\n        longPressListener = listener;\n    }\n\n    @Override public void setFeatureClickListener(@Nullable FeatureListener listener) {\n        featureClickListener = listener;\n    }\n\n    @Override public void setDragEndListener(@Nullable FeatureListener listener) {\n        dragEndListener = listener;\n    }\n\n    @Override public void setGpsLocationListener(@Nullable PointListener listener) {\n        gpsLocationListener = listener;\n    }\n\n    @Override\n    public void setRetainMockAccuracy(boolean retainMockAccuracy) {\n        locationClient.setRetainMockAccuracy(retainMockAccuracy);\n    }\n\n    @Override public void setGpsLocationEnabled(boolean enable) {\n        if (enable != clientWantsLocationUpdates) {\n            clientWantsLocationUpdates = enable;\n            enableLocationUpdates(clientWantsLocationUpdates);\n        }\n    }\n\n    @Override public void onLocationChanged(Location location) {\n        Timber.i(\"onLocationChanged: location = %s\", location);\n        lastLocationFix = fromLocation(location);\n        for (ReadyListener listener : gpsLocationReadyListeners) {\n            listener.onReady(this);\n        }\n        gpsLocationReadyListeners.clear();\n        if (gpsLocationListener != null) {\n            gpsLocationListener.onPoint(lastLocationFix);\n        }\n\n        if (getActivity() != null) {\n            updateLocationIndicator(toLatLng(lastLocationFix), location.getAccuracy());\n        }\n    }\n\n    @Override public @Nullable MapPoint getGpsLocation() {\n        return lastLocationFix;\n    }\n\n    @Override public void onMapClick(LatLng latLng) {\n        if (clickListener != null) {\n            clickListener.onPoint(fromLatLng(latLng));\n        }\n    }\n\n    @Override public void onMapLongClick(LatLng latLng) {\n        if (longPressListener != null) {\n            longPressListener.onPoint(fromLatLng(latLng));\n        }\n    }\n\n    @Override public boolean onMarkerClick(Marker marker) {\n        // Avoid calling listeners if location crosshair is clicked on.\n        if (marker == locationCrosshairs) {\n            return true;\n        }\n\n        if (featureClickListener != null) { // FormMapActivity\n            featureClickListener.onFeature(findFeature(marker));\n        } else { // GeoWidget\n            onMapClick(marker.getPosition());\n        }\n        return true;  // consume the event (no default zoom and popup behaviour)\n    }\n\n    @Override public void onPolylineClick(Polyline polyline) {\n        if (featureClickListener != null) {\n            featureClickListener.onFeature(findFeature(polyline));\n        }\n    }\n\n    @Override public void onPolygonClick(@NonNull Polygon polygon) {\n        if (featureClickListener != null) {\n            featureClickListener.onFeature(findFeature(polygon));\n        }\n    }\n\n    @Override public void onMarkerDragStart(Marker marker) {\n        // When dragging starts, GoogleMap makes the marker jump up to move it\n        // out from under the user's finger; whenever a marker moves, we have\n        // to update its corresponding feature.\n        updateFeature(findFeature(marker));\n    }\n\n    @Override public void onMarkerDrag(Marker marker) {\n        // When a marker is manually dragged, the position is no longer\n        // obtained from a GPS reading, so the altitude and standard deviation\n        // fields are no longer meaningful; reset them to zero.\n        marker.setSnippet(\"0;0\");\n        updateFeature(findFeature(marker));\n    }\n\n    @Override public void onMarkerDragEnd(Marker marker) {\n        int featureId = findFeature(marker);\n        updateFeature(featureId);\n        if (dragEndListener != null && featureId != -1) {\n            dragEndListener.onFeature(featureId);\n        }\n    }\n\n    @Override public void onClientStart() {\n        lastLocationFix = fromLocation(locationClient.getLastLocation());\n        Timber.i(\"Requesting location updates (to %s)\", this);\n        locationClient.requestLocationUpdates(this);\n    }\n\n    @Override public void onClientStartFailure() {\n    }\n\n    @Override public void onClientStop() {\n    }\n\n    private static @NonNull MapPoint fromLatLng(@NonNull LatLng latLng) {\n        return new MapPoint(latLng.latitude, latLng.longitude);\n    }\n\n    private static @Nullable MapPoint fromLocation(@Nullable Location location) {\n        if (location == null) {\n            return null;\n        }\n        return new MapPoint(location.getLatitude(), location.getLongitude(),\n            location.getAltitude(), location.getAccuracy());\n    }\n\n    private static @NonNull MapPoint fromMarker(@NonNull Marker marker) {\n        LatLng position = marker.getPosition();\n        String snippet = marker.getSnippet();\n        String[] parts = (snippet != null ? snippet : \"\").split(\";\");\n        double alt = 0;\n        double sd = 0;\n        try {\n            if (parts.length >= 1) {\n                alt = Double.parseDouble(parts[0]);\n            }\n            if (parts.length >= 2) {\n                sd = Double.parseDouble(parts[1]);\n            }\n        } catch (NumberFormatException e) {\n            Timber.w(\"Marker.getSnippet() did not contain two numbers\");\n        }\n        return new MapPoint(position.latitude, position.longitude, alt, sd);\n    }\n\n\n\n    /** Updates the map to reflect the value of referenceLayerFile. */\n    private void loadReferenceOverlay() {\n        if (referenceOverlay != null) {\n            referenceOverlay.remove();\n            referenceOverlay = null;\n        }\n        if (referenceLayerFile != null) {\n            referenceOverlay = this.map.addTileOverlay(new TileOverlayOptions().tileProvider(\n                new GoogleMapsMapBoxOfflineTileProvider(referenceLayerFile)\n            ));\n            setLabelsVisibility(\"off\");\n        } else {\n            setLabelsVisibility(\"on\");\n        }\n    }\n\n    private void setLabelsVisibility(String state) {\n        String style = String.format(\" [ { featureType: all, elementType: labels, stylers: [ { visibility: %s } ] } ]\", state);\n        map.setMapStyle(new MapStyleOptions(style));\n    }\n\n    private LatLngBounds expandBounds(LatLngBounds bounds, double factor) {\n        double north = bounds.northeast.latitude;\n        double south = bounds.southwest.latitude;\n        double latCenter = (north + south) / 2;\n        double latRadius = ((north - south) / 2) * factor;\n        north = Math.min(90, latCenter + latRadius);\n        south = Math.max(-90, latCenter - latRadius);\n\n        double east = bounds.northeast.longitude;\n        double west = bounds.southwest.longitude;\n        while (east < west) {\n            east += 360;\n        }\n        double lonCenter = (east + west) / 2;\n        double lonRadius = Math.min(180 - 1e-6, ((east - west) / 2) * factor);\n        east = lonCenter + lonRadius;\n        west = lonCenter - lonRadius;\n\n        return new LatLngBounds(new LatLng(south, west), new LatLng(north, east));\n    }\n\n    private void moveOrAnimateCamera(CameraUpdate movement, boolean animate) {\n        if (animate) {\n            map.animateCamera(movement);\n        } else {\n            map.moveCamera(movement);\n        }\n    }\n\n    private void enableLocationUpdates(boolean enable) {\n        if (enable) {\n            Timber.i(\"Starting LocationClient %s (for MapFragment %s)\", locationClient, this);\n            locationClient.start(this);\n        } else {\n            Timber.i(\"Stopping LocationClient %s (for MapFragment %s)\", locationClient, this);\n            locationClient.stop();\n        }\n    }\n\n    private void updateLocationIndicator(LatLng loc, double radius) {\n        if (map == null) {\n            return;\n        }\n        if (locationCrosshairs == null) {\n            locationCrosshairs = map.addMarker(new MarkerOptions()\n                .position(loc)\n                .icon(getBitmapDescriptor(getContext(), new MarkerIconDescription.DrawableResource(org.odk.collect.maps.R.drawable.ic_crosshairs)))\n                .anchor(0.5f, 0.5f)  // center the crosshairs on the position\n            );\n        }\n        if (accuracyCircle == null) {\n            int stroke = ContextExt.getThemeAttributeValue(requireContext(), androidx.appcompat.R.attr.colorPrimary);\n            int fill = getResources().getColor(org.odk.collect.androidshared.R.color.color_primary_low_emphasis);\n            accuracyCircle = map.addCircle(new CircleOptions()\n                .center(loc)\n                .radius(radius)\n                .strokeWidth(1)\n                .strokeColor(stroke)\n                .fillColor(fill)\n            );\n        }\n\n        locationCrosshairs.setPosition(loc);\n        accuracyCircle.setCenter(loc);\n        accuracyCircle.setRadius(radius);\n    }\n\n    /** Finds the feature to which the given marker belongs. */\n    private int findFeature(Marker marker) {\n        for (int featureId : features.keySet()) {\n            if (features.get(featureId).ownsMarker(marker)) {\n                return featureId;\n            }\n        }\n        return -1;  // not found\n    }\n\n    /** Finds the feature to which the given polyline belongs. */\n    private int findFeature(Polyline polyline) {\n        for (int featureId : features.keySet()) {\n            if (features.get(featureId).ownsPolyline(polyline)) {\n                return featureId;\n            }\n        }\n        return -1;  // not found\n    }\n\n    private int findFeature(Polygon polygon) {\n        for (int featureId : features.keySet()) {\n            if (features.get(featureId).ownsPolygon(polygon)) {\n                return featureId;\n            }\n        }\n        return -1;  // not found\n    }\n\n    private void updateFeature(int featureId) {\n        MapFeature feature = features.get(featureId);\n        if (feature != null) {\n            feature.update();\n        }\n    }\n\n    private static Marker createMarker(Context context, MarkerDescription markerDescription, GoogleMap map) {\n        if (map == null || context == null) {  // during Robolectric tests, map will be null\n            return null;\n        }\n        // A Marker's position is a LatLng with just latitude and longitude\n        // fields.  We need to store the point's altitude and standard\n        // deviation values somewhere, so they go in the marker's snippet.\n        return map.addMarker(new MarkerOptions()\n            .position(toLatLng(markerDescription.getPoint()))\n            .snippet(markerDescription.getPoint().altitude + \";\" + markerDescription.getPoint().accuracy)\n            .draggable(markerDescription.isDraggable())\n            .icon(getBitmapDescriptor(context, markerDescription.getIconDescription()))\n            .anchor(getIconAnchorValueX(markerDescription.getIconAnchor()), getIconAnchorValueY(markerDescription.getIconAnchor()))  // center the icon on the position\n        );\n    }\n\n    private static float getIconAnchorValueX(MapFragment.IconAnchor iconAnchor) {\n        switch (iconAnchor) {\n            case BOTTOM:\n            default:\n                return 0.5f;\n        }\n    }\n\n    private static float getIconAnchorValueY(MapFragment.IconAnchor iconAnchor) {\n        switch (iconAnchor) {\n            case BOTTOM:\n                return 1.0f;\n            default:\n                return 0.5f;\n        }\n    }\n\n    private static BitmapDescriptor getBitmapDescriptor(Context context, MarkerIconDescription markerIconDescription) {\n        return BitmapDescriptorCache.getBitmapDescriptor(context, markerIconDescription);\n    }\n\n    private void onConfigChanged(Bundle config) {\n        mapType = config.getInt(KEY_MAP_TYPE, GoogleMap.MAP_TYPE_NORMAL);\n        referenceLayerFile = MapFragmentReferenceLayerUtils.getReferenceLayerFile(config, referenceLayerRepository);\n        if (map != null) {\n            map.setMapType(mapType);\n            loadReferenceOverlay();\n        }\n    }\n\n    private MapConfigurator createConfigurator() {\n        return new GoogleMapConfigurator(\n                ProjectKeys.KEY_GOOGLE_MAP_STYLE, org.odk.collect.strings.R.string.basemap_source_google,\n                new GoogleMapTypeOption(GoogleMap.MAP_TYPE_NORMAL, org.odk.collect.strings.R.string.streets),\n                new GoogleMapTypeOption(GoogleMap.MAP_TYPE_TERRAIN, org.odk.collect.strings.R.string.terrain),\n                new GoogleMapTypeOption(GoogleMap.MAP_TYPE_HYBRID, org.odk.collect.strings.R.string.hybrid),\n                new GoogleMapTypeOption(GoogleMap.MAP_TYPE_SATELLITE, org.odk.collect.strings.R.string.satellite)\n        );\n    }\n\n    @NonNull\n    @Override\n    public MapViewModel getMapViewModel() {\n        return new ViewModelProvider(this, new ViewModelProvider.Factory() {\n            @NonNull\n            @Override\n            public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {\n                return (T) new MapViewModel(settingsProvider.getUnprotectedSettings(), settingsProvider.getMetaSettings());\n            }\n        }).get(MapViewModel.class);\n    }\n\n    @Override\n    public void updateMarker(int featureId, @NotNull MarkerDescription markerDescription) {\n        features.get(featureId).dispose();\n        addMarker(featureId, markerDescription);\n    }\n\n    @Override\n    public int addCircle(@NotNull CircleDescription circleDescription) {\n        int featureId = nextFeatureId++;\n        addCircle(featureId, circleDescription);\n        return featureId;\n    }\n\n    private void addCircle(int featureId, @NotNull CircleDescription circleDescription) {\n        features.put(featureId, new CircleFeature(circleDescription, map));\n    }\n\n    @Override\n    public void updateCircle(int featureId, @NotNull CircleDescription circleDescription) {\n        features.get(featureId).dispose();\n        addCircle(featureId, circleDescription);\n    }\n\n    /**\n     * A MapFeature is a physical feature on a map, such as a point, a road,\n     * a building, a region, etc.  It is presented to the user as one editable\n     * object, though its appearance may be constructed from multiple overlays\n     * (e.g. geometric elements, handles for manipulation, etc.).\n     */\n    public interface MapFeature {\n        /** Returns true if the given marker belongs to this feature. */\n        boolean ownsMarker(Marker marker);\n\n        /** Returns true if the given polyline belongs to this feature. */\n        boolean ownsPolyline(Polyline polyline);\n\n        boolean ownsPolygon(Polygon polygon);\n\n        /** Updates the feature's geometry after any UI handles have moved. */\n        void update();\n\n        /** Removes the feature from the map, leaving it no longer usable. */\n        void dispose();\n    }\n\n    private static class MarkerFeature implements MapFeature {\n        private Marker marker;\n        private final Context context;\n\n        MarkerFeature(Context context, MarkerDescription markerDescription, GoogleMap map) {\n            this.context = context;\n            marker = createMarker(context, markerDescription, map);\n        }\n\n        public void setIcon(MarkerIconDescription markerIconDescription) {\n            marker.setIcon(getBitmapDescriptor(context, markerIconDescription));\n        }\n\n        public MapPoint getPoint() {\n            return fromMarker(marker);\n        }\n\n        public boolean ownsMarker(Marker givenMarker) {\n            return marker.equals(givenMarker);\n        }\n\n        public boolean ownsPolyline(Polyline givenPolyline) {\n            return false;\n        }\n\n        @Override\n        public boolean ownsPolygon(Polygon polygon) {\n            return false;\n        }\n\n        public void update() { }\n\n        public void dispose() {\n            marker.remove();\n            marker = null;\n        }\n    }\n\n    private interface LineFeature extends MapFeature {\n        List<MapPoint> getPoints();\n    }\n\n    /** A polyline or polygon that can not be manipulated by dragging markers at its vertices. */\n    private static class StaticPolyLineFeature implements LineFeature {\n\n        private List<MapPoint> points;\n        private Polyline polyline;\n\n        StaticPolyLineFeature(LineDescription lineDescription, GoogleMap map) {\n            if (map == null) {  // during Robolectric tests, map will be null\n                return;\n            }\n\n            points = lineDescription.getPoints();\n            List<LatLng> latLngs = StreamSupport.stream(points.spliterator(), false).map(mapPoint -> new LatLng(mapPoint.latitude, mapPoint.longitude)).collect(Collectors.toList());\n            if (latLngs.isEmpty()) {\n                clearPolyline();\n            } else if (polyline == null) {\n                polyline = map.addPolyline(new PolylineOptions()\n                        .color(lineDescription.getStrokeColor())\n                        .zIndex(1)\n                        .width(lineDescription.getStrokeWidth())\n                        .addAll(latLngs)\n                        .clickable(true)\n                );\n            } else {\n                polyline.setPoints(latLngs);\n            }\n        }\n\n        @Override\n        public boolean ownsMarker(Marker givenMarker) {\n            return false;\n        }\n\n        @Override\n        public boolean ownsPolyline(Polyline givenPolyline) {\n            return polyline.equals(givenPolyline);\n        }\n\n        @Override\n        public boolean ownsPolygon(Polygon polygon) {\n            return false;\n        }\n\n        @Override\n        public void update() {\n        }\n\n        @Override\n        public void dispose() {\n            clearPolyline();\n        }\n\n        private void clearPolyline() {\n            if (polyline != null) {\n                polyline.setVisible(false);\n                polyline.remove();\n                polyline = null;\n            }\n        }\n\n        @Override\n        public List<MapPoint> getPoints() {\n            return points;\n        }\n    }\n\n    private class DynamicPolyLineFeature implements LineFeature {\n\n        private final GoogleMap map;\n        private final List<Marker> markers = new ArrayList<>();\n        private final LineDescription lineDescription;\n        private Polyline polyline;\n\n        DynamicPolyLineFeature(Context context, LineDescription lineDescription, GoogleMap map) {\n            this.lineDescription = lineDescription;\n            this.map = map;\n\n            if (map == null) {  // during Robolectric tests, map will be null\n                return;\n            }\n\n            List<MarkerDescription> markerDescriptions = getMarkersForPoints(lineDescription);\n            for (MarkerDescription markerDescription : markerDescriptions) {\n                Marker marker = createMarker(requireContext(), markerDescription, map);\n                markers.add(marker);\n            }\n\n            update();\n        }\n\n        @Override\n        public boolean ownsMarker(Marker givenMarker) {\n            return markers.contains(givenMarker);\n        }\n\n        @Override\n        public boolean ownsPolyline(Polyline givenPolyline) {\n            return polyline.equals(givenPolyline);\n        }\n\n        @Override\n        public boolean ownsPolygon(Polygon polygon) {\n            return false;\n        }\n\n        @Override\n        public void update() {\n            List<LatLng> latLngs = new ArrayList<>();\n            for (Marker marker : markers) {\n                latLngs.add(marker.getPosition());\n            }\n            if (markers.isEmpty()) {\n                clearPolyline();\n            } else if (polyline == null) {\n                polyline = map.addPolyline(new PolylineOptions()\n                    .color(lineDescription.getStrokeColor())\n                    .zIndex(1)\n                    .width(lineDescription.getStrokeWidth())\n                    .addAll(latLngs)\n                );\n            } else {\n                polyline.setPoints(latLngs);\n            }\n        }\n\n        @Override\n        public void dispose() {\n            clearPolyline();\n            for (Marker marker : markers) {\n                marker.remove();\n            }\n            markers.clear();\n        }\n\n        public List<MapPoint> getPoints() {\n            List<MapPoint> points = new ArrayList<>();\n            for (Marker marker : markers) {\n                points.add(fromMarker(marker));\n            }\n            return points;\n        }\n\n        private void clearPolyline() {\n            if (polyline != null) {\n                polyline.remove();\n                polyline = null;\n            }\n        }\n    }\n\n    private class DynamicPolygonFeature implements LineFeature {\n\n        private final GoogleMap map;\n        private final List<Marker> markers = new ArrayList<>();\n        private final PolygonDescription polygonDescription;\n        private Polygon polygon;\n\n        DynamicPolygonFeature(GoogleMap map, PolygonDescription polygonDescription) {\n            this.map = map;\n            this.polygonDescription = polygonDescription;\n\n            if (map == null) {  // during Robolectric tests, map will be null\n                return;\n            }\n\n            List<MarkerDescription> markerDescriptions = getMarkersForPoints(polygonDescription);\n            for (MarkerDescription markerDescription : markerDescriptions) {\n                Marker marker = createMarker(requireContext(), markerDescription, map);\n                markers.add(marker);\n            }\n\n            update();\n        }\n\n        @Override\n        public boolean ownsMarker(Marker givenMarker) {\n            return markers.contains(givenMarker);\n        }\n\n        @Override\n        public boolean ownsPolyline(Polyline givenPolyline) {\n            return false;\n        }\n\n        @Override\n        public boolean ownsPolygon(Polygon polygon) {\n            return this.polygon.equals(polygon);\n        }\n\n        @Override\n        public void update() {\n            List<LatLng> latLngs = new ArrayList<>();\n            for (Marker marker : markers) {\n                latLngs.add(marker.getPosition());\n            }\n            if (markers.isEmpty()) {\n                clearPolygon();\n            } else if (polygon == null) {\n                polygon = map.addPolygon(new PolygonOptions()\n                        .strokeColor(polygonDescription.getStrokeColor())\n                        .zIndex(1)\n                        .strokeWidth(polygonDescription.getStrokeWidth())\n                        .fillColor(polygonDescription.getFillColor())\n                        .addAll(latLngs)\n                );\n            } else {\n                polygon.setPoints(latLngs);\n            }\n        }\n\n        @Override\n        public void dispose() {\n            clearPolygon();\n            for (Marker marker : markers) {\n                marker.remove();\n            }\n            markers.clear();\n        }\n\n        public List<MapPoint> getPoints() {\n            List<MapPoint> points = new ArrayList<>();\n            for (Marker marker : markers) {\n                points.add(fromMarker(marker));\n            }\n            return points;\n        }\n\n        private void clearPolygon() {\n            if (polygon != null) {\n                polygon.setVisible(false);\n                polygon.remove();\n                polygon = null;\n            }\n        }\n    }\n\n    private static class StaticPolygonFeature implements LineFeature {\n        @NonNull\n        private final PolygonDescription polygonDescription;\n        private Polygon polygon;\n\n        StaticPolygonFeature(GoogleMap map, PolygonDescription polygonDescription) {\n            this.polygonDescription = polygonDescription;\n            polygon = map.addPolygon(new PolygonOptions()\n                    .addAll(StreamSupport.stream(polygonDescription.getPoints().spliterator(), false).map(mapPoint -> new LatLng(mapPoint.latitude, mapPoint.longitude)).collect(Collectors.toList()))\n                    .strokeColor(polygonDescription.getStrokeColor())\n                    .strokeWidth(polygonDescription.getStrokeWidth())\n                    .fillColor(polygonDescription.getFillColor())\n                    .clickable(true)\n            );\n        }\n\n        @Override\n        public boolean ownsMarker(Marker marker) {\n            return false;\n        }\n\n        @Override\n        public boolean ownsPolyline(Polyline polyline) {\n            return false;\n        }\n\n        @Override\n        public boolean ownsPolygon(Polygon polygon) {\n            return polygon.equals(this.polygon);\n        }\n\n        @Override\n        public void update() {\n        }\n\n        @Override\n        public void dispose() {\n            if (polygon != null) {\n                polygon.remove();\n                polygon = null;\n            }\n        }\n\n        @Override\n        public List<MapPoint> getPoints() {\n            return polygonDescription.getPoints();\n        }\n    }\n\n    private abstract static class CameraListener implements GoogleMap.OnCameraMoveStartedListener, GoogleMap.OnCameraIdleListener {\n\n        private final GoogleMap map;\n        private boolean isGesture;\n        private float startingZoom;\n\n        CameraListener(GoogleMap map) {\n            this.map = map;\n        }\n\n        abstract void onCameraMoveFinished(boolean isUser, boolean isZoom);\n\n        @Override\n        public void onCameraMoveStarted(int reason) {\n            isGesture = reason == GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE;\n            startingZoom = map.getCameraPosition().zoom;\n        }\n\n        @Override\n        public void onCameraIdle() {\n            onCameraMoveFinished(isGesture, map.getCameraPosition().zoom != startingZoom);\n        }\n    }\n}\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapsMapBoxOfflineTileProvider.java",
    "content": "/*\n * Copyright (C) 2016 GeoODK\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.googlemaps;\n\n/**\n * Created by jnordling on 12/29/15.\n *\n * @author jonnordling@gmail.com\n */\n\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport com.google.android.gms.maps.model.LatLng;\nimport com.google.android.gms.maps.model.LatLngBounds;\nimport com.google.android.gms.maps.model.Tile;\nimport com.google.android.gms.maps.model.TileProvider;\n\nimport java.io.Closeable;\nimport java.io.File;\n\npublic class GoogleMapsMapBoxOfflineTileProvider implements TileProvider, Closeable {\n\n    // ------------------------------------------------------------------------\n    // Instance Variables\n    // ------------------------------------------------------------------------\n\n    private int minimumZoom = Integer.MIN_VALUE;\n\n    private int maximumZoom = Integer.MAX_VALUE;\n\n    private LatLngBounds bounds;\n\n    private SQLiteDatabase database;\n\n    // ------------------------------------------------------------------------\n    // Constructors\n    // ------------------------------------------------------------------------\n\n    public GoogleMapsMapBoxOfflineTileProvider(File file) {\n        this(file.getAbsolutePath());\n    }\n\n    public GoogleMapsMapBoxOfflineTileProvider(String pathToFile) {\n        int flags = SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS;\n        this.database = SQLiteDatabase.openDatabase(pathToFile, null, flags);\n        this.calculateZoomConstraints();\n        this.calculateBounds();\n    }\n\n    // ------------------------------------------------------------------------\n    // TileProvider Interface\n    // ------------------------------------------------------------------------\n\n    @Override\n    public Tile getTile(int x, int y, int z) {\n        Tile tile = NO_TILE;\n        if (this.isZoomLevelAvailable(z) && this.isDatabaseAvailable()) {\n            String[] projection = {\n                    \"tile_data\"\n            };\n            int row = (int) (Math.pow(2, z) - y) - 1;\n            String predicate = \"tile_row = ? AND tile_column = ? AND zoom_level = ?\";\n            String[] values = {\n                    String.valueOf(row), String.valueOf(x), String.valueOf(z)\n            };\n            Cursor c = this.database.query(\"tiles\", projection, predicate, values, null, null,\n                    null);\n            if (c != null) {\n                c.moveToFirst();\n                if (!c.isAfterLast()) {\n                    tile = new Tile(256, 256, c.getBlob(0));\n                }\n                c.close();\n            }\n        }\n        return tile;\n    }\n\n    // ------------------------------------------------------------------------\n    // Closeable Interface\n    // ------------------------------------------------------------------------\n    @Override\n    public void close() {\n        if (this.database != null) {\n            this.database.close();\n            this.database = null;\n        }\n    }\n\n    // ------------------------------------------------------------------------\n    // Public Methods\n    // ------------------------------------------------------------------------\n\n    public int getMinimumZoom() {\n        return this.minimumZoom;\n    }\n\n    public int getMaximumZoom() {\n        return this.maximumZoom;\n    }\n\n    public LatLngBounds getBounds() {\n        return this.bounds;\n    }\n\n    public boolean isZoomLevelAvailable(int zoom) {\n        return (zoom >= this.minimumZoom) && (zoom <= this.maximumZoom);\n    }\n\n    // ------------------------------------------------------------------------\n    // Private Methods\n    // ------------------------------------------------------------------------\n\n    private void calculateZoomConstraints() {\n        if (this.isDatabaseAvailable()) {\n\n            String[] projection = {\"value\"};\n            String[] minArgs = {\"minzoom\"};\n\n            Cursor c = this.database.query(\"metadata\", projection, \"name = ?\", minArgs, null, null, null);\n\n            c.moveToFirst();\n            if (!c.isAfterLast()) {\n                this.minimumZoom = c.getInt(0);\n            }\n            c.close();\n\n            String[] maxArgs = {\"maxzoom\"};\n            c = this.database.query(\"metadata\", projection, \"name = ?\", maxArgs, null, null, null);\n\n            c.moveToFirst();\n            if (!c.isAfterLast()) {\n                this.maximumZoom = c.getInt(0);\n            }\n            c.close();\n        }\n    }\n\n    private void calculateBounds() {\n        if (this.isDatabaseAvailable()) {\n            String[] projection = {\"value\"};\n            String[] subArgs = {\"bounds\"};\n\n            Cursor c = this.database.query(\"metadata\", projection, \"name = ?\", subArgs, null, null,\n                    null);\n            c.moveToFirst();\n            if (!c.isAfterLast()) {\n                String[] parts = c.getString(0).split(\",\\\\s*\");\n\n                double w = Double.parseDouble(parts[0]);\n                double s = Double.parseDouble(parts[1]);\n                double e = Double.parseDouble(parts[2]);\n                double n = Double.parseDouble(parts[3]);\n\n                LatLng ne = new LatLng(n, e);\n                LatLng sw = new LatLng(s, w);\n\n                this.bounds = new LatLngBounds(sw, ne);\n            }\n            c.close();\n        }\n    }\n\n    private boolean isDatabaseAvailable() {\n        return (this.database != null) && (this.database.isOpen());\n    }\n\n}\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/MapPointExt.kt",
    "content": "package org.odk.collect.googlemaps\n\nimport com.google.android.gms.maps.model.LatLng\nimport org.odk.collect.maps.MapPoint\n\nobject MapPointExt {\n    @JvmStatic\n    fun MapPoint.toLatLng(): LatLng {\n        return LatLng(this.latitude, this.longitude)\n    }\n}"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/circles/CircleFeature.kt",
    "content": "package org.odk.collect.googlemaps.circles\n\nimport com.google.android.gms.maps.GoogleMap\nimport com.google.android.gms.maps.model.CircleOptions\nimport com.google.android.gms.maps.model.Marker\nimport com.google.android.gms.maps.model.Polygon\nimport com.google.android.gms.maps.model.Polyline\nimport org.odk.collect.googlemaps.GoogleMapFragment\nimport org.odk.collect.googlemaps.MapPointExt.toLatLng\nimport org.odk.collect.maps.circles.CircleDescription\nimport org.odk.collect.maps.circles.getFillColor\nimport org.odk.collect.maps.circles.getStrokeColor\n\nclass CircleFeature(circleDescription: CircleDescription, map: GoogleMap) :\n    GoogleMapFragment.MapFeature {\n\n    private val circle = map.addCircle(\n        CircleOptions()\n            .center(circleDescription.center.toLatLng())\n            .radius(circleDescription.radius.toDouble())\n            .strokeColor(circleDescription.getStrokeColor())\n            .fillColor(circleDescription.getFillColor())\n            .strokeWidth(1f)\n    )\n\n    override fun ownsMarker(marker: Marker?): Boolean {\n        return false\n    }\n\n    override fun ownsPolyline(polyline: Polyline?): Boolean {\n        return false\n    }\n\n    override fun ownsPolygon(polygon: Polygon?): Boolean {\n        return false\n    }\n\n    override fun update() {\n\n    }\n\n    override fun dispose() {\n        circle.remove()\n    }\n}"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/scaleview/Drawer.java",
    "content": "/*\n * This file includes code from MapScaleView (https://github.com/pengrad/MapScaleView),\n * licensed under the Apache License, Version 2.0.\n */\npackage org.odk.collect.googlemaps.scaleview;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.Rect;\nimport android.graphics.Typeface;\n\npublic class Drawer {\n\n    private final Paint textPaint = new Paint();\n    private final Paint strokePaint = new Paint();\n    private final Path strokePath = new Path();\n\n    private final Paint outlinePaint = new Paint();\n    private final Path outlineDiffPath = new Path();\n    private float outlineStrokeWidth = 2; // strokeWidth * 2\n    private float outlineStrokeDiff = outlineStrokeWidth / 2 / 2;  // strokeWidth / 2\n    private float outlineTextStrokeWidth = 3; // density * 2\n    private boolean outlineEnabled = true;\n\n    private float textHeight;\n    private float horizontalLineY;\n\n    private boolean expandRtlEnabled;\n    private int viewWidth;\n\n    private Scales scales = new Scales(null, null);\n\n    Drawer(int color, float textSize, float strokeWidth, float density, boolean outlineEnabled, boolean expandRtlEnabled) {\n        textPaint.setAntiAlias(true);\n        textPaint.setColor(color);\n        textPaint.setStyle(Paint.Style.FILL);\n        textPaint.setTextSize(textSize);\n\n        strokePaint.setAntiAlias(true);\n        strokePaint.setColor(color);\n        strokePaint.setStyle(Paint.Style.STROKE);\n        strokePaint.setStrokeWidth(strokeWidth);\n\n        outlinePaint.set(strokePaint);\n        outlinePaint.setARGB(255, 255, 255, 255);\n        outlineStrokeWidth = strokeWidth * 2;\n        outlineStrokeDiff = strokeWidth / 2;\n        outlineTextStrokeWidth = density * 2;\n        this.outlineEnabled = outlineEnabled;\n        this.expandRtlEnabled = expandRtlEnabled;\n\n        update();\n    }\n\n    private void update() {\n        outlinePaint.setTextSize(textPaint.getTextSize());\n        outlinePaint.setTypeface(textPaint.getTypeface());\n        outlinePaint.setStrokeWidth(outlineTextStrokeWidth);\n\n        Rect textRect = new Rect();\n        Paint highestPaint = outlineEnabled ? outlinePaint : textPaint;\n        String possibleText = \"1234567890kmift\";\n        highestPaint.getTextBounds(possibleText, 0, possibleText.length(), textRect);\n        textHeight = textRect.height();\n\n        horizontalLineY = textHeight + textHeight / 2;\n    }\n\n    int getWidth() {\n        return (int) (scales.maxLength() + strokePaint.getStrokeWidth());\n    }\n\n    int getHeight() {\n        if (scales.bottom() != null) {\n            return (int) (textHeight * 3 + outlineTextStrokeWidth / 2);\n        } else {\n            return (int) (horizontalLineY + strokePaint.getStrokeWidth());\n        }\n    }\n\n    void setScales(Scales scales) {\n        this.scales = scales;\n    }\n\n    void setColor(int color) {\n        textPaint.setColor(color);\n        strokePaint.setColor(color);\n    }\n\n    void setTextSize(float textSize) {\n        textPaint.setTextSize(textSize);\n        update();\n    }\n\n    void setTextFont(Typeface font) {\n        textPaint.setTypeface(font);\n        update();\n    }\n\n    void setStrokeWidth(float strokeWidth) {\n        strokePaint.setStrokeWidth(strokeWidth);\n        outlineStrokeWidth = strokeWidth * 2;\n        outlineStrokeDiff = strokeWidth / 2;\n        update();\n    }\n\n    void setOutlineEnabled(boolean enabled) {\n        outlineEnabled = enabled;\n        update();\n    }\n\n    void setExpandRtlEnabled(boolean enabled) {\n        expandRtlEnabled = enabled;\n    }\n\n    void setViewWidth(int width) {\n        viewWidth = width;\n    }\n\n    void draw(Canvas canvas) {\n        Scale top = scales.top();\n        if (top == null) {\n            return;\n        }\n        if (expandRtlEnabled && viewWidth == 0) {\n            expandRtlEnabled = false;\n        }\n\n        if (expandRtlEnabled) {\n            outlinePaint.setTextAlign(Paint.Align.RIGHT);\n            textPaint.setTextAlign(Paint.Align.RIGHT);\n        } else {\n            outlinePaint.setTextAlign(Paint.Align.LEFT);\n            textPaint.setTextAlign(Paint.Align.LEFT);\n        }\n\n        if (outlineEnabled) {\n            outlinePaint.setStrokeWidth(outlineTextStrokeWidth);\n            canvas.drawText(top.text(), expandRtlEnabled ? viewWidth : 0, textHeight, outlinePaint);\n        }\n        canvas.drawText(top.text(), expandRtlEnabled ? viewWidth : 0, textHeight, textPaint);\n\n        strokePath.rewind();\n        strokePath.moveTo(expandRtlEnabled ? (viewWidth - outlineStrokeDiff) : outlineStrokeDiff, horizontalLineY);\n        strokePath.lineTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), horizontalLineY);\n        if (outlineEnabled) {\n            strokePath.lineTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), textHeight + outlineStrokeDiff);\n        } else {\n            strokePath.lineTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), textHeight);\n        }\n\n        Scale bottom = scales.bottom();\n        if (bottom != null) {\n\n            if (bottom.length() > top.length()) {\n                strokePath.moveTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), horizontalLineY);\n                strokePath.lineTo(expandRtlEnabled ? (viewWidth - bottom.length()) : bottom.length(), horizontalLineY);\n            } else {\n                strokePath.moveTo(expandRtlEnabled ? (viewWidth - bottom.length()) : bottom.length(), horizontalLineY);\n            }\n\n            strokePath.lineTo(expandRtlEnabled ? (viewWidth - bottom.length()) : bottom.length(), textHeight * 2);\n\n            float bottomTextY = horizontalLineY + textHeight + textHeight / 2;\n            if (outlineEnabled) {\n                canvas.drawText(bottom.text(), expandRtlEnabled ? viewWidth : 0, bottomTextY, outlinePaint);\n            }\n            canvas.drawText(bottom.text(), expandRtlEnabled ? viewWidth : 0, bottomTextY, textPaint);\n        }\n\n        if (outlineEnabled) {\n            outlinePaint.setStrokeWidth(outlineStrokeWidth);\n            outlineDiffPath.rewind();\n            outlineDiffPath.moveTo(expandRtlEnabled ? viewWidth : 0, horizontalLineY);\n            outlineDiffPath.lineTo(expandRtlEnabled ? (viewWidth - outlineStrokeDiff) : outlineStrokeDiff, horizontalLineY);\n            outlineDiffPath.moveTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), textHeight + outlineStrokeDiff);\n            outlineDiffPath.lineTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), textHeight);\n            if (bottom != null) {\n                outlineDiffPath.moveTo(expandRtlEnabled ? (viewWidth - bottom.length()) : bottom.length(), textHeight * 2);\n                outlineDiffPath.lineTo(expandRtlEnabled ? (viewWidth - bottom.length()) : bottom.length(), textHeight * 2 + outlineStrokeDiff);\n            }\n\n            canvas.drawPath(outlineDiffPath, outlinePaint);\n            canvas.drawPath(strokePath, outlinePaint);\n        }\n\n        canvas.drawPath(strokePath, strokePaint);\n    }\n}\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/scaleview/MapScaleModel.java",
    "content": "/*\n * This file includes code from MapScaleView (https://github.com/pengrad/MapScaleView),\n * licensed under the Apache License, Version 2.0.\n */\npackage org.odk.collect.googlemaps.scaleview;\n\nclass MapScaleModel {\n\n    private static final double EQUATOR_LENGTH_METERS = 40075016.686;\n    private static final double EQUATOR_LENGTH_FEET = 131479713.537;\n\n    private static final int FT_IN_MILE = 5280;\n\n    private static final float[] METERS = {0.2f, 0.5f, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000,\n            2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000, 2000000};\n\n    private static final float[] FT = {1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000,\n            FT_IN_MILE, 2 * FT_IN_MILE, 5 * FT_IN_MILE, 10 * FT_IN_MILE, 20 * FT_IN_MILE, 50 * FT_IN_MILE,\n            100 * FT_IN_MILE, 200 * FT_IN_MILE, 500 * FT_IN_MILE, 1000 * FT_IN_MILE, 2000 * FT_IN_MILE};\n\n    private final float density;\n    private int maxWidth;\n\n    private float lastZoom = -1;\n    private double lastLatitude = -100;\n\n    private double tileSizeMetersAt0Zoom = EQUATOR_LENGTH_METERS / 256;\n    private double tileSizeFeetAt0Zoom = EQUATOR_LENGTH_FEET / 256;\n\n    MapScaleModel(float density) {\n        this.density = density;\n    }\n\n    // returns true if width changed\n    boolean updateMaxWidth(int width) {\n        if (maxWidth != width) {\n            maxWidth = width;\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    void setTileSize(int tileSize) {\n        tileSizeMetersAt0Zoom = EQUATOR_LENGTH_METERS / tileSize;\n        tileSizeFeetAt0Zoom = EQUATOR_LENGTH_FEET / tileSize;\n    }\n\n    void setPosition(float zoom, double latitude) {\n        lastZoom = zoom;\n        lastLatitude = latitude;\n    }\n\n    /**\n     * See http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Resolution_and_Scale\n     */\n    Scale update(boolean meters) {\n        float zoom = lastZoom;\n        double latitude = lastLatitude;\n        if (zoom < 0 || Math.abs(latitude) > 90) {\n            return null;\n        }\n\n        double tileSizeAtZoom0 = meters ? tileSizeMetersAt0Zoom : tileSizeFeetAt0Zoom;\n        float[] distances = meters ? METERS : FT;\n\n        final double resolution = tileSizeAtZoom0 / density * Math.cos(latitude * Math.PI / 180) / Math.pow(2, zoom);\n\n        float distance = 0;\n        int distanceIndex = distances.length;\n        double screenDistance = maxWidth + 1;\n\n        while (screenDistance > maxWidth && distanceIndex > 0) {\n            distance = distances[--distanceIndex];\n            screenDistance = Math.abs(distance / resolution);\n        }\n\n        lastZoom = zoom;\n        lastLatitude = latitude;\n        return new Scale(text(distance, meters), (float) screenDistance);\n    }\n\n    private String text(float distance, boolean meters) {\n        if (meters) {\n            if (distance < 1) {\n                return (int) (distance * 100) + \" cm\";\n            } else if (distance < 1000) {\n                return (int) distance + \" m\";\n            } else {\n                return (int) distance / 1000 + \" km\";\n            }\n        } else {\n            if (distance < FT_IN_MILE) {\n                return (int) distance + \" ft\";\n            } else {\n                return (int) distance / FT_IN_MILE + \" mi\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/scaleview/MapScaleView.java",
    "content": "/*\n * This file includes code from MapScaleView (https://github.com/pengrad/MapScaleView),\n * licensed under the Apache License, Version 2.0.\n */\npackage org.odk.collect.googlemaps.scaleview;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Typeface;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport androidx.annotation.ColorInt;\n\npublic class MapScaleView extends View {\n\n    private final MapScaleModel mapScaleModel;\n    private final Drawer drawer;\n\n    private final int maxWidth;\n\n    private ScaleType scaleType = ScaleType.BOTH;\n\n    private enum ScaleType {\n        METERS_ONLY, MILES_ONLY, BOTH\n    }\n\n    public MapScaleView(Context context) {\n        this(context, null);\n    }\n\n    public MapScaleView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public MapScaleView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n\n        float density = getResources().getDisplayMetrics().density;\n        mapScaleModel = new MapScaleModel(density);\n\n        ViewConfig viewConfig = new ViewConfig(context, attrs);\n        drawer = new Drawer(viewConfig.color, viewConfig.textSize, viewConfig.strokeWidth, density, viewConfig.outline, viewConfig.expandRtl);\n\n        maxWidth = viewConfig.maxWidth;\n\n        if (viewConfig.isMiles) {\n            scaleType = ScaleType.MILES_ONLY;\n        }\n    }\n\n    public void setTileSize(int tileSize) {\n        mapScaleModel.setTileSize(tileSize);\n        updateScales();\n    }\n\n    public void setColor(@ColorInt int color) {\n        drawer.setColor(color);\n        invalidate();\n    }\n\n    public void setTextSize(float textSize) {\n        drawer.setTextSize(textSize);\n        invalidate();\n        requestLayout();\n    }\n\n    public void setTextFont(Typeface font) {\n        drawer.setTextFont(font);\n        invalidate();\n        requestLayout();\n    }\n\n    public void setStrokeWidth(float strokeWidth) {\n        drawer.setStrokeWidth(strokeWidth);\n        invalidate();\n        requestLayout();\n    }\n\n    public void setOutlineEnabled(boolean enabled) {\n        drawer.setOutlineEnabled(enabled);\n        invalidate();\n    }\n\n    public void setExpandRtlEnabled(boolean enabled) {\n        drawer.setExpandRtlEnabled(enabled);\n        invalidate();\n    }\n\n    /**\n     * @deprecated Use milesOnly()\n     */\n    @Deprecated\n    public void setIsMiles(boolean miles) {\n        if (miles) {\n            milesOnly();\n        } else {\n            metersAndMiles();\n        }\n    }\n\n    public void metersOnly() {\n        scaleType = ScaleType.METERS_ONLY;\n        updateScales();\n    }\n\n    public void milesOnly() {\n        scaleType = ScaleType.MILES_ONLY;\n        updateScales();\n    }\n\n    public void metersAndMiles() {\n        scaleType = ScaleType.BOTH;\n        updateScales();\n    }\n\n    public void update(float zoom, double latitude) {\n        mapScaleModel.setPosition(zoom, latitude);\n        updateScales();\n    }\n\n    private void updateScales() {\n        Scale top;\n        Scale bottom = null;\n\n        if (scaleType == ScaleType.MILES_ONLY) {\n            top = mapScaleModel.update(false);\n        } else {\n            top = mapScaleModel.update(true);\n            if (scaleType == ScaleType.BOTH) {\n                bottom = mapScaleModel.update(false);\n            }\n        }\n\n        drawer.setScales(new Scales(top, bottom));\n        invalidate();\n        requestLayout();\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        int width = measureDimension(desiredWidth(), widthMeasureSpec);\n        int height = measureDimension(desiredHeight(), heightMeasureSpec);\n\n        if (mapScaleModel.updateMaxWidth(width)) {\n            updateScales();\n        }\n\n        if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {\n            width = drawer.getWidth();\n        }\n\n        drawer.setViewWidth(width);\n        setMeasuredDimension(width, height);\n    }\n\n    private int desiredWidth() {\n        return maxWidth;\n    }\n\n    private int desiredHeight() {\n        return drawer.getHeight();\n    }\n\n    private int measureDimension(int desiredSize, int measureSpec) {\n        int mode = View.MeasureSpec.getMode(measureSpec);\n        int size = View.MeasureSpec.getSize(measureSpec);\n\n        if (mode == View.MeasureSpec.EXACTLY) {\n            return size;\n        } else if (mode == View.MeasureSpec.AT_MOST) {\n            return Math.min(desiredSize, size);\n        } else {\n            return desiredSize;\n        }\n    }\n\n    @Override\n    public void onDraw(Canvas canvas) {\n        drawer.draw(canvas);\n    }\n}\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/scaleview/Scale.java",
    "content": "/*\n * This file includes code from MapScaleView (https://github.com/pengrad/MapScaleView),\n * licensed under the Apache License, Version 2.0.\n */\npackage org.odk.collect.googlemaps.scaleview;\n\nclass Scale {\n\n    private final String text;\n    private final float length;\n\n    Scale(String text, float length) {\n        this.text = text;\n        this.length = length;\n    }\n\n    public String text() {\n        return text;\n    }\n\n    public float length() {\n        return length;\n    }\n}\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/scaleview/Scales.java",
    "content": "/*\n * This file includes code from MapScaleView (https://github.com/pengrad/MapScaleView),\n * licensed under the Apache License, Version 2.0.\n */\npackage org.odk.collect.googlemaps.scaleview;\n\nimport androidx.annotation.Nullable;\n\nclass Scales {\n    private final Scale top;\n    private final Scale bottom;\n\n    Scales(Scale top, Scale bottom) {\n        this.top = top;\n        this.bottom = bottom;\n    }\n\n    @Nullable\n    Scale top() {\n        return top;\n    }\n\n    @Nullable\n    Scale bottom() {\n        return bottom;\n    }\n\n    float maxLength() {\n        return Math.max(top != null ? top.length() : 0, bottom != null ? bottom.length() : 0);\n    }\n}\n"
  },
  {
    "path": "google-maps/src/main/java/org/odk/collect/googlemaps/scaleview/ViewConfig.java",
    "content": "/*\n * This file includes code from MapScaleView (https://github.com/pengrad/MapScaleView),\n * licensed under the Apache License, Version 2.0.\n */\npackage org.odk.collect.googlemaps.scaleview;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Color;\nimport android.util.AttributeSet;\n\nimport org.odk.collect.googlemaps.R;\n\nclass ViewConfig {\n\n    final int maxWidth;\n    final int color;\n    final float textSize;\n    final float strokeWidth;\n    final boolean isMiles;\n    final boolean outline;\n    final boolean expandRtl;\n\n    ViewConfig(Context context, AttributeSet attrs) {\n        float density = context.getResources().getDisplayMetrics().density;\n\n        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MapScaleView, 0, 0);\n        try {\n            maxWidth = a.getDimensionPixelSize(R.styleable.MapScaleView_scale_maxWidth, (int) (100 * density));\n            color = a.getColor(R.styleable.MapScaleView_scale_color, Color.parseColor(\"#333333\"));\n            textSize = a.getDimension(R.styleable.MapScaleView_scale_textSize, 12 * density);\n            strokeWidth = a.getDimension(R.styleable.MapScaleView_scale_strokeWidth, 1.5f * density);\n            isMiles = a.getBoolean(R.styleable.MapScaleView_scale_miles, false);\n            outline = a.getBoolean(R.styleable.MapScaleView_scale_outline, true);\n            expandRtl = a.getBoolean(R.styleable.MapScaleView_scale_expandRtl, false);\n        } finally {\n            a.recycle();\n        }\n    }\n}\n"
  },
  {
    "path": "google-maps/src/main/res/layout/map_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/map\"\n        android:name=\"com.google.android.gms.maps.SupportMapFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"/>\n\n    <org.odk.collect.googlemaps.scaleview.MapScaleView\n        android:id=\"@+id/scale_view\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:layout_marginStart=\"@dimen/margin_extra_small\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "google-maps/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n    This file includes code from MapScaleView (https://github.com/pengrad/MapScaleView),\n    licensed under the Apache License, Version 2.0.\n-->\n<resources>\n    <declare-styleable name=\"MapScaleView\">\n        <attr name=\"scale_maxWidth\" format=\"dimension\"/>\n        <attr name=\"scale_textSize\" format=\"dimension\"/>\n        <attr name=\"scale_strokeWidth\" format=\"dimension\"/>\n        <attr name=\"scale_color\" format=\"color\"/>\n        <attr name=\"scale_miles\" format=\"boolean\"/>\n        <attr name=\"scale_outline\" format=\"boolean\"/>\n        <attr name=\"scale_expandRtl\" format=\"boolean\"/>\n    </declare-styleable>\n</resources>\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\ncompileSdk = \"36\"\nminSdk = \"26\"\ntargetSdk = \"36\"\nlifecycle = \"2.10.0\"\nnavigation = \"2.9.7\"\nwork = \"2.11.1\"\nandroidxFragment = \"1.8.9\"\nokhttp3 = \"5.3.2\"\ndagger = \"2.59.1\"\nglide = \"5.0.5\"\ncamerax = \"1.5.3\"\nespresso = \"3.7.0\"\nkotlin = \"2.3.10\"\nmockito = \"5.21.0\"\ncoroutines=\"1.10.2\"\n\n[libraries]\nfirebaseCrashlyticsGradle = { group = \"com.google.firebase\", name = \"firebase-crashlytics-gradle\", version = \"3.0.6\" }\ngoogleServices = { group = \"com.google.gms\", name = \"google-services\", version = \"4.4.4\" }\ngradle = { group = \"com.android.tools.build\", name = \"gradle\", version = \"9.0.0\" }\nkotlinGradlePlugin = { group = \"org.jetbrains.kotlin\", name = \"kotlin-gradle-plugin\", version.ref = \"kotlin\" }\nkotlinStdlib = { group = \"org.jetbrains.kotlin\", name = \"kotlin-stdlib\", version.ref = \"kotlin\" }\nktlintGradle = { group = \"org.jlleitschuh.gradle\", name = \"ktlint-gradle\", version = \"14.0.1\" }\nossLicensesPlugin = { group = \"com.google.android.gms\", name = \"oss-licenses-plugin\", version = \"0.10.10\" }\ndesugar = { group = \"com.android.tools\", name = \"desugar_jdk_libs\", version = \"2.1.5\" }\nandroidxStartup = { group = \"androidx.startup\", name = \"startup-runtime\", version = \"1.2.0\" }\nandroidxLifecycleRuntimeKtx = { group = \"androidx.lifecycle\", name = \"lifecycle-runtime-ktx\", version.ref = \"lifecycle\" }\nandroidxLifecycleLivedataKtx = { group = \"androidx.lifecycle\", name = \"lifecycle-livedata-ktx\", version.ref = \"lifecycle\" }\nandroidxLifecycleViewmodelKtx = { group = \"androidx.lifecycle\", name = \"lifecycle-viewmodel-ktx\", version.ref = \"lifecycle\" }\nandroidxLifecycleViewmodelCompose = { group = \"androidx.lifecycle\", name = \"lifecycle-viewmodel-compose\", version.ref = \"lifecycle\" }\nandroidxViewpager2 = { group = \"androidx.viewpager2\", name = \"viewpager2\", version = \"1.1.0\" }\nandroidxCoreKtx = { group = \"androidx.core\", name = \"core-ktx\", version = \"1.17.0\" }\nandroidxBrowser = { group = \"androidx.browser\", name = \"browser\", version = \"1.9.0\" }\nandroidxRecyclerview = { group = \"androidx.recyclerview\", name = \"recyclerview\", version = \"1.4.0\" }\nandroidxNavigationFragmentKtx = { group = \"androidx.navigation\", name = \"navigation-fragment-ktx\", version.ref = \"navigation\" }\nandroidxNavigationUi = { group = \"androidx.navigation\", name = \"navigation-ui-ktx\", version.ref = \"navigation\" }\nnavigationSafeArgsGradlePlugin = { group = \"androidx.navigation\", name = \"navigation-safe-args-gradle-plugin\", version.ref = \"navigation\" }\nandroidxActivity = { group = \"androidx.activity\", name = \"activity-ktx\", version = \"1.12.4\" }\nandroidxAppcompat = { group = \"androidx.appcompat\", name = \"appcompat\", version = \"1.7.1\" }\nandroidxWorkRuntime = { group = \"androidx.work\", name = \"work-runtime-ktx\", version.ref = \"work\" }\nandroidxExinterface = { group = \"androidx.exifinterface\", name = \"exifinterface\", version = \"1.4.2\" }\nandroidxPreferenceKtx = { group = \"androidx.preference\", name = \"preference-ktx\", version = \"1.2.1\" }\nandroidxFragmentKtx = { group = \"androidx.fragment\", name = \"fragment-ktx\", version.ref = \"androidxFragment\" }\nandroidMaterial = { group = \"com.google.android.material\", name = \"material\", version = \"1.12.0\" } # Check if https://github.com/getodk/collect/issues/6974 is still an issue before upgrading\nandroidFlexbox = { group = \"com.google.android.flexbox\", name = \"flexbox\", version = \"3.0.0\" }\nandroidxComposeBom = { group = \"androidx.compose\", name = \"compose-bom\", version = \"2026.02.00\" }\nandroidXComposeMaterial = { group = \"androidx.compose.material3\", name = \"material3\" }\nandroidXComposeMaterialIconsExtended = { group = \"androidx.compose.material\", name = \"material-icons-extended\" }\nandroidXComposePreview = { group = \"androidx.compose.ui\", name = \"ui-tooling-preview\"}\nandroidXComposeTooling = { group = \"androidx.compose.ui\", name = \"ui-tooling\"}\nandroidXComposeUiTestJunit4 = { group = \"androidx.compose.ui\", name = \"ui-test-junit4\"}\nandroidXComposeUiTestManifest = { group = \"androidx.compose.ui\", name = \"ui-test-manifest\"}\nandroidXConstraintLayoutCompose = { group = \"androidx.constraintlayout\", name = \"constraintlayout-compose\", version = \"1.1.1\" }\nandroidXComposeMaterialIcons = { group = \"androidx.compose.material\", name = \"material-icons-extended\", version =\"1.7.8\"}\nplayServicesMaps = { group = \"com.google.android.gms\", name = \"play-services-maps\", version = \"20.0.0\" }\nplayServicesLocation = { group = \"com.google.android.gms\", name = \"play-services-location\", version = \"21.3.0\" }\nplayServicesOssLicenses = { group = \"com.google.android.gms\", name = \"play-services-oss-licenses\", version = \"17.4.0\" }\nmapboxAndroidSdk = { group = \"com.mapbox.maps\", name = \"android-ndk27\", version = \"10.19.1\" } # Check if https://github.com/mapbox/mapbox-maps-android/issues/2389#issuecomment-2432018812 no longer takes place before upgrading\nosmdroid = { group = \"org.osmdroid\", name = \"osmdroid-android\", version = \"6.1.20\" }\nokHttp = { group = \"com.squareup.okhttp3\", name = \"okhttp\", version.ref = \"okhttp3\" }\nokHttpTls = { group = \"com.squareup.okhttp3\", name = \"okhttp-tls\", version.ref = \"okhttp3\" }\nokHttpDigest = { group = \"io.github.rburgst\", name = \"okhttp-digest\", version = \"3.1.1\" }\npersianJodaTime = { group = \"com.github.mohamadian\", name = \"persianjodatime\", version = \"1.2\" }\nmyanmarCalendar = { group = \"com.github.chanmratekoko\", name = \"myanmar-calendar\", version = \"1.1.0.RELEASE\" }\ndanlewAndroidJoda = { group = \"net.danlew\", name = \"android.joda\", version = \"2.14.0\" }\nrarepebbleColorpicker = { group = \"com.github.martin-stone\", name = \"hsv-alpha-color-picker-android\", version = \"3.1.0\" }\ncommonsIo = { group = \"commons-io\", name = \"commons-io\", version = \"2.21.0\" }\nopencsv = { group = \"com.opencsv\", name = \"opencsv\", version = \"5.12.0\" }\njavarosa = { group = \"org.getodk\", name = \"javarosa\", version = \"5.2.0-0dfdce0-SNAPSHOT\" } # Online\n# javarosa = { group = \"org.getodk\", name = \"javarosa\", version = \"local\" } # Local\nkarumiDexter = { group = \"com.karumi\", name = \"dexter\", version = \"6.2.3\" }\nzxingAndroidEmbedded = { group = \"com.journeyapps\", name = \"zxing-android-embedded\", version = \"4.3.0\" }\ndagger = { group = \"com.google.dagger\", name = \"dagger\", version.ref = \"dagger\" }\ndaggerCompiler = { group = \"com.google.dagger\", name = \"dagger-compiler\", version.ref = \"dagger\" }\ndaggerAndroid = { group = \"com.google.dagger\", name = \"dagger-android\", version.ref = \"dagger\" }\ndaggerAndroidProcessor = { group = \"com.google.dagger\", name = \"dagger-android-processor\", version.ref = \"dagger\" }\nkotlinxCoroutinesAndroid = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-coroutines-android\", version.ref = \"coroutines\" }\nkotlinxCoroutinesTest = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-coroutines-test\", version.ref = \"coroutines\" }\narmen101AudioRecorderView = { group = \"com.github.Armen101\", name = \"AudioRecordView\", version = \"1.0.5\" }\nglide = { group = \"com.github.bumptech.glide\", name = \"glide\", version.ref = \"glide\" }\nglideKsp = { group = \"com.github.bumptech.glide\", name = \"ksp\", version.ref = \"glide\" }\ncaverockAndroidsvg = { group = \"com.caverock\", name = \"androidsvg-aar\", version = \"1.4\" }\nmp4parserMuxer = { group = \"org.mp4parser\", name = \"muxer\", version = \"1.9.41\" } # Check if https://github.com/getodk/collect/issues/5323 no longer takes place before upgrading\njson = { module = \"org.json:json\", version = \"20251224\" }\ngson = { group = \"com.google.code.gson\", name = \"gson\", version = \"2.13.2\" }\nfirebaseAnalytics = { group = \"com.google.firebase\", name = \"firebase-analytics\", version = \"23.0.0\" }\nfirebaseCrashlytics = { group = \"com.google.firebase\", name = \"firebase-crashlytics\", version = \"20.0.4\" }\nleakcanary = { group = \"com.squareup.leakcanary\", name = \"leakcanary-android\", version = \"2.14\" }\ntimber = { group = \"com.jakewharton.timber\", name = \"timber\", version = \"5.0.1\" }\nslf4jApi = { group = \"org.slf4j\", name = \"slf4j-api\", version = \"2.0.17\" }\nslf4jTimber = { group = \"com.arcao\", name = \"slf4j-timber\", version = \"3.1\" }\njsonSchemaValidator = { group = \"com.networknt\", name = \"json-schema-validator\", version = \"3.0.0\" }\nsplashscreen = { group = \"androidx.core\", name = \"core-splashscreen\", version = \"1.2.0\" }\ncameraxCore = { group = \"androidx.camera\", name = \"camera-core\", version.ref = \"camerax\" }\ncameraxView = { group = \"androidx.camera\", name = \"camera-view\", version.ref = \"camerax\" }\ncameraxLifecycle = { group = \"androidx.camera\", name = \"camera-lifecycle\", version.ref = \"camerax\" }\ncameraxCamera2 = { group = \"androidx.camera\", name = \"camera-camera2\", version.ref = \"camerax\" }\ncameraxVideo = { group = \"androidx.camera\", name = \"camera-video\", version.ref = \"camerax\" }\ncamera-mlkit-vision = { group = \"androidx.camera\", name = \"camera-mlkit-vision\", version.ref = \"camerax\" }\njsoup = { group = \"org.jsoup\", name = \"jsoup\", version = \"1.22.1\" }\nmlkit-barcodescanning = { group = \"com.google.android.gms\", name = \"play-services-mlkit-barcode-scanning\", version = \"18.3.1\" }\nruntime-livedata = { group = \"androidx.compose.runtime\", name = \"runtime-livedata\" }\n\n# Test dependencies\njunit = { group = \"junit\", name = \"junit\", version = \"4.13.2\" }\nmockitoAndroid = { group = \"org.mockito\", name = \"mockito-android\", version.ref = \"mockito\" }\nmockitoCore = { group = \"org.mockito\", name = \"mockito-core\", version.ref = \"mockito\" }\nmockitoKotlin = { group = \"org.mockito.kotlin\", name = \"mockito-kotlin\", version = \"6.2.3\" }\nandroidxFragmentTesting = { group = \"androidx.fragment\", name = \"fragment-testing\", version.ref = \"androidxFragment\" }\nandroidxArchCoreTesting = { group = \"androidx.arch.core\", name = \"core-testing\", version = \"2.2.0\" }\nandroidxWorkTesting = { group = \"androidx.work\", name = \"work-testing\", version.ref = \"work\" }\nandroidxTestCoreKtx = { group = \"androidx.test\", name = \"core-ktx\", version = \"1.7.0\" }\nandroidxTestRules = { group = \"androidx.test\", name = \"rules\", version = \"1.7.0\" }\nandroidxTestEspressoContrib = { group = \"androidx.test.espresso\", name = \"espresso-contrib\", version.ref = \"espresso\" }\nandroidxTestEspressoCore = { group = \"androidx.test.espresso\", name = \"espresso-core\", version.ref = \"espresso\" }\nandroidxTestEspressoIntents = { group = \"androidx.test.espresso\", name = \"espresso-intents\", version.ref = \"espresso\" }\nandroidxTestExtJunit = { group = \"androidx.test.ext\", name = \"junit-ktx\", version = \"1.3.0\" }\nokhttp3Mockwebserver = { group = \"com.squareup.okhttp3\", name = \"mockwebserver\", version.ref = \"okhttp3\" }\nhamcrest = { group = \"org.hamcrest\", name = \"hamcrest\", version = \"3.0\" }\nrobolectric = { group = \"org.robolectric\", name = \"robolectric\", version = \"4.16.1\" }\nuiautomator = { group = \"androidx.test.uiautomator\", name = \"uiautomator\", version = \"2.3.0\" }\nandroidXComposeUiTest = { group = \"androidx.compose.ui\", name = \"ui-test\", version = \"1.10.3\" }\n\n[plugins]\nandroidApplication = { id = \"com.android.application\" }\nandroidLibrary = { id = \"com.android.library\" }\njavaLibrary = { id = \"java-library\" }\nkotlinLibrary = { id = \"kotlin\" }\nkotlinKsp = { id = \"com.google.devtools.ksp\", version=\"2.3.5\" }\nkotlinParcelize = { id = \"kotlin-parcelize\" }\nsafeargsKotlin = { id = \"androidx.navigation.safeargs.kotlin\" }\nsafeargs = { id = \"androidx.navigation.safeargs\" }\nossLicenses = { id = \"com.google.android.gms.oss-licenses-plugin\" }\ncomposeCompiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Tue Mar 24 09:13:30 GMT 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\nandroid.enableD8.desugaring=true\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.3.1-all.zip\norg.gradle.configureondemand=false\n"
  },
  {
    "path": "gradle.properties",
    "content": "#You can override this in ~/.gradle/gradle.properties\nandroid.useAndroidX=true\norg.gradle.jvmargs=-Xmx2560m -XX:MaxMetaspaceSize=1g\norg.gradle.parallel=true\ntest.heap.max=1g\nandroid.nonTransitiveRClass=true\nandroid.injected.androidTest.leaveApksInstalledAfterRun=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "icons/.gitignore",
    "content": "/build"
  },
  {
    "path": "icons/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    namespace = \"org.odk.collect.icons\"\n}\n\ndependencies {\n    implementation(libs.androidMaterial)\n}\n"
  },
  {
    "path": "icons/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest>\n\n</manifest>"
  },
  {
    "path": "icons/src/main/res/drawable/ic_add_white_24.xml",
    "content": "<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"@android:color/white\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_add_24.xml",
    "content": "<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_barcode_scanner_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M40,840v-200h80v120h120v80L40,840ZM720,840v-80h120v-120h80v200L720,840ZM160,720v-480h80v480h-80ZM280,720v-480h40v480h-40ZM400,720v-480h80v480h-80ZM520,720v-480h120v480L520,720ZM680,720v-480h40v480h-40ZM760,720v-480h40v480h-40ZM40,320v-200h200v80L120,200v120L40,320ZM840,320v-120L720,200v-80h200v200h-80Z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_calendar_today_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M20,3h-1L19,1h-2v2L7,3L7,1L5,1v2L4,3c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,5c0,-1.1 -0.9,-2 -2,-2zM20,21L4,21L4,8h16v13z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_check_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z\" />\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_collapse_24.xml",
    "content": "<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_done_all_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_draw_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M18.85,10.39l1.06,-1.06c0.78,-0.78 0.78,-2.05 0,-2.83L18.5,5.09c-0.78,-0.78 -2.05,-0.78 -2.83,0l-1.06,1.06L18.85,10.39zM13.19,7.56L4,16.76V21h4.24l9.19,-9.19L13.19,7.56zM19,17.5c0,2.19 -2.54,3.5 -5,3.5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1c1.54,0 3,-0.73 3,-1.5c0,-0.47 -0.48,-0.87 -1.23,-1.2l1.48,-1.48C18.32,15.45 19,16.29 19,17.5zM4.58,13.35C3.61,12.79 3,12.06 3,11c0,-1.8 1.89,-2.63 3.56,-3.36C7.59,7.18 9,6.56 9,6c0,-0.41 -0.78,-1 -2,-1C5.74,5 5.2,5.61 5.17,5.64C4.82,6.05 4.19,6.1 3.77,5.76C3.36,5.42 3.28,4.81 3.62,4.38C3.73,4.24 4.76,3 7,3c2.24,0 4,1.32 4,3c0,1.87 -1.93,2.72 -3.64,3.47C6.42,9.88 5,10.5 5,11c0,0.31 0.43,0.6 1.07,0.86L4.58,13.35z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_expand_24.xml",
    "content": "<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_explore_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,10.9c-0.61,0 -1.1,0.49 -1.1,1.1s0.49,1.1 1.1,1.1c0.61,0 1.1,-0.49 1.1,-1.1s-0.49,-1.1 -1.1,-1.1zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM14.19,14.19L6,18l3.81,-8.19L18,6l-3.81,8.19z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_format_list_bulleted_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M4,10.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM4,4.5c-0.83,0 -1.5,0.67 -1.5,1.5S3.17,7.5 4,7.5 5.5,6.83 5.5,6 4.83,4.5 4,4.5zM4,16.5c-0.83,0 -1.5,0.68 -1.5,1.5s0.68,1.5 1.5,1.5 1.5,-0.68 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM7,19h14v-2L7,17v2zM7,13h14v-2L7,11v2zM7,5v2h14L21,5L7,5z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_format_list_numbered_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M2,17h2v0.5L3,17.5v1h1v0.5L2,19v1h3v-4L2,16v1zM3,8h1L4,4L2,4v1h1v3zM2,11h1.8L2,13.1v0.9h3v-1L3.2,13L5,10.9L5,10L2,10v1zM7,5v2h14L21,5L7,5zM7,19h14v-2L7,17v2zM7,13h14v-2L7,11v2z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_language_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z\"/>\n</vector>"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_layers_24.xml",
    "content": "<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n      \n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_library_music_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,7h-3v5.5c0,1.38 -1.12,2.5 -2.5,2.5S10,13.88 10,12.5s1.12,-2.5 2.5,-2.5c0.57,0 1.08,0.19 1.5,0.51L14,5h4v2zM4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_list_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <group\n        android:pivotX=\"12\"\n        android:pivotY=\"12\"\n        android:scaleX=\"1.2\"\n        android:scaleY=\"1.5\">\n        <path\n            android:fillColor=\"?colorOnSurface\"\n            android:pathData=\"M3,13h2v-2L3,11v2zM3,17h2v-2L3,15v2zM3,9h2L5,7L3,7v2zM7,13h14v-2L7,11v2zM7,17h14v-2L7,15v2zM7,7v2h14L21,7L7,7z\" />\n    </group>\n\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_location_off_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,6.5c1.38,0 2.5,1.12 2.5,2.5 0,0.74 -0.33,1.39 -0.83,1.85l3.63,3.63c0.98,-1.86 1.7,-3.8 1.7,-5.48 0,-3.87 -3.13,-7 -7,-7 -1.98,0 -3.76,0.83 -5.04,2.15l3.19,3.19c0.46,-0.52 1.11,-0.84 1.85,-0.84zM16.37,16.1l-4.63,-4.63 -0.11,-0.11L3.27,3 2,4.27l3.18,3.18C5.07,7.95 5,8.47 5,9c0,5.25 7,13 7,13s1.67,-1.85 3.38,-4.35L18.73,21 20,19.73l-3.63,-3.63z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_location_on_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_location_on_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_markup_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M2,12C2,6.477 6.477,2 12,2C17.523,2 22,6.477 22,12C22,17.523 17.523,22 12,22C6.477,22 2,17.523 2,12ZM7.38,18.532L8.26,15.451C8.505,14.592 9.29,14 10.183,14L13.817,14C14.71,14 15.495,14.592 15.74,15.451L16.621,18.532C15.316,19.456 13.721,20 12,20C10.279,20 8.685,19.456 7.38,18.532ZM11.536,7.161L10.143,10.642C10.049,10.879 10,11.131 10,11.385L10,11.9C10,11.955 10.045,12 10.1,12L13.9,12C13.955,12 14,11.955 14,11.9L14,11.385C14,11.131 13.951,10.879 13.857,10.642L12.464,7.161C12.297,6.742 11.703,6.742 11.536,7.161Z\" android:strokeColor=\"#00000000\" android:strokeWidth=\"1\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_mic_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z\"/>\n</vector>"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_mic_off_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M19,11h-1.7c0,0.74 -0.16,1.43 -0.43,2.05l1.23,1.23c0.56,-0.98 0.9,-2.09 0.9,-3.28zM14.98,11.17c0,-0.06 0.02,-0.11 0.02,-0.17L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v0.18l5.98,5.99zM4.27,3L3,4.27l6.01,6.01L9.01,11c0,1.66 1.33,3 2.99,3 0.22,0 0.44,-0.03 0.65,-0.08l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.76,0 -5.3,-2.1 -5.3,-5.1L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c0.91,-0.13 1.77,-0.45 2.54,-0.9L19.73,21 21,19.73 4.27,3z\" />\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_mic_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_my_location_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@android:color/white\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_open_in_new_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_photo_camera_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0\"/>\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_photo_library_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_print_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,8L5,8c-1.66,0 -3,1.34 -3,3v6h4v4h12v-4h4v-6c0,-1.66 -1.34,-3 -3,-3zM16,19L8,19v-5h8v5zM19,12c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM18,3L6,3v4h12L18,3z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_qr_code_2_add_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M200,560v-80h80v80h-80ZM120,480v-80h80v80h-80ZM480,200v-80h80v80h-80ZM180,300h120v-120L180,180v120ZM120,360v-240h240v240L120,360ZM180,780h120v-120L180,660v120ZM120,840v-240h240v240L120,840ZM660,300h120v-120L660,180v120ZM600,360v-240h240v240L600,360ZM360,560v-80h-80v-80h160v160h-80ZM400,360v-160h80v80h80v80L400,360ZM210,270v-60h60v60h-60ZM210,750v-60h60v60h-60ZM690,270v-60h60v60h-60ZM640,840v-120L520,720v-80h120v-120h80v120h120v80L720,720v120h-80Z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_remove_24.xml",
    "content": "<vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M19,13H5v-2h14v2z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_rule_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M16.54,11L13,7.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L16.54,11zM11,7H2v2h9V7zM21,13.41L19.59,12L17,14.59L14.41,12L13,13.41L15.59,16L13,18.59L14.41,20L17,17.41L19.59,20L21,18.59L18.41,16L21,13.41zM11,15H2v2h9V15z\" />\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_settings_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z\"/>\n</vector>"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_signature_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M563,469q73,-54 114,-118.5T718,222q0,-32 -10.5,-47T679,160q-47,0 -83,79.5T560,419q0,14 0.5,26.5T563,469ZM120,840v-80h80v80h-80ZM280,840v-80h80v80h-80ZM440,840v-80h80v80h-80ZM600,840v-80h80v80h-80ZM760,840v-80h80v80h-80ZM136,680l-56,-56 64,-64 -64,-64 56,-56 64,64 64,-64 56,56 -64,64 64,64 -56,56 -64,-64 -64,64ZM618,640q-30,0 -55,-11.5T520,591q-25,14 -51.5,25T414,638l-28,-75q28,-10 53.5,-21.5T489,517q-5,-22 -7.5,-48t-2.5,-56q0,-144 57,-238.5T679,80q52,0 85,38.5T797,226q0,86 -54.5,170T591,547q7,7 14.5,10.5T621,561q26,0 60.5,-33t62.5,-87l73,34q-7,17 -11,41t1,42q10,-5 23.5,-17t27.5,-30l63,49q-26,36 -60,58t-63,22q-21,0 -37.5,-12.5T733,589q-28,25 -57,38t-58,13Z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_time_filled_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n      \n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM15.29,16.71L11,12.41V7h2v4.59l3.71,3.71L15.29,16.71z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_visibility_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_warning_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorError\">\n    <path\n        android:fillColor=\"?colorError\"\n        android:pathData=\"M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_baseline_wifi_off_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M22.99,9C19.15,5.16 13.8,3.76 8.84,4.78l2.52,2.52c3.47,-0.17 6.99,1.05 9.63,3.7l2,-2zM18.99,13c-1.29,-1.29 -2.84,-2.13 -4.49,-2.56l3.53,3.53 0.96,-0.97zM2,3.05L5.07,6.1C3.6,6.82 2.22,7.78 1,9l1.99,2c1.24,-1.24 2.67,-2.16 4.2,-2.77l2.24,2.24C7.81,10.89 6.27,11.73 5,13v0.01L6.99,15c1.36,-1.36 3.14,-2.04 4.92,-2.06L18.98,20l1.27,-1.26L3.29,1.79 2,3.05zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0z\"/>\n    \n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_clear_white.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@android:color/white\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z\"/>\n</vector>"
  },
  {
    "path": "icons/src/main/res/drawable/ic_close.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\" />\n    <path\n        android:pathData=\"M0 0h24v24H0z\" />\n</vector>"
  },
  {
    "path": "icons/src/main/res/drawable/ic_color_lens_white.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@android:color/white\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z\"/>\n</vector>"
  },
  {
    "path": "icons/src/main/res/drawable/ic_delete.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z\" />\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_delete_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z\" />\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_edit.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"@android:color/white\"\n        android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_map_marker_big.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:viewportWidth=\"20\"\n    android:viewportHeight=\"20\"\n    android:tint=\"?colorPrimary\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M10,20C10,20 3,10.87 3,7C3,1.611 8.833,-1.756 13.5,0.938C15.666,2.188 17,4.499 17,7C17,10.87 10,20 10,20Z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_map_marker_small.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"20\"\n    android:viewportHeight=\"20\"\n    android:tint=\"?colorPrimary\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M10,20C10,20 3,10.87 3,7C3,1.611 8.833,-1.756 13.5,0.938C15.666,2.188 17,4.499 17,7C17,10.87 10,20 10,20Z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_map_marker_with_hole_big.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"48dp\"\n    android:width=\"48dp\"\n    android:viewportHeight=\"20\"\n    android:viewportWidth=\"20\"\n    android:tint=\"?colorPrimary\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"m10,20s-7,-9.13 -7,-13a7,7 0,1 1,14 0c0,3.87 -7,13 -7,13zM10,9a2,2 0,1 0,0 -4a2,2 0,0 0,0 4z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_map_marker_with_hole_small.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportHeight=\"20\"\n    android:viewportWidth=\"20\"\n    android:tint=\"?colorPrimary\">\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"m10,20s-7,-9.13 -7,-13a7,7 0,1 1,14 0c0,3.87 -7,13 -7,13zM10,9a2,2 0,1 0,0 -4a2,2 0,0 0,0 4z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_map_point.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"36dp\"\n        android:height=\"36dp\"\n        android:viewportWidth=\"36\"\n        android:viewportHeight=\"36\">\n    <path android:fillColor=\"#ffff0000\" android:pathData=\"M18,13c-2.77,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5s-2.24,-5 -5,-5z\"/>\n    <path android:fillColor=\"#ffffff\" android:pathData=\"M18,21.33c-1.84,0 -3.33,-1.5 -3.33,-3.33s1.5,-3.33 3.33,-3.33s3.33,1.5 3.33,3.33s-1.5,3.33 -3.33,3.33z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_notification_small.xml",
    "content": "<!-- Copyright 2018 Nafundi\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\nhttp://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"16.0\"\n        android:viewportHeight=\"16.0\"\n        android:tint=\"@android:color/white\">\n    <path\n        android:pathData=\"m6.507,0.667c-0.2,0 -0.361,0.133 -0.361,0.298l0,0.521l-0.5,0c-0.315,0 -0.568,0.278 -0.568,0.623l0,0.788l5.845,0l0,-0.788c0,-0.345 -0.254,-0.623 -0.569,-0.623l-0.5,0l0,-0.521c0,-0.165 -0.161,-0.298 -0.361,-0.298z\"\n        android:fillColor=\"@android:color/white\" />\n    <path\n        android:pathData=\"m2.304,1.831c-0.352,0 -0.635,0.283 -0.635,0.634L1.669,14.545c0,0.352 0.283,0.635 0.635,0.635L13.697,15.18c0.352,0 0.634,-0.283 0.634,-0.635L14.331,2.465c0,-0.352 -0.283,-0.634 -0.634,-0.634L11.492,1.831c0.024,0.064 0.037,0.133 0.037,0.204l0,0.46l1.181,0c0.183,0 0.33,0.147 0.33,0.33L13.04,11.119l-2.954,0c-0.088,0 -0.158,0.07 -0.158,0.158l0,2.888c0,0.02 0.004,0.039 0.011,0.056l3.102,-3.101l0,2.774c0,0.183 -0.147,0.33 -0.33,0.33L3.291,14.224c-0.183,0 -0.33,-0.147 -0.33,-0.33L2.961,2.825c0,-0.183 0.147,-0.33 0.33,-0.33l1.18,0l0,-0.46c0,-0.072 0.014,-0.14 0.037,-0.204z\"\n        android:fillColor=\"@android:color/white\" />\n    <path\n        android:pathData=\"m4.524,4.304c-0.105,0 -0.19,0.074 -0.19,0.167l0,0.212c0,0.093 0.084,0.168 0.19,0.168L11.223,4.851c0.105,0 0.191,-0.075 0.191,-0.168l0,-0.212c0,-0.093 -0.085,-0.167 -0.191,-0.167zM4.524,6.895c-0.105,0 -0.19,0.075 -0.19,0.168l0,0.212c0,0.093 0.084,0.168 0.19,0.168l5.949,0c0.105,0 0.191,-0.075 0.191,-0.168L10.664,7.063c0,-0.093 -0.085,-0.168 -0.191,-0.168zM4.524,9.488c-0.105,0 -0.19,0.075 -0.19,0.168l0,0.212c0,0.093 0.084,0.167 0.19,0.167l7.033,0c0.105,0 0.191,-0.074 0.191,-0.167L11.747,9.655c0,-0.093 -0.085,-0.168 -0.191,-0.168zM4.524,12.08c-0.105,0 -0.19,0.074 -0.19,0.167l0,0.212c0,0.093 0.084,0.167 0.19,0.167l4.332,0c0.105,0 0.191,-0.074 0.191,-0.167L9.046,12.247c0,-0.093 -0.085,-0.167 -0.191,-0.167z\"\n        android:fillColor=\"@android:color/white\" />\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_outline_info_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n  <path\n      android:fillColor=\"?colorOnSurface\"\n      android:pathData=\"M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_outline_polygon_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"100\"\n    android:viewportHeight=\"100\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M32.5,10.95c-6.89,0 -12.55,5.66 -12.55,12.55c0,4.02 1.93,7.61 4.91,9.92L14.81,54.17a12.35,12.35 0,0 0,-2.32 -0.22C5.61,53.95 -0.05,59.61 -0.05,66.5c0,6.89 5.66,12.55 12.55,12.55c5.13,0 9.54,-3.15 11.46,-7.6l51.28,7.71c1.23,5.63 6.28,9.89 12.26,9.89c6.66,0 12.11,-5.3 12.48,-11.87a3.5,3.5 0,0 0,0.07 -0.68a3.5,3.5 0,0 0,-0.07 -0.7c-0.38,-6.56 -5.83,-11.85 -12.48,-11.85c-0.13,0 -0.26,0.01 -0.4,0.02L80.24,43.05c3.28,-2.13 5.51,-5.75 5.74,-9.87a3.5,3.5 0,0 0,0.07 -0.68a3.5,3.5 0,0 0,-0.07 -0.7c-0.38,-6.56 -5.83,-11.85 -12.48,-11.85c-5.06,0 -9.45,3.06 -11.43,7.41l-17.08,-4.52a3.5,3.5 0,0 0,-0.01 -0.05c-0.37,-6.56 -5.83,-11.85 -12.48,-11.85zM32.5,17.95c3.11,0 5.55,2.44 5.55,5.55c0,3.11 -2.44,5.55 -5.55,5.55c-3.11,0 -5.55,-2.44 -5.55,-5.55c0,-3.11 2.44,-5.55 5.55,-5.55zM73.5,26.95c3.11,0 5.55,2.44 5.55,5.55c0,3.11 -2.44,5.55 -5.55,5.55c-3.11,0 -5.55,-2.44 -5.55,-5.55c0,-3.11 2.44,-5.55 5.55,-5.55zM43.36,29.66l17.74,4.69C62.01,40.37 67.24,45.05 73.5,45.05l0.03,-0l6.92,21.09a12.69,12.69 0,0 0,-4.7 6.01l-50.92,-7.65a12.61,12.61 0,0 0,-3.79 -7.13l10.34,-21.38c0.37,0.03 0.74,0.06 1.11,0.06c4.65,0 8.71,-2.59 10.86,-6.39zM12.5,60.95c3.11,0 5.55,2.44 5.55,5.55s-2.44,5.55 -5.55,5.55c-3.11,0 -5.55,-2.44 -5.55,-5.55c0,-3.11 2.44,-5.55 5.55,-5.55zM87.5,70.95c3.11,0 5.55,2.44 5.55,5.55s-2.44,5.55 -5.55,5.55c-3.11,0 -5.55,-2.44 -5.55,-5.55c0,-3.11 2.44,-5.55 5.55,-5.55z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_outline_polyline_white_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"100\"\n    android:viewportHeight=\"100\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M32.55,11C25.66,11 20,16.66 20,23.55c0,3.89 1.8,7.38 4.61,9.69L14.48,54.17a12.33,12.33 0,0 0,-1.93 -0.17C5.66,54 0,59.66 0,66.55C0,73.44 5.66,79.1 12.55,79.1c6.65,0 12.11,-5.29 12.48,-11.85a3.5,3.5 0,0 0,0.07 -0.7a3.5,3.5 0,0 0,-0.07 -0.7c-0.2,-3.44 -1.8,-6.52 -4.22,-8.68L31.05,36c0.49,0.06 0.99,0.1 1.5,0.1c4.61,0 8.65,-2.55 10.81,-6.3l17.81,4.71c0.93,5.84 5.95,10.38 12,10.57l7.01,21.36C77.05,68.73 75,72.41 75,76.55c0,6.89 5.66,12.55 12.55,12.55c6.65,0 12.11,-5.29 12.48,-11.85a3.5,3.5 0,0 0,0.07 -0.7a3.5,3.5 0,0 0,-0.07 -0.7C99.65,69.29 94.2,64 87.55,64c-0.27,0 -0.53,0.02 -0.79,0.04l-6.8,-20.74c3.45,-2.09 5.83,-5.8 6.07,-10.05a3.5,3.5 0,0 0,0.07 -0.7a3.5,3.5 0,0 0,-0.07 -0.7C85.66,25.29 80.2,20 73.55,20c-5.1,0 -9.52,3.11 -11.48,7.51l-17.02,-4.5a3.5,3.5 0,0 0,-0.03 -0.16C44.66,16.29 39.2,11 32.55,11zM32.55,18c3.11,0 5.55,2.44 5.55,5.55c0,3.11 -2.44,5.55 -5.55,5.55c-3.11,0 -5.55,-2.44 -5.55,-5.55c0,-3.11 2.44,-5.55 5.55,-5.55zM73.55,27c3.11,0 5.55,2.44 5.55,5.55c0,3.11 -2.44,5.55 -5.55,5.55c-3.11,0 -5.55,-2.44 -5.55,-5.55c0,-3.11 2.44,-5.55 5.55,-5.55zM12.55,61c3.11,0 5.55,2.44 5.55,5.55c0,3.11 -2.44,5.55 -5.55,5.55C9.44,72.1 7,69.66 7,66.55C7,63.44 9.44,61 12.55,61zM87.55,71c3.11,0 5.55,2.44 5.55,5.55c0,3.11 -2.44,5.55 -5.55,5.55c-3.11,0 -5.55,-2.44 -5.55,-5.55c0,-3.11 2.44,-5.55 5.55,-5.55z\"/>\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_save.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z\" />\n</vector>\n"
  },
  {
    "path": "icons/src/main/res/drawable/ic_save_white.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@android:color/white\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z\"/>\n</vector>"
  },
  {
    "path": "image-loader/.gitignore",
    "content": "/build"
  },
  {
    "path": "image-loader/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    namespace = \"org.odk.collect.imageloader\"\n}\n\ndependencies {\n    implementation(project(\":shared\"))\n\n    implementation(libs.glide)\n    implementation(libs.caverockAndroidsvg)\n    ksp(libs.glideKsp)\n}\n"
  },
  {
    "path": "image-loader/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "image-loader/src/main/java/org/odk/collect/imageloader/GlideImageLoader.kt",
    "content": "package org.odk.collect.imageloader\n\nimport android.graphics.drawable.Drawable\nimport android.graphics.drawable.PictureDrawable\nimport android.widget.ImageView\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.load.DataSource\nimport com.bumptech.glide.load.engine.GlideException\nimport com.bumptech.glide.request.RequestListener\nimport com.bumptech.glide.request.RequestOptions\nimport com.bumptech.glide.request.target.Target\nimport com.bumptech.glide.signature.ObjectKey\nimport org.odk.collect.imageloader.svg.SvgSoftwareLayerSetter\nimport org.odk.collect.shared.strings.Md5.getMd5Hash\nimport java.io.File\n\nclass GlideImageLoader : ImageLoader {\n\n    override fun loadImage(\n        imageView: ImageView,\n        imageFile: File?,\n        scaleType: ImageView.ScaleType,\n        requestListener: ImageLoaderCallback?\n    ) {\n        if (imageFile == null || !imageFile.exists()) {\n            return\n        }\n\n        val requestOptions = RequestOptions().apply {\n            imageView.scaleType = scaleType\n        }\n\n        if (imageFile.name != null && imageFile.name.endsWith(\"svg\")) {\n            Glide.with(imageView)\n                .`as`(PictureDrawable::class.java)\n                .listener(SvgSoftwareLayerSetter())\n                .load(imageFile)\n                .signature(ObjectKey(imageFile.getMd5Hash()!!))\n                .listener(object : RequestListener<PictureDrawable> {\n                    override fun onLoadFailed(\n                        e: GlideException?,\n                        model: Any?,\n                        target: Target<PictureDrawable>,\n                        isFirstResource: Boolean\n                    ): Boolean {\n                        requestListener?.onLoadFailed()\n                        return false\n                    }\n\n                    override fun onResourceReady(\n                        resource: PictureDrawable,\n                        model: Any,\n                        target: Target<PictureDrawable>?,\n                        dataSource: DataSource,\n                        isFirstResource: Boolean\n                    ): Boolean {\n                        requestListener?.onLoadSucceeded()\n                        return false\n                    }\n                })\n                .apply(requestOptions)\n                .into(imageView)\n        } else {\n            Glide.with(imageView)\n                .load(imageFile)\n                .signature(ObjectKey(imageFile.getMd5Hash()!!))\n                .listener(object : RequestListener<Drawable> {\n                    override fun onLoadFailed(\n                        e: GlideException?,\n                        model: Any?,\n                        target: Target<Drawable>,\n                        isFirstResource: Boolean\n                    ): Boolean {\n                        requestListener?.onLoadFailed()\n                        return false\n                    }\n\n                    override fun onResourceReady(\n                        resource: Drawable,\n                        model: Any,\n                        target: Target<Drawable>?,\n                        dataSource: DataSource,\n                        isFirstResource: Boolean\n                    ): Boolean {\n                        requestListener?.onLoadSucceeded()\n                        return false\n                    }\n                })\n                .apply(requestOptions)\n                .into(imageView)\n        }\n    }\n\n    interface ImageLoaderCallback {\n        fun onLoadFailed()\n\n        fun onLoadSucceeded()\n    }\n}\n\ninterface ImageLoader {\n    fun loadImage(\n        imageView: ImageView,\n        imageFile: File?,\n        scaleType: ImageView.ScaleType,\n        requestListener: GlideImageLoader.ImageLoaderCallback?\n    )\n}\n"
  },
  {
    "path": "image-loader/src/main/java/org/odk/collect/imageloader/svg/SvgDecoder.kt",
    "content": "package org.odk.collect.imageloader.svg\n\nimport com.bumptech.glide.load.Options\nimport com.bumptech.glide.load.ResourceDecoder\nimport com.bumptech.glide.load.engine.Resource\nimport com.bumptech.glide.load.resource.SimpleResource\nimport com.bumptech.glide.request.target.Target\nimport com.caverock.androidsvg.SVG\nimport com.caverock.androidsvg.SVGParseException\nimport java.io.IOException\nimport java.io.InputStream\n\n/**\n * Decodes an SVG internal representation from an [InputStream].\n *\n * https://github.com/bumptech/glide/tree/master/samples/svg\n */\nclass SvgDecoder : ResourceDecoder<InputStream, SVG> {\n    override fun handles(source: InputStream, options: Options): Boolean {\n        // TODO: Can we tell?\n        return true\n    }\n\n    @Throws(IOException::class)\n    override fun decode(\n        source: InputStream,\n        width: Int,\n        height: Int,\n        options: Options\n    ): Resource<SVG>? {\n        return try {\n            val svg = SVG.getFromInputStream(source)\n            if (width != Target.SIZE_ORIGINAL) {\n                svg.documentWidth = width.toFloat()\n            }\n            if (height != Target.SIZE_ORIGINAL) {\n                svg.documentHeight = height.toFloat()\n            }\n            SimpleResource(svg)\n        } catch (ex: SVGParseException) {\n            throw IOException(\"Cannot load SVG from stream\", ex)\n        }\n    }\n}\n"
  },
  {
    "path": "image-loader/src/main/java/org/odk/collect/imageloader/svg/SvgDrawableTranscoder.kt",
    "content": "package org.odk.collect.imageloader.svg\n\nimport android.graphics.drawable.PictureDrawable\nimport com.bumptech.glide.load.Options\nimport com.bumptech.glide.load.engine.Resource\nimport com.bumptech.glide.load.resource.SimpleResource\nimport com.bumptech.glide.load.resource.transcode.ResourceTranscoder\nimport com.caverock.androidsvg.SVG\n\n/**\n * Convert the [SVG]'s internal representation to an Android-compatible one ([Picture]).\n *\n * https://github.com/bumptech/glide/tree/master/samples/svg\n */\nclass SvgDrawableTranscoder : ResourceTranscoder<SVG?, PictureDrawable> {\n    override fun transcode(\n        toTranscode: Resource<SVG?>,\n        options: Options\n    ): Resource<PictureDrawable> {\n        val svg = toTranscode.get()\n        val picture = svg.renderToPicture()\n        val drawable = PictureDrawable(picture)\n        return SimpleResource(drawable)\n    }\n}\n"
  },
  {
    "path": "image-loader/src/main/java/org/odk/collect/imageloader/svg/SvgModule.kt",
    "content": "package org.odk.collect.imageloader.svg\n\nimport android.content.Context\nimport android.graphics.drawable.PictureDrawable\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.Registry\nimport com.bumptech.glide.annotation.GlideModule\nimport com.bumptech.glide.module.AppGlideModule\nimport com.caverock.androidsvg.SVG\nimport java.io.InputStream\n\n/**\n *  Module for the SVG sample app.\n *\n *  https://github.com/bumptech/glide/tree/master/samples/svg\n */\n@GlideModule\nopen class SvgModule : AppGlideModule() {\n    override fun registerComponents(\n        context: Context,\n        glide: Glide,\n        registry: Registry\n    ) {\n        registry\n            .register(SVG::class.java, PictureDrawable::class.java, SvgDrawableTranscoder())\n            .append(InputStream::class.java, SVG::class.java, SvgDecoder())\n    }\n\n    // Disable manifest parsing to avoid adding similar modules twice.\n    override fun isManifestParsingEnabled(): Boolean {\n        return false\n    }\n}\n"
  },
  {
    "path": "image-loader/src/main/java/org/odk/collect/imageloader/svg/SvgSoftwareLayerSetter.kt",
    "content": "package org.odk.collect.imageloader.svg\n\nimport android.graphics.drawable.PictureDrawable\nimport android.widget.ImageView\nimport com.bumptech.glide.load.DataSource\nimport com.bumptech.glide.load.engine.GlideException\nimport com.bumptech.glide.request.RequestListener\nimport com.bumptech.glide.request.target.ImageViewTarget\nimport com.bumptech.glide.request.target.Target\n\n/**\n * Listener which updates the [ImageView] to be software rendered, because [ ]/[Picture][android.graphics.Picture] can't render on a\n * hardware backed [Canvas][android.graphics.Canvas].\n *\n * https://github.com/bumptech/glide/tree/master/samples/svg\n */\nclass SvgSoftwareLayerSetter : RequestListener<PictureDrawable?> {\n    override fun onLoadFailed(\n        e: GlideException?,\n        model: Any?,\n        target: Target<PictureDrawable?>,\n        isFirstResource: Boolean\n    ): Boolean {\n        val view = (target as ImageViewTarget<*>).view\n        view.setLayerType(ImageView.LAYER_TYPE_NONE, null)\n        return false\n    }\n\n    override fun onResourceReady(\n        resource: PictureDrawable?,\n        model: Any?,\n        target: Target<PictureDrawable?>?,\n        dataSource: DataSource?,\n        isFirstResource: Boolean\n    ): Boolean {\n        val view = (target as ImageViewTarget<*>).view\n        view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null)\n        return false\n    }\n}\n"
  },
  {
    "path": "lists/.gitignore",
    "content": "/build"
  },
  {
    "path": "lists/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n\n    namespace = \"org.odk.collect.lists\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":androidshared\"))\n    implementation(project(\":material\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":icons\"))\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxLifecycleLivedataKtx)\n    implementation(libs.androidxLifecycleViewmodelKtx)\n    implementation(libs.androidxRecyclerview)\n\n    testImplementation(project(\":test-shared\"))\n    testImplementation(project(\":androidtest\"))\n    testImplementation(libs.junit)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.androidxTestEspressoCore)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.androidxArchCoreTesting)\n\n    debugImplementation(project(\":fragments-test\"))\n}\n"
  },
  {
    "path": "lists/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "lists/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "lists/src/main/java/org/odk/collect/lists/EmptyListView.kt",
    "content": "package org.odk.collect.lists\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport androidx.annotation.DrawableRes\nimport androidx.core.content.withStyledAttributes\nimport org.odk.collect.lists.databinding.EmptyListViewBinding\n\nclass EmptyListView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {\n    constructor(context: Context) : this(context, null)\n\n    private val binding = EmptyListViewBinding.inflate(LayoutInflater.from(context), this, true)\n\n    init {\n        context.withStyledAttributes(attrs, R.styleable.EmptyListView) {\n            val icon = this.getResourceId(R.styleable.EmptyListView_icon, 0)\n            val title = this.getString(R.styleable.EmptyListView_title)\n            val subtitle = this.getString(R.styleable.EmptyListView_subtitle)\n\n            binding.icon.setImageResource(icon)\n            binding.title.text = title\n            binding.subtitle.text = subtitle\n        }\n    }\n\n    fun setIcon(@DrawableRes icon: Int) {\n        binding.icon.setImageResource(icon)\n    }\n\n    fun setTitle(title: String) {\n        binding.title.text = title\n    }\n\n    fun setSubtitle(subtitle: String) {\n        binding.subtitle.text = subtitle\n    }\n}\n"
  },
  {
    "path": "lists/src/main/java/org/odk/collect/lists/RecyclerViewUtils.kt",
    "content": "package org.odk.collect.lists\n\nimport android.content.Context\nimport android.view.ViewGroup\nimport androidx.core.content.ContextCompat\nimport androidx.recyclerview.widget.DividerItemDecoration\nimport androidx.recyclerview.widget.RecyclerView\nimport org.odk.collect.androidshared.R\n\nobject RecyclerViewUtils {\n\n    fun verticalLineDivider(context: Context): DividerItemDecoration {\n        val itemDecoration = DividerItemDecoration(context, DividerItemDecoration.VERTICAL)\n        val drawable = ContextCompat.getDrawable(context, R.drawable.list_item_divider)!!\n        itemDecoration.setDrawable(drawable)\n\n        return itemDecoration\n    }\n\n    fun RecyclerView.ViewHolder.matchParentWidth() {\n        itemView.layoutParams = ViewGroup.LayoutParams(\n            ViewGroup.LayoutParams.MATCH_PARENT,\n            ViewGroup.LayoutParams.WRAP_CONTENT\n        )\n    }\n}\n"
  },
  {
    "path": "lists/src/main/java/org/odk/collect/lists/selects/MultiSelectAdapter.kt",
    "content": "package org.odk.collect.lists.selects\n\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.CheckBox\nimport androidx.recyclerview.widget.RecyclerView\n\n/**\n * An adapter for creating multi select lists with `MultiSelectViewModel` and `RecyclerView`.\n */\nclass MultiSelectAdapter<T, VH : MultiSelectAdapter.ViewHolder<T>>(\n    private val multiSelectViewModel: MultiSelectViewModel<*>,\n    private val viewHolderFactory: (ViewGroup) -> VH\n) : RecyclerView.Adapter<VH>() {\n\n    var selected: Set<String> = emptySet()\n        set(value) {\n            field = value\n            notifyDataSetChanged()\n        }\n\n    var data = emptyList<SelectItem<T>>()\n        set(value) {\n            field = value.toList()\n            notifyDataSetChanged()\n        }\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {\n        return viewHolderFactory(parent)\n    }\n\n    override fun onBindViewHolder(holder: VH, position: Int) {\n        val item = data[position]\n        holder.setItem(item.item)\n\n        val checkbox = holder.getCheckbox().also {\n            it.isChecked = selected.contains(item.id)\n            it.setOnClickListener {\n                multiSelectViewModel.toggle(item.id)\n            }\n        }\n\n        holder.getSelectArea().setOnClickListener {\n            checkbox.performClick()\n        }\n    }\n\n    override fun getItemCount(): Int {\n        return data.size\n    }\n\n    abstract class ViewHolder<T>(view: View) : RecyclerView.ViewHolder(view) {\n        abstract fun setItem(item: T)\n        abstract fun getCheckbox(): CheckBox\n        open fun getSelectArea(): View {\n            return itemView\n        }\n    }\n}\n"
  },
  {
    "path": "lists/src/main/java/org/odk/collect/lists/selects/MultiSelectControlsFragment.kt",
    "content": "package org.odk.collect.lists.selects\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\nimport androidx.fragment.app.Fragment\nimport org.odk.collect.lists.databinding.MultiSelectControlsLayoutBinding\n\n/**\n * A control UI for performing \"select all\" and \"clear all\" on multi select lists using\n * `MultiSelectViewModel`. Also supports an action that's text can be defined (via the\n * constructor) and can be reacted to by responding to the `\"action\"` Fragment result.\n */\nclass MultiSelectControlsFragment(\n    private val actionText: String,\n    private val multiSelectViewModel: MultiSelectViewModel<*>\n) : Fragment() {\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return MultiSelectControlsView(requireContext())\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        val controls = view as MultiSelectControlsView\n\n        controls.actionText = actionText\n\n        multiSelectViewModel.getSelected().observe(viewLifecycleOwner) {\n            controls.selected = it\n        }\n\n        multiSelectViewModel.isAllSelected().observe(viewLifecycleOwner) {\n            controls.isAllSelected = it\n        }\n\n        controls.listener = object : MultiSelectControlsView.Listener {\n            override fun onSelectAll() {\n                multiSelectViewModel.selectAll()\n            }\n\n            override fun onClearAll() {\n                multiSelectViewModel.unselectAll()\n            }\n\n            override fun onAction(selected: Set<String>) {\n                parentFragmentManager.setFragmentResult(\n                    REQUEST_ACTION,\n                    Bundle().apply {\n                        putStringArray(\n                            RESULT_SELECTED,\n                            selected.toTypedArray()\n                        )\n                    }\n                )\n            }\n        }\n    }\n\n    companion object {\n        const val REQUEST_ACTION = \"action\"\n        const val RESULT_SELECTED = \"selected\"\n    }\n}\n\nprivate class MultiSelectControlsView(context: Context) :\n    FrameLayout(context) {\n\n    var selected = emptySet<String>()\n        set(value) {\n            field = value\n            render()\n        }\n\n    var isAllSelected = false\n        set(value) {\n            field = value\n            render()\n        }\n\n    var listener: Listener? = null\n    var actionText: String = \"\"\n        set(value) {\n            field = value\n            render()\n        }\n\n    private val binding =\n        MultiSelectControlsLayoutBinding.inflate(LayoutInflater.from(context), this, true)\n\n    init {\n        render()\n    }\n\n    private fun render() {\n        if (isAllSelected) {\n            binding.selectAll.setText(org.odk.collect.strings.R.string.clear_all)\n            binding.selectAll.setOnClickListener {\n                listener?.onClearAll()\n            }\n        } else {\n            binding.selectAll.setText(org.odk.collect.strings.R.string.select_all)\n            binding.selectAll.setOnClickListener {\n                listener?.onSelectAll()\n            }\n        }\n\n        binding.action.text = actionText\n        binding.action.isEnabled = selected.isNotEmpty()\n        binding.action.setOnClickListener {\n            listener?.onAction(selected)\n        }\n    }\n\n    interface Listener {\n        fun onSelectAll()\n        fun onClearAll()\n        fun onAction(selected: Set<String>)\n    }\n}\n"
  },
  {
    "path": "lists/src/main/java/org/odk/collect/lists/selects/MultiSelectListFragment.kt",
    "content": "package org.odk.collect.lists.selects\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.Fragment\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.appbar.AppBarLayout\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.lists.R\nimport org.odk.collect.lists.databinding.MultiSelectListBinding\n\nclass MultiSelectListFragment<T, VH : MultiSelectAdapter.ViewHolder<T>>(\n    private val actionText: String,\n    private val multiSelectViewModel: MultiSelectViewModel<T>,\n    private val viewHolderFactory: (ViewGroup) -> VH,\n    private val onViewCreated: (MultiSelectListBinding) -> Unit = {}\n) : Fragment() {\n\n    private var appBarLayout: AppBarLayout? = null\n    private lateinit var list: RecyclerView\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n\n        childFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(MultiSelectControlsFragment::class) {\n                MultiSelectControlsFragment(\n                    actionText,\n                    multiSelectViewModel\n                )\n            }.build()\n\n        childFragmentManager.setFragmentResultListener(\n            MultiSelectControlsFragment.REQUEST_ACTION,\n            this\n        ) { _, result ->\n            parentFragmentManager.setFragmentResult(\n                MultiSelectControlsFragment.REQUEST_ACTION,\n                result\n            )\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        return inflater.inflate(R.layout.multi_select_list, container, false)\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        appBarLayout = requireActivity().findViewById(org.odk.collect.androidshared.R.id.appBarLayout)\n\n        val binding = MultiSelectListBinding.bind(view)\n        onViewCreated(binding)\n\n        list = binding.list\n        list.layoutManager = LinearLayoutManager(requireContext())\n        val adapter = MultiSelectAdapter(\n            multiSelectViewModel,\n            viewHolderFactory\n        )\n        list.adapter = adapter\n\n        multiSelectViewModel.getData().observe(viewLifecycleOwner) {\n            adapter.data = it\n            binding.empty.isVisible = it.isEmpty()\n            binding.buttons.isVisible = it.isNotEmpty()\n        }\n        multiSelectViewModel.getSelected().observe(viewLifecycleOwner) {\n            adapter.selected = it\n        }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        appBarLayout?.setLiftOnScrollTargetView(list)\n    }\n}\n"
  },
  {
    "path": "lists/src/main/java/org/odk/collect/lists/selects/MultiSelectViewModel.kt",
    "content": "package org.odk.collect.lists.selects\n\nimport android.widget.Button\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.map\nimport androidx.lifecycle.viewmodel.CreationExtras\nimport org.odk.collect.androidshared.livedata.LiveDataUtils\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\n\n/**\n * A `ViewModel` for holding state around selected items (identified by `Long` ids). This can\n * optionally also handle the data for the list which allows the `ViewModel` to perform a select\n * all and determine whether all items are selected or not.\n */\nclass MultiSelectViewModel<T>(\n    private val data: LiveData<List<SelectItem<T>>> = MutableLiveData(emptyList())\n) : ViewModel() {\n\n    private val selected = MutableNonNullLiveData(emptySet<String>())\n    private val isAllSelected = LiveDataUtils.combine(data, selected).map { (data, selected) ->\n        if (data != null && selected != null) {\n            data.isNotEmpty() && data.size == selected.size\n        } else {\n            false\n        }\n    }\n\n    fun getData(): LiveData<List<SelectItem<T>>> {\n        return data\n    }\n\n    fun select(item: String) {\n        updateSelected(selected.value + item)\n    }\n\n    fun getSelected(): NonNullLiveData<Set<String>> {\n        return selected\n    }\n\n    fun unselect(item: String) {\n        updateSelected(selected.value - item)\n    }\n\n    fun unselectAll() {\n        updateSelected(emptySet())\n    }\n\n    fun selectAll() {\n        updateSelected(data.value?.map { it.id }?.toSet() ?: emptySet())\n    }\n\n    fun isAllSelected(): LiveData<Boolean> {\n        return isAllSelected\n    }\n\n    fun toggle(item: String) {\n        if (selected.value.contains(item)) {\n            unselect(item)\n        } else {\n            select(item)\n        }\n    }\n\n    private fun updateSelected(new: Set<String>) {\n        selected.value = new\n    }\n\n    class Factory<T>(private val data: LiveData<List<SelectItem<T>>> = MutableLiveData(emptyList())) :\n        ViewModelProvider.Factory {\n        @Suppress(\"UNCHECKED_CAST\")\n        override fun <VM : ViewModel> create(modelClass: Class<VM>, extras: CreationExtras): VM {\n            return MultiSelectViewModel(data) as VM\n        }\n    }\n}\n\nfun updateSelectAll(button: Button, itemCount: Int, selectedCount: Int): Boolean {\n    val allSelected = itemCount > 0 && selectedCount == itemCount\n\n    if (allSelected) {\n        button.setText(org.odk.collect.strings.R.string.clear_all)\n    } else {\n        button.setText(org.odk.collect.strings.R.string.select_all)\n    }\n\n    return allSelected\n}\n"
  },
  {
    "path": "lists/src/main/java/org/odk/collect/lists/selects/SelectItem.kt",
    "content": "package org.odk.collect.lists.selects\n\ndata class SelectItem<T>(val id: String, val item: T)\n"
  },
  {
    "path": "lists/src/main/java/org/odk/collect/lists/selects/SingleSelectViewModel.kt",
    "content": "package org.odk.collect.lists.selects\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.map\nimport org.odk.collect.androidshared.livedata.LiveDataUtils\n\nclass SingleSelectViewModel(\n    selected: String?,\n    data: LiveData<List<SelectItem<*>>>\n) : ViewModel() {\n\n    private val _selected = MutableLiveData<String?>(selected)\n    private val selected = LiveDataUtils.combine(_selected, data).map { (selected, data) ->\n        if (selected != null && data != null) {\n            selected.takeIf { id -> data.any { it.id == id } }\n        } else {\n            null\n        }\n    }\n\n    fun getSelected(): LiveData<String?> {\n        return selected\n    }\n\n    fun select(item: String) {\n        _selected.value = item\n    }\n\n    fun clear() {\n        _selected.value = null\n    }\n}\n"
  },
  {
    "path": "lists/src/main/res/layout/empty_list_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"@dimen/margin_standard\"\n    android:background=\"?colorSurface\">\n\n    <ImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:alpha=\"@dimen/medium_emphasis\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintBottom_toTopOf=\"@+id/title\"\n        app:layout_constraintVertical_chainStyle=\"packed\"\n        tools:src=\"@drawable/ic_baseline_qr_code_2_add_24\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"?textAppearanceLabelExtraLarge\"\n        android:fontFamily=\"sans-serif-medium\"\n        android:layout_marginTop=\"@dimen/margin_large\"\n        android:gravity=\"center_horizontal\"\n        android:textColor=\"@color/color_on_surface_medium_emphasis\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/icon\"\n        app:layout_constraintBottom_toTopOf=\"@+id/subtitle\"\n        tools:text=\"Nothing available to display\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/subtitle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"?textAppearanceLabelExtraLarge\"\n        android:layout_marginTop=\"@dimen/margin_large\"\n        android:gravity=\"center_horizontal\"\n        android:textColor=\"@color/color_on_surface_medium_emphasis\"\n        app:layout_constraintTop_toBottomOf=\"@id/title\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        tools:text=\"When you save as draft, forms will appear here\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "lists/src/main/res/layout/multi_select_controls_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!--\n    Using a LinearLayout rather than ConstraintLayout allows the items to grow to the height\n    of the tallest item (not possible with the latter currently).\n-->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    style=\"@style/Widget.AndroidShared.ButtonBar\">\n\n    <com.google.android.material.button.MaterialButton\n        android:id=\"@+id/select_all\"\n        style=\"?materialButtonOutlinedStyle\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_weight=\"1\"\n        android:text=\"@string/select_all\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.DoubleClickSafeMaterialButton\n        android:id=\"@+id/action\"\n        style=\"?materialButtonStyle\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_marginEnd=\"@dimen/margin_standard\"\n        android:layout_weight=\"1\" />\n</LinearLayout>\n"
  },
  {
    "path": "lists/src/main/res/layout/multi_select_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintBottom_toTopOf=\"@id/buttons\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <org.odk.collect.lists.EmptyListView\n        android:id=\"@+id/empty\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:visibility=\"gone\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/buttons\"\n        android:name=\"org.odk.collect.lists.selects.MultiSelectControlsFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "lists/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"EmptyListView\">\n        <attr name=\"icon\" />\n        <attr name=\"title\" format=\"string\" />\n        <attr name=\"subtitle\" format=\"string\" />\n    </declare-styleable>s\n</resources>\n"
  },
  {
    "path": "lists/src/test/java/org/odk/collect/lists/EmptyListViewTest.kt",
    "content": "package org.odk.collect.lists\n\nimport android.app.Application\nimport android.graphics.drawable.VectorDrawable\nimport android.widget.ImageView\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport com.google.android.material.textview.MaterialTextView\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.icons.R.drawable\nimport org.robolectric.Robolectric\n\n@RunWith(AndroidJUnit4::class)\nclass EmptyListViewTest {\n    private val context: Application =\n        ApplicationProvider.getApplicationContext<Application>().also {\n            it.setTheme(com.google.android.material.R.style.Theme_Material3_Light)\n        }\n\n    @Test\n    fun `when icon attribute is not used then it is null`() {\n        val emptyListView = EmptyListView(context)\n\n        assertThat(emptyListView.findViewById<ImageView>(R.id.icon).drawable, equalTo(null))\n    }\n\n    @Test\n    fun `when icon attribute is used then it is set correctly`() {\n        val attrs = Robolectric.buildAttributeSet().addAttribute(R.attr.icon, \"@drawable/ic_baseline_warning_24\").build()\n        val emptyListView = EmptyListView(context, attrs)\n\n        assertThat(\n            (emptyListView.findViewById<ImageView>(R.id.icon).drawable as VectorDrawable).toBitmap().sameAs((context.getDrawable(drawable.ic_baseline_warning_24) as VectorDrawable).toBitmap()),\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `when title attribute is not used then it is empty`() {\n        val emptyListView = EmptyListView(context)\n\n        assertThat(emptyListView.findViewById<MaterialTextView>(R.id.title).text, equalTo(\"\"))\n    }\n\n    @Test\n    fun `when title attribute is used then it is set correctly`() {\n        val attrs = Robolectric.buildAttributeSet().addAttribute(R.attr.title, \"blah\").build()\n        val emptyListView = EmptyListView(context, attrs)\n\n        assertThat(emptyListView.findViewById<MaterialTextView>(R.id.title).text, equalTo(\"blah\"))\n    }\n\n    @Test\n    fun `when subtitle attribute is not used then it is empty`() {\n        val emptyListView = EmptyListView(context)\n\n        assertThat(emptyListView.findViewById<MaterialTextView>(R.id.subtitle).text, equalTo(\"\"))\n    }\n\n    @Test\n    fun `when subtitle attribute is used then it is set correctly`() {\n        val attrs = Robolectric.buildAttributeSet().addAttribute(R.attr.subtitle, \"blah\").build()\n        val emptyListView = EmptyListView(context, attrs)\n\n        assertThat(emptyListView.findViewById<MaterialTextView>(R.id.subtitle).text, equalTo(\"blah\"))\n    }\n\n    @Test\n    fun `icon can be set programmatically`() {\n        val emptyListView = EmptyListView(context)\n        emptyListView.setIcon(drawable.ic_baseline_warning_24)\n\n        assertThat(\n            (emptyListView.findViewById<ImageView>(R.id.icon).drawable as VectorDrawable).toBitmap().sameAs((context.getDrawable(drawable.ic_baseline_warning_24) as VectorDrawable).toBitmap()),\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `title can be set programmatically`() {\n        val emptyListView = EmptyListView(context)\n        emptyListView.setTitle(\"blah\")\n\n        assertThat(emptyListView.findViewById<MaterialTextView>(R.id.title).text, equalTo(\"blah\"))\n    }\n\n    @Test\n    fun `subtitle can be set programmatically`() {\n        val emptyListView = EmptyListView(context)\n        emptyListView.setSubtitle(\"blah\")\n\n        assertThat(emptyListView.findViewById<MaterialTextView>(R.id.subtitle).text, equalTo(\"blah\"))\n    }\n}\n"
  },
  {
    "path": "lists/src/test/java/org/odk/collect/lists/RobolectricApplication.kt",
    "content": "package org.odk.collect.lists\n\nimport android.app.Application\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\nclass RobolectricApplication : Application() {\n    override fun onCreate() {\n        super.onCreate()\n\n        // We don't want any clicks to be blocked\n        MultiClickGuard.test = true\n    }\n}\n"
  },
  {
    "path": "lists/src/test/java/org/odk/collect/lists/selects/MultiSelectAdapterTest.kt",
    "content": "package org.odk.collect.lists.selects\n\nimport android.content.Context\nimport android.widget.FrameLayout\nimport androidx.lifecycle.MutableLiveData\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.RecyclerView.ViewHolder\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.lists.selects.support.TextAndCheckBoxViewHolder\n\n@RunWith(AndroidJUnit4::class)\nclass MultiSelectAdapterTest {\n\n    private val context = ApplicationProvider.getApplicationContext<Context>()\n\n    @Test\n    fun `selected items are checked`() {\n        val data = MutableLiveData(listOf(SelectItem<Long>(\"1\", 1), SelectItem<Long>(\"2\", 2)))\n        val multiSelectViewModel = MultiSelectViewModel(data)\n        multiSelectViewModel.select(\"1\")\n\n        val adapter = MultiSelectAdapter(multiSelectViewModel) {\n            TextAndCheckBoxViewHolder<Long>(it.context)\n        }\n\n        adapter.data = multiSelectViewModel.getData().value!!\n        adapter.selected = multiSelectViewModel.getSelected().value\n\n        val holders = createAndBindList(adapter)\n        assertThat(holders.size, equalTo(2))\n        assertThat(holders[0].view.checkBox.isChecked, equalTo(true))\n        assertThat(holders[1].view.checkBox.isChecked, equalTo(false))\n    }\n\n    @Test\n    fun `checking an item selects it`() {\n        val data = MutableLiveData(listOf(SelectItem<Long>(\"1\", 1), SelectItem<Long>(\"2\", 2)))\n        val multiSelectViewModel = MultiSelectViewModel(data)\n\n        val adapter = MultiSelectAdapter(multiSelectViewModel) {\n            TextAndCheckBoxViewHolder<Long>(it.context)\n        }\n\n        adapter.data = multiSelectViewModel.getData().value!!\n        adapter.selected = multiSelectViewModel.getSelected().value\n\n        val holders = createAndBindList(adapter)\n        holders[0].view.checkBox.performClick()\n        assertThat(multiSelectViewModel.getSelected().value, equalTo(setOf(\"1\")))\n    }\n\n    @Test\n    fun `clicking an item selects it`() {\n        val data = MutableLiveData(listOf(SelectItem<Long>(\"1\", 1), SelectItem<Long>(\"2\", 2)))\n        val multiSelectViewModel = MultiSelectViewModel(data)\n\n        val adapter = MultiSelectAdapter(multiSelectViewModel) {\n            TextAndCheckBoxViewHolder<Long>(it.context)\n        }\n\n        adapter.data = multiSelectViewModel.getData().value!!\n        adapter.selected = multiSelectViewModel.getSelected().value\n\n        val holders = createAndBindList(adapter)\n        holders[0].view.performClick()\n        assertThat(multiSelectViewModel.getSelected().value, equalTo(setOf(\"1\")))\n    }\n\n    @Test\n    fun `unchecking an item selects it`() {\n        val data = MutableLiveData(listOf(SelectItem<Long>(\"1\", 1), SelectItem<Long>(\"2\", 2)))\n        val multiSelectViewModel = MultiSelectViewModel(data)\n        multiSelectViewModel.select(\"1\")\n\n        val adapter = MultiSelectAdapter(multiSelectViewModel) {\n            TextAndCheckBoxViewHolder<Long>(it.context)\n        }\n\n        adapter.data = multiSelectViewModel.getData().value!!\n        adapter.selected = multiSelectViewModel.getSelected().value\n\n        val holders = createAndBindList(adapter)\n        holders[0].view.checkBox.performClick()\n        assertThat(multiSelectViewModel.getSelected().value, equalTo(setOf()))\n    }\n\n    private fun <T : ViewHolder> createAndBindList(adapter: RecyclerView.Adapter<T>): List<T> {\n        return 0.rangeUntil(adapter.itemCount).map { position ->\n            adapter.onCreateViewHolder(FrameLayout(context), position).also { holder ->\n                adapter.onBindViewHolder(holder, position)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "lists/src/test/java/org/odk/collect/lists/selects/MultiSelectControlsFragmentTest.kt",
    "content": "package org.odk.collect.lists.selects\n\nimport androidx.lifecycle.MutableLiveData\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.assertion.ViewAssertions.doesNotExist\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.espresso.matcher.ViewMatchers.isEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.androidtest.getOrAwaitValue\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.strings.R.string\n\n@RunWith(AndroidJUnit4::class)\nclass MultiSelectControlsFragmentTest {\n\n    private val data = MutableLiveData(listOf(SelectItem(\"1\", null), SelectItem(\"2\", null)))\n    private val multiSelectViewModel = MultiSelectViewModel(data)\n\n    @get:Rule\n    val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule(\n        FragmentFactoryBuilder()\n            .forClass(MultiSelectControlsFragment::class) {\n                MultiSelectControlsFragment(\"Action\", multiSelectViewModel)\n            }.build()\n    )\n\n    @Test\n    fun `clicking select all selects all items`() {\n        fragmentScenarioLauncherRule.launchInContainer(MultiSelectControlsFragment::class.java)\n\n        onView(withText(string.select_all)).perform(click())\n        assertThat(multiSelectViewModel.getSelected().getOrAwaitValue(), equalTo(setOf(\"1\", \"2\")))\n    }\n\n    @Test\n    fun `clicking clear all unselects all items`() {\n        fragmentScenarioLauncherRule.launchInContainer(MultiSelectControlsFragment::class.java)\n\n        onView(withText(string.select_all)).perform(click())\n        onView(withText(string.select_all)).check(doesNotExist())\n\n        onView(withText(string.clear_all)).perform(click())\n        assertThat(multiSelectViewModel.getSelected().getOrAwaitValue(), equalTo(emptySet()))\n    }\n\n    @Test\n    fun `action is disabled when nothing is selected`() {\n        fragmentScenarioLauncherRule.launchInContainer(MultiSelectControlsFragment::class.java)\n        onView(withText(\"Action\")).check(matches(not(isEnabled())))\n\n        multiSelectViewModel.select(\"1\")\n        onView(withText(\"Action\")).check(matches(isEnabled()))\n    }\n}\n"
  },
  {
    "path": "lists/src/test/java/org/odk/collect/lists/selects/MultiSelectListFragmentTest.kt",
    "content": "package org.odk.collect.lists.selects\n\nimport androidx.lifecycle.MutableLiveData\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.ViewMatchers.isChecked\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.Matchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.lists.R\nimport org.odk.collect.lists.selects.support.TextAndCheckBoxView\nimport org.odk.collect.lists.selects.support.TextAndCheckBoxViewHolder\nimport org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView\nimport org.odk.collect.testshared.ViewActions.clickOnItemWith\nimport org.odk.collect.testshared.ViewMatchers.recyclerView\n\n@RunWith(AndroidJUnit4::class)\nclass MultiSelectListFragmentTest {\n\n    private val data = MutableLiveData<List<SelectItem<String>>>(emptyList())\n    private val multiSelectViewModel = MultiSelectViewModel(data)\n\n    @get:Rule\n    val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule(\n        FragmentFactoryBuilder()\n            .forClass(MultiSelectListFragment::class) {\n                MultiSelectListFragment(\n                    \"Action\",\n                    multiSelectViewModel,\n                    { parent -> TextAndCheckBoxViewHolder(parent.context) }\n                )\n            }.build()\n    )\n\n    @Test\n    fun `empty message shows when there are no forms`() {\n        fragmentScenarioLauncherRule.launchInContainer(MultiSelectListFragment::class.java)\n        onView(withId(R.id.empty)).check(matches(isDisplayed()))\n\n        data.value = listOf(SelectItem(\"1\", \"Blah\"))\n        onView(withId(R.id.empty)).check(matches(not(isDisplayed())))\n    }\n\n    @Test\n    fun `bottom buttons are hidden when there are no forms`() {\n        fragmentScenarioLauncherRule.launchInContainer(MultiSelectListFragment::class.java)\n        onView(withId(R.id.buttons)).check(matches(not(isDisplayed())))\n\n        data.value = listOf(SelectItem(\"1\", \"Blah\"))\n        onView(withId(R.id.buttons)).check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `recreating maintains selection`() {\n        val scenario =\n            fragmentScenarioLauncherRule.launchInContainer(MultiSelectListFragment::class.java)\n        data.value = listOf(SelectItem(\"1\", \"Blah 1\"), SelectItem(\"1\", \"Blah 2\"))\n\n        onView(recyclerView()).perform(clickOnItemWith(withText(\"Blah 2\")))\n\n        scenario.recreate()\n        onView(withRecyclerView(R.id.list).atPositionOnView(1, TextAndCheckBoxView.TEXT_VIEW_ID))\n            .check(matches(withText(\"Blah 2\")))\n        onView(withRecyclerView(R.id.list).atPositionOnView(1, TextAndCheckBoxView.CHECK_BOX_ID))\n            .check(matches(isChecked()))\n    }\n}\n"
  },
  {
    "path": "lists/src/test/java/org/odk/collect/lists/selects/MultiSelectViewModelTest.kt",
    "content": "package org.odk.collect.lists.selects\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.lifecycle.MutableLiveData\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.odk.collect.androidtest.getOrAwaitValue\n\nclass MultiSelectViewModelTest {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    @Test\n    fun `getSelected returns selected`() {\n        val viewModel = MultiSelectViewModel<Any>()\n        viewModel.select(\"1\")\n        viewModel.select(\"11\")\n\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf(\"1\", \"11\")))\n    }\n\n    @Test\n    fun `getSelected does not return unselected items`() {\n        val viewModel = MultiSelectViewModel<Any>()\n        viewModel.select(\"1\")\n        viewModel.select(\"11\")\n        viewModel.unselect(\"1\")\n\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf(\"11\")))\n    }\n\n    @Test\n    fun `unselectAll unselects all items`() {\n        val viewModel = MultiSelectViewModel<Any>()\n        viewModel.select(\"1\")\n        viewModel.select(\"11\")\n        viewModel.unselectAll()\n\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(emptySet()))\n    }\n\n    @Test\n    fun `toggle changes item back and forth`() {\n        val viewModel = MultiSelectViewModel<Any>()\n\n        viewModel.toggle(\"1\")\n        viewModel.toggle(\"11\")\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf(\"1\", \"11\")))\n\n        viewModel.toggle(\"11\")\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf(\"1\")))\n    }\n\n    @Test\n    fun `selectAll selects all data`() {\n        val data = MutableLiveData(listOf(SelectItem<Long>(\"1\", 1), SelectItem<Long>(\"2\", 2)))\n        val viewModel = MultiSelectViewModel(data)\n\n        viewModel.selectAll()\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(setOf(\"1\", \"2\")))\n    }\n\n    @Test\n    fun `isAllSelected is true when all data selected`() {\n        val data = MutableLiveData(listOf(SelectItem<Long>(\"1\", 1), SelectItem<Long>(\"2\", 2)))\n        val viewModel = MultiSelectViewModel(data)\n        assertThat(viewModel.isAllSelected().getOrAwaitValue(), equalTo(false))\n\n        viewModel.select(\"1\")\n        assertThat(viewModel.isAllSelected().getOrAwaitValue(), equalTo(false))\n\n        viewModel.select(\"2\")\n        assertThat(viewModel.isAllSelected().getOrAwaitValue(), equalTo(true))\n    }\n\n    @Test\n    fun `isAllSelected returns false when no data`() {\n        val data = MutableLiveData(listOf<SelectItem<Any>>())\n        val viewModel = MultiSelectViewModel(data)\n        assertThat(viewModel.isAllSelected().getOrAwaitValue(), equalTo(false))\n    }\n}\n"
  },
  {
    "path": "lists/src/test/java/org/odk/collect/lists/selects/SingleSelectViewModelTest.kt",
    "content": "package org.odk.collect.lists.selects\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Rule\nimport org.junit.Test\nimport org.odk.collect.androidtest.getOrAwaitValue\n\nclass SingleSelectViewModelTest {\n\n    @get:Rule\n    val instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    @Test\n    fun `nothing is selected on viewmodel initialization if selected item id is null`() {\n        val selected = null\n        val data: LiveData<List<SelectItem<*>>> = MutableLiveData(listOf(SelectItem(\"1\", 1), SelectItem(\"2\", 2)))\n        val viewModel = SingleSelectViewModel(selected, data)\n\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun `nothing is selected on viewmodel initialization if selected item id is not null but there is no item witch matching id`() {\n        val selected = \"0\"\n        val data: LiveData<List<SelectItem<*>>> = MutableLiveData(listOf(SelectItem(\"1\", 1), SelectItem(\"2\", 2)))\n        val viewModel = SingleSelectViewModel(selected, data)\n\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(null))\n    }\n\n    @Test\n    fun `proper item is selected on viewmodel initialization if selected item id is not null`() {\n        val selected = \"1\"\n        val data: LiveData<List<SelectItem<*>>> = MutableLiveData(listOf(SelectItem(\"1\", 1), SelectItem(\"2\", 2)))\n        val viewModel = SingleSelectViewModel(selected, data)\n\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(\"1\"))\n    }\n\n    @Test\n    fun `getSelected returns selected item id`() {\n        val selected = null\n        val data: LiveData<List<SelectItem<*>>> = MutableLiveData(listOf(SelectItem(\"1\", 1), SelectItem(\"2\", 2)))\n        val viewModel = SingleSelectViewModel(selected, data)\n\n        viewModel.select(\"1\")\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(\"1\"))\n\n        viewModel.select(\"2\")\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(\"2\"))\n    }\n\n    @Test\n    fun `clear unselects selected item`() {\n        val selected = null\n        val data: LiveData<List<SelectItem<*>>> = MutableLiveData(listOf(SelectItem(\"1\", 1), SelectItem(\"2\", 2)))\n        val viewModel = SingleSelectViewModel(selected, data)\n\n        viewModel.select(\"1\")\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(\"1\"))\n        viewModel.clear()\n        assertThat(viewModel.getSelected().getOrAwaitValue(), equalTo(null))\n    }\n}\n"
  },
  {
    "path": "lists/src/test/java/org/odk/collect/lists/selects/support/TextAndCheckboxViewHolder.kt",
    "content": "package org.odk.collect.lists.selects.support\n\nimport android.content.Context\nimport android.widget.CheckBox\nimport android.widget.FrameLayout\nimport android.widget.TextView\nimport org.odk.collect.lists.selects.MultiSelectAdapter\n\nclass TextAndCheckBoxView(context: Context) : FrameLayout(context) {\n\n    val textView = TextView(context).also {\n        it.id = TEXT_VIEW_ID\n        addView(it)\n    }\n\n    val checkBox = CheckBox(context).also {\n        it.id = CHECK_BOX_ID\n        addView(it)\n    }\n\n    companion object {\n        const val TEXT_VIEW_ID = 101\n        const val CHECK_BOX_ID = 102\n    }\n}\n\nclass TextAndCheckBoxViewHolder<T>(context: Context) :\n    MultiSelectAdapter.ViewHolder<T>(TextAndCheckBoxView(context)) {\n\n    val view = itemView as TextAndCheckBoxView\n\n    override fun setItem(item: T) {\n        view.textView.text = item.toString()\n    }\n\n    override fun getCheckbox(): CheckBox {\n        return view.checkBox\n    }\n}\n"
  },
  {
    "path": "lists/src/test/resources/robolectric.properties",
    "content": "application=org.odk.collect.lists.RobolectricApplication\nsdk=33"
  },
  {
    "path": "location/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "location/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n    namespace = \"org.odk.collect.location\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":androidshared\"))\n    implementation(project(\":icons\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":analytics\"))\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.playServicesLocation)\n    implementation(libs.timber)\n    implementation(libs.androidxAppcompat)\n    implementation(libs.dagger)\n    ksp(libs.daggerCompiler)\n\n    testImplementation(project(\":androidtest\"))\n    testImplementation(project(\":test-shared\"))\n    testImplementation(project(\":service-test\"))\n    testImplementation(libs.junit)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.androidxTestExtJunit)\n}\n"
  },
  {
    "path": "location/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_LOCATION\" />\n\n    <application>\n        <service\n            android:name=\".tracker.LocationTrackerService\"\n            android:foregroundServiceType=\"location\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/AndroidLocationClient.java",
    "content": "package org.odk.collect.location;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.location.Location;\nimport android.location.LocationManager;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.google.android.gms.location.LocationListener;\n\nimport timber.log.Timber;\n\n/**\n * An implementation of {@link LocationClient} that uses the existing\n * Android Location Services (LocationManager) to retrieve the User's\n * location.\n * <p>\n * Should be used whenever there Google Play Services is not present.\n * <p>\n * Package-private, use {@link LocationClientProvider} to retrieve the correct\n * {@link LocationClient}.\n */\n@SuppressLint(\"MissingPermission\") // Permission checks for location services handled in components that use this class\npublic class AndroidLocationClient\n        extends BaseLocationClient\n        implements android.location.LocationListener {\n\n    @Nullable\n    private LocationListener locationListener;\n\n    private boolean isConnected;\n    private boolean retainMockAccuracy;\n\n    /**\n     * Constructs a new AndroidLocationClient with the provided Context.\n     * This Constructor should be used normally.\n     *\n     * @param context The Context where the AndroidLocationClient will be running.\n     */\n    public AndroidLocationClient(@NonNull Context context) {\n        this((LocationManager) context.getSystemService(Context.LOCATION_SERVICE));\n    }\n\n    /**\n     * Constructs a new AndroidLocationClient with the provided LocationManager.\n     * This Constructor should only be used for testing.\n     *\n     * @param locationManager The LocationManager to retrieve locations from.\n     */\n    public AndroidLocationClient(@NonNull LocationManager locationManager) {\n        super(locationManager);\n    }\n\n    // LocationClient:\n\n    @Override\n    public void start(LocationClientListener listener) {\n        setListener(listener);\n        if (getProvider() == null) {\n            if (getListener() != null) {\n                getListener().onClientStartFailure();\n            }\n\n            return;\n        }\n\n        isConnected = true;\n        if (getListener() != null) {\n            getListener().onClientStart();\n        }\n    }\n\n    @Override\n    public void stop() {\n        // Implementations of LocationClient are expected to call this:\n        stopLocationUpdates();\n        isConnected = false;\n\n        if (getListener() != null) {\n            getListener().onClientStop();\n        }\n        setListener(null);\n    }\n\n    @Override\n    public void requestLocationUpdates(@NonNull LocationListener locationListener) {\n        if (!isConnected) {\n            // This is to maintain expected behavior across LocationClient implementations.\n            return;\n        }\n\n        if (!isMonitoringLocation()) {\n            getLocationManager().requestLocationUpdates(getProvider(), 0, 0, this);\n        }\n\n        this.locationListener = locationListener;\n    }\n\n    @Override\n    public void stopLocationUpdates() {\n        if (!isMonitoringLocation()) {\n            return;\n        }\n\n        getLocationManager().removeUpdates(this);\n        this.locationListener = null;\n    }\n\n    @Override\n    public void setRetainMockAccuracy(boolean retainMockAccuracy) {\n        this.retainMockAccuracy = retainMockAccuracy;\n    }\n\n    @Override\n    public Location getLastLocation() {\n        String provider = getProvider();\n        if (provider != null) {\n            Location lastKnownLocation = getLocationManager().getLastKnownLocation(provider);\n            return sanitizeLocation(lastKnownLocation);\n        }\n\n        return null;\n    }\n\n    @Override\n    public boolean isMonitoringLocation() {\n        return locationListener != null;\n    }\n\n    @Override\n    public void setUpdateInterval(long updateInterval) {\n        // Do nothing.\n    }\n\n    // LocationListener:\n\n    @Override\n    public void onLocationChanged(Location location) {\n        Timber.i(\"Location changed: %s\", location.toString());\n\n        if (locationListener != null) {\n            locationListener.onLocationChanged(sanitizeLocation(location));\n        }\n    }\n\n    @Override\n    public void onStatusChanged(String provider, int status, Bundle extras) {\n\n    }\n\n    @Override\n    public void onProviderEnabled(String provider) {\n\n    }\n\n    @Override\n    public void onProviderDisabled(String provider) {\n\n    }\n\n    private Location sanitizeLocation(Location location) {\n        return LocationUtils.sanitizeAccuracy(location, retainMockAccuracy);\n    }\n}\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/BaseLocationClient.kt",
    "content": "package org.odk.collect.location\n\nimport android.location.LocationManager\nimport org.odk.collect.location.LocationClient.LocationClientListener\n\n/**\n * An abstract base LocationClient class that provides some shared functionality for determining\n * whether or not certain Location providers are available.\n *\n * Constructs a new BaseLocationClient with the provided LocationManager.\n * This Constructor is only accessible to child classes.\n *\n * @param locationManager The LocationManager to retrieve locations from.\n */\nabstract class BaseLocationClient(protected val locationManager: LocationManager?) :\n    LocationClient {\n\n    private var listener: LocationClientListener? = null\n    private var priority = LocationClient.Priority.PRIORITY_HIGH_ACCURACY\n\n    override fun isLocationAvailable(): Boolean {\n        return getProvider() != null\n    }\n\n    protected fun getProvider(): String? {\n        var provider = LocationManager.PASSIVE_PROVIDER\n        var backupProvider: String? = null\n\n        when (priority) {\n            LocationClient.Priority.PRIORITY_HIGH_ACCURACY -> {\n                provider = LocationManager.GPS_PROVIDER\n                backupProvider = LocationManager.NETWORK_PROVIDER\n            }\n            LocationClient.Priority.PRIORITY_BALANCED_POWER_ACCURACY -> {\n                provider = LocationManager.NETWORK_PROVIDER\n                backupProvider = LocationManager.GPS_PROVIDER\n            }\n            LocationClient.Priority.PRIORITY_LOW_POWER -> {\n                provider = LocationManager.NETWORK_PROVIDER\n                backupProvider = LocationManager.PASSIVE_PROVIDER\n            }\n            LocationClient.Priority.PRIORITY_NO_POWER -> {\n                provider = LocationManager.PASSIVE_PROVIDER\n                backupProvider = null\n            }\n        }\n\n        return getProviderIfEnabled(provider, backupProvider)\n    }\n\n    private fun getProviderIfEnabled(provider: String, backupProvider: String?): String? {\n        if (hasProvider(provider)) {\n            return provider\n        } else if (hasProvider(backupProvider)) {\n            return backupProvider\n        }\n        return null\n    }\n\n    private fun hasProvider(provider: String?): Boolean {\n        if (provider == null) {\n            return false\n        }\n\n        val enabledProviders = locationManager!!.getProviders(true)\n        for (enabledProvider in enabledProviders) {\n            if (enabledProvider.equals(provider, ignoreCase = true)) {\n                return true\n            }\n        }\n\n        return false\n    }\n\n    override fun setPriority(priority: LocationClient.Priority) {\n        this.priority = priority\n    }\n\n    protected fun getPriority(): LocationClient.Priority {\n        return priority\n    }\n\n    override fun setListener(locationClientListener: LocationClientListener?) {\n        listener = locationClientListener\n    }\n\n    fun getListener(): LocationClientListener? {\n        return listener\n    }\n}\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/DaggerSetup.kt",
    "content": "package org.odk.collect.location\n\nimport dagger.Component\nimport dagger.Module\nimport dagger.Provides\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.location.tracker.LocationTrackerService\nimport javax.inject.Singleton\n\ninterface LocationDependencyComponentProvider {\n    val locationDependencyComponent: LocationDependencyComponent\n}\n\n@Component(modules = [LocationDependencyModule::class])\n@Singleton\ninterface LocationDependencyComponent {\n    fun inject(locationTrackerService: LocationTrackerService)\n}\n\n@Module\nopen class LocationDependencyModule {\n\n    @Provides\n    open fun providesUniqueIdGenerator(): UniqueIdGenerator {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n}\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/GoogleFusedLocationClient.kt",
    "content": "package org.odk.collect.location\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.location.Location\nimport android.location.LocationManager\nimport android.os.Looper\nimport com.google.android.gms.location.FusedLocationProviderClient\nimport com.google.android.gms.location.LocationCallback\nimport com.google.android.gms.location.LocationListener\nimport com.google.android.gms.location.LocationRequest\nimport com.google.android.gms.location.LocationResult\nimport com.google.android.gms.location.LocationServices\nimport org.odk.collect.location.LocationClient.LocationClientListener\nimport org.odk.collect.location.LocationUtils.sanitizeAccuracy\n\n/**\n * An implementation of [LocationClient] that uses Google Play Services to retrieve the user's location.\n *\n * Should be used whenever Google Play Services is present. In general, use\n * [LocationClientProvider] to retrieve a configured [LocationClient].\n */\nclass GoogleFusedLocationClient(\n    private val fusedLocationProviderClientWrapper: FusedLocationProviderClientWrapper,\n    locationManager: LocationManager\n) : BaseLocationClient(locationManager), LocationListener {\n\n    constructor(context: Context) : this(\n        FusedLocationProviderClientWrapper(LocationServices.getFusedLocationProviderClient(context)),\n        (context.getSystemService(Context.LOCATION_SERVICE) as LocationManager)\n    )\n\n    private var locationListener: LocationListener? = null\n    private var updateInterval = DEFAULT_UPDATE_INTERVAL\n    private var retainMockAccuracy = false\n\n    override fun start(listener: LocationClientListener) {\n        fusedLocationProviderClientWrapper.start(this)\n        setListener(listener)\n        listener.onClientStart()\n    }\n\n    override fun stop() {\n        stopLocationUpdates()\n        getListener()?.onClientStop()\n        setListener(null)\n    }\n\n    override fun requestLocationUpdates(locationListener: LocationListener) {\n        if (!isMonitoringLocation) {\n            fusedLocationProviderClientWrapper.requestLocationUpdates(createLocationRequest())\n        }\n        this.locationListener = locationListener\n    }\n\n    override fun stopLocationUpdates() {\n        if (isMonitoringLocation) {\n            fusedLocationProviderClientWrapper.removeLocationUpdates()\n            locationListener = null\n        }\n    }\n\n    override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) {\n        this.retainMockAccuracy = retainMockAccuracy\n    }\n\n    override fun getLastLocation(): Location? {\n        return sanitizeAccuracy(fusedLocationProviderClientWrapper.getLastLocation(), retainMockAccuracy)\n    }\n\n    override fun isMonitoringLocation(): Boolean {\n        return locationListener != null\n    }\n\n    override fun setUpdateInterval(updateInterval: Long) {\n        this.updateInterval = updateInterval\n    }\n\n    override fun onLocationChanged(location: Location) {\n        sanitizeAccuracy(location, retainMockAccuracy)?.let {\n            locationListener?.onLocationChanged(it)\n        }\n    }\n\n    private fun createLocationRequest(): LocationRequest {\n        return LocationRequest.create().apply {\n            priority = this@GoogleFusedLocationClient.getPriority().value\n            interval = updateInterval\n            fastestInterval = updateInterval\n        }\n    }\n\n    companion object {\n        /**\n         * The default requested time between location updates, in milliseconds.\n         */\n        private const val DEFAULT_UPDATE_INTERVAL: Long = 5000\n    }\n}\n\n@SuppressLint(\"MissingPermission\") // Permission checks for location services handled in components that use this class\nclass FusedLocationProviderClientWrapper(private val fusedLocationProviderClient: FusedLocationProviderClient) {\n    private lateinit var listener: LocationListener\n    private var lastLocation: Location? = null\n\n    private var locationCallback: LocationCallback = object : LocationCallback() {\n        override fun onLocationResult(locationResult: LocationResult) {\n            for (location in locationResult.locations) {\n                listener.onLocationChanged(location)\n            }\n        }\n    }\n\n    fun start(listener: LocationListener) {\n        this.listener = listener\n        fusedLocationProviderClient\n            .lastLocation\n            .addOnSuccessListener { location: Location? ->\n                lastLocation = location\n            }\n    }\n\n    fun requestLocationUpdates(locationRequest: LocationRequest) {\n        fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())\n    }\n\n    fun removeLocationUpdates() {\n        fusedLocationProviderClient.removeLocationUpdates(locationCallback)\n    }\n\n    fun getLastLocation(): Location? {\n        return lastLocation\n    }\n}\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/Location.kt",
    "content": "package org.odk.collect.location\n\ndata class Location(\n    val latitude: Double,\n    val longitude: Double,\n    val altitude: Double = 0.0,\n    val accuracy: Float = 0.0f\n)\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/LocationClient.java",
    "content": "package org.odk.collect.location;\n\nimport android.location.Location;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.google.android.gms.location.LocationListener;\nimport com.google.android.gms.location.LocationRequest;\n\n/**\n * An interface for classes that allow monitoring and retrieving the User's Location.\n * Currently there are only two implementations:\n * - {@link GoogleFusedLocationClient}: A LocationClient using Google Play Services.\n * - {@link AndroidLocationClient}: A LocationClient using Android's existing Location Services.\n */\npublic interface LocationClient {\n    /**\n     * Prepares the LocationClient for use. This method must be called prior\n     * to {@link LocationClient#requestLocationUpdates(LocationListener)}\n     * or {@link LocationClient#getLastLocation()}.\n     */\n    void start(LocationClientListener listener);\n\n    /**\n     * Stops the LocationClient, ending any current connections and allowing\n     * resources to be reclaimed.\n     * <p>\n     * Implementations should call {@link LocationClient#stopLocationUpdates()} if\n     * they have been previously requested.\n     */\n    void stop();\n\n    /**\n     * Begins requesting Location updates with the provided {@link LocationListener}\n     *\n     * @param locationListener The LocationListener to pass location updates to.\n     */\n    void requestLocationUpdates(@NonNull LocationListener locationListener);\n\n    /**\n     * Ends Location updates for the previously provided LocationListener.\n     * Implementations should call this from within {@link LocationClient#stop()}.\n     */\n    void stopLocationUpdates();\n\n    /**\n     * Sets the {@link LocationClientListener} which will receive status updates\n     * for the LocationClient.\n     *\n     * @param locationClientListener The new {@link LocationClientListener}.\n     */\n    void setListener(@Nullable LocationClientListener locationClientListener);\n\n    /**\n     * Sets the LocationClient's {@link Priority} which will be used to determine\n     * which Provider (GPS, Network, etc.) will be used to retrieve the User's location.\n     * <p>\n     * If the LocationClient is already receiving updates, the new Priority will not\n     * take effect until the next time Location updates are requested.\n     *\n     * @param priority The new Priorty.\n     */\n    void setPriority(@NonNull Priority priority);\n\n    /**\n     * Sets whether the client will use the accuracy from mock providers or not. When {@code false}\n     * (which it should be by default) the accuracy of {@link Location} objects from mock providers\n     * will always be {@code 0}.\n     */\n    void setRetainMockAccuracy(boolean retainMockAccuracy);\n\n    /**\n     * Retrieves the most recent known Location, or null if none is available.\n     * This method may block if start was not called before hand.\n     *\n     * @return The most recent Location.\n     */\n    @Nullable\n    Location getLastLocation();\n\n    /**\n     * Provides a way to tell if the LocationClient can access the User's Location based\n     * on the current Priority settings. Must be called <b>after</b> Location updates have been requested.\n     *\n     * @return Whether or not the User's Location is currently available.\n     */\n    boolean isLocationAvailable();\n\n    /**\n     * Provides a way to tell if the LocationClient is currently monitoring the User's Location.\n     *\n     * @return Whether or not the clientForContext is currently monitoring the User's Location.\n     */\n    boolean isMonitoringLocation();\n\n    /**\n     * Sets the LocationClient's updateInterval (how often we would like updates).\n     * <p>\n     * Implementations that don't offer this feature should do nothing here.\n     *\n     * @param updateInterval How often we would like updates from the LocationClient (inexact).\n     */\n    void setUpdateInterval(long updateInterval);\n\n    /**\n     * An interface for listening to status changes on a LocaitonClient.\n     */\n    interface LocationClientListener {\n        /**\n         * Called after the LocationClient has been successfully started.\n         */\n        void onClientStart();\n\n        /**\n         * Called if any issue ocurred during LocationClient start-up.\n         */\n        void onClientStartFailure();\n\n        /**\n         * Called after the LocationClient has been stopped, either by calling\n         * {@link LocationClient#stop()} or because it was stopped by another process.\n         */\n        void onClientStop();\n    }\n\n    /**\n     * Enumerates the options for preferring certain Location Providers over others.\n     */\n    enum Priority {\n\n        /**\n         * Preferred: GPS\n         * Backup: Network\n         */\n        PRIORITY_HIGH_ACCURACY(LocationRequest.PRIORITY_HIGH_ACCURACY),\n\n        /**\n         * Preferred: Network\n         * Backup: GPS\n         */\n        PRIORITY_BALANCED_POWER_ACCURACY(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY),\n\n        /**\n         * Preferred: Network\n         * Backup: GPS (Play Services), Passive (Android)\n         */\n        PRIORITY_LOW_POWER(LocationRequest.PRIORITY_LOW_POWER),\n\n        /**\n         * Preferred: Passive (only receives updates if another Application requests them).\n         * Backup: N/A\n         */\n        PRIORITY_NO_POWER(LocationRequest.PRIORITY_NO_POWER);\n\n        private final int value;\n\n        Priority(int value) {\n            this.value = value;\n        }\n\n        /**\n         * The numeric value of the Priority;\n         * LocationServices uses integer constants.\n         *\n         * @return The integer constant value for the Priority.\n         */\n        public int getValue() {\n            return value;\n        }\n    }\n}\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/LocationClientProvider.kt",
    "content": "package org.odk.collect.location\n\nimport android.app.Application\nimport android.content.Context\nimport com.google.android.gms.common.ConnectionResult\nimport com.google.android.gms.common.GoogleApiAvailability\nimport java.util.function.Supplier\n\n/**\n * A static helper class for obtaining the appropriate LocationClient to use.\n */\nobject LocationClientProvider {\n\n    private var testClient: LocationClient? = null\n\n    @JvmStatic\n    fun getClient(application: Application): LocationClient {\n        return getClient(\n            application,\n            { GoogleFusedLocationClient(application) },\n            GoogleApiAvailability.getInstance()\n        )\n    }\n\n    /**\n     * Returns a LocationClient appropriate for a given context.\n     */\n    // NOTE(ping): As of 2018-11-01, the GoogleFusedLocationClient never returns an\n    // accuracy radius below 3m: https://issuetracker.google.com/issues/118789585\n    internal fun getClient(\n        context: Context,\n        googleFusedLocationClientProvider: Supplier<GoogleFusedLocationClient>,\n        googleApiAvailability: GoogleApiAvailability\n    ): LocationClient {\n        val playServicesAvailable = googleApiAvailability\n            .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS\n\n        return testClient.let {\n            if (it != null) {\n                it\n            } else if (playServicesAvailable) {\n                googleFusedLocationClientProvider.get()\n            } else {\n                AndroidLocationClient(context)\n            }\n        }\n    }\n\n    /**\n     * Sets the LocationClient.  For use in tests only.\n     */\n    fun setTestClient(testClient: LocationClient?) {\n        LocationClientProvider.testClient = testClient\n    }\n}\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/LocationUtils.kt",
    "content": "package org.odk.collect.location\n\nimport android.location.Location\n\nobject LocationUtils {\n\n    @JvmStatic\n    @JvmOverloads\n    fun sanitizeAccuracy(location: Location?, retainMockAccuracy: Boolean = false): Location? {\n        if (location != null && (location.isFromMockProvider && !retainMockAccuracy || location.accuracy < 0)) {\n            location.accuracy = 0f\n        }\n\n        return location\n    }\n}\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/satellites/GpsStatusSatelliteInfoClient.kt",
    "content": "package org.odk.collect.location.satellites\n\nimport android.annotation.SuppressLint\nimport android.location.GnssStatus\nimport android.location.LocationManager\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\n\nclass GpsStatusSatelliteInfoClient(private val locationManager: LocationManager) :\n    SatelliteInfoClient {\n\n    override val satellitesUsedInLastFix: NonNullLiveData<Int>\n        get() {\n            return GnssStatusSatellitesLiveData(locationManager)\n        }\n}\n\n@SuppressLint(\"MissingPermission\")\nprivate class GnssStatusSatellitesLiveData(private val locationManager: LocationManager) :\n    NonNullLiveData<Int>(0) {\n\n    private val gnssStatusCallback = object : GnssStatus.Callback() {\n        override fun onSatelliteStatusChanged(status: GnssStatus) {\n            value = (0 until status.satelliteCount).fold(0) { count, index ->\n                when {\n                    status.usedInFix(index) -> count + 1\n                    else -> count\n                }\n            }\n        }\n    }\n\n    override fun onActive() {\n        locationManager.registerGnssStatusCallback(gnssStatusCallback, null)\n    }\n\n    override fun onInactive() {\n        locationManager.unregisterGnssStatusCallback(gnssStatusCallback)\n    }\n}\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/satellites/SatelliteInfoClient.kt",
    "content": "package org.odk.collect.location.satellites\n\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\n\ninterface SatelliteInfoClient {\n\n    val satellitesUsedInLastFix: NonNullLiveData<Int>\n}\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt",
    "content": "package org.odk.collect.location.tracker\n\nimport android.app.Application\nimport android.app.Notification\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.app.Service\nimport android.content.Intent\nimport android.os.IBinder\nimport androidx.core.app.NotificationCompat\nimport kotlinx.coroutines.flow.StateFlow\nimport org.odk.collect.androidshared.data.getState\nimport org.odk.collect.androidshared.ui.ReturnToAppActivity\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\nimport org.odk.collect.location.Location\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.location.LocationClientProvider\nimport org.odk.collect.location.LocationDependencyComponentProvider\nimport org.odk.collect.strings.localization.getLocalizedString\nimport javax.inject.Inject\n\nprivate const val LOCATION_KEY = \"location\"\n\nclass ForegroundServiceLocationTracker(private val application: Application) : LocationTracker {\n\n    override fun getLocation(): StateFlow<Location?> {\n        return application.getState().getFlow(LOCATION_KEY, null)\n    }\n\n    override fun start(retainMockAccuracy: Boolean, updateInterval: Long?) {\n        val intent = Intent(application, LocationTrackerService::class.java).also { intent ->\n            intent.putExtra(LocationTrackerService.EXTRA_RETAIN_MOCK_ACCURACY, retainMockAccuracy)\n            updateInterval?.let {\n                intent.putExtra(LocationTrackerService.EXTRA_UPDATE_INTERVAL, it)\n            }\n        }\n\n        application.startService(intent)\n    }\n\n    override fun stop() {\n        application.stopService(Intent(application, LocationTrackerService::class.java))\n    }\n}\n\nclass LocationTrackerService : Service(), LocationClient.LocationClientListener {\n\n    @Inject\n    lateinit var uniqueIdGenerator: UniqueIdGenerator\n\n    private val locationClient: LocationClient by lazy {\n        LocationClientProvider.getClient(application)\n    }\n\n    override fun onCreate() {\n        super.onCreate()\n        val provider = applicationContext as LocationDependencyComponentProvider\n        provider.locationDependencyComponent.inject(this)\n    }\n\n    override fun onBind(intent: Intent?): IBinder? {\n        return null\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        setupNotificationChannel()\n        startForeground(\n            uniqueIdGenerator.getInt(NOTIFICATION_IDENTIFIER),\n            createNotification()\n        )\n\n        locationClient.setRetainMockAccuracy(\n            intent?.getBooleanExtra(\n                EXTRA_RETAIN_MOCK_ACCURACY,\n                false\n            ) ?: false\n        )\n\n        if (intent?.hasExtra(EXTRA_UPDATE_INTERVAL) == true) {\n            val interval = intent.getLongExtra(EXTRA_UPDATE_INTERVAL, -1)\n            locationClient.setUpdateInterval(\n                interval\n            )\n        }\n\n        locationClient.start(this)\n        return START_NOT_STICKY\n    }\n\n    override fun onDestroy() {\n        locationClient.stop()\n        application.getState().clear(LOCATION_KEY)\n    }\n\n    override fun onClientStart() {\n        locationClient.requestLocationUpdates {\n            application.getState().setFlow(\n                LOCATION_KEY,\n                Location(it.latitude, it.longitude, it.altitude, it.accuracy)\n            )\n        }\n    }\n\n    override fun onClientStartFailure() {\n        // Ignored\n    }\n\n    override fun onClientStop() {\n        // Ignored\n    }\n\n    private fun createNotification(): Notification {\n        val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL)\n            .setSmallIcon(org.odk.collect.icons.R.drawable.ic_notification_small)\n            .setContentTitle(getLocalizedString(org.odk.collect.strings.R.string.location_tracking_notification_title))\n            .setPriority(NotificationCompat.PRIORITY_LOW)\n            .setContentIntent(createNotificationIntent())\n\n        return notification\n            .build()\n    }\n\n    private fun createNotificationIntent() =\n        PendingIntent.getActivity(this, 0, Intent(this, ReturnToAppActivity::class.java), PendingIntent.FLAG_IMMUTABLE)\n\n    private fun setupNotificationChannel() {\n        val notificationChannel = NotificationChannel(\n            NOTIFICATION_CHANNEL,\n            getLocalizedString(org.odk.collect.strings.R.string.location_tracking_notification_channel_name),\n            NotificationManager.IMPORTANCE_LOW\n        )\n\n        (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(\n            notificationChannel\n        )\n    }\n\n    companion object {\n        const val EXTRA_RETAIN_MOCK_ACCURACY = \"retain_mock_accuracy\"\n        const val EXTRA_UPDATE_INTERVAL = \"update_interval\"\n\n        private const val NOTIFICATION_IDENTIFIER = \"location_tracking\"\n        private const val NOTIFICATION_CHANNEL = \"location_tracking\"\n    }\n}\n"
  },
  {
    "path": "location/src/main/java/org/odk/collect/location/tracker/LocationTracker.kt",
    "content": "package org.odk.collect.location.tracker\n\nimport kotlinx.coroutines.flow.StateFlow\nimport org.odk.collect.location.Location\n\n/**\n * Provides a way to track the location of a device.\n */\ninterface LocationTracker {\n\n    /**\n     * Will be `null` if a location hasn't been determined or [LocationTracker.start] hasn't been\n     * called yet.\n     */\n    fun getLocation(): StateFlow<Location?>\n\n    /**\n     * @param updateInterval requested (not guaranteed) interval for location updates\n     */\n    fun start(retainMockAccuracy: Boolean, updateInterval: Long? = null)\n    fun start(retainMockAccuracy: Boolean) = start(retainMockAccuracy, null)\n    fun start(updateInterval: Long?) = start(false, updateInterval)\n    fun start() = start(false, null)\n\n    /**\n     * Stops tracking location. Does not reset the value returned by [LocationTracker.getCurrentLocation].\n     */\n    fun stop()\n}\n\nfun LocationTracker.getCurrentLocation(): Location? {\n    return this.getLocation().value\n}\n"
  },
  {
    "path": "location/src/test/java/org/odk/collect/location/AndroidLocationClientTest.java",
    "content": "package org.odk.collect.location;\n\nimport android.location.Location;\nimport android.location.LocationManager;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport static android.location.LocationManager.GPS_PROVIDER;\nimport static android.location.LocationManager.NETWORK_PROVIDER;\nimport static android.location.LocationManager.PASSIVE_PROVIDER;\nimport static java.util.Arrays.asList;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.assertFalse;\nimport static org.junit.Assert.assertNull;\nimport static org.junit.Assert.assertSame;\nimport static org.junit.Assert.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.odk.collect.location.LocationClient.Priority.PRIORITY_BALANCED_POWER_ACCURACY;\nimport static org.odk.collect.location.LocationClient.Priority.PRIORITY_HIGH_ACCURACY;\nimport static org.odk.collect.location.LocationClient.Priority.PRIORITY_LOW_POWER;\nimport static org.odk.collect.location.LocationClient.Priority.PRIORITY_NO_POWER;\nimport static org.odk.collect.testshared.LocationTestUtils.createLocation;\n\n@RunWith(AndroidJUnit4.class)\npublic class AndroidLocationClientTest {\n\n    private LocationManager locationManager;\n    private AndroidLocationClient androidLocationClient;\n\n    @Before\n    public void setUp() {\n        locationManager = mock(LocationManager.class);\n        androidLocationClient = new AndroidLocationClient(locationManager);\n    }\n\n    @Test\n    public void startingShouldSetListenerAndStoppingShouldRemoveIt() {\n        LocationClient.LocationClientListener listener = mock(LocationClient.LocationClientListener.class);\n        androidLocationClient.start(listener);\n        assertThat(androidLocationClient.getListener(), is(notNullValue()));\n\n        androidLocationClient.stop();\n        assertThat(androidLocationClient.getListener(), is(nullValue()));\n    }\n\n    @Test\n    public void startingWithProvidersEnabledShouldCallStartAndStop() {\n\n        List<String> providers = asList(GPS_PROVIDER, NETWORK_PROVIDER);\n        when(locationManager.getProviders(true)).thenReturn(providers);\n\n        TestClientListener testListener = new TestClientListener();\n\n        androidLocationClient.start(testListener);\n        assertTrue(testListener.wasStartCalled());\n        assertFalse(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        androidLocationClient.stop();\n        assertTrue(testListener.wasStartCalled());\n        assertFalse(testListener.wasStartFailureCalled());\n        assertTrue(testListener.wasStopCalled());\n    }\n\n    @Test\n    public void startingWithoutProvidersEnabledShouldCallStartFailureAndStop() {\n        List<String> providers = asList();\n        when(locationManager.getProviders(true)).thenReturn(providers);\n\n        TestClientListener testListener = new TestClientListener();\n\n        // Without any providers enabled, start shouldn't be called, but startFailure should be:\n        androidLocationClient.start(testListener);\n        assertFalse(testListener.wasStartCalled());\n        assertTrue(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        // Stop should still be called if the user calls stop:\n        androidLocationClient.stop();\n        assertFalse(testListener.wasStartCalled());\n        assertTrue(testListener.wasStartFailureCalled());\n        assertTrue(testListener.wasStopCalled());\n    }\n\n    @Test\n    public void requestingLocationUpdatesShouldUpdateCorrectListener() {\n        List<String> providers = asList(GPS_PROVIDER, NETWORK_PROVIDER);\n        when(locationManager.getProviders(true)).thenReturn(providers);\n\n        androidLocationClient.start(null);\n\n        TestLocationListener firstListener = new TestLocationListener();\n        androidLocationClient.requestLocationUpdates(firstListener);\n\n        Location firstLocation = newMockLocation();\n        androidLocationClient.onLocationChanged(firstLocation);\n\n        assertSame(firstLocation, firstListener.getLastLocation());\n\n        Location secondLocation = newMockLocation();\n        androidLocationClient.onLocationChanged(secondLocation);\n\n        assertSame(secondLocation, firstListener.getLastLocation());\n\n        // Now stop updates:\n        androidLocationClient.stopLocationUpdates();\n\n        Location thirdLocation = newMockLocation();\n        androidLocationClient.onLocationChanged(thirdLocation);\n\n        // Should still be second:\n        assertSame(secondLocation, firstListener.getLastLocation());\n\n        // Call requestLocationUpdates again with new Listener:\n        TestLocationListener secondListener = new TestLocationListener();\n        androidLocationClient.requestLocationUpdates(secondListener);\n\n        Location fourthLocation = newMockLocation();\n        androidLocationClient.onLocationChanged(fourthLocation);\n\n        // First listener should still have second location:\n        assertSame(secondLocation, firstListener.getLastLocation());\n        assertSame(fourthLocation, secondListener.getLastLocation());\n\n        // Call stop() and make sure it called stopUpdates():\n        androidLocationClient.stop();\n\n        Location fifthLocation = newMockLocation();\n        androidLocationClient.onLocationChanged(fifthLocation);\n\n        // Listener should still have fourth location:\n        assertSame(fourthLocation, secondListener.getLastLocation());\n    }\n\n    @Test\n    public void passiveProviderOnlyShouldFailOnHighAndBalancedPriorities() {\n        List<String> highAccuracyProviders = asList(PASSIVE_PROVIDER);\n        when(locationManager.getProviders(true)).thenReturn(highAccuracyProviders);\n\n        TestClientListener testListener = new TestClientListener();\n\n        // HIGH_ACCURACY and BALANCED_POWER_ACCURACY should fail with only\n        // PASSIVE_PROVIDER enabled:\n        androidLocationClient.setPriority(PRIORITY_HIGH_ACCURACY);\n        androidLocationClient.start(testListener);\n\n        assertFalse(testListener.wasStartCalled());\n        assertTrue(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n\n        androidLocationClient.setPriority(PRIORITY_BALANCED_POWER_ACCURACY);\n        androidLocationClient.start(testListener);\n\n        assertFalse(testListener.wasStartCalled());\n        assertTrue(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n\n        // PRIORITY_LOW_POWER and PRIORITY_NO_POWER should succeed with only\n        // PASSIVE_PROVIDER enabled:\n        androidLocationClient.setPriority(PRIORITY_LOW_POWER);\n        androidLocationClient.start(testListener);\n\n        assertTrue(testListener.wasStartCalled());\n        assertFalse(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n\n        androidLocationClient.setPriority(PRIORITY_NO_POWER);\n        androidLocationClient.start(testListener);\n\n        assertTrue(testListener.wasStartCalled());\n        assertFalse(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n    }\n\n    @Test\n    public void networkProviderOnlyShouldFailOnNoPowerPriority() {\n        List<String> highAccuracyProviders = asList(NETWORK_PROVIDER);\n        when(locationManager.getProviders(true)).thenReturn(highAccuracyProviders);\n\n        TestClientListener testListener = new TestClientListener();\n\n        // PRIORITY_NO_POWER should fail with only\n        // NETWORK_PROVIDER enabled:\n        androidLocationClient.setPriority(PRIORITY_NO_POWER);\n        androidLocationClient.start(testListener);\n\n        assertFalse(testListener.wasStartCalled());\n        assertTrue(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n\n        // PRIORITY_LOW_POWER, PRIORITY_BALANCED_POWER_ACCURACY, and\n        // PRIORITY_HIGH_ACCURACY should succeed with only\n        // NETWORK_PROVIDER enabled:\n        androidLocationClient.setPriority(PRIORITY_LOW_POWER);\n        androidLocationClient.start(testListener);\n\n        assertTrue(testListener.wasStartCalled());\n        assertFalse(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n\n        androidLocationClient.setPriority(PRIORITY_BALANCED_POWER_ACCURACY);\n        androidLocationClient.start(testListener);\n\n        assertTrue(testListener.wasStartCalled());\n        assertFalse(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n\n        androidLocationClient.setPriority(PRIORITY_HIGH_ACCURACY);\n        androidLocationClient.start(testListener);\n\n        assertTrue(testListener.wasStartCalled());\n        assertFalse(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n    }\n\n    @Test\n    public void gpsProviderOnlyShouldFailOnLowAndNoPowerPriorities() {\n        List<String> highAccuracyProviders = asList(GPS_PROVIDER);\n        when(locationManager.getProviders(true)).thenReturn(highAccuracyProviders);\n\n        TestClientListener testListener = new TestClientListener();\n\n        // PRIORITY_NO_POWER and PRIORITY_LOW_POWER should fail with only\n        // GPS_PROVIDER enabled:\n        androidLocationClient.setPriority(PRIORITY_NO_POWER);\n        androidLocationClient.start(testListener);\n\n        assertFalse(testListener.wasStartCalled());\n        assertTrue(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n\n        androidLocationClient.setPriority(PRIORITY_LOW_POWER);\n        androidLocationClient.start(testListener);\n\n        assertFalse(testListener.wasStartCalled());\n        assertTrue(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n\n        // PRIORITY_BALANCED_POWER_ACCURACY, and PRIORITY_HIGH_ACCURACY\n        // should succeed with only GPS_PROVIDER enabled:\n\n        androidLocationClient.setPriority(PRIORITY_BALANCED_POWER_ACCURACY);\n        androidLocationClient.start(testListener);\n\n        assertTrue(testListener.wasStartCalled());\n        assertFalse(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n\n        androidLocationClient.setPriority(PRIORITY_HIGH_ACCURACY);\n        androidLocationClient.start(testListener);\n\n        assertTrue(testListener.wasStartCalled());\n        assertFalse(testListener.wasStartFailureCalled());\n        assertFalse(testListener.wasStopCalled());\n\n        testListener.reset();\n    }\n\n    @Test\n    public void getLastLocationShouldReturnCorrectFromCorrectProviderForPriority() {\n        // Set-up mock Locations: -------------------------------------------------------------- //\n\n        Location gpsLocation = newMockLocation();\n        when(locationManager.getLastKnownLocation(GPS_PROVIDER))\n                .thenReturn(gpsLocation);\n\n        Location networkLocation = newMockLocation();\n        when(locationManager.getLastKnownLocation(NETWORK_PROVIDER))\n                .thenReturn(networkLocation);\n\n        Location passiveLocation = newMockLocation();\n        when(locationManager.getLastKnownLocation(PASSIVE_PROVIDER))\n                .thenReturn(passiveLocation);\n\n        // High Accuracy: -------------------------------------------------------------- //\n\n        androidLocationClient.setPriority(PRIORITY_HIGH_ACCURACY);\n\n        // W/ GPS\n        when(locationManager.getProviders(true))\n                .thenReturn(asList(GPS_PROVIDER, NETWORK_PROVIDER));\n        assertSame(androidLocationClient.getLastLocation(), gpsLocation);\n\n        // W/out GPS:\n        when(locationManager.getProviders(true))\n                .thenReturn(asList(NETWORK_PROVIDER));\n\n        assertSame(androidLocationClient.getLastLocation(), networkLocation);\n\n        // Balanced Accuracy: -------------------------------------------------------------- //\n\n        androidLocationClient.setPriority(PRIORITY_BALANCED_POWER_ACCURACY);\n\n        // W/ both (should be Network)\n        when(locationManager.getProviders(true))\n                .thenReturn(asList(GPS_PROVIDER, NETWORK_PROVIDER));\n        assertSame(androidLocationClient.getLastLocation(), networkLocation);\n\n        // W/out Network\n        when(locationManager.getProviders(true))\n                .thenReturn(asList(GPS_PROVIDER));\n        assertSame(androidLocationClient.getLastLocation(), gpsLocation);\n\n        // Low Power Accuracy: -------------------------------------------------------------- //\n\n        androidLocationClient.setPriority(PRIORITY_LOW_POWER);\n\n        // W/ Network\n\n        when(locationManager.getProviders(true))\n                .thenReturn(asList(NETWORK_PROVIDER, PASSIVE_PROVIDER));\n        assertSame(androidLocationClient.getLastLocation(), networkLocation);\n\n        // W/out Network:\n        when(locationManager.getProviders(true))\n                .thenReturn(asList(PASSIVE_PROVIDER));\n        assertSame(androidLocationClient.getLastLocation(), passiveLocation);\n\n        // No Power: -------------------------------------------------------------- //\n        androidLocationClient.setPriority(PRIORITY_NO_POWER);\n\n        // W/ Passive:\n        when(locationManager.getProviders(true))\n                .thenReturn(asList(PASSIVE_PROVIDER));\n        assertSame(androidLocationClient.getLastLocation(), passiveLocation);\n\n        // W/out any Providers:\n        when(locationManager.getProviders(true))\n                .thenReturn(asList());\n\n        assertNull(androidLocationClient.getLastLocation());\n    }\n\n    @Test\n    public void whenNewlyReceivedLocationAccuracyIsNegative_shouldBeSetToZero() {\n        when(locationManager.getProviders(true)).thenReturn(Collections.singletonList(GPS_PROVIDER));\n\n        androidLocationClient.start(null);\n\n        TestLocationListener listener = new TestLocationListener();\n        androidLocationClient.requestLocationUpdates(listener);\n\n        Location location = createLocation(GPS_PROVIDER, 7, 2, 3, -1.0f);\n        androidLocationClient.onLocationChanged(location);\n\n        assertThat(listener.getLastLocation().getAccuracy(), is(0.0f));\n    }\n\n    @Test\n    public void whenNewlyReceivedLocationIsMocked_shouldAccuracyBeSetToZero() {\n        when(locationManager.getProviders(true)).thenReturn(Collections.singletonList(GPS_PROVIDER));\n\n        androidLocationClient.start(null);\n\n        TestLocationListener listener = new TestLocationListener();\n        androidLocationClient.requestLocationUpdates(listener);\n\n        Location location = createLocation(GPS_PROVIDER, 7, 2, 3, 5.0f, true);\n        androidLocationClient.onLocationChanged(location);\n\n        assertThat(listener.getLastLocation().getAccuracy(), is(0.0f));\n    }\n\n    @Test\n    public void whenNewlyReceivedLocationIsMocked_andRetainMockAccuracyIsTrue_doesNotChangeAccuracy() {\n        when(locationManager.getProviders(true)).thenReturn(Collections.singletonList(GPS_PROVIDER));\n\n        androidLocationClient.setRetainMockAccuracy(true);\n        androidLocationClient.start(null);\n\n        TestLocationListener listener = new TestLocationListener();\n        androidLocationClient.requestLocationUpdates(listener);\n\n        Location location = createLocation(GPS_PROVIDER, 7, 2, 3, 5.0f, true);\n        androidLocationClient.onLocationChanged(location);\n\n        assertThat(listener.getLastLocation().getAccuracy(), is(5.0f));\n    }\n\n    @Test\n    public void whenLastKnownLocationAccuracyIsNegative_shouldBeSetToZero() {\n        when(locationManager.getProviders(true)).thenReturn(Collections.singletonList(GPS_PROVIDER));\n\n        Location location = createLocation(GPS_PROVIDER, 7, 2, 3, -1.0f);\n        when(locationManager.getLastKnownLocation(GPS_PROVIDER)).thenReturn(location);\n\n        assertThat(androidLocationClient.getLastLocation().getAccuracy(), is(0.0f));\n    }\n\n    @Test\n    public void whenLastKnownLocationIsMocked_shouldAccuracyBeSetToZero() {\n        when(locationManager.getProviders(true)).thenReturn(Collections.singletonList(GPS_PROVIDER));\n\n        Location location = createLocation(GPS_PROVIDER, 7, 2, 3, 5.0f, true);\n        when(locationManager.getLastKnownLocation(GPS_PROVIDER)).thenReturn(location);\n\n        assertThat(androidLocationClient.getLastLocation().getAccuracy(), is(0.0f));\n    }\n\n    @Test\n    public void whenLastKnownLocationIsMocked_andRetainMockAccuracyIsTrue_doesNotChangeAccuracy() {\n        when(locationManager.getProviders(true)).thenReturn(Collections.singletonList(GPS_PROVIDER));\n        androidLocationClient.setRetainMockAccuracy(true);\n\n        Location location = createLocation(GPS_PROVIDER, 7, 2, 3, 5.0f, true);\n        when(locationManager.getLastKnownLocation(GPS_PROVIDER)).thenReturn(location);\n\n        assertThat(androidLocationClient.getLastLocation().getAccuracy(), is(5.0f));\n    }\n\n    private static Location newMockLocation() {\n        return mock(Location.class);\n    }\n}\n"
  },
  {
    "path": "location/src/test/java/org/odk/collect/location/GoogleFusedLocationClientTest.kt",
    "content": "package org.odk.collect.location\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport com.google.android.gms.location.LocationRequest\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.notNullValue\nimport org.hamcrest.CoreMatchers.nullValue\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.Mockito.verify\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.argThat\nimport org.mockito.kotlin.isA\nimport org.mockito.kotlin.mock\nimport org.odk.collect.location.LocationClient.LocationClientListener\nimport org.odk.collect.testshared.LocationTestUtils.createLocation\n\n@RunWith(AndroidJUnit4::class)\nclass GoogleFusedLocationClientTest {\n    private val fusedLocationProviderClientWrapper = mock<FusedLocationProviderClientWrapper>()\n    private val locationClientListener = mock<LocationClientListener>()\n    private val locationListener = TestLocationListener()\n    private lateinit var client: GoogleFusedLocationClient\n\n    @Before\n    fun setUp() {\n        client = GoogleFusedLocationClient(fusedLocationProviderClientWrapper, mock())\n    }\n\n    @Test\n    fun `#start sets listener and calls #onClientStart`() {\n        client.start(locationClientListener)\n\n        assertThat(client.getListener(), notNullValue())\n        verify(locationClientListener).onClientStart()\n    }\n\n    @Test\n    fun `#start starts FusedLocationProviderClientWrapper`() {\n        client.start(locationClientListener)\n\n        verify(fusedLocationProviderClientWrapper).start(isA<GoogleFusedLocationClient>())\n    }\n\n    @Test\n    fun `#stop removes listener and calls #onClientStop`() {\n        client.start(locationClientListener)\n        client.stop()\n\n        assertThat(client.getListener(), nullValue())\n        verify(locationClientListener).onClientStop()\n    }\n\n    @Test\n    fun `#stop stops location updates`() {\n        val listener = TestLocationListener()\n        client.requestLocationUpdates(listener)\n        client.stop()\n\n        verify(fusedLocationProviderClientWrapper).removeLocationUpdates()\n    }\n\n    @Test\n    fun `#requestLocationUpdates starts location updates`() {\n        val listener = TestLocationListener()\n        client.requestLocationUpdates(listener)\n\n        verify(fusedLocationProviderClientWrapper).requestLocationUpdates(any())\n    }\n\n    @Test\n    fun `#requestLocationUpdates builds location request based on passed values`() {\n        client.setPriority(LocationClient.Priority.PRIORITY_NO_POWER)\n        client.setUpdateInterval(10L)\n        val listener = TestLocationListener()\n        client.requestLocationUpdates(listener)\n\n        verify(fusedLocationProviderClientWrapper).requestLocationUpdates(\n            argThat { locationRequest: LocationRequest ->\n                assertThat(locationRequest.priority, equalTo(LocationClient.Priority.PRIORITY_NO_POWER.value))\n                assertThat(locationRequest.interval, equalTo(10L))\n                assertThat(locationRequest.fastestInterval, equalTo(10L))\n                true\n            }\n        )\n    }\n\n    @Test\n    fun `#isMonitoringLocation returns false if listener is mot set`() {\n        client.stopLocationUpdates()\n\n        assertThat(client.isMonitoringLocation, equalTo(false))\n    }\n\n    @Test\n    fun `#isMonitoringLocation returns true if listener is set`() {\n        val listener = TestLocationListener()\n        client.requestLocationUpdates(listener)\n\n        assertThat(client.isMonitoringLocation, equalTo(true))\n    }\n\n    @Test\n    fun `#stopLocationUpdates stops location updates`() {\n        val listener = TestLocationListener()\n        client.requestLocationUpdates(listener)\n        client.stopLocationUpdates()\n\n        verify(fusedLocationProviderClientWrapper).removeLocationUpdates()\n    }\n\n    @Test\n    fun `when received location accuracy is negative set to zero`() {\n        client.requestLocationUpdates(locationListener)\n        val location = createLocation(\"GPS\", 7.0, 2.0, 3.0, -1f)\n        client.onLocationChanged(location)\n\n        assertThat(locationListener.lastLocation!!.accuracy, equalTo(0.0f))\n    }\n\n    @Test\n    fun `when received location accuracy is mocked set to zero`() {\n        client.requestLocationUpdates(locationListener)\n        val location = createLocation(\"GPS\", 7.0, 2.0, 3.0, 5.0f, true)\n        client.onLocationChanged(location)\n\n        assertThat(locationListener.lastLocation!!.accuracy, equalTo(0.0f))\n    }\n\n    @Test\n    fun `when received location accuracy is mocked and #retainMockAccuracy is true do not change accuracy`() {\n        client.setRetainMockAccuracy(true)\n        client.requestLocationUpdates(locationListener)\n        val location = createLocation(\"GPS\", 7.0, 2.0, 3.0, 5.0f, true)\n        client.onLocationChanged(location)\n\n        assertThat(locationListener.lastLocation!!.accuracy, equalTo(5.0f))\n    }\n}\n"
  },
  {
    "path": "location/src/test/java/org/odk/collect/location/LocationClientProviderTest.kt",
    "content": "/*\n * Copyright (C) 2020 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.location\n\nimport android.content.Context\nimport com.google.android.gms.common.ConnectionResult\nimport com.google.android.gms.common.GoogleApiAvailability\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.instanceOf\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Test\nimport org.mockito.ArgumentMatchers\nimport org.mockito.Mockito\nimport org.mockito.Mockito.`when`\n\nclass LocationClientProviderTest {\n\n    private val context = Mockito.mock(Context::class.java)\n    private val googleApiAvailability = Mockito.mock(\n        GoogleApiAvailability::class.java\n    )\n    private val googleFusedLocationClient = Mockito.mock(\n        GoogleFusedLocationClient::class.java\n    )\n\n    @Test\n    fun fusedLocationClient_returnedWhenPlayServicesAvailable() {\n        `when`(googleApiAvailability.isGooglePlayServicesAvailable(ArgumentMatchers.any()))\n            .thenReturn(\n                ConnectionResult.SUCCESS\n            )\n        val client = LocationClientProvider.getClient(\n            context,\n            { googleFusedLocationClient },\n            googleApiAvailability\n        )\n\n        assertThat(client, `is`(googleFusedLocationClient))\n    }\n\n    @Test\n    fun androidLocationClient_returnedWhenPlayServicesNotAvailable() {\n        `when`(googleApiAvailability.isGooglePlayServicesAvailable(ArgumentMatchers.any()))\n            .thenReturn(\n                ConnectionResult.API_UNAVAILABLE\n            )\n\n        val client = LocationClientProvider.getClient(\n            context,\n            { googleFusedLocationClient },\n            googleApiAvailability\n        )\n\n        assertThat(client, instanceOf(AndroidLocationClient::class.java))\n    }\n}\n"
  },
  {
    "path": "location/src/test/java/org/odk/collect/location/LocationUtilsTest.kt",
    "content": "package org.odk.collect.location\n\nimport android.location.LocationManager\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.core.Is.`is`\nimport org.hamcrest.core.IsNull.nullValue\nimport org.junit.Test\nimport org.odk.collect.location.LocationUtils.sanitizeAccuracy\nimport org.odk.collect.testshared.LocationTestUtils.createLocation\n\nclass LocationUtilsTest {\n\n    @Test\n    fun whenAccuracyIsNegative_shouldBeSetToZeroAfterSanitizing() {\n        val location = createLocation(LocationManager.GPS_PROVIDER, 7.0, 2.0, 3.0, -1.0f)\n        val sanitizedLocation = sanitizeAccuracy(location)\n        assertThat(sanitizedLocation!!.latitude, `is`(7.0))\n        assertThat(sanitizedLocation.longitude, `is`(2.0))\n        assertThat(sanitizedLocation.altitude, `is`(3.0))\n        assertThat(sanitizedLocation.accuracy, `is`(0.0f))\n    }\n\n    @Test\n    fun whenAccuracyIsNegative_andRetainMockAccuracyIsTrue_shouldBeSetToZeroAfterSanitizing() {\n        val location = createLocation(LocationManager.GPS_PROVIDER, 7.0, 2.0, 3.0, -1.0f)\n        val sanitizedLocation = sanitizeAccuracy(location, retainMockAccuracy = true)\n        assertThat(sanitizedLocation!!.latitude, `is`(7.0))\n        assertThat(sanitizedLocation.longitude, `is`(2.0))\n        assertThat(sanitizedLocation.altitude, `is`(3.0))\n        assertThat(sanitizedLocation.accuracy, `is`(0.0f))\n    }\n\n    @Test\n    fun whenLocationIsMocked_shouldAccuracyBeSetToZeroAfterSanitizing() {\n        val location = createLocation(LocationManager.GPS_PROVIDER, 7.0, 2.0, 3.0, 5.0f, true)\n        val sanitizedLocation = sanitizeAccuracy(location)\n        assertThat(sanitizedLocation!!.latitude, `is`(7.0))\n        assertThat(sanitizedLocation.longitude, `is`(2.0))\n        assertThat(sanitizedLocation.altitude, `is`(3.0))\n        assertThat(sanitizedLocation.accuracy, `is`(0.0f))\n    }\n\n    @Test\n    fun whenLocationIsMocked_andRetainMockAccuracyIsTrue_retainsAccuracy() {\n        val location = createLocation(LocationManager.GPS_PROVIDER, 7.0, 2.0, 3.0, 5.0f, true)\n        val sanitizedLocation = sanitizeAccuracy(location, retainMockAccuracy = true)\n        assertThat(sanitizedLocation!!.latitude, `is`(7.0))\n        assertThat(sanitizedLocation.longitude, `is`(2.0))\n        assertThat(sanitizedLocation.altitude, `is`(3.0))\n        assertThat(sanitizedLocation.accuracy, `is`(5.0f))\n    }\n\n    @Test\n    fun whenLocationIsNull_shouldNullBeReturnedAfterSanitizing() {\n        assertThat(sanitizeAccuracy(null), nullValue())\n    }\n}\n"
  },
  {
    "path": "location/src/test/java/org/odk/collect/location/RobolectricApplication.kt",
    "content": "package org.odk.collect.location\n\nimport android.app.Application\nimport org.odk.collect.androidshared.data.AppState\nimport org.odk.collect.androidshared.data.StateStore\nimport org.odk.collect.androidshared.utils.InMemUniqueIdGenerator\nimport org.odk.collect.androidshared.utils.UniqueIdGenerator\n\nclass RobolectricApplication : Application(), StateStore, LocationDependencyComponentProvider {\n\n    override val locationDependencyComponent: LocationDependencyComponent =\n        DaggerLocationDependencyComponent.builder().locationDependencyModule(\n            object : LocationDependencyModule() {\n                override fun providesUniqueIdGenerator(): UniqueIdGenerator {\n                    return InMemUniqueIdGenerator()\n                }\n            }\n        ).build()\n\n    private val appState = AppState()\n\n    override fun getState(): AppState {\n        return appState\n    }\n}\n"
  },
  {
    "path": "location/src/test/java/org/odk/collect/location/TestClientListener.java",
    "content": "package org.odk.collect.location;\n\nclass TestClientListener implements LocationClient.LocationClientListener {\n\n    private boolean wasStartCalled;\n    private boolean wasStartFailureCalled;\n    private boolean wasStopCalled;\n\n    void reset() {\n        wasStartCalled = false;\n        wasStartFailureCalled = false;\n        wasStopCalled = false;\n    }\n\n    boolean wasStartCalled() {\n        return wasStartCalled;\n    }\n\n    boolean wasStartFailureCalled() {\n        return wasStartFailureCalled;\n    }\n\n    boolean wasStopCalled() {\n        return wasStopCalled;\n    }\n\n    @Override\n    public void onClientStart() {\n        wasStartCalled = true;\n    }\n\n    @Override\n    public void onClientStartFailure() {\n        wasStartFailureCalled = true;\n    }\n\n    @Override\n    public void onClientStop() {\n        wasStopCalled = true;\n    }\n}\n"
  },
  {
    "path": "location/src/test/java/org/odk/collect/location/TestLocationListener.java",
    "content": "package org.odk.collect.location;\n\nimport android.location.Location;\n\nimport androidx.annotation.Nullable;\n\nclass TestLocationListener implements com.google.android.gms.location.LocationListener {\n\n    @Nullable\n    private Location lastLocation;\n\n    @Nullable\n    Location getLastLocation() {\n        return lastLocation;\n    }\n\n    @Override\n    public void onLocationChanged(Location location) {\n        this.lastLocation = location;\n    }\n}\n"
  },
  {
    "path": "location/src/test/java/org/odk/collect/location/tracker/ForegroundServiceLocationTrackerTest.kt",
    "content": "package org.odk.collect.location.tracker\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport com.google.android.gms.location.LocationListener\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.location.Location\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.location.LocationClient.LocationClientListener\nimport org.odk.collect.location.LocationClientProvider\nimport org.odk.collect.testshared.RobolectricHelpers\n\n@RunWith(AndroidJUnit4::class)\nclass ForegroundServiceLocationTrackerTest : LocationTrackerTest() {\n\n    private val application: Application = ApplicationProvider.getApplicationContext()\n    private val locationClient = FakeLocationClient()\n\n    override val locationTracker: LocationTracker = ForegroundServiceLocationTracker(application)\n\n    override fun runBackground() {\n        RobolectricHelpers.runServices(true)\n    }\n\n    override fun setDeviceLocation(location: Location) {\n        val androidLocation = android.location.Location(\"gps\")\n        androidLocation.latitude = location.latitude\n        androidLocation.longitude = location.longitude\n        androidLocation.altitude = location.altitude\n        androidLocation.accuracy = location.accuracy\n\n        locationClient.updateLocation(androidLocation)\n    }\n\n    @Before\n    fun fakeLocationClient() {\n        LocationClientProvider.setTestClient(locationClient)\n    }\n\n    @After\n    fun teardown() {\n        RobolectricHelpers.clearServices()\n        LocationClientProvider.setTestClient(null)\n    }\n\n    @Test\n    fun start_whenRetainMockAccuracyIsTrue_setsRetainMockAccuracyOnClient() {\n        locationTracker.start(retainMockAccuracy = true)\n        runBackground()\n\n        assertThat(locationClient.getRetainMockAccuracy(), equalTo(true))\n    }\n\n    @Test\n    fun start_whenRetainMockAccuracyIsFalse_setsRetainMockAccuracyOnClient() {\n        locationTracker.start(retainMockAccuracy = false)\n        runBackground()\n\n        assertThat(locationClient.getRetainMockAccuracy(), equalTo(false))\n    }\n\n    @Test\n    fun start_whenUpdateIntervalIsNull_doesNotSetIntervalOnClient() {\n        locationTracker.start(updateInterval = null)\n        runBackground()\n\n        assertThat(locationClient.getUpdateInterval(), equalTo(null))\n    }\n\n    @Test\n    fun start_whenUpdateIntervalIsNonNull_setsIntervalsOnClient() {\n        locationTracker.start(updateInterval = 1000)\n        runBackground()\n\n        assertThat(locationClient.getUpdateInterval(), equalTo(1000L))\n    }\n\n    @Test\n    fun start_afterAnotherStart_updatesClient() {\n        locationTracker.start(retainMockAccuracy = false, updateInterval = 1000L)\n        runBackground()\n\n        locationTracker.start(retainMockAccuracy = true, updateInterval = 2000L)\n        runBackground()\n\n        assertThat(locationClient.getRetainMockAccuracy(), equalTo(true))\n        assertThat(locationClient.getUpdateInterval(), equalTo(2000L))\n    }\n}\n\nprivate class FakeLocationClient : LocationClient {\n\n    private var started = false\n    private var locationListener: LocationListener? = null\n    private var locationClientListener: LocationClientListener? = null\n    private var retainMockAccuracy: Boolean = false\n    private var updateInterval: Long? = null\n\n    override fun start(listener: LocationClientListener) {\n        setListener(listener)\n        this.started = true\n        locationClientListener?.onClientStart()\n    }\n\n    override fun stop() {\n        this.started = false\n        locationClientListener?.onClientStop()\n        setListener(null)\n    }\n\n    override fun requestLocationUpdates(locationListener: LocationListener) {\n        if (!started) {\n            throw IllegalStateException(\"Can't request location updated before starting!\")\n        }\n\n        this.locationListener = locationListener\n    }\n\n    override fun stopLocationUpdates() {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun setListener(locationClientListener: LocationClient.LocationClientListener?) {\n        this.locationClientListener = locationClientListener\n    }\n\n    override fun setPriority(priority: LocationClient.Priority) {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) {\n        this.retainMockAccuracy = retainMockAccuracy\n    }\n\n    override fun getLastLocation(): android.location.Location? {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun isLocationAvailable(): Boolean {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun isMonitoringLocation(): Boolean {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun setUpdateInterval(updateInterval: Long) {\n        this@FakeLocationClient.updateInterval = updateInterval\n    }\n\n    fun updateLocation(location: android.location.Location) {\n        if (started) {\n            locationListener?.onLocationChanged(location)\n        }\n    }\n\n    fun getRetainMockAccuracy(): Boolean {\n        return retainMockAccuracy\n    }\n\n    fun getUpdateInterval(): Long? {\n        return updateInterval\n    }\n}\n"
  },
  {
    "path": "location/src/test/java/org/odk/collect/location/tracker/LocationTrackerServiceTest.kt",
    "content": "package org.odk.collect.location.tracker\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.notNullValue\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidshared.ui.ReturnToAppActivity\nimport org.odk.collect.servicetest.ServiceScenario\n\n@RunWith(AndroidJUnit4::class)\nclass LocationTrackerServiceTest {\n\n    @Test\n    fun onStartCommand_startsServiceInForeground() {\n        val service = ServiceScenario.launch(LocationTrackerService::class.java)\n        assertThat(service.getForegroundNotification(), notNullValue())\n    }\n\n    @Test\n    fun clickingForegroundNotification_navigatesBackToApp() {\n        val service = ServiceScenario.launch(LocationTrackerService::class.java)\n        val intent = service.getForegroundNotification()!!.contentIntent\n        assertThat(intent.component?.className, equalTo(ReturnToAppActivity::class.qualifiedName))\n    }\n}\n"
  },
  {
    "path": "location/src/test/java/org/odk/collect/location/tracker/LocationTrackerTest.kt",
    "content": "package org.odk.collect.location.tracker\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.location.Location\n\nabstract class LocationTrackerTest {\n\n    abstract val locationTracker: LocationTracker\n\n    abstract fun runBackground()\n    abstract fun setDeviceLocation(location: Location)\n\n    @Test\n    fun updatingDeviceLocation_beforeStarting_doesNothing() {\n        setDeviceLocation(Location(1.0, 2.0, 3.0, 4.0f))\n        runBackground()\n\n        assertThat(locationTracker.getCurrentLocation(), equalTo(null))\n    }\n\n    @Test\n    fun updatingDeviceLocation_whenStarted_updatesCurrentLocation() {\n        locationTracker.start()\n        runBackground()\n\n        val location = Location(1.0, 2.0, 3.0, 4.0f)\n        setDeviceLocation(location)\n        assertThat(locationTracker.getCurrentLocation(), equalTo(location))\n    }\n\n    @Test\n    fun updatingDeviceLocation_whenStopped_doesNothing() {\n        locationTracker.start()\n        locationTracker.stop()\n        runBackground()\n\n        setDeviceLocation(Location(1.0, 2.0, 3.0, 4.0f))\n        assertThat(locationTracker.getCurrentLocation(), equalTo(null))\n    }\n\n    @Test\n    fun stopping_clearsLocation() {\n        locationTracker.start()\n        runBackground()\n\n        setDeviceLocation(Location(1.0, 2.0, 3.0, 4.0f))\n\n        locationTracker.stop()\n        runBackground()\n        assertThat(locationTracker.getCurrentLocation(), equalTo(null))\n    }\n}\n"
  },
  {
    "path": "location/src/test/resources/robolectric.properties",
    "content": "application=org.odk.collect.location.RobolectricApplication\nsdk=33"
  },
  {
    "path": "mapbox/.gitignore",
    "content": "/build"
  },
  {
    "path": "mapbox/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    namespace = \"org.odk.collect.mapbox\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":androidshared\"))\n    implementation(project(\":icons\"))\n    implementation(project(\":location\"))\n    implementation(project(\":maps\"))\n    implementation(project(\":settings\"))\n    implementation(project(\":shared\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":async\"))\n    implementation(libs.playServicesLocation)\n    implementation(libs.androidxPreferenceKtx)\n    implementation(libs.mapboxAndroidSdk)\n    implementation(libs.timber)\n    implementation(libs.androidxStartup)\n\n    testImplementation(project(\":test-shared\"))\n    testImplementation(libs.junit)\n    testImplementation(libs.mockitoCore)\n    testImplementation(libs.hamcrest)\n}\n"
  },
  {
    "path": "mapbox/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <application>\n\n        <!-- Disable Mapbox initializers to prevent them running on devices that don't support it -->\n        <provider\n            android:name=\"androidx.startup.InitializationProvider\"\n            android:authorities=\"${applicationId}.androidx-startup\"\n            android:exported=\"false\"\n            tools:node=\"merge\">\n\n            <meta-data\n                android:name=\"com.mapbox.common.MapboxSDKCommonInitializer\"\n                tools:node=\"remove\" />\n\n            <meta-data\n                android:name=\"com.mapbox.maps.loader.MapboxMapsInitializer\"\n                tools:node=\"remove\" />\n        </provider>\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt",
    "content": "package org.odk.collect.mapbox\n\nimport android.content.Context\nimport com.mapbox.geojson.Point\nimport com.mapbox.maps.plugin.annotation.generated.OnPointAnnotationClickListener\nimport com.mapbox.maps.plugin.annotation.generated.OnPointAnnotationDragListener\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotation\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotation\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationOptions\nimport org.odk.collect.maps.traces.LineDescription\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.traces.getMarkersForPoints\n\ninternal class DynamicPolyLineFeature(\n    private val context: Context,\n    private val pointAnnotationManager: PointAnnotationManager,\n    private val polylineAnnotationManager: PolylineAnnotationManager,\n    private val featureId: Int,\n    private val featureClickListener: MapFragment.FeatureListener?,\n    private val featureDragEndListener: MapFragment.FeatureListener?,\n    private val lineDescription: LineDescription\n) : LineFeature {\n    override val points: List<MapPoint>\n        get() = _points.toList()\n\n    private val _points = mutableListOf<MapPoint>()\n    private val pointAnnotations = mutableListOf<PointAnnotation>()\n    private val pointAnnotationClickListener = ClickListener()\n    private val pointAnnotationDragListener = DragListener()\n    private var polylineAnnotation: PolylineAnnotation? = null\n\n    init {\n        val markerDescriptions = lineDescription.getMarkersForPoints()\n        markerDescriptions.forEach {\n            _points.add(it.point)\n            pointAnnotations.add(\n                MapUtils.createPointAnnotation(pointAnnotationManager, context, it)\n            )\n        }\n\n        updateLine()\n\n        pointAnnotationManager.addClickListener(pointAnnotationClickListener)\n        pointAnnotationManager.addDragListener(pointAnnotationDragListener)\n        polylineAnnotationManager.addClickListener { annotation ->\n            polylineAnnotation?.let {\n                if (annotation.id == it.id && featureClickListener != null) {\n                    featureClickListener.onFeature(featureId)\n                    true\n                } else {\n                    false\n                }\n            } ?: false\n        }\n    }\n\n    override fun dispose() {\n        pointAnnotationManager.apply {\n            removeClickListener(pointAnnotationClickListener)\n            removeDragListener(pointAnnotationDragListener)\n            delete(pointAnnotations)\n        }\n\n        polylineAnnotation?.let {\n            polylineAnnotationManager.delete(it)\n        }\n\n        pointAnnotations.clear()\n        _points.clear()\n    }\n\n    private fun updateLine() {\n        val points = points\n            .map {\n                Point.fromLngLat(it.longitude, it.latitude, it.altitude)\n            }\n            .toMutableList()\n\n        polylineAnnotation?.let {\n            polylineAnnotationManager.delete(it)\n        }\n\n        if (points.size > 1) {\n            polylineAnnotation = polylineAnnotationManager.create(\n                PolylineAnnotationOptions()\n                    .withPoints(points)\n                    .withLineColor(lineDescription.getStrokeColor())\n                    .withLineWidth(MapUtils.convertStrokeWidth(lineDescription))\n            ).also {\n                polylineAnnotationManager.update(it)\n            }\n        }\n    }\n\n    private inner class ClickListener : OnPointAnnotationClickListener {\n        override fun onAnnotationClick(annotation: PointAnnotation): Boolean {\n            for (pointAnnotation in pointAnnotations) {\n                if (annotation.id == pointAnnotation.id && featureClickListener != null) {\n                    featureClickListener.onFeature(featureId)\n                    return true\n                }\n            }\n            return false\n        }\n    }\n\n    private inner class DragListener : OnPointAnnotationDragListener {\n        override fun onAnnotationDragStarted(annotation: com.mapbox.maps.plugin.annotation.Annotation<*>) = Unit\n\n        override fun onAnnotationDrag(annotation: com.mapbox.maps.plugin.annotation.Annotation<*>) {\n            pointAnnotations.forEachIndexed { index, pointAnnotation ->\n                if (annotation.id == pointAnnotation.id) {\n                    _points[index] = MapUtils.mapPointFromPointAnnotation(pointAnnotation)\n                }\n            }\n            updateLine()\n        }\n\n        override fun onAnnotationDragFinished(annotation: com.mapbox.maps.plugin.annotation.Annotation<*>) {\n            onAnnotationDrag(annotation)\n            if (featureDragEndListener != null) {\n                for (pointAnnotation in pointAnnotations) {\n                    if (annotation.id == pointAnnotation.id) {\n                        featureDragEndListener.onFeature(featureId)\n                        break\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolygonFeature.kt",
    "content": "package org.odk.collect.mapbox\n\nimport android.content.Context\nimport com.mapbox.geojson.Point\nimport com.mapbox.maps.plugin.annotation.generated.OnPointAnnotationClickListener\nimport com.mapbox.maps.plugin.annotation.generated.OnPointAnnotationDragListener\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotation\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PolygonAnnotation\nimport com.mapbox.maps.plugin.annotation.generated.PolygonAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PolygonAnnotationOptions\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotation\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationOptions\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.traces.PolygonDescription\nimport org.odk.collect.maps.traces.getMarkersForPoints\n\ninternal class DynamicPolygonFeature(\n    private val context: Context,\n    private val pointAnnotationManager: PointAnnotationManager,\n    private val polygonAnnotationManager: PolygonAnnotationManager,\n    private val polylineAnnotationManager: PolylineAnnotationManager,\n    private val featureId: Int,\n    private val featureClickListener: MapFragment.FeatureListener?,\n    private val featureDragEndListener: MapFragment.FeatureListener?,\n    private val polygonDescription: PolygonDescription\n) : LineFeature {\n    override val points: List<MapPoint>\n        get() = _points.toList()\n\n    private val _points = mutableListOf<MapPoint>()\n    private val pointAnnotations = mutableListOf<PointAnnotation>()\n    private val pointAnnotationClickListener = ClickListener()\n    private val pointAnnotationDragListener = DragListener()\n    private var polygonAnnotation: PolygonAnnotation? = null\n    private var polylineAnnotation: PolylineAnnotation? = null\n\n    init {\n        val markerDescriptions = polygonDescription.getMarkersForPoints()\n        markerDescriptions.forEach {\n            _points.add(it.point)\n            pointAnnotations.add(\n                MapUtils.createPointAnnotation(pointAnnotationManager, context, it)\n            )\n        }\n\n        updateLine()\n\n        pointAnnotationManager.addClickListener(pointAnnotationClickListener)\n        pointAnnotationManager.addDragListener(pointAnnotationDragListener)\n        polygonAnnotationManager.addClickListener { annotation ->\n            polygonAnnotation?.let {\n                if (annotation.id == it.id && featureClickListener != null) {\n                    featureClickListener.onFeature(featureId)\n                    true\n                } else {\n                    false\n                }\n            } ?: false\n        }\n    }\n\n    override fun dispose() {\n        pointAnnotationManager.apply {\n            removeClickListener(pointAnnotationClickListener)\n            removeDragListener(pointAnnotationDragListener)\n            delete(pointAnnotations)\n        }\n\n        polygonAnnotation?.let {\n            polygonAnnotationManager.delete(it)\n        }\n\n        polylineAnnotation?.let {\n            polylineAnnotationManager.delete(it)\n        }\n\n        pointAnnotations.clear()\n        _points.clear()\n    }\n\n    private fun updateLine() {\n        val points = points\n            .map {\n                Point.fromLngLat(it.longitude, it.latitude, it.altitude)\n            }\n\n        polygonAnnotation?.let {\n            polygonAnnotationManager.delete(it)\n        }\n\n        if (points.size > 1) {\n            polygonAnnotation = polygonAnnotationManager.create(\n                PolygonAnnotationOptions()\n                    .withPoints(listOf(points))\n                    .withFillOutlineColor(polygonDescription.getStrokeColor())\n                    .withFillColor(polygonDescription.getFillColor())\n            ).also {\n                polygonAnnotationManager.update(it)\n            }\n        }\n\n        polylineAnnotation?.let {\n            polylineAnnotationManager.delete(it)\n        }\n\n        if (points.size > 1) {\n            polylineAnnotation = polylineAnnotationManager.create(\n                PolylineAnnotationOptions()\n                    .withPoints(points + points.first())\n                    .withLineColor(polygonDescription.getStrokeColor())\n                    .withLineWidth(MapUtils.convertStrokeWidth(polygonDescription))\n            ).also {\n                polylineAnnotationManager.update(it)\n            }\n        }\n    }\n\n    private inner class ClickListener : OnPointAnnotationClickListener {\n        override fun onAnnotationClick(annotation: PointAnnotation): Boolean {\n            for (pointAnnotation in pointAnnotations) {\n                if (annotation.id == pointAnnotation.id && featureClickListener != null) {\n                    featureClickListener.onFeature(featureId)\n                    return true\n                }\n            }\n            return false\n        }\n    }\n\n    private inner class DragListener : OnPointAnnotationDragListener {\n        override fun onAnnotationDragStarted(annotation: com.mapbox.maps.plugin.annotation.Annotation<*>) = Unit\n\n        override fun onAnnotationDrag(annotation: com.mapbox.maps.plugin.annotation.Annotation<*>) {\n            pointAnnotations.forEachIndexed { index, pointAnnotation ->\n                if (annotation.id == pointAnnotation.id) {\n                    _points[index] = MapUtils.mapPointFromPointAnnotation(pointAnnotation)\n                }\n            }\n            updateLine()\n        }\n\n        override fun onAnnotationDragFinished(annotation: com.mapbox.maps.plugin.annotation.Annotation<*>) {\n            onAnnotationDrag(annotation)\n            if (featureDragEndListener != null) {\n                for (pointAnnotation in pointAnnotations) {\n                    if (annotation.id == pointAnnotation.id) {\n                        featureDragEndListener.onFeature(featureId)\n                        break\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/LineFeature.kt",
    "content": "package org.odk.collect.mapbox\n\nimport org.odk.collect.maps.MapPoint\n\ninterface LineFeature : MapFeature {\n    val points: List<MapPoint>\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/MapBoxInitializationFragment.kt",
    "content": "package org.odk.collect.mapbox\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\nimport androidx.fragment.app.Fragment\nimport androidx.startup.AppInitializer\nimport com.mapbox.maps.MapView\nimport com.mapbox.maps.Style\nimport com.mapbox.maps.loader.MapboxMapsInitializer\nimport org.odk.collect.androidshared.data.getState\nimport org.odk.collect.async.network.NetworkStateProvider\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.MetaKeys\nimport org.odk.collect.shared.injection.ObjectProviderHost\n\nclass MapBoxInitializationFragment : Fragment() {\n\n    private val settingsProvider: SettingsProvider by lazy {\n        (requireActivity().applicationContext as ObjectProviderHost).getObjectProvider().provide(SettingsProvider::class.java)\n    }\n\n    private val connectivityProvider: NetworkStateProvider by lazy {\n        (requireActivity().applicationContext as ObjectProviderHost).getObjectProvider().provide(NetworkStateProvider::class.java)\n    }\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        initializeMapbox(context)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        val rootView = inflater.inflate(R.layout.mapbox_fragment_layout, container, false)\n        initMapBox(rootView)\n        return rootView\n    }\n\n    private fun initMapBox(rootView: View) {\n        val metaSharedPreferences = settingsProvider.getMetaSettings()\n        if (!metaSharedPreferences.getBoolean(MetaKeys.KEY_MAPBOX_INITIALIZED) && connectivityProvider.isDeviceOnline) {\n            // This \"one weird trick\" lets us initialize MapBox at app start when the internet is\n            // most likely to be available. This is annoyingly needed for offline tiles to work.\n            try {\n                val mapView = MapView(requireContext())\n                val mapBoxContainer = rootView.findViewById<FrameLayout>(R.id.map_box_container)\n                mapBoxContainer.addView(mapView)\n                mapView.getMapboxMap().loadStyleUri(\n                    Style.MAPBOX_STREETS\n                ) {\n                    metaSharedPreferences.save(\n                        MetaKeys.KEY_MAPBOX_INITIALIZED,\n                        true\n                    )\n                }\n            } catch (ignored: Throwable) {\n                // This will crash on devices where the arch for MapBox is not included\n            }\n        }\n    }\n\n    private fun initializeMapbox(context: Context) {\n        // Auto initialization is disabled to stop x86 devices from crashing so we do this manually\n        context.getState().let {\n            if (!it.get(KEY_MAPBOX_INITIALIZED, false)) {\n                AppInitializer.getInstance(context)\n                    .initializeComponent(MapboxMapsInitializer::class.java)\n                it.set(KEY_MAPBOX_INITIALIZED, true)\n            }\n        }\n    }\n\n    companion object {\n        private const val KEY_MAPBOX_INITIALIZED = \"mapbox_initialized\"\n    }\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/MapFeature.kt",
    "content": "package org.odk.collect.mapbox\n\n/**\n * A MapFeature is a physical feature on a map, such as a point, a road,\n * a building, a region, etc.  It is presented to the user as one editable\n * object, though its appearance may be constructed from multiple overlays\n * (e.g. geometric elements, handles for manipulation, etc.).\n */\ninterface MapFeature {\n    /** Removes the feature from the map, leaving it no longer usable.  */\n    fun dispose()\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt",
    "content": "package org.odk.collect.mapbox\n\nimport android.content.Context\nimport com.mapbox.geojson.Point\nimport com.mapbox.maps.extension.style.layers.properties.generated.IconAnchor\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotation\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.traces.TraceDescription\nimport org.odk.collect.maps.markers.MarkerDescription\nimport org.odk.collect.maps.markers.MarkerIconCreator.toBitmap\n\nobject MapUtils {\n    fun createPointAnnotation(\n        pointAnnotationManager: PointAnnotationManager,\n        context: Context,\n        markerDescription: MarkerDescription\n    ): PointAnnotation {\n        return pointAnnotationManager.create(\n            PointAnnotationOptions()\n                .withPoint(Point.fromLngLat(markerDescription.point.longitude, markerDescription.point.latitude, markerDescription.point.altitude))\n                .withIconImage(markerDescription.iconDescription.toBitmap(context))\n                .withIconSize(1.0)\n                .withSymbolSortKey(10.0)\n                .withDraggable(markerDescription.isDraggable)\n                .withTextOpacity(0.0)\n                .withIconAnchor(getIconAnchorValue(markerDescription.iconAnchor))\n        )\n    }\n\n    fun createPointAnnotations(\n        context: Context,\n        pointAnnotationManager: PointAnnotationManager,\n        markerFeatures: List<MarkerDescription>\n    ): List<PointAnnotation> {\n        val pointAnnotationOptionsList = markerFeatures.map {\n            PointAnnotationOptions()\n                .withPoint(Point.fromLngLat(it.point.longitude, it.point.latitude, it.point.altitude))\n                .withIconImage(it.iconDescription.toBitmap(context))\n                .withIconSize(1.0)\n                .withSymbolSortKey(10.0)\n                .withDraggable(it.isDraggable)\n                .withTextOpacity(0.0)\n                .withIconAnchor(getIconAnchorValue(it.iconAnchor))\n        }\n\n        return pointAnnotationManager.create(pointAnnotationOptionsList)\n    }\n\n    private fun getIconAnchorValue(iconAnchor: MapFragment.IconAnchor): IconAnchor {\n        return when (iconAnchor) {\n            MapFragment.IconAnchor.BOTTOM -> IconAnchor.BOTTOM\n            else -> IconAnchor.CENTER\n        }\n    }\n\n    fun mapPointFromPointAnnotation(pointAnnotation: PointAnnotation): MapPoint {\n        // When a symbol is manually dragged, the position is no longer\n        // obtained from a GPS reading, so the altitude and standard\n        // deviation fields are no longer meaningful; reset them to zero.\n        return MapPoint(pointAnnotation.point.latitude(), pointAnnotation.point.longitude(), 0.0, 0.0)\n    }\n\n    // To ensure consistent stroke width across map platforms like Mapbox, Google, and OSM,\n    // the value for Mapbox needs to be divided by 3.\n    fun convertStrokeWidth(traceDescription: TraceDescription): Double {\n        return (traceDescription.getStrokeWidth() / 3).toDouble()\n    }\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapConfigurator.java",
    "content": "package org.odk.collect.mapbox;\n\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_MAPBOX_MAP_STYLE;\nimport static kotlin.collections.SetsKt.setOf;\n\nimport android.content.Context;\nimport android.os.Bundle;\n\nimport androidx.preference.Preference;\n\nimport com.mapbox.maps.Style;\n\nimport org.odk.collect.androidshared.system.OpenGLVersionChecker;\nimport org.odk.collect.androidshared.ui.PrefUtils;\nimport org.odk.collect.androidshared.ui.ToastUtils;\nimport org.odk.collect.maps.MapConfigurator;\nimport org.odk.collect.maps.layers.MbtilesFile;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.io.File;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\n\npublic class MapboxMapConfigurator implements MapConfigurator {\n    private final String prefKey;\n    private final int sourceLabelId;\n    private final MapboxUrlOption[] options;\n\n    /** Constructs a configurator with a few Mapbox style URL options to choose from. */\n    public MapboxMapConfigurator() {\n        this.prefKey = KEY_MAPBOX_MAP_STYLE;\n        this.sourceLabelId = org.odk.collect.strings.R.string.basemap_source_mapbox;\n        this.options = new MapboxUrlOption[]{\n                new MapboxUrlOption(Style.MAPBOX_STREETS, org.odk.collect.strings.R.string.streets),\n                new MapboxUrlOption(Style.LIGHT, org.odk.collect.strings.R.string.light),\n                new MapboxUrlOption(Style.DARK, org.odk.collect.strings.R.string.dark),\n                new MapboxUrlOption(Style.SATELLITE, org.odk.collect.strings.R.string.satellite),\n                new MapboxUrlOption(Style.SATELLITE_STREETS, org.odk.collect.strings.R.string.hybrid),\n                new MapboxUrlOption(Style.OUTDOORS, org.odk.collect.strings.R.string.outdoors)\n        };\n    }\n\n    @Override public boolean isAvailable(Context context) {\n        /*\n         * The Mapbox SDK for Android requires OpenGL ES version 3.\n         * See: https://github.com/mapbox/mapbox-maps-android/blob/main/CHANGELOG.md#1100-november-29-2023\n         */\n        return OpenGLVersionChecker.isOpenGLv3Supported(context);\n    }\n\n    @Override public void showUnavailableMessage(Context context) {\n        ToastUtils.showLongToast(context.getString(org.odk.collect.strings.R.string.basemap_source_unavailable, context.getString(sourceLabelId)));\n    }\n\n    @Override public List<Preference> createPrefs(Context context, Settings settings) {\n        int[] labelIds = new int[options.length];\n        String[] values = new String[options.length];\n        for (int i = 0; i < options.length; i++) {\n            labelIds[i] = options[i].labelId;\n            values[i] = options[i].url;\n        }\n        String prefTitle = context.getString(\n            org.odk.collect.strings.R.string.map_style_label, context.getString(sourceLabelId));\n        return Collections.singletonList(PrefUtils.createListPref(\n            context, prefKey, prefTitle, labelIds, values, settings\n        ));\n    }\n\n    @Override public Set<String> getPrefKeys() {\n        return prefKey.isEmpty() ? setOf(ProjectKeys.KEY_REFERENCE_LAYER) :\n                setOf(prefKey, ProjectKeys.KEY_REFERENCE_LAYER);\n    }\n\n    @Override public Bundle buildConfig(Settings prefs) {\n        Bundle config = new Bundle();\n        config.putString(MapboxMapFragment.KEY_STYLE_URL,\n            prefs.getString(ProjectKeys.KEY_MAPBOX_MAP_STYLE));\n        config.putString(MapboxMapFragment.KEY_REFERENCE_LAYER,\n            prefs.getString(ProjectKeys.KEY_REFERENCE_LAYER));\n        return config;\n    }\n\n    @Override public boolean supportsLayer(File file) {\n        // MapboxMapFragment supports any file that MbtilesFile can read.\n        return MbtilesFile.readLayerType(file) != null;\n    }\n\n    @Override public String getDisplayName(File file) {\n        String name = MbtilesFile.readName(file);\n        return name != null ? name : file.getName();\n    }\n\n    static class MapboxUrlOption {\n        final String url;\n        final int labelId;\n\n        MapboxUrlOption(String url, int labelId) {\n            this.url = url;\n            this.labelId = labelId;\n        }\n    }\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt",
    "content": "package org.odk.collect.mapbox\n\nimport android.graphics.Color\nimport android.location.Location\nimport android.os.Bundle\nimport android.view.Gravity\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.content.res.AppCompatResources\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport androidx.startup.AppInitializer\nimport com.google.android.gms.location.LocationListener\nimport com.mapbox.android.gestures.MoveGestureDetector\nimport com.mapbox.android.gestures.StandardScaleGestureDetector\nimport com.mapbox.geojson.Point\nimport com.mapbox.maps.EdgeInsets\nimport com.mapbox.maps.MapView\nimport com.mapbox.maps.MapboxMap\nimport com.mapbox.maps.Style\nimport com.mapbox.maps.dsl.cameraOptions\nimport com.mapbox.maps.extension.style.layers.Layer\nimport com.mapbox.maps.extension.style.layers.addLayerAbove\nimport com.mapbox.maps.extension.style.layers.generated.LineLayer\nimport com.mapbox.maps.extension.style.layers.generated.RasterLayer\nimport com.mapbox.maps.extension.style.sources.Source\nimport com.mapbox.maps.extension.style.sources.TileSet\nimport com.mapbox.maps.extension.style.sources.addSource\nimport com.mapbox.maps.extension.style.sources.generated.RasterSource\nimport com.mapbox.maps.extension.style.sources.generated.VectorSource\nimport com.mapbox.maps.extension.style.sources.getSource\nimport com.mapbox.maps.loader.MapboxMapsInitializer\nimport com.mapbox.maps.plugin.LocationPuck2D\nimport com.mapbox.maps.plugin.animation.MapAnimationOptions.Companion.mapAnimationOptions\nimport com.mapbox.maps.plugin.animation.flyTo\nimport com.mapbox.maps.plugin.annotation.annotations\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotation\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PolygonAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.createPolygonAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.createPolylineAnnotationManager\nimport com.mapbox.maps.plugin.compass.compass\nimport com.mapbox.maps.plugin.gestures.OnMapClickListener\nimport com.mapbox.maps.plugin.gestures.OnMapLongClickListener\nimport com.mapbox.maps.plugin.gestures.OnMoveListener\nimport com.mapbox.maps.plugin.gestures.OnScaleListener\nimport com.mapbox.maps.plugin.gestures.addOnMapClickListener\nimport com.mapbox.maps.plugin.gestures.addOnMapLongClickListener\nimport com.mapbox.maps.plugin.gestures.addOnMoveListener\nimport com.mapbox.maps.plugin.gestures.addOnScaleListener\nimport com.mapbox.maps.plugin.locationcomponent.location\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.odk.collect.androidshared.utils.ScreenUtils\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.location.LocationClient.LocationClientListener\nimport org.odk.collect.maps.traces.LineDescription\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapFragment.ErrorListener\nimport org.odk.collect.maps.MapFragment.FeatureListener\nimport org.odk.collect.maps.MapFragment.PointListener\nimport org.odk.collect.maps.MapFragment.ReadyListener\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.MapViewModel\nimport org.odk.collect.maps.MapViewModelMapFragment\nimport org.odk.collect.maps.traces.PolygonDescription\nimport org.odk.collect.maps.Zoom\nimport org.odk.collect.maps.ZoomObserver\nimport org.odk.collect.maps.circles.CircleDescription\nimport org.odk.collect.maps.layers.MapFragmentReferenceLayerUtils.getReferenceLayerFile\nimport org.odk.collect.maps.layers.MbtilesFile\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.maps.markers.MarkerDescription\nimport org.odk.collect.maps.markers.MarkerIconCreator\nimport org.odk.collect.maps.markers.MarkerIconDescription\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.shared.injection.ObjectProviderHost\nimport timber.log.Timber\nimport java.io.File\nimport java.io.IOException\n\nclass MapboxMapFragment :\n    MapViewModelMapFragment(),\n    OnMapClickListener,\n    OnMapLongClickListener,\n    LocationListener,\n    LocationClientListener {\n\n    private lateinit var mapView: MapView\n    private lateinit var mapboxMap: MapboxMap\n\n    private lateinit var pointAnnotationManager: PointAnnotationManager\n    private lateinit var polylineAnnotationManager: PolylineAnnotationManager\n    private lateinit var polygonAnnotationManager: PolygonAnnotationManager\n    private var mapReadyListener: ReadyListener? = null\n    private val gpsLocationReadyListeners = mutableListOf<ReadyListener>()\n\n    private var nextFeatureId = 1\n    private val features = mutableMapOf<Int, MapFeature>()\n\n    private var gpsLocationListener: PointListener? = null\n    private var clickListener: PointListener? = null\n    private var longPressListener: PointListener? = null\n\n    private var featureClickListener: FeatureListener? = null\n    private var featureDragEndListener: FeatureListener? = null\n\n    private var lastLocationProvider: String? = null\n    private var lastLocationFix: MapPoint? = null\n    private var tileServer: TileHttpServer? = null\n    private var referenceLayerFile: File? = null\n    private var clientWantsLocationUpdates = false\n    private var topStyleLayerId: String? = null\n\n    private val _mapViewModel by viewModels<MapViewModel> {\n        viewModelFactory {\n            addInitializer(MapViewModel::class) {\n                MapViewModel(\n                    settingsProvider.getUnprotectedSettings(),\n                    settingsProvider.getMetaSettings()\n                )\n            }\n        }\n    }\n\n    private val settingsProvider: SettingsProvider by lazy {\n        (requireActivity().applicationContext as ObjectProviderHost).getObjectProvider().provide(SettingsProvider::class.java)\n    }\n\n    private val referenceLayerRepository: ReferenceLayerRepository by lazy {\n        (requireActivity().applicationContext as ObjectProviderHost).getObjectProvider().provide(ReferenceLayerRepository::class.java)\n    }\n\n    private val locationClient: LocationClient by lazy {\n        (requireActivity().applicationContext as ObjectProviderHost).getObjectProvider().provide(LocationClient::class.java)\n    }\n\n    override fun init(readyListener: ReadyListener?, errorListener: ErrorListener?) {\n        mapReadyListener = readyListener\n\n        // Mapbox SDK only knows how to fetch tiles via HTTP. If we want it to\n        // display tiles from a local file, we have to serve them locally over HTTP.\n        try {\n            tileServer = TileHttpServer().also {\n                it.start()\n            }\n        } catch (e: IOException) {\n            Timber.e(e, \"Could not start the TileHttpServer\")\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        AppInitializer.getInstance(requireContext()).initializeComponent(MapboxMapsInitializer::class.java)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        mapView = MapView(inflater.context).apply {\n            compass.position = Gravity.TOP or Gravity.START\n            compass.marginTop = 36f\n            compass.marginBottom = 36f\n            compass.marginLeft = 36f\n            compass.marginRight = 36f\n        }\n\n        mapboxMap = mapView\n            .getMapboxMap()\n            .apply {\n                addOnMapClickListener(this@MapboxMapFragment)\n                addOnMapLongClickListener(this@MapboxMapFragment)\n                addOnScaleListener(object : OnScaleListener {\n                    override fun onScale(detector: StandardScaleGestureDetector) = Unit\n\n                    override fun onScaleBegin(detector: StandardScaleGestureDetector) = Unit\n\n                    override fun onScaleEnd(detector: StandardScaleGestureDetector) {\n                        val center = MapPoint(cameraState.center.latitude(), cameraState.center.longitude())\n                        getMapViewModel().onUserZoom(center, cameraState.zoom)\n                    }\n                })\n                addOnMoveListener(object : OnMoveListener {\n                    override fun onMove(detector: MoveGestureDetector): Boolean = false\n                    override fun onMoveBegin(detector: MoveGestureDetector) = Unit\n\n                    override fun onMoveEnd(detector: MoveGestureDetector) {\n                        val center = MapPoint(cameraState.center.latitude(), cameraState.center.longitude())\n                        getMapViewModel().onUserMove(center, cameraState.zoom)\n                    }\n                })\n            }\n\n        polylineAnnotationManager = mapView\n            .annotations\n            .createPolylineAnnotationManager()\n\n        polygonAnnotationManager = mapView\n            .annotations\n            .createPolygonAnnotationManager()\n\n        pointAnnotationManager = mapView\n            .annotations\n            .createPointAnnotationManager()\n\n        initLocationComponent()\n        moveOrAnimateCamera(MapFragment.INITIAL_CENTER, false, MapFragment.INITIAL_ZOOM.toDouble())\n\n        // If the screen is rotated before the map is ready, this fragment could already be detached,\n        // which makes it unsafe to use. Only call the ReadyListener if this fragment is still attached.\n        if (mapReadyListener != null && activity != null) {\n            mapReadyListener!!.onReady(this)\n        }\n\n        val mapConfigurator = MapboxMapConfigurator()\n        getMapViewModel().getSettings(mapConfigurator.prefKeys).observe(viewLifecycleOwner) {\n            val newConfig = mapConfigurator.buildConfig(it)\n            onConfigChanged(newConfig)\n        }\n\n        getMapViewModel().zoom.observe(viewLifecycleOwner, object : ZoomObserver() {\n            override fun onZoomToPoint(zoom: Zoom.Point) {\n                moveOrAnimateCamera(zoom.point, zoom.animate, zoom.level)\n            }\n\n            override fun onZoomToBox(zoom: Zoom.Box) {\n                val points = zoom.box.map {\n                    Point.fromLngLat(it.longitude, it.latitude, it.altitude)\n                }\n\n                val screenWidth = ScreenUtils.getScreenWidth(context)\n                val screenHeight = ScreenUtils.getScreenHeight(context)\n\n                lifecycleScope.launch {\n                    delay(100L)\n                    mapboxMap.setCamera(\n                        mapboxMap.cameraForCoordinates(\n                            points,\n                            EdgeInsets(\n                                screenHeight / 5.0,\n                                screenWidth / 5.0,\n                                screenHeight / 5.0,\n                                screenWidth / 5.0\n                            )\n                        )\n                    )\n                }\n            }\n        })\n\n        return mapView\n    }\n\n    override fun onResume() {\n        super.onResume()\n        enableLocationUpdates(clientWantsLocationUpdates)\n    }\n\n    override fun onPause() {\n        super.onPause()\n        enableLocationUpdates(false)\n    }\n\n    override fun onDestroy() {\n        tileServer?.destroy()\n        MarkerIconCreator.clearCache()\n        super.onDestroy()\n    }\n\n    private fun onConfigChanged(config: Bundle) {\n        val styleUrl = config.getString(KEY_STYLE_URL) ?: Style.MAPBOX_STREETS\n        referenceLayerFile = getReferenceLayerFile(config, referenceLayerRepository)\n        mapboxMap.loadStyleUri(styleUrl) {\n            if (topStyleLayerId == null) {\n                // remember the id of the top style layer\n                topStyleLayerId = it.styleLayers.last().id\n            }\n            loadReferenceOverlay()\n        }\n    }\n\n    override fun getCenter(): MapPoint {\n        val point = mapboxMap.cameraState.center\n        return MapPoint(point.latitude(), point.longitude())\n    }\n\n    override fun getZoom(): Double {\n        return mapboxMap.cameraState.zoom\n    }\n\n    override fun getMapViewModel(): MapViewModel {\n        return _mapViewModel\n    }\n\n    override fun addMarker(markerDescription: MarkerDescription): Int {\n        return addMarkers(listOf(markerDescription)).first()\n    }\n\n    override fun updateMarker(\n        featureId: Int,\n        markerDescription: MarkerDescription\n    ) {\n        features[featureId]?.dispose()\n        val pointAnnotation = MapUtils.createPointAnnotation(\n            pointAnnotationManager,\n            requireContext(),\n            markerDescription\n        )\n\n        addMarker(featureId, markerDescription, pointAnnotation)\n    }\n\n    override fun addMarkers(markers: List<MarkerDescription>): List<Int> {\n        val pointAnnotations =\n            MapUtils.createPointAnnotations(requireContext(), pointAnnotationManager, markers)\n\n        val featureIds = mutableListOf<Int>()\n        markers.asSequence()\n            .zip(pointAnnotations.asSequence())\n            .forEach { (marker, pointAnnotation) ->\n                val featureId = nextFeatureId++\n                featureIds.add(featureId)\n\n                addMarker(featureId, marker, pointAnnotation)\n            }\n\n        return featureIds\n    }\n\n    private fun addMarker(\n        featureId: Int,\n        marker: MarkerDescription,\n        pointAnnotation: PointAnnotation\n    ) {\n        val markerFeature = MarkerFeature(\n            requireContext(),\n            pointAnnotationManager,\n            pointAnnotation,\n            featureId,\n            featureClickListener,\n            featureDragEndListener,\n            marker.point\n        )\n\n        features[featureId] = markerFeature\n    }\n\n    override fun setMarkerIcon(featureId: Int, markerIconDescription: MarkerIconDescription) {\n        val feature = features[featureId]\n        if (feature is MarkerFeature) {\n            feature.setIcon(markerIconDescription)\n        }\n    }\n\n    override fun getMarkerPoint(featureId: Int): MapPoint? {\n        val feature = features[featureId]\n        return if (feature is MarkerFeature) {\n            feature.point\n        } else {\n            null\n        }\n    }\n\n    override fun addPolyLine(lineDescription: LineDescription): Int {\n        val featureId = nextFeatureId++\n        addPolyLine(featureId, lineDescription)\n        return featureId\n    }\n\n    override fun updatePolyLine(featureId: Int, lineDescription: LineDescription) {\n        features[featureId]?.dispose()\n        addPolyLine(featureId, lineDescription)\n    }\n\n    private fun addPolyLine(\n        featureId: Int,\n        lineDescription: LineDescription\n    ) {\n        if (lineDescription.draggable) {\n            features[featureId] = DynamicPolyLineFeature(\n                requireContext(),\n                pointAnnotationManager,\n                polylineAnnotationManager,\n                featureId,\n                featureClickListener,\n                featureDragEndListener,\n                lineDescription\n            )\n        } else {\n            features[featureId] = StaticPolyLineFeature(\n                polylineAnnotationManager,\n                featureId,\n                featureClickListener,\n                lineDescription\n            )\n        }\n    }\n\n    override fun addPolygon(polygonDescription: PolygonDescription): Int {\n        val featureId = nextFeatureId++\n        addPolygon(featureId, polygonDescription)\n\n        return featureId\n    }\n\n    private fun addPolygon(\n        featureId: Int,\n        polygonDescription: PolygonDescription\n    ) {\n        if (polygonDescription.draggable) {\n            features[featureId] = DynamicPolygonFeature(\n                requireContext(),\n                pointAnnotationManager,\n                polygonAnnotationManager,\n                polylineAnnotationManager,\n                featureId,\n                featureClickListener,\n                featureDragEndListener,\n                polygonDescription\n            )\n        } else {\n            features[featureId] = StaticPolygonFeature(\n                polygonAnnotationManager,\n                polylineAnnotationManager,\n                polygonDescription,\n                featureClickListener,\n                featureId\n            )\n        }\n    }\n\n    override fun updatePolygon(\n        featureId: Int,\n        polygonDescription: PolygonDescription\n    ) {\n        features[featureId]?.dispose()\n        addPolygon(featureId, polygonDescription)\n    }\n\n    override fun addCircle(circleDescription: CircleDescription): Int {\n        return -1\n    }\n\n    override fun updateCircle(\n        featureId: Int,\n        circleDescription: CircleDescription\n    ) {\n\n    }\n\n    override fun getPolyPoints(featureId: Int): List<MapPoint> {\n        val feature = features[featureId]\n        return if (feature is LineFeature) {\n            feature.points\n        } else {\n            emptyList()\n        }\n    }\n\n    override fun clearFeatures() {\n        for (feature in features.values) {\n            feature.dispose()\n        }\n\n        features.clear()\n        nextFeatureId = 1\n    }\n\n    override fun setClickListener(listener: PointListener?) {\n        clickListener = listener\n    }\n\n    override fun setLongPressListener(listener: PointListener?) {\n        longPressListener = listener\n    }\n\n    override fun setFeatureClickListener(listener: FeatureListener?) {\n        featureClickListener = listener\n    }\n\n    override fun setDragEndListener(listener: FeatureListener?) {\n        featureDragEndListener = listener\n    }\n\n    override fun setGpsLocationEnabled(enabled: Boolean) {\n        if (enabled != clientWantsLocationUpdates) {\n            clientWantsLocationUpdates = enabled\n            enableLocationUpdates(clientWantsLocationUpdates)\n        }\n    }\n\n    override fun getGpsLocation(): MapPoint? {\n        return lastLocationFix\n    }\n\n    override fun setGpsLocationListener(listener: PointListener?) {\n        gpsLocationListener = listener\n    }\n\n    override fun setRetainMockAccuracy(retainMockAccuracy: Boolean) {\n        locationClient.setRetainMockAccuracy(retainMockAccuracy)\n    }\n\n    override fun onMapClick(point: Point): Boolean {\n        clickListener?.onPoint(MapPoint(point.latitude(), point.longitude()))\n\n        // MAPBOX ISSUE: Unfortunately, onMapClick is called before onAnnotationClick,\n        // which means that every click on a marker will also cause a click event on\n        // the map. Returning true will consume the event and prevent the marker's\n        // onAnnotationClick from ever being called, so we have to return false.\n        return false\n    }\n\n    override fun onMapLongClick(point: Point): Boolean {\n        longPressListener?.onPoint(MapPoint(point.latitude(), point.longitude()))\n        return true\n    }\n\n    override fun onLocationChanged(location: Location) {\n        lastLocationFix = MapPoint(\n            location.latitude,\n            location.longitude,\n            location.altitude,\n            location.accuracy.toDouble()\n        )\n        lastLocationProvider = location.provider\n        Timber.i(\n            \"Received location update: %s (%s)\",\n            lastLocationFix,\n            lastLocationProvider\n        )\n        for (listener in gpsLocationReadyListeners) {\n            listener.onReady(this)\n        }\n        gpsLocationReadyListeners.clear()\n        gpsLocationListener?.onPoint(lastLocationFix!!)\n    }\n\n    @SuppressWarnings(\"MissingPermission\") // permission checks for location services are handled in widgets\n    private fun enableLocationUpdates(enabled: Boolean) {\n        if (enabled) {\n            Timber.i(\"Starting LocationClient %s (for MapFragment %s)\", locationClient, this)\n            locationClient.start(this)\n        } else {\n            Timber.i(\"Stopping LocationClient %s (for MapFragment %s)\", locationClient, this)\n            locationClient.stop()\n        }\n\n        mapView.location.enabled = enabled\n    }\n\n    private fun initLocationComponent() {\n        mapView.location.updateSettings {\n            this.enabled = true\n            this.locationPuck = LocationPuck2D(\n                AppCompatResources.getDrawable(\n                    requireContext(),\n                    org.odk.collect.maps.R.drawable.ic_crosshairs\n                )\n            )\n        }\n    }\n\n    private fun moveOrAnimateCamera(point: MapPoint, animate: Boolean, zoom: Double? = getZoom()) {\n        mapboxMap.flyTo(\n            cameraOptions {\n                center(Point.fromLngLat(point.longitude, point.latitude, point.altitude))\n                zoom(zoom)\n            },\n            mapAnimationOptions {\n                duration(if (animate) 300 else 0)\n            }\n        )\n    }\n\n    private fun loadReferenceOverlay() {\n        referenceLayerFile?.let {\n            addMbtiles(it.name, referenceLayerFile!!)\n        }\n    }\n\n    private fun addMbtiles(id: String, file: File) {\n        tileServer?.let {\n            val mbtiles: MbtilesFile = try {\n                MbtilesFile(file)\n            } catch (e: MbtilesFile.MbtilesException) {\n                Timber.w(e.message)\n                return\n            }\n\n            val tileSet = createTileSet(mbtiles, it.getUrlTemplate(id))\n            it.addSource(id, mbtiles)\n\n            if (mbtiles.layerType == MbtilesFile.LayerType.VECTOR) {\n                addOverlaySource(VectorSource.Builder(id).tileSet(tileSet).build())\n                for (layer in mbtiles.vectorLayers) {\n                    // Pick a colour that's a function of the filename and layer name.\n                    // The colour will appear essentially random; the only purpose here\n                    // is to try to assign different colours to different layers, such\n                    // that each individual layer appears in its own consistent colour.\n                    val hue = ((id + \".\" + layer.name).hashCode() and 0x7fffffff) % 360\n                    addOverlayLayer(\n                        LineLayer(id + \".\" + layer.name, id)\n                            .lineColor(Color.HSVToColor(floatArrayOf(hue.toFloat(), 0.7f, 1f)))\n                            .lineWidth(1.0)\n                            .lineOpacity(0.7)\n                            .sourceLayer(layer.name)\n                    )\n                }\n            }\n            if (mbtiles.layerType == MbtilesFile.LayerType.RASTER) {\n                addOverlaySource(RasterSource.Builder(id).tileSet(tileSet).build())\n                addOverlayLayer(RasterLayer(id + \".raster\", id))\n            }\n            Timber.i(\"Added %s as a %s layer at /%s\", file, mbtiles.layerType, id)\n        }\n    }\n\n    private fun createTileSet(mbtiles: MbtilesFile, urlTemplate: String): TileSet {\n        val tileSet = TileSet.Builder(\"2.2.0\", listOf(urlTemplate))\n\n        // Configure the TileSet using the metadata in the .mbtiles file.\n        try {\n            tileSet.name(mbtiles.getMetadata(\"name\"))\n            try {\n                tileSet.minZoom(mbtiles.getMetadata(\"minzoom\").toInt())\n                tileSet.maxZoom(mbtiles.getMetadata(\"maxzoom\").toInt())\n            } catch (e: NumberFormatException) {\n                // ignore\n            }\n            var parts = mbtiles.getMetadata(\"center\").split(\",\").toTypedArray()\n            if (parts.size == 3) { // latitude, longitude, zoom\n                try {\n                    tileSet.center(\n                        listOf(\n                            parts[0].toDouble(),\n                            parts[1].toDouble(),\n                            parts[2].toDouble()\n                        )\n                    )\n                } catch (e: NumberFormatException) {\n                    // ignore\n                }\n            }\n            parts = mbtiles.getMetadata(\"bounds\").split(\",\").toTypedArray()\n            if (parts.size == 4) { // left, bottom, right, top\n                try {\n                    tileSet.bounds(\n                        listOf(\n                            parts[0].toDouble(),\n                            parts[1].toDouble(),\n                            parts[2].toDouble(),\n                            parts[3].toDouble()\n                        )\n                    )\n                } catch (e: NumberFormatException) {\n                    // ignore\n                }\n            }\n        } catch (e: MbtilesFile.MbtilesException) {\n            Timber.w(e.message)\n        }\n        return tileSet.build()\n    }\n\n    private fun addOverlayLayer(layer: Layer) {\n        topStyleLayerId?.let {\n            mapboxMap.getStyle()?.addLayerAbove(layer, topStyleLayerId)\n        }\n    }\n\n    private fun addOverlaySource(source: Source) {\n        if (mapboxMap.getStyle()?.getSource(source.sourceId) == null) {\n            mapboxMap.getStyle()?.addSource(source)\n        }\n    }\n\n    override fun onClientStart() {\n        Timber.i(\"Requesting location updates (to %s)\", this)\n        locationClient.requestLocationUpdates(this)\n    }\n\n    override fun onClientStartFailure() {\n    }\n\n    override fun onClientStop() {\n    }\n\n    companion object {\n        const val KEY_STYLE_URL = \"STYLE_URL\"\n    }\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt",
    "content": "package org.odk.collect.mapbox\n\nimport android.content.Context\nimport com.mapbox.maps.plugin.annotation.generated.OnPointAnnotationClickListener\nimport com.mapbox.maps.plugin.annotation.generated.OnPointAnnotationDragListener\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotation\nimport com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.markers.MarkerIconCreator.toBitmap\nimport org.odk.collect.maps.markers.MarkerIconDescription\n\n/** A point annotation that can optionally be dragged by the user. */\nclass MarkerFeature(\n    val context: Context,\n    private val pointAnnotationManager: PointAnnotationManager,\n    private val pointAnnotation: PointAnnotation,\n    val featureId: Int,\n    private val featureClickListener: MapFragment.FeatureListener?,\n    private val featureDragEndListener: MapFragment.FeatureListener?,\n    var point: MapPoint\n) : MapFeature {\n    private val clickListener = ClickListener()\n    private val dragListener = DragListener()\n\n    init {\n        pointAnnotationManager.apply {\n            addClickListener(clickListener)\n            addDragListener(dragListener)\n        }\n    }\n\n    fun setIcon(markerIconDescription: MarkerIconDescription) {\n        pointAnnotation.iconImageBitmap = markerIconDescription.toBitmap(context)\n        pointAnnotationManager.update(pointAnnotation)\n    }\n\n    override fun dispose() {\n        pointAnnotationManager.apply {\n            removeClickListener(clickListener)\n            removeDragListener(dragListener)\n            delete(pointAnnotation)\n        }\n    }\n\n    private inner class ClickListener : OnPointAnnotationClickListener {\n        override fun onAnnotationClick(annotation: PointAnnotation): Boolean {\n            if (annotation.id == pointAnnotation.id && featureClickListener != null) {\n                featureClickListener.onFeature(featureId)\n                return true\n            }\n            return false\n        }\n    }\n\n    private inner class DragListener : OnPointAnnotationDragListener {\n        override fun onAnnotationDragStarted(annotation: com.mapbox.maps.plugin.annotation.Annotation<*>) = Unit\n\n        override fun onAnnotationDrag(annotation: com.mapbox.maps.plugin.annotation.Annotation<*>) {\n            if (annotation.id == pointAnnotation.id) {\n                point = MapUtils.mapPointFromPointAnnotation(annotation as PointAnnotation)\n            }\n        }\n\n        override fun onAnnotationDragFinished(annotation: com.mapbox.maps.plugin.annotation.Annotation<*>) {\n            onAnnotationDrag(annotation)\n            if (annotation.id == pointAnnotation.id && featureDragEndListener != null) {\n                featureDragEndListener.onFeature(featureId)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/StaticPolyLineFeature.kt",
    "content": "package org.odk.collect.mapbox\n\nimport com.mapbox.geojson.Point\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotation\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationOptions\nimport org.odk.collect.maps.traces.LineDescription\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\n\n/** A polyline that can not be manipulated by dragging Symbols at its vertices. */\ninternal class StaticPolyLineFeature(\n    private val polylineAnnotationManager: PolylineAnnotationManager,\n    private val featureId: Int,\n    private val featureClickListener: MapFragment.FeatureListener?,\n    private val lineDescription: LineDescription\n) : LineFeature {\n    override val points = mutableListOf<MapPoint>()\n    private var polylineAnnotation: PolylineAnnotation? = null\n\n    init {\n        lineDescription.points.forEach {\n            points.add(it)\n        }\n\n        val points = points\n            .map {\n                Point.fromLngLat(it.longitude, it.latitude, it.altitude)\n            }\n            .toMutableList()\n\n        polylineAnnotation?.let {\n            polylineAnnotationManager.delete(it)\n        }\n\n        if (points.size > 1) {\n            polylineAnnotation = polylineAnnotationManager.create(\n                PolylineAnnotationOptions()\n                    .withPoints(points)\n                    .withLineColor(lineDescription.getStrokeColor())\n                    .withLineWidth(MapUtils.convertStrokeWidth(lineDescription))\n            ).also {\n                polylineAnnotationManager.update(it)\n            }\n        }\n\n        polylineAnnotationManager.addClickListener { annotation ->\n            polylineAnnotation?.let {\n                if (annotation.id == it.id && featureClickListener != null) {\n                    featureClickListener.onFeature(featureId)\n                    true\n                } else {\n                    false\n                }\n            } ?: false\n        }\n    }\n\n    override fun dispose() {\n        polylineAnnotation?.let {\n            polylineAnnotationManager.delete(it)\n        }\n        points.clear()\n    }\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/StaticPolygonFeature.kt",
    "content": "package org.odk.collect.mapbox\n\nimport com.mapbox.geojson.Point\nimport com.mapbox.maps.plugin.annotation.generated.OnPolygonAnnotationClickListener\nimport com.mapbox.maps.plugin.annotation.generated.OnPolylineAnnotationClickListener\nimport com.mapbox.maps.plugin.annotation.generated.PolygonAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PolygonAnnotationOptions\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationManager\nimport com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationOptions\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.traces.PolygonDescription\n\nclass StaticPolygonFeature(\n    private val polygonAnnotationManager: PolygonAnnotationManager,\n    private val polylineAnnotationManager: PolylineAnnotationManager,\n    polygonDescription: PolygonDescription,\n    featureClickListener: MapFragment.FeatureListener?,\n    featureId: Int\n) : LineFeature {\n\n    override val points: List<MapPoint> = polygonDescription.points\n\n    private val mapboxPoints = points.map { Point.fromLngLat(it.longitude, it.latitude) }\n\n    private val polygonAnnotation = polygonAnnotationManager.create(\n        PolygonAnnotationOptions()\n            .withPoints(listOf(mapboxPoints))\n            .withFillOutlineColor(polygonDescription.getStrokeColor())\n            .withFillColor(polygonDescription.getFillColor())\n    )\n\n    private val polylineAnnotation = polylineAnnotationManager.create(\n        PolylineAnnotationOptions()\n            .withPoints(mapboxPoints)\n            .withLineColor(polygonDescription.getStrokeColor())\n            .withLineWidth(MapUtils.convertStrokeWidth(polygonDescription))\n    )\n\n    private val polygonClickListener = OnPolygonAnnotationClickListener { annotation ->\n        if (annotation.id == polygonAnnotation.id) {\n            featureClickListener?.onFeature(featureId)\n            true\n        } else {\n            false\n        }\n    }.also {\n        polygonAnnotationManager.addClickListener(it)\n    }\n\n    private val polylineClickListener = OnPolylineAnnotationClickListener { annotation ->\n        if (annotation.id == polylineAnnotation.id) {\n            featureClickListener?.onFeature(featureId)\n            true\n        } else {\n            false\n        }\n    }.also(polylineAnnotationManager::addClickListener)\n\n    override fun dispose() {\n        polygonAnnotationManager.run {\n            delete(polygonAnnotation)\n            removeClickListener(polygonClickListener)\n        }\n\n        polylineAnnotationManager.run {\n            delete(polylineAnnotation)\n            removeClickListener(polylineClickListener)\n        }\n    }\n}\n"
  },
  {
    "path": "mapbox/src/main/java/org/odk/collect/mapbox/TileHttpServer.java",
    "content": "package org.odk.collect.mapbox;\n\nimport org.odk.collect.maps.layers.TileSource;\n\nimport java.io.BufferedReader;\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.io.UnsupportedEncodingException;\nimport java.net.BindException;\nimport java.net.ServerSocket;\nimport java.net.Socket;\nimport java.net.URLDecoder;\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Map;\n\nimport timber.log.Timber;\n\n/** A minimal HTTP server that serves tiles from a set of TileSources. */\nclass TileHttpServer {\n    private static final int PORT_MIN = 8000;\n    private static final int PORT_MAX = 8999;\n\n    private final Map<String, TileSource> sources = new HashMap<>();\n    private final ServerThread server;\n    private final ServerSocket socket;\n\n    TileHttpServer() throws IOException {\n        socket = createBoundSocket(PORT_MIN, PORT_MAX);\n        if (socket == null) {\n            throw new IOException(\"Could not find an available port\");\n        }\n        server = new ServerThread(socket);\n    }\n\n    public void start() {\n        server.start();\n    }\n\n    /**\n     * Constructs a URL template for fetching tiles from this server for a given\n     * tileset, with placeholders {z} for zoom level and {x} and {y} for coordinates.\n     */\n    public String getUrlTemplate(String key) {\n        return String.format(\n            Locale.US, \"http://localhost:%d/%s/{z}/{x}/{y}\", socket.getLocalPort(), key);\n    }\n\n    /**\n     * Adds a TileSource with a given key.  Tiles from this source will be served\n     * under the URL path /{key}/{zoom}/{x}/{y}.  If this TileSource implements\n     * Closeable, it will be closed when this server is finalized with destroy().\n     */\n    public void addSource(String key, TileSource source) {\n        sources.put(key, source);\n    }\n\n    /** Permanently closes all sockets and closeable TileSources. */\n    public void destroy() {\n        try {\n            socket.close();\n        } catch (IOException e) { /* ignore */ }\n        server.interrupt();\n        for (TileSource source : sources.values()) {\n            if (source instanceof Closeable) {\n                try {\n                    ((Closeable) source).close();\n                } catch (IOException e) { /* ignore */ }\n            }\n        }\n    }\n\n    /** Finds an available port and binds a ServerSocket to it. */\n    protected static ServerSocket createBoundSocket(int portMin, int portMax) throws IOException {\n        for (int port = portMin; port <= portMax; port++) {\n            try {\n                return new ServerSocket(port);\n            } catch (BindException e) {\n                continue;  // this port is in use; try another one\n            }\n        }\n        Timber.e(new Error(\"No ports available from \" + portMin + \" to \" + portMax));\n        return null;\n    }\n\n    class ServerThread extends Thread {\n        final ServerSocket socket;\n\n        ServerThread(ServerSocket socket) {\n            this.socket = socket;\n        }\n\n        public void run() {\n            try {\n                socket.setReuseAddress(true);\n                Timber.i(\"Ready for requests on port %d\", socket.getLocalPort());\n                while (!isInterrupted()) {\n                    Socket connection = socket.accept();\n                    Timber.i(\"Accepted a client connection\");\n                    new ResponseThread(connection).start();\n                }\n                Timber.i(\"Server thread interrupted\");\n            } catch (IOException e) {\n                Timber.i(\"Server thread stopped: %s\", e.getMessage());\n            }\n        }\n    }\n\n    class ResponseThread extends Thread {\n        final Socket connection;\n\n        ResponseThread(Socket connection) {\n            this.connection = connection;\n        }\n\n        public void run() {\n            try (Socket connection = this.connection) {\n                InputStreamReader reader = new InputStreamReader(connection.getInputStream());\n                String request = new BufferedReader(reader).readLine();\n                Timber.i(\"Received request: %s\", request);\n                if (request == null) {\n                    return;\n                }\n                long start = System.currentTimeMillis();\n                Response response = getResponse(request);\n                if (response == null) {\n                    Timber.i(\"%s: No tile at these coordinates\", request);\n                    return;\n                }\n                sendResponse(connection, response);\n                long finish = System.currentTimeMillis();\n                Timber.i(\"%s: Served %d bytes in %d ms\", request, response.data.length, finish - start);\n            } catch (IOException e) {\n                Timber.e(e, \"Unable to read request from socket\");\n            }\n        }\n\n        protected Response getResponse(String request) {\n            if (request.startsWith(\"GET /\")) {\n                String path = request.substring(5).split(\" \", 2)[0];\n                String[] parts = path.split(\"/\");\n                if (parts.length == 4) {\n                    try {\n                        String key = URLDecoder.decode(parts[0], \"utf-8\");\n                        int zoom = Integer.parseInt(parts[1]);\n                        int x = Integer.parseInt(parts[2]);\n                        int y = Integer.parseInt(parts[3]);\n                        TileSource source = sources.get(key);\n                        if (source != null) {\n                            byte[] data = source.getTileBlob(zoom, x, y);\n                            if (data != null) {\n                                return new Response(data, source.getContentType(), source.getContentEncoding());\n                            }\n                        }\n                    } catch (NumberFormatException e) {\n                        Timber.w(e, \"Bad request %s\", request);\n                    } catch (UnsupportedEncodingException e) { /* cannot happen because UTF-8 is built in */ }\n                }\n            }\n            Timber.w(\"Ignoring request: %s\", request);\n            return null;\n        }\n\n        protected void sendResponse(Socket connection, Response response) {\n            String headers = String.format(\n                Locale.US,\n                \"HTTP/1.0 200\\r\\n\" +\n                    \"Content-Type: %s\\r\\n\" +\n                    \"Content-Encoding: %s\\r\\n\" +\n                    \"Content-Length: %d\\r\\n\" +\n                    \"\\r\\n\",\n                response.contentType,\n                response.contentEncoding,\n                response.data.length\n            );\n\n            try (OutputStream output = connection.getOutputStream()) {\n                output.write(headers.getBytes());\n                output.write(response.data);\n                output.flush();\n            } catch (IOException e) {\n                Timber.e(e, \"Unable to write response to socket\");\n            }\n        }\n    }\n\n    public static class Response {\n        byte[] data;\n        String contentType;\n        String contentEncoding;\n\n        Response(byte[] data, String contentType, String contentEncoding) {\n            this.data = data;\n            this.contentType = contentType;\n            this.contentEncoding = contentEncoding;\n        }\n    }\n}\n"
  },
  {
    "path": "mapbox/src/main/res/layout/mapbox_fragment_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/map_box_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"1dp\"\n    android:visibility=\"invisible\">\n\n</FrameLayout>"
  },
  {
    "path": "maps/.gitignore",
    "content": "/build"
  },
  {
    "path": "maps/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinParcelize)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n\n    namespace = \"org.odk.collect.maps\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":async\"))\n    implementation(project(\":shared\"))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":icons\"))\n    implementation(project(\":material\"))\n    implementation(project(\":settings\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":web-page\"))\n    implementation(project(\":analytics\"))\n    implementation(project(\":lists\"))\n    implementation(libs.androidMaterial)\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxFragmentKtx)\n    implementation(libs.androidxPreferenceKtx)\n    implementation(libs.timber)\n\n    debugImplementation(project(\":fragments-test\"))\n\n    testImplementation(project(\":androidtest\"))\n    testImplementation(project(\":test-shared\"))\n    testImplementation(libs.junit)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.androidxTestEspressoContrib)\n    testImplementation(libs.androidxTestEspressoCore)\n}\n"
  },
  {
    "path": "maps/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/AnalyticsEvents.kt",
    "content": "package org.odk.collect.maps\n\nobject AnalyticsEvents {\n\n    /**\n     * Tracks how many offline layers people are importing at once\n     */\n    const val IMPORT_LAYER_SINGLE = \"ImportLayerSingle\" // One\n    const val IMPORT_LAYER_FEW = \"ImportLayerFew\" // <= 5\n    const val IMPORT_LAYER_MANY = \"ImportLayerMany\" // > 5\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/MapConfigurator.kt",
    "content": "package org.odk.collect.maps\n\nimport android.content.Context\nimport android.os.Bundle\nimport androidx.preference.Preference\nimport org.odk.collect.shared.settings.Settings\nimport java.io.File\n\n/**\n * For each MapFragment implementation class, there is one instance of this\n * interface that encapsulates the details of how to configure that MapFragment\n * and reconfigure it as settings are changed.  In particular, the buildConfig()\n * method builds a Bundle object containing configuration details, which the\n * MapFragment.applyConfig() method should understand.  Each MapConfigurator can\n * define its own preference widgets for customizing the MapFragment, and\n * those preferences will be inserted on the Maps preference screen.\n * For example, the GoogleMapConfigurator can define a \"Google map style\"\n * preference with choices such as Terrain or Satellite.\n */\ninterface MapConfigurator {\n    /** Returns true if this MapFragment implementation is available on this device.  */\n    fun isAvailable(context: Context): Boolean\n\n    /**\n     * Displays a warning to the user that this MapFragment implementation is\n     * unavailable.  This will be invoked when isSupported() is false or\n     * createMapFragment(context) returns null.\n     */\n    fun showUnavailableMessage(context: Context)\n\n    /** Constructs any preference widgets that are specific to this map implementation.  */\n    fun createPrefs(context: Context, settings: Settings): List<Preference>\n\n    /** Gets the set of keys for preferences that should be watched for changes.  */\n    val prefKeys: Collection<String>\n\n    /** Packs map-related preferences into a Bundle for MapFragment.applyConfig().  */\n    fun buildConfig(prefs: Settings): Bundle\n\n    /**\n     * Returns true if map fragments obtained from this MapConfigurator are\n     * expected to be able to render the given file as an overlay.  This\n     * check determines which files appear as available Reference Layers.\n     */\n    fun supportsLayer(file: File): Boolean\n\n    /** Returns a String name for a given overlay file, or null if unsupported.  */\n    fun getDisplayName(file: File): String\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/MapConsts.kt",
    "content": "package org.odk.collect.maps\n\nimport androidx.core.graphics.toColorInt\n\nobject MapConsts {\n    @JvmField\n    val DEFAULT_STROKE_COLOR = \"#3e9fcc\".toColorInt()\n\n    @JvmField\n    val DEFAULT_HIGHLIGHT_COLOR = \"#1F5976\".toColorInt()\n\n    val DEFAULT_ERROR_COLOR = \"#ba1a1a\".toColorInt()\n\n    const val DEFAULT_STROKE_WIDTH = 8f\n    const val DEFAULT_FILL_COLOR_OPACITY = 68\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/MapFragment.kt",
    "content": "package org.odk.collect.maps\n\nimport org.odk.collect.maps.circles.CircleDescription\nimport org.odk.collect.maps.traces.LineDescription\nimport org.odk.collect.maps.markers.MarkerDescription\nimport org.odk.collect.maps.markers.MarkerIconDescription\nimport org.odk.collect.maps.traces.PolygonDescription\n\n/**\n * Interface for a Fragment that renders a map view.  The plan is to have one\n * implementation for each map SDK, e.g. GoogleMapFragment, OsmDroidMapFragment, etc.\n *\n * This is intended to be a single map API that provides all functionality needed\n * for the three geo widgets (collecting or editing a point, a trace, or a shape):\n *   - Basic control of the viewport (panning, zooming)\n *   - Displaying and getting the current GPS location\n *   - Requesting a callback on the first GPS location fix\n *   - Requesting callbacks for short clicks and long presses on the map\n *   - Adding editable points to the map\n *   - Adding editable traces (polylines) to the map\n *   - Adding editable shapes (closed polygons) to the map\n *\n * Editable points, traces, and shapes are called \"map features\" in this API.\n * To keep the API small, features are not exposed as objects; instead, they are\n * identified by integer feature IDs.  To keep the API unified (instead of having\n * three distinct modes), the map always supports all three kinds of features,\n * even though the geo widgets only use one kind of feature at a time.\n */\ninterface MapFragment {\n\n    fun init(readyListener: ReadyListener?, errorListener: ErrorListener?)\n\n    /** Gets the point currently shown at the center of the map view.  */\n    fun getCenter(): MapPoint\n\n    /**\n     * Gets the current zoom level.  For maps that only support zooming by\n     * powers of 2, the zoom level will always be an integer.\n     */\n    fun getZoom(): Double\n\n    /**\n     * Centers the map view on the given point, leaving zoom level unchanged,\n     * possibly with animation.\n     */\n    fun setCenter(center: MapPoint?, animate: Boolean)\n\n    /**\n     * Centers the map view on the current location, zooming in to the last zoom level set by the\n     * user if available, or to a close-up level deemed appropriate by\n     * the implementation, possibly with animation.\n     */\n    fun zoomToCurrentLocation(center: MapPoint?)\n\n    /**\n     * Centers the map view on the given point, zooming in to a close-up level\n     * deemed appropriate by the implementation, possibly with animation.\n     */\n    fun zoomToPoint(center: MapPoint?, animate: Boolean)\n\n    /**\n     * Centers the map view on the given point with a zoom level as close as\n     * possible to the given zoom level, possibly with animation.\n     */\n    fun zoomToPoint(center: MapPoint?, zoom: Double, animate: Boolean)\n\n    /**\n     * Adjusts the map's viewport to enclose all of the given points, possibly\n     * with animation.  A scaleFactor of 1.0 ensures that all the points will be\n     * just visible in the viewport; a scaleFactor less than 1 shrinks the view\n     * beyond that.  For example, a scaleFactor of 0.8 causes the bounding box\n     * to occupy at most 80% of the width and 80% of the height of the viewport,\n     * ensuring a margin of at least 10% on all sides.\n     */\n    fun zoomToBoundingBox(points: Iterable<MapPoint>, scaleFactor: Double, animate: Boolean)\n\n    /**\n     * Adds a marker to the map at the given location. If draggable is true,\n     * the user will be able to drag the marker to change its location.\n     * Returns a positive integer, the featureId for the newly added shape.\n     */\n    fun addMarker(markerDescription: MarkerDescription): Int\n    fun updateMarker(featureId: Int, markerDescription: MarkerDescription)\n\n    fun addMarkers(markers: List<MarkerDescription>): List<Int>\n\n    /** Sets the icon for a marker.  */\n    @Deprecated(message = \"Use #updateMarker instead\")\n    fun setMarkerIcon(featureId: Int, markerIconDescription: MarkerIconDescription)\n\n    /** Gets the location of an existing marker.  */\n    fun getMarkerPoint(featureId: Int): MapPoint?\n\n    /**\n     * Adds a polyline to the map with the given sequence of vertices.\n     * The vertices will have handles that can be dragged by the user.\n     * Returns a positive integer, the featureId for the newly added shape.\n     */\n    fun addPolyLine(lineDescription: LineDescription): Int\n    fun updatePolyLine(featureId: Int, lineDescription: LineDescription)\n\n    /**\n     * Adds a polygon to the map with given sequence of vertices. * Returns a positive integer,\n     * the featureId for the newly added shape.\n     */\n    fun addPolygon(polygonDescription: PolygonDescription): Int\n    fun updatePolygon(featureId: Int, polygonDescription: PolygonDescription)\n\n    fun addCircle(circleDescription: CircleDescription): Int\n    fun updateCircle(featureId: Int, circleDescription: CircleDescription)\n\n    /**\n     * Returns the vertices of the polyline or polygon specified by featureId, or an\n     * empty list if the featureId does not identify an existing polyline or polygon.\n     */\n    fun getPolyPoints(featureId: Int): List<MapPoint>\n\n    /** Removes all map features from the map.  */\n    fun clearFeatures()\n\n    /** Sets or clears the callback for a click on the map.  */\n    fun setClickListener(listener: PointListener?)\n\n    /** Sets or clears the callback for a long press on the map.  */\n    fun setLongPressListener(listener: PointListener?)\n\n    /** Sets or clears the callback for a click on a feature.  */\n    fun setFeatureClickListener(listener: FeatureListener?)\n\n    /** Sets or clears the callback for when a drag is completed.  */\n    fun setDragEndListener(listener: FeatureListener?)\n\n    /**\n     * Enables/disables GPS tracking.  While enabled, the GPS location is shown\n     * on the map, the first GPS fix will trigger any pending callbacks set by\n     * runOnGpsLocationReady(), and every GPS fix will invoke the callback set\n     * by setGpsLocationListener().\n     */\n    @Deprecated(message = \"Location should be handled outside of MapFragment\")\n    fun setGpsLocationEnabled(enabled: Boolean)\n\n    /** Gets the last GPS location fix, or null if there hasn't been one.  */\n    @Deprecated(message = \"Location should be handled outside of MapFragment\")\n    fun getGpsLocation(): MapPoint?\n\n    /**\n     * Sets or clears the callback for GPS location updates.  This callback\n     * will only be invoked while GPS is enabled with setGpsLocationEnabled().\n     */\n    @Deprecated(message = \"Location should be handled outside of MapFragment\")\n    fun setGpsLocationListener(listener: PointListener?)\n\n    @Deprecated(message = \"Location should be handled outside of MapFragment\")\n    fun setRetainMockAccuracy(retainMockAccuracy: Boolean)\n\n    /**\n     * @return true if the [MapFragment] center has already been set (by [MapFragment.zoomToPoint] for instance).\n     */\n    fun hasCenter(): Boolean\n\n    fun interface ErrorListener {\n        fun onError()\n    }\n\n    fun interface ReadyListener {\n        fun onReady(mapFragment: MapFragment)\n    }\n\n    fun interface PointListener {\n        fun onPoint(point: MapPoint)\n    }\n\n    fun interface FeatureListener {\n        fun onFeature(featureId: Int)\n    }\n\n    companion object {\n        val INITIAL_CENTER: MapPoint = MapPoint(0.0, -30.0)\n        const val INITIAL_ZOOM: Float = 2f\n        const val POINT_ZOOM: Float = 16f\n\n        const val KEY_REFERENCE_LAYER: String = \"REFERENCE_LAYER\"\n    }\n\n    enum class IconAnchor { CENTER, BOTTOM }\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/MapFragmentFactory.kt",
    "content": "package org.odk.collect.maps\n\ninterface MapFragmentFactory {\n    fun createMapFragment(): MapFragment\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/MapPoint.kt",
    "content": "/*\n * Copyright (C) 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage org.odk.collect.maps\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\nimport org.odk.collect.shared.geometry.Point\n\n@Parcelize\ndata class MapPoint @JvmOverloads constructor(\n    @JvmField val latitude: Double,\n    @JvmField val longitude: Double,\n    @JvmField val altitude: Double = 0.0,\n    @JvmField val accuracy: Double = 0.0\n) : Parcelable\n\nfun MapPoint.toPoint(): Point {\n    return Point(latitude, longitude)\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/MapViewModel.kt",
    "content": "package org.odk.collect.maps\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MediatorLiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport org.odk.collect.settings.keys.MetaKeys.LAST_KNOWN_ZOOM_LEVEL\nimport org.odk.collect.shared.settings.Settings\n\nclass MapViewModel(\n    private val unprotectedSettings: Settings,\n    private val metaSettings: Settings\n) : ViewModel(),\n    Settings.OnSettingChangeListener {\n\n    private var userZoomLevel: Double? = null\n\n    private val _zoom = MutableLiveData<Zoom?>()\n    val zoom: LiveData<Zoom?> = _zoom\n\n    private val lastSettingsKeyChange = MutableLiveData<String>(null)\n\n    init {\n        unprotectedSettings.registerOnSettingChangeListener(this)\n    }\n\n    fun zoomTo(point: MapPoint?, level: Double?, animate: Boolean) {\n        if (point != null) {\n            _zoom.value = Zoom.Point(point, level ?: DEFAULT_ZOOM, animate, false)\n        }\n    }\n\n    fun zoomTo(boundingBox: List<MapPoint>, scaleFactor: Double, animate: Boolean) {\n        _zoom.value = Zoom.Box(boundingBox, scaleFactor, _zoom.value?.level ?: DEFAULT_ZOOM, animate)\n    }\n\n    fun zoomToCurrentLocation(location: MapPoint?) {\n        if (location == null) {\n            return\n        }\n\n        val level = if (userZoomLevel != null) {\n            userZoomLevel\n        } else if (metaSettings.contains(LAST_KNOWN_ZOOM_LEVEL)) {\n            metaSettings.getFloat(LAST_KNOWN_ZOOM_LEVEL).toDouble()\n        } else {\n            DEFAULT_ZOOM\n        }\n\n        zoomTo(location, level, true)\n    }\n\n    fun moveTo(location: MapPoint?, animate: Boolean) {\n        if (location != null) {\n            _zoom.value = Zoom.Point(location, _zoom.value?.level ?: DEFAULT_ZOOM, animate, false)\n        }\n    }\n\n    fun onUserZoom(point: MapPoint, level: Double) {\n        userZoomLevel = level\n        _zoom.value = Zoom.Point(point, level, animate = false, user = true)\n    }\n\n    fun onUserMove(point: MapPoint, level: Double) {\n        _zoom.value = Zoom.Point(point, level, animate = false, user = true)\n    }\n\n    fun getSettings(keys: Collection<String>): LiveData<Settings> {\n        return MediatorLiveData<Settings>().apply {\n            addSource(lastSettingsKeyChange) {\n                if (it == null || keys.contains(it)) {\n                    this.value = unprotectedSettings\n                }\n            }\n        }\n    }\n\n    override fun onCleared() {\n        userZoomLevel?.let {\n            metaSettings.save(LAST_KNOWN_ZOOM_LEVEL, it.toFloat())\n        }\n\n        unprotectedSettings.unregisterOnSettingChangeListener(this)\n    }\n\n    override fun onSettingChanged(key: String) {\n        lastSettingsKeyChange.value = key\n    }\n\n    companion object {\n        const val DEFAULT_ZOOM: Double = 16.0\n    }\n}\n\nsealed class Zoom {\n\n    abstract val level: Double\n    abstract val animate: Boolean\n    abstract val user: Boolean\n\n    data class Point(\n        val point: MapPoint,\n        override val level: Double,\n        override val animate: Boolean,\n        override val user: Boolean\n    ) : Zoom()\n\n    data class Box(\n        val box: List<MapPoint>,\n        val scaleFactor: Double,\n        override val level: Double,\n        override val animate: Boolean\n    ) : Zoom() {\n        override val user = false\n    }\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/MapViewModelMapFragment.kt",
    "content": "package org.odk.collect.maps\n\nimport androidx.fragment.app.Fragment\n\n/**\n * Convenience abstract class for implementing a [MapFragment] using [MapViewModel] to handle\n * common behaviour and state.\n */\nabstract class MapViewModelMapFragment : Fragment(), MapFragment {\n\n    protected abstract fun getMapViewModel(): MapViewModel\n\n    final override fun setCenter(center: MapPoint?, animate: Boolean) {\n        getMapViewModel().moveTo(center, animate)\n    }\n\n    final override fun zoomToPoint(center: MapPoint?, animate: Boolean) {\n        getMapViewModel().zoomTo(center, null, animate)\n    }\n\n    final override fun zoomToPoint(center: MapPoint?, zoom: Double, animate: Boolean) {\n        getMapViewModel().zoomTo(center, zoom, animate)\n    }\n\n    final override fun zoomToBoundingBox(\n        points: Iterable<MapPoint>,\n        scaleFactor: Double,\n        animate: Boolean\n    ) {\n        getMapViewModel().zoomTo(points.toList(), scaleFactor, animate)\n    }\n\n    final override fun zoomToCurrentLocation(center: MapPoint?) {\n        getMapViewModel().zoomToCurrentLocation(center)\n    }\n\n    final override fun hasCenter(): Boolean {\n        return getMapViewModel().zoom.getValue() != null\n    }\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/ZoomObserver.kt",
    "content": "package org.odk.collect.maps\n\nimport androidx.lifecycle.Observer\n\nabstract class ZoomObserver : Observer<Zoom?> {\n\n    abstract fun onZoomToPoint(zoom: Zoom.Point)\n    abstract fun onZoomToBox(zoom: Zoom.Box)\n\n    private var isFirstChange = true\n\n    override fun onChanged(value: Zoom?) {\n        if (value == null) {\n            return\n        }\n\n        /**\n         * Ignore zooms from the user unless it's the first change that this observer is dealing\n         * with. In that case, we always want to use the value as we're rendering a fresh map.\n         */\n        if (!value.user || isFirstChange) {\n            when (value) {\n                is Zoom.Box -> onZoomToBox(value)\n                is Zoom.Point -> onZoomToPoint(value)\n            }\n        }\n\n        isFirstChange = false\n    }\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/circles/CircleDescription.kt",
    "content": "package org.odk.collect.maps.circles\n\nimport androidx.core.graphics.ColorUtils\nimport org.odk.collect.maps.MapConsts\nimport org.odk.collect.maps.MapPoint\n\ndata class CircleDescription(\n    val center: MapPoint,\n    val radius: Float,\n    val color: Int = MapConsts.DEFAULT_STROKE_COLOR\n)\n\nfun CircleDescription.getStrokeColor(): Int {\n    return color\n}\n\nfun CircleDescription.getFillColor(): Int {\n    return ColorUtils.setAlphaComponent(color, MapConsts.DEFAULT_FILL_COLOR_OPACITY)\n}"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/circles/CurrentLocationDelegate.kt",
    "content": "package org.odk.collect.maps.circles\n\nimport org.odk.collect.maps.MapConsts\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.markers.MarkerDescription\nimport org.odk.collect.maps.markers.MarkerIconDescription\n\n/**\n * Helper for rendering a current location icon with accuracy \"halo\" on a [MapFragment] as well\n * as zooming/moving the map on location updates.\n */\nclass CurrentLocationDelegate {\n\n    private var currentLocation: MapPoint? = null\n    private var locationMarkerId: Int? = null\n    private var accuracyHaloId: Int? = null\n\n    fun update(map: MapFragment, location: MapPoint, follow: Boolean) {\n        currentLocation = location\n\n        val markerDescription = MarkerDescription(\n            location,\n            false,\n            MapFragment.IconAnchor.CENTER,\n            MarkerIconDescription.DrawableResource(org.odk.collect.maps.R.drawable.ic_crosshairs)\n        )\n\n        locationMarkerId.let {\n            if (it == null) {\n                locationMarkerId = map.addMarker(markerDescription)\n            } else {\n                map.updateMarker(it, markerDescription)\n            }\n        }\n\n\n        val circleDescription = CircleDescription(location, location.accuracy.toFloat())\n        accuracyHaloId.let {\n            if (it == null) {\n                accuracyHaloId = map.addCircle(circleDescription)\n            } else {\n                map.updateCircle(it, circleDescription)\n            }\n        }\n\n        if (!map.hasCenter()) {\n            map.zoomToCurrentLocation(location)\n        }\n\n        if (follow) {\n            map.setCenter(location, false)\n        }\n    }\n\n    fun zoomToCurrentLocation(map: MapFragment) {\n        map.zoomToCurrentLocation(currentLocation)\n    }\n}"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport org.odk.collect.maps.MapConfigurator\nimport org.odk.collect.shared.PathUtils\nimport org.odk.collect.shared.files.FileExt.listFilesRecursively\nimport java.io.File\n\nclass DirectoryReferenceLayerRepository(\n    private val sharedLayersDirPath: String,\n    private val projectLayersDirPath: String,\n    private val getMapConfigurator: () -> MapConfigurator\n) : ReferenceLayerRepository {\n\n    override fun getAll(): List<ReferenceLayer> {\n        return getAllFilesWithDirectory()\n            .map { ReferenceLayer(getIdForFile(it.second, it.first), it.first, getName(it.first)) }\n            .distinctBy { it.id }\n            .filter { getMapConfigurator().supportsLayer(it.file) }\n    }\n\n    override fun get(id: String): ReferenceLayer? {\n        val file = getAllFilesWithDirectory().firstOrNull { getIdForFile(it.second, it.first) == id }\n\n        return if (file != null && getMapConfigurator().supportsLayer(file.first)) {\n            ReferenceLayer(getIdForFile(file.second, file.first), file.first, getName(file.first))\n        } else {\n            null\n        }\n    }\n\n    override fun addLayer(file: File, shared: Boolean) {\n        if (shared) {\n            file.copyTo(File(sharedLayersDirPath, file.name), true)\n        } else {\n            file.copyTo(File(projectLayersDirPath, file.name), true)\n        }\n    }\n\n    override fun delete(id: String) {\n        get(id)?.file?.delete()\n    }\n\n    private fun getAllFilesWithDirectory() = listOf(sharedLayersDirPath, projectLayersDirPath).flatMap { dir ->\n        File(dir).listFilesRecursively().map { file ->\n            Pair(file, dir)\n        }\n    }\n\n    fun getIdForFile(directoryPath: String, file: File) =\n        PathUtils.getRelativeFilePath(directoryPath, file.absolutePath)\n\n    private fun getName(file: File): String {\n        return getMapConfigurator().getDisplayName(file)\n    }\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtils.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport android.os.Bundle\nimport org.odk.collect.maps.MapFragment\nimport java.io.File\n\nobject MapFragmentReferenceLayerUtils {\n\n    @JvmStatic\n    fun getReferenceLayerFile(\n        config: Bundle,\n        layerRepository: ReferenceLayerRepository\n    ): File? {\n        val filePath = config.getString(MapFragment.KEY_REFERENCE_LAYER)\n        return if (filePath != null) {\n            val referenceLayer = layerRepository.get(filePath)\n            referenceLayer?.file\n        } else {\n            null\n        }\n    }\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/layers/MbtilesFile.java",
    "content": "package org.odk.collect.maps.layers;\n\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport androidx.annotation.NonNull;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\n\nimport timber.log.Timber;\n\nimport static android.database.sqlite.SQLiteDatabase.NO_LOCALIZED_COLLATORS;\nimport static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;\n\n/**\n * This class provides access to the metadata and tiles in an .mbtiles file.\n * An .mbtiles file is a SQLite database file containing specific tables and\n * columns, including tiles that may contain raster images or vector geometry.\n * See https://github.com/mapbox/mbtiles-spec for the detailed specification.\n */\npublic class MbtilesFile implements Closeable, TileSource {\n    public static final String FILE_EXTENSION = \".mbtiles\";\n\n    public enum LayerType { RASTER, VECTOR }\n\n    private final File file;\n    private final LayerType layerType;\n    private final String contentType;\n    private final String contentEncoding;\n    private SQLiteDatabase db;  // see getTileBlob for why this is not final\n\n    public MbtilesFile(File file) throws MbtilesException {\n        this(file, detectContentType(file));\n    }\n\n    private MbtilesFile(File file, String contentType) throws MbtilesException {\n        this.file = file;\n        this.db = openSqliteReadOnly(file);\n        this.contentType = contentType;\n        switch (contentType) {\n            case \"application/protobuf\":\n                contentEncoding = \"gzip\";\n                layerType = LayerType.VECTOR;\n                return;\n            case \"image/jpeg\":\n            case \"image/png\":\n                contentEncoding = \"identity\";\n                layerType = LayerType.RASTER;\n                return;\n        }\n        throw new MbtilesException(String.format(\n            \"Unrecognized content type \\\"%s\\\" in %s\", contentType, file));\n    }\n\n    public String getContentType() {\n        return contentType;\n    }\n\n    public String getContentEncoding() {\n        return contentEncoding;\n    }\n\n    public LayerType getLayerType() {\n        return layerType;\n    }\n\n    public @NonNull String getMetadata(String key) throws MbtilesException {\n        return queryMetadata(db, key);\n    }\n\n    public void close() {\n        db.close();\n    }\n\n    /** Fetches a tile out of the .mbtiles SQLite database. */\n    public byte[] getTileBlob(int zoom, int x, int y) {\n        // TMS coordinates are used in .mbtiles files, so Y needs to be flipped.\n        y = (1 << zoom) - 1 - y;\n\n        // We have to use String.format because the templating mechanism in\n        // SQLiteDatabase.query is written for a strange alternate universe\n        // in which numbers don't exist -- it only supports strings!\n        String selection = String.format(\n            Locale.US,\n            \"zoom_level = %d and tile_column = %d and tile_row = %d\",\n            zoom, x, y\n        );\n\n        try (Cursor results = db.query(\"tiles\", new String[] {\"tile_data\"},\n            selection, null, null, null, null)) {\n            if (results.moveToFirst()) {\n                try {\n                    return results.getBlob(0);\n                } catch (IllegalStateException e) {\n                    Timber.w(e, \"Could not select tile data at zoom=%d, x=%d, y=%d\", zoom, x, y);\n                    // In Android, the SQLite cursor can handle at most 2 MB in one row;\n                    // exceeding 2 MB in an .mbtiles file is rare, but it can happen.\n                    // When an attempt to fetch a large row fails, the database ends up\n                    // in an unusable state, so we need to close it and reopen it.\n                    // See https://stackoverflow.com/questions/20094421/cursor-window-window-is-full\n                    db.close();\n                    db = openSqliteReadOnly(file);\n                }\n            }\n        } catch (Throwable e) {\n            Timber.w(e);\n        }\n        return null;\n    }\n\n    /** Returns information about the vector layers available in the tiles. */\n    public List<VectorLayer> getVectorLayers() {\n        List<VectorLayer> layers = new ArrayList<>();\n        JSONArray jsonLayers;\n        try {\n            JSONObject json = new JSONObject(getMetadata(\"json\"));\n            jsonLayers = json.getJSONArray(\"vector_layers\");\n            for (int i = 0; i < jsonLayers.length(); i++) {\n                layers.add(new VectorLayer(jsonLayers.getJSONObject(i)));\n            }\n        } catch (MbtilesException | JSONException e) {\n            Timber.w(e);\n        }\n        return layers;\n    }\n\n    /** Reads the internal name from an MBTiles file, or null if the file is invalid. */\n    public static String readName(File file) {\n        try {\n            return new MbtilesFile(file).getMetadata(\"name\");\n        } catch (MbtilesException e) {\n            return null;\n        }\n    }\n\n    /** Reads the layer type from an MBTiles file, or null if the file is invalid. */\n    public static LayerType readLayerType(File file) {\n        try {\n            return new MbtilesFile(file).getLayerType();\n        } catch (MbtilesException e) {\n            return null;\n        }\n    }\n\n    /** Vector layer metadata.  See https://github.com/mapbox/mbtiles-spec for details. */\n    public static class VectorLayer {\n        public final String name;\n\n        VectorLayer(JSONObject json) {\n            name = json.optString(\"id\", \"\");\n        }\n    }\n\n    /** Reads or guesses the tile data content type in an .mbtiles file. */\n    private static String detectContentType(File file) throws MbtilesException {\n        if (!file.exists() || !file.isFile()) {\n            throw new NotFileException(file);\n        }\n        if (!file.getName().toLowerCase(Locale.US).endsWith(FILE_EXTENSION)) {\n            throw new UnsupportedFilenameException(file);\n        }\n        try (SQLiteDatabase db = openSqliteReadOnly(file)) {\n            // The \"format\" code indicates whether the binary tiles are raster image\n            // files (JPEG, PNG) or protobuf-encoded vector geometry (PBF, MVT).\n            String format = queryMetadata(db, \"format\");\n            switch (format.toLowerCase(Locale.US)) {\n                case \"pbf\":\n                case \"mvt\":\n                    return \"application/protobuf\";\n                case \"jpg\":\n                case \"jpeg\":\n                    return \"image/jpeg\";\n                case \"png\":\n                    return \"image/png\";\n            }\n\n            // We have seen some raster .mbtiles files in the wild that are\n            // missing the \"format\" field, so let's attempt autodetection.\n            byte[] tileHeader = queryAnyTileHeader(db);\n            if (startsWithBytes(tileHeader, 0xff, 0xd8, 0xff, 0xe0)) {\n                return \"image/jpeg\";\n            }\n            if (startsWithBytes(tileHeader, 0x89, 'P', 'N', 'G')) {\n                return \"image/png\";\n            }\n            if (startsWithBytes(tileHeader, 0x1f, 0x8b)) {  // gzip header\n                return \"application/protobuf\";\n            }\n            throw new UnsupportedFormatException(format, file);\n        } catch (Throwable e) {\n            throw new MbtilesException(e);\n        }\n    }\n\n    private static SQLiteDatabase openSqliteReadOnly(File file) {\n        return SQLiteDatabase.openDatabase(\n            file.getPath(), null, OPEN_READONLY | NO_LOCALIZED_COLLATORS);\n    }\n\n    private static boolean startsWithBytes(byte[] actual, int... expected) {\n        int count = 0;\n        for (int i = 0; i < actual.length && i < expected.length; i++) {\n            count += (actual[i] == (byte) expected[i]) ? 1 : 0;\n        }\n        return count == expected.length;\n    }\n\n    /** Queries the \"metadata\" table, which has just \"name\" and \"value\" columns. */\n    private static @NonNull String queryMetadata(SQLiteDatabase db, String key) throws MbtilesException {\n        try (Cursor results = db.query(\"metadata\", new String[] {\"value\"},\n            \"name = ?\", new String[] {key}, null, null, null, \"1\")) {\n            return results.moveToFirst() ? results.getString(0) : \"\";\n        } catch (Throwable e) {\n            throw new MbtilesException(e);\n        }\n    }\n\n    /** Fetches the first 16 bytes of any tile blob found in the \"tiles\" table. */\n    private static @NonNull byte[] queryAnyTileHeader(SQLiteDatabase db) throws MbtilesException {\n        try (Cursor results = db.query(\"tiles\", new String[] {\"substr(tile_data, 1, 16)\"},\n            null, null, null, null, null, \"1\")) {\n            return results.moveToFirst() ? results.getBlob(0) : new byte[0];\n        } catch (Throwable e) {\n            throw new MbtilesException(e);\n        }\n    }\n\n    public static class MbtilesException extends IOException {\n        MbtilesException(Throwable cause) {\n            this(cause.getMessage());\n            initCause(cause);\n        }\n\n        MbtilesException(String message) {\n            super(message);\n        }\n    }\n\n    public static class NotFileException extends MbtilesException {\n        NotFileException(File file) {\n            super(\"Not a file: \" + file);\n        }\n    }\n\n    public static class UnsupportedFilenameException extends MbtilesException {\n        UnsupportedFilenameException(File file) {\n            super(\"Illegal filename for SQLite file: \" + file);\n        }\n    }\n\n    public static class UnsupportedFormatException extends MbtilesException {\n        UnsupportedFormatException(String format, File file) {\n            super(String.format(\"Unrecognized .mbtiles format \\\"%s\\\" in %s\", format, file));\n        }\n    }\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterAdapter.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.recyclerview.widget.RecyclerView\nimport org.odk.collect.maps.databinding.OfflineMapLayersImporterItemBinding\n\nclass OfflineMapLayersImporterAdapter(\n    private val layers: List<ReferenceLayer>,\n) : RecyclerView.Adapter<OfflineMapLayersImporterAdapter.ViewHolder>() {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        val binding = OfflineMapLayersImporterItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        return ViewHolder(binding)\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n        holder.binding.layerName.text = layers[position].name\n    }\n\n    override fun getItemCount() = layers.size\n\n    class ViewHolder(val binding: OfflineMapLayersImporterItemBinding) : RecyclerView.ViewHolder(binding.root)\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterDialogFragment.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.Toolbar\nimport androidx.fragment.app.activityViewModels\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.maps.databinding.OfflineMapLayersImporterBinding\nimport org.odk.collect.material.MaterialFullScreenDialogFragment\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.strings.R\nimport org.odk.collect.strings.localization.getLocalizedQuantityString\nimport org.odk.collect.strings.localization.getLocalizedString\n\nclass OfflineMapLayersImporterDialogFragment(\n    private val referenceLayerRepository: ReferenceLayerRepository,\n    private val scheduler: Scheduler,\n    private val settingsProvider: SettingsProvider\n) : MaterialFullScreenDialogFragment() {\n    val viewModel: OfflineMapLayersViewModel by activityViewModels {\n        object : ViewModelProvider.Factory {\n            override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                return OfflineMapLayersViewModel(\n                    referenceLayerRepository,\n                    scheduler,\n                    settingsProvider\n                ) as T\n            }\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        val binding = OfflineMapLayersImporterBinding.inflate(inflater)\n        binding.toolbarLayout.toolbar.setTitle(R.string.add_layer)\n\n        binding.cancelButton.setOnClickListener {\n            dismiss()\n        }\n\n        binding.addLayerButton.setOnClickListener {\n            viewModel.importNewLayers(binding.allProjectsOption.isChecked)\n            dismiss()\n        }\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        val binding = OfflineMapLayersImporterBinding.bind(view)\n\n        viewModel.trackableWorker.isWorking.observe(this) { isLoading ->\n            if (isLoading) {\n                binding.addLayerButton.isEnabled = false\n                binding.layers.visibility = View.GONE\n                binding.progressIndicator.visibility = View.VISIBLE\n            } else {\n                binding.addLayerButton.isEnabled = true\n                binding.layers.visibility = View.VISIBLE\n                binding.progressIndicator.visibility = View.GONE\n            }\n        }\n\n        viewModel.layersToImport.observe(this) { layersToImport ->\n            val adapter = OfflineMapLayersImporterAdapter(layersToImport.value.layers)\n            binding.layers.setAdapter(adapter)\n\n            if (!layersToImport.isConsumed()) {\n                layersToImport.consume()\n\n                if (layersToImport.value.numberOfSelectedLayers == layersToImport.value.numberOfUnsupportedLayers) {\n                    dismiss()\n                    showNoSupportedLayersWarning(layersToImport.value.numberOfUnsupportedLayers)\n                } else if (layersToImport.value.numberOfUnsupportedLayers > 0) {\n                    showSomeUnsupportedLayersWarning(layersToImport.value.numberOfUnsupportedLayers)\n                }\n            }\n        }\n    }\n\n    override fun onCloseClicked() = Unit\n\n    override fun onBackPressed() {\n        dismiss()\n    }\n\n    override fun getToolbar(): Toolbar {\n        return OfflineMapLayersImporterBinding.bind(requireView()).toolbarLayout.toolbar\n    }\n\n    private fun showNoSupportedLayersWarning(numberOfLayers: Int) {\n        MaterialAlertDialogBuilder(requireActivity())\n            .setTitle(\n                requireActivity().getLocalizedQuantityString(\n                    R.plurals.non_mbtiles_files_selected_title,\n                    numberOfLayers,\n                    numberOfLayers\n                )\n            )\n            .setMessage(requireActivity().getLocalizedString(R.string.all_non_mbtiles_files_selected_message))\n            .setPositiveButton(R.string.ok, null)\n            .create()\n            .show()\n    }\n\n    private fun showSomeUnsupportedLayersWarning(numberOfLayers: Int) {\n        MaterialAlertDialogBuilder(requireActivity())\n            .setTitle(\n                requireActivity().getLocalizedQuantityString(\n                    R.plurals.non_mbtiles_files_selected_title,\n                    numberOfLayers,\n                    numberOfLayers\n                )\n            )\n            .setMessage(requireActivity().getLocalizedString(R.string.some_non_mbtiles_files_selected_message))\n            .setPositiveButton(R.string.ok, null)\n            .create()\n            .show()\n    }\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.content.ContextCompat\nimport androidx.core.view.isInvisible\nimport androidx.recyclerview.widget.AsyncListDiffer\nimport androidx.recyclerview.widget.DiffUtil\nimport androidx.recyclerview.widget.RecyclerView\nimport org.odk.collect.androidshared.ui.addOnClickListener\nimport org.odk.collect.maps.databinding.OfflineMapLayersPickerItemBinding\nimport java.io.File\n\nclass OfflineMapLayersPickerAdapter(\n    private val listener: OfflineMapLayersPickerAdapterInterface\n) : RecyclerView.Adapter<OfflineMapLayersPickerAdapter.ViewHolder>() {\n    interface OfflineMapLayersPickerAdapterInterface {\n        fun onLayerChecked(layerId: String?)\n        fun onLayerToggled(layerId: String)\n        fun onDeleteLayer(layerItem: CheckableReferenceLayer)\n    }\n\n    private val diffUtil = object : DiffUtil.ItemCallback<CheckableReferenceLayer>() {\n        override fun areItemsTheSame(oldItem: CheckableReferenceLayer, newItem: CheckableReferenceLayer): Boolean {\n            return oldItem.id == newItem.id\n        }\n\n        override fun areContentsTheSame(oldItem: CheckableReferenceLayer, newItem: CheckableReferenceLayer): Boolean {\n            return oldItem == newItem\n        }\n    }\n    private val asyncListDiffer = AsyncListDiffer(this, diffUtil)\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        val binding = OfflineMapLayersPickerItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)\n        return ViewHolder(binding)\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n        val layer = asyncListDiffer.currentList[position]\n\n        holder.binding.radioButton.setChecked(layer.isChecked)\n        holder.binding.title.text = layer.name\n        holder.binding.path.text = layer.file?.absolutePath\n        holder.binding.arrow.isInvisible = layer.id == null\n\n        if (layer.isExpanded) {\n            holder.binding.arrow.setImageDrawable(ContextCompat.getDrawable(holder.binding.root.context, org.odk.collect.icons.R.drawable.ic_baseline_collapse_24))\n            holder.binding.path.visibility = View.VISIBLE\n            holder.binding.deleteLayer.visibility = View.VISIBLE\n        } else {\n            holder.binding.arrow.setImageDrawable(ContextCompat.getDrawable(holder.binding.root.context, org.odk.collect.icons.R.drawable.ic_baseline_expand_24))\n            holder.binding.path.visibility = View.GONE\n            holder.binding.deleteLayer.visibility = View.GONE\n        }\n\n        listOf(holder.binding.radioButton, holder.binding.title, holder.binding.path).addOnClickListener {\n            listener.onLayerChecked(layer.id)\n        }\n\n        holder.binding.arrow.setOnClickListener {\n            if (layer.id != null) {\n                listener.onLayerToggled(layer.id)\n            }\n        }\n\n        holder.binding.deleteLayer.setOnClickListener {\n            listener.onDeleteLayer(layer)\n        }\n    }\n\n    override fun getItemCount() = asyncListDiffer.currentList.size\n\n    fun setData(layers: List<CheckableReferenceLayer>) {\n        asyncListDiffer.submitList(layers)\n    }\n\n    class ViewHolder(val binding: OfflineMapLayersPickerItemBinding) : RecyclerView.ViewHolder(binding.root)\n}\n\ndata class CheckableReferenceLayer(\n    val id: String?,\n    val file: File?,\n    val name: String,\n    val isChecked: Boolean,\n    val isExpanded: Boolean\n)\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerBottomSheetDialogFragment.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.activity.result.ActivityResultRegistry\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.fragment.app.activityViewModels\nimport androidx.fragment.app.viewModels\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.map\nimport androidx.lifecycle.viewmodel.viewModelFactory\nimport com.google.android.material.bottomsheet.BottomSheetBehavior\nimport com.google.android.material.bottomsheet.BottomSheetDialogFragment\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.androidshared.livedata.LiveDataUtils\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.androidshared.ui.addOnClickListener\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.lists.selects.MultiSelectViewModel\nimport org.odk.collect.lists.selects.SelectItem\nimport org.odk.collect.lists.selects.SingleSelectViewModel\nimport org.odk.collect.maps.databinding.OfflineMapLayersPickerBinding\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.strings.localization.getLocalizedString\nimport org.odk.collect.webpage.WebPageService\n\nclass OfflineMapLayersPickerBottomSheetDialogFragment(\n    registry: ActivityResultRegistry,\n    private val referenceLayerRepository: ReferenceLayerRepository,\n    private val scheduler: Scheduler,\n    private val settingsProvider: SettingsProvider,\n    private val webPageService: WebPageService\n) : BottomSheetDialogFragment(),\n    OfflineMapLayersPickerAdapter.OfflineMapLayersPickerAdapterInterface {\n\n    private val sharedViewModel: OfflineMapLayersViewModel by activityViewModels {\n        object : ViewModelProvider.Factory {\n            override fun <T : ViewModel> create(modelClass: Class<T>): T {\n                return OfflineMapLayersViewModel(\n                    referenceLayerRepository,\n                    scheduler,\n                    settingsProvider\n                ) as T\n            }\n        }\n    }\n    private val expandedStateViewModel: MultiSelectViewModel<*> by viewModels {\n        MultiSelectViewModel.Factory<Any>()\n    }\n\n    private val checkedStateViewModel: SingleSelectViewModel by viewModels {\n        viewModelFactory {\n            addInitializer(SingleSelectViewModel::class) {\n                SingleSelectViewModel(\n                    settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER),\n                    sharedViewModel.existingLayers.map {\n                        it.map { layer ->\n                            SelectItem(layer.id, layer)\n                        }\n                    }\n                )\n            }\n        }\n    }\n\n    private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents(), registry) { uris ->\n        if (uris.isNotEmpty()) {\n            sharedViewModel.loadLayersToImport(uris, requireContext())\n            DialogFragmentUtils.showIfNotShowing(\n                OfflineMapLayersImporterDialogFragment::class.java,\n                childFragmentManager\n            )\n        }\n    }\n\n    override fun onAttach(context: Context) {\n        super.onAttach(context)\n        sharedViewModel.loadExistingLayers()\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        childFragmentManager.fragmentFactory = FragmentFactoryBuilder()\n            .forClass(OfflineMapLayersImporterDialogFragment::class) {\n                OfflineMapLayersImporterDialogFragment(referenceLayerRepository, scheduler, settingsProvider)\n            }\n            .build()\n\n        super.onCreate(savedInstanceState)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View {\n        val binding = OfflineMapLayersPickerBinding.inflate(inflater)\n\n        binding.mbtilesInfoGroup.addOnClickListener {\n            webPageService.openWebPage(\n                requireActivity(),\n                Uri.parse(\"https://docs.getodk.org/collect-offline-maps/#transferring-offline-tilesets-to-devices\")\n            )\n        }\n\n        binding.addLayer.setOnClickListener {\n            getLayers.launch(\"*/*\")\n        }\n\n        binding.cancel.setOnClickListener {\n            dismiss()\n        }\n\n        binding.save.setOnClickListener {\n            sharedViewModel.saveCheckedLayer(checkedStateViewModel.getSelected().value)\n            dismiss()\n        }\n\n        if (sharedViewModel.layersToImport.value?.value == null) {\n            DialogFragmentUtils.dismissDialog(\n                OfflineMapLayersImporterDialogFragment::class.java,\n                childFragmentManager\n            )\n        }\n\n        return binding.root\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        val binding = OfflineMapLayersPickerBinding.bind(view)\n\n        sharedViewModel.trackableWorker.isWorking.observe(this) { isLoading ->\n            if (isLoading) {\n                binding.progressIndicator.visibility = View.VISIBLE\n                binding.layers.visibility = View.GONE\n                binding.save.isEnabled = false\n            } else {\n                binding.progressIndicator.visibility = View.GONE\n                binding.layers.visibility = View.VISIBLE\n                binding.save.isEnabled = true\n            }\n        }\n\n        val adapter = OfflineMapLayersPickerAdapter(this)\n        binding.layers.setAdapter(adapter)\n        LiveDataUtils.combine3(\n            sharedViewModel.existingLayers,\n            checkedStateViewModel.getSelected(),\n            expandedStateViewModel.getSelected()\n        ).observe(this) { (layers, checkedLayerId, expandedLayerIds) ->\n            if (layers != null && expandedLayerIds != null) {\n                updateAdapter(layers, checkedLayerId, expandedLayerIds.toList(), adapter)\n            }\n        }\n    }\n\n    override fun onStart() {\n        super.onStart()\n        try {\n            BottomSheetBehavior.from(requireView().parent as View).apply {\n                maxWidth = ViewGroup.LayoutParams.MATCH_PARENT\n            }\n        } catch (e: Exception) {\n            // ignore\n        }\n    }\n\n    override fun onLayerChecked(layerId: String?) {\n        if (layerId != null) {\n            checkedStateViewModel.select(layerId)\n        } else {\n            checkedStateViewModel.clear()\n        }\n    }\n\n    override fun onLayerToggled(layerId: String) {\n        expandedStateViewModel.toggle(layerId)\n    }\n\n    override fun onDeleteLayer(layerItem: CheckableReferenceLayer) {\n        MaterialAlertDialogBuilder(requireActivity())\n            .setMessage(\n                requireActivity().getLocalizedString(\n                    org.odk.collect.strings.R.string.delete_layer_confirmation_message,\n                    layerItem.name\n                )\n            )\n            .setPositiveButton(org.odk.collect.strings.R.string.delete_layer) { _, _ ->\n                sharedViewModel.deleteLayer(layerItem.id!!)\n            }\n            .setNegativeButton(org.odk.collect.strings.R.string.cancel, null)\n            .create()\n            .show()\n    }\n\n    private fun updateAdapter(\n        layers: List<ReferenceLayer>?,\n        checkedLayerId: String?,\n        expandedLayerIds: List<String?>,\n        adapter: OfflineMapLayersPickerAdapter\n    ) {\n        if (layers == null) {\n            return\n        }\n\n        val newData = mutableListOf(\n            CheckableReferenceLayer(\n                null,\n                null,\n                requireContext().getLocalizedString(org.odk.collect.strings.R.string.none),\n                checkedLayerId == null,\n                false\n            )\n        )\n\n        newData.addAll(\n            layers.map {\n                CheckableReferenceLayer(\n                    it.id,\n                    it.file,\n                    it.name,\n                    checkedLayerId == it.id,\n                    expandedLayerIds.contains(it.id)\n                )\n            }\n        )\n        adapter.setData(newData)\n    }\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport android.content.Context\nimport android.net.Uri\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.androidshared.async.TrackableWorker\nimport org.odk.collect.androidshared.data.Consumable\nimport org.odk.collect.androidshared.system.copyToFile\nimport org.odk.collect.androidshared.system.getFileExtension\nimport org.odk.collect.androidshared.system.getFileName\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.maps.AnalyticsEvents\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.TempFiles\nimport java.io.File\n\nclass OfflineMapLayersViewModel(\n    private val referenceLayerRepository: ReferenceLayerRepository,\n    scheduler: Scheduler,\n    private val settingsProvider: SettingsProvider\n) : ViewModel() {\n    val trackableWorker = TrackableWorker(scheduler)\n\n    private val _existingLayers = MutableLiveData<List<ReferenceLayer>>()\n    val existingLayers: LiveData<List<ReferenceLayer>> = _existingLayers\n\n    private val _layersToImport = MutableLiveData<Consumable<LayersToImport>>()\n    val layersToImport: LiveData<Consumable<LayersToImport>> = _layersToImport\n\n    private lateinit var tempLayersDir: File\n\n    fun loadExistingLayers() {\n        trackableWorker.immediate {\n            val layers = referenceLayerRepository.getAll().sortedBy { it.name }\n            _existingLayers.postValue(layers)\n        }\n    }\n\n    fun loadLayersToImport(uris: List<Uri>, context: Context) {\n        trackableWorker.immediate {\n            tempLayersDir = TempFiles.createTempDir().also {\n                it.deleteOnExit()\n            }\n            val layers = mutableListOf<ReferenceLayer>()\n            uris.forEach { uri ->\n                if (uri.getFileExtension(context) == MbtilesFile.FILE_EXTENSION) {\n                    uri.getFileName(context)?.let { fileName ->\n                        val layerFile = File(tempLayersDir, fileName).also { file ->\n                            uri.copyToFile(context, file)\n                        }\n                        layers.add(\n                            ReferenceLayer(\n                                layerFile.absolutePath,\n                                layerFile,\n                                MbtilesFile.readName(layerFile) ?: layerFile.name\n                            )\n                        )\n                    }\n                }\n            }\n            _layersToImport.postValue(\n                Consumable(\n                    LayersToImport(\n                        uris.size,\n                        uris.size - layers.size,\n                        layers.sortedBy { it.name }\n                    )\n                )\n            )\n        }\n    }\n\n    fun importNewLayers(shared: Boolean) {\n        trackableWorker.immediate(\n            background = {\n                val layers = tempLayersDir.listFiles()\n                logImport(layers)\n\n                layers?.forEach {\n                    referenceLayerRepository.addLayer(it, shared)\n                }\n                tempLayersDir.delete()\n            },\n            foreground = {\n                loadExistingLayers()\n            }\n        )\n    }\n\n    fun saveCheckedLayer(layerId: String?) {\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, layerId)\n    }\n\n    fun deleteLayer(layerId: String) {\n        trackableWorker.immediate {\n            if (settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) == layerId) {\n                settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, null)\n            }\n\n            referenceLayerRepository.delete(layerId)\n            _existingLayers.postValue(_existingLayers.value?.filter { it.id != layerId })\n        }\n    }\n\n    private fun logImport(layers: Array<File>?) {\n        val count = layers?.size ?: return\n        val event = when {\n            count == 1 -> AnalyticsEvents.IMPORT_LAYER_SINGLE\n            count <= 5 -> AnalyticsEvents.IMPORT_LAYER_FEW\n            else -> AnalyticsEvents.IMPORT_LAYER_MANY\n        }\n\n        Analytics.log(event)\n    }\n\n    data class LayersToImport(\n        val numberOfSelectedLayers: Int,\n        val numberOfUnsupportedLayers: Int,\n        val layers: List<ReferenceLayer>\n    )\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport java.io.File\n\ninterface ReferenceLayerRepository {\n\n    fun getAll(): List<ReferenceLayer>\n    fun get(id: String): ReferenceLayer?\n    fun addLayer(file: File, shared: Boolean)\n    fun delete(id: String)\n}\n\ndata class ReferenceLayer(val id: String, val file: File, val name: String)\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/layers/TileSource.java",
    "content": "package org.odk.collect.maps.layers;\n\npublic interface TileSource {\n    byte[] getTileBlob(int zoom, int x, int y);\n\n    String getContentType();  // a MIME type such as \"image/jpeg\"\n\n    String getContentEncoding();  // either \"identity\" or \"gzip\"\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/markers/MarkerDescription.kt",
    "content": "package org.odk.collect.maps.markers\n\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\n\ndata class MarkerDescription(\n    val point: MapPoint,\n    val isDraggable: Boolean,\n    val iconAnchor: MapFragment.IconAnchor,\n    val iconDescription: MarkerIconDescription\n)\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt",
    "content": "package org.odk.collect.maps.markers\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Bitmap.Config\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.Paint\nimport android.graphics.Typeface\nimport android.util.LruCache\nimport androidx.core.content.ContextCompat\nimport androidx.core.graphics.ColorUtils\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.core.graphics.toColorInt\n\nobject MarkerIconCreator {\n    /**\n     * We use different markers in different features and in normal cases we should not need to use\n     * more than 10 different types of markers at the same time.\n     * The case when markers have symbols is an exception and then it doesn't make sense to cache\n     * bitmaps because every marker will most likely be unique.\n     */\n    private val cache = LruCache<String, Bitmap>(10)\n\n    @JvmStatic\n    fun getMarkerIcon(context: Context, markerIconDescription: MarkerIconDescription): Bitmap {\n        return when (markerIconDescription) {\n            is MarkerIconDescription.TracePoint -> {\n                fromCache(\"LinePoint\" + markerIconDescription.lineSize + markerIconDescription.color) {\n                    createPoint(\n                        markerIconDescription.lineSize * 6,\n                        markerIconDescription.lineSize,\n                        markerIconDescription.color\n                    )\n                }\n            }\n\n            is MarkerIconDescription.DrawableResource -> {\n                val drawableId = markerIconDescription.drawable\n                val color = markerIconDescription.getColor()\n                val symbol = markerIconDescription.getSymbol()\n\n                val bitmapId = drawableId.toString() + color + symbol\n                fromCache(bitmapId) {\n                    createBitmap(context, drawableId, color, symbol)\n                }\n            }\n        }\n    }\n\n    private fun createPoint(diameter: Float, strokeSize: Float, color: Int): Bitmap {\n        val bitmap =\n            Bitmap.createBitmap(diameter.toInt(), diameter.toInt(), Config.ARGB_8888)\n\n        Canvas(bitmap).also { canvas ->\n            val radius = diameter / 2\n\n            val fill = Paint().also {\n                it.style = Paint.Style.FILL\n                it.color = \"#ffffff\".toColorInt()\n            }\n            canvas.drawCircle(radius, radius, radius, fill)\n\n            val stroke = Paint().also {\n                it.style = Paint.Style.STROKE\n                it.color = color\n                it.strokeWidth = strokeSize\n            }\n            canvas.drawCircle(radius, radius, radius - (strokeSize / 2), stroke)\n        }\n\n        return bitmap\n    }\n\n    private fun fromCache(bitmapId: String, factory: () -> Bitmap): Bitmap {\n        return if (cache[bitmapId] == null) {\n            factory().also {\n                cache.put(bitmapId, it)\n            }\n        } else {\n            cache[bitmapId]\n        }\n    }\n\n    private fun createBitmap(\n        context: Context,\n        drawableId: Int,\n        color: Int?,\n        symbol: String?\n    ): Bitmap {\n        val drawable = ContextCompat.getDrawable(context, drawableId)\n        if (drawable != null) {\n            drawable.mutate()\n\n            val isBackgroundDark = color?.let {\n                drawable.setTint(it)\n                ColorUtils.calculateLuminance(color) < 0.5\n            } ?: true\n\n            return drawable.toBitmap().also { bitmap ->\n                symbol?.let {\n                    val paint = Paint().also {\n                        it.style = Paint.Style.FILL\n                        it.color = if (isBackgroundDark) Color.WHITE else Color.BLACK\n                        it.textSize = (bitmap.width / 2.3).toFloat()\n                        it.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)\n                        it.textAlign = Paint.Align.CENTER\n                    }\n\n                    Canvas(bitmap).also {\n                        val x: Int = it.width / 2\n                        val y: Int = it.height / 2\n                        it.drawText(symbol, x.toFloat(), y.toFloat(), paint)\n                    }\n                }\n            }\n        } else {\n            throw IllegalStateException(\"Drawable cannot be created!\")\n        }\n    }\n\n    @JvmStatic\n    fun clearCache() {\n        cache.evictAll()\n    }\n\n    @JvmStatic\n    fun MarkerIconDescription.toBitmap(context: Context): Bitmap {\n        return getMarkerIcon(context, this)\n    }\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt",
    "content": "package org.odk.collect.maps.markers\n\nimport org.odk.collect.androidshared.utils.sanitizeToColorInt\nimport org.odk.collect.shared.strings.StringUtils\nimport java.util.Locale\n\nsealed interface MarkerIconDescription {\n    data class DrawableResource @JvmOverloads constructor(\n        val drawable: Int,\n        private val color: String? = null,\n        private val symbol: String? = null\n    ) : MarkerIconDescription {\n        fun getColor(): Int? = color?.sanitizeToColorInt()\n\n        fun getSymbol(): String? = symbol?.let {\n            if (it.isBlank()) {\n                null\n            } else {\n                StringUtils.firstCharacterOrEmoji(it).uppercase(Locale.US)\n            }\n        }\n    }\n\n    data class TracePoint(val lineSize: Float, val color: Int) : MarkerIconDescription\n}\n"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/traces/LineDescription.kt",
    "content": "package org.odk.collect.maps.traces\n\nimport org.odk.collect.maps.MapConsts\nimport org.odk.collect.maps.MapPoint\n\ndata class LineDescription(\n    override val points: List<MapPoint> = emptyList(),\n    private val strokeWidth: String? = null,\n    private val strokeColor: Int? = null,\n    override val highlightLastPoint: Boolean = false,\n    val draggable: Boolean = false\n) : TraceDescription {\n    override fun getStrokeWidth(): Float {\n        return try {\n            strokeWidth?.toFloat()?.let {\n                if (it >= 0) {\n                    it\n                } else {\n                    MapConsts.DEFAULT_STROKE_WIDTH\n                }\n            } ?: MapConsts.DEFAULT_STROKE_WIDTH\n        } catch (e: NumberFormatException) {\n            MapConsts.DEFAULT_STROKE_WIDTH\n        }\n    }\n\n    override fun getStrokeColor(): Int {\n        return strokeColor ?: MapConsts.DEFAULT_STROKE_COLOR\n    }\n}"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/traces/PolygonDescription.kt",
    "content": "package org.odk.collect.maps.traces\n\nimport androidx.core.graphics.ColorUtils\nimport org.odk.collect.maps.MapConsts\nimport org.odk.collect.maps.MapPoint\n\ndata class PolygonDescription(\n    override val points: List<MapPoint> = emptyList(),\n    private val strokeWidth: String? = null,\n    private val strokeColor: Int? = null,\n    private val fillColor: Int? = null,\n    override val highlightLastPoint: Boolean = false,\n    val draggable: Boolean = false\n) : TraceDescription {\n    override fun getStrokeWidth(): Float {\n        return try {\n            strokeWidth?.toFloat()?.let {\n                if (it >= 0) {\n                    it\n                } else {\n                    MapConsts.DEFAULT_STROKE_WIDTH\n                }\n            } ?: MapConsts.DEFAULT_STROKE_WIDTH\n        } catch (e: NumberFormatException) {\n            MapConsts.DEFAULT_STROKE_WIDTH\n        }\n    }\n\n    override fun getStrokeColor(): Int {\n        return strokeColor ?: MapConsts.DEFAULT_STROKE_COLOR\n    }\n\n    fun getFillColor(): Int {\n        val customColor = fillColor?.let {\n            ColorUtils.setAlphaComponent(\n                it,\n                MapConsts.DEFAULT_FILL_COLOR_OPACITY\n            )\n        }\n\n        return customColor ?: ColorUtils.setAlphaComponent(\n            MapConsts.DEFAULT_STROKE_COLOR,\n            MapConsts.DEFAULT_FILL_COLOR_OPACITY\n        )\n    }\n}"
  },
  {
    "path": "maps/src/main/java/org/odk/collect/maps/traces/TraceDescription.kt",
    "content": "package org.odk.collect.maps.traces\n\nimport org.odk.collect.maps.MapConsts\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.maps.MapPoint\nimport org.odk.collect.maps.markers.MarkerDescription\nimport org.odk.collect.maps.markers.MarkerIconDescription.TracePoint\n\ninterface TraceDescription {\n    val points: List<MapPoint>\n    val highlightLastPoint: Boolean\n    fun getStrokeWidth(): Float\n    fun getStrokeColor(): Int\n}\n\nfun TraceDescription.getMarkersForPoints(): List<MarkerDescription> {\n    return points.mapIndexed { i, point ->\n        val color = if (highlightLastPoint && i == points.lastIndex) {\n            MapConsts.DEFAULT_HIGHLIGHT_COLOR\n        } else {\n            getStrokeColor()\n        }\n\n        MarkerDescription(point, true, MapFragment.IconAnchor.CENTER, TracePoint(getStrokeWidth(), color))\n    }\n}\n"
  },
  {
    "path": "maps/src/main/res/drawable/ic_crosshairs.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:viewportWidth=\"48\"\n    android:viewportHeight=\"48\"\n    android:tint=\"?colorPrimary\">\n\n    <!-- Do not use theme attributes to define the color https://github.com/mapbox/mapbox-gl-native-android/issues/716 -->\n    <path\n        android:fillColor=\"?colorPrimary\"\n        android:pathData=\"M18,23v2h-18v-2h18z m12,0h18v2h-18v-2z m-7,-5v-18h2v18h-2z m0,12h2v18h-2v-18z\" />\n</vector>\n"
  },
  {
    "path": "maps/src/main/res/layout/offline_map_layers_importer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <include\n        android:id=\"@+id/toolbar_layout\"\n        layout=\"@layout/app_bar_layout\" />\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fillViewport=\"true\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <com.google.android.material.textview.MaterialTextView\n                android:id=\"@+id/layers_title\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginHorizontal=\"@dimen/margin_standard\"\n                android:layout_marginTop=\"@dimen/margin_small\"\n                android:text=\"@string/layers_list_title\"\n                android:textAppearance=\"?textAppearanceTitleLarge\"\n                app:layout_constraintEnd_toEndOf=\"parent\"\n                app:layout_constraintStart_toStartOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <androidx.constraintlayout.widget.ConstraintLayout\n                android:id=\"@+id/layers_container\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                app:layout_constrainedHeight=\"true\"\n                app:layout_constraintBottom_toTopOf=\"@id/container\"\n                app:layout_constraintEnd_toEndOf=\"@id/layers_title\"\n                app:layout_constraintStart_toStartOf=\"@id/layers_title\"\n                app:layout_constraintTop_toBottomOf=\"@id/layers_title\"\n                app:layout_constraintVertical_bias=\"0\"\n                app:layout_constraintVertical_chainStyle=\"packed\">\n\n                <androidx.recyclerview.widget.RecyclerView\n                    android:id=\"@+id/layers\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"@dimen/margin_small\"\n                    app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n                    app:layout_constraintEnd_toEndOf=\"parent\"\n                    app:layout_constraintHeight_max=\"320dp\"\n                    app:layout_constraintStart_toStartOf=\"parent\"\n                    app:layout_constraintTop_toTopOf=\"parent\" />\n\n                <com.google.android.material.progressindicator.CircularProgressIndicator\n                    android:id=\"@+id/progress_indicator\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginVertical=\"@dimen/margin_small\"\n                    android:indeterminate=\"true\"\n                    app:layout_constrainedHeight=\"true\"\n                    app:layout_constraintEnd_toEndOf=\"parent\"\n                    app:layout_constraintStart_toStartOf=\"parent\"\n                    app:layout_constraintTop_toTopOf=\"parent\" />\n            </androidx.constraintlayout.widget.ConstraintLayout>\n\n            <androidx.constraintlayout.widget.ConstraintLayout\n                android:id=\"@+id/container\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                app:layout_constraintBottom_toTopOf=\"@id/bottom_divider\"\n                app:layout_constraintEnd_toEndOf=\"@id/layers_title\"\n                app:layout_constraintStart_toStartOf=\"@id/layers_title\"\n                app:layout_constraintTop_toBottomOf=\"@id/layers_container\">\n\n                <com.google.android.material.textview.MaterialTextView\n                    android:id=\"@+id/select_layer_access_title\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"@dimen/margin_standard\"\n                    android:text=\"@string/select_layer_access_title\"\n                    android:textAppearance=\"?textAppearanceTitleLarge\"\n                    app:layout_constraintEnd_toEndOf=\"parent\"\n                    app:layout_constraintStart_toStartOf=\"parent\"\n                    app:layout_constraintTop_toTopOf=\"parent\" />\n\n                <com.google.android.material.textview.MaterialTextView\n                    android:id=\"@+id/select_layer_access_subtitle\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"@dimen/margin_standard\"\n                    android:text=\"@string/select_layer_access_subtitle\"\n                    android:textAppearance=\"?textAppearanceBodyMedium\"\n                    app:layout_constraintEnd_toEndOf=\"parent\"\n                    app:layout_constraintStart_toStartOf=\"parent\"\n                    app:layout_constraintTop_toBottomOf=\"@id/select_layer_access_title\" />\n\n                <RadioGroup\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"@dimen/margin_extra_extra_small\"\n                    android:checkedButton=\"@id/all_projects_option\"\n                    android:paddingBottom=\"@dimen/margin_small\"\n                    app:layout_constraintEnd_toEndOf=\"parent\"\n                    app:layout_constraintStart_toStartOf=\"parent\"\n                    app:layout_constraintTop_toBottomOf=\"@id/select_layer_access_subtitle\">\n\n                    <RadioButton\n                        android:id=\"@+id/all_projects_option\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:paddingStart=\"@dimen/margin_standard\"\n                        android:text=\"@string/all_projects_option\"\n                        android:textAppearance=\"?textAppearanceBodyMedium\" />\n\n                    <RadioButton\n                        android:id=\"@+id/current_project_option\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:paddingStart=\"@dimen/margin_standard\"\n                        android:text=\"@string/current_project_option\"\n                        android:textAppearance=\"?textAppearanceBodyMedium\" />\n                </RadioGroup>\n            </androidx.constraintlayout.widget.ConstraintLayout>\n\n            <View\n                android:id=\"@+id/bottom_divider\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"1dp\"\n                android:layout_marginBottom=\"@dimen/margin_extra_extra_small\"\n                android:background=\"@drawable/list_item_divider\"\n                app:layout_constraintBottom_toTopOf=\"@id/cancel_button\" />\n\n            <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n                android:id=\"@+id/cancel_button\"\n                style=\"?borderlessButtonStyle\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"@dimen/margin_extra_extra_small\"\n                android:text=\"@string/cancel\"\n                app:layout_constraintBottom_toBottomOf=\"@id/add_layer_button\"\n                app:layout_constraintEnd_toStartOf=\"@id/add_layer_button\"\n                app:layout_constraintTop_toTopOf=\"@id/add_layer_button\" />\n\n            <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n                android:id=\"@+id/add_layer_button\"\n                style=\"?materialButtonStyle\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"@dimen/margin_standard\"\n                android:layout_marginBottom=\"@dimen/margin_extra_small\"\n                android:text=\"@string/add_layer\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintEnd_toEndOf=\"parent\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n    </androidx.core.widget.NestedScrollView>\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "maps/src/main/res/layout/offline_map_layers_importer_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingVertical=\"@dimen/margin_extra_small\">\n\n    <ImageView\n        android:id=\"@+id/layer_icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:src=\"@drawable/ic_baseline_layers_24\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/layer_name\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        app:layout_constraintStart_toEndOf=\"@id/layer_icon\"\n        app:layout_constraintTop_toTopOf=\"@id/layer_icon\"\n        app:layout_constraintBottom_toBottomOf=\"@id/layer_icon\"\n        app:layout_constraintEnd_toEndOf=\"parent\"/>\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "maps/src/main/res/layout/offline_map_layers_picker.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.constraintlayout.widget.Guideline\n        android:id=\"@+id/guideline_start\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        app:layout_constraintGuide_begin=\"@dimen/margin_standard\" />\n\n    <androidx.constraintlayout.widget.Guideline\n        android:id=\"@+id/guideline_end\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        app:layout_constraintGuide_end=\"@dimen/margin_standard\" />\n\n    <com.google.android.material.bottomsheet.BottomSheetDragHandleView\n        android:id=\"@+id/drag_handle\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"/>\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/offline_layer_dialog_title\"\n        android:textAppearance=\"?textAppearanceTitleLarge\"\n        app:layout_constraintStart_toEndOf=\"@id/guideline_start\"\n        app:layout_constraintEnd_toStartOf=\"@id/guideline_end\"\n        app:layout_constraintTop_toBottomOf=\"@id/drag_handle\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/message\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_small\"\n        android:text=\"@string/offline_layer_dialog_description\"\n        android:textAppearance=\"?textAppearanceBodyMedium\"\n        app:layout_constraintStart_toEndOf=\"@id/guideline_start\"\n        app:layout_constraintEnd_toStartOf=\"@id/guideline_end\"\n        app:layout_constraintTop_toBottomOf=\"@id/title\" />\n\n    <ImageView\n        android:id=\"@+id/mbtiles_info_icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        android:src=\"@drawable/ic_outline_info_24\"\n        app:layout_constraintStart_toEndOf=\"@id/guideline_start\"\n        app:layout_constraintTop_toBottomOf=\"@id/message\"\n        app:tint=\"?colorPrimary\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/mbtiles_info_icon_text\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:text=\"@string/get_help_with_offline_layers\"\n        android:textAppearance=\"?textAppearanceLabelLarge\"\n        android:textColor=\"?colorPrimary\"\n        app:layout_constraintBottom_toBottomOf=\"@id/mbtiles_info_icon\"\n        app:layout_constraintEnd_toStartOf=\"@id/guideline_end\"\n        app:layout_constraintStart_toEndOf=\"@id/mbtiles_info_icon\"\n        app:layout_constraintTop_toTopOf=\"@id/mbtiles_info_icon\" />\n\n    <androidx.constraintlayout.widget.Group\n        android:id=\"@+id/mbtiles_info_group\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:visibility=\"visible\"\n        app:constraint_referenced_ids=\"mbtiles_info_icon,mbtiles_info_icon_text\" />\n\n    <View\n        android:id=\"@+id/top_divider\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@drawable/list_item_divider\"\n        android:layout_marginTop=\"@dimen/margin_standard\"\n        app:layout_constraintTop_toBottomOf=\"@id/mbtiles_info_icon\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/layers\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n        android:layout_marginBottom=\"@dimen/margin_extra_small\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintBottom_toTopOf=\"@id/add_layer\"\n        app:layout_constraintHeight_default=\"wrap\"\n        app:layout_constraintTop_toBottomOf=\"@id/top_divider\" />\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        android:id=\"@+id/progress_indicator\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginVertical=\"@dimen/margin_small\"\n        android:indeterminate=\"true\"\n        app:layout_constraintBottom_toTopOf=\"@id/add_layer\"\n        app:layout_constraintEnd_toEndOf=\"@id/message\"\n        app:layout_constraintStart_toStartOf=\"@id/message\"\n        app:layout_constraintTop_toBottomOf=\"@id/top_divider\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/add_layer\"\n        style=\"?materialButtonOutlinedIconStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/add_layer\"\n        android:layout_marginBottom=\"@dimen/margin_extra_small\"\n        app:icon=\"@drawable/ic_add_white_24\"\n        app:layout_constraintBottom_toTopOf=\"@id/bottom_divider\"\n        app:layout_constraintStart_toEndOf=\"@id/guideline_start\" />\n\n    <View\n        android:id=\"@+id/bottom_divider\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"@drawable/list_item_divider\"\n        android:layout_marginBottom=\"@dimen/margin_extra_extra_small\"\n        app:layout_constraintBottom_toTopOf=\"@id/save\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/save\"\n        style=\"?materialButtonStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/save\"\n        android:layout_marginBottom=\"@dimen/margin_extra_extra_small\"\n        android:enabled=\"false\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toStartOf=\"@id/guideline_end\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/cancel\"\n        style=\"?borderlessButtonStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/margin_small\"\n        android:text=\"@string/cancel\"\n        app:layout_constraintBottom_toBottomOf=\"@id/save\"\n        app:layout_constraintEnd_toStartOf=\"@id/save\"\n        app:layout_constraintTop_toTopOf=\"@id/save\" />\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "maps/src/main/res/layout/offline_map_layers_picker_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <com.google.android.material.radiobutton.MaterialRadioButton\n        android:id=\"@+id/radio_button\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:button=\"@drawable/radio_button_inset\"\n        android:paddingVertical=\"@dimen/margin_extra_small\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/title\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"?textAppearanceTitleMedium\"\n        app:layout_constraintStart_toEndOf=\"@id/radio_button\"\n        app:layout_constraintEnd_toStartOf=\"@id/arrow\"\n        app:layout_constraintTop_toTopOf=\"@id/radio_button\"\n        app:layout_constraintBottom_toBottomOf=\"@id/radio_button\"\n        tools:text=\"Layer1\" />\n\n    <com.google.android.material.textview.MaterialTextView\n        android:id=\"@+id/path\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"?textAppearanceBodyMedium\"\n        app:layout_constraintStart_toStartOf=\"@id/title\"\n        app:layout_constraintEnd_toEndOf=\"@id/title\"\n        app:layout_constraintTop_toBottomOf=\"@id/title\"\n        tools:text=\"org.odk.collect.android/projects/FSDDSDS5/layers/layer2.mbtiles\" />\n\n    <org.odk.collect.androidshared.ui.multiclicksafe.MultiClickSafeMaterialButton\n        android:id=\"@+id/delete_layer\"\n        style=\"?materialButtonOutlinedIconStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/delete_layer\"\n        android:layout_marginBottom=\"@dimen/margin_extra_small\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        app:icon=\"@drawable/ic_delete_24\"\n        app:layout_constraintStart_toStartOf=\"@id/title\"\n        app:layout_constraintTop_toBottomOf=\"@id/path\" />\n\n    <ImageView\n        android:id=\"@+id/arrow\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"0dp\"\n        android:src=\"@drawable/ic_baseline_expand_24\"\n        android:paddingHorizontal=\"@dimen/margin_standard\"\n        app:layout_constraintTop_toTopOf=\"@id/radio_button\"\n        app:layout_constraintBottom_toBottomOf=\"@id/radio_button\"\n        app:layout_constraintEnd_toEndOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "maps/src/test/java/org/odk/collect/maps/LineDescriptionTest.kt",
    "content": "package org.odk.collect.maps\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.maps.traces.LineDescription\n\n@RunWith(AndroidJUnit4::class)\nclass LineDescriptionTest {\n    @Test\n    fun `getStrokeWidth returns the default value when the passed one is null`() {\n        val lineDescription = LineDescription(strokeWidth = null)\n        assertThat(lineDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH))\n    }\n\n    @Test\n    fun `getStrokeWidth returns the default value when the passed one is invalid`() {\n        val lineDescription = LineDescription(strokeWidth = \"blah\")\n        assertThat(lineDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH))\n    }\n\n    @Test\n    fun `getStrokeWidth returns the default value when the passed one is not greater than or equal to zero`() {\n        val lineDescription = LineDescription(strokeWidth = \"-1\")\n        assertThat(lineDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH))\n    }\n\n    @Test\n    fun `getStrokeWidth returns custom value when the passed one is a valid int number`() {\n        val lineDescription = LineDescription(strokeWidth = \"10\")\n        assertThat(lineDescription.getStrokeWidth(), equalTo(10f))\n    }\n\n    @Test\n    fun `getStrokeWidth returns custom value when the passed one is a valid float number`() {\n        val lineDescription = LineDescription(strokeWidth = \"10.5\")\n        assertThat(lineDescription.getStrokeWidth(), equalTo(10.5f))\n    }\n\n    @Test\n    fun `getStrokeColor returns the default color when the passed one is null`() {\n        val lineDescription = LineDescription(strokeColor = null)\n        assertThat(lineDescription.getStrokeColor(), equalTo(MapConsts.DEFAULT_STROKE_COLOR))\n    }\n\n    @Test\n    fun `getStrokeColor returns custom color`() {\n        val lineDescription = LineDescription(strokeColor = -5583634)\n        assertThat(lineDescription.getStrokeColor(), equalTo(-5583634))\n    }\n}\n"
  },
  {
    "path": "maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt",
    "content": "package org.odk.collect.maps\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.nullValue\nimport org.hamcrest.core.Is.`is`\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.maps.markers.MarkerIconDescription\n\n@RunWith(AndroidJUnit4::class)\nclass MarkerIconDescriptionTest {\n    @Test\n    fun `return null when color is null`() {\n        val markerIconDescription = MarkerIconDescription.DrawableResource(0, null)\n        assertThat(markerIconDescription.getColor(), `is`(nullValue()))\n    }\n\n    @Test\n    fun `return null when symbol is null`() {\n        val markerIconDescription = MarkerIconDescription.DrawableResource(0, symbol = null)\n        assertThat(markerIconDescription.getSymbol(), `is`(nullValue()))\n    }\n\n    @Test\n    fun `return null when symbol is empty`() {\n        val markerIconDescription = MarkerIconDescription.DrawableResource(0, symbol = \"\")\n        assertThat(markerIconDescription.getSymbol(), `is`(nullValue()))\n    }\n\n    @Test\n    fun `return first char when symbol consists of multiple chars`() {\n        val markerIconDescription = MarkerIconDescription.DrawableResource(0, symbol = \"Blah\")\n        assertThat(markerIconDescription.getSymbol(), `is`(\"B\"))\n    }\n\n    @Test\n    fun `return uppercase symbol`() {\n        val markerIconDescription = MarkerIconDescription.DrawableResource(0, symbol = \"b\")\n        assertThat(markerIconDescription.getSymbol(), `is`(\"B\"))\n    }\n\n    @Test\n    fun `return emoji symbol`() {\n        val markerIconDescription = MarkerIconDescription.DrawableResource(0, symbol = \"\\uD83E\\uDDDB\")\n        assertThat(markerIconDescription.getSymbol(), `is`(\"\\uD83E\\uDDDB\"))\n    }\n}\n"
  },
  {
    "path": "maps/src/test/java/org/odk/collect/maps/PolygonDescriptionTest.kt",
    "content": "package org.odk.collect.maps\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.maps.traces.PolygonDescription\n\n@RunWith(AndroidJUnit4::class)\nclass PolygonDescriptionTest {\n    @Test\n    fun `getStrokeWidth returns the default value when the passed one is null`() {\n        val polygonDescription = PolygonDescription(strokeWidth = null)\n        assertThat(polygonDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH))\n    }\n\n    @Test\n    fun `getStrokeWidth returns the default value when the passed one is invalid`() {\n        val polygonDescription = PolygonDescription(strokeWidth = \"blah\")\n        assertThat(polygonDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH))\n    }\n\n    @Test\n    fun `getStrokeWidth returns the default value when the passed one is not greater than or equal to zero`() {\n        val polygonDescription = PolygonDescription(strokeWidth = \"-1\")\n        assertThat(polygonDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH))\n    }\n\n    @Test\n    fun `getStrokeWidth returns custom value when the passed one is a valid int number`() {\n        val polygonDescription = PolygonDescription(strokeWidth = \"10\")\n        assertThat(polygonDescription.getStrokeWidth(), equalTo(10f))\n    }\n\n    @Test\n    fun `getStrokeWidth returns custom value when the passed one is a valid float number`() {\n        val polygonDescription = PolygonDescription(strokeWidth = \"10.5\")\n        assertThat(polygonDescription.getStrokeWidth(), equalTo(10.5f))\n    }\n\n    @Test\n    fun `getStrokeColor returns the default color when the passed one is null`() {\n        val polygonDescription = PolygonDescription(strokeColor = null)\n        assertThat(polygonDescription.getStrokeColor(), equalTo(MapConsts.DEFAULT_STROKE_COLOR))\n    }\n\n    @Test\n    fun `getStrokeColor returns custom color`() {\n        val polygonDescription = PolygonDescription(strokeColor = -5583634)\n        assertThat(polygonDescription.getStrokeColor(), equalTo(-5583634))\n    }\n\n    @Test\n    fun `getFillColor returns the default color when the passed one is null`() {\n        val polygonDescription = PolygonDescription(fillColor = null)\n        assertThat(polygonDescription.getFillColor(), equalTo(1144954828))\n    }\n\n    @Test\n    fun `getFillColor returns custom color`() {\n        val polygonDescription = PolygonDescription(fillColor = 1152044270)\n        assertThat(polygonDescription.getFillColor(), equalTo(1152044270))\n    }\n}\n"
  },
  {
    "path": "maps/src/test/java/org/odk/collect/maps/RobolectricApplication.kt",
    "content": "package org.odk.collect.maps\n\nimport android.app.Application\nimport org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard\n\nclass RobolectricApplication : Application() {\n    override fun onCreate() {\n        super.onCreate()\n\n        // We don't want any clicks to be blocked\n        MultiClickGuard.test = true\n    }\n}\n"
  },
  {
    "path": "maps/src/test/java/org/odk/collect/maps/TraceDescriptionTest.kt",
    "content": "package org.odk.collect.maps\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.contains\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.maps.markers.MarkerDescription\nimport org.odk.collect.maps.markers.MarkerIconDescription\nimport org.odk.collect.maps.traces.TraceDescription\nimport org.odk.collect.maps.traces.getMarkersForPoints\n\n@RunWith(AndroidJUnit4::class)\nclass TraceDescriptionTest {\n\n    @Test\n    fun `#getMarkersForPoints returns MarkerDescriptions for points on trace`() {\n        val traceDescription = TestTraceDescription(\n            points = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 1.0)),\n            highlightLastPoint = false,\n            strokeWidth = 5f,\n            strokeColor = 123\n        )\n\n        val markers = traceDescription.getMarkersForPoints()\n        assertThat(\n            markers,\n            contains(\n                MarkerDescription(\n                    traceDescription.points[0],\n                    true,\n                    MapFragment.IconAnchor.CENTER,\n                    MarkerIconDescription.TracePoint(\n                        traceDescription.getStrokeWidth(),\n                        traceDescription.getStrokeColor()\n                    )\n                ),\n                MarkerDescription(\n                    traceDescription.points[1],\n                    true,\n                    MapFragment.IconAnchor.CENTER,\n                    MarkerIconDescription.TracePoint(\n                        traceDescription.getStrokeWidth(),\n                        traceDescription.getStrokeColor()\n                    )\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `#getMarkersForPoints returns last marker with highlight color when highlightLastPoint is true`() {\n        val traceDescription = TestTraceDescription(\n            points = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 1.0)),\n            highlightLastPoint = true,\n            strokeWidth = 5f,\n            strokeColor = 123\n        )\n\n        val icons = traceDescription.getMarkersForPoints().map {\n            it.iconDescription as MarkerIconDescription.TracePoint\n        }\n\n        assertThat(icons[0].color, equalTo(traceDescription.getStrokeColor()))\n        assertThat(icons[1].color, equalTo(MapConsts.DEFAULT_HIGHLIGHT_COLOR))\n    }\n}\n\nprivate class TestTraceDescription(\n    override val points: List<MapPoint>,\n    override val highlightLastPoint: Boolean,\n    private val strokeWidth: Float,\n    private val strokeColor: Int\n) : TraceDescription {\n    override fun getStrokeWidth(): Float {\n        return strokeWidth\n    }\n\n    override fun getStrokeColor(): Int {\n        return strokeColor\n    }\n}\n"
  },
  {
    "path": "maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport android.content.Context\nimport android.os.Bundle\nimport androidx.preference.Preference\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.contains\nimport org.hamcrest.Matchers.containsInAnyOrder\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.maps.MapConfigurator\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.shared.settings.Settings\nimport java.io.File\n\nclass DirectoryReferenceLayerRepositoryTest {\n    private val sharedLayersDir = TempFiles.createTempDir()\n    private val projectLayersDir = TempFiles.createTempDir()\n    private var mapConfigurator = StubMapConfigurator()\n    private val repository = DirectoryReferenceLayerRepository(\n        sharedLayersDir.absolutePath,\n        projectLayersDir.absolutePath\n    ) { mapConfigurator }\n\n    @Test\n    fun getAll_returnsAllSupportedLayersInTheDirectory() {\n        val file1 = TempFiles.createTempFile(sharedLayersDir)\n        val file2 = TempFiles.createTempFile(sharedLayersDir)\n        val file3 = TempFiles.createTempFile(sharedLayersDir)\n        mapConfigurator.apply {\n            addFile(file1, true, file1.name)\n            addFile(file2, false, file2.name)\n            addFile(file3, true, file3.name)\n        }\n\n        assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3))\n    }\n\n    @Test\n    fun getAll_returnsAllSupportedLayersInSubDirectories() {\n        val subDir = TempFiles.createTempDir(sharedLayersDir)\n        val file1 = TempFiles.createTempFile(subDir)\n        val file2 = TempFiles.createTempFile(subDir)\n        val file3 = TempFiles.createTempFile(subDir)\n        mapConfigurator.apply {\n            addFile(file1, true, file1.name)\n            addFile(file2, false, file2.name)\n            addFile(file3, true, file3.name)\n        }\n\n        assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3))\n    }\n\n    @Test\n    fun getAll_withMultipleDirectories_returnsAllSupportedLayersInAllDirectories() {\n        val file1 = TempFiles.createTempFile(sharedLayersDir)\n        val file2 = TempFiles.createTempFile(sharedLayersDir)\n        val file3 = TempFiles.createTempFile(projectLayersDir)\n        val file4 = TempFiles.createTempFile(projectLayersDir)\n        mapConfigurator.apply {\n            addFile(file1, true, file1.name)\n            addFile(file2, false, file2.name)\n            addFile(file3, true, file3.name)\n            addFile(file4, false, file4.name)\n        }\n\n        assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3))\n    }\n\n    /**\n     * We do this so we don't end up returns a list of reference layers with non-unique IDs. If two\n     * (or more) files have the same relative path, only the first one (in the order of declared\n     * layer directories) will be returned.\n     */\n    @Test\n    fun getAll_withMultipleDirectoriesWithFilesWithTheSameRelativePath_onlyReturnsTheSupportedFileFromTheFirstDirectory() {\n        val file1 = TempFiles.createTempFile(sharedLayersDir, \"blah\", \".temp\")\n        val file2 = TempFiles.createTempFile(projectLayersDir, \"blah\", \".temp\")\n        mapConfigurator.apply {\n            addFile(file1, true, file1.name)\n            addFile(file2, true, file2.name)\n        }\n\n        assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1))\n    }\n\n    @Test\n    fun getAllAlwaysUsesMapConfiguratorThatRepresentsTheCurrentConfiguration() {\n        val file = TempFiles.createTempFile(sharedLayersDir)\n\n        mapConfigurator.apply {\n            addFile(file, false, file.name)\n        }\n\n        assertThat(repository.getAll().isEmpty(), equalTo(true))\n\n        mapConfigurator = StubMapConfigurator().apply {\n            addFile(file, true, file.name)\n        }\n\n        assertThat(repository.getAll().isEmpty(), equalTo(false))\n    }\n\n    @Test\n    fun get_returnsProperLayer() {\n        val file1 = TempFiles.createTempFile(sharedLayersDir)\n        val file2 = TempFiles.createTempFile(sharedLayersDir)\n        mapConfigurator.apply {\n            addFile(file1, true, file1.name)\n            addFile(file2, true, file2.name)\n        }\n\n        val file2Layer = repository.getAll().first { it.file == file2 }\n        assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2))\n    }\n\n    @Test\n    fun get_returnsNullIfLayerIsNotSupported() {\n        val file = TempFiles.createTempFile(sharedLayersDir)\n        mapConfigurator.apply {\n            addFile(file, false, file.name)\n        }\n\n        val fileId = repository.getIdForFile(sharedLayersDir.absolutePath, file)\n        assertThat(repository.get(fileId), equalTo(null))\n    }\n\n    @Test\n    fun get_withMultipleDirectories_returnsLayer() {\n        val file1 = TempFiles.createTempFile(sharedLayersDir)\n        val file2 = TempFiles.createTempFile(projectLayersDir)\n        mapConfigurator.apply {\n            addFile(file1, true, file1.name)\n            addFile(file2, true, file2.name)\n        }\n\n        val file2Layer = repository.getAll().first { it.file == file2 }\n        assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2))\n    }\n\n    @Test\n    fun get_withMultipleDirectoriesWithFilesWithTheSameRelativePath_returnsLayerFromFirstDirectory() {\n        val file1 = TempFiles.createTempFile(sharedLayersDir, \"blah\", \".temp\")\n        val file2 = TempFiles.createTempFile(projectLayersDir, \"blah\", \".temp\")\n        mapConfigurator.apply {\n            addFile(file1, true, file1.name)\n            addFile(file2, true, file2.name)\n        }\n\n        val layerId = repository.getAll().first().id\n        assertThat(repository.get(layerId)!!.file, equalTo(file1))\n    }\n\n    @Test\n    fun get_whenFileDoesNotExist_returnsNull() {\n        val file = TempFiles.createTempFile(sharedLayersDir)\n        mapConfigurator.apply {\n            addFile(file, true, file.name)\n        }\n\n        val fileLayer = repository.getAll().first { it.file == file }\n\n        file.delete()\n        assertThat(repository.get(fileLayer.id), equalTo(null))\n    }\n\n    @Test\n    fun get_returnsLayerWithCorrectName() {\n        val file = TempFiles.createTempFile(sharedLayersDir)\n\n        mapConfigurator.apply {\n            addFile(file, true, file.name)\n        }\n\n        val fileLayer = repository.getAll().first { it.file == file }\n\n        assertThat(repository.get(fileLayer.id)!!.name, equalTo(file.name))\n    }\n\n    @Test\n    fun getAlwaysUsesMapConfiguratorThatRepresentsTheCurrentConfiguration() {\n        val file = TempFiles.createTempFile(sharedLayersDir)\n\n        mapConfigurator.apply {\n            addFile(file, false, file.name)\n        }\n\n        val fileId = repository.getIdForFile(sharedLayersDir.absolutePath, file)\n        assertThat(repository.get(fileId), equalTo(null))\n\n        mapConfigurator = StubMapConfigurator().apply {\n            addFile(file, true, file.name)\n        }\n\n        assertThat(repository.get(fileId)!!.file, equalTo(file))\n    }\n\n    @Test\n    fun addLayer_movesFileToTheSharedLayersDir_whenSharedIsTrue() {\n        val file = TempFiles.createTempFile().also {\n            it.writeText(\"blah\")\n        }\n\n        repository.addLayer(file, true)\n\n        assertThat(sharedLayersDir.listFiles().size, equalTo(1))\n        assertThat(sharedLayersDir.listFiles()[0].name, equalTo(file.name))\n        assertThat(sharedLayersDir.listFiles()[0].readText(), equalTo(\"blah\"))\n        assertThat(projectLayersDir.listFiles().size, equalTo(0))\n    }\n\n    @Test\n    fun addLayer_movesFileToTheProjectLayersDir_whenSharedIsFalse() {\n        val file = TempFiles.createTempFile().also {\n            it.writeText(\"blah\")\n        }\n\n        repository.addLayer(file, false)\n\n        assertThat(sharedLayersDir.listFiles().size, equalTo(0))\n        assertThat(projectLayersDir.listFiles().size, equalTo(1))\n        assertThat(projectLayersDir.listFiles()[0].name, equalTo(file.name))\n        assertThat(projectLayersDir.listFiles()[0].readText(), equalTo(\"blah\"))\n    }\n\n    @Test\n    fun delete_deletesLayerWithId() {\n        val file1 = TempFiles.createTempFile(sharedLayersDir)\n        val file2 = TempFiles.createTempFile(sharedLayersDir)\n\n        mapConfigurator.apply {\n            addFile(file1, true, file2.name)\n            addFile(file2, true, file2.name)\n        }\n\n        val fileLayer1 = repository.getAll().first { it.file == file1 }\n        val fileLayer2 = repository.getAll().first { it.file == file2 }\n        repository.delete(fileLayer1.id)\n\n        assertThat(repository.getAll(), contains(fileLayer2))\n    }\n\n    private class StubMapConfigurator : MapConfigurator {\n        private val files = mutableMapOf<File, Pair<Boolean, String>>()\n\n        override fun supportsLayer(file: File): Boolean {\n            return files[file]!!.first\n        }\n\n        override fun getDisplayName(file: File): String {\n            return files[file]!!.second\n        }\n\n        fun addFile(file: File, isSupported: Boolean, displayName: String) {\n            files[file] = Pair(isSupported, displayName)\n        }\n\n        override fun isAvailable(context: Context): Boolean {\n            TODO(\"Not yet implemented\")\n        }\n\n        override fun showUnavailableMessage(context: Context) {\n            TODO(\"Not yet implemented\")\n        }\n\n        override fun createPrefs(context: Context, settings: Settings): MutableList<Preference> {\n            TODO(\"Not yet implemented\")\n        }\n\n        override val prefKeys: Collection<String>\n            get() = TODO(\"Not yet implemented\")\n\n        override fun buildConfig(prefs: Settings): Bundle {\n            TODO(\"Not yet implemented\")\n        }\n    }\n}\n"
  },
  {
    "path": "maps/src/test/java/org/odk/collect/maps/layers/InMemReferenceLayerRepository.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport java.io.File\n\nclass InMemReferenceLayerRepository : ReferenceLayerRepository {\n    val sharedLayers = mutableListOf<ReferenceLayer>()\n    val projectLayers = mutableListOf<ReferenceLayer>()\n\n    override fun getAll(): List<ReferenceLayer> {\n        return sharedLayers + projectLayers\n    }\n\n    override fun get(id: String): ReferenceLayer? {\n        return sharedLayers.find { it.id == id } ?: projectLayers.find { it.id == id }\n    }\n\n    override fun addLayer(file: File, shared: Boolean) {\n        if (shared) {\n            sharedLayers.add(ReferenceLayer(file.absolutePath, file, file.name))\n        } else {\n            projectLayers.add(ReferenceLayer(file.absolutePath, file, file.name))\n        }\n    }\n\n    override fun delete(id: String) {\n        sharedLayers.removeIf { it.id == id }\n        projectLayers.removeIf { it.id == id }\n    }\n}\n"
  },
  {
    "path": "maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport android.os.Bundle\nimport org.junit.Assert.assertNotNull\nimport org.junit.Assert.assertNull\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.maps.MapConfigurator\nimport org.odk.collect.maps.MapFragment\nimport org.odk.collect.shared.TempFiles.createTempDir\nimport org.robolectric.RobolectricTestRunner\nimport java.io.File\n\n@RunWith(RobolectricTestRunner::class)\nclass MapFragmentReferenceLayerUtilsTest {\n\n    @Test\n    fun getReferenceLayerFile_whenPathIsNull_should_getReferenceLayerFileReturnNull() {\n        val layersPath = createTempDir().absolutePath\n        val config = Bundle()\n        config.putString(MapFragment.KEY_REFERENCE_LAYER, null)\n        assertNull(\n            MapFragmentReferenceLayerUtils.getReferenceLayerFile(\n                config,\n                DirectoryReferenceLayerRepository(layersPath, \"\", mock())\n            )\n        )\n    }\n\n    @Test\n    fun whenOfflineLayerFileDoesNotExist_should_getReferenceLayerFileReturnNull() {\n        val layersPath = createTempDir().absolutePath\n        val config = Bundle()\n        config.putString(MapFragment.KEY_REFERENCE_LAYER, \"blah\")\n        assertNull(\n            MapFragmentReferenceLayerUtils.getReferenceLayerFile(\n                config,\n                DirectoryReferenceLayerRepository(layersPath, \"\", mock())\n            )\n        )\n    }\n\n    @Test\n    fun whenOfflineLayerFileExist_should_getReferenceLayerFileReturnThatFile() {\n        val layersPath = createTempDir().absolutePath\n        val file = File(layersPath, \"blah\").also {\n            it.writeBytes(byteArrayOf())\n        }\n\n        val config = Bundle()\n        config.putString(MapFragment.KEY_REFERENCE_LAYER, \"blah\")\n\n        val mapConfigurator = mock<MapConfigurator>().also {\n            whenever(it.supportsLayer(file)).thenReturn(true)\n            whenever(it.getDisplayName(File(layersPath, \"blah\"))).thenReturn(\"blah\")\n        }\n\n        assertNotNull(\n            MapFragmentReferenceLayerUtils.getReferenceLayerFile(\n                config,\n                DirectoryReferenceLayerRepository(layersPath, \"\") { mapConfigurator }\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterDialogFragmentTest.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport android.app.Application\nimport androidx.core.net.toUri\nimport androidx.fragment.app.testing.FragmentScenario\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers.isChecked\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.isEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.not\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.containsInAnyOrder\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.strings.R\nimport org.odk.collect.strings.localization.getLocalizedQuantityString\nimport org.odk.collect.testshared.FakeScheduler\nimport org.odk.collect.testshared.EspressoInteractions\nimport org.odk.collect.testshared.RecyclerViewMatcher\nimport org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView\nimport org.odk.collect.testshared.RobolectricHelpers\n\n@RunWith(AndroidJUnit4::class)\nclass OfflineMapLayersImporterDialogFragmentTest {\n    private val scheduler = FakeScheduler()\n    private val referenceLayerRepository = InMemReferenceLayerRepository()\n    private val settingsProvider = InMemSettingsProvider()\n\n    @get:Rule\n    val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule(\n        FragmentFactoryBuilder()\n            .forClass(OfflineMapLayersImporterDialogFragment::class) {\n                OfflineMapLayersImporterDialogFragment(\n                    referenceLayerRepository,\n                    scheduler,\n                    settingsProvider\n                )\n            }.build()\n    )\n\n    @Test\n    fun `clicking the 'cancel' button dismisses the dialog`() {\n        launchFragment().onFragment {\n            scheduler.flush()\n            assertThat(it.isVisible, equalTo(true))\n            EspressoInteractions.clickOn(withText(R.string.cancel))\n            assertThat(it.isVisible, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `clicking the 'add layer' button dismisses the dialog`() {\n        launchFragment().onFragment {\n            scheduler.flush()\n            assertThat(it.isVisible, equalTo(true))\n            it.viewModel.loadLayersToImport(emptyList(), it.requireContext())\n            EspressoInteractions.clickOn(withId(org.odk.collect.maps.R.id.add_layer_button))\n            scheduler.flush()\n            RobolectricHelpers.runLooper()\n            assertThat(it.isVisible, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `progress indicator is displayed during loading layers`() {\n        val file1 = TempFiles.createTempFile(\"layer1\", MbtilesFile.FILE_EXTENSION)\n        val file2 = TempFiles.createTempFile(\"layer2\", MbtilesFile.FILE_EXTENSION)\n\n        launchFragment().onFragment {\n            it.viewModel.loadLayersToImport(\n                listOf(file1.toUri(), file2.toUri()),\n                it.requireContext()\n            )\n        }\n\n        onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(isDisplayed()))\n        onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(not(isDisplayed())))\n\n        scheduler.flush()\n\n        onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(not(isDisplayed())))\n        onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `the 'cancel' button is enabled during loading layers`() {\n        launchFragment()\n\n        onView(withId(org.odk.collect.maps.R.id.cancel_button)).check(matches(isEnabled()))\n        scheduler.flush()\n        onView(withId(org.odk.collect.maps.R.id.cancel_button)).check(matches(isEnabled()))\n    }\n\n    @Test\n    fun `the 'add layer' button is disabled during loading layers`() {\n        val file = TempFiles.createTempFile(\"layer\", MbtilesFile.FILE_EXTENSION)\n\n        launchFragment().onFragment {\n            it.viewModel.loadLayersToImport(listOf(file.toUri()), it.requireContext())\n        }\n\n        onView(withId(org.odk.collect.maps.R.id.add_layer_button)).check(matches(not(isEnabled())))\n        scheduler.flush()\n        onView(withId(org.odk.collect.maps.R.id.add_layer_button)).check(matches(isEnabled()))\n    }\n\n    @Test\n    fun `'All projects' location should be selected by default`() {\n        launchFragment()\n\n        onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(isChecked()))\n        onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(not(isChecked())))\n    }\n\n    @Test\n    fun `checking location sets selection correctly`() {\n        launchFragment()\n        scheduler.flush()\n\n        EspressoInteractions.clickOn(withId(org.odk.collect.maps.R.id.current_project_option))\n\n        onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(not(isChecked())))\n        onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(isChecked()))\n\n        EspressoInteractions.clickOn(withId(org.odk.collect.maps.R.id.all_projects_option))\n\n        onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(isChecked()))\n        onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(not(isChecked())))\n    }\n\n    @Test\n    fun `recreating maintains the selected layers location`() {\n        val scenario = launchFragment()\n        scheduler.flush()\n\n        EspressoInteractions.clickOn(withId(org.odk.collect.maps.R.id.current_project_option))\n\n        scenario.recreate()\n\n        onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(not(isChecked())))\n        onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(isChecked()))\n    }\n\n    @Test\n    fun `the list of selected layers should be displayed in A-Z order`() {\n        val file1 = TempFiles.createTempFile(\"layerB\", MbtilesFile.FILE_EXTENSION)\n        val file2 = TempFiles.createTempFile(\"layerA\", MbtilesFile.FILE_EXTENSION)\n\n        launchFragment().onFragment {\n            it.viewModel.loadLayersToImport(\n                listOf(file1.toUri(), file2.toUri()),\n                it.requireContext()\n            )\n        }\n\n        scheduler.flush()\n\n        onView(withId(org.odk.collect.maps.R.id.layers)).check(\n            matches(\n                RecyclerViewMatcher.withListSize(\n                    2\n                )\n            )\n        )\n        onView(\n            withRecyclerView(org.odk.collect.maps.R.id.layers).atPositionOnView(\n                0,\n                org.odk.collect.maps.R.id.layer_name\n            )\n        ).check(matches(withText(file2.name)))\n        onView(\n            withRecyclerView(org.odk.collect.maps.R.id.layers).atPositionOnView(\n                1,\n                org.odk.collect.maps.R.id.layer_name\n            )\n        ).check(matches(withText(file1.name)))\n    }\n\n    @Test\n    fun `recreating maintains the list of selected layers`() {\n        val file1 = TempFiles.createTempFile(\"layer1\", MbtilesFile.FILE_EXTENSION)\n        val file2 = TempFiles.createTempFile(\"layer2\", MbtilesFile.FILE_EXTENSION)\n\n        val scenario = launchFragment().onFragment {\n            it.viewModel.loadLayersToImport(\n                listOf(file1.toUri(), file2.toUri()),\n                it.requireContext()\n            )\n        }\n\n        scheduler.flush()\n\n        scenario.recreate()\n\n        onView(withId(org.odk.collect.maps.R.id.layers)).check(\n            matches(\n                RecyclerViewMatcher.withListSize(\n                    2\n                )\n            )\n        )\n        onView(\n            withRecyclerView(org.odk.collect.maps.R.id.layers).atPositionOnView(\n                0,\n                org.odk.collect.maps.R.id.layer_name\n            )\n        ).check(matches(withText(file1.name)))\n        onView(\n            withRecyclerView(org.odk.collect.maps.R.id.layers).atPositionOnView(\n                1,\n                org.odk.collect.maps.R.id.layer_name\n            )\n        ).check(matches(withText(file2.name)))\n    }\n\n    @Test\n    fun `only mbtiles files are taken into account`() {\n        val file1 = TempFiles.createTempFile(\"layer1\", MbtilesFile.FILE_EXTENSION)\n        val file2 = TempFiles.createTempFile(\"layer2\", \".txt\")\n\n        launchFragment().onFragment {\n            it.viewModel.loadLayersToImport(\n                listOf(file1.toUri(), file2.toUri()),\n                it.requireContext()\n            )\n        }\n\n        scheduler.flush()\n\n        onView(withId(org.odk.collect.maps.R.id.layers)).check(\n            matches(\n                RecyclerViewMatcher.withListSize(\n                    1\n                )\n            )\n        )\n        onView(\n            withRecyclerView(org.odk.collect.maps.R.id.layers).atPositionOnView(\n                0,\n                org.odk.collect.maps.R.id.layer_name\n            )\n        ).check(matches(withText(file1.name)))\n    }\n\n    @Test\n    fun `clicking the 'add layer' button moves the files to the shared layers dir if it is selected`() {\n        val file1 = TempFiles.createTempFile(\"layer1\", MbtilesFile.FILE_EXTENSION)\n        val file2 = TempFiles.createTempFile(\"layer2\", MbtilesFile.FILE_EXTENSION)\n\n        launchFragment().onFragment {\n            it.viewModel.loadLayersToImport(\n                listOf(file1.toUri(), file2.toUri()),\n                it.requireContext()\n            )\n        }\n\n        scheduler.flush()\n\n        EspressoInteractions.clickOn(withId(org.odk.collect.maps.R.id.add_layer_button))\n        scheduler.flush()\n\n        assertThat(referenceLayerRepository.projectLayers.size, equalTo(0))\n        assertThat(referenceLayerRepository.sharedLayers.size, equalTo(2))\n        val filesInSharedLayersDir = referenceLayerRepository.sharedLayers.map { it.file.name }\n        assertThat(filesInSharedLayersDir, containsInAnyOrder(file1.name, file2.name))\n    }\n\n    @Test\n    fun `clicking the 'add layer' button moves the files to the project layers dir if it is selected`() {\n        val file1 = TempFiles.createTempFile(\"layer1\", MbtilesFile.FILE_EXTENSION)\n        val file2 = TempFiles.createTempFile(\"layer2\", MbtilesFile.FILE_EXTENSION)\n\n        launchFragment().onFragment {\n            it.viewModel.loadLayersToImport(\n                listOf(file1.toUri(), file2.toUri()),\n                it.requireContext()\n            )\n        }\n\n        scheduler.flush()\n\n        EspressoInteractions.clickOn(withId(org.odk.collect.maps.R.id.current_project_option))\n        EspressoInteractions.clickOn(withId(org.odk.collect.maps.R.id.add_layer_button))\n        scheduler.flush()\n\n        assertThat(referenceLayerRepository.sharedLayers.size, equalTo(0))\n        assertThat(referenceLayerRepository.projectLayers.size, equalTo(2))\n        val filesInProjectLayersDir = referenceLayerRepository.projectLayers.map { it.file.name }\n        assertThat(filesInProjectLayersDir, containsInAnyOrder(file1.name, file2.name))\n    }\n\n    @Test\n    fun `the warning dialog is displayed if some selected files are not supported and the importer dialog is kept displayed`() {\n        val file1 = TempFiles.createTempFile(\"layerA\", \".txt\")\n        val file2 = TempFiles.createTempFile(\"layerB\", MbtilesFile.FILE_EXTENSION)\n\n        launchFragment().onFragment {\n            it.viewModel.loadLayersToImport(\n                listOf(file1.toUri(), file2.toUri()),\n                it.requireContext()\n            )\n\n            scheduler.flush()\n\n            val context = ApplicationProvider.getApplicationContext<Application>()\n            onView(\n                withText(\n                    context.getLocalizedQuantityString(\n                        R.plurals.non_mbtiles_files_selected_title,\n                        1,\n                        1\n                    )\n                )\n            ).inRoot(isDialog()).check(matches(isDisplayed()))\n            onView(withText(R.string.some_non_mbtiles_files_selected_message)).inRoot(isDialog())\n                .check(matches(isDisplayed()))\n            onView(withText(R.string.ok)).inRoot(isDialog()).check(matches(isDisplayed()))\n\n            assertThat(it.isVisible, equalTo(true))\n        }\n    }\n\n    @Test\n    fun `the warning dialog is displayed if all selected files are not supported and the importer dialog is dismissed`() {\n        val file = TempFiles.createTempFile(\"layerA\", \".txt\")\n\n        launchFragment().onFragment {\n            it.viewModel.loadLayersToImport(listOf(file.toUri()), it.requireContext())\n\n            scheduler.flush()\n\n            val context = ApplicationProvider.getApplicationContext<Application>()\n            onView(\n                withText(\n                    context.getLocalizedQuantityString(\n                        R.plurals.non_mbtiles_files_selected_title,\n                        1,\n                        1\n                    )\n                )\n            ).inRoot(isDialog()).check(matches(isDisplayed()))\n            onView(withText(R.string.all_non_mbtiles_files_selected_message)).inRoot(isDialog())\n                .check(matches(isDisplayed()))\n            onView(withText(R.string.ok)).inRoot(isDialog()).check(matches(isDisplayed()))\n\n            assertThat(it.isVisible, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `the warning dialog shows correct number of unsupported layers`() {\n        val context = ApplicationProvider.getApplicationContext<Application>()\n\n        launchFragment().onFragment {\n            // Three unsupported layers\n            it.viewModel.loadLayersToImport(\n                listOf(\n                    TempFiles.createTempFile(\"layerA\", \".txt\").toUri(),\n                    TempFiles.createTempFile(\"layerB\", \".txt\").toUri(),\n                    TempFiles.createTempFile(\"layerC\", \".txt\").toUri()\n                ),\n                context\n            )\n            scheduler.flush()\n            onView(\n                withText(\n                    context.getLocalizedQuantityString(\n                        R.plurals.non_mbtiles_files_selected_title,\n                        3,\n                        3\n                    )\n                )\n            ).inRoot(isDialog()).check(matches(isDisplayed()))\n        }\n\n        launchFragment().onFragment {\n            // Two unsupported layers\n            it.viewModel.loadLayersToImport(\n                listOf(\n                    TempFiles.createTempFile(\"layerA\", \".txt\").toUri(),\n                    TempFiles.createTempFile(\"layerB\", \".txt\").toUri(),\n                    TempFiles.createTempFile(\"layerC\", MbtilesFile.FILE_EXTENSION).toUri()\n                ),\n                context\n            )\n            scheduler.flush()\n            onView(\n                withText(\n                    context.getLocalizedQuantityString(\n                        R.plurals.non_mbtiles_files_selected_title,\n                        2,\n                        2\n                    )\n                )\n            ).inRoot(isDialog()).check(matches(isDisplayed()))\n        }\n\n        launchFragment().onFragment {\n            // One unsupported layer\n            it.viewModel.loadLayersToImport(\n                listOf(\n                    TempFiles.createTempFile(\"layerA\", \".txt\").toUri(),\n                    TempFiles.createTempFile(\"layerB\", MbtilesFile.FILE_EXTENSION).toUri(),\n                    TempFiles.createTempFile(\"layerC\", MbtilesFile.FILE_EXTENSION).toUri()\n                ),\n                context\n            )\n            scheduler.flush()\n            onView(\n                withText(\n                    context.getLocalizedQuantityString(\n                        R.plurals.non_mbtiles_files_selected_title,\n                        1,\n                        1\n                    )\n                )\n            ).inRoot(isDialog()).check(matches(isDisplayed()))\n        }\n    }\n\n    private fun launchFragment(): FragmentScenario<OfflineMapLayersImporterDialogFragment> {\n        return fragmentScenarioLauncherRule.launchInContainer(OfflineMapLayersImporterDialogFragment::class.java)\n    }\n}\n"
  },
  {
    "path": "maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerBottomSheetDialogFragmentTest.kt",
    "content": "package org.odk.collect.maps.layers\n\nimport android.net.Uri\nimport androidx.activity.result.ActivityResultRegistry\nimport androidx.activity.result.contract.ActivityResultContract\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.core.app.ActivityOptionsCompat\nimport androidx.core.net.toUri\nimport androidx.fragment.app.testing.FragmentScenario\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.scrollTo\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition\nimport androidx.test.espresso.matcher.RootMatchers.isDialog\nimport androidx.test.espresso.matcher.ViewMatchers\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport androidx.test.espresso.matcher.ViewMatchers.isChecked\nimport androidx.test.espresso.matcher.ViewMatchers.isDisplayed\nimport androidx.test.espresso.matcher.ViewMatchers.isEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.espresso.matcher.ViewMatchers.withText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.instanceOf\nimport org.hamcrest.CoreMatchers.not\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.eq\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.odk.collect.androidshared.ui.FragmentFactoryBuilder\nimport org.odk.collect.androidtest.DrawableMatcher.withImageDrawable\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule\nimport org.odk.collect.maps.R\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.strings.R.string\nimport org.odk.collect.testshared.FakeScheduler\nimport org.odk.collect.testshared.EspressoInteractions\nimport org.odk.collect.testshared.RecyclerViewMatcher\nimport org.odk.collect.testshared.ViewMatchers.atPositionInRecyclerView\nimport org.odk.collect.testshared.WaitFor\nimport org.odk.collect.webpage.WebPageService\n\n@RunWith(AndroidJUnit4::class)\nclass OfflineMapLayersPickerBottomSheetDialogFragmentTest {\n    private val referenceLayerRepository = InMemReferenceLayerRepository()\n    private val scheduler = FakeScheduler()\n    private val settingsProvider = InMemSettingsProvider()\n    private val webPageService = mock<WebPageService>()\n\n    private val testRegistry = TestRegistry()\n\n    @get:Rule\n    val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule(\n        FragmentFactoryBuilder()\n            .forClass(OfflineMapLayersPickerBottomSheetDialogFragment::class) {\n                OfflineMapLayersPickerBottomSheetDialogFragment(\n                    testRegistry,\n                    referenceLayerRepository,\n                    scheduler,\n                    settingsProvider,\n                    webPageService\n                )\n            }.build()\n    )\n\n    @Test\n    fun `clicking the 'cancel' button dismisses the layers picker`() {\n        val scenario = launchFragment()\n\n        scenario.onFragment {\n            assertThat(it.isVisible, equalTo(true))\n            EspressoInteractions.clickOn(withText(string.cancel))\n            assertThat(it.isVisible, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `clicking the 'cancel' button does not save the layer`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        EspressoInteractions.clickOn(withText(string.cancel))\n        assertThat(\n            settingsProvider.getUnprotectedSettings().contains(ProjectKeys.KEY_REFERENCE_LAYER),\n            equalTo(false)\n        )\n    }\n\n    @Test\n    fun `the 'cancel' button should be enabled during loading layers`() {\n        launchFragment()\n\n        onView(withText(string.cancel)).check(matches(isEnabled()))\n    }\n\n    @Test\n    fun `clicking the 'save' button dismisses the layers picker`() {\n        val scenario = launchFragment()\n\n        scheduler.flush()\n\n        scenario.onFragment {\n            assertThat(it.isVisible, equalTo(true))\n            EspressoInteractions.clickOn(withText(string.save))\n            assertThat(it.isVisible, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `the 'save' button should be disabled during loading layers`() {\n        launchFragment()\n\n        onView(withText(string.save)).check(matches(not(isEnabled())))\n        scheduler.flush()\n        onView(withText(string.save)).check(matches(isEnabled()))\n    }\n\n    @Test\n    fun `clicking the 'save' button saves null when 'None' option is checked`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        EspressoInteractions.clickOn(withText(string.save))\n        assertThat(\n            settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER),\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun `clicking the 'save' button saves the layer id if any is checked`() {\n        val file = TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION)\n        referenceLayerRepository.addLayer(file, true)\n\n        launchFragment()\n\n        scheduler.flush()\n\n        EspressoInteractions.clickOn(withText(\"layer1${MbtilesFile.FILE_EXTENSION}\"))\n        EspressoInteractions.clickOn(withText(string.save))\n        assertThat(\n            settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER),\n            equalTo(file.absolutePath)\n        )\n    }\n\n    @Test\n    fun `when no layer id is saved in settings the 'None' option should be checked`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.radio_button))\n            .check(matches(isChecked()))\n\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.radio_button))\n            .check(matches(not(isChecked())))\n    }\n\n    @Test\n    fun `when layer id is saved in settings the layer it belongs to should be checked`() {\n        val file1 = TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION)\n        val file2 = TempFiles.createTempFileWithName(\"layer2\", MbtilesFile.FILE_EXTENSION)\n        referenceLayerRepository.addLayer(file1, true)\n        referenceLayerRepository.addLayer(file2, true)\n\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, file2.absolutePath)\n\n        launchFragment()\n\n        scheduler.flush()\n\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.radio_button))\n            .check(matches(not(isChecked())))\n\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.radio_button))\n            .check(matches(not(isChecked())))\n\n        onView(atPositionInRecyclerView(R.id.layers, 2, R.id.radio_button))\n            .check(matches(isChecked()))\n    }\n\n    @Test\n    fun `when layer id is saved in settings but the layer it belongs to does not exist the 'None' option should be checked`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, \"2\")\n\n        launchFragment()\n\n        scheduler.flush()\n\n        onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2)))\n\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.title))\n            .check(matches(withText(string.none)))\n\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.radio_button))\n            .check(matches(isChecked()))\n    }\n\n    @Test\n    fun `progress indicator is displayed during loading layers`() {\n        launchFragment()\n\n        onView(withId(R.id.progress_indicator)).check(matches(isDisplayed()))\n        onView(withId(R.id.layers)).check(matches(not(isDisplayed())))\n\n        scheduler.flush()\n\n        onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed())))\n        onView(withId(R.id.layers)).check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `the 'learn more' button should be enabled during loading layers`() {\n        launchFragment()\n\n        onView(withText(string.get_help_with_offline_layers)).check(matches(isEnabled()))\n    }\n\n    @Test\n    fun `clicking the 'learn more' button opens the forum thread`() {\n        launchFragment()\n\n        scheduler.flush()\n\n        EspressoInteractions.clickOn(withText(string.get_help_with_offline_layers))\n\n        verify(webPageService).openWebPage(\n            any(),\n            eq(Uri.parse(\"https://docs.getodk.org/collect-offline-maps/#transferring-offline-tilesets-to-devices\"))\n        )\n    }\n\n    @Test\n    fun `if there are no layers the 'none' option is displayed`() {\n        launchFragment()\n\n        scheduler.flush()\n\n        onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(1)))\n\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.title))\n            .check(matches(withText(string.none)))\n    }\n\n    @Test\n    fun `if there are multiple layers all of them are displayed along with the 'None' and sorted in A-Z order`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layerB\", MbtilesFile.FILE_EXTENSION), true\n        )\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layerA\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3)))\n\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.title))\n            .check(matches(withText(string.none)))\n\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.title))\n            .check(matches(withText(\"layerA${MbtilesFile.FILE_EXTENSION}\")))\n\n        onView(atPositionInRecyclerView(R.id.layers, 2, R.id.title))\n            .check(matches(withText(\"layerB${MbtilesFile.FILE_EXTENSION}\")))\n    }\n\n    @Test\n    fun `checking layers sets selection correctly`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        EspressoInteractions.clickOn(withText(\"layer1${MbtilesFile.FILE_EXTENSION}\"))\n        WaitFor.waitFor {\n            onView(atPositionInRecyclerView(R.id.layers, 0, R.id.radio_button))\n                .check(matches(not(isChecked())))\n\n            onView(atPositionInRecyclerView(R.id.layers, 1, R.id.radio_button))\n                .check(matches(isChecked()))\n        }\n\n        EspressoInteractions.clickOn(withText(string.none))\n        WaitFor.waitFor {\n            onView(atPositionInRecyclerView(R.id.layers, 0, R.id.radio_button))\n                .check(matches(isChecked()))\n\n            onView(atPositionInRecyclerView(R.id.layers, 1, R.id.radio_button))\n                .check(matches(not(isChecked())))\n        }\n    }\n\n    @Test\n    fun `recreating maintains selection`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        val scenario = launchFragment()\n\n        scheduler.flush()\n\n        EspressoInteractions.clickOn(withText(\"layer1${MbtilesFile.FILE_EXTENSION}\"))\n        scenario.recreate()\n        scheduler.flush()\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.radio_button))\n            .check(matches(not(isChecked())))\n\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.radio_button))\n            .check(matches(isChecked()))\n    }\n\n    @Test\n    fun `clicking the 'add layer' and selecting layers displays the confirmation dialog`() {\n        val scenario = launchFragment()\n\n        testRegistry.addUris(Uri.parse(\"blah\"))\n        EspressoInteractions.clickOn(withText(string.add_layer))\n\n        scenario.onFragment {\n            assertThat(\n                it.childFragmentManager.findFragmentByTag(OfflineMapLayersImporterDialogFragment::class.java.name),\n                instanceOf(OfflineMapLayersImporterDialogFragment::class.java)\n            )\n        }\n    }\n\n    @Test\n    fun `clicking the 'add layer' and selecting nothing does not display the confirmation dialog`() {\n        val scenario = launchFragment()\n\n        EspressoInteractions.clickOn(withText(string.add_layer))\n\n        scenario.onFragment {\n            assertThat(\n                it.childFragmentManager.findFragmentByTag(OfflineMapLayersImporterDialogFragment::class.java.name),\n                equalTo(null)\n            )\n        }\n    }\n\n    @Test\n    fun `progress indicator is displayed during loading layers after receiving new ones`() {\n        val file1 = TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION)\n        val file2 = TempFiles.createTempFileWithName(\"layer2\", MbtilesFile.FILE_EXTENSION)\n\n        launchFragment()\n\n        scheduler.flush()\n\n        testRegistry.addUris(file1.toUri(), file2.toUri())\n\n        EspressoInteractions.clickOn(withText(string.add_layer))\n        scheduler.flush()\n        onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(scrollTo(), click())\n\n        onView(withId(R.id.progress_indicator)).check(matches(isDisplayed()))\n        onView(withId(R.id.layers)).check(matches(not(isDisplayed())))\n\n        scheduler.flush()\n\n        onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed())))\n        onView(withId(R.id.layers)).check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `when new layers added the list should be updated`() {\n        val file1 = TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION)\n        val file2 = TempFiles.createTempFileWithName(\"layer2\", MbtilesFile.FILE_EXTENSION)\n\n        launchFragment()\n        scheduler.flush()\n        onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(1)))\n\n        testRegistry.addUris(file1.toUri(), file2.toUri())\n\n        EspressoInteractions.clickOn(withText(string.add_layer))\n        scheduler.flush()\n        onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(scrollTo(), click())\n        scheduler.flush()\n\n        WaitFor.waitFor {\n            onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3)))\n        }\n\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.title))\n            .check(matches(withText(string.none)))\n\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.title))\n            .check(matches(withText(file1.name)))\n\n        onView(atPositionInRecyclerView(R.id.layers, 2, R.id.title))\n            .check(matches(withText(file2.name)))\n    }\n\n    @Test\n    fun `layers are collapsed by default`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer2\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        assertLayerCollapsed(1)\n        assertLayerCollapsed(2)\n    }\n\n    @Test\n    fun `recreating maintains expanded layers`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer2\", MbtilesFile.FILE_EXTENSION), true\n        )\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer3\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        val scenario = launchFragment()\n\n        scheduler.flush()\n\n        expandLayer(1)\n        onView(withId(R.id.layers)).perform(scrollToPosition<RecyclerView.ViewHolder>(3))\n        expandLayer(3)\n\n        scenario.recreate()\n        scheduler.flush()\n\n        assertLayerExpanded(1)\n        assertLayerCollapsed(2)\n        assertLayerExpanded(3)\n    }\n\n    @Test\n    fun `correct path is displayed after expanding layers`() {\n        val file1 = TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION)\n        val file2 = TempFiles.createTempFileWithName(\"layer2\", MbtilesFile.FILE_EXTENSION)\n        referenceLayerRepository.addLayer(file1, true)\n        referenceLayerRepository.addLayer(file2, true)\n\n        launchFragment()\n\n        scheduler.flush()\n\n        expandLayer(1)\n        onView(withId(R.id.layers)).perform(scrollToPosition<RecyclerView.ViewHolder>(2))\n        expandLayer(2)\n\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.path))\n            .check(matches(withText(file1.absolutePath)))\n\n        onView(atPositionInRecyclerView(R.id.layers, 2, R.id.path))\n            .check(matches(withText(file2.absolutePath)))\n    }\n\n    @Test\n    fun `clicking delete shows the confirmation dialog`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        expandLayer(1)\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.delete_layer))\n            .perform(scrollTo(), click())\n\n        onView(withText(string.cancel)).inRoot(isDialog()).check(matches(isDisplayed()))\n        onView(withText(string.delete_layer)).inRoot(isDialog()).check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `clicking delete and canceling does not remove the layer`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        expandLayer(1)\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.delete_layer))\n            .perform(scrollTo(), click())\n\n        onView(withText(string.cancel)).inRoot(isDialog()).perform(click())\n\n        onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2)))\n        onView(withId(R.id.layers)).perform(scrollToPosition<RecyclerView.ViewHolder>(0))\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.title))\n            .check(matches(withText(string.none)))\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.title))\n            .check(matches(withText(\"layer1${MbtilesFile.FILE_EXTENSION}\")))\n        assertThat(referenceLayerRepository.getAll().size, equalTo(1))\n    }\n\n    @Test\n    fun `clicking delete and confirming removes the layer`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer2\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        expandLayer(1)\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.delete_layer))\n            .perform(scrollTo(), click())\n\n        onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click())\n        scheduler.flush()\n\n        onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2)))\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.title))\n            .check(matches(withText(string.none)))\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.title))\n            .check(matches(withText(\"layer2${MbtilesFile.FILE_EXTENSION}\")))\n\n        assertThat(referenceLayerRepository.getAll().size, equalTo(1))\n    }\n\n    @Test\n    fun `deleting the selected layer changes selection to 'none' and saves it`() {\n        val file = TempFiles.createTempFileWithName(\"layer1.${MbtilesFile.FILE_EXTENSION}\", MbtilesFile.FILE_EXTENSION)\n        referenceLayerRepository.addLayer(file, true)\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, file.absolutePath)\n\n        launchFragment()\n\n        scheduler.flush()\n\n        expandLayer(1)\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.delete_layer))\n            .perform(scrollTo(), click())\n\n        onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click())\n        scheduler.flush()\n\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.title))\n            .check(matches(withText(string.none)))\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.radio_button))\n            .check(matches(isChecked()))\n        assertThat(\n            settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER),\n            equalTo(null)\n        )\n    }\n\n    @Test\n    fun `deleting one of the layers keeps the list sorted in A-Z order`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layerC\", MbtilesFile.FILE_EXTENSION), true\n        )\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layerB\", MbtilesFile.FILE_EXTENSION), true\n        )\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layerA\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        onView(withId(R.id.layers)).perform(scrollToPosition<RecyclerView.ViewHolder>(2))\n        expandLayer(2)\n        onView(atPositionInRecyclerView(R.id.layers, 2, R.id.delete_layer))\n            .perform(scrollTo(), click())\n\n        onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click())\n        scheduler.flush()\n\n        onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3)))\n        onView(atPositionInRecyclerView(R.id.layers, 0, R.id.title))\n            .check(matches(withText(string.none)))\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.title))\n            .check(matches(withText(\"layerA${MbtilesFile.FILE_EXTENSION}\")))\n        onView(atPositionInRecyclerView(R.id.layers, 2, R.id.title))\n            .check(matches(withText(\"layerC${MbtilesFile.FILE_EXTENSION}\")))\n    }\n\n    @Test\n    fun `progress indicator is displayed during deleting layers`() {\n        referenceLayerRepository.addLayer(\n            TempFiles.createTempFileWithName(\"layer1\", MbtilesFile.FILE_EXTENSION), true\n        )\n\n        launchFragment()\n\n        scheduler.flush()\n\n        expandLayer(1)\n        onView(atPositionInRecyclerView(R.id.layers, 1, R.id.delete_layer))\n            .perform(scrollTo(), click())\n        onView(withText(string.delete_layer)).inRoot(isDialog()).perform(click())\n\n        onView(withId(R.id.progress_indicator)).check(matches(isDisplayed()))\n        onView(withId(R.id.layers)).check(matches(not(isDisplayed())))\n\n        scheduler.flush()\n\n        onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed())))\n        onView(withId(R.id.layers)).check(matches(isDisplayed()))\n    }\n\n    @Test\n    fun `the confirmation dialog is dismissed o activity recreation`() {\n        val scenario = launchFragment()\n\n        testRegistry.addUris(Uri.parse(\"blah\"))\n        EspressoInteractions.clickOn(withText(string.add_layer))\n\n        scenario.onFragment {\n            assertThat(\n                it.childFragmentManager.findFragmentByTag(OfflineMapLayersImporterDialogFragment::class.java.name),\n                instanceOf(OfflineMapLayersImporterDialogFragment::class.java)\n            )\n        }\n\n        scenario.recreate()\n\n        scenario.onFragment {\n            assertThat(\n                it.childFragmentManager.findFragmentByTag(OfflineMapLayersImporterDialogFragment::class.java.name),\n                equalTo(null)\n            )\n        }\n    }\n\n    private fun expandLayer(position: Int) {\n        onView(atPositionInRecyclerView(R.id.layers, position, R.id.arrow))\n            .perform(click())\n        WaitFor.waitFor {\n            assertLayerExpanded(position)\n        }\n    }\n\n    private fun assertLayerCollapsed(position: Int) {\n        onView(atPositionInRecyclerView(R.id.layers, position, R.id.arrow))\n            .check(\n                matches(\n                    withImageDrawable(org.odk.collect.icons.R.drawable.ic_baseline_expand_24)\n                )\n            )\n        onView(atPositionInRecyclerView(R.id.layers, position, R.id.path))\n            .check(\n                matches(\n                    withEffectiveVisibility(ViewMatchers.Visibility.GONE)\n                )\n            )\n        onView(atPositionInRecyclerView(R.id.layers, position, R.id.delete_layer))\n            .check(\n                matches(\n                    withEffectiveVisibility(ViewMatchers.Visibility.GONE)\n                )\n            )\n    }\n\n    private fun assertLayerExpanded(position: Int) {\n        onView(atPositionInRecyclerView(R.id.layers, position, R.id.arrow))\n            .check(\n                matches(\n                    withImageDrawable(org.odk.collect.icons.R.drawable.ic_baseline_collapse_24)\n                )\n            )\n\n        onView(atPositionInRecyclerView(R.id.layers, position, R.id.path))\n            .check(\n                matches(\n                    withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)\n                )\n            )\n\n        onView(atPositionInRecyclerView(R.id.layers, position, R.id.delete_layer))\n            .check(\n                matches(\n                    withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)\n                )\n            )\n    }\n\n    private fun launchFragment(): FragmentScenario<OfflineMapLayersPickerBottomSheetDialogFragment> {\n        return fragmentScenarioLauncherRule.launchInContainer(\n            OfflineMapLayersPickerBottomSheetDialogFragment::class.java\n        )\n    }\n\n    private class TestRegistry : ActivityResultRegistry() {\n        val uris = mutableListOf<Uri>()\n\n        override fun <I, O> onLaunch(\n            requestCode: Int,\n            contract: ActivityResultContract<I, O>,\n            input: I,\n            options: ActivityOptionsCompat?\n        ) {\n            assertThat(\n                contract,\n                instanceOf(ActivityResultContracts.GetMultipleContents()::class.java)\n            )\n            assertThat(input, equalTo(\"*/*\"))\n            dispatchResult(requestCode, uris)\n        }\n\n        fun addUris(vararg uris: Uri) {\n            this.uris.addAll(uris)\n        }\n    }\n}\n"
  },
  {
    "path": "maps/src/test/resources/robolectric.properties",
    "content": "application=org.odk.collect.maps.RobolectricApplication\nsdk=33\n"
  },
  {
    "path": "material/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "material/README.md",
    "content": "# material\n\nProvides implementations of [Material Components](https://material.io/components) that are missing/not complete in the [Android implementation](https://material.io/develop/android/components).\n"
  },
  {
    "path": "material/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.composeCompiler)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n\n    namespace = \"org.odk.collect.material\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":androidshared\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":icons\"))\n    implementation(libs.androidxAppcompat)\n    api(libs.androidMaterial)\n    implementation(libs.androidxFragmentKtx)\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxCoreKtx)\n\n    val composeBom = platform(libs.androidxComposeBom)\n    implementation(composeBom)\n    implementation(libs.androidXComposeMaterial)\n    implementation(libs.androidXComposePreview)\n    debugImplementation(libs.androidXComposeTooling)\n\n    debugImplementation(project(\":fragments-test\"))\n\n    testImplementation(project(\":test-shared\"))\n    testImplementation(libs.junit)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.androidxTestEspressoCore)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.robolectric)\n}\n"
  },
  {
    "path": "material/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" />\n"
  },
  {
    "path": "material/src/main/java/org/odk/collect/material/BottomSheetBehavior.kt",
    "content": "package org.odk.collect.material\n\nimport android.view.View\nimport com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback\nimport com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN\n\n/**\n * Wrapper around [com.google.android.material.bottomsheet.BottomSheetBehavior] that allows animations/dragging to be disabled. This is\n * useful for both UI and Robolectric tests.\n */\nclass BottomSheetBehavior<V : View> private constructor(private val view: V) {\n\n    private val actual = com.google.android.material.bottomsheet.BottomSheetBehavior.from(view)\n\n    private val callbacks = mutableListOf<BottomSheetCallback>()\n\n    var peekHeight: Int = actual.peekHeight\n        set(value) {\n            if (DRAGGING_ENABLED) {\n                actual.peekHeight = value\n            }\n\n            field = value\n        }\n        get() {\n            return if (DRAGGING_ENABLED) {\n                actual.peekHeight\n            } else {\n                field\n            }\n        }\n\n    var state: Int = actual.state\n        set(value) {\n            if (DRAGGING_ENABLED) {\n                actual.state = value\n            } else {\n                if (value != STATE_HIDDEN) {\n                    view.visibility = View.VISIBLE\n                } else {\n                    view.visibility = View.GONE\n                }\n\n                if (value != field) {\n                    callbacks.forEach {\n                        it.onStateChanged(view, value)\n                    }\n\n                    field = value\n                }\n            }\n        }\n        get() {\n            return if (DRAGGING_ENABLED) {\n                actual.state\n            } else {\n                field\n            }\n        }\n\n    fun removeBottomSheetCallback(callback: BottomSheetCallback) {\n        callbacks.remove(callback)\n        actual.removeBottomSheetCallback(callback)\n    }\n\n    fun addBottomSheetCallback(callback: BottomSheetCallback) {\n        callbacks.add(callback)\n        actual.addBottomSheetCallback(callback)\n    }\n\n    companion object {\n\n        var DRAGGING_ENABLED = true\n\n        fun <V : View> from(view: V): BottomSheetBehavior<V> {\n            return BottomSheetBehavior(view)\n        }\n    }\n}\n"
  },
  {
    "path": "material/src/main/java/org/odk/collect/material/ErrorsPill.kt",
    "content": "package org.odk.collect.material\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.View\nimport com.google.android.material.color.MaterialColors\nimport org.odk.collect.androidshared.system.ContextExt\n\nclass ErrorsPill(context: Context, attrs: AttributeSet?) : MaterialPill(context, attrs) {\n    var errors: Boolean = false\n        set(value) {\n            setup(value)\n            field = value\n        }\n\n    private fun setup(errors: Boolean) {\n        if (errors) {\n            visibility = View.VISIBLE\n            setIcon(org.odk.collect.icons.R.drawable.ic_baseline_rule_24)\n            setText(org.odk.collect.strings.R.string.draft_errors)\n            setPillBackgroundColor(MaterialColors.getColor(this, com.google.android.material.R.attr.colorErrorContainer))\n            setTextColor(ContextExt.getThemeAttributeValue(context, com.google.android.material.R.attr.colorOnErrorContainer))\n            setIconTint(ContextExt.getThemeAttributeValue(context, com.google.android.material.R.attr.colorOnErrorContainer))\n        } else {\n            visibility = View.VISIBLE\n            setIcon(org.odk.collect.icons.R.drawable.ic_baseline_check_24)\n            setText(org.odk.collect.strings.R.string.draft_no_errors)\n            setPillBackgroundColor(MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimaryContainer))\n            setTextColor(ContextExt.getThemeAttributeValue(context, com.google.android.material.R.attr.colorOnPrimaryContainer))\n            setIconTint(ContextExt.getThemeAttributeValue(context, com.google.android.material.R.attr.colorOnPrimaryContainer))\n        }\n    }\n}\n"
  },
  {
    "path": "material/src/main/java/org/odk/collect/material/MaterialFullScreenDialogFragment.kt",
    "content": "package org.odk.collect.material\n\nimport android.os.Bundle\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.WindowManager\nimport androidx.activity.ComponentDialog\nimport androidx.activity.OnBackPressedCallback\nimport androidx.appcompat.widget.Toolbar\nimport androidx.fragment.app.DialogFragment\nimport org.odk.collect.androidshared.ui.EdgeToEdge.handleEdgeToEdge\n\n/**\n * Provides an implementation of Material's \"Full Screen Dialog\"\n * (https://material.io/components/dialogs/#full-screen-dialog) as no implementation currently\n * exists in the Material Components framework\n */\nabstract class MaterialFullScreenDialogFragment : DialogFragment() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setStyle(STYLE_NORMAL, R.style.Theme_MaterialComponents_Dialog_FullScreen)\n    }\n\n    override fun onStart() {\n        super.onStart()\n\n        val dialog = dialog as ComponentDialog?\n        dialog?.window?.apply {\n            val width = ViewGroup.LayoutParams.MATCH_PARENT\n            val height = ViewGroup.LayoutParams.MATCH_PARENT\n            setLayout(width, height)\n\n            if (shouldShowSoftKeyboard()) {\n                // Make sure soft keyboard shows for focused field - annoyingly needed\n                setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)\n            }\n\n            setCancelable(false)\n            dialog.onBackPressedDispatcher.addCallback(this@MaterialFullScreenDialogFragment, object : OnBackPressedCallback(true) {\n                override fun handleOnBackPressed() {\n                    onBackPressed()\n                }\n            })\n\n            handleEdgeToEdge(requireContext())\n        }\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        getToolbar()?.apply {\n            setNavigationOnClickListener { _: View? ->\n                onCloseClicked()\n            }\n        }\n    }\n\n    protected abstract fun onCloseClicked()\n\n    protected abstract fun onBackPressed()\n\n    protected abstract fun getToolbar(): Toolbar?\n\n    protected open fun shouldShowSoftKeyboard(): Boolean {\n        return false\n    }\n}\n"
  },
  {
    "path": "material/src/main/java/org/odk/collect/material/MaterialPill.kt",
    "content": "package org.odk.collect.material\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport androidx.annotation.ColorInt\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.StringRes\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.ui.graphics.Color\nimport androidx.core.content.withStyledAttributes\nimport org.odk.collect.androidshared.system.ContextExt.getThemeAttributeValue\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\nimport org.odk.collect.material.databinding.PillBinding\n\n/**\n * [android.view.View] wrapper for [Pill]\n */\n@Deprecated(\"Use org.odk.collect.material.Pill instead\")\nopen class MaterialPill(context: Context, attrs: AttributeSet?) :\n    FrameLayout(context, attrs) {\n\n    private var text: String = \"\"\n\n    @DrawableRes\n    private var icon: Int? = null\n\n    @ColorInt\n    private var iconTint: Int? = null\n\n    @ColorInt\n    private var backgroundColor: Int? = null\n\n    @ColorInt\n    private var textColor: Int? = null\n\n    val binding = PillBinding.inflate(LayoutInflater.from(context), this, true)\n\n    init {\n        context.withStyledAttributes(attrs, R.styleable.MaterialPill) {\n            text = getString(R.styleable.MaterialPill_text) ?: \"\"\n\n            val iconId = getResourceId(R.styleable.MaterialPill_icon, -1)\n            if (iconId != -1) {\n                setIcon(iconId)\n            }\n\n            val backgroundColor = getColor(\n                R.styleable.MaterialPill_pillBackgroundColor,\n                getDefaultBackgroundColor(context)\n            )\n\n            setPillBackgroundColor(backgroundColor)\n        }\n\n        render()\n    }\n\n    fun setText(@StringRes id: Int) {\n        text = context.getString(id)\n        render()\n    }\n\n    fun setIcon(@DrawableRes id: Int) {\n        icon = id\n        render()\n    }\n\n    fun setIconTint(@ColorInt color: Int) {\n        iconTint = color\n        render()\n    }\n\n    fun setPillBackgroundColor(@ColorInt color: Int) {\n        backgroundColor = color\n        render()\n    }\n\n    fun setTextColor(@ColorInt color: Int) {\n        textColor = color\n        render()\n    }\n\n    fun getIcon(): Int? {\n        return icon\n    }\n\n    fun getText(): String {\n        return text\n    }\n\n    private fun getDefaultBackgroundColor(context: Context) = getThemeAttributeValue(\n        context,\n        com.google.android.material.R.attr.colorPrimaryContainer\n    )\n\n    private fun render() {\n        binding.composeView.setContextThemedContent {\n            val backgroundColor =\n                backgroundColor?.let { Color(it) } ?: MaterialTheme.colorScheme.primaryContainer\n            val iconTint =\n                iconTint?.let { Color(it) } ?: MaterialTheme.colorScheme.onPrimaryContainer\n            val textColor =\n                textColor?.let { Color(it) } ?: MaterialTheme.colorScheme.onPrimaryContainer\n\n            icon?.let {\n                Pill(\n                    text = text,\n                    icon = it,\n                    backgroundColor = backgroundColor,\n                    textColor = textColor,\n                    iconColor = iconTint\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "material/src/main/java/org/odk/collect/material/MaterialProgressDialogFragment.java",
    "content": "package org.odk.collect.material;\n\nimport static android.content.DialogInterface.BUTTON_NEGATIVE;\n\nimport android.app.Dialog;\nimport android.content.DialogInterface;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.LiveData;\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder;\n\nimport org.odk.collect.androidshared.ui.DialogFragmentUtils;\n\nimport java.io.Serializable;\nimport java.util.function.Supplier;\n\n/**\n * Provides a reusable progress dialog implemented with {@link MaterialAlertDialogBuilder}. Progress\n * dialogs don't appear in the Material guidelines/specs due to the design language's instistance\n * that progress shouldn't block the user - this is pretty unrealistic for the app in it's current\n * state so having a reliable \"Material\" version of the Android progress dialog is useful.\n */\npublic class MaterialProgressDialogFragment extends DialogFragment {\n\n    private static final String TITLE = \"title\";\n    private static final String MESSAGE = \"message\";\n    private static final String CANCELABLE = \"true\";\n\n    private View dialogView;\n\n    /**\n     * Override to have something cancelled when the ProgressDialog's cancel button is pressed\n     */\n    protected OnCancelCallback getOnCancelCallback() {\n        return null;\n    }\n\n    /**\n     * Override to show cancel button with returned text\n     */\n    protected String getCancelButtonText() {\n        return null;\n    }\n\n    public String getTitle() {\n        return getArguments().getString(TITLE);\n    }\n\n    public String getMessage() {\n        return getArguments().getString(MESSAGE);\n    }\n\n    public void setTitle(String title) {\n        setArgument(TITLE, title);\n\n        AlertDialog dialog = (AlertDialog) getDialog();\n        if (dialog != null) {\n            setupView(dialog);\n        }\n    }\n\n    public void setMessage(String message) {\n        setArgument(MESSAGE, message);\n\n        AlertDialog dialog = (AlertDialog) getDialog();\n        if (dialog != null) {\n            setupView(dialog);\n        }\n    }\n\n    @Override\n    public void setCancelable(boolean cancelable) {\n        setArgument(CANCELABLE, cancelable);\n        super.setCancelable(cancelable);\n    }\n\n    @Override\n    public Dialog onCreateDialog(Bundle savedInstanceState) {\n        super.onCreateDialog(savedInstanceState);\n\n        dialogView = requireActivity().getLayoutInflater().inflate(R.layout.progress_dialog, null, false);\n        AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())\n                .setView(dialogView)\n                .create();\n\n        setupView(dialog);\n\n        return dialog;\n    }\n\n    @Override\n    public void onCancel(@NonNull DialogInterface dialog) {\n        OnCancelCallback onCancelCallback = getOnCancelCallback();\n        if (onCancelCallback != null) {\n            onCancelCallback.cancel();\n        }\n    }\n\n    private void setupView(@NonNull AlertDialog dialog) {\n        if (getArguments() != null && getArguments().getString(TITLE) != null) {\n            dialog.setTitle(getArguments().getString(TITLE));\n        }\n\n        if (getArguments() != null && getArguments().getString(MESSAGE) != null) {\n            ((TextView) dialogView.findViewById(R.id.message)).setText(getArguments().getString(MESSAGE));\n        }\n\n        if (getArguments() != null) {\n            setCancelable(getArguments().getBoolean(CANCELABLE));\n        }\n\n        if (getCancelButtonText() != null) {\n            dialog.setButton(BUTTON_NEGATIVE, getCancelButtonText(), (dialog1, which) -> {\n                dismiss();\n                getOnCancelCallback().cancel();\n            });\n        }\n    }\n\n    private void setArgument(String key, Serializable value) {\n        if (getArguments() == null) {\n            setArguments(new Bundle());\n        }\n\n        getArguments().putSerializable(key, value);\n    }\n\n    public interface OnCancelCallback {\n        boolean cancel();\n    }\n\n    public View getDialogView() {\n        return dialogView;\n    }\n\n    public static void showOn(LifecycleOwner lifecycleOwner, LiveData<Boolean> liveData, FragmentManager fragmentManager, Supplier<MaterialProgressDialogFragment> supplier) {\n        liveData.observe(lifecycleOwner, isLoading -> {\n            if (isLoading != null) {\n                if (isLoading) {\n                    MaterialProgressDialogFragment dialog = supplier.get();\n                    DialogFragmentUtils.showIfNotShowing(dialog, MaterialProgressDialogFragment.class.getName(), fragmentManager);\n                } else {\n                    DialogFragmentUtils.dismissDialog(MaterialProgressDialogFragment.class.getName(), fragmentManager);\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "material/src/main/java/org/odk/collect/material/Pill.kt",
    "content": "package org.odk.collect.material\n\nimport androidx.annotation.DrawableRes\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.tooling.preview.Preview\n\n/**\n * Implementation of \"pills\" present on the Material 3 website and its examples, but not\n * included in the spec or in Android's MaterialComponents. The pill will use the\n * `?shapeAppearanceCornerSmall` shape appearance for the current theme.\n */\n@Composable\nfun Pill(\n    modifier: Modifier = Modifier,\n    text: String,\n    @DrawableRes icon: Int,\n    backgroundColor: Color = MaterialTheme.colorScheme.primaryContainer,\n    textColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,\n    iconColor: Color = MaterialTheme.colorScheme.onPrimaryContainer\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .clearAndSetSemantics {\n                contentDescription = text\n            }\n            .clip(MaterialTheme.shapes.small)\n            .background(backgroundColor)\n            .padding(\n                horizontal = dimensionResource(org.odk.collect.androidshared.R.dimen.margin_small),\n                vertical = dimensionResource(\n                    org.odk.collect.androidshared.R.dimen.margin_extra_extra_small\n                )\n            )\n    ) {\n        Icon(\n            modifier = Modifier.size(dimensionResource(org.odk.collect.androidshared.R.dimen.margin_standard)),\n            painter = painterResource(icon),\n            contentDescription = null,\n            tint = iconColor\n        )\n\n        Text(\n            modifier = Modifier.padding(start = dimensionResource(org.odk.collect.androidshared.R.dimen.margin_extra_small)),\n            text = text,\n            style = MaterialTheme.typography.labelLarge,\n            color = textColor\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun Preview() {\n    MaterialTheme {\n        Pill(text = \"Text\", icon = org.odk.collect.icons.R.drawable.ic_baseline_wifi_off_24)\n    }\n}\n"
  },
  {
    "path": "material/src/main/res/layout/pill.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\">\n\n    <androidx.compose.ui.platform.ComposeView\n        android:id=\"@+id/compose_view\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"/>\n</FrameLayout>\n"
  },
  {
    "path": "material/src/main/res/layout/progress_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingLeft=\"@dimen/margin_large\"\n    android:paddingTop=\"@dimen/margin_standard\"\n    android:paddingRight=\"@dimen/margin_large\"\n    android:paddingBottom=\"@dimen/margin_large\">\n\n    <ProgressBar\n        android:layout_centerVertical=\"true\"\n        android:layout_alignParentStart=\"true\"\n        android:id=\"@+id/progress_bar\"\n        android:layout_width=\"48dp\"\n        android:layout_height=\"48dp\"\n        android:indeterminate=\"true\" />\n\n    <TextView\n        android:layout_marginStart=\"@dimen/margin_standard\"\n        android:textAppearance=\"?textAppearanceBody1\"\n        android:id=\"@+id/message\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_toEndOf=\"@id/progress_bar\"\n        android:layout_centerVertical=\"true\"\n        tools:text=\"Loading...\" />\n\n</RelativeLayout>\n"
  },
  {
    "path": "material/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!--  Should be removed when these attributes are added to Material Components  -->\n    <declare-styleable name=\"Material3PolyFill\">\n        <!-- https://github.com/material-components/material-components-android/issues/3506 -->\n        <attr name=\"materialButtonOutlinedIconStyle\" format=\"reference\" />\n        <attr name=\"materialButtonIconStyle\" format=\"reference\" />\n        <attr name=\"borderlessButtonIconStyle\" format=\"reference\" />\n    </declare-styleable>\n\n    <!--  These attributes are extensions to Material 3  -->\n    <declare-styleable name=\"Material3Custom\">\n        <attr name=\"textAppearanceLabelExtraLarge\" format=\"reference\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"MaterialPill\">\n        <attr name=\"icon\" />\n        <attr name=\"text\" format=\"string\" />\n        <attr name=\"pillBackgroundColor\" format=\"reference\"/>\n    </declare-styleable>\n</resources>\n"
  },
  {
    "path": "material/src/main/res/values/material_3_button_icon_end_style.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!--\n        Reverse padding for icon at the end rather than the start. Work around for\n        https://github.com/material-components/material-components-android/issues/3502\n    -->\n    <style name=\"ThemeOverlay.Material3.Button.Icon.End\" parent=\"\">\n        <item name=\"android:paddingStart\" tools:ignore=\"PrivateResource\">\n            @dimen/m3_btn_icon_btn_padding_right\n        </item>\n        <item name=\"android:paddingEnd\" tools:ignore=\"PrivateResource\">\n            @dimen/m3_btn_icon_btn_padding_left\n        </item>\n        <item name=\"iconGravity\">end</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "material/src/main/res/values/material_full_screen_dialog_theme.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Theme.MaterialComponents.Dialog.FullScreen\" parent=\"\">\n        <item name=\"android:windowIsFloating\">false</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "material/src/test/java/org/odk/collect/material/ErrorsPillTest.kt",
    "content": "package org.odk.collect.material\n\nimport android.app.Application\nimport android.view.View\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.robolectric.Shadows\n\n@RunWith(AndroidJUnit4::class)\nclass ErrorsPillTest {\n    private val context = ApplicationProvider.getApplicationContext<Application>().also {\n        it.setTheme(com.google.android.material.R.style.Theme_Material3_Light)\n    }\n    private val errorsPill: ErrorsPill = ErrorsPill(context, null)\n\n    @Test\n    fun `setup with State ERRORS should set appropriate properties`() {\n        errorsPill.errors = true\n        assertErrorsPill()\n    }\n\n    @Test\n    fun `setup with State NO_ERRORS should set appropriate properties`() {\n        errorsPill.errors = false\n        assertNoErrorsPill()\n    }\n\n    @Test\n    fun `pill can be recycled`() {\n        errorsPill.errors = true\n        assertErrorsPill()\n\n        errorsPill.errors = false\n        assertNoErrorsPill()\n    }\n\n    private fun assertErrorsPill() {\n        assertThat(errorsPill.visibility, equalTo(View.VISIBLE))\n        assertThat(errorsPill.getIcon(), equalTo(org.odk.collect.icons.R.drawable.ic_baseline_rule_24))\n        assertThat(errorsPill.getText(), equalTo(context.getString(org.odk.collect.strings.R.string.draft_errors)))\n    }\n\n    private fun assertNoErrorsPill() {\n        assertThat(errorsPill.visibility, equalTo(View.VISIBLE))\n        assertThat(errorsPill.getIcon(), equalTo(org.odk.collect.icons.R.drawable.ic_baseline_check_24))\n        assertThat(errorsPill.getText(), equalTo(context.getString(org.odk.collect.strings.R.string.draft_no_errors)))\n    }\n}\n"
  },
  {
    "path": "material/src/test/java/org/odk/collect/material/MaterialProgressDialogFragmentTest.java",
    "content": "package org.odk.collect.material;\n\nimport static android.os.Looper.getMainLooper;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.robolectric.Shadows.shadowOf;\nimport static org.robolectric.shadows.ShadowView.innerText;\n\nimport android.content.DialogInterface;\nimport android.view.View;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.testing.FragmentScenario;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.hamcrest.Matchers;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.fragmentstest.FragmentScenarioLauncherRule;\n\n@RunWith(AndroidJUnit4.class)\npublic class MaterialProgressDialogFragmentTest {\n\n    @Rule\n    public FragmentScenarioLauncherRule launcherRule = new FragmentScenarioLauncherRule();\n\n    @Test\n    public void setTitle_updatesTitle() {\n        FragmentScenario<MaterialProgressDialogFragment> scenario = launcherRule.launch(MaterialProgressDialogFragment.class);\n        scenario.onFragment(fragment -> {\n            fragment.setTitle(\"blah\");\n            CharSequence message = shadowOf(fragment.getDialog()).getTitle();\n            assertThat(message, equalTo(\"blah\"));\n        });\n    }\n\n    @Test\n    public void recreate_persistsTitle() {\n        FragmentScenario<MaterialProgressDialogFragment> fragmentScenario = launcherRule.launch(MaterialProgressDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            fragment.setTitle(\"blah\");\n        });\n\n        fragmentScenario.recreate();\n        fragmentScenario.onFragment(fragment -> {\n            CharSequence title = shadowOf(fragment.getDialog()).getTitle();\n            assertThat(title, equalTo(\"blah\"));\n        });\n    }\n\n    @Test\n    public void whenMessageNotSet_showsProgressBar() {\n        FragmentScenario<MaterialProgressDialogFragment> fragmentScenario = launcherRule.launch(MaterialProgressDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            View dialogView = fragment.getDialogView();\n            assertThat(dialogView.findViewById(R.id.progress_bar).getVisibility(), Matchers.is(View.VISIBLE));\n        });\n    }\n\n    @Test\n    public void setMessage_updatesMessage() {\n        FragmentScenario<MaterialProgressDialogFragment> fragmentScenario = launcherRule.launch(MaterialProgressDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            View dialogView = fragment.getDialogView();\n\n            fragment.setMessage(\"blah\");\n            assertThat(innerText(dialogView), equalTo(\"blah\"));\n        });\n    }\n\n    @Test\n    public void recreate_persistsMessage() {\n        FragmentScenario<MaterialProgressDialogFragment> fragmentScenario = launcherRule.launch(MaterialProgressDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            fragment.setMessage(\"blah\");\n        });\n\n        fragmentScenario.recreate();\n        fragmentScenario.onFragment(fragment -> {\n            CharSequence message = innerText(fragment.getDialogView());\n            assertThat(message, equalTo(\"blah\"));\n        });\n    }\n\n    @Test\n    public void setCancellableFalse_makesTheDialogNotCancellable() {\n        FragmentScenario<MaterialProgressDialogFragment> fragmentScenario = launcherRule.launch(MaterialProgressDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            fragment.setCancelable(false);\n            assertThat(fragment.isCancelable(), equalTo(false));\n        });\n    }\n\n    @Test\n    public void recreate_persistsCancellable() {\n        FragmentScenario<MaterialProgressDialogFragment> fragmentScenario = launcherRule.launch(MaterialProgressDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            fragment.setCancelable(false);\n        });\n\n        fragmentScenario.recreate();\n        fragmentScenario.onFragment(fragment -> {\n            assertThat(fragment.isCancelable(), equalTo(false));\n        });\n    }\n\n    @Test\n    public void cancelling_callsCancelOnCancellable() {\n        FragmentScenario<TestProgressDialogFragment> fragmentScenario = launcherRule.launch(TestProgressDialogFragment.class);\n        fragmentScenario.onFragment(fragment -> {\n            MaterialProgressDialogFragment.OnCancelCallback onCancelCallback = mock(MaterialProgressDialogFragment.OnCancelCallback.class);\n            fragment.setCancellableCallback(onCancelCallback);\n\n            fragment.onCancel(fragment.getDialog());\n            verify(onCancelCallback).cancel();\n        });\n    }\n\n    @Test\n    public void whenThereIsCancelButtonText_clickingCancel_dismissesAndCallsCancelOnCancellable() {\n        FragmentScenario<TestProgressDialogFragment> fragmentScenario = launcherRule.launch(TestProgressDialogFragment.class);\n\n        fragmentScenario.onFragment(fragment -> {\n            MaterialProgressDialogFragment.OnCancelCallback onCancelCallback = mock(MaterialProgressDialogFragment.OnCancelCallback.class);\n            fragment.setCancellableCallback(onCancelCallback);\n\n            AlertDialog dialog = (AlertDialog) fragment.getDialog();\n            dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick();\n            shadowOf(getMainLooper()).idle();\n\n            verify(onCancelCallback).cancel();\n            assertThat(dialog.isShowing(), equalTo(false));\n        });\n    }\n\n    public static class TestProgressDialogFragment extends MaterialProgressDialogFragment {\n\n        private OnCancelCallback onCancelCallback;\n\n        @Override\n        protected String getCancelButtonText() {\n            return \"Blah\";\n        }\n\n        @Override\n        protected OnCancelCallback getOnCancelCallback() {\n            return onCancelCallback;\n        }\n\n        public void setCancellableCallback(OnCancelCallback onCancelCallback) {\n            this.onCancelCallback = onCancelCallback;\n        }\n    }\n}\n"
  },
  {
    "path": "metadata/.gitignore",
    "content": "/build"
  },
  {
    "path": "metadata/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.metadata\"\n\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":permissions\"))\n    implementation(project(\":settings\"))\n    implementation(project(\":shared\"))\n\n    implementation(libs.javarosa) {\n        exclude(group = \"joda-time\")\n        exclude(group = \"org.hamcrest\", module = \"hamcrest-all\")\n    }\n    implementation(libs.timber)\n\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.junit)\n    testImplementation(libs.mockitoKotlin)\n}\n"
  },
  {
    "path": "metadata/src/main/java/org/odk/collect/metadata/InstallIDProvider.kt",
    "content": "package org.odk.collect.metadata\n\nimport org.odk.collect.shared.settings.Settings\nimport org.odk.collect.shared.strings.RandomString\n\ninterface InstallIDProvider {\n    val installID: String\n}\n\nclass SettingsInstallIDProvider(\n    private val metaPreferences: Settings,\n    private val preferencesKey: String\n) : InstallIDProvider {\n\n    override val installID: String\n        get() {\n            return if (metaPreferences.contains(preferencesKey)) {\n                metaPreferences.getString(preferencesKey) ?: generateAndStoreInstallID()\n            } else {\n                generateAndStoreInstallID()\n            }\n        }\n\n    private fun generateAndStoreInstallID(): String {\n        val installID = \"collect:\" + RandomString.randomString(16)\n        metaPreferences.save(preferencesKey, installID)\n        return installID\n    }\n}\n"
  },
  {
    "path": "metadata/src/main/java/org/odk/collect/metadata/PropertyManager.kt",
    "content": "package org.odk.collect.metadata\n\nimport org.javarosa.core.services.IPropertyManager\nimport org.javarosa.core.services.properties.IPropertyRules\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\n\n/**\n * Returns device properties and metadata to JavaRosa\n *\n * @author Yaw Anokwa (yanokwa@gmail.com)\n */\nclass PropertyManager(\n    private val installIDProvider: InstallIDProvider,\n    private val settingsProvider: SettingsProvider\n) : IPropertyManager {\n    private val properties = mutableMapOf<String, String>()\n\n    fun reload(): PropertyManager {\n        properties.clear()\n\n        val generalSettings = settingsProvider.getUnprotectedSettings()\n\n        var username = generalSettings.getString(ProjectKeys.KEY_METADATA_USERNAME)\n        // Use the server username by default if the metadata username is not defined\n        if (username.isNullOrBlank()) {\n            username = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_USERNAME)\n        }\n\n        putProperty(PROPMGR_USERNAME, SCHEME_USERNAME, username)\n        putProperty(PROPMGR_PHONE_NUMBER, SCHEME_TEL, generalSettings.getString(ProjectKeys.KEY_METADATA_PHONENUMBER))\n        putProperty(PROPMGR_EMAIL, SCHEME_MAILTO, generalSettings.getString(ProjectKeys.KEY_METADATA_EMAIL))\n        putProperty(PROPMGR_DEVICE_ID, \"\", installIDProvider.installID)\n\n        return this\n    }\n\n    private fun putProperty(propName: String, scheme: String, value: String?) {\n        if (value != null && value.isNotBlank()) {\n            properties[propName] = value\n            properties[\"uri:$propName\"] = \"$scheme:$value\"\n        }\n    }\n\n    override fun getSingularProperty(propertyName: String): String {\n        return properties[propertyName.lowercase()] ?: \"\"\n    }\n\n    override fun getProperty(propertyName: String): List<String> = emptyList()\n    override fun setProperty(propertyName: String, propertyValue: String) = Unit\n    override fun setProperty(propertyName: String, propertyValue: List<String>) = Unit\n    override fun addRules(rules: IPropertyRules) = Unit\n    override fun getRules(): List<IPropertyRules> = emptyList()\n\n    companion object {\n        const val PROPMGR_DEVICE_ID = \"deviceid\"\n        const val PROPMGR_PHONE_NUMBER = \"phonenumber\"\n        const val PROPMGR_USERNAME = \"username\"\n        const val PROPMGR_EMAIL = \"email\"\n\n        private const val SCHEME_USERNAME = \"username\"\n        private const val SCHEME_TEL = \"tel\"\n        private const val SCHEME_MAILTO = \"mailto\"\n    }\n}\n"
  },
  {
    "path": "metadata/src/test/java/org/odk/collect/metadata/PropertyManagerTest.kt",
    "content": "package org.odk.collect.metadata\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Before\nimport org.junit.Test\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.ProjectKeys\n\nclass PropertyManagerTest {\n    private val installIDProvider = TestInstallIDProvider()\n    private val settingsProvider = InMemSettingsProvider()\n\n    private lateinit var propertyManager: PropertyManager\n\n    @Before\n    fun setup() {\n        propertyManager = PropertyManager(installIDProvider, settingsProvider)\n    }\n\n    @Test\n    fun `getSingularProperty should return empty string for undefined properties`() {\n        assertThat(propertyManager.getSingularProperty(\"blah\"), equalTo(\"\"))\n    }\n\n    @Test\n    fun `reload should populate properties`() {\n        settingsProvider.getUnprotectedSettings().apply {\n            save(ProjectKeys.KEY_METADATA_USERNAME, \"John\")\n            save(ProjectKeys.KEY_METADATA_PHONENUMBER, \"789\")\n            save(ProjectKeys.KEY_METADATA_EMAIL, \"john@gmail.com\")\n        }\n\n        propertyManager.reload()\n\n        assertThat(propertyManager.getSingularProperty(PropertyManager.PROPMGR_USERNAME), equalTo(\"John\"))\n        assertThat(propertyManager.getSingularProperty(PropertyManager.PROPMGR_PHONE_NUMBER), equalTo(\"789\"))\n        assertThat(propertyManager.getSingularProperty(PropertyManager.PROPMGR_EMAIL), equalTo(\"john@gmail.com\"))\n        assertThat(propertyManager.getSingularProperty(PropertyManager.PROPMGR_DEVICE_ID), equalTo(\"123\"))\n    }\n\n    @Test\n    fun `reload should clear existing properties`() {\n        settingsProvider.getUnprotectedSettings().apply {\n            save(ProjectKeys.KEY_METADATA_USERNAME, \"John\")\n            save(ProjectKeys.KEY_METADATA_PHONENUMBER, \"789\")\n            save(ProjectKeys.KEY_METADATA_EMAIL, \"john@gmail.com\")\n        }\n\n        propertyManager.reload()\n\n        settingsProvider.getUnprotectedSettings().apply {\n            save(ProjectKeys.KEY_METADATA_USERNAME, \"\")\n            save(ProjectKeys.KEY_METADATA_PHONENUMBER, \"\")\n            save(ProjectKeys.KEY_METADATA_EMAIL, \"\")\n        }\n\n        propertyManager.reload()\n\n        assertThat(propertyManager.getSingularProperty(PropertyManager.PROPMGR_USERNAME), equalTo(\"\"))\n        assertThat(propertyManager.getSingularProperty(PropertyManager.PROPMGR_PHONE_NUMBER), equalTo(\"\"))\n        assertThat(propertyManager.getSingularProperty(PropertyManager.PROPMGR_EMAIL), equalTo(\"\"))\n        assertThat(propertyManager.getSingularProperty(PropertyManager.PROPMGR_DEVICE_ID), equalTo(\"123\"))\n    }\n\n    @Test\n    fun `reload should use server username if metadata username is not defined`() {\n        settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_USERNAME, \"Mark\")\n\n        propertyManager.reload()\n\n        assertThat(propertyManager.getSingularProperty(PropertyManager.PROPMGR_USERNAME), equalTo(\"Mark\"))\n    }\n\n    @Test\n    fun `reload should use metadata username if both metadata and server usernames are defined`() {\n        settingsProvider.getUnprotectedSettings().apply {\n            save(ProjectKeys.KEY_METADATA_USERNAME, \"John\")\n            save(ProjectKeys.KEY_USERNAME, \"Mark\")\n        }\n\n        propertyManager.reload()\n\n        assertThat(propertyManager.getSingularProperty(PropertyManager.PROPMGR_USERNAME), equalTo(\"John\"))\n    }\n\n    private class TestInstallIDProvider : InstallIDProvider {\n        override val installID: String\n            get() = \"123\"\n    }\n}\n"
  },
  {
    "path": "metadata/src/test/java/org/odk/collect/metadata/SettingsInstallIDProviderTest.kt",
    "content": "package org.odk.collect.metadata\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.CoreMatchers.not\nimport org.hamcrest.CoreMatchers.notNullValue\nimport org.hamcrest.CoreMatchers.startsWith\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.odk.collect.shared.settings.InMemSettings\nimport org.odk.collect.shared.settings.Settings\n\nclass SettingsInstallIDProviderTest {\n    private val metaPreferences: Settings = InMemSettings()\n    private val provider = SettingsInstallIDProvider(metaPreferences, \"blah\")\n\n    @Test\n    fun returnsSameValueEveryTime() {\n        val firstValue = provider.installID\n        val secondValue = provider.installID\n\n        assertThat(firstValue, equalTo(secondValue))\n    }\n\n    @Test\n    fun returnsValueWithPrefix() {\n        assertThat(provider.installID, startsWith(\"collect:\"))\n    }\n\n    @Test\n    fun returns24CharacterValue() {\n        assertThat(provider.installID.length, equalTo(24))\n    }\n\n    @Test\n    fun clearingSharedPreferences_resetsInstallID() {\n        val firstValue = provider.installID\n        metaPreferences.clear()\n        val secondValue = provider.installID\n\n        assertThat(secondValue, notNullValue())\n        assertThat(firstValue, not(equalTo(secondValue)))\n    }\n}\n"
  },
  {
    "path": "mobile-device-management/.gitignore",
    "content": "/build"
  },
  {
    "path": "mobile-device-management/build.gradle.kts",
    "content": "apply(from = \"../config/quality.gradle\")\n\nplugins {\n    alias(libs.plugins.androidLibrary)\n}\n\nandroid {\n    namespace = \"org.odk.collect.mobiledevicemanagement\"\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":async\"))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":settings\"))\n    implementation(project(\":shared\"))\n    implementation(project(\":projects\"))\n    implementation(project(\":analytics\"))\n\n    testImplementation(project(\":test-shared\"))\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.androidxTestExtJunit)\n}\n"
  },
  {
    "path": "mobile-device-management/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n    <application>\n        <meta-data\n            android:name=\"android.content.APP_RESTRICTIONS\"\n            android:resource=\"@xml/managed_configuration\"\n            tools:node=\"merge\" />\n    </application>\n</manifest>"
  },
  {
    "path": "mobile-device-management/src/main/java/org/odk/collect/mobiledevicemanagement/MDMConfigHandler.kt",
    "content": "package org.odk.collect.mobiledevicemanagement\n\nimport android.os.Bundle\nimport org.odk.collect.analytics.Analytics\nimport org.odk.collect.projects.ProjectCreator\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.projects.SettingsConnectionMatcher\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.MetaKeys.KEY_INSTALL_ID\n\nconst val DEVICE_ID_KEY = \"device_id\"\nconst val SETTINGS_JSON_KEY = \"settings_json\"\n\ninterface MDMConfigHandler {\n    fun applyConfig(managedConfig: Bundle)\n}\n\nclass MDMConfigHandlerImpl(\n    private val settingsProvider: SettingsProvider,\n    private val projectsRepository: ProjectsRepository,\n    private val projectCreator: ProjectCreator,\n    private val settingsImporter: ODKAppSettingsImporter,\n    private val settingsConnectionMatcher: SettingsConnectionMatcher\n) : MDMConfigHandler {\n    override fun applyConfig(managedConfig: Bundle) {\n        Analytics.setUserProperty(\"SawMDMConfig\", \"true\")\n        applyDeviceId(managedConfig)\n        applySettingsJson(managedConfig)\n    }\n\n    private fun applyDeviceId(managedConfig: Bundle) {\n        if (managedConfig.containsKey(DEVICE_ID_KEY) && !managedConfig.getString(DEVICE_ID_KEY).isNullOrBlank()) {\n            settingsProvider.getMetaSettings().save(KEY_INSTALL_ID, managedConfig.getString(\n                DEVICE_ID_KEY\n            ))\n        }\n    }\n\n    private fun applySettingsJson(managedConfig: Bundle) {\n        if (managedConfig.containsKey(SETTINGS_JSON_KEY) && !managedConfig.getString(SETTINGS_JSON_KEY).isNullOrBlank()) {\n            val settingsJson = managedConfig.getString(SETTINGS_JSON_KEY)\n\n            if (!settingsJson.isNullOrBlank()) {\n                when (val matchingProjectUUID = settingsJson.let { settingsConnectionMatcher.getProjectWithMatchingConnection(it) }) {\n                    null -> projectCreator.createNewProject(settingsJson, projectsRepository.getAll().isEmpty())\n                    else -> settingsImporter.fromJSON(settingsJson, projectsRepository.get(matchingProjectUUID)!!)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "mobile-device-management/src/main/java/org/odk/collect/mobiledevicemanagement/MDMConfigObserver.kt",
    "content": "package org.odk.collect.mobiledevicemanagement\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.content.RestrictionsManager\nimport androidx.lifecycle.DefaultLifecycleObserver\nimport androidx.lifecycle.LifecycleOwner\nimport org.odk.collect.androidshared.system.BroadcastReceiverRegister\nimport org.odk.collect.async.Scheduler\n\n/**\n * Manages configuration changes from a mobile device management system.\n *\n * See android.content.APP_RESTRICTIONS in AndroidManifest for supported configuration keys.\n */\nclass MDMConfigObserver(\n    private val scheduler: Scheduler,\n    private val mdmConfigHandler: MDMConfigHandler,\n    private val broadcastReceiverRegister: BroadcastReceiverRegister,\n    private val restrictionsManager: RestrictionsManager\n) : DefaultLifecycleObserver {\n\n    private val restrictionsReceiver = object : BroadcastReceiver() {\n        override fun onReceive(context: Context, intent: Intent) {\n            scheduler.immediate {\n                mdmConfigHandler.applyConfig(restrictionsManager.applicationRestrictions)\n            }\n        }\n    }\n\n    override fun onResume(owner: LifecycleOwner) {\n        super.onResume(owner)\n        scheduler.immediate {\n            mdmConfigHandler.applyConfig(restrictionsManager.applicationRestrictions)\n        }\n\n        val restrictionsFilter = IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED)\n        broadcastReceiverRegister.registerReceiver(restrictionsReceiver, restrictionsFilter)\n    }\n\n    override fun onPause(owner: LifecycleOwner) {\n        super.onPause(owner)\n        broadcastReceiverRegister.unregisterReceiver(restrictionsReceiver)\n    }\n}\n"
  },
  {
    "path": "mobile-device-management/src/main/res/xml/managed_configuration.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<restrictions xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <restriction\n        android:key=\"device_id\"\n        android:title=\"@string/device_id\"\n        android:restrictionType=\"string\"\n        android:defaultValue=\"\" />\n\n    <restriction\n        android:key=\"settings_json\"\n        android:title=\"@string/settings_json\"\n        android:restrictionType=\"string\"\n        android:defaultValue=\"\" />\n</restrictions>"
  },
  {
    "path": "mobile-device-management/src/test/java/org/odk/collect/mobiledevicemanagement/MDMConfigHandlerTest.kt",
    "content": "package org.odk.collect.mobiledevicemanagement\n\nimport android.os.Bundle\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoInteractions\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectCreator\nimport org.odk.collect.projects.SettingsConnectionMatcher\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.ODKAppSettingsImporter\nimport org.odk.collect.settings.keys.MetaKeys.KEY_INSTALL_ID\n\n@RunWith(AndroidJUnit4::class)\nclass MDMConfigHandlerTest {\n    private val settingsProvider = InMemSettingsProvider()\n    private val projectsRepository = InMemProjectsRepository()\n    private val projectCreator = mock<ProjectCreator>()\n    private val settingsImporter = mock<ODKAppSettingsImporter>()\n    private val settingsConnectionMatcher = mock<SettingsConnectionMatcher>()\n    private val mdmConfigHandler = MDMConfigHandlerImpl(\n        settingsProvider,\n        projectsRepository,\n        projectCreator,\n        settingsImporter,\n        settingsConnectionMatcher\n    )\n\n    @Test\n    fun `deviceId is ignored if it is null`() {\n        settingsProvider.getMetaSettings().save(KEY_INSTALL_ID, \"foo\")\n\n        val managedConfig = Bundle().apply {\n            putString(DEVICE_ID_KEY, null)\n        }\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        assertThat(settingsProvider.getMetaSettings().getString(KEY_INSTALL_ID), equalTo(\"foo\"))\n    }\n\n    @Test\n    fun `deviceId is ignored if it is empty`() {\n        settingsProvider.getMetaSettings().save(KEY_INSTALL_ID, \"foo\")\n\n        val managedConfig = Bundle().apply {\n            putString(DEVICE_ID_KEY, \"\")\n        }\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        assertThat(settingsProvider.getMetaSettings().getString(KEY_INSTALL_ID), equalTo(\"foo\"))\n    }\n\n    @Test\n    fun `deviceId is ignored if it is blank`() {\n        settingsProvider.getMetaSettings().save(KEY_INSTALL_ID, \"foo\")\n\n        val managedConfig = Bundle().apply {\n            putString(DEVICE_ID_KEY, \" \")\n        }\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        assertThat(settingsProvider.getMetaSettings().getString(KEY_INSTALL_ID), equalTo(\"foo\"))\n    }\n\n    @Test\n    fun `deviceId is saved if it is has value`() {\n        settingsProvider.getMetaSettings().save(KEY_INSTALL_ID, \"foo\")\n\n        val managedConfig = Bundle().apply {\n            putString(DEVICE_ID_KEY, \"bar\")\n        }\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        assertThat(settingsProvider.getMetaSettings().getString(KEY_INSTALL_ID), equalTo(\"bar\"))\n    }\n\n    @Test\n    fun `settingsJson is ignored if it is null`() {\n        val managedConfig = Bundle().apply {\n            putString(SETTINGS_JSON_KEY, null)\n        }\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        verifyNoInteractions(projectCreator)\n        verifyNoInteractions(settingsImporter)\n    }\n\n    @Test\n    fun `settingsJson is ignored if it is empty`() {\n        val managedConfig = Bundle().apply {\n            putString(SETTINGS_JSON_KEY, \"\")\n        }\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        verifyNoInteractions(projectCreator)\n        verifyNoInteractions(settingsImporter)\n    }\n\n    @Test\n    fun `settingsJson is ignored if it is blank`() {\n        val managedConfig = Bundle().apply {\n            putString(SETTINGS_JSON_KEY, \" \")\n        }\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        verifyNoInteractions(projectCreator)\n        verifyNoInteractions(settingsImporter)\n    }\n\n    @Test\n    fun `new project is created and switched to if there are no saved projects yet`() {\n        val settingsJson = \"{ \\\"general\\\": {}, \\\"admin\\\": {} }\"\n        val managedConfig = Bundle().apply {\n            putString(SETTINGS_JSON_KEY, settingsJson)\n        }\n        whenever(settingsConnectionMatcher.getProjectWithMatchingConnection(settingsJson)).thenReturn(null)\n\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        verify(projectCreator).createNewProject(settingsJson, true)\n    }\n\n    @Test\n    fun `new project is created but not switched to if there are projects saved but none of them contains a URL and username combination that matches an existing project`() {\n        val project = Project.Saved(\"1\", \"project\", \"Q\", \"#000000\")\n        projectsRepository.save(project)\n\n        val settingsJson = \"{ \\\"general\\\": {}, \\\"admin\\\": {} }\"\n        val managedConfig = Bundle().apply {\n            putString(SETTINGS_JSON_KEY, settingsJson)\n        }\n        whenever(settingsConnectionMatcher.getProjectWithMatchingConnection(settingsJson)).thenReturn(null)\n\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        verify(projectCreator).createNewProject(settingsJson, false)\n    }\n\n    @Test\n    fun `existing project is updated when settingsJson contains a URL and username combination that matches an existing project`() {\n        val project = Project.Saved(\"1\", \"project\", \"Q\", \"#000000\")\n        projectsRepository.save(project)\n\n        val settingsJson = \"{ \\\"general\\\": {}, \\\"admin\\\": {} }\"\n        val managedConfig = Bundle().apply {\n            putString(SETTINGS_JSON_KEY, settingsJson)\n        }\n        whenever(settingsConnectionMatcher.getProjectWithMatchingConnection(settingsJson)).thenReturn(\"1\")\n\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        verifyNoInteractions(projectCreator)\n        verify(settingsImporter).fromJSON(settingsJson, project)\n    }\n\n    @Test\n    fun `unsupported settings are ignored`() {\n        val managedConfig = Bundle().apply {\n            putString(\"foo\", \"bar\")\n        }\n        mdmConfigHandler.applyConfig(managedConfig)\n\n        assertThat(settingsProvider.getMetaSettings().contains(\"foo\"), equalTo(false))\n        verifyNoInteractions(projectCreator)\n        verifyNoInteractions(settingsImporter)\n    }\n}\n"
  },
  {
    "path": "mobile-device-management/src/test/java/org/odk/collect/mobiledevicemanagement/MDMConfigObserverTest.kt",
    "content": "package org.odk.collect.mobiledevicemanagement\n\nimport android.app.Application\nimport android.content.Intent\nimport android.content.RestrictionsManager\nimport android.os.Bundle\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.testshared.FakeBroadcastReceiverRegister\nimport org.odk.collect.testshared.FakeScheduler\n\n@RunWith(AndroidJUnit4::class)\nclass MDMConfigObserverTest {\n    private val scheduler = FakeScheduler()\n    private val mdmConfigHandler = FakeMDMConfigHandler()\n    private val broadcastReceiverRegister = FakeBroadcastReceiverRegister()\n    private val managedConfig = Bundle()\n    private val restrictionsManager = mock<RestrictionsManager>()\n        .apply { whenever(applicationRestrictions).thenReturn(managedConfig) }\n    private val context = ApplicationProvider.getApplicationContext<Application>()\n    private val lifecycleOwner = mock<LifecycleOwner>()\n    private val mdmConfigObserver = MDMConfigObserver(\n        scheduler,\n        mdmConfigHandler,\n        broadcastReceiverRegister,\n        restrictionsManager\n    )\n\n    @Test\n    fun `broadcast receiver is registered in #onResume`() {\n        assertThat(\n            broadcastReceiverRegister.registeredReceivers.any { it.first == Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED },\n            equalTo(false)\n        )\n        mdmConfigObserver.onResume(lifecycleOwner)\n\n        assertThat(\n            broadcastReceiverRegister.registeredReceivers.any { it.first == Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED },\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `broadcast receiver is unregistered in #onPause`() {\n        mdmConfigObserver.onResume(lifecycleOwner)\n        mdmConfigObserver.onPause(lifecycleOwner)\n\n        assertThat(\n            broadcastReceiverRegister.registeredReceivers.any { it.first == Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED },\n            equalTo(false)\n        )\n    }\n\n    @Test\n    fun `#onResume triggers ManagedConfigSaver#applyConfig`() {\n        mdmConfigObserver.onResume(lifecycleOwner)\n        scheduler.runBackground()\n\n        assertThat(mdmConfigHandler.applyConfigCounter, equalTo(1))\n    }\n\n    @Test\n    fun `broadcast receiver triggers ManagedConfigSaver#applyConfig`() {\n        mdmConfigObserver.onResume(lifecycleOwner)\n\n        broadcastReceiverRegister.broadcast(context, Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED))\n        scheduler.runBackground()\n\n        assertThat(mdmConfigHandler.applyConfigCounter, equalTo(2))\n    }\n}\n\nprivate class FakeMDMConfigHandler : MDMConfigHandler {\n    var applyConfigCounter = 0\n        private set\n\n    override fun applyConfig(managedConfig: Bundle) {\n        applyConfigCounter++\n    }\n}\n"
  },
  {
    "path": "nbistubs/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "nbistubs/README.md",
    "content": "This Android library includes zero byte stubs for every ABI (architecture) so that Collect looks like it supports them all. This lets us exclude unneeded x86 native libs for dependencies like Mapbox without getting `INSTALL_FAILED_NO_MATCHING_ABIS` errors."
  },
  {
    "path": "nbistubs/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    namespace = \"org.odk.collect.nbistubs\"\n}\n"
  },
  {
    "path": "nbistubs/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" />\n"
  },
  {
    "path": "open-rosa/.gitignore",
    "content": "/build"
  },
  {
    "path": "open-rosa/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.openrosa\"\n\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n            isReturnDefaultValues = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n}\n\ndependencies {\n    implementation(project(\":entities\"))\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":shared\"))\n    implementation(project(\":forms\"))\n\n    implementation(libs.kotlinStdlib)\n    implementation(libs.timber)\n    implementation(libs.okHttp)\n    implementation(libs.okHttpDigest)\n    implementation(libs.okHttpTls)\n    implementation(libs.commonsIo)\n    implementation(libs.slf4jApi)\n    implementation(libs.javarosa) {\n        exclude(group = \"commons-io\")\n        exclude(group = \"joda-time\")\n        exclude(group = \"org.slf4j\")\n        exclude(group = \"org.hamcrest\", module = \"hamcrest-all\")\n    }\n\n    testImplementation(libs.junit)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.okhttp3Mockwebserver)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.androidxTestExtJunit)\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/forms/DocumentFetchResult.java",
    "content": "/*\n * Copyright (C) 2011 University of Washington\n * \n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n * \n * http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.openrosa.forms;\n\nimport org.kxml2.kdom.Document;\n\npublic class DocumentFetchResult {\n    public final String errorMessage;\n    public final int responseCode;\n    public final Document doc;\n    public final boolean isOpenRosaResponse;\n    private String hash;\n\n    public DocumentFetchResult(String msg, int response) {\n        responseCode = response;\n        errorMessage = msg;\n        doc = null;\n        isOpenRosaResponse = false;\n    }\n\n    public DocumentFetchResult(Document doc, boolean isOpenRosaResponse, String hash) {\n        responseCode = 0;\n        errorMessage = null;\n        this.doc = doc;\n        this.isOpenRosaResponse = isOpenRosaResponse;\n        this.hash = hash;\n    }\n\n    public String getHash() {\n        return hash;\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/forms/EntityIntegrity.kt",
    "content": "package org.odk.collect.openrosa.forms\n\ndata class EntityIntegrity(val id: String, val deleted: Boolean)\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/forms/OpenRosaClient.kt",
    "content": "package org.odk.collect.openrosa.forms\n\nimport android.net.Uri\nimport org.odk.collect.entities.server.EntitySource\nimport org.odk.collect.forms.FormListItem\nimport org.odk.collect.forms.FormSource\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.forms.FormSourceException.AuthRequired\nimport org.odk.collect.forms.FormSourceException.FetchError\nimport org.odk.collect.forms.FormSourceException.SecurityError\nimport org.odk.collect.forms.FormSourceException.ServerNotOpenRosaError\nimport org.odk.collect.forms.ManifestFile\nimport org.odk.collect.openrosa.forms.OpenRosaXmlFetcher.WebCredentialsProvider\nimport org.odk.collect.openrosa.http.OpenRosaConstants\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface\nimport org.odk.collect.openrosa.parse.OpenRosaResponseParser\nimport java.io.InputStream\nimport java.net.HttpURLConnection\nimport java.net.UnknownHostException\nimport java.util.concurrent.Callable\nimport javax.net.ssl.SSLException\n\nclass OpenRosaClient(\n    serverURL: String,\n    openRosaHttpInterface: OpenRosaHttpInterface?,\n    private val webCredentialsProvider: WebCredentialsProvider,\n    private val openRosaResponseParser: OpenRosaResponseParser\n) : FormSource, EntitySource {\n    private val openRosaXMLFetcher =\n        OpenRosaXmlFetcher(openRosaHttpInterface, this.webCredentialsProvider)\n\n    private var serverUrl: String = serverURL\n\n    @Throws(FormSourceException::class)\n    override fun fetchFormList(): List<FormListItem> {\n        val result = mapException { openRosaXMLFetcher.getXML(getFormListURL()) }\n\n        if (result.errorMessage != null) {\n            when (result.responseCode) {\n                HttpURLConnection.HTTP_UNAUTHORIZED -> {\n                    throw AuthRequired()\n                }\n\n                HttpURLConnection.HTTP_NOT_FOUND -> {\n                    throw FormSourceException.Unreachable(serverUrl)\n                }\n\n                else -> {\n                    throw FormSourceException.ServerError(result.responseCode, serverUrl)\n                }\n            }\n        }\n\n        if (result.isOpenRosaResponse) {\n            val formList = openRosaResponseParser.parseFormList(result.doc)\n\n            if (formList != null) {\n                return formList\n            } else {\n                throw FormSourceException.ParseError(serverUrl)\n            }\n        } else {\n            throw ServerNotOpenRosaError()\n        }\n    }\n\n    @Throws(FormSourceException::class)\n    override fun fetchManifest(manifestURL: String?): ManifestFile? {\n        if (manifestURL == null) {\n            return null\n        }\n\n        val result = mapException { openRosaXMLFetcher.getXML(manifestURL) }\n\n        if (result.errorMessage != null) {\n            if (result.responseCode != HttpURLConnection.HTTP_OK) {\n                throw FormSourceException.ServerError(result.responseCode, serverUrl)\n            } else {\n                throw FetchError()\n            }\n        }\n\n        if (!result.isOpenRosaResponse) {\n            throw FormSourceException.ParseError(serverUrl)\n        }\n\n        val mediaFiles = openRosaResponseParser.parseManifest(result.doc)\n        if (mediaFiles != null) {\n            return ManifestFile(result.hash, mediaFiles)\n        } else {\n            throw FormSourceException.ParseError(serverUrl)\n        }\n    }\n\n    @Throws(FormSourceException::class)\n    override fun fetchForm(formURL: String): InputStream {\n        val result = mapException { openRosaXMLFetcher.fetch(formURL, null) }\n\n        if (result.inputStream == null) {\n            throw FormSourceException.ServerError(result.statusCode, serverUrl)\n        } else {\n            return result.inputStream\n        }\n    }\n\n    @Throws(FormSourceException::class)\n    override fun fetchMediaFile(mediaFileURL: String): InputStream {\n        val result = mapException { openRosaXMLFetcher.fetch(mediaFileURL, null) }\n\n        if (result.inputStream == null) {\n            throw FormSourceException.ServerError(result.statusCode, serverUrl)\n        } else {\n            return result.inputStream\n        }\n    }\n\n    fun updateUrl(url: String) {\n        this.serverUrl = url\n    }\n\n    fun updateWebCredentialsUtils(webCredentialsProvider: WebCredentialsProvider?) {\n        openRosaXMLFetcher.updateWebCredentialsProvider(webCredentialsProvider)\n    }\n\n    @Throws(FormSourceException::class)\n    private fun <T> mapException(callable: Callable<T>): T {\n        try {\n            val result = callable.call()\n\n            if (result != null) {\n                return result\n            } else {\n                throw FetchError()\n            }\n        } catch (e: UnknownHostException) {\n            throw FormSourceException.Unreachable(serverUrl)\n        } catch (e: SSLException) {\n            throw SecurityError(serverUrl)\n        } catch (e: Exception) {\n            throw FetchError()\n        }\n    }\n\n    private fun getFormListURL(): String {\n        var downloadListUrl = serverUrl\n\n        while (downloadListUrl.endsWith(\"/\")) {\n            downloadListUrl = downloadListUrl.substring(0, downloadListUrl.length - 1)\n        }\n\n        downloadListUrl += OpenRosaConstants.FORM_LIST\n        return downloadListUrl\n    }\n\n    override fun fetchDeletedStates(integrityUrl: String, ids: List<String>): List<Pair<String, Boolean>> {\n        val uri = Uri.parse(integrityUrl)\n            .buildUpon()\n            .appendQueryParameter(\"id\", ids.joinToString(\",\"))\n            .build()\n\n        val result = openRosaXMLFetcher.getXML(uri.toString())\n        if (!result.isOpenRosaResponse) {\n            throw FormSourceException.ParseError(serverUrl)\n        }\n\n        val parsedResponse = openRosaResponseParser.parseIntegrityResponse(result.doc)\n\n        if (parsedResponse != null) {\n            return parsedResponse.map { Pair(it.id, it.deleted) }\n        } else {\n            throw FormSourceException.ParseError(serverUrl)\n        }\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/forms/OpenRosaXmlFetcher.java",
    "content": "package org.odk.collect.openrosa.forms;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport org.javarosa.xform.parse.XFormParser;\nimport org.kxml2.kdom.Document;\nimport org.odk.collect.openrosa.http.HttpCredentialsInterface;\nimport org.odk.collect.openrosa.http.HttpGetResult;\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface;\n\nimport java.io.InputStreamReader;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\n\nimport timber.log.Timber;\n\n/**\n * This is only used inside {@link OpenRosaClient} and could potentially be absorbed there. Some\n * of the parsing logic here might be better broken out somewhere else however if it can be used\n * in other scenarios.\n */\npublic class OpenRosaXmlFetcher {\n\n    private static final String HTTP_CONTENT_TYPE_TEXT_XML = \"text/xml\";\n\n    private final OpenRosaHttpInterface httpInterface;\n    private WebCredentialsProvider webCredentialsUtils;\n\n    OpenRosaXmlFetcher(OpenRosaHttpInterface httpInterface, WebCredentialsProvider webCredentialsUtils) {\n        this.httpInterface = httpInterface;\n        this.webCredentialsUtils = webCredentialsUtils;\n    }\n\n    /**\n     * Gets an XML document for a given url\n     *\n     * @param urlString - url of the XML document\n     * @return DocumentFetchResult - an object that contains the results of the \"get\" operation\n     */\n\n    public DocumentFetchResult getXML(String urlString) throws Exception {\n\n        // parse response\n        Document doc;\n        HttpGetResult inputStreamResult;\n\n        inputStreamResult = fetch(urlString, HTTP_CONTENT_TYPE_TEXT_XML);\n\n        if (inputStreamResult.getStatusCode() != HttpURLConnection.HTTP_OK) {\n            String error = \"getXML failed while accessing \"\n                    + urlString + \" with status code: \" + inputStreamResult.getStatusCode();\n            return new DocumentFetchResult(error, inputStreamResult.getStatusCode());\n        }\n\n        doc = XFormParser.getXMLDocument(new InputStreamReader(inputStreamResult.getInputStream()));\n\n        return new DocumentFetchResult(doc, inputStreamResult.isOpenRosaResponse(), inputStreamResult.getHash());\n    }\n\n    /**\n     * Creates a Http connection and input stream\n     *\n     * @param downloadUrl uri of the stream\n     * @param contentType check the returned Mime Type to ensure it matches. \"text/xml\" causes a Hash to be calculated\n     * @return HttpGetResult - An object containing the Stream, Hash and Headers\n     * @throws Exception - Can throw a multitude of Exceptions, such as MalformedURLException or IOException\n     */\n\n    @NonNull\n    public HttpGetResult fetch(@NonNull String downloadUrl, @Nullable final String contentType) throws Exception {\n        URI uri;\n        try {\n            // assume the downloadUrl is escaped properly\n            URL url = new URL(downloadUrl);\n            uri = url.toURI();\n        } catch (MalformedURLException | URISyntaxException e) {\n            Timber.e(e, \"Unable to get a URI for download URL : %s  due to %s : \", downloadUrl, e.getMessage());\n            throw e;\n        }\n\n        if (uri.getHost() == null) {\n            Timber.e(new Error(\"Invalid server URL (no hostname): \" + downloadUrl));\n            throw new Exception(\"Invalid server URL (no hostname): \" + downloadUrl);\n        }\n\n        return httpInterface.executeGetRequest(uri, contentType, webCredentialsUtils.getCredentials(uri));\n    }\n\n    public void updateWebCredentialsProvider(WebCredentialsProvider webCredentialsUtils) {\n        this.webCredentialsUtils = webCredentialsUtils;\n    }\n\n    public interface WebCredentialsProvider {\n        HttpCredentialsInterface getCredentials(@NonNull URI url);\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/CaseInsensitiveEmptyHeaders.java",
    "content": "package org.odk.collect.openrosa.http;\n\nimport androidx.annotation.Nullable;\n\nimport java.util.List;\nimport java.util.Set;\nimport java.util.TreeSet;\n\npublic class CaseInsensitiveEmptyHeaders implements CaseInsensitiveHeaders {\n    @Nullable\n    @Override\n    public Set<String> getHeaders() {\n        return new TreeSet<>();\n    }\n\n    @Override\n    public boolean containsHeader(String header) {\n        return false;\n    }\n\n    @Nullable\n    @Override\n    public String getAnyValue(String header) {\n        return null;\n    }\n\n    @Nullable\n    @Override\n    public List<String> getValues(String header) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/CaseInsensitiveHeaders.java",
    "content": "package org.odk.collect.openrosa.http;\n\nimport androidx.annotation.Nullable;\n\nimport java.util.List;\nimport java.util.Set;\n\npublic interface CaseInsensitiveHeaders {\n    @Nullable\n    Set<String> getHeaders();\n\n    boolean containsHeader(String header);\n\n    @Nullable String getAnyValue(String header);\n\n    @Nullable List<String> getValues(String header);\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/CollectThenSystemContentTypeMapper.java",
    "content": "package org.odk.collect.openrosa.http;\n\nimport android.webkit.MimeTypeMap;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.Locale;\n\n/**\n * This covers types not included in Android's MimeTypeMap\n * Reference https://android.googlesource.com/platform/frameworks/base/+/61ae88e/core/java/android/webkit/MimeTypeMap.java\n */\npublic class CollectThenSystemContentTypeMapper implements OpenRosaHttpInterface.FileToContentTypeMapper {\n\n    private final MimeTypeMap androidTypeMap;\n\n    public CollectThenSystemContentTypeMapper(MimeTypeMap androidTypeMap) {\n        this.androidTypeMap = androidTypeMap;\n    }\n\n    @NonNull\n    @Override\n    public String map(String fileName) {\n        String extension = getFileExtension(fileName);\n\n        String collectContentType = CollectContentTypeMappings.of(extension);\n        String androidContentType = androidTypeMap.getMimeTypeFromExtension(extension);\n\n        if (collectContentType != null) {\n            return collectContentType;\n        } else if (androidContentType != null) {\n            return androidContentType;\n        } else {\n            return \"application/octet-stream\";\n        }\n    }\n\n    private static String getFileExtension(String fileName) {\n        int dotIndex = fileName.lastIndexOf('.');\n        if (dotIndex == -1) {\n            return \"\";\n        }\n        return fileName.substring(dotIndex + 1).toLowerCase(Locale.ROOT);\n    }\n\n    private enum CollectContentTypeMappings {\n        AMR(\"amr\",  \"audio/amr\"),\n        OGA(\"oga\",  \"audio/ogg\"),\n        OGV(\"ogv\",  \"video/ogg\"),\n        WEBM(\"webm\", \"video/webm\");\n\n        private String extension;\n        private String contentType;\n\n        CollectContentTypeMappings(String extension, String contentType) {\n            this.extension = extension;\n            this.contentType = contentType;\n        }\n\n        public static String of(String extension) {\n            for (CollectContentTypeMappings m : CollectContentTypeMappings.values()) {\n                if (m.extension.equals(extension)) {\n                    return m.contentType;\n                }\n            }\n\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/HttpCredentials.java",
    "content": "package org.odk.collect.openrosa.http;\n\npublic class HttpCredentials implements HttpCredentialsInterface {\n\n    private final String username;\n    private final String password;\n\n    public HttpCredentials(String username, String password) {\n        this.username = (username == null) ? \"\" : username;\n        this.password = (password == null) ? \"\" : password;\n    }\n\n    @Override\n    public String getUsername() {\n        return username;\n    }\n\n    @Override\n    public String getPassword() {\n        return password;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj == null) {\n            return false;\n        }\n\n        if (super.equals(obj)) {\n            return true;\n        }\n\n        return ((HttpCredentials) obj).getUsername().equals(getUsername()) &&\n                ((HttpCredentials) obj).getPassword().equals(getPassword());\n    }\n\n    @Override\n    public int hashCode() {\n        return (getUsername() + getPassword()).hashCode();\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/HttpCredentialsInterface.java",
    "content": "package org.odk.collect.openrosa.http;\n\npublic interface HttpCredentialsInterface {\n    String getUsername();\n\n    String getPassword();\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/HttpGetResult.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.openrosa.http;\n\nimport androidx.annotation.NonNull;\n\nimport java.io.InputStream;\nimport java.util.Map;\n\nimport timber.log.Timber;\n\npublic class HttpGetResult {\n\n    private static final String OPEN_ROSA_VERSION_HEADER = OpenRosaConstants.VERSION_HEADER;\n    private static final String OPEN_ROSA_VERSION = \"1.0\";\n\n    private final InputStream inputStream;\n    private final Map<String, String> headers;\n    private final String hash;\n    private final int statusCode;\n\n    public HttpGetResult(InputStream is, @NonNull Map<String, String> headers, String hash, int statusCode) {\n        inputStream = is;\n        this.headers = headers;\n        this.hash = hash;\n        this.statusCode = statusCode;\n    }\n\n    public InputStream getInputStream() {\n        return inputStream;\n    }\n\n    public String getHash() {\n        return hash;\n    }\n\n    public boolean isOpenRosaResponse() {\n        boolean openRosaResponse = false;\n\n        if (!headers.isEmpty()) {\n            boolean versionMatch = false;\n            boolean first = true;\n\n            StringBuilder appendedVersions = new StringBuilder();\n\n            for (String key : headers.keySet()) {\n                if (key.equalsIgnoreCase(OPEN_ROSA_VERSION_HEADER)) {\n                    openRosaResponse = true;\n                    if (OPEN_ROSA_VERSION.equals(headers.get(key))) {\n                        versionMatch = true;\n                        break;\n                    }\n                    if (!first) {\n                        appendedVersions.append(\"; \");\n                    }\n                    first = false;\n                    appendedVersions.append(headers.get(key));\n                }\n            }\n            if (!versionMatch) {\n                Timber.w(\"%s unrecognized version(s): %s\", OPEN_ROSA_VERSION_HEADER, appendedVersions.toString());\n            }\n        }\n\n        return openRosaResponse;\n    }\n\n    public int getStatusCode() {\n        return statusCode;\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/HttpHeadResult.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.openrosa.http;\n\npublic class HttpHeadResult {\n\n    private final int statusCode;\n\n    private final CaseInsensitiveHeaders headers;\n\n    public HttpHeadResult(int statusCode, CaseInsensitiveHeaders headers) {\n        this.statusCode = statusCode;\n        this.headers = headers;\n    }\n\n    public int getStatusCode() {\n        return statusCode;\n    }\n\n    public CaseInsensitiveHeaders getHeaders() {\n        return headers;\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/HttpPostResult.java",
    "content": "package org.odk.collect.openrosa.http;\n\npublic class HttpPostResult {\n\n    private final String httpResponse;\n    private final int responseCode;\n    private final String reasonPhrase;\n\n    public HttpPostResult(String httpResponse, int responseCode, String reasonPhrase) {\n        this.httpResponse = httpResponse;\n        this.responseCode = responseCode;\n        this.reasonPhrase = reasonPhrase;\n    }\n\n    public int getResponseCode() {\n        return responseCode;\n    }\n\n    public String getReasonPhrase() {\n        return reasonPhrase;\n    }\n\n    public String getHttpResponse() {\n        return httpResponse;\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/OpenRosaConstants.kt",
    "content": "package org.odk.collect.openrosa.http\n\nobject OpenRosaConstants {\n    // HTTP Header strings\n    const val VERSION_HEADER = \"X-OpenRosa-Version\"\n    const val ACCEPT_CONTENT_LENGTH_HEADER = \"X-OpenRosa-Accept-Content-Length\"\n\n    // Endpoints\n    const val FORM_LIST = \"/formList\"\n    const val SUBMISSION = \"/submission\"\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/OpenRosaHttpInterface.java",
    "content": "/*\n * Copyright 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.odk.collect.openrosa.http;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.util.List;\n\npublic interface OpenRosaHttpInterface {\n\n    /**\n     * Creates a http connection and sets up an input stream.\n     *\n     * @param uri         of the stream\n     * @param contentType check the returned Mime Type to ensure it matches. \"text/xml\" causes a Hash to be calculated\n     * @param credentials to use for this executeGetRequest request\n     * @return HttpGetResult - An object containing the Stream, Hash and Headers\n     * @throws Exception various Exceptions such as IOException can be thrown\n     */\n    @NonNull\n    HttpGetResult executeGetRequest(@NonNull URI uri, @Nullable String contentType, @NonNull HttpCredentialsInterface credentials) throws Exception;\n\n    /**\n     * Performs a Http Head request.\n     *\n     * @param uri         of which to perform a Http head\n     * @param credentials to use for this head request\n     * @return HttpHeadResult containing status code and headers\n     * @throws Exception various Exceptions such as IOException can be thrown\n     */\n    @NonNull\n    HttpHeadResult executeHeadRequest(@NonNull URI uri, @NonNull HttpCredentialsInterface credentials) throws Exception;\n\n    /**\n     * Uploads submission files and then list of other files to server\n     *\n     * @param submissionFile The main file to be uploaded (Form file)\n     * @param fileList       List of Files to be uploaded\n     * @param uri            where to send the submissionFile and fileList\n     * @param contentLength  contentLength requested by the server\n     * @return ResponseMessageParser object that contains the response XML\n     * @throws IOException can be thrown if files do not exist\n     */\n    @NonNull\n    HttpPostResult uploadSubmissionAndFiles(@NonNull File submissionFile,\n                                            @NonNull List<File> fileList,\n                                            @NonNull URI uri,\n                                            @NonNull HttpCredentialsInterface credentials,\n                                            @NonNull long contentLength) throws Exception;\n\n    interface FileToContentTypeMapper {\n\n        @NonNull\n        String map(String fileName);\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/okhttp/OkHttpCaseInsensitiveHeaders.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport androidx.annotation.Nullable;\n\nimport org.odk.collect.openrosa.http.CaseInsensitiveHeaders;\n\nimport java.util.List;\nimport java.util.Set;\n\nimport okhttp3.Headers;\n\npublic class OkHttpCaseInsensitiveHeaders implements CaseInsensitiveHeaders {\n    Headers headers;\n\n    public OkHttpCaseInsensitiveHeaders(Headers headers) {\n        this.headers = headers;\n    }\n\n    @Override\n    public Set<String> getHeaders() {\n        return headers.names();\n    }\n\n    @Override\n    public boolean containsHeader(String header) {\n        return header != null && headers.get(header) != null;\n    }\n\n    @Nullable\n    @Override\n    public String getAnyValue(String header) {\n        return headers.get(header);\n    }\n\n    @Nullable\n    @Override\n    public List<String> getValues(String header) {\n        return headers.values(header);\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/okhttp/OkHttpConnection.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport org.apache.commons.io.IOUtils;\nimport org.odk.collect.openrosa.http.CaseInsensitiveEmptyHeaders;\nimport org.odk.collect.openrosa.http.CaseInsensitiveHeaders;\nimport org.odk.collect.openrosa.http.HttpCredentialsInterface;\nimport org.odk.collect.openrosa.http.HttpGetResult;\nimport org.odk.collect.openrosa.http.HttpHeadResult;\nimport org.odk.collect.openrosa.http.HttpPostResult;\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.net.HttpURLConnection;\nimport java.net.URI;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\n\nimport okhttp3.Headers;\nimport okhttp3.MediaType;\nimport okhttp3.MultipartBody;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport okhttp3.ResponseBody;\nimport timber.log.Timber;\n\npublic class OkHttpConnection implements OpenRosaHttpInterface {\n\n    private static final String HTTP_CONTENT_TYPE_TEXT_XML = \"text/xml\";\n\n    private final OkHttpOpenRosaServerClientProvider clientFactory;\n\n    @NonNull\n    private final FileToContentTypeMapper fileToContentTypeMapper;\n\n    @NonNull\n    private final String userAgent;\n\n    public OkHttpConnection(@Nullable String cacheDir, @NonNull FileToContentTypeMapper fileToContentTypeMapper, @NonNull String userAgent) {\n        this.clientFactory = new OkHttpOpenRosaServerClientProvider(cacheDir);\n        this.fileToContentTypeMapper = fileToContentTypeMapper;\n        this.userAgent = userAgent;\n    }\n\n    @NonNull\n    @Override\n    public HttpGetResult executeGetRequest(@NonNull URI uri, @Nullable String contentType, @Nullable HttpCredentialsInterface credentials) throws Exception {\n        OpenRosaServerClient httpClient = clientFactory.get(uri.getScheme(), userAgent, credentials);\n        Request request = new Request.Builder()\n                .url(uri.toURL())\n                .get()\n                .build();\n\n        Response response = httpClient.makeRequest(request, new Date());\n        int statusCode = response.code();\n\n        if (statusCode != HttpURLConnection.HTTP_OK) {\n            discardEntityBytes(response);\n            Timber.i(\"Error: %s (%s at %s\", response.message(), String.valueOf(statusCode), uri.toString());\n\n            return new HttpGetResult(null, new HashMap<>(), \"\", statusCode);\n        }\n\n        ResponseBody body = response.body();\n\n        if (body == null) {\n            throw new Exception(\"No entity body returned from: \" + uri.toString());\n        }\n\n        if (contentType != null && contentType.length() > 0) {\n            MediaType type = body.contentType();\n\n            if (type != null && !type.toString().toLowerCase(Locale.ENGLISH).contains(contentType)) {\n                discardEntityBytes(response);\n\n                String error = \"ContentType: \" + type.toString() + \" returned from: \"\n                        + uri.toString() + \" is not \" + contentType\n                        + \".  This is often caused by a network proxy.  Do you need \"\n                        + \"to login to your network?\";\n\n                throw new Exception(error);\n            }\n        }\n\n        InputStream downloadStream = body.byteStream();\n\n        String hash = \"\";\n\n        if (HTTP_CONTENT_TYPE_TEXT_XML.equals(contentType)) {\n            byte[] bytes = IOUtils.toByteArray(downloadStream);\n            downloadStream = new ByteArrayInputStream(bytes);\n            hash = Md5.getMd5Hash(new ByteArrayInputStream(bytes));\n        }\n\n        Map<String, String> responseHeaders = new HashMap<>();\n        Headers headers = response.headers();\n\n        for (int i = 0; i < headers.size(); i++) {\n            responseHeaders.put(headers.name(i), headers.value(i));\n        }\n\n        return new HttpGetResult(downloadStream, responseHeaders, hash, statusCode);\n    }\n\n    @NonNull\n    @Override\n    public HttpHeadResult executeHeadRequest(@NonNull URI uri, @Nullable HttpCredentialsInterface credentials) throws Exception {\n        OpenRosaServerClient httpClient = clientFactory.get(uri.getScheme(), userAgent, credentials);\n        Request request = new Request.Builder()\n                .url(uri.toURL())\n                .head()\n                .build();\n\n        Timber.i(\"Issuing HEAD request to: %s\", uri.toString());\n        Response response = httpClient.makeRequest(request, new Date());\n        int statusCode = response.code();\n\n        CaseInsensitiveHeaders responseHeaders = new CaseInsensitiveEmptyHeaders();\n\n        if (statusCode == HttpURLConnection.HTTP_NO_CONTENT) {\n            responseHeaders = new OkHttpCaseInsensitiveHeaders(response.headers());\n        }\n\n        discardEntityBytes(response);\n\n        return new HttpHeadResult(statusCode, responseHeaders);\n    }\n\n    @NonNull\n    @Override\n    public HttpPostResult uploadSubmissionAndFiles(@NonNull File submissionFile, @NonNull List<File> fileList, @NonNull URI uri, @Nullable HttpCredentialsInterface credentials, @NonNull long contentLength) throws Exception {\n        HttpPostResult postResult = null;\n\n        boolean first = true;\n        int fileIndex = 0;\n        int lastFileIndex;\n        while (fileIndex < fileList.size() || first) {\n            lastFileIndex = fileIndex;\n            first = false;\n            long byteCount = 0L;\n\n            RequestBody requestBody = RequestBody.create(MediaType.parse(HTTP_CONTENT_TYPE_TEXT_XML), submissionFile);\n\n            MultipartBody.Builder multipartBuilder = new MultipartBody.Builder()\n                    .setType(MultipartBody.FORM)\n                    .addPart(MultipartBody.Part.createFormData(\"xml_submission_file\", submissionFile.getName(), requestBody));\n\n            Timber.i(\"added xml_submission_file: %s\", submissionFile.getName());\n            byteCount += submissionFile.length();\n\n            for (; fileIndex < fileList.size(); fileIndex++) {\n                File file = fileList.get(fileIndex);\n\n                String contentType = fileToContentTypeMapper.map(file.getName());\n\n                RequestBody fileRequestBody = RequestBody.create(MediaType.parse(contentType), file);\n                multipartBuilder.addPart(MultipartBody.Part.createFormData(file.getName(), file.getName(), fileRequestBody));\n\n                byteCount += file.length();\n                Timber.i(\"added file of type '%s' %s\", contentType, file.getName());\n\n                // we've added at least one attachment to the request...\n                if (fileIndex + 1 < fileList.size()) {\n                    if ((fileIndex - lastFileIndex + 1 > 100) || (byteCount + fileList.get(fileIndex + 1).length()\n                            > contentLength)) {\n                        // the next file would exceed the 10MB threshold...\n                        Timber.i(\"Extremely long post is being split into multiple posts\");\n                        multipartBuilder.addPart(MultipartBody.Part.createFormData(\"*isIncomplete*\", \"yes\"));\n                        ++fileIndex; // advance over the last attachment added...\n                        break;\n                    }\n                }\n            }\n\n            MultipartBody multipartBody = multipartBuilder.build();\n            postResult = executePostRequest(uri, credentials, multipartBody);\n\n            if (postResult.getResponseCode() != HttpURLConnection.HTTP_CREATED &&\n                    postResult.getResponseCode() != HttpURLConnection.HTTP_ACCEPTED) {\n                return postResult;\n            }\n\n        }\n\n        return postResult;\n    }\n\n    @NonNull\n    private HttpPostResult executePostRequest(@NonNull URI uri, @Nullable HttpCredentialsInterface credentials, MultipartBody multipartBody) throws Exception {\n        OpenRosaServerClient httpClient = clientFactory.get(uri.getScheme(), userAgent, credentials);\n        HttpPostResult postResult;\n        Request request = new Request.Builder()\n                .url(uri.toURL())\n                .post(multipartBody)\n                .build();\n        Response response = httpClient.makeRequest(request, new Date());\n\n        if (response.code() == 204) {\n            throw new Exception();\n        }\n\n        postResult = new HttpPostResult(\n                response.body().string(),\n                response.code(),\n                response.message());\n\n        discardEntityBytes(response);\n\n        return postResult;\n    }\n\n    /**\n     * Utility to ensure that the entity stream of a response is drained of\n     * bytes.\n     * Apparently some servers require that we manually read all data from the\n     * stream to allow its re-use.  Please add more details or bug ID here if\n     * you know them.\n     */\n    private void discardEntityBytes(Response response) {\n        ResponseBody body = response.body();\n        if (body != null) {\n            try (InputStream is = body.byteStream()) {\n                while (is.read() != -1) {\n                    // loop until all bytes read\n                }\n            } catch (Exception e) {\n                Timber.i(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/okhttp/OkHttpOpenRosaServerClientProvider.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport androidx.annotation.NonNull;\n\nimport com.burgstaller.okhttp.AuthenticationCacheInterceptor;\nimport com.burgstaller.okhttp.CachingAuthenticatorDecorator;\nimport com.burgstaller.okhttp.DispatchingAuthenticator;\nimport com.burgstaller.okhttp.basic.BasicAuthenticator;\nimport com.burgstaller.okhttp.digest.CachingAuthenticator;\nimport com.burgstaller.okhttp.digest.Credentials;\nimport com.burgstaller.okhttp.digest.DigestAuthenticator;\n\nimport org.odk.collect.openrosa.http.HttpCredentialsInterface;\nimport org.odk.collect.openrosa.http.OpenRosaConstants;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.TimeZone;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.TimeUnit;\n\nimport kotlin.Pair;\nimport okhttp3.Cache;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\n\npublic class OkHttpOpenRosaServerClientProvider implements OpenRosaServerClientProvider {\n\n    private static final int CONNECTION_TIMEOUT = 30000;\n    private static final int WRITE_CONNECTION_TIMEOUT = 60000; // it can take up to 27 seconds to spin up an Aggregate\n    private static final int READ_CONNECTION_TIMEOUT = 60000; // it can take up to 27 seconds to spin up an Aggregate\n    private static final String USER_AGENT_HEADER = \"User-Agent\";\n    private static final String OPEN_ROSA_VERSION_HEADER = OpenRosaConstants.VERSION_HEADER;\n    private static final String OPEN_ROSA_VERSION = \"1.0\";\n    private static final String DATE_HEADER = \"Date\";\n\n    private final OkHttpClient baseClient;\n    private final String cacheDir;\n\n    private final Map<Pair<String, HttpCredentialsInterface>, OkHttpOpenRosaServerClient> clients = new HashMap<>();\n\n    public OkHttpOpenRosaServerClientProvider(@NonNull OkHttpClient baseClient, String cacheDir) {\n        this.baseClient = baseClient;\n        this.cacheDir = cacheDir;\n    }\n\n    public OkHttpOpenRosaServerClientProvider(String cacheDir) {\n        this(new OkHttpClient(), cacheDir);\n    }\n\n    @Override\n    public synchronized OpenRosaServerClient get(String scheme, String userAgent, @NonNull HttpCredentialsInterface credentials) {\n        OkHttpOpenRosaServerClient existingClient = clients.get(new Pair<>(scheme, credentials));\n\n        if (existingClient == null) {\n            OkHttpOpenRosaServerClient newClient = createNewClient(scheme, userAgent, credentials);\n            clients.put(new Pair<>(scheme, credentials), newClient);\n            return newClient;\n        } else {\n            return existingClient;\n        }\n    }\n\n    @NonNull\n    private OkHttpOpenRosaServerClient createNewClient(String scheme, String userAgent, @NonNull HttpCredentialsInterface credentials) {\n        OkHttpClient.Builder builder = baseClient.newBuilder()\n                .connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)\n                .writeTimeout(WRITE_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)\n                .readTimeout(READ_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)\n                .followRedirects(true);\n\n        if (cacheDir != null && new File(cacheDir).exists()) {\n            builder.cache(new Cache(\n                    new File(cacheDir, \"http_\" + credentials.hashCode()),\n                    50L * 1024L * 1024L // 50 MiB\n            ));\n        }\n\n        if (credentials != null) {\n            Credentials cred = new Credentials(credentials.getUsername(), credentials.getPassword());\n\n            DispatchingAuthenticator.Builder daBuilder = new DispatchingAuthenticator.Builder();\n            daBuilder.with(\"digest\", new DigestAuthenticator(cred));\n            if (scheme.equalsIgnoreCase(\"https\")) {\n                daBuilder.with(\"basic\", new BasicAuthenticator(cred));\n            }\n\n            DispatchingAuthenticator authenticator = daBuilder.build();\n            ConcurrentHashMap<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();\n            builder.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))\n                    .addInterceptor(new AuthenticationCacheInterceptor(authCache)).build();\n        }\n\n        return new OkHttpOpenRosaServerClient(builder.build(), userAgent);\n    }\n\n    private static class OkHttpOpenRosaServerClient implements OpenRosaServerClient {\n\n        private final OkHttpClient client;\n        private final String userAgent;\n\n        OkHttpOpenRosaServerClient(OkHttpClient client, String userAgent) {\n            this.client = client;\n            this.userAgent = userAgent;\n        }\n\n        @Override\n        public Response makeRequest(Request request, Date currentTime) throws IOException {\n            return client.newCall(request.newBuilder()\n                    .addHeader(USER_AGENT_HEADER, userAgent)\n                    .addHeader(OPEN_ROSA_VERSION_HEADER, OPEN_ROSA_VERSION)\n                    .addHeader(DATE_HEADER, getHeaderDate(currentTime))\n                    .build()).execute();\n        }\n\n        private static String getHeaderDate(Date currentTime) {\n            SimpleDateFormat dateFormatGmt = new SimpleDateFormat(\"E, dd MMM yyyy hh:mm:ss zz\", Locale.US);\n            dateFormatGmt.setTimeZone(TimeZone.getTimeZone(\"GMT\"));\n            return dateFormatGmt.format(currentTime);\n        }\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/okhttp/OpenRosaServerClient.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport java.io.IOException;\nimport java.util.Date;\n\nimport okhttp3.Request;\nimport okhttp3.Response;\n\npublic interface OpenRosaServerClient {\n\n    Response makeRequest(Request request, Date currentTime) throws IOException;\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/http/okhttp/OpenRosaServerClientProvider.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport androidx.annotation.NonNull;\n\nimport org.odk.collect.openrosa.http.HttpCredentialsInterface;\n\npublic interface OpenRosaServerClientProvider {\n\n    OpenRosaServerClient get(String schema, String userAgent, @NonNull HttpCredentialsInterface credentialsInterface);\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/parse/Kxml2OpenRosaResponseParser.kt",
    "content": "package org.odk.collect.openrosa.parse\n\nimport org.javarosa.xform.parse.XFormParser\nimport org.kxml2.kdom.Document\nimport org.kxml2.kdom.Element\nimport org.odk.collect.forms.FormListItem\nimport org.odk.collect.forms.MediaFile\nimport org.odk.collect.forms.MediaFile.Type\nimport org.odk.collect.openrosa.forms.EntityIntegrity\nimport org.odk.collect.shared.strings.StringUtils.isBlank\nimport java.io.File\n\nobject Kxml2OpenRosaResponseParser :\n    OpenRosaResponseParser {\n\n    private const val MD5_STRING_PREFIX = \"md5:\"\n\n    private const val NAMESPACE_OPENROSA_ORG_XFORMS_XFORMS_LIST =\n        \"http://openrosa.org/xforms/xformsList\"\n    private const val NAMESPACE_OPENROSA_ORG_XFORMS_XFORMS_MANIFEST =\n        \"http://openrosa.org/xforms/xformsManifest\"\n\n    override fun parseFormList(document: Document): List<FormListItem>? {\n        // Attempt OpenRosa 1.0 parsing\n        val xformsElement = try {\n            document.rootElement\n        } catch (e: RuntimeException) {\n            return null\n        }\n\n        if (xformsElement.name != \"xforms\") {\n            return null\n        }\n\n        if (!hasNamespace(xformsElement, NAMESPACE_OPENROSA_ORG_XFORMS_XFORMS_LIST)) {\n            return null\n        }\n\n        val formList: MutableList<FormListItem> = ArrayList()\n        val elements = xformsElement.childCount\n        for (i in 0 until elements) {\n            if (xformsElement.getType(i) != Element.ELEMENT) {\n                // e.g., whitespace (text)\n                continue\n            }\n\n            val xformElement = xformsElement.getElement(i)\n            if (!hasNamespace(\n                    xformElement,\n                    NAMESPACE_OPENROSA_ORG_XFORMS_XFORMS_LIST\n                )) {\n                // someone else's extension?\n                continue\n            }\n\n            val name = xformElement.name\n            if (!name.equals(\"xform\", ignoreCase = true)) {\n                // someone else's extension?\n                continue\n            }\n\n            // this is something we know how to interpret\n            var formId: String? = null\n            var formName: String? = null\n            var version: String? = null\n            var downloadUrl: String? = null\n            var manifestUrl: String? = null\n            var hash: String? = null\n            // don't process descriptionUrl\n\n            val fieldCount = xformElement.childCount\n            for (j in 0 until fieldCount) {\n                if (xformElement.getType(j) != Element.ELEMENT) {\n                    // whitespace\n                    continue\n                }\n\n                val child = xformElement.getElement(j)\n                if (!hasNamespace(child, NAMESPACE_OPENROSA_ORG_XFORMS_XFORMS_LIST)) {\n                    // someone else's extension?\n                    continue\n                }\n\n                when (child.name) {\n                    \"formID\" -> {\n                        formId = XFormParser.getXMLText(child, true)\n                        if (formId != null && formId.isEmpty()) {\n                            formId = null\n                        }\n                    }\n                    \"name\" -> {\n                        formName = XFormParser.getXMLText(child, true)\n                        if (formName != null && formName.isEmpty()) {\n                            formName = null\n                        }\n                    }\n                    \"version\" -> {\n                        version = XFormParser.getXMLText(child, true)\n                        if (version != null && isBlank(version)) {\n                            version = null\n                        }\n                    }\n                    \"majorMinorVersion\" -> {\n                    }\n                    \"descriptionText\" -> {\n                    }\n                    \"downloadUrl\" -> {\n                        downloadUrl = XFormParser.getXMLText(child, true)\n                        if (downloadUrl != null && downloadUrl.isEmpty()) {\n                            downloadUrl = null\n                        }\n                    }\n                    \"manifestUrl\" -> {\n                        manifestUrl = XFormParser.getXMLText(child, true)\n                        if (manifestUrl != null && manifestUrl.isEmpty()) {\n                            manifestUrl = null\n                        }\n                    }\n                    \"hash\" -> {\n                        hash = XFormParser.getXMLText(child, true)\n                        hash = if (hash != null && (hash.isEmpty() || !hash.startsWith(\n                                MD5_STRING_PREFIX\n                            ))) {\n                            null\n                        } else {\n                            hash.substring(MD5_STRING_PREFIX.length)\n                        }\n                    }\n                }\n            }\n\n            if (formId == null || downloadUrl == null || formName == null) {\n                formList.clear()\n                return null\n            }\n\n            formList.add(FormListItem(downloadUrl, formId, version, hash, formName, manifestUrl))\n        }\n\n        return formList\n    }\n\n    override fun parseManifest(document: Document): List<MediaFile>? {\n        // Attempt OpenRosa 1.0 parsing\n        val manifestElement = try {\n            document.rootElement\n        } catch (e: RuntimeException) {\n            return null\n        }\n\n        if (manifestElement.name != \"manifest\") {\n            return null\n        }\n\n        if (!hasNamespace(manifestElement, NAMESPACE_OPENROSA_ORG_XFORMS_XFORMS_MANIFEST)) {\n            return null\n        }\n\n        val elements = manifestElement.childCount\n        val files: MutableList<MediaFile> = ArrayList()\n        for (i in 0 until elements) {\n            if (manifestElement.getType(i) != Element.ELEMENT) {\n                // e.g., whitespace (text)\n                continue\n            }\n\n            val mediaFileElement = manifestElement.getElement(i)\n            if (!hasNamespace(mediaFileElement, NAMESPACE_OPENROSA_ORG_XFORMS_XFORMS_MANIFEST)) {\n                // someone else's extension?\n                continue\n            }\n\n            val type = mediaFileElement.getAttributeValue(null, \"type\")\n\n            val name = mediaFileElement.name\n            if (name.equals(\"mediaFile\", ignoreCase = true)) {\n                var filename: String? = null\n                var hash: String? = null\n                var downloadUrl: String? = null\n                var integrityUrl: String? = null\n                // don't process descriptionUrl\n                val childCount = mediaFileElement.childCount\n                for (j in 0 until childCount) {\n                    if (mediaFileElement.getType(j) != Element.ELEMENT) {\n                        // e.g., whitespace (text)\n                        continue\n                    }\n\n                    val child = mediaFileElement.getElement(j)\n                    if (!hasNamespace(child, NAMESPACE_OPENROSA_ORG_XFORMS_XFORMS_MANIFEST)) {\n                        // someone else's extension?\n                        continue\n                    }\n\n                    when (child.name) {\n                        \"filename\" -> {\n                            filename = XFormParser.getXMLText(child, true)\n                            if (filename != null && filename.isEmpty()) {\n                                filename = null\n                            }\n                            if (filename != null) {\n                                filename = File(filename).name\n                            }\n                        }\n                        \"hash\" -> {\n                            hash = XFormParser.getXMLText(child, true)\n                            hash = if (hash != null && hash.isEmpty()) {\n                                null\n                            } else {\n                                hash.substring(MD5_STRING_PREFIX.length)\n                            }\n                        }\n                        \"downloadUrl\" -> {\n                            downloadUrl = XFormParser.getXMLText(child, true)\n                            if (downloadUrl != null && downloadUrl.isEmpty()) {\n                                downloadUrl = null\n                            }\n                        }\n                        \"integrityUrl\" -> {\n                            integrityUrl = XFormParser.getXMLText(child, true)\n                        }\n                    }\n                }\n\n                if (filename == null || downloadUrl == null || hash == null) {\n                    return null\n                }\n\n                val mediaFileType = when (type) {\n                    \"entityList\" -> Type.ENTITY_LIST\n                    \"approvalEntityList\" -> Type.APPROVAL_ENTITY_LIST\n                    else -> null\n                }\n\n                files.add(MediaFile(filename, hash, downloadUrl, mediaFileType, integrityUrl))\n            }\n        }\n        return files\n    }\n\n    override fun parseIntegrityResponse(doc: Document): List<EntityIntegrity>? {\n        val data = doc.rootElement\n        if (data.name != \"data\") {\n            return null\n        }\n\n        try {\n            val entities = data.getElement(null, \"entities\")\n            val entitiesList = 0.until(entities.childCount)\n                .map { entities.getElement(it) }\n                .filter { it.name == \"entity\" }\n                .map {\n                    val deleted = it.getElement(null, \"deleted\").getChild(0) as String\n                    EntityIntegrity(it.getAttributeValue(null, \"id\"), deleted.toBooleanStrict())\n                }\n\n            return entitiesList\n        } catch (e: RuntimeException) {\n            return null\n        }\n    }\n\n    private fun hasNamespace(e: Element, namespace: String): Boolean {\n        return e.namespace.equals(namespace, ignoreCase = true)\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/main/java/org/odk/collect/openrosa/parse/OpenRosaResponseParser.kt",
    "content": "package org.odk.collect.openrosa.parse\n\nimport org.kxml2.kdom.Document\nimport org.odk.collect.forms.FormListItem\nimport org.odk.collect.forms.MediaFile\nimport org.odk.collect.openrosa.forms.EntityIntegrity\n\ninterface OpenRosaResponseParser {\n    fun parseFormList(document: Document): List<FormListItem>?\n    fun parseManifest(document: Document): List<MediaFile>?\n    fun parseIntegrityResponse(doc: Document): List<EntityIntegrity>?\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/forms/OpenRosaClientTest.kt",
    "content": "package org.odk.collect.openrosa.forms\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Assert.fail\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.eq\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.forms.FormSourceException\nimport org.odk.collect.forms.FormSourceException.FetchError\nimport org.odk.collect.forms.FormSourceException.SecurityError\nimport org.odk.collect.forms.FormSourceException.ServerNotOpenRosaError\nimport org.odk.collect.openrosa.http.HttpGetResult\nimport org.odk.collect.openrosa.http.OpenRosaConstants\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface\nimport org.odk.collect.openrosa.parse.OpenRosaResponseParser\nimport org.odk.collect.openrosa.support.StubWebCredentialsProvider\nimport java.io.ByteArrayInputStream\nimport java.net.SocketTimeoutException\nimport java.net.URI\nimport java.net.UnknownHostException\nimport javax.net.ssl.SSLException\n\n@RunWith(AndroidJUnit4::class)\nclass OpenRosaClientTest {\n    private val httpInterface = mock<OpenRosaHttpInterface>()\n    private val webCredentialsProvider = StubWebCredentialsProvider()\n    private val responseParser = mock<OpenRosaResponseParser>()\n\n    @Test\n    fun fetchFormList_removesTrailingSlashesFromUrl() {\n        val formListApi = OpenRosaClient(\n            \"http://blah.com///\",\n            httpInterface,\n            webCredentialsProvider,\n            responseParser\n        )\n\n        whenever(\n            httpInterface.executeGetRequest(any(), any(), any())\n        ).thenReturn(\n            HttpGetResult(\n                ByteArrayInputStream(RESPONSE.toByteArray()),\n                object : HashMap<String?, String?>() {\n                    init {\n                        put(OpenRosaConstants.VERSION_HEADER, \"1.0\")\n                    }\n                },\n                \"\", 200\n            )\n        )\n\n        formListApi.fetchFormList()\n        verify(httpInterface).executeGetRequest(eq(URI(\"http://blah.com/formList\")), any(), any())\n    }\n\n    @Test\n    fun fetchFormList_whenThereIsAnUnknownHostException_throwsUnreachableFormApiException() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenThrow(\n                UnknownHostException::class.java\n            )\n\n            formListApi.fetchFormList()\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.Unreachable) {\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchFormList_whenThereIsAnSSLException_throwsSecurityErrorFormApiException() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenThrow(\n                SSLException::class.java\n            )\n\n            formListApi.fetchFormList()\n            fail(\"No exception thrown!\")\n        } catch (e: SecurityError) {\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchFormList_whenThereIsATimeout_throwsFetchError() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenThrow(\n                SocketTimeoutException::class.java\n            )\n\n            formListApi.fetchFormList()\n            fail(\"No exception thrown!\")\n        } catch (e: FetchError) {\n            // pass\n        }\n    }\n\n    @Test\n    fun fetchFormList_whenThereIsA404_throwsUnreachableApiException() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenReturn(HttpGetResult(null, HashMap(), \"hash\", 404))\n\n            formListApi.fetchFormList()\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.Unreachable) {\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchFormList_whenThereIsAServerError_throwsServerError() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenReturn(HttpGetResult(null, HashMap(), \"hash\", 500))\n\n            formListApi.fetchFormList()\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.ServerError) {\n            assertThat(e.statusCode, equalTo(500))\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchFormList_whenOpenRosaResponse_whenParserFails_throwsParseError() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenReturn(\n                HttpGetResult(\n                    ByteArrayInputStream(\"<xml></xml>\".toByteArray()),\n                    object : HashMap<String?, String?>() {\n                        init {\n                            put(OpenRosaConstants.VERSION_HEADER, \"1.0\")\n                        }\n                    },\n                    \"hash\",\n                    200\n                )\n            )\n\n            whenever(responseParser.parseFormList(any())).thenReturn(null)\n            formListApi.fetchFormList()\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.ParseError) {\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchFormList_whenResponseHasNoOpenRosaHeader_throwsServerNotOpenRosaError() {\n        val formListApi = OpenRosaClient(\n            \"http://blah.com///\",\n            httpInterface,\n            webCredentialsProvider,\n            responseParser\n        )\n\n        whenever(\n            httpInterface.executeGetRequest(any(), any(), any())\n        ).thenReturn(\n            HttpGetResult(\n                ByteArrayInputStream(RESPONSE.toByteArray()),\n                HashMap(),\n                \"\", 200\n            )\n        )\n\n        try {\n            formListApi.fetchFormList()\n            fail(\"Expected exception because server is not OpenRosa server.\")\n        } catch (e: ServerNotOpenRosaError) {\n            // pass\n        }\n    }\n\n    @Test\n    fun fetchManifest_whenThereIsAnUnknownHostException_throwsUnreachableFormApiException() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenThrow(\n                UnknownHostException::class.java\n            )\n\n            formListApi.fetchManifest(\"http://blah.com/manifest\")\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.Unreachable) {\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchManifest_whenThereIsAServerError_throwsServerError() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenReturn(HttpGetResult(null, HashMap(), \"hash\", 503))\n\n            formListApi.fetchManifest(\"http://blah.com/manifest\")\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.ServerError) {\n            assertThat(e.statusCode, equalTo(503))\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchManifest_whenOpenRosaResponse_whenParserFails_throwsParseError() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenReturn(\n                HttpGetResult(\n                    ByteArrayInputStream(\"<xml></xml>\".toByteArray()),\n                    object : HashMap<String?, String?>() {\n                        init {\n                            put(OpenRosaConstants.VERSION_HEADER, \"1.0\")\n                        }\n                    },\n                    \"hash\",\n                    200\n                )\n            )\n\n            whenever(responseParser.parseManifest(any())).thenReturn(null)\n            formListApi.fetchManifest(\"http://blah.com/manifest\")\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.ParseError) {\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchManifest_whenNotOpenRosaResponse_throwsParseError() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenReturn(\n                HttpGetResult(\n                    ByteArrayInputStream(\"<xml></xml>\".toByteArray()),\n                    HashMap(),\n                    \"hash\",\n                    200\n                )\n            )\n\n            formListApi.fetchManifest(\"http://blah.com/manifest\")\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.ParseError) {\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchForm_whenThereIsAServerError_throwsServerError() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), eq(null), any())\n            ).thenReturn(HttpGetResult(null, HashMap(), \"hash\", 500))\n\n            formListApi.fetchForm(\"http://blah.com/form\")\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.ServerError) {\n            assertThat(e.statusCode, equalTo(500))\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    @Throws(Exception::class)\n    fun fetchMediaFile_whenThereIsAServerError_throwsServerError() {\n        val formListApi =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), eq(null), any())\n            ).thenReturn(HttpGetResult(null, HashMap(), \"hash\", 500))\n\n            formListApi.fetchMediaFile(\"http://blah.com/mediaFile\")\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.ServerError) {\n            assertThat(e.statusCode, equalTo(500))\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchDeletedStates_whenNotOpenRosaResponse_throwsParseError() {\n        val client =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenReturn(\n                HttpGetResult(\n                    ByteArrayInputStream(\"<xml></xml>\".toByteArray()),\n                    HashMap(),\n                    \"hash\",\n                    200\n                )\n            )\n\n            whenever(responseParser.parseIntegrityResponse(any())).thenReturn(emptyList())\n            client.fetchDeletedStates(\"http://blah.com/integrity\", listOf(\"1\", \"2\", \"3\"))\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.ParseError) {\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    @Test\n    fun fetchDeletedStates_whenOpenRosaResponse_whenParserFails_throwsParseError() {\n        val client =\n            OpenRosaClient(\"http://blah.com\", httpInterface, webCredentialsProvider, responseParser)\n\n        try {\n            whenever(\n                httpInterface.executeGetRequest(any(), any(), any())\n            ).thenReturn(\n                HttpGetResult(\n                    ByteArrayInputStream(\"<xml></xml>\".toByteArray()),\n                    object : HashMap<String?, String?>() {\n                        init {\n                            put(OpenRosaConstants.VERSION_HEADER, \"1.0\")\n                        }\n                    },\n                    \"hash\",\n                    200\n                )\n            )\n\n            whenever(responseParser.parseIntegrityResponse(any())).thenReturn(null)\n            client.fetchDeletedStates(\"http://blah.com/integrity\", listOf(\"1\", \"2\", \"3\"))\n            fail(\"No exception thrown!\")\n        } catch (e: FormSourceException.ParseError) {\n            assertThat(e.serverUrl, equalTo(\"http://blah.com\"))\n        }\n    }\n\n    companion object {\n        private fun join(vararg strings: String): String {\n            val bob = StringBuilder()\n            for (s in strings) {\n                bob.append(s).append('\\n')\n            }\n            return bob.toString()\n        }\n\n        private val RESPONSE = join(\n            \"<xforms xmlns=\\\"http://openrosa.org/xforms/xformsList\\\">\",\n            \"<xform><formID>one</formID>\",\n            \"<name>The First Form</name>\",\n            \"<majorMinorVersion></majorMinorVersion>\",\n            \"<version></version>\",\n            \"<hash>md5:b71c92bec48730119eab982044a8adff</hash>\",\n            \"<downloadUrl>https://example.com/formXml?formId=one</downloadUrl>\",\n            \"</xform>\",\n            \"<xform><formID>two</formID>\",\n            \"<name>The Second Form</name>\",\n            \"<majorMinorVersion></majorMinorVersion>\",\n            \"<version></version>\",\n            \"<hash>md5:4428adffbbec48771c9230119eab9820</hash>\",\n            \"<downloadUrl>https://example.com/formXml?formId=two</downloadUrl>\",\n            \"</xform>\",\n            \"</xforms>\"\n        )\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/forms/OpenRosaXmlFetcherTest.java",
    "content": "package org.odk.collect.openrosa.forms;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.odk.collect.openrosa.http.HttpGetResult;\nimport org.odk.collect.openrosa.http.OpenRosaConstants;\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface;\nimport org.odk.collect.openrosa.support.StubWebCredentialsProvider;\n\nimport java.io.ByteArrayInputStream;\nimport java.util.HashMap;\n\n@RunWith(AndroidJUnit4.class)\npublic class OpenRosaXmlFetcherTest {\n\n    private OpenRosaHttpInterface httpInterface;\n    private OpenRosaXmlFetcher openRosaXMLFetcher;\n\n    @Before\n    public void setup() {\n        httpInterface = mock(OpenRosaHttpInterface.class);\n        openRosaXMLFetcher = new OpenRosaXmlFetcher(httpInterface, new StubWebCredentialsProvider());\n    }\n\n    @Test\n    public void getXML_returnsResultWith0Status() throws Exception {\n        HashMap<String, String> headers = new HashMap<>();\n        headers.put(OpenRosaConstants.VERSION_HEADER, \"1.0\");\n        when(httpInterface.executeGetRequest(any(), any(), any())).thenReturn(new HttpGetResult(\n                new ByteArrayInputStream(\"<xml></xml>\".getBytes()),\n                headers,\n                \"hash\",\n                200\n        ));\n\n        DocumentFetchResult result = openRosaXMLFetcher.getXML(\"http://testurl\");\n        assertThat(result.responseCode, equalTo(0));\n        assertThat(result.isOpenRosaResponse, equalTo(true));\n        assertThat(result.errorMessage, nullValue());\n    }\n\n    @Test\n    public void getXML_whenUnsuccessful_returnsResultWithStatusAndErrorMessage() throws Exception {\n        when(httpInterface.executeGetRequest(any(), any(), any())).thenReturn(new HttpGetResult(null, new HashMap<>(), \"\", 500));\n\n        DocumentFetchResult result = openRosaXMLFetcher.getXML(\"http://testurl\");\n        assertThat(result.responseCode, equalTo(500));\n        assertThat(result.errorMessage, equalTo(\"getXML failed while accessing http://testurl with status code: 500\"));\n    }\n}\n\n\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/CaseInsensitiveEmptyHeadersTest.java",
    "content": "package org.odk.collect.openrosa.http;\n\nimport org.junit.Assert;\nimport org.junit.Test;\n\npublic class CaseInsensitiveEmptyHeadersTest {\n    private final CaseInsensitiveHeaders headers = new CaseInsensitiveEmptyHeaders();\n\n    @Test\n    public void testGetHeaders() {\n        Assert.assertEquals(0, headers.getHeaders().size());\n    }\n\n    @Test\n    public void testContainsHeader() {\n        Assert.assertFalse(headers.containsHeader(\"\"));\n    }\n\n    @Test\n    public void testNullHeaderLookup() {\n        Assert.assertFalse(headers.containsHeader(null));\n    }\n\n    @Test\n    public void testGetAnyValue() {\n        Assert.assertNull(headers.getAnyValue(\"\"));\n    }\n\n    @Test\n    public void testGetValues() {\n        Assert.assertNull(headers.getValues(\"\"));\n    }\n}\n\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/CollectThenSystemContentTypeMapperTest.java",
    "content": "package org.odk.collect.openrosa.http;\n\nimport static org.junit.Assert.assertEquals;\n\nimport android.webkit.MimeTypeMap;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.MockitoJUnitRunner;\n\n@RunWith(MockitoJUnitRunner.class)\npublic class CollectThenSystemContentTypeMapperTest {\n\n    @Mock\n    public MimeTypeMap mimeTypeMap;\n\n    private CollectThenSystemContentTypeMapper mapper;\n\n    @Before\n    public void setup() {\n        mapper = new CollectThenSystemContentTypeMapper(mimeTypeMap);\n    }\n\n    @Test\n    public void whenExtensionIsRecognized_returnsTypeForFile() {\n        assertEquals(\"audio/amr\", mapper.map(\"file.amr\"));\n        assertEquals(\"audio/ogg\", mapper.map(\"file.oga\"));\n        assertEquals(\"video/ogg\", mapper.map(\"file.ogv\"));\n        assertEquals(\"video/webm\", mapper.map(\"file.webm\"));\n    }\n\n    @Test\n    public void whenExtensionIsNotRecognized_returnsTypeFromAndroid() {\n        Mockito.when(mimeTypeMap.getMimeTypeFromExtension(\"mystery\")).thenReturn(\"text/mystery\");\n        assertEquals(\"text/mystery\", mapper.map(\"file.mystery\"));\n    }\n\n    @Test\n    public void whenExtensionIsNotRecognized_andAndroidDoesNotRecognize_returnsOctetStreamType() {\n        assertEquals(\"application/octet-stream\", mapper.map(\"file.bizarre\"));\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/OpenRosaGetRequestTest.java",
    "content": "package org.odk.collect.openrosa.http;\n\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.nullValue;\nimport static org.junit.Assert.assertThat;\n\nimport org.apache.commons.io.IOUtils;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.odk.collect.openrosa.support.MockWebServerRule;\nimport org.odk.collect.shared.strings.Md5;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.charset.Charset;\nimport java.util.zip.GZIPOutputStream;\n\nimport okhttp3.mockwebserver.MockResponse;\nimport okhttp3.mockwebserver.MockWebServer;\nimport okhttp3.mockwebserver.RecordedRequest;\nimport okio.Buffer;\n\npublic abstract class OpenRosaGetRequestTest {\n\n    protected static final String USER_AGENT = \"Test Agent\";\n\n    protected abstract OpenRosaHttpInterface buildSubject();\n\n    @Rule\n    public MockWebServerRule mockWebServerRule = new MockWebServerRule();\n\n    private MockWebServer mockWebServer;\n    private OpenRosaHttpInterface subject;\n\n    @Before\n    public void setup() throws Exception {\n        subject = buildSubject();\n        mockWebServer = mockWebServerRule.start();\n    }\n\n    @Test\n    public void makesAGetRequestToUri() throws Exception {\n        mockWebServer.enqueue(new MockResponse());\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        subject.executeGetRequest(uri, null, null);\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(1));\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getMethod(), equalTo(\"GET\"));\n        assertThat(request.getRequestUrl().uri(), equalTo(uri));\n    }\n\n    @Test\n    public void sendsCollectHeaders() throws Exception {\n        mockWebServer.enqueue(new MockResponse());\n\n        subject.executeGetRequest(mockWebServer.url(\"\").uri(), null, null);\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"User-Agent\"), equalTo(USER_AGENT));\n    }\n\n    @Test\n    public void returnsBodyWithEmptyHash() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .setBody(\"I AM BODY\"));\n\n        HttpGetResult result = subject.executeGetRequest(mockWebServer.url(\"\").uri(), null, null);\n        assertThat(IOUtils.toString(result.getInputStream(), Charset.defaultCharset()), equalTo(\"I AM BODY\"));\n        assertThat(result.getHash(), equalTo(\"\"));\n    }\n\n    @Test\n    public void whenResponseIsGzipped_returnsBody() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .addHeader(\"Content-Encoding\", \"gzip\")\n                .setBody(new Buffer().write(gzip(\"I AM BODY\"))));\n\n        HttpGetResult result = subject.executeGetRequest(mockWebServer.url(\"\").uri(), null, null);\n        assertThat(IOUtils.toString(result.getInputStream(), Charset.defaultCharset()), equalTo(\"I AM BODY\"));\n    }\n\n    @Test\n    public void whenContentTypeIsXML_returnsBodyWithMD5Hash() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .addHeader(\"Content-Type\", \"text/xml\")\n                .setBody(\"I AM BODY\"));\n\n        HttpGetResult result = subject.executeGetRequest(mockWebServer.url(\"\").uri(), \"text/xml\", null);\n        assertThat(IOUtils.toString(result.getInputStream(), Charset.defaultCharset()), equalTo(\"I AM BODY\"));\n        assertThat(result.getHash(), equalTo(Md5.getMd5Hash(new ByteArrayInputStream(\"I AM BODY\".getBytes()))));\n    }\n\n    @Test(expected = Exception.class)\n    public void withContentType_whenResponseHasDifferentContentType_throwsException() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .addHeader(\"Content-Type\", \"application/json\"));\n\n        subject.executeGetRequest(mockWebServer.url(\"\").uri(), \"text/xml\", null);\n    }\n\n    @Test\n    public void withContentType_whenResponseContainsContentType_returnsResult() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .addHeader(\"Content-Type\", \"application/json; charset=utf-8\")\n                .setBody(\"I AM BODY\"));\n\n        HttpGetResult result = subject.executeGetRequest(mockWebServer.url(\"\").uri(), \"application/json\", null);\n        assertThat(IOUtils.toString(result.getInputStream(), Charset.defaultCharset()), equalTo(\"I AM BODY\"));\n    }\n\n    @Test\n    public void returnsOpenRosaVersion() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .addHeader(OpenRosaConstants.VERSION_HEADER, \"1.0\"));\n\n        HttpGetResult result1 = subject.executeGetRequest(mockWebServer.url(\"\").uri(), null, null);\n        assertThat(result1.isOpenRosaResponse(), equalTo(true));\n\n        mockWebServer.enqueue(new MockResponse());\n\n        HttpGetResult result2 = subject.executeGetRequest(mockWebServer.url(\"\").uri(), null, null);\n        assertThat(result2.isOpenRosaResponse(), equalTo(false));\n    }\n\n    @Test\n    public void whenStatusCodeIsNot200_returnsNullBodyAndStatusCode() throws Exception {\n        mockWebServer.enqueue(new MockResponse().setResponseCode(500));\n\n        HttpGetResult result = subject.executeGetRequest(mockWebServer.url(\"\").uri(), null, null);\n        assertThat(result.getInputStream(), nullValue());\n        assertThat(result.getStatusCode(), equalTo(500));\n    }\n\n    @Test\n    public void whenResponseBodyIsNull_returnsNullBodyAndStatusCode() throws Exception {\n        mockWebServer.enqueue(new MockResponse().setResponseCode(204));\n\n        HttpGetResult result1 = subject.executeGetRequest(mockWebServer.url(\"\").uri(), null, null);\n        assertThat(result1.getInputStream(), nullValue());\n        assertThat(result1.getStatusCode(), equalTo(204));\n\n        mockWebServer.enqueue(new MockResponse().setResponseCode(304));\n\n        HttpGetResult result2 = subject.executeGetRequest(mockWebServer.url(\"\").uri(), null, null);\n        assertThat(result2.getInputStream(), nullValue());\n        assertThat(result2.getStatusCode(), equalTo(304));\n    }\n\n    private static byte[] gzip(String data) throws IOException {\n        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length());\n        GZIPOutputStream gzipStream = new GZIPOutputStream(outputStream);\n        gzipStream.write(data.getBytes());\n        gzipStream.close();\n\n        byte[] compressed = outputStream.toByteArray();\n        outputStream.close();\n\n        return compressed;\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/OpenRosaHeadRequestTest.java",
    "content": "package org.odk.collect.openrosa.http;\n\nimport static org.hamcrest.CoreMatchers.not;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.isEmptyString;\nimport static org.junit.Assert.assertThat;\nimport static org.junit.Assert.assertTrue;\nimport static org.junit.Assert.fail;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.odk.collect.openrosa.support.MockWebServerRule;\n\nimport java.net.URI;\n\nimport okhttp3.mockwebserver.MockResponse;\nimport okhttp3.mockwebserver.MockWebServer;\nimport okhttp3.mockwebserver.RecordedRequest;\n\npublic abstract class OpenRosaHeadRequestTest {\n\n    protected static final String USER_AGENT = \"Test Agent\";\n\n    protected abstract OpenRosaHttpInterface buildSubject();\n\n    @Rule\n    public MockWebServerRule mockWebServerRule = new MockWebServerRule();\n\n    private MockWebServer mockWebServer;\n    private OpenRosaHttpInterface subject;\n\n    @Before\n    public void setup() throws Exception {\n        subject = buildSubject();\n        mockWebServer = mockWebServerRule.start();\n    }\n\n    @Test\n    public void makesAHeadRequestToUri() throws Exception {\n        mockWebServer.enqueue(new MockResponse());\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        subject.executeHeadRequest(uri, null);\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(1));\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getMethod(), equalTo(\"HEAD\"));\n        assertThat(request.getRequestUrl().uri(), equalTo(uri));\n    }\n\n    @Test\n    public void sendsCollectHeaders() throws Exception {\n        mockWebServer.enqueue(new MockResponse());\n\n        subject.executeHeadRequest(mockWebServer.url(\"\").uri(), null);\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"User-Agent\"), equalTo(USER_AGENT));\n    }\n\n    @Test\n    public void when204Response_returnsHeaders() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .setResponseCode(204)\n                .addHeader(\"X-1\", \"Blah1\")\n                .addHeader(\"X-2\", \"Blah2\"));\n\n        HttpHeadResult result = subject.executeHeadRequest(mockWebServer.url(\"\").uri(), null);\n        assertThat(result.getHeaders().getAnyValue(\"X-1\"), equalTo(\"Blah1\"));\n        assertThat(result.getHeaders().getAnyValue(\"X-2\"), equalTo(\"Blah2\"));\n    }\n\n    // Ensure we can look up lower-case headers using mixed-case header names.\n    // https://github.com/getodk/collect/issues/3068\n    @Test\n    public void when204Response_returnsLowerCaseHeaders() throws Exception {\n        String headerLowerCase = \"header-case-test\";\n        mockWebServer.enqueue(new MockResponse()\n                .setResponseCode(204)\n                .addHeader(headerLowerCase, \"value\"));\n\n        String headerMixedCase = \"Header-Case-Test\";\n        HttpHeadResult result = subject.executeHeadRequest(mockWebServer.url(\"\").uri(), null);\n        assertTrue(result.getHeaders().containsHeader(headerMixedCase));\n        assertThat(result.getHeaders().getAnyValue(headerMixedCase), equalTo(\"value\"));\n    }\n\n    @Test\n    public void whenRequestFails_throwsExceptionWithMessage() {\n        try {\n            subject.executeHeadRequest(new URI(\"http://localhost:8443\"), null);\n            fail();\n        } catch (Exception e) {\n            assertThat(e.getMessage(), not(isEmptyString()));\n        }\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/OpenRosaPostRequestTest.java",
    "content": "package org.odk.collect.openrosa.http;\n\nimport static org.hamcrest.CoreMatchers.containsString;\nimport static org.hamcrest.CoreMatchers.not;\nimport static org.hamcrest.CoreMatchers.notNullValue;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.isA;\nimport static org.hamcrest.Matchers.isEmptyString;\nimport static org.junit.Assert.assertThat;\nimport static org.junit.Assert.fail;\nimport static java.util.Arrays.asList;\n\nimport androidx.annotation.NonNull;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.odk.collect.openrosa.support.MockWebServerRule;\n\nimport java.io.BufferedWriter;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.zip.GZIPOutputStream;\n\nimport okhttp3.mockwebserver.MockResponse;\nimport okhttp3.mockwebserver.MockWebServer;\nimport okhttp3.mockwebserver.RecordedRequest;\nimport okio.Buffer;\n\npublic abstract class OpenRosaPostRequestTest {\n\n    protected abstract OpenRosaHttpInterface buildSubject(OpenRosaHttpInterface.FileToContentTypeMapper mapper);\n\n    @Rule\n    public MockWebServerRule mockWebServerRule = new MockWebServerRule();\n\n    private MockWebServer mockWebServer;\n    private OpenRosaHttpInterface subject;\n\n    @Before\n    public void setup() throws Exception {\n        subject = buildSubject(new XmlOrBlahContentTypeMapper());\n        mockWebServer = mockWebServerRule.start();\n    }\n\n    @Test\n    public void makesAPostRequestToUri() throws Exception {\n        mockWebServer.enqueue(new MockResponse().setResponseCode(201));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        subject.uploadSubmissionAndFiles(File.createTempFile(\"blah\", \"blah\"), new ArrayList<>(), uri, null, 0);\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(1));\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getMethod(), equalTo(\"POST\"));\n        assertThat(request.getRequestUrl().uri(), equalTo(uri));\n    }\n\n    @Test\n    public void returnsPostResult() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .setResponseCode(200)\n                .setBody(\"I AM BODY\"));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        HttpPostResult response = subject.uploadSubmissionAndFiles(File.createTempFile(\"blah\", \"blah\"), new ArrayList<>(), uri, null, 0);\n\n        assertThat(response.getResponseCode(), equalTo(200));\n        assertThat(response.getHttpResponse(), equalTo(\"I AM BODY\"));\n    }\n\n    @Test\n    public void whenResponseIsGzipped_returnsBody() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .setResponseCode(200)\n                .addHeader(\"Content-Encoding\", \"gzip\")\n                .setBody(new Buffer().write(gzip(\"I AM BODY\"))));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        HttpPostResult response = subject.uploadSubmissionAndFiles(File.createTempFile(\"blah\", \"blah\"), new ArrayList<>(), uri, null, 0);\n\n        assertThat(response.getHttpResponse(), equalTo(\"I AM BODY\"));\n    }\n\n    @Test(expected = Exception.class)\n    public void whenResponseIs204_throwsException() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .setResponseCode(204));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        subject.uploadSubmissionAndFiles(File.createTempFile(\"blah\", \"blah\"), new ArrayList<>(), uri, null, 0);\n    }\n\n    @Test\n    public void whenThereIsAServerError_returnsPostBody() throws Exception {\n        mockWebServer.enqueue(new MockResponse()\n                .setResponseCode(500)\n                .setBody(\"blah\"));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        HttpPostResult response = subject.uploadSubmissionAndFiles(File.createTempFile(\"blah\", \"blah\"), new ArrayList<>(), uri, null, 0);\n\n        assertThat(response, notNullValue());\n        assertThat(response.getResponseCode(), equalTo(500));\n    }\n\n    @Test\n    public void whenRequestFails_throwsExceptionWithMessage() {\n        try {\n            URI uri = new URI(\"http://localhost:8443\");\n            subject.uploadSubmissionAndFiles(File.createTempFile(\"blah\", \"blah\"), new ArrayList<>(), uri, null, 0);\n            fail();\n        } catch (Exception e) {\n            assertThat(e, isA(Exception.class));\n            assertThat(e.getMessage(), not(isEmptyString()));\n        }\n    }\n\n    @Test\n    public void sendsSubmissionFileAsFirstPartOfBody() throws Exception {\n        mockWebServer.enqueue(new MockResponse().setResponseCode(201));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        String submissionContent = \"<node>content</node>\";\n        File tempFile = createTempFile(submissionContent);\n        subject.uploadSubmissionAndFiles(tempFile, new ArrayList<>(), uri, null, 0);\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        String[] firstPartLines = splitMultiPart(request).get(0);\n        assertThat(firstPartLines[1], containsString(\"name=\\\"xml_submission_file\\\"\"));\n        assertThat(firstPartLines[1], containsString(\"filename=\\\"\" + tempFile.getName() + \"\\\"\"));\n        assertThat(firstPartLines[2], containsString(\"Content-Type: text/xml\"));\n        assertThat(firstPartLines[4], equalTo(\"<node>content</node>\"));\n    }\n\n    @Test\n    public void sendsAttachmentsAsPartsOfBody() throws Exception {\n        mockWebServer.enqueue(new MockResponse().setResponseCode(201));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        File attachment1 = createTempFile(\"blah blah blah\");\n        File attachment2 = createTempFile(\"blah2 blah2 blah2\");\n        subject.uploadSubmissionAndFiles(createTempFile(\"<node>content</node>\"), asList(attachment1, attachment2), uri, null, 1024);\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        List<String[]> parts = splitMultiPart(request);\n\n        String[] secondPartLines = parts.get(1);\n        assertThat(secondPartLines[1], containsString(\"name=\\\"\" + attachment1.getName() + \"\\\"\"));\n        assertThat(secondPartLines[1], containsString(\"filename=\\\"\" + attachment1.getName() + \"\\\"\"));\n        assertThat(secondPartLines[4], equalTo(\"blah blah blah\"));\n\n        String[] thirdPartLines = parts.get(2);\n        assertThat(thirdPartLines[1], containsString(\"name=\\\"\" + attachment2.getName() + \"\\\"\"));\n        assertThat(thirdPartLines[1], containsString(\"filename=\\\"\" + attachment2.getName() + \"\\\"\"));\n        assertThat(thirdPartLines[4], equalTo(\"blah2 blah2 blah2\"));\n    }\n\n    @Test\n    public void sendsAttachmentsAsPartsOfBody_withContentType() throws Exception {\n        mockWebServer.enqueue(new MockResponse().setResponseCode(201));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        File xmlAttachment = createTempFile(\"<node>blah blah blah</node>\", \".xml\");\n        File plainAttachment = createTempFile(\"blah\", \".blah\");\n\n        subject.uploadSubmissionAndFiles(createTempFile(\"<node>content</node>\"), asList(xmlAttachment, plainAttachment), uri, null, 1024);\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        List<String[]> parts = splitMultiPart(request);\n        assertThat(parts.get(1)[2], containsString(\"Content-Type: text/xml\"));\n        assertThat(parts.get(2)[2], containsString(\"Content-Type: text/blah\"));\n    }\n\n    @Test\n    public void whenMoreThanOneAttachment_andRequestIsLargerThanMaxContentLength_sendsTwoRequests() throws Exception {\n        mockWebServer.enqueue(new MockResponse().setResponseCode(201));\n        mockWebServer.enqueue(new MockResponse().setResponseCode(201));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        File attachment1 = createTempFile(\"blah blah blah\");\n        File attachment2 = createTempFile(\"blah2 blah2 blah2\");\n        subject.uploadSubmissionAndFiles(createTempFile(\"<node>content</node>\"), asList(attachment1, attachment2), uri, null, 0);\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        List<String[]> parts = splitMultiPart(request);\n\n        assertThat(parts.size(), equalTo(3));\n\n        String[] secondPartLines = parts.get(1);\n        assertThat(secondPartLines[1], containsString(\"name=\\\"\" + attachment1.getName() + \"\\\"\"));\n        assertThat(secondPartLines[1], containsString(\"filename=\\\"\" + attachment1.getName() + \"\\\"\"));\n        assertThat(secondPartLines[4], equalTo(\"blah blah blah\"));\n\n        String[] thirdPartLines = parts.get(2);\n        assertThat(thirdPartLines[1], containsString(\"name=\\\"*isIncomplete*\\\"\"));\n\n        request = mockWebServer.takeRequest();\n        parts = splitMultiPart(request);\n\n        assertThat(parts.size(), equalTo(2));\n\n        secondPartLines = parts.get(1);\n        assertThat(secondPartLines[1], containsString(\"name=\\\"\" + attachment2.getName() + \"\\\"\"));\n        assertThat(secondPartLines[1], containsString(\"filename=\\\"\" + attachment2.getName() + \"\\\"\"));\n        assertThat(secondPartLines[4], equalTo(\"blah2 blah2 blah2\"));\n    }\n\n    @Test\n    public void whenMoreThanOneAttachment_andRequestIsLargerThanMaxContentLength__andFirstRequestIs500_returnsErrorResult() throws Exception {\n        mockWebServer.enqueue(new MockResponse().setResponseCode(500));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        File attachment1 = createTempFile(\"blah blah blah\");\n        File attachment2 = createTempFile(\"blah2 blah2 blah2\");\n        HttpPostResult response = subject.uploadSubmissionAndFiles(createTempFile(\"<node>content</node>\"), asList(attachment1, attachment2), uri, null, 0);\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(1));\n        assertThat(response, notNullValue());\n        assertThat(response.getResponseCode(), equalTo(500));\n    }\n\n    @Test\n    public void whenMoreThanOneAttachment_andRequestIsLargerThanMaxContentLength_andSecondRequestIs500_returnsErrorResult() throws Exception {\n        mockWebServer.enqueue(new MockResponse().setResponseCode(201));\n        mockWebServer.enqueue(new MockResponse().setResponseCode(500));\n\n        URI uri = mockWebServer.url(\"/blah\").uri();\n        File attachment1 = createTempFile(\"blah blah blah\");\n        File attachment2 = createTempFile(\"blah2 blah2 blah2\");\n        HttpPostResult response = subject.uploadSubmissionAndFiles(createTempFile(\"<node>content</node>\"), asList(attachment1, attachment2), uri, null, 0);\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(2));\n        assertThat(response, notNullValue());\n        assertThat(response.getResponseCode(), equalTo(500));\n    }\n\n    private File createTempFile(String content) throws Exception {\n        return createTempFile(content, \".tmp\");\n    }\n\n    private File createTempFile(String content, String extension) throws Exception {\n        File temp = File.createTempFile(\"tempfile\", extension);\n\n        BufferedWriter bw = new BufferedWriter(new FileWriter(temp));\n        bw.write(content);\n        bw.close();\n\n        return temp;\n    }\n\n    private List<String[]> splitMultiPart(RecordedRequest request) {\n        String body = request.getBody().readUtf8();\n        String boundary = body.split(\"\\r\\n\")[0];\n        String[] split = body.split(boundary);\n        String[] stringParts = Arrays.copyOfRange(split, 1, split.length - 1);\n        return Arrays.stream(stringParts).map(part -> part.split(\"\\r\\n\")).collect(Collectors.toList());\n    }\n\n    private static byte[] gzip(String data) throws IOException {\n        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length());\n        GZIPOutputStream gzipStream = new GZIPOutputStream(outputStream);\n        gzipStream.write(data.getBytes());\n        gzipStream.close();\n\n        byte[] compressed = outputStream.toByteArray();\n        outputStream.close();\n\n        return compressed;\n    }\n\n    private class XmlOrBlahContentTypeMapper implements OpenRosaHttpInterface.FileToContentTypeMapper {\n\n        @NonNull\n        @Override\n        public String map(String fileName) {\n            if (fileName.endsWith(\".xml\")) {\n                return \"text/xml\";\n            } else {\n                return \"text/blah\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/okhttp/OkHttpCaseInsensitiveHeadersTest.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.odk.collect.openrosa.http.CaseInsensitiveHeaders;\n\nimport java.util.Set;\nimport java.util.TreeSet;\n\nimport okhttp3.Headers;\n\npublic class OkHttpCaseInsensitiveHeadersTest {\n    private static CaseInsensitiveHeaders headers;\n\n    @Before\n    public void setup() {\n        headers = buildTestHeaders();\n    }\n\n    @Test\n    public void testMixedCaseHeaderLookup() {\n        Assert.assertTrue(headers.containsHeader(\"Mixed-Case\"));\n        Assert.assertTrue(headers.containsHeader(\"mixed-case\"));\n        Assert.assertTrue(headers.containsHeader(\"MIXED-CASE\"));\n    }\n\n    @Test\n    public void testLowerCaseHeaderLookup() {\n        Assert.assertTrue(headers.containsHeader(\"lower-case\"));\n        Assert.assertTrue(headers.containsHeader(\"Lower-Case\"));\n        Assert.assertTrue(headers.containsHeader(\"LOWER-CASE\"));\n    }\n\n    @Test\n    public void testUpperCaseHeaderLookup() {\n        Assert.assertTrue(headers.containsHeader(\"UPPER-CASE\"));\n        Assert.assertTrue(headers.containsHeader(\"upper-case\"));\n        Assert.assertTrue(headers.containsHeader(\"Upper-Case\"));\n    }\n\n    @Test\n    public void testNullHeaderLookup() {\n        Assert.assertFalse(headers.containsHeader(null));\n    }\n\n    @Test\n    public void testGetAnyForSingleValue() {\n        Assert.assertEquals(1, headers.getValues(\"Mixed-Case\").size());\n        Assert.assertEquals(\"value\", headers.getAnyValue(\"Mixed-Case\"));\n    }\n\n    @Test\n    public void testGetAnyForMultipleValue() {\n        Set<String> values = new TreeSet<>();\n        values.add(\"v1\");\n        values.add(\"v2\");\n        values.add(\"v3\");\n\n        Assert.assertTrue(headers.getValues(\"Collision\").size() > 1);\n        String anyValue = headers.getAnyValue(\"Collision\");\n        Assert.assertTrue(values.contains(anyValue));\n    }\n\n    @Test\n    public void testCaseInsensitiveNameCollisions() {\n        Assert.assertTrue(headers.containsHeader(\"Collision\"));\n        Assert.assertTrue(headers.getValues(\"Collision\").size() > 1);\n    }\n\n    private static OkHttpCaseInsensitiveHeaders buildTestHeaders() {\n        Headers.Builder headerBuilder = new Headers.Builder();\n\n        headerBuilder.add(\"Mixed-Case\", \"value\");\n        headerBuilder.add(\"lower-case\", \"value\");\n        headerBuilder.add(\"UPPER-CASE\", \"value\");\n        headerBuilder.add(\"collision\", \"v1\");\n        headerBuilder.add(\"Collision\", \"v2\");\n        headerBuilder.add(\"COLLISION\", \"v3\");\n\n        return new OkHttpCaseInsensitiveHeaders(headerBuilder.build());\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/okhttp/OkHttpConnectionGetRequestTest.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport android.webkit.MimeTypeMap;\n\nimport org.odk.collect.openrosa.http.CollectThenSystemContentTypeMapper;\nimport org.odk.collect.openrosa.http.OpenRosaGetRequestTest;\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface;\n\npublic class OkHttpConnectionGetRequestTest extends OpenRosaGetRequestTest {\n\n    @Override\n    protected OpenRosaHttpInterface buildSubject() {\n        return new OkHttpConnection(\n                null,\n                new CollectThenSystemContentTypeMapper(MimeTypeMap.getSingleton()),\n                USER_AGENT\n        );\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/okhttp/OkHttpConnectionHeadRequestTest.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport android.webkit.MimeTypeMap;\n\nimport org.odk.collect.openrosa.http.CollectThenSystemContentTypeMapper;\nimport org.odk.collect.openrosa.http.OpenRosaHeadRequestTest;\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface;\n\npublic class OkHttpConnectionHeadRequestTest extends OpenRosaHeadRequestTest {\n\n    @Override\n    protected OpenRosaHttpInterface buildSubject() {\n        return new OkHttpConnection(\n                null,\n                new CollectThenSystemContentTypeMapper(MimeTypeMap.getSingleton()),\n                USER_AGENT\n        );\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/okhttp/OkHttpConnectionPostRequestTest.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport org.odk.collect.openrosa.http.OpenRosaHttpInterface;\nimport org.odk.collect.openrosa.http.OpenRosaPostRequestTest;\n\npublic class OkHttpConnectionPostRequestTest extends OpenRosaPostRequestTest {\n\n    @Override\n    protected OpenRosaHttpInterface buildSubject(OpenRosaHttpInterface.FileToContentTypeMapper mapper) {\n        return new OkHttpConnection(\n                null,\n                mapper,\n                \"Test Agent\"\n        );\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/okhttp/OkHttpOpenRosaServerClientProviderTest.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.not;\n\nimport org.junit.Test;\nimport org.odk.collect.openrosa.http.HttpCredentials;\nimport org.odk.collect.openrosa.support.MockWebServerHelper;\nimport org.odk.collect.shared.TempFiles;\n\nimport java.io.File;\nimport java.util.Date;\n\nimport okhttp3.OkHttpClient;\nimport okhttp3.mockwebserver.MockWebServer;\nimport okhttp3.tls.internal.TlsUtil;\n\npublic class OkHttpOpenRosaServerClientProviderTest extends OpenRosaServerClientProviderTest {\n\n    @Override\n    protected OpenRosaServerClientProvider buildSubject() {\n        return buildSubject(null);\n    }\n\n    private OkHttpOpenRosaServerClientProvider buildSubject(String cacheDir) {\n        OkHttpClient baseClient = new OkHttpClient.Builder()\n                .sslSocketFactory(\n                        TlsUtil.localhost().sslSocketFactory(),\n                        TlsUtil.localhost().trustManager())\n                .build();\n\n        return new OkHttpOpenRosaServerClientProvider(baseClient, cacheDir);\n    }\n\n    @Test\n    public void differentCredentialsHaveDifferentInstances() {\n        OpenRosaServerClientProvider provider = buildSubject();\n\n        OpenRosaServerClient instance1 = provider.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\"));\n        OpenRosaServerClient instance2 = provider.get(\"http\", \"Android\", new HttpCredentials(\"other\", \"pass\"));\n        OpenRosaServerClient instance3 = provider.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\"));\n\n\n        assertThat(instance1, not(equalTo(instance2)));\n        assertThat(instance1, equalTo(instance3));\n    }\n\n    @Test\n    public void whenCacheDirDoesNotExist_doesNotCreateCache() throws Exception {\n        File noneExistingFile = new File(TempFiles.getPathInTempDir());\n        assertThat(noneExistingFile.exists(), equalTo(false));\n\n        MockWebServer mockWebServer = mockWebServerRule.start();\n        enqueueSuccess(mockWebServer);\n\n        OkHttpOpenRosaServerClientProvider provider = buildSubject(noneExistingFile.getAbsolutePath());\n        OpenRosaServerClient client = provider.get(\"http\", \"Android\", new HttpCredentials(\"\", \"\"));\n        client.makeRequest(MockWebServerHelper.buildRequest(mockWebServer, \"\"), new Date());\n\n        assertThat(noneExistingFile.exists(), equalTo(false));\n    }\n\n    @Test\n    public void whenCacheDirIsFile_doesNotCreateCache() throws Exception {\n        File file = new File(TempFiles.getPathInTempDir());\n        file.createNewFile();\n        assertThat(file.exists(), equalTo(true));\n        assertThat(file.isDirectory(), equalTo(false));\n\n        MockWebServer mockWebServer = mockWebServerRule.start();\n        enqueueSuccess(mockWebServer);\n\n        OkHttpOpenRosaServerClientProvider provider = buildSubject(file.getAbsolutePath());\n        OpenRosaServerClient client = provider.get(\"http\", \"Android\", new HttpCredentials(\"\", \"\"));\n        client.makeRequest(MockWebServerHelper.buildRequest(mockWebServer, \"\"), new Date());\n\n        assertThat(file.exists(), equalTo(true));\n        assertThat(file.isDirectory(), equalTo(false));\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/http/okhttp/OpenRosaServerClientProviderTest.java",
    "content": "package org.odk.collect.openrosa.http.okhttp;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.isEmptyOrNullString;\nimport static org.hamcrest.Matchers.notNullValue;\nimport static org.hamcrest.Matchers.startsWith;\nimport static org.odk.collect.openrosa.support.MockWebServerHelper.buildRequest;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.odk.collect.openrosa.http.HttpCredentials;\nimport org.odk.collect.openrosa.http.OpenRosaConstants;\nimport org.odk.collect.openrosa.support.MockWebServerRule;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport java.util.Locale;\nimport java.util.TimeZone;\n\nimport okhttp3.mockwebserver.MockResponse;\nimport okhttp3.mockwebserver.MockWebServer;\nimport okhttp3.mockwebserver.RecordedRequest;\n\npublic abstract class OpenRosaServerClientProviderTest {\n\n    protected abstract OpenRosaServerClientProvider buildSubject();\n\n    private OpenRosaServerClientProvider subject;\n\n    @Rule\n    public MockWebServerRule mockWebServerRule = new MockWebServerRule();\n\n    @Before\n    public void setup() {\n        subject = buildSubject();\n    }\n\n    @Test\n    public void sendsOpenRosaHeaders() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n        enqueueSuccess(mockWebServer);\n\n        OpenRosaServerClient client = subject.get(\"http\", \"Android\", null);\n        client.makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(OpenRosaConstants.VERSION_HEADER), equalTo(\"1.0\"));\n    }\n\n    @Test\n    public void sendsDateHeader() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n        enqueueSuccess(mockWebServer);\n\n        Date currentTime = new Date();\n\n        OpenRosaServerClient client = subject.get(\"http\", \"Android\", null);\n        client.makeRequest(buildRequest(mockWebServer, \"\"), currentTime);\n\n        RecordedRequest request = mockWebServer.takeRequest();\n\n        SimpleDateFormat dateFormatGmt = new SimpleDateFormat(\"E, dd MMM yyyy hh:mm:ss zz\", Locale.US);\n        dateFormatGmt.setTimeZone(TimeZone.getTimeZone(\"GMT\"));\n        assertThat(request.getHeader(\"Date\"), equalTo(dateFormatGmt.format(currentTime)));\n    }\n\n    @Test\n    public void sendsAcceptsGzipHeader() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n        enqueueSuccess(mockWebServer);\n\n        OpenRosaServerClient client = subject.get(\"http\", \"Android\", null);\n        client.makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Accept-Encoding\"), equalTo(\"gzip\"));\n    }\n\n    @Test\n    public void withCredentials_whenBasicChallengeReceived_whenHttp_doesNotRetryWithCredentials() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n        enqueueBasicChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n\n        OpenRosaServerClient client = subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\"));\n        client.makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(1));\n    }\n\n    @Test\n    public void withCredentials_whenBasicChallengeReceived_whenHttps_retriesWithCredentials() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n\n        enqueueBasicChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n\n        OpenRosaServerClient client = subject.get(\"https\", \"Android\", new HttpCredentials(\"user\", \"pass\"));\n        client.makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(2));\n        mockWebServer.takeRequest();\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), equalTo(\"Basic dXNlcjpwYXNz\"));\n    }\n\n    @Test\n    public void withCredentials_whenDigestChallengeReceived_retriesWithCredentials() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n        enqueueDigestChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n\n        OpenRosaServerClient client = subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\"));\n        client.makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(2));\n        mockWebServer.takeRequest();\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), startsWith(\"Digest\"));\n    }\n\n    @Test\n    public void withCredentials_whenDigestChallengeReceived_whenHttps_retriesWithCredentials() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n\n        enqueueDigestChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n\n        OpenRosaServerClient client = subject.get(\"https\", \"Android\", new HttpCredentials(\"user\", \"pass\"));\n        client.makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(2));\n        mockWebServer.takeRequest();\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), startsWith(\"Digest\"));\n    }\n\n    @Test\n    public void withCredentials_onceBasicChallenged_whenHttps_proactivelySendsCredentials() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n\n        enqueueBasicChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n        enqueueSuccess(mockWebServer);\n\n        OpenRosaServerClient client = subject.get(\"https\", \"Android\", new HttpCredentials(\"user\", \"pass\"));\n        client.makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n        client.makeRequest(buildRequest(mockWebServer, \"/different\"), new Date());\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(3));\n        mockWebServer.takeRequest();\n        mockWebServer.takeRequest();\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), equalTo(\"Basic dXNlcjpwYXNz\"));\n    }\n\n    @Test\n    public void withCredentials_onceDigestChallenged_proactivelySendsCredentials() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n\n        enqueueDigestChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n        enqueueSuccess(mockWebServer);\n\n        OpenRosaServerClient client = subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\"));\n        client.makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n        client.makeRequest(buildRequest(mockWebServer, \"/different\"), new Date());\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(3));\n        mockWebServer.takeRequest();\n        mockWebServer.takeRequest();\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), startsWith(\"Digest\"));\n    }\n\n    @Test\n    public void withCredentials_onceDigestChallenged_whenHttps_proactivelySendsCredentials() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n\n        enqueueDigestChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n        enqueueSuccess(mockWebServer);\n\n        OpenRosaServerClient client = subject.get(\"https\", \"Android\", new HttpCredentials(\"user\", \"pass\"));\n        client.makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n        client.makeRequest(buildRequest(mockWebServer, \"/different\"), new Date());\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(3));\n        mockWebServer.takeRequest();\n        mockWebServer.takeRequest();\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), startsWith(\"Digest\"));\n    }\n\n    @Test\n    public void authenticationIsCachedBetweenInstances() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n\n        enqueueDigestChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n        enqueueSuccess(mockWebServer);\n\n        subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\")).makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n        subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\")).makeRequest(buildRequest(mockWebServer, \"/different\"), new Date());\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(3));\n        mockWebServer.takeRequest();\n        mockWebServer.takeRequest();\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), startsWith(\"Digest\"));\n    }\n\n    @Test\n    public void whenUsingDifferentCredentials_authenticationIsNotCachedBetweenInstances() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n\n        enqueueDigestChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n        enqueueDigestChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n\n        subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\")).makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n        subject.get(\"http\", \"Android\", new HttpCredentials(\"new-user\", \"pass\")).makeRequest(buildRequest(mockWebServer, \"/different\"), new Date());\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(4));\n        mockWebServer.takeRequest();\n        mockWebServer.takeRequest();\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), equalTo(null));\n    }\n\n    @Test\n    public void whenUsingNullAndThenNonNullCredentials_authenticationIsNotCachedBetweenInstances() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n\n        enqueueSuccess(mockWebServer);\n        enqueueDigestChallenge(mockWebServer);\n        enqueueSuccess(mockWebServer);\n\n        subject.get(\"http\", \"Android\", null).makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n        subject.get(\"http\", \"Android\", new HttpCredentials(\"new-user\", \"pass\")).makeRequest(buildRequest(mockWebServer, \"/different\"), new Date());\n\n        assertThat(mockWebServer.getRequestCount(), equalTo(3));\n        mockWebServer.takeRequest();\n        mockWebServer.takeRequest();\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), notNullValue());\n    }\n\n    @Test\n    public void whenConnectingToDifferentHosts_authenticationIsNotCachedBetweenInstances() throws Exception {\n        MockWebServer host1 = mockWebServerRule.start();\n        MockWebServer host2 = mockWebServerRule.start();\n\n        enqueueDigestChallenge(host1);\n        enqueueSuccess(host1);\n\n        enqueueDigestChallenge(host2);\n        enqueueSuccess(host2);\n\n        subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\")).makeRequest(buildRequest(host1, \"\"), new Date());\n        subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\")).makeRequest(buildRequest(host2, \"\"), new Date());\n\n        assertThat(host2.getRequestCount(), equalTo(2));\n\n        RecordedRequest request = host2.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), equalTo(null));\n    }\n\n    @Test\n    public void whenUsingHttpsThenHttp_doesNotRespondToBasicAuthChallengesInSecondInstance() throws Exception {\n        MockWebServer host = mockWebServerRule.start();\n\n        enqueueDigestChallenge(host);\n        enqueueSuccess(host);\n\n        enqueueBasicChallenge(host);\n        enqueueSuccess(host);\n\n        subject.get(\"https\", \"Android\", new HttpCredentials(\"user\", \"pass\")).makeRequest(buildRequest(host, \"\"), new Date());\n        subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\")).makeRequest(buildRequest(host, \"\"), new Date());\n\n        assertThat(host.getRequestCount(), equalTo(3));\n\n        host.takeRequest();\n        host.takeRequest();\n        RecordedRequest request = host.takeRequest();\n        assertThat(request.getHeader(\"Authorization\"), equalTo(null));\n    }\n\n    @Test\n    public void whenLastRequestSetCookies_nextRequestDoesNotSendThem() throws Exception {\n        MockWebServer mockWebServer = mockWebServerRule.start();\n\n        mockWebServer.enqueue(new MockResponse()\n                .addHeader(\"Set-Cookie\", \"blah=blah\"));\n        enqueueSuccess(mockWebServer);\n\n        subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\")).makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n        subject.get(\"http\", \"Android\", new HttpCredentials(\"user\", \"pass\")).makeRequest(buildRequest(mockWebServer, \"\"), new Date());\n\n        mockWebServer.takeRequest();\n        RecordedRequest request = mockWebServer.takeRequest();\n        assertThat(request.getHeader(\"Cookie\"), isEmptyOrNullString());\n    }\n\n    protected void enqueueSuccess(MockWebServer mockWebServer) {\n        mockWebServer.enqueue(new MockResponse());\n    }\n\n    private void enqueueBasicChallenge(MockWebServer mockWebServer) {\n        mockWebServer.enqueue(new MockResponse()\n                .setResponseCode(401)\n                .addHeader(\"WWW-Authenticate: Basic realm=\\\"protected area\\\"\")\n                .setBody(\"Please authenticate.\"));\n    }\n\n    private void enqueueDigestChallenge(MockWebServer mockWebServer) {\n        mockWebServer.enqueue(new MockResponse()\n                .setResponseCode(401)\n                .addHeader(\"WWW-Authenticate: Digest realm=\\\"ODK Aggregate\\\", qop=\\\"auth\\\", nonce=\\\"MTU2NTA4MjEzODI4OTpmMjc4MDM5N2YxZTJiNDRiNjNiYTBiMThiOWQ4ZTlkMg==\\\"\")\n                .setBody(\"Please authenticate.\"));\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/parse/Kxml2OpenRosaResponseParserTest.kt",
    "content": "package org.odk.collect.openrosa.parse\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.javarosa.xform.parse.XFormParser\nimport org.junit.Ignore\nimport org.junit.Test\nimport org.kxml2.kdom.Document\nimport org.odk.collect.forms.MediaFile\n\nclass Kxml2OpenRosaResponseParserTest {\n\n    @Test\n    fun `#parseFormList when document is empty, returns null`() {\n        val formList = Kxml2OpenRosaResponseParser.parseFormList(Document())\n        assertThat(formList, equalTo(null))\n    }\n\n    @Test\n    fun `#parseFormList when xform hash is missing prefix, returns null hash for item`() {\n        val response = \"\"\"\n            <?xml version='1.0' encoding='UTF-8' ?>\n            <xforms xmlns=\"http://openrosa.org/xforms/xformsList\">\n                <xform>\n                    <formID>id</formID>\n                    <name>form name</name>\n                    <version>1</version>\n                    <hash>blahblah</hash>\n                    <downloadUrl>http://example.com</downloadUrl>\n                </xform>\n            </xforms>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(response.reader())\n        val formList = Kxml2OpenRosaResponseParser.parseFormList(doc)\n        assertThat(formList!![0].hash, equalTo(null))\n    }\n\n    @Test\n    fun `#parseManifest when media file hash is empty, returns null`() {\n        val response = \"\"\"\n            <?xml version='1.0' encoding='UTF-8' ?>\n            <manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">\n                <mediaFile>\n                    <filename>badger.png</filename>\n                    <hash></hash>\n                    <downloadUrl>http://funk.appspot.com/binaryData?blobKey=%3A477e3</downloadUrl>\n                </mediaFile>\n            </manifest>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(response.reader())\n        val mediaFiles = Kxml2OpenRosaResponseParser.parseManifest(doc)\n        assertThat(mediaFiles, equalTo(null))\n    }\n\n    @Test\n    fun `#parseManifest when document is empty, returns null`() {\n        val formList = Kxml2OpenRosaResponseParser.parseManifest(Document())\n        assertThat(formList, equalTo(null))\n    }\n\n    @Test\n    fun `#parseManifest sanitizes media file names`() {\n        val response = \"\"\"\n            <?xml version='1.0' encoding='UTF-8' ?>\n            <manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">\n                <mediaFile>\n                    <filename>/../badgers.csv</filename>\n                    <hash>blah</hash>\n                    <downloadUrl>http://funk.appspot.com/binaryData?blobKey=%3A477e3</downloadUrl>\n                </mediaFile>\n            </manifest>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(response.reader())\n        val mediaFiles = Kxml2OpenRosaResponseParser.parseManifest(doc)!!\n        assertThat(mediaFiles.size, equalTo(1))\n        assertThat(mediaFiles[0].filename, equalTo(\"badgers.csv\"))\n    }\n\n    @Test\n    fun `#parseManifest when media file has type entityList returns it with entity list type`() {\n        val response = \"\"\"\n            <?xml version='1.0' encoding='UTF-8' ?>\n            <manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">\n                <mediaFile type=\"entityList\">\n                    <filename>badgers.csv</filename>\n                    <hash>blah</hash>\n                    <downloadUrl>http://funk.appspot.com/binaryData?blobKey=%3A477e3</downloadUrl>\n                    <integrityUrl>https://some.server/forms/12/integrity</integrityUrl>\n                </mediaFile>\n            </manifest>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(response.reader())\n        val mediaFiles = Kxml2OpenRosaResponseParser.parseManifest(doc)!!\n        assertThat(mediaFiles.size, equalTo(1))\n        assertThat(mediaFiles[0].type, equalTo(MediaFile.Type.ENTITY_LIST))\n    }\n\n    @Test\n    fun `#parseManifest when media file does not have type returns it with null type`() {\n        val response = \"\"\"\n            <?xml version='1.0' encoding='UTF-8' ?>\n            <manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">\n                <mediaFile type=\"blah\">\n                    <filename>badgers.csv</filename>\n                    <hash>blah</hash>\n                    <downloadUrl>http://funk.appspot.com/binaryData?blobKey=%3A477e3</downloadUrl>\n                </mediaFile>\n            </manifest>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(response.reader())\n        val mediaFiles = Kxml2OpenRosaResponseParser.parseManifest(doc)!!\n        assertThat(mediaFiles.size, equalTo(1))\n        assertThat(mediaFiles[0].type, equalTo(null))\n    }\n\n    @Test\n    fun `#parseManifest when media file has an unrecognized type returns it with null type`() {\n        val response = \"\"\"\n            <?xml version='1.0' encoding='UTF-8' ?>\n            <manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">\n                <mediaFile>\n                    <filename>badgers.csv</filename>\n                    <hash>blah</hash>\n                    <downloadUrl>http://funk.appspot.com/binaryData?blobKey=%3A477e3</downloadUrl>\n                </mediaFile>\n            </manifest>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(response.reader())\n        val mediaFiles = Kxml2OpenRosaResponseParser.parseManifest(doc)!!\n        assertThat(mediaFiles.size, equalTo(1))\n        assertThat(mediaFiles[0].type, equalTo(null))\n    }\n\n    @Test\n    fun `#parseManifest includes integrityUrl when there is one`() {\n        val response = \"\"\"\n            <?xml version='1.0' encoding='UTF-8' ?>\n            <manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">\n                <mediaFile type=\"entityList\">\n                    <filename>badgers.csv</filename>\n                    <hash>blah</hash>\n                    <downloadUrl>http://funk.appspot.com/binaryData?blobKey=%3A477e3</downloadUrl>\n                    <integrityUrl>https://some.server/forms/12/integrity</integrityUrl>\n                </mediaFile>\n            </manifest>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(response.reader())\n        val mediaFiles = Kxml2OpenRosaResponseParser.parseManifest(doc)!!\n        assertThat(mediaFiles.size, equalTo(1))\n        assertThat(mediaFiles[0].integrityUrl, equalTo(\"https://some.server/forms/12/integrity\"))\n    }\n\n    @Test\n    fun `#parseManifest does not include integrityUrl when there isn't one`() {\n        val response = \"\"\"\n            <?xml version='1.0' encoding='UTF-8' ?>\n            <manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">\n                <mediaFile>\n                    <filename>badgers.csv</filename>\n                    <hash>blah</hash>\n                    <downloadUrl>http://funk.appspot.com/binaryData?blobKey=%3A477e3</downloadUrl>\n                </mediaFile>\n            </manifest>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(response.reader())\n        val mediaFiles = Kxml2OpenRosaResponseParser.parseManifest(doc)!!\n        assertThat(mediaFiles.size, equalTo(1))\n        assertThat(mediaFiles[0].integrityUrl, equalTo(null))\n    }\n\n    @Test\n    @Ignore(\"This would break servers that had implemented type before integrityUrl was added to the spec. https://forum.getodk.org/t/openrosa-spec-proposal-support-offline-entities/48052/2\")\n    fun `#parseManifest returns null if a media file with type entityList is missing integrityUrl`() {\n        val response = \"\"\"\n            <?xml version='1.0' encoding='UTF-8' ?>\n            <manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">\n                <mediaFile type=\"entityList\">\n                    <filename>badgers.csv</filename>\n                    <hash>blah</hash>\n                    <downloadUrl>http://funk.appspot.com/binaryData?blobKey=%3A477e3</downloadUrl>\n                </mediaFile>\n            </manifest>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(response.reader())\n        assertThat(Kxml2OpenRosaResponseParser.parseManifest(doc), equalTo(null))\n    }\n\n    @Test\n    fun `#parseIntegrityResponse returns null when the response structure is incorrect`() {\n        val noData = \"\"\"\n            <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n            <blah>\n                <entities>\n                    <entity id=\"958e00b2-43f5-4d21-8adb-3d27aaa045f5\">\n                        <deleted>true</deleted>\n                    </entity>\n                </entities>\n            </blah>\n        \"\"\".trimIndent()\n\n        val noEntities = \"\"\"\n            <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n            <data>\n                <stuff>\n                    <entity id=\"958e00b2-43f5-4d21-8adb-3d27aaa045f5\">\n                        <deleted>true</deleted>\n                    </entity>\n                </stuff>\n            </data>\n        \"\"\".trimIndent()\n\n        val noDeleted = \"\"\"\n            <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n            <data>\n                <entities>\n                    <entity id=\"958e00b2-43f5-4d21-8adb-3d27aaa045f5\">\n                        <blah>true</blah>\n                    </entity>\n                </entities>\n            </data>\n        \"\"\".trimIndent()\n\n        val emptyDeleted = \"\"\"\n            <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n            <data>\n                <entities>\n                    <entity id=\"958e00b2-43f5-4d21-8adb-3d27aaa045f5\">\n                        <deleted></deleted>\n                    </entity>\n                </entities>\n            </data>\n        \"\"\".trimIndent()\n\n        val invalidDeleted = \"\"\"\n            <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n            <data>\n                <entities>\n                    <entity id=\"958e00b2-43f5-4d21-8adb-3d27aaa045f5\">\n                        <deleted>yes</deleted>\n                    </entity>\n                </entities>\n            </data>\n        \"\"\".trimIndent()\n\n        listOf(noData, noEntities, noDeleted, emptyDeleted, invalidDeleted).forEach {\n            val doc = XFormParser.getXMLDocument(it.reader())\n            val response = Kxml2OpenRosaResponseParser.parseIntegrityResponse(doc)\n            assertThat(\"The following should not be parsed:\\n$it\\n\\n\", response, equalTo(null))\n        }\n    }\n\n    @Test\n    fun `#parseIntegrityResponse returns list of deleted entities when the response contains multiple entities`() {\n        val integrityDoc = \"\"\"\n            <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n            <data>\n                <entities>\n                    <entity id=\"1\">\n                        <deleted>true</deleted>\n                    </entity>\n                    <entity id=\"2\">\n                        <deleted>true</deleted>\n                    </entity>\n                </entities>\n            </data>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(integrityDoc.reader())\n        val response = Kxml2OpenRosaResponseParser.parseIntegrityResponse(doc)!!\n\n        assertThat(response.size, equalTo(2))\n        assertThat(response[0].id, equalTo(\"1\"))\n        assertThat(response[0].deleted, equalTo(true))\n        assertThat(response[1].id, equalTo(\"2\"))\n        assertThat(response[1].deleted, equalTo(true))\n    }\n\n    @Test\n    fun `#parseIntegrityResponse returns an empty list if the response has no entities`() {\n        val integrityDoc = \"\"\"\n            <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n            <data>\n                <entities>\n\n                </entities>\n            </data>\n        \"\"\".trimIndent()\n\n        val doc = XFormParser.getXMLDocument(integrityDoc.reader())\n        val response = Kxml2OpenRosaResponseParser.parseIntegrityResponse(doc)!!\n\n        assertThat(response.isEmpty(), equalTo(true))\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/support/MockWebServerHelper.java",
    "content": "package org.odk.collect.openrosa.support;\n\nimport okhttp3.Request;\nimport okhttp3.mockwebserver.MockWebServer;\n\npublic final class MockWebServerHelper {\n\n    private MockWebServerHelper() {\n\n    }\n\n    public static Request buildRequest(MockWebServer mockWebServer, String path) {\n        return new Request.Builder().url(mockWebServer.url(path)).build();\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/support/MockWebServerRule.kt",
    "content": "package org.odk.collect.openrosa.support\n\nimport okhttp3.mockwebserver.MockWebServer\nimport org.junit.rules.TestRule\nimport org.junit.runner.Description\nimport org.junit.runners.model.Statement\n\nclass MockWebServerRule : TestRule {\n\n    private val mockWebServers: MutableList<MockWebServer> = ArrayList()\n\n    override fun apply(base: Statement, description: Description): Statement =\n        object : Statement() {\n            override fun evaluate() {\n                try {\n                    base.evaluate()\n                } finally {\n                    teardown()\n                }\n            }\n        }\n\n    fun start(): MockWebServer {\n        val mockWebServer = MockWebServer()\n        mockWebServers.add(mockWebServer)\n\n        mockWebServer.start()\n        return mockWebServer\n    }\n\n    fun teardown() {\n        for (mockWebServer in mockWebServers) {\n            mockWebServer.close()\n        }\n    }\n}\n"
  },
  {
    "path": "open-rosa/src/test/java/org/odk/collect/openrosa/support/StubWebCredentialsProvider.java",
    "content": "package org.odk.collect.openrosa.support;\n\nimport androidx.annotation.NonNull;\n\nimport org.odk.collect.openrosa.forms.OpenRosaXmlFetcher;\nimport org.odk.collect.openrosa.http.HttpCredentials;\nimport org.odk.collect.openrosa.http.HttpCredentialsInterface;\n\nimport java.net.URI;\n\npublic class StubWebCredentialsProvider implements OpenRosaXmlFetcher.WebCredentialsProvider {\n\n    @Override\n    public HttpCredentialsInterface getCredentials(@NonNull URI url) {\n        return new HttpCredentials(null, null);\n    }\n}\n"
  },
  {
    "path": "osmdroid/.gitignore",
    "content": "/build"
  },
  {
    "path": "osmdroid/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    namespace = \"org.odk.collect.osmdroid\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":shared\"))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":icons\"))\n    implementation(project(\":maps\"))\n    implementation(project(\":location\"))\n    implementation(project(\":settings\"))\n    implementation(project(\":strings\"))\n\n    implementation(libs.osmdroid)\n    implementation(libs.androidxFragmentKtx)\n    implementation(libs.androidxPreferenceKtx)\n    implementation(libs.timber)\n    implementation(libs.playServicesLocation)\n    implementation(libs.androidMaterial)\n    implementation(libs.dagger)\n    ksp(libs.daggerCompiler)\n}\n"
  },
  {
    "path": "osmdroid/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "osmdroid/src/main/java/org/odk/collect/osmdroid/DaggerSetup.kt",
    "content": "package org.odk.collect.osmdroid\n\nimport dagger.Component\nimport dagger.Module\nimport dagger.Provides\nimport org.odk.collect.location.LocationClient\nimport org.odk.collect.maps.MapConfigurator\nimport org.odk.collect.maps.layers.ReferenceLayerRepository\nimport org.odk.collect.settings.SettingsProvider\nimport javax.inject.Singleton\n\ninterface OsmDroidDependencyComponentProvider {\n    val osmDroidDependencyComponent: OsmDroidDependencyComponent\n}\n\n@Component(modules = [OsmDroidDependencyModule::class])\n@Singleton\ninterface OsmDroidDependencyComponent {\n    fun inject(osmDroidMapFragment: OsmDroidMapFragment)\n}\n\n@Module\nopen class OsmDroidDependencyModule {\n\n    @Provides\n    open fun providesReferenceLayerRepository(): ReferenceLayerRepository {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesLocationClient(): LocationClient {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesMapConfigurator(): MapConfigurator {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    open fun providesSettingsProvider(): SettingsProvider {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n}\n"
  },
  {
    "path": "osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidInitializer.kt",
    "content": "package org.odk.collect.osmdroid\n\nimport org.osmdroid.config.Configuration\n\nobject OsmDroidInitializer {\n\n    fun initialize(userAgent: String) {\n        Configuration.getInstance().userAgentValue = userAgent\n    }\n}\n"
  },
  {
    "path": "osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapConfigurator.java",
    "content": "package org.odk.collect.osmdroid;\n\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_REFERENCE_LAYER;\nimport static kotlin.collections.SetsKt.setOf;\n\nimport android.content.Context;\nimport android.os.Bundle;\n\nimport androidx.preference.Preference;\n\nimport org.odk.collect.androidshared.ui.PrefUtils;\nimport org.odk.collect.maps.MapConfigurator;\nimport org.odk.collect.maps.layers.MbtilesFile;\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.io.File;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class OsmDroidMapConfigurator implements MapConfigurator {\n    private final String prefKey;\n    private final int sourceLabelId;\n    private final WmsOption[] options;\n\n    /** Constructs a configurator that renders just one Web Map Service. */\n    public OsmDroidMapConfigurator(WebMapService service) {\n        prefKey = \"\";\n        sourceLabelId = 0;\n        options = new WmsOption[] {new WmsOption(\"\", 0, service)};\n    }\n\n    /**\n     * Constructs a configurator that offers a few Web Map Services to choose from.\n     * The choice of which Web Map Service will be stored in a string preference.\n     */\n    public OsmDroidMapConfigurator(String prefKey, int sourceLabelId, WmsOption... options) {\n        this.prefKey = prefKey;\n        this.sourceLabelId = sourceLabelId;\n        this.options = options;\n    }\n\n    @Override public boolean isAvailable(Context context) {\n        // OSMdroid is always supported, as far as we know.\n        return true;\n    }\n\n    @Override public void showUnavailableMessage(Context context) { }\n\n    @Override public List<Preference> createPrefs(Context context, Settings settings) {\n        if (options.length > 1) {\n            int[] labelIds = new int[options.length];\n            String[] values = new String[options.length];\n            for (int i = 0; i < options.length; i++) {\n                labelIds[i] = options[i].labelId;\n                values[i] = options[i].id;\n            }\n            String prefTitle = context.getString(\n                org.odk.collect.strings.R.string.map_style_label, context.getString(sourceLabelId));\n            return Collections.singletonList(PrefUtils.createListPref(\n                context, prefKey, prefTitle, labelIds, values, settings\n            ));\n        }\n        return Collections.emptyList();\n    }\n\n    @Override public Collection<String> getPrefKeys() {\n        return prefKey.isEmpty() ? setOf(KEY_REFERENCE_LAYER) : setOf(prefKey, KEY_REFERENCE_LAYER);\n    }\n\n    @Override public Bundle buildConfig(Settings prefs) {\n        Bundle config = new Bundle();\n        if (options.length == 1) {\n            config.putSerializable(OsmDroidMapFragment.KEY_WEB_MAP_SERVICE, options[0].service);\n        } else {\n            String value = prefs.getString(prefKey);\n            for (int i = 0; i < options.length; i++) {\n                if (options[i].id.equals(value)) {\n                    config.putSerializable(OsmDroidMapFragment.KEY_WEB_MAP_SERVICE, options[i].service);\n                }\n            }\n        }\n        config.putString(OsmDroidMapFragment.KEY_REFERENCE_LAYER,\n            prefs.getString(KEY_REFERENCE_LAYER));\n        return config;\n    }\n\n    @Override public boolean supportsLayer(File file) {\n        // OSMdroid supports only raster tiles.\n        return MbtilesFile.readLayerType(file) == MbtilesFile.LayerType.RASTER;\n    }\n\n    @Override public String getDisplayName(File file) {\n        String name = MbtilesFile.readName(file);\n        return name != null ? name : file.getName();\n    }\n\n    public static class WmsOption {\n        final String id;\n        final int labelId;\n        final WebMapService service;\n\n        public WmsOption(String id, int labelId, WebMapService service) {\n            this.id = id;\n            this.labelId = labelId;\n            this.service = service;\n        }\n    }\n}\n"
  },
  {
    "path": "osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java",
    "content": "/*\n * Copyright (C) 2018 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage org.odk.collect.osmdroid;\n\nimport static androidx.core.graphics.drawable.BitmapDrawableKt.toDrawable;\nimport static androidx.core.graphics.drawable.DrawableKt.toBitmap;\nimport static org.odk.collect.maps.traces.TraceDescriptionKt.getMarkersForPoints;\nimport static org.odk.collect.maps.markers.MarkerIconCreator.toBitmap;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.drawable.Drawable;\nimport android.location.Location;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.core.content.ContextCompat;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\n\nimport com.google.android.gms.location.LocationListener;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.odk.collect.androidshared.system.ContextExt;\nimport org.odk.collect.location.LocationClient;\nimport org.odk.collect.maps.circles.CircleDescription;\nimport org.odk.collect.maps.traces.LineDescription;\nimport org.odk.collect.maps.MapConfigurator;\nimport org.odk.collect.maps.MapFragment;\nimport org.odk.collect.maps.MapPoint;\nimport org.odk.collect.maps.MapViewModel;\nimport org.odk.collect.maps.MapViewModelMapFragment;\nimport org.odk.collect.maps.traces.PolygonDescription;\nimport org.odk.collect.maps.Zoom;\nimport org.odk.collect.maps.ZoomObserver;\nimport org.odk.collect.maps.layers.MapFragmentReferenceLayerUtils;\nimport org.odk.collect.maps.layers.ReferenceLayerRepository;\nimport org.odk.collect.maps.markers.MarkerDescription;\nimport org.odk.collect.maps.markers.MarkerIconCreator;\nimport org.odk.collect.maps.markers.MarkerIconDescription;\nimport org.odk.collect.settings.SettingsProvider;\nimport org.osmdroid.api.IGeoPoint;\nimport org.osmdroid.events.MapListener;\nimport org.osmdroid.events.ScrollEvent;\nimport org.osmdroid.events.ZoomEvent;\nimport org.osmdroid.tileprovider.IRegisterReceiver;\nimport org.osmdroid.util.BoundingBox;\nimport org.osmdroid.util.GeoPoint;\nimport org.osmdroid.views.CustomZoomButtonsController;\nimport org.osmdroid.views.MapView;\nimport org.osmdroid.views.overlay.MapEventsOverlay;\nimport org.osmdroid.views.overlay.Marker;\nimport org.osmdroid.views.overlay.Overlay;\nimport org.osmdroid.views.overlay.Polygon;\nimport org.osmdroid.views.overlay.Polyline;\nimport org.osmdroid.views.overlay.ScaleBarOverlay;\nimport org.osmdroid.views.overlay.TilesOverlay;\nimport org.osmdroid.views.overlay.mylocation.IMyLocationConsumer;\nimport org.osmdroid.views.overlay.mylocation.IMyLocationProvider;\nimport org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport java.util.stream.StreamSupport;\n\nimport javax.inject.Inject;\n\nimport timber.log.Timber;\n\n/**\n * A MapFragment drawn by OSMDroid.\n */\npublic class OsmDroidMapFragment extends MapViewModelMapFragment implements\n        LocationListener, LocationClient.LocationClientListener {\n\n    // Bundle keys understood by applyConfig().\n    public static final String KEY_WEB_MAP_SERVICE = \"WEB_MAP_SERVICE\";\n\n    @Inject\n    ReferenceLayerRepository referenceLayerRepository;\n\n    @Inject\n    LocationClient locationClient;\n\n    @Inject\n    MapConfigurator mapConfigurator;\n\n    @Inject\n    SettingsProvider settingsProvider;\n\n    private MapView map;\n    private ReadyListener readyListener;\n    private PointListener clickListener;\n    private PointListener longPressListener;\n    private PointListener gpsLocationListener;\n    private FeatureListener featureClickListener;\n    private FeatureListener dragEndListener;\n    private MyLocationNewOverlay myLocationOverlay;\n    private OsmLocationClientWrapper osmLocationClientWrapper;\n    private int nextFeatureId = 1;\n    private final Map<Integer, MapFeature> features = new HashMap<>();\n    private boolean clientWantsLocationUpdates;\n    private IGeoPoint lastMapCenter;\n    private WebMapService webMapService;\n    private File referenceLayerFile;\n    private TilesOverlay referenceOverlay;\n    private boolean isSystemZooming;\n    private MapViewModel mapViewModel;\n\n    @Override\n    public void init(@Nullable ReadyListener readyListener, @Nullable ErrorListener errorListener) {\n        this.readyListener = readyListener;\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        OsmDroidDependencyComponent component = ((OsmDroidDependencyComponentProvider) context.getApplicationContext()).getOsmDroidDependencyComponent();\n        component.inject(this);\n\n        ViewModelProvider.Factory viewModelFactory = new ViewModelProvider.Factory() {\n            @NonNull\n            @Override\n            public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {\n                return (T) new MapViewModel(\n                        settingsProvider.getUnprotectedSettings(),\n                        settingsProvider.getMetaSettings()\n                );\n            }\n        };\n\n        mapViewModel = new ViewModelProvider(this, viewModelFactory).get(MapViewModel.class);\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        enableLocationUpdates(clientWantsLocationUpdates);\n    }\n\n    @Override\n    public void onPause() {\n        super.onPause();\n        enableLocationUpdates(false);\n    }\n\n    @Override\n    public void onDestroy() {\n        clearFeatures();  // prevent a memory leak due to refs held by markers\n        MarkerIconCreator.clearCache();\n        super.onDestroy();\n    }\n\n    @Override\n    public View onCreateView(@NonNull LayoutInflater inflater,\n                             @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        View view = inflater.inflate(R.layout.osm_map_layout, container, false);\n        map = view.findViewById(R.id.osm_map_view);\n        if (webMapService != null) {\n            map.setTileSource(webMapService.asOnlineTileSource());\n        }\n        map.setMultiTouchControls(true);\n        map.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);\n        map.setMinZoomLevel(2.0);\n        map.getController().setCenter(toGeoPoint(MapFragment.Companion.getINITIAL_CENTER()));\n        map.getController().setZoom((int) INITIAL_ZOOM);\n        map.setTilesScaledToDpi(true);\n        map.setFlingEnabled(false);\n        map.getOverlays().add(new ScaleBarOverlay(map));\n        map.addMapListener(new MapListener() {\n\n            private boolean initialized;\n\n            @Override\n            public boolean onZoom(ZoomEvent event) {\n                if (!isSystemZooming) {\n                    mapViewModel.onUserZoom(getCenter(), event.getZoomLevel());\n                }\n                return false;\n            }\n\n            @Override\n            public boolean onScroll(ScrollEvent event) {\n                // Ignore initial scroll event that we get when the map loads\n                if (initialized) {\n                    mapViewModel.onUserMove(getCenter(), event.getSource().getZoomLevelDouble());\n                } else {\n                    initialized = true;\n                }\n\n                return false;\n            }\n        });\n        addAttributionAndMapEventsOverlays();\n        loadReferenceOverlay();\n        addMapLayoutChangeListener(map);\n        osmLocationClientWrapper = new OsmLocationClientWrapper(locationClient);\n        myLocationOverlay = new MyLocationNewOverlay(osmLocationClientWrapper, map);\n        myLocationOverlay.setDrawAccuracyEnabled(true);\n        Drawable drawable = ContextCompat.getDrawable(requireActivity(), org.odk.collect.maps.R.drawable.ic_crosshairs);\n        Bitmap crosshairs = toBitmap(drawable, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), null);\n        myLocationOverlay.setDirectionArrow(crosshairs, crosshairs);\n        myLocationOverlay.setPersonHotspot(crosshairs.getWidth() / 2.0f, crosshairs.getHeight() / 2.0f);\n\n        new Handler().postDelayed(() -> {\n            // If the screen is rotated before the map is ready, this fragment\n            // could already be detached, which makes it unsafe to use.  Only\n            // call the ReadyListener if this fragment is still attached.\n            if (readyListener != null && getActivity() != null) {\n                readyListener.onReady(this);\n            }\n        }, 100);\n\n        mapViewModel.getSettings(mapConfigurator.getPrefKeys()).observe(getViewLifecycleOwner(), settings -> {\n            Bundle newConfig = mapConfigurator.buildConfig(settings);\n            onConfigChanged(newConfig);\n        });\n\n        mapViewModel.getZoom().observe(getViewLifecycleOwner(), new ZoomObserver() {\n            @Override\n            public void onZoomToPoint(@NonNull Zoom.Point zoom) {\n                isSystemZooming = true;\n                map.getController().setZoom((int) Math.round(zoom.getLevel()));\n                map.getController().setCenter(toGeoPoint(zoom.getPoint()));\n                isSystemZooming = false;\n            }\n\n            @Override\n            public void onZoomToBox(@NonNull Zoom.Box zoom) {\n                List<MapPoint> points = zoom.getBox();\n                boolean animate = zoom.getAnimate();\n                Double scaleFactor = zoom.getScaleFactor();\n\n                int count = 0;\n                List<GeoPoint> geoPoints = new ArrayList<>();\n                MapPoint lastPoint = null;\n                for (MapPoint point : points) {\n                    lastPoint = point;\n                    geoPoints.add(toGeoPoint(point));\n                    count++;\n                }\n                if (count == 1) {\n                    zoomToPoint(lastPoint, zoom.getAnimate());\n                } else if (count > 1) {\n                    // TODO(ping): Find a better solution.\n                    // zoomToBoundingBox sometimes fails to zoom correctly, either\n                    // zooming by the correct amount but leaving the bounding box\n                    // off-center, or centering correctly but not zooming in enough.\n                    // Adding a 100-ms delay avoids the problem most of the time, but\n                    // not always; it's here because the old GeoShapeOsmMapActivity\n                    // did it, not because it's known to be the best solution.\n                    final BoundingBox box = BoundingBox.fromGeoPoints(geoPoints)\n                            .increaseByScale((float) (1 / scaleFactor));\n                    new Handler().postDelayed(() -> {\n                        isSystemZooming = true;\n                        map.zoomToBoundingBox(box, animate);\n                        isSystemZooming = false;\n                    }, 100);\n                }\n            }\n        });\n\n        return view;\n    }\n\n    @Override\n    public @NonNull\n    MapPoint getCenter() {\n        return fromGeoPoint(map.getMapCenter());\n    }\n\n    @Override\n    public double getZoom() {\n        return map.getZoomLevel();\n    }\n\n    @Override\n    public int addMarker(MarkerDescription markerDescription) {\n        int featureId = nextFeatureId++;\n        return addMarker(featureId, markerDescription);\n    }\n\n    private int addMarker(int featureId, MarkerDescription markerDescription) {\n        features.put(featureId, new MarkerFeature(map, markerDescription));\n        return featureId;\n    }\n\n    @Override\n    public List<Integer> addMarkers(List<MarkerDescription> markers) {\n        List<Integer> featureIds = new ArrayList<>();\n        for (MarkerDescription markerDescription : markers) {\n            int featureId = addMarker(markerDescription);\n            featureIds.add(featureId);\n        }\n\n        return featureIds;\n    }\n\n    @Override\n    public void setMarkerIcon(int featureId, MarkerIconDescription markerIconDescription) {\n        MapFeature feature = features.get(featureId);\n        if (feature instanceof MarkerFeature) {\n            ((MarkerFeature) feature).setIcon(markerIconDescription);\n            map.invalidate();\n        }\n    }\n\n    @Override\n    public @Nullable\n    MapPoint getMarkerPoint(int featureId) {\n        MapFeature feature = features.get(featureId);\n        return feature instanceof MarkerFeature ? ((MarkerFeature) feature).getPoint() : null;\n    }\n\n    @Override\n    public int addPolyLine(LineDescription lineDescription) {\n        int featureId = nextFeatureId++;\n        addPolyLine(featureId, lineDescription);\n        return featureId;\n    }\n\n    private void addPolyLine(int featureId, LineDescription lineDescription) {\n        if (lineDescription.getDraggable()) {\n            features.put(featureId, new DynamicPolyLineFeature(map, lineDescription));\n        } else {\n            features.put(featureId, new StaticPolyLineFeature(map, lineDescription));\n        }\n    }\n\n    @Override\n    public void updatePolyLine(int featureId, @NotNull LineDescription lineDescription) {\n        features.get(featureId).dispose();\n        addPolyLine(featureId, lineDescription);\n    }\n\n    @Override\n    public int addPolygon(PolygonDescription polygonDescription) {\n        int featureId = nextFeatureId++;\n        addPolygon(featureId, polygonDescription);\n        return featureId;\n    }\n\n    private void addPolygon(int featureId, PolygonDescription polygonDescription) {\n        if (polygonDescription.getDraggable()) {\n            features.put(featureId, new DynamicPolygonFeature(map, polygonDescription));\n        } else {\n            features.put(featureId, new StaticPolygonFeature(map, polygonDescription));\n        }\n    }\n\n    @Override\n    public void updatePolygon(int featureId, @NotNull PolygonDescription polygonDescription) {\n        features.get(featureId).dispose();\n        addPolygon(featureId, polygonDescription);\n    }\n\n    @Override\n    public @NonNull\n    List<MapPoint> getPolyPoints(int featureId) {\n        MapFeature feature = features.get(featureId);\n        if (feature instanceof LineFeature) {\n            return ((LineFeature) feature).getPoints();\n        }\n        return new ArrayList<>();\n    }\n\n    @Override\n    public void clearFeatures() {\n        for (MapFeature feature : features.values()) {\n            feature.dispose();\n        }\n        map.invalidate();\n        features.clear();\n        nextFeatureId = 1;\n    }\n\n    @Override\n    public void setClickListener(@Nullable PointListener listener) {\n        clickListener = listener;\n    }\n\n    @Override\n    public void setLongPressListener(@Nullable PointListener listener) {\n        longPressListener = listener;\n    }\n\n    @Override\n    public void setFeatureClickListener(@Nullable FeatureListener listener) {\n        featureClickListener = listener;\n    }\n\n    @Override\n    public void setDragEndListener(@Nullable FeatureListener listener) {\n        dragEndListener = listener;\n    }\n\n    @Override\n    public void setGpsLocationListener(@Nullable PointListener listener) {\n        gpsLocationListener = listener;\n    }\n\n    @Override\n    public void setRetainMockAccuracy(boolean retainMockAccuracy) {\n        locationClient.setRetainMockAccuracy(retainMockAccuracy);\n    }\n\n    @Override\n    public void setGpsLocationEnabled(boolean enable) {\n        if (enable != clientWantsLocationUpdates) {\n            clientWantsLocationUpdates = enable;\n            enableLocationUpdates(clientWantsLocationUpdates);\n        }\n    }\n\n    @Override\n    public @Nullable\n    MapPoint getGpsLocation() {\n        return fromLocation(myLocationOverlay);\n    }\n\n    @Override\n    public void onLocationChanged(Location location) {\n        Timber.i(\"onLocationChanged: location = %s\", location);\n        if (gpsLocationListener != null) {\n            MapPoint point = fromLocation(myLocationOverlay);\n            if (point != null) {\n                gpsLocationListener.onPoint(point);\n            }\n        }\n\n        if (myLocationOverlay != null) {\n            myLocationOverlay.onLocationChanged(location, osmLocationClientWrapper);\n        }\n    }\n\n    @Override\n    public void onClientStart() {\n        map.getOverlays().add(myLocationOverlay);\n        myLocationOverlay.setEnabled(true);\n        myLocationOverlay.enableMyLocation();\n\n        Timber.i(\"Requesting location updates (to %s)\", this);\n        locationClient.requestLocationUpdates(this);\n    }\n\n    @Override\n    public void onClientStartFailure() {\n    }\n\n    @Override\n    public void onClientStop() {\n    }\n\n    private void enableLocationUpdates(boolean enable) {\n        if (enable) {\n            Timber.i(\"Starting LocationClient %s (for MapFragment %s)\", locationClient, this);\n            locationClient.start(this);\n        } else {\n            Timber.i(\"Stopping LocationClient %s (for MapFragment %s)\", locationClient, this);\n            locationClient.stop();\n            myLocationOverlay.setEnabled(false);\n            safelyDisableOverlayLocationFollowing();\n        }\n    }\n\n    /**\n     * <a href=\"https://github.com/osmdroid/osmdroid/issues/1783\">\n     * https://github.com/osmdroid/osmdroid/issues/1783\n     * </a>\n     **/\n    private void safelyDisableOverlayLocationFollowing() {\n        if (map.isAttachedToWindow()) {\n            myLocationOverlay.disableFollowLocation();\n            myLocationOverlay.disableMyLocation();\n        }\n    }\n\n    private static @Nullable\n    MapPoint fromLocation(@NonNull MyLocationNewOverlay overlay) {\n        GeoPoint geoPoint = overlay.getMyLocation();\n        if (geoPoint == null) {\n            return null;\n        }\n        return new MapPoint(\n                geoPoint.getLatitude(), geoPoint.getLongitude(),\n                geoPoint.getAltitude(), overlay.getLastFix().getAccuracy()\n        );\n    }\n\n    private static @NonNull\n    MapPoint fromGeoPoint(@NonNull IGeoPoint geoPoint) {\n        return new MapPoint(geoPoint.getLatitude(), geoPoint.getLongitude());\n    }\n\n    private static @NonNull\n    MapPoint fromGeoPoint(@NonNull GeoPoint geoPoint) {\n        return new MapPoint(geoPoint.getLatitude(), geoPoint.getLongitude(), geoPoint.getAltitude());\n    }\n\n    private static @NonNull\n    MapPoint fromMarker(@NonNull Marker marker) {\n        GeoPoint geoPoint = marker.getPosition();\n        double sd = 0;\n        try {\n            sd = Double.parseDouble(marker.getSubDescription());\n        } catch (NumberFormatException e) {\n            Timber.w(\"Marker.getSubDescription() did not contain a number\");\n        }\n        return new MapPoint(\n                geoPoint.getLatitude(), geoPoint.getLongitude(), geoPoint.getAltitude(), sd\n        );\n    }\n\n    private static @NonNull\n    GeoPoint toGeoPoint(@NonNull MapPoint point) {\n        return new GeoPoint(point.latitude, point.longitude, point.altitude);\n    }\n\n    /**\n     * Updates the map to reflect the value of referenceLayerFile.\n     */\n    private void loadReferenceOverlay() {\n        if (referenceOverlay != null) {\n            map.getOverlays().remove(referenceOverlay);\n            referenceOverlay.onDetach(map);\n            referenceOverlay = null;\n        }\n        if (referenceLayerFile != null) {\n            OsmMBTileProvider mbprovider = new OsmMBTileProvider(new RegisterReceiver(requireActivity()), referenceLayerFile);\n            referenceOverlay = new TilesOverlay(mbprovider, getContext());\n            referenceOverlay.setLoadingBackgroundColor(Color.TRANSPARENT);\n            map.getOverlays().add(0, referenceOverlay);\n        }\n        map.invalidate();\n    }\n\n    /**\n     * Adds a listener that keeps track of the map center, and another\n     * listener that restores the map center when the MapView's layout changes.\n     * We have to do this because the MapView is buggy and fails to preserve its\n     * view on a layout change, causing the map viewport to jump around when the\n     * screen is resized or rotated in a way that doesn't restart the activity.\n     */\n    private void addMapLayoutChangeListener(MapView map) {\n        lastMapCenter = map.getMapCenter();\n        map.setMapListener(new MapListener() {\n            @Override\n            public boolean onScroll(ScrollEvent event) {\n                lastMapCenter = map.getMapCenter();\n                return false;\n            }\n\n            @Override\n            public boolean onZoom(ZoomEvent event) {\n                lastMapCenter = map.getMapCenter();\n                return false;\n            }\n        });\n        map.addOnLayoutChangeListener(\n                (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->\n                        map.getController().setCenter(lastMapCenter));\n    }\n\n    private Marker createMarker(MapView map, MarkerDescription markerDescription) {\n        // A Marker's position is a GeoPoint with latitude, longitude, and\n        // altitude fields.  We need to store the standard deviation value\n        // somewhere, so it goes in the marker's sub-description field.\n        Marker marker = new Marker(map);\n        marker.setPosition(toGeoPoint(markerDescription.getPoint()));\n        marker.setSubDescription(Double.toString(markerDescription.getPoint().accuracy));\n        marker.setDraggable(markerDescription.isDraggable());\n        Bitmap iconBitmap = toBitmap(markerDescription.getIconDescription(), requireContext());\n        marker.setIcon(toDrawable(iconBitmap, requireContext().getResources()));\n        marker.setAnchor(getIconAnchorValueX(markerDescription.getIconAnchor()), getIconAnchorValueY(markerDescription.getIconAnchor()));\n        marker.setOnMarkerClickListener((clickedMarker, mapView) -> {\n            int featureId = findFeature(clickedMarker);\n            if (featureClickListener != null && featureId != -1) {\n                featureClickListener.onFeature(featureId);\n                return true;  // consume the event\n            }\n            return false;\n        });\n        marker.setOnMarkerDragListener(new Marker.OnMarkerDragListener() {\n            @Override\n            public void onMarkerDragStart(Marker marker) {\n            }\n\n            @Override\n            public void onMarkerDrag(Marker marker) {\n                // When a marker is manually dragged, the position is no longer\n                // obtained from a GPS reading, so the standard deviation field\n                // is no longer meaningful; reset it to zero.\n                marker.setSubDescription(\"0\");\n                updateFeature(findFeature(marker));\n            }\n\n            @Override\n            public void onMarkerDragEnd(Marker marker) {\n                int featureId = findFeature(marker);\n                updateFeature(featureId);\n                if (dragEndListener != null && featureId != -1) {\n                    dragEndListener.onFeature(featureId);\n                }\n            }\n        });\n\n        map.getOverlays().add(marker);\n        return marker;\n    }\n\n    private float getIconAnchorValueX(MapFragment.IconAnchor iconAnchor) {\n        switch (iconAnchor) {\n            case BOTTOM:\n            default:\n                return Marker.ANCHOR_CENTER;\n        }\n    }\n\n    private float getIconAnchorValueY(MapFragment.IconAnchor iconAnchor) {\n        switch (iconAnchor) {\n            case BOTTOM:\n                return Marker.ANCHOR_BOTTOM;\n            default:\n                return Marker.ANCHOR_CENTER;\n        }\n    }\n\n    /**\n     * Finds the feature to which the given marker belongs.\n     */\n    private int findFeature(Marker marker) {\n        for (int featureId : features.keySet()) {\n            if (features.get(featureId).ownsMarker(marker)) {\n                return featureId;\n            }\n        }\n        return -1;  // not found\n    }\n\n    /**\n     * Finds the feature to which the given polyline belongs.\n     */\n    private int findFeature(Polyline polyline) {\n        for (int featureId : features.keySet()) {\n            if (features.get(featureId).ownsPolyline(polyline)) {\n                return featureId;\n            }\n        }\n        return -1;  // not found\n    }\n\n    private int findFeature(Polygon polygon) {\n        for (int featureId : features.keySet()) {\n            if (features.get(featureId).ownsPolygon(polygon)) {\n                return featureId;\n            }\n        }\n        return -1;\n    }\n\n    private void updateFeature(int featureId) {\n        MapFeature feature = features.get(featureId);\n        if (feature != null) {\n            feature.update();\n        }\n    }\n\n    private void addAttributionAndMapEventsOverlays() {\n        map.getOverlays().add(new AttributionOverlay(getContext()));\n        map.getOverlays().add(\n                new MapEventsOverlay(\n                        new MapEventsReceiver(\n                                point -> {\n                                    if (clickListener != null) {\n                                        clickListener.onPoint(point);\n                                    }\n                                },\n                                point -> {\n                                    if (longPressListener != null) {\n                                        longPressListener.onPoint(point);\n                                    }\n                                }\n                        )\n                )\n        );\n    }\n\n    private void onConfigChanged(Bundle config) {\n        webMapService = (WebMapService) config.getSerializable(KEY_WEB_MAP_SERVICE);\n        referenceLayerFile = MapFragmentReferenceLayerUtils.getReferenceLayerFile(config, referenceLayerRepository);\n        if (map != null) {\n            map.setTileSource(webMapService.asOnlineTileSource());\n            loadReferenceOverlay();\n        }\n    }\n\n    @NonNull\n    @Override\n    public MapViewModel getMapViewModel() {\n        return mapViewModel;\n    }\n\n    @Override\n    public void updateMarker(int featureId, @NotNull MarkerDescription markerDescription) {\n        features.get(featureId).dispose();\n        addMarker(featureId, markerDescription);\n    }\n\n    @Override\n    public int addCircle(@NotNull CircleDescription circleDescription) {\n        return -1;\n    }\n\n    @Override\n    public void updateCircle(int featureId, @NotNull CircleDescription circleDescription) {\n\n    }\n\n    /**\n     * A MapFeature is a physical feature on a map, such as a point, a road,\n     * a building, a region, etc.  It is presented to the user as one editable\n     * object, though its appearance may be constructed from multiple overlays\n     * (e.g. geometric elements, handles for manipulation, etc.).\n     */\n    interface MapFeature {\n        /**\n         * Returns true if the given marker belongs to this feature.\n         */\n        boolean ownsMarker(Marker marker);\n\n        /**\n         * Returns true if the given polyline belongs to this feature.\n         */\n        boolean ownsPolyline(Polyline polyline);\n\n        boolean ownsPolygon(Polygon polygon);\n\n        /**\n         * Updates the feature's geometry after any UI handles have moved.\n         */\n        void update();\n\n        /**\n         * Removes the feature from the map, leaving it no longer usable.\n         */\n        void dispose();\n    }\n\n    /**\n     * A marker that can optionally be dragged by the user.\n     */\n    private class MarkerFeature implements MapFeature {\n        final MapView map;\n        Marker marker;\n\n        MarkerFeature(MapView map, MarkerDescription markerDescription) {\n            this.map = map;\n            this.marker = createMarker(map, markerDescription);\n        }\n\n        public void setIcon(MarkerIconDescription markerIconDescription) {\n            Context context = requireContext();\n            Bitmap bitmap = toBitmap(markerIconDescription, context);\n            Drawable drawable = toDrawable(bitmap, context.getResources());\n\n            marker.setIcon(drawable);\n        }\n\n        public MapPoint getPoint() {\n            return fromMarker(marker);\n        }\n\n        public boolean ownsMarker(Marker givenMarker) {\n            return marker.equals(givenMarker);\n        }\n\n        public boolean ownsPolyline(Polyline polyline) {\n            return false;\n        }\n\n        @Override\n        public boolean ownsPolygon(Polygon polygon) {\n            return false;\n        }\n\n        public void update() {\n        }\n\n        public void dispose() {\n            map.getOverlays().remove(marker);\n            marker = null;\n        }\n    }\n\n    private interface LineFeature extends MapFeature {\n\n        List<MapPoint> getPoints();\n    }\n\n    private class StaticPolyLineFeature implements LineFeature {\n        final MapView map;\n        final Polyline polyline;\n        private final List<MapPoint> points;\n\n        StaticPolyLineFeature(MapView map, LineDescription lineDescription) {\n            this.map = map;\n            polyline = new Polyline();\n            polyline.setColor(lineDescription.getStrokeColor());\n            polyline.setOnClickListener((clickedPolyline, mapView, eventPos) -> {\n                int featureId = findFeature(clickedPolyline);\n                if (featureClickListener != null && featureId != -1) {\n                    featureClickListener.onFeature(featureId);\n                    return true;  // consume the event\n                }\n                return false;\n            });\n            Paint paint = polyline.getPaint();\n            paint.setStrokeWidth(lineDescription.getStrokeWidth());\n            map.getOverlays().add(polyline);\n\n            points = lineDescription.getPoints();\n            List<GeoPoint> geoPoints = StreamSupport.stream(points.spliterator(), false).map(mapPoint -> new GeoPoint(mapPoint.latitude, mapPoint.longitude, mapPoint.altitude)).collect(Collectors.toList());\n            polyline.setPoints(geoPoints);\n            map.invalidate();\n        }\n\n        @Override\n        public boolean ownsMarker(Marker givenMarker) {\n            return false;\n        }\n\n        @Override\n        public boolean ownsPolyline(Polyline givenPolyline) {\n            return polyline.equals(givenPolyline);\n        }\n\n        @Override\n        public boolean ownsPolygon(Polygon polygon) {\n            return false;\n        }\n\n        @Override\n        public void update() {\n        }\n\n        @Override\n        public void dispose() {\n            map.getOverlays().remove(polyline);\n        }\n\n        @Override\n        public List<MapPoint> getPoints() {\n            return points;\n        }\n    }\n\n    private class DynamicPolyLineFeature implements LineFeature {\n        final MapView map;\n        final List<Marker> markers = new ArrayList<>();\n        final Polyline polyline;\n\n        DynamicPolyLineFeature(MapView map, LineDescription lineDescription) {\n            this.map = map;\n            polyline = new Polyline();\n            polyline.setColor(lineDescription.getStrokeColor());\n            polyline.setOnClickListener((clickedPolyline, mapView, eventPos) -> {\n                int featureId = findFeature(clickedPolyline);\n                if (featureClickListener != null && featureId != -1) {\n                    featureClickListener.onFeature(featureId);\n                    return true;  // consume the event\n                }\n                return false;\n            });\n            Paint paint = polyline.getPaint();\n            paint.setStrokeWidth(lineDescription.getStrokeWidth());\n            map.getOverlays().add(polyline);\n\n            List<MarkerDescription> markerDescriptions = getMarkersForPoints(lineDescription);\n            for (MarkerDescription markerDescription : markerDescriptions) {\n                markers.add(createMarker(map, markerDescription));\n            }\n            update();\n        }\n\n        @Override\n        public boolean ownsMarker(Marker givenMarker) {\n            return markers.contains(givenMarker);\n        }\n\n        @Override\n        public boolean ownsPolyline(Polyline givenPolyline) {\n            return polyline.equals(givenPolyline);\n        }\n\n        @Override\n        public boolean ownsPolygon(Polygon polygon) {\n            return false;\n        }\n\n        @Override\n        public void update() {\n            List<GeoPoint> geoPoints = new ArrayList<>();\n            for (Marker marker : markers) {\n                geoPoints.add(marker.getPosition());\n            }\n\n            polyline.setPoints(geoPoints);\n            map.invalidate();\n        }\n\n        @Override\n        public void dispose() {\n            for (Marker marker : markers) {\n                map.getOverlays().remove(marker);\n            }\n            markers.clear();\n            map.getOverlays().remove(polyline);\n        }\n\n        @Override\n        public List<MapPoint> getPoints() {\n            List<MapPoint> points = new ArrayList<>();\n            for (Marker marker : markers) {\n                points.add(fromMarker(marker));\n            }\n            return points;\n        }\n    }\n\n    private class DynamicPolygonFeature implements LineFeature {\n\n        final MapView map;\n        final List<Marker> markers = new ArrayList<>();\n        final Polygon polygon;\n\n        DynamicPolygonFeature(MapView map, PolygonDescription polygonDescription) {\n            this.map = map;\n            polygon = new Polygon();\n            polygon.setStrokeColor(polygonDescription.getStrokeColor());\n            polygon.setStrokeWidth(polygonDescription.getStrokeWidth());\n            polygon.getFillPaint().setColor(polygonDescription.getFillColor());\n            polygon.setOnClickListener((clickedPolygon, mapView, eventPos) -> {\n                int featureId = findFeature(clickedPolygon);\n                if (featureClickListener != null && featureId != -1) {\n                    featureClickListener.onFeature(featureId);\n                    return true;  // consume the event\n                }\n                return false;\n            });\n\n            map.getOverlays().add(polygon);\n\n            List<MarkerDescription> markerDescriptions = getMarkersForPoints(polygonDescription);\n            for (MarkerDescription markerDescription : markerDescriptions) {\n                markers.add(createMarker(map, markerDescription));\n            }\n            update();\n        }\n\n        @Override\n        public boolean ownsMarker(Marker givenMarker) {\n            return markers.contains(givenMarker);\n        }\n\n        @Override\n        public boolean ownsPolyline(Polyline other) {\n            return false;\n        }\n\n        @Override\n        public boolean ownsPolygon(Polygon other) {\n            return polygon.equals(other);\n        }\n\n        @Override\n        public void update() {\n            List<GeoPoint> geoPoints = new ArrayList<>();\n            for (Marker marker : markers) {\n                geoPoints.add(marker.getPosition());\n            }\n\n            polygon.setPoints(geoPoints);\n            map.invalidate();\n        }\n\n        @Override\n        public void dispose() {\n            for (Marker marker : markers) {\n                map.getOverlays().remove(marker);\n            }\n            markers.clear();\n            map.getOverlays().remove(polygon);\n        }\n\n        @Override\n        public List<MapPoint> getPoints() {\n            List<MapPoint> points = new ArrayList<>();\n            for (Marker marker : markers) {\n                points.add(fromMarker(marker));\n            }\n            return points;\n        }\n    }\n\n    private class StaticPolygonFeature implements LineFeature {\n        private final MapView map;\n        @NonNull\n        private final PolygonDescription polygonDescription;\n        private final Polygon polygon = new Polygon();\n\n        StaticPolygonFeature(MapView map, PolygonDescription polygonDescription) {\n            this.map = map;\n            this.polygonDescription = polygonDescription;\n\n            map.getOverlays().add(polygon);\n            polygon.getOutlinePaint().setColor(polygonDescription.getStrokeColor());\n            polygon.setStrokeWidth(polygonDescription.getStrokeWidth());\n            polygon.getFillPaint().setColor(polygonDescription.getFillColor());\n            polygon.setPoints(StreamSupport.stream(polygonDescription.getPoints().spliterator(), false).map(point -> new GeoPoint(point.latitude, point.longitude)).collect(Collectors.toList()));\n            polygon.setOnClickListener((polygon, mapView, eventPos) -> {\n                int featureId = findFeature(polygon);\n                if (featureClickListener != null && featureId != -1) {\n                    featureClickListener.onFeature(featureId);\n                    return true;  // consume the event\n                }\n\n                return false;\n            });\n        }\n\n        @Override\n        public boolean ownsMarker(Marker marker) {\n            return false;\n        }\n\n        @Override\n        public boolean ownsPolyline(Polyline polyline) {\n            return false;\n        }\n\n        @Override\n        public boolean ownsPolygon(Polygon polygon) {\n            return polygon.equals(this.polygon);\n        }\n\n        @Override\n        public void update() {\n        }\n\n        @Override\n        public void dispose() {\n            map.getOverlays().remove(polygon);\n        }\n\n        @Override\n        public List<MapPoint> getPoints() {\n            return polygonDescription.getPoints();\n        }\n    }\n\n    /**\n     * An overlay that draws an attribution message in the lower-right corner.\n     */\n    private static class AttributionOverlay extends Overlay {\n        public static final int FONT_SIZE_DP = 12;\n        public static final int MARGIN_DP = 10;\n\n        private final Paint paint;\n\n        AttributionOverlay(Context context) {\n            super();\n\n            paint = new Paint();\n            paint.setAntiAlias(true);\n            paint.setColor(ContextExt.getThemeAttributeValue(context, com.google.android.material.R.attr.colorOnSurface));\n            paint.setTextSize(FONT_SIZE_DP *\n                    context.getResources().getDisplayMetrics().density);\n            paint.setTextAlign(Paint.Align.RIGHT);\n        }\n\n        @Override\n        public void draw(Canvas canvas, MapView map, boolean shadow) {\n            String attribution = map.getTileProvider().getTileSource().getCopyrightNotice();\n            if (!shadow && !map.isAnimating() && attribution != null && !attribution.isEmpty()) {\n                String[] lines = attribution.split(\"\\n\");\n                float lineHeight = paint.getFontSpacing();\n                float x = canvas.getWidth() - MARGIN_DP;\n                float y = canvas.getHeight() - MARGIN_DP - lineHeight * lines.length;\n\n                canvas.save();\n                canvas.concat(map.getProjection().getInvertedScaleRotateCanvasMatrix());\n                for (String line : lines) {\n                    y += lineHeight;\n                    canvas.drawText(line, x, y, paint);\n                }\n                canvas.restore();\n            }\n        }\n    }\n\n    private static class OsmLocationClientWrapper implements IMyLocationProvider {\n        private LocationClient locationClient;\n\n        OsmLocationClientWrapper(LocationClient locationClient) {\n            this.locationClient = locationClient;\n        }\n\n        @Override\n        public boolean startLocationProvider(IMyLocationConsumer myLocationConsumer) {\n            // locationClient.start launches async work and we need to be confident that\n            // getLastKnownLocation is never called before onClientStart so we don't let the OSM\n            // location overlay start the provider. We also ignore the location consumer passed in\n            // and instead explicitly forward location updates to the overlay from onLocationChanged\n            return true;\n        }\n\n        @Override\n        public void stopLocationProvider() {\n            locationClient.stop();\n        }\n\n        @Override\n        public Location getLastKnownLocation() {\n            return locationClient.getLastLocation();\n        }\n\n        @Override\n        public void destroy() {\n            locationClient.stop();\n            locationClient = null;\n        }\n    }\n\n    private static class RegisterReceiver implements IRegisterReceiver {\n\n        private final Context context;\n\n        RegisterReceiver(Context context) {\n            this.context = context;\n        }\n\n        @Override\n        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {\n            return context != null ? context.registerReceiver(receiver, filter) : null;\n        }\n\n        @Override\n        public void unregisterReceiver(BroadcastReceiver receiver) {\n            if (context != null) {\n                context.unregisterReceiver(receiver);\n            }\n        }\n\n        @Override\n        public void destroy() {\n        }\n    }\n\n    private static class MapEventsReceiver implements org.osmdroid.events.MapEventsReceiver {\n\n        private final PointListener clickListener;\n        private final PointListener longPressListener;\n\n        MapEventsReceiver(PointListener clickListener, PointListener longPressListener) {\n            this.clickListener = clickListener;\n            this.longPressListener = longPressListener;\n        }\n\n        @Override\n        public boolean singleTapConfirmedHelper(GeoPoint geoPoint) {\n            if (clickListener != null) {\n                clickListener.onPoint(fromGeoPoint(geoPoint));\n                return true;\n            }\n            return false;\n        }\n\n        @Override\n        public boolean longPressHelper(GeoPoint geoPoint) {\n            if (longPressListener != null) {\n                longPressListener.onPoint(fromGeoPoint(geoPoint));\n                return true;\n            }\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "osmdroid/src/main/java/org/odk/collect/osmdroid/OsmMBTileModuleProvider.java",
    "content": "/*\n * Copyright (C) 2014 GeoODK\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\n/**\n * @author Jon Nordling (jonnordling@gmail.com)\n */\n\npackage org.odk.collect.osmdroid;\n\nimport android.graphics.drawable.Drawable;\n\nimport org.osmdroid.config.Configuration;\nimport org.osmdroid.tileprovider.IRegisterReceiver;\nimport org.osmdroid.tileprovider.modules.MapTileFileStorageProviderBase;\nimport org.osmdroid.tileprovider.modules.MapTileModuleProviderBase;\nimport org.osmdroid.tileprovider.tilesource.ITileSource;\nimport org.osmdroid.tileprovider.util.StreamUtils;\n\nimport java.io.InputStream;\n\nimport timber.log.Timber;\n\nclass OsmMBTileModuleProvider extends MapTileFileStorageProviderBase {\n\n    protected OsmMBTileSource tileSource;\n\n    OsmMBTileModuleProvider(IRegisterReceiver receiverRegistrar, OsmMBTileSource tileSource) {\n\n        // Call the super constructor\n        super(receiverRegistrar,\n                Configuration.getInstance().getTileFileSystemThreads(),\n                Configuration.getInstance().getTileFileSystemMaxQueueSize());\n\n        // Initialize fields\n        this.tileSource = tileSource;\n\n    }\n\n    @Override\n    protected String getName() {\n        return \"MBTiles File Archive Provider\";\n    }\n\n    @Override\n    protected String getThreadGroupName() {\n        return \"mbtilesarchive\";\n    }\n\n    @Override\n    public MapTileModuleProviderBase.TileLoader getTileLoader() {\n        return new TileLoader();\n    }\n\n    @Override\n    public boolean getUsesDataConnection() {\n        return false;\n    }\n\n    @Override\n    public int getMinimumZoomLevel() {\n        return tileSource.getMinimumZoomLevel();\n    }\n\n    @Override\n    public int getMaximumZoomLevel() {\n        return tileSource.getMaximumZoomLevel();\n    }\n\n    @Override\n    public void setTileSource(ITileSource tileSource) {\n        Timber.w(\"*** Warning: someone's trying to reassign MBTileModuleProvider's tileSource!\");\n        if (tileSource instanceof OsmMBTileSource) {\n            this.tileSource = (OsmMBTileSource) tileSource;\n        } else {\n        // logger.warn(\"*** Warning: and it wasn't even an MBTileSource! That's just rude!\");\n\n        }\n    }\n\n    private class TileLoader extends MapTileModuleProviderBase.TileLoader {\n\n        @Override\n        public Drawable loadTile(long mapTileIndex) {\n            InputStream inputStream = null;\n\n            try {\n                inputStream = tileSource.getInputStream(mapTileIndex);\n\n                if (inputStream != null) {\n\n                    // Note that the finally clause will be called before\n                    // the value is returned!\n                    return tileSource.getDrawable(inputStream);\n                }\n\n            } catch (Throwable e) {\n                Timber.e(e, \"Error loading tile\");\n\n            } finally {\n                if (inputStream != null) {\n                    StreamUtils.closeStream(inputStream);\n                }\n            }\n\n            return null;\n        }\n    }\n\n}\n"
  },
  {
    "path": "osmdroid/src/main/java/org/odk/collect/osmdroid/OsmMBTileProvider.java",
    "content": "/*\n * Copyright (C) 2014 GeoODK\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\n/**\n * @author Jon Nordling (jonnordling@gmail.com)\n */\n\npackage org.odk.collect.osmdroid;\n\nimport org.osmdroid.tileprovider.IRegisterReceiver;\nimport org.osmdroid.tileprovider.MapTileProviderArray;\nimport org.osmdroid.tileprovider.modules.MapTileModuleProviderBase;\n\nimport java.io.File;\nimport java.util.Collections;\n\n/**\n * This class is a simplification of the the MapTileProviderArray: it only\n * allows a single provider.\n */\nclass OsmMBTileProvider extends MapTileProviderArray {\n\n    OsmMBTileProvider(IRegisterReceiver receiverRegistrar, File file) {\n\n        /**\n         * Call the super-constructor.\n         *\n         * MapTileProviderBase requires a TileSource. As far as I can tell it is\n         * only used in its method rescaleCache(...) to get the pixel size of a\n         * tile. It seems to me that this is inappropriate, as a MapTileProvider\n         * can have multiple sources (like the module array defined below) and\n         * therefore multiple tileSources which might return different values!!\n         *\n         * If the requirement is that the tile size is equal across tile\n         * sources, then the parameter should be obtained from a different\n         * location, From TileSystem for example.\n         */\n        super(OsmMBTileSource.createFromFile(file), receiverRegistrar);\n\n        // Create the module provider; this class provides a TileLoader that\n        // actually loads the tile from the DB.\n        OsmMBTileModuleProvider moduleProvider;\n        moduleProvider = new OsmMBTileModuleProvider(receiverRegistrar, (OsmMBTileSource) getTileSource());\n\n        MapTileModuleProviderBase[] tileProviderArray;\n        tileProviderArray = new MapTileModuleProviderBase[]{moduleProvider};\n\n        // Add the module provider to the array of providers; mTileProviderList\n        // is defined by the superclass.\n        Collections.addAll(mTileProviderList, tileProviderArray);\n    }\n\n    // TODO: implement public Drawable getMapTile(final MapTile pTile) {}\n    //       The current implementation is needlessly complex because it uses\n    //       MapTileProviderArray as a basis.\n\n}\n"
  },
  {
    "path": "osmdroid/src/main/java/org/odk/collect/osmdroid/OsmMBTileSource.java",
    "content": "/*\n * Copyright (C) 2014 GeoODK\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\n/**\n * @author Jon Nordling (jonnordling@gmail.com)\n */\n\npackage org.odk.collect.osmdroid;\n\nimport android.database.Cursor;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\n\nimport org.osmdroid.tileprovider.tilesource.BitmapTileSourceBase;\nimport org.osmdroid.util.MapTileIndex;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.InputStream;\n\nimport timber.log.Timber;\n\nclass OsmMBTileSource extends BitmapTileSourceBase {\n\n    // Log log log log ...\n    // private static final Logger logger = LoggerFactory.getLogger(MBTileSource.class);\n    // Database related fields\n    public static final String TABLE_TILES = \"tiles\";\n    public static final String COL_TILES_ZOOM_LEVEL = \"zoom_level\";\n    public static final String COL_TILES_TILE_COLUMN = \"tile_column\";\n    public static final String COL_TILES_TILE_ROW = \"tile_row\";\n    public static final String COL_TILES_TILE_DATA = \"tile_data\";\n\n    protected SQLiteDatabase database;\n\n    // Reasonable defaults ..\n    public static final int MIN_ZOOM = 8;\n    public static final int MAX_ZOOM = 15;\n    public static final int TILE_SIZE_PIXELS = 256;\n\n    /**\n     * The reason this constructor is protected is because all parameters,\n     * except file should be determined from the archive file. Therefore a\n     * factory method is necessary.\n     */\n    protected OsmMBTileSource(int minZoom,\n                              int maxZoom,\n                              int tileSizePixels,\n                              SQLiteDatabase db) {\n        super(\"MBTiles\", minZoom, maxZoom, tileSizePixels, \".png\");\n\n        database = db;\n    }\n\n    /**\n     * Creates a new MBTileSource from file.\n     * <p>\n     * Parameters minZoom, maxZoom en tileSizePixels are obtained from the\n     * database. If they cannot be obtained from the DB, the default values as\n     * defined by this class are used.\n     */\n    public static OsmMBTileSource createFromFile(File file) {\n        int flags = SQLiteDatabase.NO_LOCALIZED_COLLATORS | SQLiteDatabase.OPEN_READONLY;\n        int tileSize = TILE_SIZE_PIXELS;\n\n        // Open the database\n        SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, flags);\n\n        // Get the tile size\n        Cursor cursor = db.rawQuery(\"SELECT tile_data FROM tiles LIMIT 0,1\",\n                new String[]{});\n\n        if (cursor.getCount() != 0) {\n            cursor.moveToFirst();\n            InputStream is = new ByteArrayInputStream(cursor.getBlob(0));\n\n            Bitmap bitmap = BitmapFactory.decodeStream(is);\n            if (bitmap != null) {\n                tileSize = bitmap.getHeight();\n            }\n            Timber.w(\"Found a tile size of %d\", tileSize);\n        }\n        cursor.close();\n\n        // Get the minimum zoomlevel from the MBTiles file\n        int value = getInt(db, \"SELECT MIN(zoom_level) FROM tiles;\");\n        int minZoomLevel = value > -1 ? value : MIN_ZOOM;\n\n        // Get the maximum zoomlevel from the MBTiles file\n        value = getInt(db, \"SELECT MAX(zoom_level) FROM tiles;\");\n        int maxZoomLevel = value > -1 ? value : MAX_ZOOM;\n\n        return new OsmMBTileSource(minZoomLevel, maxZoomLevel, tileSize, db);\n    }\n\n    protected static int getInt(SQLiteDatabase db, String sql) {\n        Cursor cursor = db.rawQuery(sql, new String[]{});\n        int value = -1;\n\n        if (cursor.getCount() != 0) {\n            cursor.moveToFirst();\n            value = cursor.getInt(0);\n            Timber.e(new Error(\"Found a minimum zoomlevel of \" + value));\n        }\n\n        cursor.close();\n        return value;\n    }\n\n    public InputStream getInputStream(long tileIndex) {\n\n        try {\n            InputStream ret = null;\n            final String[] tile = {COL_TILES_TILE_DATA};\n            final String[] xyz = {Integer.toString(MapTileIndex.getX(tileIndex)),\n                    Double.toString(Math.pow(2, MapTileIndex.getZoom(tileIndex)) - MapTileIndex.getY(tileIndex) - 1),\n                    Integer.toString(MapTileIndex.getZoom(tileIndex))};\n\n            final Cursor cur = database.query(TABLE_TILES,\n                    tile,\n                    \"tile_column=? and tile_row=? and zoom_level=?\",\n                    xyz,\n                    null,\n                    null,\n                    null);\n\n            if (cur.getCount() != 0) {\n                cur.moveToFirst();\n                ret = new ByteArrayInputStream(cur.getBlob(0));\n            }\n\n            cur.close();\n\n            if (ret != null) {\n                return ret;\n            }\n\n        } catch (final Throwable e) {\n            Timber.w(e, \"Error getting db stream: %s\", tileIndex);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "osmdroid/src/main/java/org/odk/collect/osmdroid/WebMapService.java",
    "content": "package org.odk.collect.osmdroid;\n\nimport org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase;\nimport org.osmdroid.util.MapTileIndex;\n\nimport java.io.Serializable;\n\n/**\n * A serializable definition of a Web Map Service in terms of its URL structure\n * and the parameters for fetching tiles from it.\n */\npublic class WebMapService implements Serializable {\n    public final String cacheName;\n    public final int minZoomLevel;\n    public final int maxZoomLevel;\n    public final int tileSize;\n    public final String copyright;\n    public final String[] urlTemplates;\n\n    private OnlineTileSourceBase onlineTileSource;\n\n    public WebMapService(String cacheName, int minZoomLevel, int maxZoomLevel,\n                         int tileSize, String copyright, String... urlTemplates) {\n        this.cacheName = cacheName;\n        this.minZoomLevel = minZoomLevel;\n        this.maxZoomLevel = maxZoomLevel;\n        this.tileSize = tileSize;\n        this.copyright = copyright;\n        this.urlTemplates = urlTemplates;\n    }\n\n    // Note: org.osmdroid.views.MapView.setTileSource takes an ITileSource,\n    // but really it requires an instance of OnlineTileSourceBase.\n    public OnlineTileSourceBase asOnlineTileSource() {\n        if (onlineTileSource == null) {\n            String extension = getExtension(urlTemplates[0]);\n            onlineTileSource = new OnlineTileSourceBase(cacheName, minZoomLevel,\n                maxZoomLevel, tileSize, extension, urlTemplates, copyright) {\n                public String getTileURLString(long tileIndex) {\n                    String urlTemplate = urlTemplates[random.nextInt(urlTemplates.length)];\n                    return urlTemplate.replace(\"{x}\", String.valueOf(MapTileIndex.getX(tileIndex)))\n                        .replace(\"{y}\", String.valueOf(MapTileIndex.getY(tileIndex)))\n                        .replace(\"{z}\", String.valueOf(MapTileIndex.getZoom(tileIndex)));\n                }\n            };\n        }\n        return onlineTileSource;\n    }\n\n    private String getExtension(String urlTemplate) {\n        String[] parts = urlTemplate.split(\"/\");\n        String lastPart = parts[parts.length - 1];\n        if (lastPart.contains(\".\")) {\n            String[] subparts = lastPart.split(\"\\\\.\");\n            return \".\" + subparts[subparts.length - 1];\n        }\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "osmdroid/src/main/res/layout/osm_map_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<org.osmdroid.views.MapView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/osm_map_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"/>"
  },
  {
    "path": "permissions/.gitignore",
    "content": "/build"
  },
  {
    "path": "permissions/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n    namespace = \"org.odk.collect.permissions\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":strings\"))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":icons\"))\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.androidxFragmentKtx)\n    implementation(libs.androidMaterial)\n    implementation(libs.karumiDexter)\n    implementation(libs.timber)\n\n    debugImplementation(project(\":fragments-test\"))\n\n    testImplementation(project(\":androidtest\"))\n    testImplementation(project(\":test-shared\"))\n    testImplementation(project(\":strings\"))\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.androidxTestEspressoCore)\n    testImplementation(libs.androidxTestEspressoIntents)\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.robolectric)\n}\n"
  },
  {
    "path": "permissions/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "permissions/src/main/java/org/odk/collect/permissions/LocationAccessibilityChecker.kt",
    "content": "package org.odk.collect.permissions\n\nimport android.app.Activity\nimport android.content.Context\nimport android.location.LocationManager\nimport androidx.core.location.LocationManagerCompat\n\ninternal interface LocationAccessibilityChecker {\n    fun isLocationEnabled(activity: Activity): Boolean\n}\n\ninternal object LocationAccessibilityCheckerImpl : LocationAccessibilityChecker {\n    override fun isLocationEnabled(activity: Activity): Boolean {\n        val locationManager = activity.getSystemService(Context.LOCATION_SERVICE) as LocationManager\n        return LocationManagerCompat.isLocationEnabled(locationManager)\n    }\n}\n"
  },
  {
    "path": "permissions/src/main/java/org/odk/collect/permissions/PermissionListener.kt",
    "content": "package org.odk.collect.permissions\n\n/**\n * This listener serves the purpose of telling the calling activity if a\n * permission was granted or not. It's primarily utilized with the PermissionsProvider\n * class that's delegated with the task of handling all permission related grants, dialogs\n * and other conditions so that that Activities can be cleaner and just know about the result\n * of a grant request.\n */\ninterface PermissionListener {\n    fun granted()\n    fun denied() = Unit\n    fun additionalExplanationClosed() = Unit\n}\n"
  },
  {
    "path": "permissions/src/main/java/org/odk/collect/permissions/PermissionsChecker.kt",
    "content": "package org.odk.collect.permissions\n\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport androidx.core.content.ContextCompat\n\ninterface PermissionsChecker {\n    fun isPermissionGranted(vararg permissions: String): Boolean\n}\n\nopen class ContextCompatPermissionChecker(private val context: Context) : PermissionsChecker {\n\n    override fun isPermissionGranted(vararg permissions: String): Boolean {\n        for (permission in permissions) {\n            if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {\n                return false\n            }\n        }\n        return true\n    }\n}\n"
  },
  {
    "path": "permissions/src/main/java/org/odk/collect/permissions/PermissionsDialogCreator.kt",
    "content": "package org.odk.collect.permissions\n\nimport android.app.Activity\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.net.Uri\nimport android.provider.Settings\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\n\ninternal interface PermissionsDialogCreator {\n    fun showEnableGPSDialog(\n        activity: Activity,\n        action: PermissionListener\n    )\n\n    fun showAdditionalExplanation(\n        activity: Activity,\n        title: Int,\n        message: Int,\n        drawable: Int,\n        action: PermissionListener\n    )\n}\n\ninternal object PermissionsDialogCreatorImpl : PermissionsDialogCreator {\n    override fun showEnableGPSDialog(\n        activity: Activity,\n        action: PermissionListener\n    ) {\n        MaterialAlertDialogBuilder(activity)\n            .setMessage(activity.getString(org.odk.collect.strings.R.string.gps_enable_message))\n            .setCancelable(false)\n            .setPositiveButton(\n                activity.getString(org.odk.collect.strings.R.string.enable_gps)\n            ) { _: DialogInterface?, _: Int ->\n                activity.startActivityForResult(\n                    Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS),\n                    0\n                )\n            }\n            .setNegativeButton(\n                activity.getString(org.odk.collect.strings.R.string.cancel)\n            ) { dialog: DialogInterface, _: Int ->\n                action.denied()\n                dialog.cancel()\n            }\n            .create()\n            .show()\n    }\n\n    override fun showAdditionalExplanation(\n        activity: Activity,\n        title: Int,\n        message: Int,\n        drawable: Int,\n        action: PermissionListener\n    ) {\n        MaterialAlertDialogBuilder(activity)\n            .setIcon(drawable)\n            .setTitle(title)\n            .setMessage(message)\n            .setCancelable(false)\n            .setPositiveButton(org.odk.collect.strings.R.string.ok) { _, _ ->\n                action.additionalExplanationClosed()\n            }\n            .setNeutralButton(org.odk.collect.strings.R.string.open_settings) { _, _ ->\n                Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {\n                    data = Uri.fromParts(\"package\", activity.packageName, null)\n                    activity.startActivity(this)\n                }\n                action.additionalExplanationClosed()\n            }\n            .create()\n            .show()\n    }\n}\n"
  },
  {
    "path": "permissions/src/main/java/org/odk/collect/permissions/PermissionsProvider.kt",
    "content": "package org.odk.collect.permissions\n\nimport android.Manifest\nimport android.app.Activity\nimport android.content.ContentResolver\nimport android.net.Uri\n\n/**\n * PermissionsProvider allows all permission related messages and checks to be encapsulated in one\n * area so that classes don't have to deal with this responsibility; they just receive a callback\n * that tells them if they have been granted the permission they requested.\n */\nopen class PermissionsProvider internal constructor(\n    private val permissionsChecker: PermissionsChecker,\n    private val requestPermissionsApi: RequestPermissionsAPI,\n    private val permissionsDialogCreator: PermissionsDialogCreator,\n    private val locationAccessibilityChecker: LocationAccessibilityChecker\n) {\n\n    /**\n     * Public facing constructor that doesn't expose [RequestPermissionsAPI]\n     */\n    constructor(permissionsChecker: PermissionsChecker) : this(\n        permissionsChecker,\n        DexterRequestPermissionsAPI,\n        PermissionsDialogCreatorImpl,\n        LocationAccessibilityCheckerImpl\n    )\n\n    val isCameraPermissionGranted: Boolean\n        get() = permissionsChecker.isPermissionGranted(Manifest.permission.CAMERA)\n\n    fun areLocationPermissionsGranted(): Boolean {\n        return permissionsChecker.isPermissionGranted(\n            Manifest.permission.ACCESS_FINE_LOCATION,\n            Manifest.permission.ACCESS_COARSE_LOCATION\n        )\n    }\n\n    fun areCameraAndRecordAudioPermissionsGranted(): Boolean {\n        return permissionsChecker.isPermissionGranted(\n            Manifest.permission.CAMERA,\n            Manifest.permission.RECORD_AUDIO\n        )\n    }\n\n    open fun requestCameraPermission(activity: Activity, action: PermissionListener) {\n        requestPermissions(\n            activity,\n            object : PermissionListener {\n                override fun granted() {\n                    action.granted()\n                }\n\n                override fun denied() {\n                    action.denied()\n\n                    permissionsDialogCreator.showAdditionalExplanation(\n                        activity,\n                        org.odk.collect.strings.R.string.camera_runtime_permission_denied_title,\n                        org.odk.collect.strings.R.string.camera_runtime_permission_denied_desc,\n                        R.drawable.ic_photo_camera,\n                        action\n                    )\n                }\n            },\n            Manifest.permission.CAMERA\n        )\n    }\n\n    /**\n     * Request location permissions and make sure Location is enabled at a system level. If the\n     * latter is not true, show a dialog prompting the user to do so rather than\n     * [PermissionListener.granted].\n     */\n    fun requestEnabledLocationPermissions(activity: Activity, action: PermissionListener) {\n        requestPermissions(\n            activity,\n            object : PermissionListener {\n                override fun granted() {\n                    if (locationAccessibilityChecker.isLocationEnabled(activity)) {\n                        action.granted()\n                    } else {\n                        permissionsDialogCreator.showEnableGPSDialog(activity, action)\n                    }\n                }\n\n                override fun denied() {\n                    action.denied()\n\n                    permissionsDialogCreator.showAdditionalExplanation(\n                        activity,\n                        org.odk.collect.strings.R.string.location_runtime_permissions_denied_title,\n                        org.odk.collect.strings.R.string.location_runtime_permissions_denied_desc,\n                        R.drawable.ic_room_24dp,\n                        action\n                    )\n                }\n            },\n            Manifest.permission.ACCESS_FINE_LOCATION,\n            Manifest.permission.ACCESS_COARSE_LOCATION\n        )\n    }\n\n    open fun requestRecordAudioPermission(activity: Activity, action: PermissionListener) {\n        requestPermissions(\n            activity,\n            object : PermissionListener {\n                override fun granted() {\n                    action.granted()\n                }\n\n                override fun denied() {\n                    action.denied()\n\n                    permissionsDialogCreator.showAdditionalExplanation(\n                        activity,\n                        org.odk.collect.strings.R.string.record_audio_runtime_permission_denied_title,\n                        org.odk.collect.strings.R.string.record_audio_runtime_permission_denied_desc,\n                        org.odk.collect.icons.R.drawable.ic_baseline_mic_24,\n                        action\n                    )\n                }\n            },\n            Manifest.permission.RECORD_AUDIO\n        )\n    }\n\n    fun requestCameraAndRecordAudioPermissions(activity: Activity, action: PermissionListener) {\n        requestPermissions(\n            activity,\n            object : PermissionListener {\n                override fun granted() {\n                    action.granted()\n                }\n\n                override fun denied() {\n                    action.denied()\n\n                    permissionsDialogCreator.showAdditionalExplanation(\n                        activity,\n                        org.odk.collect.strings.R.string.camera_runtime_permission_denied_title,\n                        org.odk.collect.strings.R.string.camera_runtime_permission_denied_desc,\n                        R.drawable.ic_photo_camera,\n                        action\n                    )\n                }\n            },\n            Manifest.permission.CAMERA,\n            Manifest.permission.RECORD_AUDIO\n        )\n    }\n\n    open fun requestPermissions(\n        activity: Activity,\n        listener: PermissionListener,\n        vararg permissions: String\n    ) {\n        val safePermissionsListener = object : PermissionListener {\n            override fun granted() {\n                if (!activity.isFinishing) {\n                    listener.granted()\n                }\n            }\n\n            override fun denied() {\n                if (!activity.isFinishing) {\n                    listener.denied()\n                }\n            }\n        }\n\n        requestPermissionsApi.requestPermissions(activity, safePermissionsListener, *permissions)\n    }\n\n    fun requestReadUriPermission(\n        activity: Activity,\n        uri: Uri,\n        contentResolver: ContentResolver,\n        listener: PermissionListener\n    ) {\n        try {\n            contentResolver.query(uri, null, null, null, null)\n                .use { listener.granted() }\n        } catch (e: SecurityException) {\n            requestPermissions(\n                activity,\n                object : PermissionListener {\n                    override fun granted() {\n                        listener.granted()\n                    }\n\n                    override fun denied() {\n                        permissionsDialogCreator.showAdditionalExplanation(\n                            activity,\n                            org.odk.collect.strings.R.string.storage_runtime_permission_denied_title,\n                            org.odk.collect.strings.R.string.storage_runtime_permission_denied_desc,\n                            R.drawable.ic_storage,\n                            listener\n                        )\n                    }\n                },\n                Manifest.permission.READ_EXTERNAL_STORAGE\n            )\n        } catch (e: Exception) {\n            listener.denied()\n        } catch (e: Error) {\n            listener.denied()\n        }\n    }\n}\n"
  },
  {
    "path": "permissions/src/main/java/org/odk/collect/permissions/RequestPermissionsAPI.kt",
    "content": "package org.odk.collect.permissions\n\nimport android.app.Activity\nimport com.karumi.dexter.Dexter\nimport com.karumi.dexter.DexterBuilder\nimport com.karumi.dexter.MultiplePermissionsReport\nimport com.karumi.dexter.PermissionToken\nimport com.karumi.dexter.listener.DexterError\nimport com.karumi.dexter.listener.PermissionDeniedResponse\nimport com.karumi.dexter.listener.PermissionGrantedResponse\nimport com.karumi.dexter.listener.PermissionRequest\nimport com.karumi.dexter.listener.multi.MultiplePermissionsListener\nimport timber.log.Timber\n\ninternal interface RequestPermissionsAPI {\n    fun requestPermissions(\n        activity: Activity,\n        listener: PermissionListener,\n        vararg permissions: String\n    )\n}\n\ninternal object DexterRequestPermissionsAPI : RequestPermissionsAPI {\n\n    override fun requestPermissions(\n        activity: Activity,\n        listener: PermissionListener,\n        vararg permissions: String\n    ) {\n        var builder: DexterBuilder? = null\n        if (permissions.size == 1) {\n            builder = createSinglePermissionRequest(activity, permissions[0], listener)\n        } else if (permissions.size > 1) {\n            builder = createMultiplePermissionsRequest(activity, listener, *permissions)\n        }\n        builder?.withErrorListener { error: DexterError -> Timber.i(error.name) }?.check()\n    }\n\n    private fun createSinglePermissionRequest(\n        activity: Activity,\n        permission: String,\n        listener: PermissionListener\n    ): DexterBuilder {\n        return Dexter.withContext(activity)\n            .withPermission(permission)\n            .withListener(object : com.karumi.dexter.listener.single.PermissionListener {\n                override fun onPermissionGranted(response: PermissionGrantedResponse) {\n                    listener.granted()\n                }\n\n                override fun onPermissionDenied(response: PermissionDeniedResponse) {\n                    listener.denied()\n                }\n\n                override fun onPermissionRationaleShouldBeShown(\n                    permission: PermissionRequest,\n                    token: PermissionToken\n                ) {\n                    token.continuePermissionRequest()\n                }\n            })\n    }\n\n    private fun createMultiplePermissionsRequest(\n        activity: Activity,\n        listener: PermissionListener,\n        vararg permissions: String\n    ): DexterBuilder {\n        return Dexter.withContext(activity)\n            .withPermissions(*permissions)\n            .withListener(object : MultiplePermissionsListener {\n                override fun onPermissionsChecked(report: MultiplePermissionsReport) {\n                    if (report.areAllPermissionsGranted()) {\n                        listener.granted()\n                    } else {\n                        listener.denied()\n                    }\n                }\n\n                override fun onPermissionRationaleShouldBeShown(\n                    permissions: List<PermissionRequest>,\n                    token: PermissionToken\n                ) {\n                    token.continuePermissionRequest()\n                }\n            })\n    }\n}\n"
  },
  {
    "path": "permissions/src/main/res/drawable/ic_photo_camera.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0\"/>\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z\"/>\n</vector>\n"
  },
  {
    "path": "permissions/src/main/res/drawable/ic_room_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"36dp\"\n    android:height=\"36dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z\" />\n</vector>\n"
  },
  {
    "path": "permissions/src/main/res/drawable/ic_storage.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?colorOnSurface\"\n    android:autoMirrored=\"true\">\n    <path\n        android:fillColor=\"?colorOnSurface\"\n        android:pathData=\"M18,8H16V4H18M15,8H13V4H15M12,8H10V4H12M18,2H10L4,8V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z\" />\n</vector>\n"
  },
  {
    "path": "permissions/src/test/java/org/odk/collect/permissions/PermissionsDialogCreatorTest.kt",
    "content": "package org.odk.collect.permissions\n\nimport android.app.Activity\nimport android.content.DialogInterface\nimport android.net.Uri\nimport android.provider.Settings\nimport androidx.appcompat.app.AlertDialog\nimport androidx.fragment.app.FragmentActivity\nimport androidx.test.espresso.intent.Intents\nimport androidx.test.espresso.intent.matcher.IntentMatchers\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoInteractions\nimport org.mockito.kotlin.verifyNoMoreInteractions\nimport org.odk.collect.androidtest.RecordedIntentsRule\nimport org.odk.collect.testshared.RobolectricHelpers\nimport org.odk.collect.testshared.RobolectricHelpers.createThemedActivity\nimport org.robolectric.shadows.ShadowDialog\n\n@RunWith(AndroidJUnit4::class)\nclass PermissionsDialogCreatorTest {\n    private lateinit var activity: Activity\n    private val permissionListener = mock<PermissionListener>()\n\n    @get:Rule\n    val recordedIntentsRule = RecordedIntentsRule()\n\n    @Before\n    fun setup() {\n        activity = createThemedActivity(FragmentActivity::class.java)\n    }\n\n    @Test\n    fun `PermissionListener should not be called immediately after displaying enable gps dialog`() {\n        PermissionsDialogCreatorImpl.showEnableGPSDialog(\n            activity,\n            permissionListener\n        )\n\n        verifyNoInteractions(permissionListener)\n    }\n\n    @Test\n    fun `Settings should be open after clicking on the positive button in enable gps dialog`() {\n        PermissionsDialogCreatorImpl.showEnableGPSDialog(\n            activity,\n            permissionListener\n        )\n\n        val dialog = ShadowDialog.getLatestDialog() as AlertDialog\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()\n        RobolectricHelpers.runLooper()\n\n        Intents.intended(IntentMatchers.hasAction(Settings.ACTION_LOCATION_SOURCE_SETTINGS))\n    }\n\n    @Test\n    fun `PermissionListener#denied should be called after clicking on the negative button in enable gps dialog`() {\n        PermissionsDialogCreatorImpl.showEnableGPSDialog(\n            activity,\n            permissionListener\n        )\n\n        val dialog = ShadowDialog.getLatestDialog() as AlertDialog\n        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick()\n        RobolectricHelpers.runLooper()\n\n        verify(permissionListener).denied()\n        verifyNoMoreInteractions(permissionListener)\n    }\n\n    @Test\n    fun `PermissionListener should not be called immediately after displaying explanation dialog`() {\n        PermissionsDialogCreatorImpl.showAdditionalExplanation(\n            activity,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_title,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_desc,\n            R.drawable.ic_photo_camera,\n            permissionListener\n        )\n\n        verifyNoInteractions(permissionListener)\n    }\n\n    @Test\n    fun `PermissionListener#additionalExplanationClosed should be called after clicking on the positive button in explanation dialog`() {\n        PermissionsDialogCreatorImpl.showAdditionalExplanation(\n            activity,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_title,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_desc,\n            R.drawable.ic_photo_camera,\n            permissionListener\n        )\n\n        val dialog = ShadowDialog.getLatestDialog() as AlertDialog\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()\n        RobolectricHelpers.runLooper()\n\n        verify(permissionListener).additionalExplanationClosed()\n        verifyNoMoreInteractions(permissionListener)\n    }\n\n    @Test\n    fun `PermissionListener#additionalExplanationClosed should be called after clicking on the 'Open Settings' button in explanation dialog`() {\n        PermissionsDialogCreatorImpl.showAdditionalExplanation(\n            activity,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_title,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_desc,\n            R.drawable.ic_photo_camera,\n            permissionListener\n        )\n\n        val dialog = ShadowDialog.getLatestDialog() as AlertDialog\n        dialog.getButton(DialogInterface.BUTTON_NEUTRAL).performClick()\n        RobolectricHelpers.runLooper()\n\n        verify(permissionListener).additionalExplanationClosed()\n        verifyNoMoreInteractions(permissionListener)\n    }\n\n    @Test\n    fun `Settings should be open after clicking on the neutral button in explanation dialog`() {\n        PermissionsDialogCreatorImpl.showAdditionalExplanation(\n            activity,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_title,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_desc,\n            R.drawable.ic_photo_camera,\n            permissionListener\n        )\n\n        val dialog = ShadowDialog.getLatestDialog() as AlertDialog\n        dialog.getButton(DialogInterface.BUTTON_NEUTRAL).performClick()\n        RobolectricHelpers.runLooper()\n\n        Intents.intended(IntentMatchers.hasAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS))\n        Intents.intended(IntentMatchers.hasData(Uri.fromParts(\"package\", activity.packageName, null)))\n    }\n}\n"
  },
  {
    "path": "permissions/src/test/java/org/odk/collect/permissions/PermissionsProviderTest.kt",
    "content": "package org.odk.collect.permissions\n\nimport android.Manifest\nimport android.app.Activity\nimport android.content.ContentResolver\nimport android.net.Uri\nimport androidx.test.espresso.matcher.ViewMatchers.assertThat\nimport org.hamcrest.CoreMatchers.`is`\nimport org.junit.Before\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoInteractions\nimport org.mockito.kotlin.verifyNoMoreInteractions\nimport org.mockito.kotlin.whenever\n\nclass PermissionsProviderTest {\n\n    private var permissionsChecker = mock<PermissionsChecker>()\n    private var activity = mock<Activity>()\n    private var uri = mock<Uri>()\n    private var contentResolver = mock<ContentResolver>()\n    private var permissionListener = mock<PermissionListener>()\n    private val permissionsApi = TestRequestPermissionsAPI()\n    private val permissionsDialogCreator = mock<PermissionsDialogCreator>()\n    private val locationAccessibilityChecker = mock<LocationAccessibilityChecker>()\n\n    private lateinit var permissionsProvider: PermissionsProvider\n\n    @Before\n    fun setup() {\n        permissionsProvider = PermissionsProvider(permissionsChecker, permissionsApi, permissionsDialogCreator, locationAccessibilityChecker)\n    }\n\n    @Test\n    fun `isCameraPermissionGranted() when camera permission is granted returns true`() {\n        whenever(permissionsChecker.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true)\n\n        assertThat(permissionsProvider.isCameraPermissionGranted, `is`(true))\n    }\n\n    @Test\n    fun `isCameraPermissionGranted() when camera permission is not granted returns false`() {\n        whenever(permissionsChecker.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(false)\n\n        assertThat(permissionsProvider.isCameraPermissionGranted, `is`(false))\n    }\n\n    @Test\n    fun `requestCameraPermission when camera permission is granted calls PermissionListener#granted`() {\n        permissionsApi.setGrantedPermission(Manifest.permission.CAMERA)\n\n        permissionsProvider.requestCameraPermission(activity, permissionListener)\n\n        verify(permissionListener).granted()\n        verifyNoMoreInteractions(permissionListener)\n        verifyNoInteractions(permissionsDialogCreator)\n    }\n\n    @Test\n    fun `requestCameraPermission() when camera permission is not granted calls PermissionListener#denied`() {\n        permissionsProvider.requestCameraPermission(activity, permissionListener)\n\n        verify(permissionListener).denied()\n        verifyNoMoreInteractions(permissionListener)\n    }\n\n    @Test\n    fun `requestCameraPermission() when camera permission is not granted calls PermissionsDialogCreator#showAdditionalExplanation`() {\n        permissionsProvider.requestCameraPermission(activity, permissionListener)\n\n        verify(permissionsDialogCreator).showAdditionalExplanation(\n            activity,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_title,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_desc,\n            R.drawable.ic_photo_camera,\n            permissionListener\n        )\n        verifyNoMoreInteractions(permissionsDialogCreator)\n    }\n\n    @Test\n    fun `areLocationPermissionsGranted() when location permission is granted returns true`() {\n        whenever(permissionsChecker.isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)).thenReturn(true)\n\n        assertThat(permissionsProvider.areLocationPermissionsGranted(), `is`(true))\n    }\n\n    @Test\n    fun `areLocationPermissionsGranted() when location permission is not granted returns false`() {\n        whenever(permissionsChecker.isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)).thenReturn(false)\n\n        assertThat(permissionsProvider.areLocationPermissionsGranted(), `is`(false))\n    }\n\n    @Test\n    fun `requestEnabledLocationPermissions() when location permission is granted and location is enabled in settings calls PermissionListener#granted`() {\n        whenever(locationAccessibilityChecker.isLocationEnabled(activity)).thenReturn(true)\n        permissionsApi.setGrantedPermission(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)\n\n        permissionsProvider.requestEnabledLocationPermissions(activity, permissionListener)\n\n        verify(permissionListener).granted()\n        verifyNoMoreInteractions(permissionListener)\n        verifyNoInteractions(permissionsDialogCreator)\n    }\n\n    @Test\n    fun `requestEnabledLocationPermissions() when location permission is granted and location is disabled in settings calls PermissionsDialogCreator#showEnableGPSDialog`() {\n        whenever(locationAccessibilityChecker.isLocationEnabled(activity)).thenReturn(false)\n        permissionsApi.setGrantedPermission(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)\n\n        permissionsProvider.requestEnabledLocationPermissions(activity, permissionListener)\n\n        verify(permissionsDialogCreator).showEnableGPSDialog(activity, permissionListener)\n        verifyNoInteractions(permissionListener)\n    }\n\n    @Test\n    fun `requestEnabledLocationPermissions() wen location permission is not granted calls PermissionListener#denied`() {\n        permissionsProvider.requestEnabledLocationPermissions(activity, permissionListener)\n\n        verify(permissionListener).denied()\n        verifyNoMoreInteractions(permissionListener)\n    }\n\n    @Test\n    fun `requestEnabledLocationPermissions() when location permission is not granted calls PermissionsDialogCreator#showAdditionalExplanation`() {\n        permissionsProvider.requestEnabledLocationPermissions(activity, permissionListener)\n\n        verify(permissionsDialogCreator).showAdditionalExplanation(\n            activity,\n            org.odk.collect.strings.R.string.location_runtime_permissions_denied_title,\n            org.odk.collect.strings.R.string.location_runtime_permissions_denied_desc,\n            R.drawable.ic_room_24dp,\n            permissionListener\n        )\n\n        verifyNoMoreInteractions(permissionsDialogCreator)\n    }\n\n    @Test\n    fun `requestRecordAudioPermission() when audio permission is granted calls PermissionListener#granted`() {\n        permissionsApi.setGrantedPermission(Manifest.permission.RECORD_AUDIO)\n\n        permissionsProvider.requestRecordAudioPermission(activity, permissionListener)\n\n        verify(permissionListener).granted()\n        verifyNoMoreInteractions(permissionListener)\n        verifyNoInteractions(permissionsDialogCreator)\n    }\n\n    @Test\n    fun `requestRecordAudioPermission() when audio permission is not granted calls PermissionListener#denied`() {\n        permissionsProvider.requestRecordAudioPermission(activity, permissionListener)\n\n        verify(permissionListener).denied()\n        verifyNoMoreInteractions(permissionListener)\n    }\n\n    @Test\n    fun `requestRecordAudioPermission() when audio permission is not granted calls PermissionsDialogCreator#showAdditionalExplanation`() {\n        permissionsProvider.requestRecordAudioPermission(activity, permissionListener)\n\n        verify(permissionsDialogCreator).showAdditionalExplanation(\n            activity,\n            org.odk.collect.strings.R.string.record_audio_runtime_permission_denied_title,\n            org.odk.collect.strings.R.string.record_audio_runtime_permission_denied_desc,\n            org.odk.collect.icons.R.drawable.ic_baseline_mic_24,\n            permissionListener\n        )\n\n        verifyNoMoreInteractions(permissionsDialogCreator)\n    }\n\n    @Test\n    fun `areCameraAndRecordAudioPermissionsGranted() when camera and audio permission is granted returns true`() {\n        whenever(permissionsChecker.isPermissionGranted(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)).thenReturn(true)\n\n        assertThat(permissionsProvider.areCameraAndRecordAudioPermissionsGranted(), `is`(true))\n    }\n\n    @Test\n    fun `areCameraAndRecordAudioPermissionsGranted() when camera and audio permission is not granted returns false`() {\n        whenever(permissionsChecker.isPermissionGranted(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)).thenReturn(false)\n\n        assertThat(permissionsProvider.areCameraAndRecordAudioPermissionsGranted(), `is`(false))\n    }\n\n    @Test\n    fun `requestCameraAndRecordAudioPermissions() when camera and audio permission is granted calls PermissionListener#granted`() {\n        permissionsApi.setGrantedPermission(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)\n\n        permissionsProvider.requestCameraAndRecordAudioPermissions(activity, permissionListener)\n\n        verify(permissionListener).granted()\n        verifyNoMoreInteractions(permissionListener)\n        verifyNoInteractions(permissionsDialogCreator)\n    }\n\n    @Test\n    fun `requestCameraAndRecordAudioPermissions() when camera and audio permission is not granted calls PermissionListener#denied`() {\n        permissionsProvider.requestCameraAndRecordAudioPermissions(activity, permissionListener)\n\n        verify(permissionListener).denied()\n        verifyNoMoreInteractions(permissionListener)\n    }\n\n    @Test\n    fun `requestCameraAndRecordAudioPermissions() when camera and audio permission is not granted calls PermissionsDialogCreator#showAdditionalExplanation`() {\n        permissionsProvider.requestCameraAndRecordAudioPermissions(activity, permissionListener)\n\n        verify(permissionsDialogCreator).showAdditionalExplanation(\n            activity,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_title,\n            org.odk.collect.strings.R.string.camera_runtime_permission_denied_desc,\n            R.drawable.ic_photo_camera,\n            permissionListener\n        )\n        verifyNoMoreInteractions(permissionsDialogCreator)\n    }\n\n    @Test\n    fun `requestReadUriPermission() when request read uri permission granted calls PermissionListener#granted`() {\n        permissionsProvider.requestReadUriPermission(\n            activity,\n            uri,\n            contentResolver,\n            permissionListener\n        )\n        verify(permissionListener).granted()\n    }\n\n    @Test\n    fun `requestReadUriPermission() when throws any exception calls PermissionListener#granted#denied`() {\n        whenever(contentResolver.query(uri, null, null, null, null)).thenThrow(\n            RuntimeException::class.java\n        )\n        permissionsProvider.requestReadUriPermission(\n            activity,\n            uri,\n            contentResolver,\n            permissionListener\n        )\n        verify(permissionListener).denied()\n    }\n\n    @Test\n    fun `granted listener is not called when Activity is finishing`() {\n        permissionsApi.setGrantedPermission(Manifest.permission.CAMERA)\n\n        whenever(activity.isFinishing).doReturn(true)\n\n        permissionsProvider.requestPermissions(\n            activity,\n            permissionListener,\n            Manifest.permission.CAMERA\n        )\n\n        verifyNoInteractions(permissionListener)\n    }\n\n    @Test\n    fun `denied listener is not called when Activity is finishing`() {\n        permissionsApi.setGrantedPermission(Manifest.permission.CAMERA)\n\n        whenever(activity.isFinishing).doReturn(true)\n\n        permissionsProvider.requestPermissions(\n            activity,\n            permissionListener,\n            Manifest.permission.CAMERA\n        )\n\n        verifyNoInteractions(permissionListener)\n    }\n\n    private class TestRequestPermissionsAPI : RequestPermissionsAPI {\n\n        private var grantedPermissions: List<String> = emptyList()\n\n        override fun requestPermissions(\n            activity: Activity,\n            listener: PermissionListener,\n            vararg permissions: String\n        ) {\n            if (grantedPermissions.containsAll(permissions.asList())) {\n                listener.granted()\n            } else {\n                listener.denied()\n            }\n        }\n\n        fun setGrantedPermission(vararg grantedPermissions: String) {\n            this.grantedPermissions = grantedPermissions.asList()\n        }\n    }\n}\n"
  },
  {
    "path": "permissions/src/test/resources/robolectric.properties",
    "content": "sdk=33\n"
  },
  {
    "path": "printer/.gitignore",
    "content": "/build"
  },
  {
    "path": "printer/build.gradle.kts",
    "content": "apply(from = \"../config/quality.gradle\")\n\nplugins {\n    alias(libs.plugins.androidLibrary)\n}\n\nandroid {\n    namespace = \"org.odk.collect.printer\"\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n}\n\ndependencies {\n}\n"
  },
  {
    "path": "printer/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "printer/src/main/java/org/odk/collect/printer/HtmlPrinter.kt",
    "content": "package org.odk.collect.printer\n\nimport android.content.Context\nimport android.print.PrintAttributes\nimport android.print.PrintManager\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\n\nclass HtmlPrinter {\n    private var webView: WebView? = null\n\n    fun print(context: Context, htmlDocument: String) {\n        webView = WebView(context).apply {\n            settings.allowFileAccess = true\n\n            webViewClient = object : WebViewClient() {\n                override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = false\n\n                override fun onPageFinished(view: WebView, url: String) {\n                    createWebPrintJob(context, view)\n                    webView = null\n                }\n            }\n\n            loadDataWithBaseURL(null, htmlDocument, \"text/HTML\", \"UTF-8\", null)\n        }\n    }\n\n    private fun createWebPrintJob(context: Context, webView: WebView) {\n        (context.getSystemService(Context.PRINT_SERVICE) as? PrintManager)?.let { printManager ->\n            val jobName = \"ODK print\"\n            val printAdapter = webView.createPrintDocumentAdapter(jobName)\n            printManager.print(\n                jobName,\n                printAdapter,\n                PrintAttributes.Builder().build()\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "projects/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "projects/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    namespace = \"org.odk.collect.projects\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":shared\"))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":material\"))\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxAppcompat)\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.androidxFragmentKtx)\n    implementation(libs.gson)\n    implementation(libs.dagger)\n    ksp(libs.daggerCompiler)\n\n    testImplementation(libs.junit)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(project(\":test-shared\"))\n    testImplementation(libs.androidxTestEspressoCore)\n    testImplementation(libs.mockitoKotlin)\n\n    debugImplementation(libs.androidxFragmentTesting) {\n        exclude(group = \"androidx.test\", module = \"monitor\") // fixes issue https://github.com/android/android-test/issues/731\n    }\n}\n"
  },
  {
    "path": "projects/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "projects/src/main/java/org/odk/collect/projects/DaggerSetup.kt",
    "content": "package org.odk.collect.projects\n\nimport dagger.Component\nimport dagger.Module\nimport dagger.Provides\nimport org.odk.collect.shared.strings.UUIDGenerator\nimport javax.inject.Singleton\n\ninterface ProjectsDependencyComponentProvider {\n    val projectsDependencyComponent: ProjectsDependencyComponent\n}\n\n@Component(modules = [ProjectsDependencyModule::class])\n@Singleton\ninterface ProjectsDependencyComponent\n\n@Module\nopen class ProjectsDependencyModule {\n\n    @Provides\n    open fun providesProjectsRepository(): ProjectsRepository {\n        return InMemProjectsRepository(UUIDGenerator())\n    }\n}\n"
  },
  {
    "path": "projects/src/main/java/org/odk/collect/projects/InMemProjectsRepository.kt",
    "content": "package org.odk.collect.projects\n\nimport org.odk.collect.shared.strings.UUIDGenerator\nimport java.util.function.Supplier\n\nclass InMemProjectsRepository(\n    private val uuidGenerator: UUIDGenerator = UUIDGenerator(),\n    private val clock: Supplier<Long> = Supplier { System.currentTimeMillis() }\n) : ProjectsRepository {\n\n    val projects = mutableListOf<Project.Saved>()\n    val timestamps = mutableListOf<Pair<String, Long>>()\n\n    override fun get(uuid: String) = projects.find { it.uuid == uuid }\n\n    override fun getAll() = timestamps.sortedBy { it.second }.map {\n        projects.first { p -> p.uuid == it.first }\n    }\n\n    override fun save(project: Project): Project.Saved {\n        when (project) {\n            is Project.New -> {\n                val projectToSave = Project.Saved(uuidGenerator.generateUUID(), project)\n                projects.add(projectToSave)\n                timestamps.add(Pair(projectToSave.uuid, clock.get()))\n                return projectToSave\n            }\n\n            is Project.Saved -> {\n                val projectIndex = projects.indexOf(get(project.uuid))\n                if (projectIndex == -1) {\n                    projects.add(project)\n                    timestamps.add(Pair(project.uuid, clock.get()))\n                } else {\n                    projects[projectIndex] = project\n                }\n\n                return project\n            }\n        }\n    }\n\n    override fun delete(uuid: String) {\n        projects.removeIf { it.uuid == uuid }\n        timestamps.removeIf { it.first == uuid }\n    }\n\n    override fun deleteAll() {\n        projects.clear()\n        timestamps.clear()\n    }\n}\n"
  },
  {
    "path": "projects/src/main/java/org/odk/collect/projects/Project.kt",
    "content": "package org.odk.collect.projects\n\nsealed class Project {\n    abstract val name: String\n    abstract val icon: String\n    abstract val color: String\n\n    data class New(\n        override val name: String,\n        override val icon: String,\n        override val color: String\n    ) : Project()\n\n    data class Saved(\n        val uuid: String,\n        override val name: String,\n        override val icon: String,\n        override val color: String,\n        val isOldGoogleDriveProject: Boolean = false\n    ) : Project() {\n\n        constructor(uuid: String, project: New) : this(\n            uuid,\n            project.name,\n            project.icon,\n            project.color\n        )\n    }\n\n    companion object {\n        const val DEMO_PROJECT_ID = \"DEMO\"\n        const val DEMO_PROJECT_NAME = \"Demo project\"\n        const val DEMO_PROJECT_ICON = \"D\"\n        const val DEMO_PROJECT_COLOR = \"#3e9fcc\"\n\n        val DEMO_PROJECT = Saved(DEMO_PROJECT_ID, DEMO_PROJECT_NAME, DEMO_PROJECT_ICON, DEMO_PROJECT_COLOR)\n    }\n}\n"
  },
  {
    "path": "projects/src/main/java/org/odk/collect/projects/ProjectConfigurationResult.kt",
    "content": "package org.odk.collect.projects\n\nenum class ProjectConfigurationResult {\n    SUCCESS,\n    INVALID_SETTINGS,\n    GD_PROJECT\n}\n"
  },
  {
    "path": "projects/src/main/java/org/odk/collect/projects/ProjectCreator.kt",
    "content": "package org.odk.collect.projects\n\ninterface ProjectCreator {\n    fun createNewProject(settingsJson: String, switchToTheNewProject: Boolean): ProjectConfigurationResult\n}\n"
  },
  {
    "path": "projects/src/main/java/org/odk/collect/projects/ProjectDependencyFactory.kt",
    "content": "package org.odk.collect.projects\n\n/**\n * Some dependencies will end up being project specific and this provides a common interface\n * for constructing them.\n */\nfun interface ProjectDependencyFactory<T> {\n    fun create(projectId: String): T\n}\n\nfun <T> projectDependency(\n    projectId: String,\n    projectDependencyFactory: ProjectDependencyFactory<T>\n): Lazy<T> {\n    return projectDependency(projectId, projectDependencyFactory) { it }\n}\n\nfun <T, U> projectDependency(\n    projectId: String,\n    projectDependencyFactory: ProjectDependencyFactory<T>,\n    transform: (T) -> U\n): Lazy<U> {\n    return lazy { transform(projectDependencyFactory.create(projectId)) }\n}\n"
  },
  {
    "path": "projects/src/main/java/org/odk/collect/projects/ProjectsRepository.kt",
    "content": "package org.odk.collect.projects\n\ninterface ProjectsRepository {\n    fun get(uuid: String): Project.Saved?\n\n    fun getAll(): List<Project.Saved>\n\n    fun save(project: Project): Project.Saved\n\n    fun delete(uuid: String)\n\n    fun deleteAll()\n}\n"
  },
  {
    "path": "projects/src/main/java/org/odk/collect/projects/SettingsConnectionMatcher.kt",
    "content": "package org.odk.collect.projects\n\ninterface SettingsConnectionMatcher {\n    fun getProjectWithMatchingConnection(settingsJson: String): String?\n}\n"
  },
  {
    "path": "projects/src/main/java/org/odk/collect/projects/SharedPreferencesProjectsRepository.kt",
    "content": "package org.odk.collect.projects\n\nimport com.google.gson.Gson\nimport com.google.gson.reflect.TypeToken.getParameterized\nimport org.odk.collect.shared.settings.Settings\nimport org.odk.collect.shared.strings.UUIDGenerator\nimport java.util.function.Supplier\n\nclass SharedPreferencesProjectsRepository @JvmOverloads constructor(\n    private val uuidGenerator: UUIDGenerator,\n    private val gson: Gson,\n    private val settings: Settings,\n    private val key: String,\n    private val clock: Supplier<Long> = Supplier { System.currentTimeMillis() }\n) : ProjectsRepository {\n\n    override fun get(uuid: String): Project.Saved? {\n        return getAll().firstOrNull { it.uuid == uuid }\n    }\n\n    override fun getAll(): List<Project.Saved> {\n        return getJsonProjects().sortedBy { it.createdAt }.map(JsonProject::toProject)\n    }\n\n    override fun save(project: Project): Project.Saved {\n        val projects = getJsonProjects().toMutableList()\n\n        when (project) {\n            is Project.New -> {\n                val projectToSave = project.toJson(uuidGenerator.generateUUID(), clock.get())\n                projects.add(projectToSave)\n\n                settings.save(key, gson.toJson(projects))\n                return projectToSave.toProject()\n            }\n\n            is Project.Saved -> {\n                val projectIndex = projects.indexOfFirst { it.uuid == project.uuid }\n                if (projectIndex == -1) {\n                    projects.add(project.toJson(clock.get()))\n                } else {\n                    projects[projectIndex] = project.toJson(projects[projectIndex].createdAt)\n                }\n\n                settings.save(key, gson.toJson(projects))\n                return project\n            }\n        }\n    }\n\n    override fun delete(uuid: String) {\n        val projects = getAll().toMutableList().minus(get(uuid))\n        settings.save(key, gson.toJson(projects))\n    }\n\n    override fun deleteAll() {\n        settings.remove(key)\n    }\n\n    private fun getJsonProjects(): List<JsonProject> {\n        val projects = settings.getString(key)\n        return if (projects != null && projects.isNotBlank()) {\n            val typeToken = getParameterized(ArrayList::class.java, JsonProject::class.java)\n            gson.fromJson<ArrayList<JsonProject>>(projects, typeToken.type)\n        } else {\n            emptyList()\n        }\n    }\n}\n\nprivate data class JsonProject(\n    val uuid: String,\n    val name: String,\n    val icon: String,\n    val color: String,\n    val createdAt: Long = 0, // Account for projects without timestamps (in older versions)\n    val isOldGoogleDriveProject: Boolean\n)\n\nprivate fun JsonProject.toProject(): Project.Saved {\n    return Project.Saved(this.uuid, this.name, this.icon, this.color, this.isOldGoogleDriveProject)\n}\n\nprivate fun Project.New.toJson(uuid: String, createdAt: Long): JsonProject {\n    return JsonProject(uuid, this.name, this.icon, this.color, createdAt, false)\n}\n\nprivate fun Project.Saved.toJson(createdAt: Long): JsonProject {\n    return JsonProject(uuid, this.name, this.icon, this.color, createdAt, this.isOldGoogleDriveProject)\n}\n"
  },
  {
    "path": "projects/src/test/java/org/odk/collect/projects/InMemProjectsRepositoryTest.kt",
    "content": "package org.odk.collect.projects\n\nimport org.odk.collect.shared.strings.UUIDGenerator\nimport java.util.function.Supplier\n\nclass InMemProjectsRepositoryTest : ProjectsRepositoryTest() {\n    override fun buildSubject(): ProjectsRepository {\n        return InMemProjectsRepository(UUIDGenerator())\n    }\n\n    override fun buildSubject(clock: Supplier<Long>): ProjectsRepository {\n        return InMemProjectsRepository(UUIDGenerator(), clock)\n    }\n}\n"
  },
  {
    "path": "projects/src/test/java/org/odk/collect/projects/ProjectsRepositoryTest.kt",
    "content": "package org.odk.collect.projects\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.hamcrest.Matchers.isEmptyString\nimport org.hamcrest.Matchers.not\nimport org.hamcrest.Matchers.notNullValue\nimport org.junit.Before\nimport org.junit.Test\nimport org.mockito.Mockito.mock\nimport org.mockito.Mockito.`when`\nimport java.util.function.Supplier\n\nabstract class ProjectsRepositoryTest {\n    private val projectX = Project.New(\"ProjectX\", \"X\", \"#FF0000\")\n    private val projectY = Project.New(\"ProjectY\", \"Y\", \"#00FF00\")\n    private val projectZ = Project.New(\"ProjectZ\", \"Z\", \"#0000FF\")\n\n    lateinit var projectsRepository: ProjectsRepository\n\n    abstract fun buildSubject(): ProjectsRepository\n    abstract fun buildSubject(clock: Supplier<Long>): ProjectsRepository\n\n    @Before\n    fun setup() {\n        projectsRepository = buildSubject()\n    }\n\n    @Test\n    fun `getAll() should return all projects from storage`() {\n        projectsRepository.save(projectX)\n        projectsRepository.save(projectY)\n        projectsRepository.save(projectZ)\n\n        val projects = projectsRepository.getAll()\n\n        assertThat(projects.size, `is`(3))\n        assertThat(projects[0], `is`(Project.Saved(projects[0].uuid, projectX)))\n        assertThat(projects[1], `is`(Project.Saved(projects[1].uuid, projectY)))\n        assertThat(projects[2], `is`(Project.Saved(projects[2].uuid, projectZ)))\n    }\n\n    @Test\n    fun `getAll() returns projects in created order`() {\n        val clock = mock(Supplier::class.java) as Supplier<Long>\n        projectsRepository = buildSubject(clock)\n\n        `when`(clock.get()).thenReturn(2)\n        projectsRepository.save(projectX)\n\n        `when`(clock.get()).thenReturn(1)\n        projectsRepository.save(projectY)\n\n        val projects = projectsRepository.getAll()\n        assertThat(projects[0].name, `is`(projectY.name))\n        assertThat(projects[1].name, `is`(projectX.name))\n    }\n\n    @Test\n    fun `save() should save project to storage`() {\n        projectsRepository.save(projectX)\n\n        val projects = projectsRepository.getAll()\n\n        assertThat(projects.size, `is`(1))\n        assertThat(projects[0], `is`(Project.Saved(projects[0].uuid, projectX)))\n    }\n\n    @Test\n    fun `save() should add uuid if not specified`() {\n        projectsRepository.save(projectX)\n        assertThat(projectsRepository.getAll()[0].uuid, `is`(not(isEmptyString())))\n    }\n\n    @Test\n    fun `save() adds project with uuid if specified`() {\n        projectsRepository.save(Project.Saved(\"blah\", projectX))\n        assertThat(projectsRepository.getAll()[0].uuid, `is`(\"blah\"))\n    }\n\n    @Test\n    fun `projects added with uuid are still sorted`() {\n        val clock = mock(Supplier::class.java) as Supplier<Long>\n        projectsRepository = buildSubject(clock)\n\n        `when`(clock.get()).thenReturn(2)\n        projectsRepository.save(Project.Saved(\"blah1\", projectX))\n\n        `when`(clock.get()).thenReturn(1)\n        projectsRepository.save(Project.Saved(\"blah2\", projectY))\n\n        assertThat(projectsRepository.getAll()[0].uuid, `is`(\"blah2\"))\n        assertThat(projectsRepository.getAll()[1].uuid, `is`(\"blah1\"))\n    }\n\n    @Test\n    fun `save() should update project if already exists`() {\n        projectsRepository.save(projectX)\n        projectsRepository.save(projectY)\n        projectsRepository.save(projectZ)\n\n        val originalProjectX = projectsRepository.getAll()[0]\n        val updatedProjectX = originalProjectX.copy(name = \"Project X2\", icon = \"2\", color = \"#ff80ff\")\n        projectsRepository.save(updatedProjectX)\n\n        val projects = projectsRepository.getAll()\n        assertThat(projects.size, `is`(3))\n        assertThat(projects[0], `is`(updatedProjectX))\n        assertThat(projects[1], `is`(Project.Saved(projects[1].uuid, projectY)))\n        assertThat(projects[2], `is`(Project.Saved(projects[2].uuid, projectZ)))\n    }\n\n    @Test\n    fun `updating project does not change its sort order`() {\n        val clock = mock(Supplier::class.java) as Supplier<Long>\n        projectsRepository = buildSubject(clock)\n\n        `when`(clock.get()).thenReturn(2)\n        projectsRepository.save(Project.Saved(\"blah1\", projectX))\n\n        `when`(clock.get()).thenReturn(1)\n        val savedProjectY = projectsRepository.save(Project.Saved(\"blah2\", projectY))\n\n        `when`(clock.get()).thenReturn(3)\n        projectsRepository.save(savedProjectY)\n\n        assertThat(projectsRepository.getAll()[0].uuid, `is`(\"blah2\"))\n        assertThat(projectsRepository.getAll()[1].uuid, `is`(\"blah1\"))\n    }\n\n    @Test\n    fun `save returns project with id`() {\n        val project = projectsRepository.save(projectX)\n        assertThat(project.uuid, `is`(notNullValue()))\n    }\n\n    @Test\n    fun `delete() should delete project from storage for given uuid`() {\n        projectsRepository.save(projectX)\n        projectsRepository.save(projectY)\n\n        var projects = projectsRepository.getAll()\n        projectsRepository.delete(projects.first { it.name == \"ProjectX\" }.uuid)\n        projects = projectsRepository.getAll()\n\n        assertThat(projects.size, `is`(1))\n        assertThat(projects[0], `is`(Project.Saved(projects[0].uuid, projectY)))\n    }\n\n    @Test\n    fun `deleteAll() should delete all projects from storage`() {\n        projectsRepository.save(projectX)\n        projectsRepository.save(projectY)\n\n        projectsRepository.deleteAll()\n        val projects = projectsRepository.getAll()\n\n        assertThat(projects.size, `is`(0))\n    }\n}\n"
  },
  {
    "path": "projects/src/test/java/org/odk/collect/projects/SharedPreferencesProjectsRepositoryTest.kt",
    "content": "package org.odk.collect.projects\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport com.google.gson.Gson\nimport org.junit.runner.RunWith\nimport org.odk.collect.shared.settings.InMemSettings\nimport org.odk.collect.shared.strings.UUIDGenerator\nimport java.util.function.Supplier\n\n@RunWith(AndroidJUnit4::class)\nclass SharedPreferencesProjectsRepositoryTest : ProjectsRepositoryTest() {\n    override fun buildSubject(): ProjectsRepository {\n        return SharedPreferencesProjectsRepository(\n            UUIDGenerator(),\n            Gson(),\n            InMemSettings(),\n            \"test\"\n        )\n    }\n\n    override fun buildSubject(clock: Supplier<Long>): ProjectsRepository {\n        return SharedPreferencesProjectsRepository(UUIDGenerator(), Gson(), InMemSettings(), \"test\", clock)\n    }\n}\n"
  },
  {
    "path": "projects/src/test/java/org/odk/collect/projects/support/RobolectricApplication.kt",
    "content": "package org.odk.collect.projects.support\n\nimport android.app.Application\nimport org.odk.collect.projects.DaggerProjectsDependencyComponent\nimport org.odk.collect.projects.ProjectsDependencyComponent\nimport org.odk.collect.projects.ProjectsDependencyComponentProvider\n\nclass RobolectricApplication : Application(), ProjectsDependencyComponentProvider {\n\n    override lateinit var projectsDependencyComponent: ProjectsDependencyComponent\n\n    override fun onCreate() {\n        super.onCreate()\n        projectsDependencyComponent = DaggerProjectsDependencyComponent.builder().build()\n    }\n}\n"
  },
  {
    "path": "projects/src/test/resources/robolectric.properties",
    "content": "application=org.odk.collect.projects.support.RobolectricApplication\nsdk=33"
  },
  {
    "path": "qr-code/.gitignore",
    "content": "/build"
  },
  {
    "path": "qr-code/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.composeCompiler)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.qrcode\"\n\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n        compose = true\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":androidshared\"))\n    implementation(project(\":strings\"))\n\n    implementation(libs.zxingAndroidEmbedded)\n    implementation(libs.mlkit.barcodescanning)\n    implementation(libs.androidxLifecycleRuntimeKtx)\n    implementation(libs.androidMaterial)\n    implementation(libs.cameraxView)\n    implementation(libs.mlkit.barcodescanning)\n    implementation(libs.camera.mlkit.vision)\n\n    val composeBom = platform(libs.androidxComposeBom)\n    implementation(composeBom)\n    implementation(libs.androidXComposeMaterial)\n    implementation(libs.androidXComposeMaterialIconsExtended)\n    implementation(libs.androidXConstraintLayoutCompose)\n\n    implementation(libs.androidXComposePreview)\n    debugImplementation(libs.androidXComposeTooling)\n\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.robolectric)\n}\n"
  },
  {
    "path": "qr-code/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/BarcodeFilter.kt",
    "content": "package org.odk.collect.qrcode\n\nimport android.graphics.Rect\n\nclass BarcodeFilter(private val bounds: Rect, private val threshold: Int = 1) {\n\n    private var potential: BarcodeCandidate? = null\n    private var potentialOccurrences = 0\n\n    fun filter(barcodeCandidates: List<BarcodeCandidate>): DetectedState {\n        val candidate = barcodeCandidates.firstOrNull()\n        return if (candidate?.boundingBox != null && bounds.contains(candidate.boundingBox)) {\n            if (!candidate.bytes.contentEquals(potential?.bytes)) {\n                potential = candidate\n                potentialOccurrences = 0\n            }\n\n            potentialOccurrences++\n            if (potentialOccurrences == threshold) {\n                if (candidate.bytes == null) {\n                    DetectedState.None\n                } else if (candidate.utfContents.isNullOrEmpty()) {\n                    DetectedState.Full(DetectedBarcode.Bytes(candidate.format, candidate.bytes))\n                } else {\n                    DetectedState.Full(\n                        DetectedBarcode.Utf8(\n                            candidate.utfContents,\n                            candidate.format,\n                            candidate.bytes\n                        )\n                    )\n                }\n            } else {\n                DetectedState.Potential\n            }\n        } else {\n            potential = null\n            potentialOccurrences = 0\n            DetectedState.None\n        }\n    }\n}\n\nclass BarcodeCandidate(\n    val bytes: ByteArray?,\n    val utfContents: String?,\n    val boundingBox: Rect?,\n    val format: BarcodeFormat\n)\n\nsealed class DetectedBarcode {\n\n    abstract val bytes: ByteArray\n    abstract val format: BarcodeFormat\n\n    data class Utf8(\n        val contents: String,\n        override val format: BarcodeFormat,\n        override val bytes: ByteArray\n    ) : DetectedBarcode() {\n        /**\n         * Requires custom [equals] due to [ByteArray] field\n         */\n        override fun equals(other: Any?): Boolean {\n            if (this === other) return true\n            if (javaClass != other?.javaClass) return false\n\n            other as Utf8\n\n            if (contents != other.contents) return false\n            if (format != other.format) return false\n            if (!bytes.contentEquals(other.bytes)) return false\n\n            return true\n        }\n\n        override fun hashCode(): Int {\n            var result = contents.hashCode()\n            result = 31 * result + format.hashCode()\n            result = 31 * result + bytes.contentHashCode()\n            return result\n        }\n    }\n\n    data class Bytes(\n        override val format: BarcodeFormat,\n        override val bytes: ByteArray\n    ) : DetectedBarcode() {\n        /**\n         * Requires custom [equals] due to [ByteArray] field\n         */\n        override fun equals(other: Any?): Boolean {\n            if (this === other) return true\n            if (javaClass != other?.javaClass) return false\n\n            other as Bytes\n\n            if (format != other.format) return false\n            if (!bytes.contentEquals(other.bytes)) return false\n\n            return true\n        }\n\n        override fun hashCode(): Int {\n            var result = format.hashCode()\n            result = 31 * result + bytes.contentHashCode()\n            return result\n        }\n    }\n}\n\nenum class BarcodeFormat {\n    PDF417,\n    OTHER\n}\n"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/BarcodeScannerViewContainer.kt",
    "content": "package org.odk.collect.qrcode\n\nimport android.app.Activity\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.widget.FrameLayout\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\n\nclass BarcodeScannerViewContainer(context: Context, attrs: AttributeSet?) :\n    FrameLayout(context, attrs) {\n\n    lateinit var barcodeScannerView: BarcodeScannerView\n        private set\n\n    fun setup(\n        factory: Factory,\n        activity: Activity,\n        lifecycleOwner: LifecycleOwner,\n        qrOnly: Boolean = false,\n        useFrontCamera: Boolean = false,\n    ) {\n        barcodeScannerView =\n            factory.create(activity, lifecycleOwner, qrOnly, useFrontCamera)\n        addView(barcodeScannerView)\n    }\n\n    interface Factory {\n        fun create(\n            activity: Activity,\n            lifecycleOwner: LifecycleOwner,\n            qrOnly: Boolean = false,\n            useFrontCamera: Boolean,\n        ): BarcodeScannerView\n    }\n}\n\nabstract class BarcodeScannerView(context: Context) : FrameLayout(context) {\n\n    private val _latestBarcode = MutableLiveData<String>()\n    val latestBarcode: LiveData<String> = _latestBarcode\n\n    protected abstract fun scan(callback: (String) -> Unit)\n    abstract fun setTorchOn(on: Boolean)\n    abstract fun setTorchListener(torchListener: TorchListener)\n\n    abstract fun supportsFullScreenViewFinder(): Boolean\n    abstract fun setFullScreenViewFinder(fullScannerViewFinder: Boolean)\n\n    fun start() {\n        this.scan { result ->\n            _latestBarcode.value = result\n        }\n    }\n\n    interface TorchListener {\n        fun onTorchOn()\n        fun onTorchOff()\n    }\n}\n"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/DetectedState.kt",
    "content": "package org.odk.collect.qrcode\n\nsealed class DetectedState {\n    object None : DetectedState()\n    object Potential : DetectedState()\n    data class Full(val barcode: DetectedBarcode) : DetectedState()\n}\n"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/FlashlightToggle.kt",
    "content": "package org.odk.collect.qrcode\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.FlashlightOff\nimport androidx.compose.material.icons.filled.FlashlightOn\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\n\n@Composable\nfun FlashlightToggle(\n    modifier: Modifier = Modifier,\n    flashlightOn: Boolean = false,\n    onFlashlightToggled: () -> Unit = {}\n) {\n    IconButton(onClick = { onFlashlightToggled() }, modifier = modifier) {\n        if (flashlightOn) {\n            Icon(\n                Icons.Filled.FlashlightOn,\n                stringResource(org.odk.collect.strings.R.string.turn_off_flashlight),\n                tint = Color.White\n            )\n        } else {\n            Icon(\n                Icons.Filled.FlashlightOff,\n                stringResource(org.odk.collect.strings.R.string.turn_on_flashlight),\n                tint = Color.White\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun PreviewFlashlightToggleOn() {\n    MaterialTheme {\n        Box(Modifier.background(Color.Gray)) {\n            FlashlightToggle(flashlightOn = true)\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun PreviewFlashlightToggleOff() {\n    MaterialTheme {\n        Box(Modifier.background(Color.Gray)) {\n            FlashlightToggle(flashlightOn = false)\n        }\n    }\n}\n"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/ScannerControls.kt",
    "content": "package org.odk.collect.qrcode\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.safeDrawingPadding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ScreenRotation\nimport androidx.compose.material3.ExtendedFloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.constraintlayout.compose.ConstraintLayout\nimport org.odk.collect.strings.R\n\n@Composable\nfun ScannerControls(\n    showFlashLight: Boolean,\n    flashlightOn: Boolean,\n    fullScreenViewFinder: Boolean = false,\n    showFullScreenToggle: Boolean = true,\n    fullScreenToggleExtended: Boolean = false,\n    onFullScreenToggled: () -> Unit = {},\n    onFlashlightToggled: () -> Unit = {}\n) {\n    BoxWithConstraints {\n        val bottomOfViewFinder = with(LocalDensity.current) {\n            val (viewFinderOffset, viewFinderSize) = calculateViewFinder(\n                maxWidth.toPx(),\n                maxHeight.toPx(),\n                false\n            )\n\n            viewFinderOffset.y.toDp() + viewFinderSize.height.toDp()\n        }\n\n        val standardMargin =\n            dimensionResource(org.odk.collect.androidshared.R.dimen.margin_standard)\n\n        ConstraintLayout(modifier = Modifier.fillMaxSize()) {\n            val isLandscape =\n                LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE\n\n            if (showFullScreenToggle) {\n                val (fullScreenToggle) = createRefs()\n                ExtendedFloatingActionButton(\n                    onClick = onFullScreenToggled,\n                    icon = {\n                        Icon(Icons.Filled.ScreenRotation, stringResource(R.string.rotate_device))\n                    },\n                    text = { Text(stringResource(R.string.rotate_device)) },\n                    expanded = fullScreenToggleExtended && !fullScreenViewFinder,\n                    modifier = Modifier\n                        .safeDrawingPadding()\n                        .constrainAs(fullScreenToggle) {\n                            end.linkTo(parent.end, margin = standardMargin)\n\n                            if (isLandscape) {\n                                top.linkTo(parent.top, margin = standardMargin)\n                            } else {\n                                bottom.linkTo(parent.bottom, margin = standardMargin)\n                            }\n                        }\n                )\n            }\n\n            if (!fullScreenViewFinder) {\n                val (prompt, flashLightToggle) = createRefs()\n\n                if (!isLandscape) {\n                    Text(\n                        stringResource(R.string.barcode_scanner_prompt),\n                        color = Color.White,\n                        style = MaterialTheme.typography.bodyMedium,\n                        modifier = Modifier.constrainAs(prompt) {\n                            top.linkTo(parent.top, margin = bottomOfViewFinder + standardMargin)\n                            start.linkTo(parent.start)\n                            end.linkTo(parent.end)\n                        }\n                    )\n                }\n\n                if (showFlashLight) {\n                    FlashlightToggle(\n                        flashlightOn = flashlightOn,\n                        onFlashlightToggled = onFlashlightToggled,\n                        modifier = Modifier\n                            .safeDrawingPadding()\n                            .constrainAs(flashLightToggle) {\n                                if (isLandscape) {\n                                    start.linkTo(parent.start, margin = standardMargin)\n                                } else {\n                                    end.linkTo(parent.end, margin = standardMargin)\n                                }\n\n                                top.linkTo(parent.top, margin = standardMargin)\n                            }\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun Preview() {\n    MaterialTheme {\n        Box(Modifier.background(Color.Gray)) {\n            ScannerControls(\n                showFlashLight = true,\n                flashlightOn = false,\n                fullScreenViewFinder = false\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun PreviewFullScreen() {\n    MaterialTheme {\n        Box(Modifier.background(Color.Gray)) {\n            ScannerControls(\n                showFlashLight = true,\n                flashlightOn = false,\n                fullScreenViewFinder = true\n            )\n        }\n    }\n}\n\n@Preview(device = \"spec:width=411dp,height=891dp,orientation=landscape\")\n@Composable\nprivate fun PreviewLandscape() {\n    MaterialTheme {\n        Box(Modifier.background(Color.Gray)) {\n            ScannerControls(\n                showFlashLight = true,\n                flashlightOn = false,\n                fullScreenViewFinder = false\n            )\n        }\n    }\n}\n\n@Preview(device = \"spec:width=411dp,height=891dp,orientation=landscape\")\n@Composable\nprivate fun PreviewFullScreenLandscape() {\n    MaterialTheme {\n        Box(Modifier.background(Color.Gray)) {\n            ScannerControls(\n                showFlashLight = true,\n                flashlightOn = false,\n                fullScreenViewFinder = true\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/ScannerOverlay.kt",
    "content": "package org.odk.collect.qrcode\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.CornerRadius\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.BlendMode\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.graphics.CompositingStrategy\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.graphics.drawscope.translate\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport kotlin.math.min\n\n@Composable\nfun ScannerOverlay(\n    detectedState: DetectedState = DetectedState.None,\n    fullScreenViewFinder: Boolean = false\n) {\n    BoxWithConstraints {\n        val (viewFinderOffset, viewFinderSize) = with(LocalDensity.current) {\n            calculateViewFinder(maxWidth.toPx(), maxHeight.toPx(), fullScreenViewFinder)\n        }\n\n        val highlightWidth = if (fullScreenViewFinder) {\n            16.dp\n        } else {\n            4.dp\n        }\n\n        if (!fullScreenViewFinder) {\n            ViewFinder(viewFinderSize, viewFinderOffset)\n        }\n\n        ViewFinderHighlight(detectedState, viewFinderSize, viewFinderOffset, highlightWidth)\n\n        if (!fullScreenViewFinder) {\n            val topRight = Offset(viewFinderOffset.x + viewFinderSize.width, viewFinderOffset.y).dp()\n            val circleRadius = 18.dp\n            val circleOffset = DpOffset(x = topRight.x - circleRadius, y = topRight.y - circleRadius)\n            ViewFinderIcon(\n                detectedState,\n                modifier = Modifier\n                    .offset(circleOffset.x, circleOffset.y)\n                    .size(circleRadius * 2)\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ViewFinderHighlight(\n    detectedState: DetectedState,\n    viewFinderSize: Size,\n    viewFinderOffset: Offset,\n    width: Dp\n) {\n    val density = LocalDensity.current\n    val smallShapeCornerSize = MaterialTheme.shapes.small.topStart\n\n    if (detectedState != DetectedState.None) {\n        val outlineColor = if (detectedState == DetectedState.Potential) {\n            Color(0xFFFFC107)\n        } else {\n            Color(0xFF9CCC65)\n        }\n\n        Canvas(modifier = Modifier.fillMaxSize()) {\n            val cornerSizePx = smallShapeCornerSize.toPx(viewFinderSize, density)\n            val cornerRadius = CornerRadius(cornerSizePx, cornerSizePx)\n            drawRoundRect(\n                color = outlineColor,\n                topLeft = viewFinderOffset,\n                size = viewFinderSize,\n                style = Stroke(width = width.toPx()),\n                cornerRadius = cornerRadius\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ViewFinderIcon(\n    detectedState: DetectedState,\n    modifier: Modifier = Modifier\n) {\n    if (detectedState == DetectedState.None) {\n        return\n    }\n\n    val lightColor = if (detectedState == DetectedState.Potential) {\n        Color(0xFFFAEDC4)\n    } else {\n        Color(0xFFCAF1D8)\n    }\n\n    val tickIcon = rememberVectorPainter(Icons.Default.Check)\n    val crossIcon = rememberVectorPainter(Icons.Default.Close)\n    val icon = if (detectedState == DetectedState.Potential) {\n        crossIcon\n    } else {\n        tickIcon\n    }\n\n    val iconColor = if (detectedState == DetectedState.Potential) {\n        Color(0xFFFFA726)\n    } else {\n        Color(0xFF66BB6A)\n    }\n\n    Canvas(modifier = modifier.graphicsLayer(shadowElevation = 10f, shape = CircleShape)) {\n        val circleRadius = size.width / 2\n\n        drawCircle(\n            lightColor,\n            center = Offset(circleRadius, circleRadius),\n            radius = circleRadius\n        )\n\n        val iconSize = Size(24.dp.toPx(), 24.dp.toPx())\n        translate(\n            left = (size.width - iconSize.width) / 2,\n            top = (size.height - iconSize.height) / 2\n        ) {\n            with(icon) {\n                draw(iconSize, colorFilter = ColorFilter.tint(iconColor))\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ViewFinder(viewFinderSize: Size, offset: Offset) {\n    val density = LocalDensity.current\n    val smallShapeCornerSize = MaterialTheme.shapes.small.topStart\n\n    Canvas(\n        modifier = Modifier\n            .fillMaxSize()\n            .graphicsLayer {\n                // Make sure the resulting layer is drawn on screen in one visible move\n                compositingStrategy = CompositingStrategy.Offscreen\n            }\n    ) {\n        drawRect(\n            color = Color(0x80000000),\n            size = size\n        )\n\n        val cornerSizePx = smallShapeCornerSize.toPx(viewFinderSize, density)\n        val cornerRadius = CornerRadius(cornerSizePx, cornerSizePx)\n        drawRoundRect(\n            color = Color(0x00000000),\n            topLeft = offset,\n            size = viewFinderSize,\n            blendMode = BlendMode.Clear,\n            cornerRadius = cornerRadius\n        )\n    }\n}\n\n@Composable\nfun Offset.dp(): DpOffset {\n    val offset = this\n    return with(LocalDensity.current) {\n        DpOffset(offset.x.toDp(), offset.y.toDp())\n    }\n}\n\nfun calculateViewFinder(width: Float, height: Float, fullScreen: Boolean): Pair<Offset, Size> {\n    return if (fullScreen) {\n        val viewFinderSize = Size(width, height)\n        val viewFinderOffset = Offset(0f, 0f)\n        Pair(viewFinderOffset, viewFinderSize)\n    } else {\n        val viewFinderWidth = min(MAX_VIEWFINDER_WIDTH, width / 100 * 75)\n        val viewFinderSize = Size(viewFinderWidth, viewFinderWidth)\n        val viewFinderOffset = Offset(\n            (width - viewFinderSize.width) / 2,\n            (height - viewFinderSize.height) / 2\n        )\n\n        Pair(viewFinderOffset, viewFinderSize)\n    }\n}\n\nprivate const val MAX_VIEWFINDER_WIDTH = 820f\n\n@Preview\n@Composable\nprivate fun PreviewNone() {\n    MaterialTheme {\n        ScannerOverlay()\n    }\n}\n\n@Preview\n@Composable\nprivate fun PreviewPotential() {\n    MaterialTheme {\n        ScannerOverlay(\n            detectedState = DetectedState.Potential\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun PreviewFull() {\n    MaterialTheme {\n        ScannerOverlay(\n            detectedState = DetectedState.Full(\n                DetectedBarcode.Utf8(\n                    \"\",\n                    BarcodeFormat.OTHER,\n                    byteArrayOf()\n                )\n            )\n        )\n    }\n}\n\n@Preview(widthDp = 200, heightDp = 200)\n@Composable\nprivate fun PreviewSmall() {\n    MaterialTheme {\n        ScannerOverlay(detectedState = DetectedState.Potential)\n    }\n}\n\n@Preview\n@Composable\nprivate fun PreviewFullScreen() {\n    MaterialTheme {\n        ScannerOverlay(detectedState = DetectedState.Potential, fullScreenViewFinder = true)\n    }\n}\n"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/mlkit/MlKitBarcodeScannerViewFactory.kt",
    "content": "package org.odk.collect.qrcode.mlkit\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.Application\nimport android.content.Context\nimport android.graphics.Rect\nimport android.view.LayoutInflater\nimport androidx.camera.core.CameraSelector\nimport androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED\nimport androidx.camera.core.TorchState\nimport androidx.camera.mlkit.vision.MlKitAnalyzer\nimport androidx.camera.view.LifecycleCameraController\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.core.content.ContextCompat\nimport androidx.lifecycle.LifecycleOwner\nimport com.google.android.gms.common.moduleinstall.ModuleInstall\nimport com.google.mlkit.vision.barcode.BarcodeScannerOptions\nimport com.google.mlkit.vision.barcode.BarcodeScanning\nimport com.google.mlkit.vision.barcode.common.Barcode\nimport org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent\nimport org.odk.collect.qrcode.BarcodeCandidate\nimport org.odk.collect.qrcode.BarcodeFilter\nimport org.odk.collect.qrcode.BarcodeFormat\nimport org.odk.collect.qrcode.BarcodeScannerView\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.qrcode.DetectedBarcode\nimport org.odk.collect.qrcode.DetectedState\nimport org.odk.collect.qrcode.ScannerOverlay\nimport org.odk.collect.qrcode.calculateViewFinder\nimport org.odk.collect.qrcode.databinding.MlkitBarcodeScannerLayoutBinding\n\nclass MlKitBarcodeScannerViewFactory(private val scanThreshold: Int) :\n    BarcodeScannerViewContainer.Factory {\n    override fun create(\n        activity: Activity,\n        lifecycleOwner: LifecycleOwner,\n        qrOnly: Boolean,\n        useFrontCamera: Boolean\n    ): BarcodeScannerView {\n        return MlKitBarcodeScannerView(\n            activity,\n            lifecycleOwner,\n            qrOnly,\n            useFrontCamera,\n            scanThreshold\n        )\n    }\n\n    companion object {\n        private var ML_KIT_AVAILABLE = false\n\n        @JvmStatic\n        fun init(application: Application) {\n            try {\n                ModuleInstall.getClient(application)\n                    .areModulesAvailable(BarcodeScanning.getClient())\n                    .addOnSuccessListener {\n                        if (it.areModulesAvailable()) {\n                            ML_KIT_AVAILABLE = true\n                        }\n                    }\n            } catch (e: Exception) {\n                // Ignored\n            }\n        }\n\n        fun isAvailable(): Boolean {\n            return ML_KIT_AVAILABLE\n        }\n    }\n}\n\n@SuppressLint(\"ViewConstructor\")\nprivate class MlKitBarcodeScannerView(\n    context: Context,\n    private val lifecycleOwner: LifecycleOwner,\n    private val qrOnly: Boolean,\n    private val useFrontCamera: Boolean,\n    private val scanThreshold: Int\n) : BarcodeScannerView(context) {\n\n    private val binding =\n        MlkitBarcodeScannerLayoutBinding.inflate(LayoutInflater.from(context), this, true)\n    private val cameraController = LifecycleCameraController(context)\n    private val viewFinderRect = Rect()\n\n    private val detectedState = mutableStateOf<DetectedState>(DetectedState.None)\n    private val fullScreenViewFinderState = mutableStateOf(false)\n\n    init {\n        binding.composeView.setContextThemedContent {\n            ScannerOverlay(detectedState.value, fullScreenViewFinderState.value)\n        }\n    }\n\n    override fun onLayout(\n        changed: Boolean,\n        left: Int,\n        top: Int,\n        right: Int,\n        bottom: Int\n    ) {\n        updateViewFinderSize()\n        super.onLayout(changed, left, top, right, bottom)\n    }\n\n    override fun scan(callback: (String) -> Unit) {\n        if (useFrontCamera) {\n            cameraController.cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA\n        }\n\n        cameraController.bindToLifecycle(lifecycleOwner)\n        binding.preview.setController(cameraController)\n\n        val format = if (qrOnly) {\n            Barcode.FORMAT_QR_CODE\n        } else {\n            Barcode.FORMAT_ALL_FORMATS\n        }\n\n        val options = BarcodeScannerOptions.Builder()\n            .setBarcodeFormats(format)\n            .build()\n        val barcodeScanner = BarcodeScanning.getClient(options)\n\n        val executor = ContextCompat.getMainExecutor(context)\n        val barcodeFilter = BarcodeFilter(viewFinderRect, scanThreshold)\n        cameraController.setImageAnalysisAnalyzer(\n            executor,\n            MlKitAnalyzer(\n                listOf(barcodeScanner),\n                COORDINATE_SYSTEM_VIEW_REFERENCED,\n                executor\n            ) { result: MlKitAnalyzer.Result ->\n                val value = result.getValue(barcodeScanner)\n                val barcodeCandidates = value?.map { it.toCandidate() } ?: emptyList()\n                barcodeFilter.filter(barcodeCandidates).also {\n                    detectedState.value = it\n\n                    if (it is DetectedState.Full) {\n                        val contents = processBarcode(it.barcode)\n                        if (!contents.isNullOrEmpty()) {\n                            cameraController.unbind()\n                            callback(contents)\n                        }\n                    }\n                }\n            }\n        )\n    }\n\n    override fun setTorchOn(on: Boolean) {\n        cameraController.enableTorch(on)\n    }\n\n    override fun setTorchListener(torchListener: TorchListener) {\n        cameraController.torchState.observe(lifecycleOwner) {\n            if (it == TorchState.ON) {\n                torchListener.onTorchOn()\n            } else if (it == TorchState.OFF) {\n                torchListener.onTorchOff()\n            }\n        }\n    }\n\n    override fun supportsFullScreenViewFinder(): Boolean {\n        return true\n    }\n\n    override fun setFullScreenViewFinder(fullScreenViewFinder: Boolean) {\n        fullScreenViewFinderState.value = fullScreenViewFinder\n        updateViewFinderSize()\n    }\n\n    private fun updateViewFinderSize() {\n        val (viewFinderOffset, viewFinderSize) = calculateViewFinder(\n            this.width.toFloat(),\n            this.height.toFloat(),\n            fullScreenViewFinderState.value\n        )\n\n        viewFinderRect.set(\n            viewFinderOffset.x.toInt(),\n            viewFinderOffset.y.toInt(),\n            (viewFinderOffset.x + viewFinderSize.width).toInt(),\n            (viewFinderOffset.y + viewFinderSize.height).toInt()\n        )\n    }\n\n    private fun processBarcode(barcode: DetectedBarcode): String? {\n        return when (barcode) {\n            is DetectedBarcode.Utf8 -> {\n                barcode.contents\n            }\n\n            is DetectedBarcode.Bytes -> {\n                if (barcode.format == BarcodeFormat.PDF417) {\n                    /**\n                     * Allow falling back to Latin encoding for PDF417 barcodes. This provides parity\n                     * with the Zxing implementation.\n                     */\n                    String(barcode.bytes, Charsets.ISO_8859_1)\n                } else {\n                    null\n                }\n            }\n        }\n    }\n\n    companion object {\n\n        private fun Barcode.toCandidate(): BarcodeCandidate {\n            val format = when (this.format) {\n                Barcode.FORMAT_PDF417 -> BarcodeFormat.PDF417\n                else -> BarcodeFormat.OTHER\n            }\n\n            return BarcodeCandidate(this.rawBytes, this.rawValue, this.boundingBox, format)\n        }\n    }\n}\n"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/mlkit/PlayServicesFallbackBarcodeScannerViewFactory.kt",
    "content": "package org.odk.collect.qrcode.mlkit\n\nimport android.app.Activity\nimport androidx.lifecycle.LifecycleOwner\nimport org.odk.collect.qrcode.BarcodeScannerView\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.qrcode.zxing.ZxingBarcodeScannerViewFactory\n\nclass PlayServicesFallbackBarcodeScannerViewFactory(mlkitScanThreshold: Int) : BarcodeScannerViewContainer.Factory {\n\n    private val mlKitBarcodeScannerViewFactory = MlKitBarcodeScannerViewFactory(mlkitScanThreshold)\n    private val zxingBarcodeScannerViewFactory = ZxingBarcodeScannerViewFactory()\n\n    override fun create(\n        activity: Activity,\n        lifecycleOwner: LifecycleOwner,\n        qrOnly: Boolean,\n        useFrontCamera: Boolean\n    ): BarcodeScannerView {\n        if (MlKitBarcodeScannerViewFactory.isAvailable()) {\n            return mlKitBarcodeScannerViewFactory.create(\n                activity,\n                lifecycleOwner,\n                qrOnly,\n                useFrontCamera\n            )\n        } else {\n            return zxingBarcodeScannerViewFactory.create(\n                activity,\n                lifecycleOwner,\n                qrOnly,\n                useFrontCamera\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/zxing/QRCodeCreator.kt",
    "content": "package org.odk.collect.qrcode.zxing\n\nimport android.graphics.Bitmap\nimport android.graphics.Color\nimport com.google.zxing.BarcodeFormat\nimport com.google.zxing.EncodeHintType\nimport com.google.zxing.qrcode.QRCodeWriter\nimport com.google.zxing.qrcode.decoder.ErrorCorrectionLevel\nimport org.odk.collect.androidshared.utils.CompressionUtils\n\nclass QRCodeCreatorImpl : QRCodeCreator {\n    @Throws(QRCodeCreator.MaximumCharactersLimitException::class)\n    override fun createEncoded(data: String): Bitmap {\n        val compressedData = CompressionUtils.compress(data)\n        return create(compressedData)\n    }\n\n    @Throws(QRCodeCreator.MaximumCharactersLimitException::class)\n    override fun create(data: String): Bitmap {\n        // Maximum capacity for QR Codes is 4,296 characters (Alphanumeric)\n        if (data.length > 4000) {\n            throw QRCodeCreator.MaximumCharactersLimitException()\n        }\n\n        val hints: Map<EncodeHintType, ErrorCorrectionLevel> = mapOf(EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.L)\n\n        val bitMatrix = QRCodeWriter().encode(\n            data,\n            BarcodeFormat.QR_CODE,\n            QR_CODE_SIDE_LENGTH,\n            QR_CODE_SIDE_LENGTH,\n            hints\n        )\n\n        val bmp = Bitmap.createBitmap(\n            QR_CODE_SIDE_LENGTH,\n            QR_CODE_SIDE_LENGTH,\n            Bitmap.Config.RGB_565\n        )\n\n        for (x in 0 until QR_CODE_SIDE_LENGTH) {\n            for (y in 0 until QR_CODE_SIDE_LENGTH) {\n                bmp.setPixel(x, y, if (bitMatrix[x, y]) Color.BLACK else Color.WHITE)\n            }\n        }\n\n        return bmp\n    }\n\n    private companion object {\n        private const val QR_CODE_SIDE_LENGTH = 400 // in pixels\n    }\n}\n\ninterface QRCodeCreator {\n    @Throws(MaximumCharactersLimitException::class)\n    fun create(data: String): Bitmap\n\n    @Throws(MaximumCharactersLimitException::class)\n    fun createEncoded(data: String): Bitmap\n\n    class MaximumCharactersLimitException : Exception()\n}\n"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/zxing/QRCodeDecoder.kt",
    "content": "package org.odk.collect.qrcode.zxing\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport com.google.zxing.BarcodeFormat\nimport com.google.zxing.BinaryBitmap\nimport com.google.zxing.DecodeHintType\nimport com.google.zxing.LuminanceSource\nimport com.google.zxing.NotFoundException\nimport com.google.zxing.RGBLuminanceSource\nimport com.google.zxing.common.HybridBinarizer\nimport com.google.zxing.multi.qrcode.QRCodeMultiReader\nimport org.odk.collect.androidshared.utils.CompressionUtils\nimport java.io.IOException\nimport java.io.InputStream\nimport java.lang.Boolean.TRUE\nimport java.util.zip.DataFormatException\nimport kotlin.Exception\nimport kotlin.IllegalArgumentException\nimport kotlin.IntArray\nimport kotlin.String\nimport kotlin.Throws\n\nclass QRCodeDecoderImpl : QRCodeDecoder {\n    @Throws(QRCodeDecoder.QRCodeInvalidException::class, QRCodeDecoder.QRCodeNotFoundException::class)\n    override fun decode(inputStream: InputStream?): String {\n        return try {\n            val bitmap = BitmapFactory.decodeStream(inputStream)\n\n            val decodedQrCode = try {\n                // There are QR codes that can't be decoded if DecodeHintType.PURE_BARCODE is present\n                QRCodeMultiReader()\n                    .decode(\n                        getBinaryBitmap(bitmap),\n                        mapOf(\n                            DecodeHintType.TRY_HARDER to TRUE,\n                            DecodeHintType.POSSIBLE_FORMATS to BarcodeFormat.QR_CODE\n                        )\n                    )\n            } catch (e: NotFoundException) {\n                // There are QR codes that can't be decoded if DecodeHintType.PURE_BARCODE is not present\n                QRCodeMultiReader()\n                    .decode(\n                        getBinaryBitmap(bitmap),\n                        mapOf(\n                            DecodeHintType.TRY_HARDER to TRUE,\n                            DecodeHintType.POSSIBLE_FORMATS to BarcodeFormat.QR_CODE,\n                            DecodeHintType.PURE_BARCODE to false\n                        )\n                    )\n            }\n\n            CompressionUtils.decompress(decodedQrCode.text)\n        } catch (e: DataFormatException) {\n            throw QRCodeDecoder.QRCodeInvalidException()\n        } catch (e: IOException) {\n            throw QRCodeDecoder.QRCodeInvalidException()\n        } catch (e: IllegalArgumentException) {\n            throw QRCodeDecoder.QRCodeInvalidException()\n        } catch (e: Throwable) {\n            throw QRCodeDecoder.QRCodeNotFoundException()\n        }\n    }\n\n    private fun getBinaryBitmap(bitmap: Bitmap): BinaryBitmap {\n        val intArray = IntArray(bitmap.width * bitmap.height)\n\n        // copy pixel data from bitmap into the array\n        bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)\n        val source: LuminanceSource = RGBLuminanceSource(bitmap.width, bitmap.height, intArray)\n        return BinaryBitmap(HybridBinarizer(source))\n    }\n}\n\ninterface QRCodeDecoder {\n    @Throws(QRCodeInvalidException::class, QRCodeNotFoundException::class)\n    fun decode(inputStream: InputStream?): String\n\n    class QRCodeInvalidException : Exception()\n    class QRCodeNotFoundException : Exception()\n}\n"
  },
  {
    "path": "qr-code/src/main/java/org/odk/collect/qrcode/zxing/ZxingBarcodeScannerViewFactory.kt",
    "content": "package org.odk.collect.qrcode.zxing\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Intent\nimport android.view.LayoutInflater\nimport androidx.lifecycle.DefaultLifecycleObserver\nimport androidx.lifecycle.LifecycleOwner\nimport com.google.zxing.client.android.Intents\nimport com.google.zxing.integration.android.IntentIntegrator\nimport com.journeyapps.barcodescanner.CaptureManager\nimport com.journeyapps.barcodescanner.DecoratedBarcodeView\nimport com.journeyapps.barcodescanner.camera.CameraSettings\nimport org.odk.collect.androidshared.system.CameraUtils\nimport org.odk.collect.qrcode.BarcodeScannerView\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\nimport org.odk.collect.qrcode.databinding.ZxingBarcodeScannerLayoutBinding\n\nclass ZxingBarcodeScannerViewFactory : BarcodeScannerViewContainer.Factory {\n    override fun create(\n        activity: Activity,\n        lifecycleOwner: LifecycleOwner,\n        qrOnly: Boolean,\n        useFrontCamera: Boolean\n    ): BarcodeScannerView {\n        return ZxingBarcodeScannerView(activity, lifecycleOwner, qrOnly, useFrontCamera)\n    }\n}\n\n@SuppressLint(\"ViewConstructor\")\nprivate class ZxingBarcodeScannerView(\n    private val activity: Activity,\n    private val lifecycleOwner: LifecycleOwner,\n    private val qrOnly: Boolean,\n    private val useFrontCamera: Boolean\n) : BarcodeScannerView(activity) {\n\n    private val binding =\n        ZxingBarcodeScannerLayoutBinding.inflate(LayoutInflater.from(activity), this, true)\n\n    override fun scan(callback: (String) -> Unit) {\n        val supportedFormats = if (qrOnly) {\n            listOf(IntentIntegrator.QR_CODE)\n        } else {\n            IntentIntegrator.ALL_CODE_TYPES\n        }\n\n        val captureManager = getCaptureManager(\n            activity,\n            binding.barcodeView,\n            supportedFormats\n        )\n\n        lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {\n            override fun onResume(owner: LifecycleOwner) {\n                binding.barcodeView.resume()\n                captureManager.onResume()\n            }\n\n            override fun onPause(owner: LifecycleOwner) {\n                binding.barcodeView.pauseAndWait()\n                captureManager.onPause()\n            }\n\n            override fun onDestroy(owner: LifecycleOwner) {\n                captureManager.onDestroy()\n            }\n        })\n\n        if (useFrontCamera) {\n            val cameraSettings = CameraSettings()\n            cameraSettings.requestedCameraId = CameraUtils.getFrontCameraId()\n            binding.barcodeView.barcodeView.cameraSettings = cameraSettings\n        }\n\n        binding.barcodeView.decodeSingle {\n            captureManager.onDestroy()\n            callback(it.text)\n        }\n    }\n\n    override fun setTorchOn(on: Boolean) {\n        if (on) {\n            binding.barcodeView.setTorchOn()\n        } else {\n            binding.barcodeView.setTorchOff()\n        }\n    }\n\n    override fun setTorchListener(torchListener: TorchListener) {\n        binding.barcodeView.setTorchListener(object : com.journeyapps.barcodescanner.DecoratedBarcodeView.TorchListener {\n            override fun onTorchOn() {\n                torchListener.onTorchOn()\n            }\n\n            override fun onTorchOff() {\n                torchListener.onTorchOff()\n            }\n        })\n    }\n\n    override fun supportsFullScreenViewFinder(): Boolean {\n        return false\n    }\n\n    override fun setFullScreenViewFinder(fullScannerViewFinder: Boolean) {\n        // Ignored\n    }\n\n    private fun getCaptureManager(\n        activity: Activity,\n        barcodeView: DecoratedBarcodeView,\n        supportedFormats: Collection<String>?,\n        prompt: String = \"\"\n    ): CaptureManager {\n        val captureManager = CaptureManager(activity, barcodeView)\n        captureManager.initializeFromIntent(getIntent(activity, supportedFormats, prompt), null)\n        captureManager.decode()\n        return captureManager\n    }\n\n    private fun getIntent(activity: Activity, supportedFormats: Collection<String>?, prompt: String = \"\"): Intent {\n        return IntentIntegrator(activity)\n            .setDesiredBarcodeFormats(supportedFormats)\n            .setPrompt(prompt)\n            .setOrientationLocked(false) // Let UI control orientation lock\n            .addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN)\n            .createScanIntent()\n    }\n}\n"
  },
  {
    "path": "qr-code/src/main/res/layout/mlkit_barcode_scanner_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.camera.view.PreviewView\n        android:id=\"@+id/preview\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <androidx.compose.ui.platform.ComposeView\n        android:id=\"@+id/compose_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "qr-code/src/main/res/layout/zxing_barcode_scanner_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n    <com.journeyapps.barcodescanner.DecoratedBarcodeView\n        android:id=\"@+id/barcode_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n</FrameLayout>\n"
  },
  {
    "path": "qr-code/src/test/java/org/odk/collect/qrcode/BarcodeFilterTest.kt",
    "content": "package org.odk.collect.qrcode\n\nimport android.graphics.Rect\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.hamcrest.Matchers.isA\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\n@RunWith(AndroidJUnit4::class)\nclass BarcodeFilterTest {\n\n    @Test\n    fun `only returns barcode after threshold met`() {\n        val barcodeFilter = BarcodeFilter(Rect(0, 0, 100, 100), 2)\n\n        val candidate =\n            BarcodeCandidate(byteArrayOf(0), \"blah\", Rect(50, 50, 50, 50), BarcodeFormat.OTHER)\n        assertThat(barcodeFilter.filter(listOf(candidate)), equalTo(DetectedState.Potential))\n        assertThat(barcodeFilter.filter(listOf(candidate)), isA(DetectedState.Full::class.java))\n    }\n\n    @Test\n    fun `requires threshold to be met in sequence`() {\n        val barcodeFilter = BarcodeFilter(Rect(0, 0, 100, 100), 2)\n\n        val candidate =\n            BarcodeCandidate(byteArrayOf(0), \"blah\", Rect(50, 50, 50, 50), BarcodeFormat.OTHER)\n        assertThat(barcodeFilter.filter(listOf(candidate)), equalTo(DetectedState.Potential))\n\n        val other =\n            BarcodeCandidate(byteArrayOf(1), \"blah\", Rect(50, 50, 50, 50), BarcodeFormat.OTHER)\n        assertThat(barcodeFilter.filter(listOf(other)), equalTo(DetectedState.Potential))\n\n        assertThat(barcodeFilter.filter(listOf(candidate)), equalTo(DetectedState.Potential))\n\n        val barcode = (barcodeFilter.filter(listOf(candidate)) as DetectedState.Full).barcode\n        assertThat(barcode.bytes.contentEquals(candidate.bytes), equalTo(true))\n    }\n\n    @Test\n    fun `an empty list of candidates clears sequence`() {\n        val barcodeFilter = BarcodeFilter(Rect(0, 0, 100, 100), 2)\n\n        val candidate =\n            BarcodeCandidate(byteArrayOf(0), \"blah\", Rect(50, 50, 50, 50), BarcodeFormat.OTHER)\n        assertThat(barcodeFilter.filter(listOf(candidate)), equalTo(DetectedState.Potential))\n\n        assertThat(barcodeFilter.filter(emptyList()), equalTo(DetectedState.None))\n\n        assertThat(barcodeFilter.filter(listOf(candidate)), equalTo(DetectedState.Potential))\n\n        val barcode = (barcodeFilter.filter(listOf(candidate)) as DetectedState.Full).barcode\n        assertThat(barcode.bytes.contentEquals(candidate.bytes), equalTo(true))\n    }\n\n    @Test\n    fun `returns UTF8 barcode when candidate has UTF8 contents`() {\n        val barcodeFilter = BarcodeFilter(Rect(0, 0, 100, 100))\n        val candidate =\n            BarcodeCandidate(byteArrayOf(0), \"blah\", Rect(50, 50, 50, 50), BarcodeFormat.OTHER)\n        assertThat(\n            barcodeFilter.filter(listOf(candidate)),\n            equalTo(\n                DetectedState.Full(\n                    DetectedBarcode.Utf8(\"blah\", BarcodeFormat.OTHER, byteArrayOf(0))\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `returns Bytes barcode when candidate has no UTF8 contents`() {\n        val barcodeFilter = BarcodeFilter(Rect(0, 0, 100, 100))\n        val candidate =\n            BarcodeCandidate(byteArrayOf(0), null, Rect(50, 50, 50, 50), BarcodeFormat.OTHER)\n        assertThat(\n            barcodeFilter.filter(listOf(candidate)),\n            equalTo(DetectedState.Full(DetectedBarcode.Bytes(BarcodeFormat.OTHER, byteArrayOf(0))))\n        )\n    }\n\n    @Test\n    fun `returns Bytes barcode when candidate has empty UTF8 contents`() {\n        val barcodeFilter = BarcodeFilter(Rect(0, 0, 100, 100))\n        val candidate =\n            BarcodeCandidate(byteArrayOf(0), \"\", Rect(50, 50, 50, 50), BarcodeFormat.OTHER)\n        assertThat(\n            barcodeFilter.filter(listOf(candidate)),\n            equalTo(DetectedState.Full(DetectedBarcode.Bytes(BarcodeFormat.OTHER, byteArrayOf(0))))\n        )\n    }\n\n    @Test\n    fun `returns None when candidate has no bytes`() {\n        val barcodeFilter = BarcodeFilter(Rect(0, 0, 100, 100))\n        val candidate = BarcodeCandidate(null, null, Rect(50, 50, 50, 50), BarcodeFormat.OTHER)\n        assertThat(barcodeFilter.filter(listOf(candidate)), equalTo(DetectedState.None))\n    }\n}\n"
  },
  {
    "path": "qr-code/src/test/java/org/odk/collect/qrcode/QRCodeEncodeDecodeTest.kt",
    "content": "package org.odk.collect.qrcode\n\nimport android.app.Application\nimport android.graphics.Bitmap\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.qrcode.zxing.QRCodeCreator\nimport org.odk.collect.qrcode.zxing.QRCodeCreatorImpl\nimport org.odk.collect.qrcode.zxing.QRCodeDecoderImpl\nimport java.io.ByteArrayInputStream\nimport java.io.ByteArrayOutputStream\nimport java.io.InputStream\n\n@RunWith(AndroidJUnit4::class)\nclass QRCodeEncodeDecodeTest {\n\n    @Test\n    fun `Encoded and decoded text should be unchanged`() {\n        val data = \"Some random text\"\n\n        val qrCodeEncoder = QRCodeCreatorImpl()\n        val qrCodeDecoder = QRCodeDecoderImpl()\n\n        val encodedData = qrCodeEncoder.createEncoded(data)\n\n        val decodedData = qrCodeDecoder.decode(toStream(encodedData))\n\n        assertThat(decodedData, equalTo(data))\n    }\n\n    @Test(expected = QRCodeCreator.MaximumCharactersLimitException::class)\n    fun `When there are more than 4k characters passed to encode, throw an exception`() {\n        val data = List(5000) { ('a'..'z').random() }.joinToString(\"\")\n\n        QRCodeCreatorImpl().createEncoded(data)\n    }\n\n    @Test\n    fun `Decoding a QR code that is a screenshot from ODK Collect does not throw any exception`() {\n        val imageStream: InputStream = ApplicationProvider.getApplicationContext<Application>().assets.open(\"qrScreenshotFromCollect.png\")\n        QRCodeDecoderImpl().decode(imageStream)\n    }\n\n    @Test\n    fun `Decoding a QR code that is shared via ODK Collect does not throw any exception`() {\n        val imageStream: InputStream = ApplicationProvider.getApplicationContext<Application>().assets.open(\"qrSharedViaCollect.png\")\n        QRCodeDecoderImpl().decode(imageStream)\n    }\n\n    private fun toStream(bitmap: Bitmap): ByteArrayInputStream {\n        val bos = ByteArrayOutputStream()\n        bitmap.compress(Bitmap.CompressFormat.PNG, 0, bos)\n        val bitmapData = bos.toByteArray()\n        return ByteArrayInputStream(bitmapData)\n    }\n}\n"
  },
  {
    "path": "qr-code/src/test/resources/robolectric.properties",
    "content": "sdk=33\n"
  },
  {
    "path": "secrets.gradle",
    "content": "// Helper for grabbing secrets from `secrets.properties` file\next.getSecrets = { ->\n    def secretsFile = file(\"$rootDir/secrets.properties\")\n    def oldSecretsFile = file(\"$rootDir/collect_app/secrets.properties\")\n\n    if (!secretsFile.exists() && oldSecretsFile.exists()) {\n        throw new InvalidUserDataException(\"Please move secrets.properties to the root directory!\")\n    }\n\n    def secrets = new Properties()\n    if (secretsFile.exists()) {\n        secrets.load(new FileInputStream(secretsFile))\n    }\n\n    return secrets\n}\n"
  },
  {
    "path": "selfie-camera/.gitignore",
    "content": "/build"
  },
  {
    "path": "selfie-camera/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.selfiecamera\"\n\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":androidshared\"))\n    implementation(project(\":strings\"))\n    implementation(project(\":permissions\"))\n    implementation(project(\":external-app\"))\n    implementation(project(\":analytics\"))\n\n    implementation(libs.cameraxCore)\n    implementation(libs.cameraxView)\n    implementation(libs.cameraxLifecycle)\n    implementation(libs.cameraxVideo)\n    implementation(libs.cameraxCamera2)\n    implementation(\"com.google.guava:guava:33.0.0-android\") // Guava is a dependency required by CameraX. It shouldn't be used in any other context and should be removed when no longer necessary.\n    implementation(libs.dagger)\n    ksp(libs.daggerCompiler)\n\n    testImplementation(project(\":androidtest\"))\n\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.robolectric)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.androidxTestEspressoCore)\n}\n"
  },
  {
    "path": "selfie-camera/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <application>\n        <activity android:name=\"org.odk.collect.selfiecamera.CaptureSelfieActivity\"\n            android:theme=\"@style/Theme.AppCompat\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "selfie-camera/src/main/java/org/odk/collect/selfiecamera/Camera.kt",
    "content": "package org.odk.collect.selfiecamera\n\nimport android.view.View\nimport androidx.activity.ComponentActivity\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\n\ninternal interface Camera {\n    fun initialize(activity: ComponentActivity, previewView: View)\n\n    fun state(): NonNullLiveData<State>\n\n    fun takePicture(imagePath: String, onImageSaved: () -> Unit, onImageSaveError: () -> Unit)\n\n    enum class State {\n        UNINITIALIZED,\n        INITIALIZED,\n        FAILED_TO_INITIALIZE\n    }\n}\n"
  },
  {
    "path": "selfie-camera/src/main/java/org/odk/collect/selfiecamera/CameraXCamera.kt",
    "content": "package org.odk.collect.selfiecamera\n\nimport android.view.View\nimport androidx.activity.ComponentActivity\nimport androidx.camera.core.CameraSelector\nimport androidx.camera.core.ImageCapture\nimport androidx.camera.core.ImageCaptureException\nimport androidx.camera.core.Preview\nimport androidx.camera.lifecycle.ProcessCameraProvider\nimport androidx.camera.view.PreviewView\nimport androidx.core.content.ContextCompat\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\nimport java.io.File\n\ninternal class CameraXCamera : Camera {\n\n    private var activity: ComponentActivity? = null\n    private var state = MutableNonNullLiveData(Camera.State.UNINITIALIZED)\n    private var imageCapture = ImageCapture.Builder().build()\n\n    override fun initialize(activity: ComponentActivity, previewView: View) {\n        this.activity = activity\n\n        val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)\n        cameraProviderFuture.addListener(\n            {\n                val cameraProvider = cameraProviderFuture.get()\n\n                val preview = Preview.Builder().build()\n                preview.setSurfaceProvider((previewView as PreviewView).surfaceProvider)\n\n                try {\n                    cameraProvider.bindToLifecycle(\n                        activity,\n                        CameraSelector.DEFAULT_FRONT_CAMERA,\n                        preview,\n                        imageCapture\n                    )\n\n                    state.value = Camera.State.INITIALIZED\n                } catch (e: IllegalArgumentException) {\n                    state.value = Camera.State.FAILED_TO_INITIALIZE\n                }\n            },\n            ContextCompat.getMainExecutor(activity)\n        )\n    }\n\n    override fun state(): NonNullLiveData<Camera.State> {\n        return state\n    }\n\n    override fun takePicture(\n        imagePath: String,\n        onImageSaved: () -> Unit,\n        onImageSaveError: () -> Unit\n    ) {\n        Pair(imageCapture, activity).let { (i, a) ->\n            if (i == null || a == null) {\n                return\n            }\n\n            val outputFile = File(imagePath)\n            val outputFileOptions = ImageCapture.OutputFileOptions.Builder(outputFile).build()\n\n            i.takePicture(\n                outputFileOptions,\n                ContextCompat.getMainExecutor(a),\n                object : ImageCapture.OnImageSavedCallback {\n                    override fun onError(error: ImageCaptureException) {\n                        onImageSaveError()\n                    }\n\n                    override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {\n                        onImageSaved()\n                    }\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "selfie-camera/src/main/java/org/odk/collect/selfiecamera/CaptureSelfieActivity.kt",
    "content": "/*\n * Copyright 2017 Nafundi\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.odk.collect.selfiecamera\n\nimport android.Manifest\nimport android.os.Bundle\nimport android.view.View\nimport androidx.core.view.WindowCompat\nimport androidx.core.view.WindowInsetsCompat\nimport org.odk.collect.androidshared.ui.EdgeToEdge.setView\nimport org.odk.collect.androidshared.ui.ToastUtils.showLongToast\nimport org.odk.collect.externalapp.ExternalAppUtils\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.strings.localization.LocalizedActivity\nimport javax.inject.Inject\n\nclass CaptureSelfieActivity : LocalizedActivity() {\n\n    @Inject\n    internal lateinit var camera: Camera\n\n    @Inject\n    lateinit var permissionsChecker: PermissionsChecker\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        (application as SelfieCameraDependencyComponentProvider)\n            .selfieCameraDependencyComponent\n            .inject(this)\n\n        super.onCreate(savedInstanceState)\n\n        if (!permissionsGranted()) {\n            finish()\n            return\n        }\n\n        val insetsController = WindowCompat.getInsetsController(window, window.decorView)\n        insetsController.hide(WindowInsetsCompat.Type.statusBars())\n        setView(R.layout.activity_capture_selfie, false)\n\n        val previewView = findViewById<View>(R.id.preview)\n\n        camera.initialize(this, previewView)\n        camera.state().observe(this) {\n            when (it) {\n                Camera.State.UNINITIALIZED -> {}\n                Camera.State.INITIALIZED -> setupCamera(camera)\n                Camera.State.FAILED_TO_INITIALIZE -> {\n                    showLongToast(org.odk.collect.strings.R.string.camera_failed_to_initialize)\n                }\n            }\n        }\n    }\n\n    private fun setupCamera(camera: Camera) {\n        val previewView = findViewById<View>(R.id.preview)\n\n        val imagePath = intent.getStringExtra(EXTRA_TMP_PATH) + \"/tmp.jpg\"\n        previewView.setOnClickListener {\n            camera.takePicture(\n                imagePath,\n                { ExternalAppUtils.returnSingleValue(this, imagePath) },\n                { showLongToast(org.odk.collect.strings.R.string.camera_error) }\n            )\n        }\n\n        showLongToast(org.odk.collect.strings.R.string.take_picture_instruction)\n    }\n\n    private fun permissionsGranted(): Boolean {\n        return permissionsChecker.isPermissionGranted(Manifest.permission.CAMERA)\n    }\n\n    companion object {\n        const val EXTRA_TMP_PATH = \"tmpPath\"\n    }\n}\n"
  },
  {
    "path": "selfie-camera/src/main/java/org/odk/collect/selfiecamera/DaggerSetup.kt",
    "content": "package org.odk.collect.selfiecamera\n\nimport dagger.Component\nimport dagger.Module\nimport dagger.Provides\nimport org.odk.collect.permissions.PermissionsChecker\nimport javax.inject.Singleton\n\ninterface SelfieCameraDependencyComponentProvider {\n    val selfieCameraDependencyComponent: SelfieCameraDependencyComponent\n}\n\n@Component(modules = [SelfieCameraDependencyModule::class])\n@Singleton\ninterface SelfieCameraDependencyComponent {\n    fun inject(captureSelfieActivity: CaptureSelfieActivity)\n}\n\n@Module\nopen class SelfieCameraDependencyModule {\n\n    @Provides\n    open fun providesPermissionChecker(): PermissionsChecker {\n        throw UnsupportedOperationException(\"This should be overridden by dependent application\")\n    }\n\n    @Provides\n    internal open fun providesCamera(): Camera {\n        return CameraXCamera()\n    }\n}\n"
  },
  {
    "path": "selfie-camera/src/main/res/layout/activity_capture_selfie.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright 2017 Nafundi\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.camera.view.PreviewView\n        android:id=\"@+id/preview\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n</merge>\n"
  },
  {
    "path": "selfie-camera/src/test/java/org/odk/collect/selfiecamera/CaptureSelfieActivityTest.kt",
    "content": "package org.odk.collect.selfiecamera\n\nimport android.Manifest\nimport android.app.Activity\nimport android.content.Intent\nimport android.view.View\nimport androidx.activity.ComponentActivity\nimport androidx.lifecycle.Lifecycle\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.androidshared.livedata.MutableNonNullLiveData\nimport org.odk.collect.androidshared.livedata.NonNullLiveData\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.androidtest.ActivityScenarioLauncherRule\nimport org.odk.collect.externalapp.ExternalAppUtils\nimport org.odk.collect.permissions.PermissionsChecker\nimport org.odk.collect.selfiecamera.support.RobolectricApplication\n\n@RunWith(AndroidJUnit4::class)\nclass CaptureSelfieActivityTest {\n\n    private val application = ApplicationProvider.getApplicationContext<RobolectricApplication>()\n    private val permissionsChecker = FakePermissionsChecker()\n    private val camera = FakeCamera()\n\n    @get:Rule\n    val launcher = ActivityScenarioLauncherRule()\n\n    @Before\n    fun setup() {\n        application.selfieCameraDependencyComponent =\n            DaggerSelfieCameraDependencyComponent.builder()\n                .selfieCameraDependencyModule(object : SelfieCameraDependencyModule() {\n                    override fun providesPermissionChecker(): PermissionsChecker {\n                        return permissionsChecker\n                    }\n\n                    override fun providesCamera(): Camera {\n                        return camera\n                    }\n                })\n                .build()\n    }\n\n    @Test\n    fun whenCameraPermissionNotGranted_finishes() {\n        permissionsChecker.deny(Manifest.permission.CAMERA)\n\n        val scenario = launcher.launch(CaptureSelfieActivity::class.java)\n        assertThat(scenario.state, equalTo(Lifecycle.State.DESTROYED))\n    }\n\n    @Test\n    fun whenAudioPermissionNotGranted_doesNotFinish() {\n        permissionsChecker.deny(Manifest.permission.RECORD_AUDIO)\n\n        val scenario = launcher.launch(CaptureSelfieActivity::class.java)\n        assertThat(scenario.state, equalTo(Lifecycle.State.RESUMED))\n    }\n\n    @Test\n    fun clickingPreview_takesPictureAndSavesToFileInPath() {\n        val intent = Intent(application, CaptureSelfieActivity::class.java).also {\n            it.putExtra(CaptureSelfieActivity.EXTRA_TMP_PATH, \"blah\")\n        }\n\n        launcher.launch<CaptureSelfieActivity>(intent)\n        onView(withId(R.id.preview)).perform(click())\n        assertThat(camera.savedPath, equalTo(\"blah/tmp.jpg\"))\n    }\n\n    @Test\n    fun clickingPreview_finishesWithFilePath() {\n        val intent = Intent(application, CaptureSelfieActivity::class.java).also {\n            it.putExtra(CaptureSelfieActivity.EXTRA_TMP_PATH, \"blah\")\n        }\n\n        val scenario = launcher.launchForResult<CaptureSelfieActivity>(intent)\n        onView(withId(R.id.preview)).perform(click())\n\n        assertThat(scenario.result.resultCode, equalTo(Activity.RESULT_OK))\n        val returnedValue = ExternalAppUtils.getReturnedSingleValue(scenario.result.resultData)\n        assertThat(returnedValue, equalTo(\"blah/tmp.jpg\"))\n    }\n\n    @Test\n    fun clickingPreview_whenThereIsAnErrorSavingImage_showsToast() {\n        ToastUtils.alertStore.enabled = true\n        camera.failToSave = true\n\n        val intent = Intent(application, CaptureSelfieActivity::class.java).also {\n            it.putExtra(CaptureSelfieActivity.EXTRA_TMP_PATH, \"blah\")\n        }\n\n        launcher.launch<CaptureSelfieActivity>(intent)\n        onView(withId(R.id.preview)).perform(click())\n\n        val latestToast = ToastUtils.alertStore.popAll().last()\n        assertThat(latestToast, equalTo(application.getString(org.odk.collect.strings.R.string.camera_error)))\n    }\n\n    @Test\n    fun whenCameraFailsToInitialize_showsToast() {\n        ToastUtils.alertStore.enabled = true\n        camera.failToInitialize = true\n\n        val intent = Intent(application, CaptureSelfieActivity::class.java).also {\n            it.putExtra(CaptureSelfieActivity.EXTRA_TMP_PATH, \"blah\")\n        }\n\n        launcher.launch<CaptureSelfieActivity>(intent)\n        val latestToast = ToastUtils.alertStore.popAll().first()\n        assertThat(latestToast, equalTo(application.getString(org.odk.collect.strings.R.string.camera_failed_to_initialize)))\n    }\n}\n\nprivate class FakePermissionsChecker : PermissionsChecker {\n\n    private val denied = mutableListOf<String>()\n\n    fun deny(permission: String) {\n        denied.add(permission)\n    }\n\n    override fun isPermissionGranted(vararg permissions: String): Boolean {\n        return permissions.none { denied.contains(it) }\n    }\n}\n\nprivate class FakeCamera : Camera {\n\n    var failToInitialize: Boolean = false\n    var failToSave = false\n    var savedPath: String? = null\n\n    private val state = MutableNonNullLiveData(Camera.State.UNINITIALIZED)\n\n    override fun initialize(activity: ComponentActivity, previewView: View) {\n        if (failToInitialize) {\n            state.value = Camera.State.FAILED_TO_INITIALIZE\n        } else {\n            state.value = Camera.State.INITIALIZED\n        }\n    }\n\n    override fun state(): NonNullLiveData<Camera.State> {\n        return state\n    }\n\n    override fun takePicture(\n        imagePath: String,\n        onImageSaved: () -> Unit,\n        onImageSaveError: () -> Unit\n    ) {\n        if (state.value == Camera.State.UNINITIALIZED) {\n            throw IllegalStateException()\n        }\n\n        savedPath = imagePath\n\n        if (failToSave) {\n            onImageSaveError()\n        } else {\n            onImageSaved()\n        }\n    }\n}\n"
  },
  {
    "path": "selfie-camera/src/test/java/org/odk/collect/selfiecamera/support/RobolectricApplication.kt",
    "content": "package org.odk.collect.selfiecamera.support\n\nimport android.app.Application\nimport org.odk.collect.androidshared.ui.ToastUtils\nimport org.odk.collect.selfiecamera.SelfieCameraDependencyComponent\nimport org.odk.collect.selfiecamera.SelfieCameraDependencyComponentProvider\n\nclass RobolectricApplication : Application(), SelfieCameraDependencyComponentProvider {\n\n    override lateinit var selfieCameraDependencyComponent: SelfieCameraDependencyComponent\n\n    override fun onCreate() {\n        super.onCreate()\n        ToastUtils.setApplication(this)\n    }\n}\n"
  },
  {
    "path": "selfie-camera/src/test/resources/robolectric.properties",
    "content": "application=org.odk.collect.selfiecamera.support.RobolectricApplication\nsdk=33"
  },
  {
    "path": "service-test/.gitignore",
    "content": "/build"
  },
  {
    "path": "service-test/README.md",
    "content": "# servicetest\n\nIncludes helpers (arguably missing from `androidx.test:core`) for writing local tests.\n"
  },
  {
    "path": "service-test/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    namespace = \"org.odk.collect.servicetest\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(libs.androidxLifecycleRuntimeKtx)\n    implementation(libs.robolectric)\n}\n"
  },
  {
    "path": "service-test/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "service-test/src/main/java/org/odk/collect/servicetest/NotificationDetails.kt",
    "content": "package org.odk.collect.servicetest\n\nimport android.app.Notification\nimport android.content.Intent\nimport org.robolectric.Shadows\n\n/**\n * Wraps a [Notification] object and provides access to details exposed by its Robolectric \"Shadow\"\n * without needing to explicitly depend on Robolectric.\n */\nclass NotificationDetails internal constructor(private val notification: Notification) {\n\n    val contentText: String\n        get() = Shadows.shadowOf(notification).contentText.toString()\n\n    val contentIntent: Intent\n        get() = Shadows.shadowOf(notification.contentIntent).savedIntent\n}\n"
  },
  {
    "path": "service-test/src/main/java/org/odk/collect/servicetest/ServiceScenario.kt",
    "content": "package org.odk.collect.servicetest\n\nimport android.app.NotificationManager\nimport android.app.Service\nimport android.content.Context\nimport android.content.Intent\nimport androidx.lifecycle.Lifecycle\nimport org.robolectric.Robolectric\nimport org.robolectric.Shadows.shadowOf\nimport org.robolectric.android.controller.ServiceController\nimport java.util.function.Consumer\n\n/**\n * Mimics [ActivityScenario]/[FragmentScenario] to provide a way to test [Service] in a similar\n * manner. This should allow a [Service] to be tested without explicitly depending on Robolectric.\n * AndroidX only provides a way to test [Service] components in instrumentation tests via\n * [ServiceTestRule] (discussed [here](https://github.com/android/android-test/issues/935)).\n *\n * This only works in Robolectric/local tests currently.\n */\nclass ServiceScenario<T : Service?>(private val serviceController: ServiceController<T>) {\n\n    fun startWithNewIntent(intent: Intent): ServiceScenario<T> {\n        serviceController.withIntent(intent)\n            .startCommand(0, 0)\n\n        return this\n    }\n\n    fun moveToState(state: Lifecycle.State) {\n        if (state == Lifecycle.State.DESTROYED) {\n            serviceController.destroy()\n        } else {\n            TODO()\n        }\n    }\n\n    fun getState(): Lifecycle.State {\n        return if (shadowOf(serviceController.get()).isStoppedBySelf) {\n            Lifecycle.State.DESTROYED\n        } else {\n            TODO()\n        }\n    }\n\n    fun onService(action: Consumer<T>) {\n        action.accept(serviceController.get())\n    }\n\n    fun getForegroundNotification(): NotificationDetails? {\n        val shadowService = shadowOf(serviceController.get())\n        return if (shadowService.isLastForegroundNotificationAttached) {\n            val notificationId = shadowService.lastForegroundNotificationId\n            val notificationManager = serviceController.get()!!\n                .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n            val shadowNotificationManager = shadowOf(notificationManager)\n\n            val notification = shadowNotificationManager.getNotification(notificationId)\n            NotificationDetails(notification)\n        } else {\n            null\n        }\n    }\n\n    companion object {\n\n        @JvmStatic\n        @JvmOverloads\n        fun <T : Service> launch(\n            serviceClass: Class<T>,\n            intent: Intent? = null\n        ): ServiceScenario<T> {\n            return ServiceScenario(\n                Robolectric.buildService(serviceClass, intent)\n                    .create()\n                    .startCommand(0, 0)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "settings/.gitignore",
    "content": "/build"
  },
  {
    "path": "settings/README.md",
    "content": "# settings\n\nFramework for importing ODK client settings from JSON to a `Project` and `Settings` based implementation.\n"
  },
  {
    "path": "settings/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles(\"consumer-rules.pro\")\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    namespace = \"org.odk.collect.settings\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":shared\"))\n    implementation(project(\":projects\"))\n\n    implementation(libs.jsonSchemaValidator)\n\n    testImplementation(libs.junit)\n    testImplementation(libs.json)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.mockitoKotlin)\n}\n"
  },
  {
    "path": "settings/consumer-rules.pro",
    "content": "-keep class com.networknt.schema.** { *; }\n"
  },
  {
    "path": "settings/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest>\n\n</manifest>\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/InMemSettingsProvider.kt",
    "content": "package org.odk.collect.settings\n\nimport org.odk.collect.shared.settings.InMemSettings\nimport org.odk.collect.shared.settings.Settings\n\nclass InMemSettingsProvider : SettingsProvider {\n\n    private val metaSettings = InMemSettings()\n    private val settings = mutableMapOf<String?, InMemSettings>()\n\n    override fun getMetaSettings(): Settings {\n        return metaSettings\n    }\n\n    override fun getUnprotectedSettings(projectId: String?): Settings {\n        return settings.getOrPut(\"general:$projectId\") { InMemSettings() }\n    }\n\n    override fun getProtectedSettings(projectId: String?): Settings {\n        return settings.getOrPut(\"admin:$projectId\") { InMemSettings() }\n    }\n\n    override fun clearAll(projectIds: List<String>) {\n        settings.values.forEach { it.clear() }\n        settings.clear()\n        metaSettings.clear()\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/ODKAppSettingsImporter.kt",
    "content": "package org.odk.collect.settings\n\nimport org.json.JSONObject\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectConfigurationResult\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.importing.ProjectDetailsCreatorImpl\nimport org.odk.collect.settings.importing.SettingsChangeHandler\nimport org.odk.collect.settings.importing.SettingsImporter\nimport org.odk.collect.settings.validation.JsonSchemaSettingsValidator\n\nclass ODKAppSettingsImporter(\n    projectsRepository: ProjectsRepository,\n    settingsProvider: SettingsProvider,\n    generalDefaults: Map<String, Any>,\n    adminDefaults: Map<String, Any>,\n    projectColors: List<String>,\n    settingsChangedHandler: SettingsChangeHandler,\n    private val deviceUnsupportedSettings: JSONObject\n) {\n\n    private val settingsImporter = SettingsImporter(\n        settingsProvider,\n        ODKAppSettingsMigrator(settingsProvider.getMetaSettings()),\n        JsonSchemaSettingsValidator { javaClass.getResourceAsStream(\"/client-settings.schema.json\")!! },\n        generalDefaults,\n        adminDefaults,\n        settingsChangedHandler,\n        projectsRepository,\n        ProjectDetailsCreatorImpl(projectColors, generalDefaults)\n    )\n\n    fun fromJSON(json: String, project: Project.Saved): ProjectConfigurationResult {\n        return try {\n            settingsImporter.fromJSON(json, project, deviceUnsupportedSettings)\n        } catch (e: Throwable) {\n            ProjectConfigurationResult.INVALID_SETTINGS\n        }\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/ODKAppSettingsMigrator.java",
    "content": "package org.odk.collect.settings;\n\nimport static org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_CARTO;\nimport static org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_OSM;\nimport static org.odk.collect.settings.keys.ProjectKeys.BASEMAP_SOURCE_USGS;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_BASEMAP_SOURCE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_CARTO_MAP_STYLE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_GOOGLE_MAP_STYLE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_MAPBOX_MAP_STYLE;\nimport static org.odk.collect.settings.keys.ProjectKeys.KEY_USGS_MAP_STYLE;\nimport static org.odk.collect.settings.migration.MigrationUtils.combineKeys;\nimport static org.odk.collect.settings.migration.MigrationUtils.extractNewKey;\nimport static org.odk.collect.settings.migration.MigrationUtils.moveKey;\nimport static org.odk.collect.settings.migration.MigrationUtils.removeKey;\nimport static org.odk.collect.settings.migration.MigrationUtils.renameKey;\nimport static org.odk.collect.settings.migration.MigrationUtils.translateKey;\nimport static org.odk.collect.settings.migration.MigrationUtils.translateValue;\nimport static org.odk.collect.settings.migration.MigrationUtils.updateKeys;\nimport static java.util.Arrays.asList;\n\nimport org.odk.collect.settings.importing.SettingsMigrator;\nimport org.odk.collect.settings.keys.ProtectedProjectKeys;\nimport org.odk.collect.settings.migration.Migration;\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.util.List;\n\n/**\n * Migrates old preference keys and values to new ones.\n */\npublic class ODKAppSettingsMigrator implements SettingsMigrator {\n\n    private final Settings metaSettings;\n    private Settings protectedSettings;\n\n    public ODKAppSettingsMigrator(Settings metaSettings) {\n        this.metaSettings = metaSettings;\n    }\n\n    @Override\n    public void migrate(Settings unprotectedSettings, Settings protectedSettings) {\n        this.protectedSettings = protectedSettings;\n\n        for (Migration migration : getUnprotectedMigrations()) {\n            migration.apply(unprotectedSettings);\n        }\n\n        for (Migration migration : getProtectedMigrations()) {\n            migration.apply(protectedSettings);\n        }\n\n        for (Migration migration : getMetaMigrations()) {\n            migration.apply(metaSettings);\n        }\n    }\n\n    private List<Migration> getUnprotectedMigrations() {\n        return asList(\n                translateKey(\"map_sdk_behavior\").toKey(KEY_BASEMAP_SOURCE)\n                        .fromValue(\"google_maps\").toValue(\"google\")\n                        .fromValue(\"mapbox_maps\").toValue(\"mapbox\"),\n\n                // ListPreferences can only handle string values, so we use string values here.\n                // Note that unfortunately there was a hidden U+200E character in the preference\n                // value for \"terrain\" in previous versions of ODK Collect, so we need to\n                // include that character to match that value correctly.\n                translateKey(\"map_basemap_behavior\").toKey(KEY_GOOGLE_MAP_STYLE)\n                        .fromValue(\"streets\").toValue(Integer.toString(1))\n                        .fromValue(\"terrain\\u200e\").toValue(Integer.toString(3))\n                        .fromValue(\"terrain\").toValue(Integer.toString(3))\n                        .fromValue(\"hybrid\").toValue(Integer.toString(4))\n                        .fromValue(\"satellite\").toValue(Integer.toString(2)),\n\n                translateKey(\"map_basemap_behavior\").toKey(KEY_MAPBOX_MAP_STYLE)\n                        .fromValue(\"mapbox_streets\").toValue(\"mapbox://styles/mapbox/streets-v11\")\n                        .fromValue(\"mapbox_light\").toValue(\"mapbox://styles/mapbox/light-v10\")\n                        .fromValue(\"mapbox_dark\").toValue(\"mapbox://styles/mapbox/dark-v10\")\n                        .fromValue(\"mapbox_satellite\").toValue(\"mapbox://styles/mapbox/satellite-v9\")\n                        .fromValue(\"mapbox_satellite_streets\").toValue(\"mapbox://styles/mapbox/satellite-streets-v11\")\n                        .fromValue(\"mapbox_outdoors\").toValue(\"mapbox://styles/mapbox/outdoors-v11\"),\n\n                // When the map_sdk_behavior is \"osmdroid\", we have to also examine the\n                // map_basemap_behavior key to determine the new basemap source.\n                combineKeys(\"map_sdk_behavior\", \"map_basemap_behavior\")\n                        .withValues(\"osmdroid\", \"openmap_streets\")\n                        .toPairs(KEY_BASEMAP_SOURCE, BASEMAP_SOURCE_OSM)\n\n                        .withValues(\"osmdroid\", \"openmap_usgs_topo\")\n                        .toPairs(KEY_BASEMAP_SOURCE, BASEMAP_SOURCE_USGS, KEY_USGS_MAP_STYLE, \"topographic\")\n                        .withValues(\"osmdroid\", \"openmap_usgs_sat\")\n                        .toPairs(KEY_BASEMAP_SOURCE, BASEMAP_SOURCE_USGS, KEY_USGS_MAP_STYLE, \"hybrid\")\n                        .withValues(\"osmdroid\", \"openmap_usgs_img\")\n                        .toPairs(KEY_BASEMAP_SOURCE, BASEMAP_SOURCE_USGS, KEY_USGS_MAP_STYLE, \"satellite\")\n\n                        .withValues(\"osmdroid\", \"openmap_stamen_terrain\")\n                        .toPairs(KEY_BASEMAP_SOURCE, \"stamen\")\n\n                        .withValues(\"osmdroid\", \"openmap_cartodb_positron\")\n                        .toPairs(KEY_BASEMAP_SOURCE, BASEMAP_SOURCE_CARTO, KEY_CARTO_MAP_STYLE, \"positron\")\n                        .withValues(\"osmdroid\", \"openmap_cartodb_darkmatter\")\n                        .toPairs(KEY_BASEMAP_SOURCE, BASEMAP_SOURCE_CARTO, KEY_CARTO_MAP_STYLE, \"dark_matter\"),\n\n                translateValue(\"other_protocol\").toValue(\"odk_default\").forKey(\"protocol\"),\n\n                removeKey(\"firstRun\"),\n                removeKey(\"lastVersion\"),\n                moveKey(\"scoped_storage_used\").toPreferences(metaSettings),\n                removeKey(\"metadata_migrated\"),\n                moveKey(\"mapbox_initialized\").toPreferences(metaSettings),\n\n                combineKeys(\"autosend_wifi\", \"autosend_network\")\n                        .withValues(false, false).toPairs(\"autosend\", \"off\")\n                        .withValues(false, true).toPairs(\"autosend\", \"cellular_only\")\n                        .withValues(true, false).toPairs(\"autosend\", \"wifi_only\")\n                        .withValues(true, true).toPairs(\"autosend\", \"wifi_and_cellular\"),\n\n                extractNewKey(\"form_update_mode\").fromKey(\"protocol\")\n                        .fromValue(\"google_sheets\").toValue(\"manual\"),\n\n                extractNewKey(\"form_update_mode\").fromKey(\"periodic_form_updates_check\")\n                        .fromValue(\"never\").toValue(\"manual\")\n                        .fromValue(\"every_fifteen_minutes\").toValue(\"previously_downloaded\")\n                        .fromValue(\"every_one_hour\").toValue(\"previously_downloaded\")\n                        .fromValue(\"every_six_hours\").toValue(\"previously_downloaded\")\n                        .fromValue(\"every_24_hours\").toValue(\"previously_downloaded\"),\n\n                translateValue(\"never\").toValue(\"every_fifteen_minutes\").forKey(\"periodic_form_updates_check\"),\n\n                moveKey(\"knownUrlList\").toPreferences(metaSettings),\n\n                moveKey(\"default_completed\").toPreferences(protectedSettings),\n\n                translateValue(\"stamen\").toValue(BASEMAP_SOURCE_OSM).forKey(KEY_BASEMAP_SOURCE)\n        );\n    }\n\n    public List<Migration> getMetaMigrations() {\n        return asList(\n                renameKey(\"firstRun\").toKey(\"first_run\"),\n                renameKey(\"lastVersion\").toKey(\"last_version\"),\n\n                renameKey(\"knownUrlList\").toKey(\"server_list\")\n        );\n    }\n\n    public List<Migration> getProtectedMigrations() {\n        return asList(\n                // When either the map SDK or the basemap selection were previously\n                // hidden, we want to hide the entire Maps preference screen.\n                translateKey(\"show_map_sdk\").toKey(\"maps\")\n                        .fromValue(false).toValue(false),\n                translateKey(\"show_map_basemap\").toKey(\"maps\")\n                        .fromValue(false).toValue(false),\n\n                combineKeys(\"mark_as_finalized\", \"default_completed\")\n                        .withValues(false, false)\n                        .toPairs(\n                                ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true,\n                                \"finalize\", false\n                        )\n                        .withValues(false, true)\n                        .toPairs(\n                                ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false,\n                                \"finalize\", true\n                        )\n                        .withValues(false, null)\n                        .toPairs(\n                                ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false,\n                                \"finalize\", true\n                        ),\n                removeKey(\"mark_as_finalized\"),\n                removeKey(\"default_completed\"),\n                updateKeys(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM)\n                        .withValues(false)\n                        .toPairs(\n                                ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false,\n                                \"finalize\", true\n                        ),\n                updateKeys(\"finalize\").withValues(false)\n                        .toPairs(\n                                ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false,\n                                ProtectedProjectKeys.KEY_BULK_FINALIZE, false\n                        )\n                        .withValues(true)\n                        .toPairs(\n                                ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true,\n                                ProtectedProjectKeys.KEY_BULK_FINALIZE, true\n                        ),\n                removeKey(\"finalize\")\n        );\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/SettingsProvider.kt",
    "content": "package org.odk.collect.settings\n\nimport org.odk.collect.shared.settings.Settings\n\ninterface SettingsProvider {\n\n    fun getMetaSettings(): Settings\n\n    fun getUnprotectedSettings(projectId: String?): Settings\n\n    fun getUnprotectedSettings(): Settings = getUnprotectedSettings(null)\n\n    fun getProtectedSettings(projectId: String?): Settings\n\n    fun getProtectedSettings(): Settings = getProtectedSettings(null)\n\n    fun clearAll(projectIds: List<String>)\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/enums/AutoSend.kt",
    "content": "package org.odk.collect.settings.enums\n\nimport androidx.annotation.StringRes\nimport org.odk.collect.settings.R\n\nenum class AutoSend(@StringRes override val stringId: Int) : StringIdEnum {\n    OFF(R.string.auto_send_off),\n    WIFI_ONLY(R.string.auto_send_wifi_only),\n    CELLULAR_ONLY(R.string.auto_send_cellular_only),\n    WIFI_AND_CELLULAR(R.string.auto_send_wifi_and_cellular)\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/enums/FormUpdateMode.java",
    "content": "package org.odk.collect.settings.enums;\n\nimport org.odk.collect.settings.R;\n\npublic enum FormUpdateMode implements StringIdEnum {\n\n    MANUAL(R.string.form_update_mode_manual),\n    PREVIOUSLY_DOWNLOADED_ONLY(R.string.form_update_mode_previously_downloaded),\n    MATCH_EXACTLY(R.string.form_update_mode_match_exactly);\n\n    private final int string;\n\n    FormUpdateMode(int string) {\n        this.string = string;\n    }\n\n    @Override\n    public int getStringId() {\n        return string;\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/enums/GuidanceHintMode.kt",
    "content": "package org.odk.collect.settings.enums\n\nimport androidx.annotation.StringRes\nimport org.odk.collect.settings.R\n\nenum class GuidanceHintMode(@StringRes override val stringId: Int) : StringIdEnum {\n    YES(R.string.guidance_hint_yes),\n    NO(R.string.guidance_hint_no),\n    YES_COLLAPSED(R.string.guidance_hint_yes_collapsed)\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/enums/StringIdEnum.kt",
    "content": "package org.odk.collect.settings.enums\n\nimport android.content.Context\n\ninternal interface StringIdEnum {\n    val stringId: Int\n\n    fun getValue(context: Context): String {\n        return context.getString(stringId)\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/enums/StringIdEnumUtils.kt",
    "content": "package org.odk.collect.settings.enums\n\nimport android.content.Context\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.settings.Settings\n\nobject StringIdEnumUtils {\n\n    @JvmStatic\n    fun Settings.getFormUpdateMode(context: Context): FormUpdateMode {\n        val setting = this.getString(ProjectKeys.KEY_FORM_UPDATE_MODE)\n        return parse(context, setting)\n    }\n\n    @JvmStatic\n    fun Settings.getAutoSend(context: Context): AutoSend {\n        val setting = this.getString(ProjectKeys.KEY_AUTOSEND)\n        return parse(context, setting)\n    }\n\n    @JvmStatic\n    fun Settings.getGuidanceHintMode(context: Context): GuidanceHintMode {\n        val setting = this.getString(ProjectKeys.KEY_GUIDANCE_HINT)\n        return parse(context, setting)\n    }\n\n    private inline fun <reified T> parse(\n        context: Context,\n        value: String?\n    ): T where T : Enum<T>, T : StringIdEnum {\n        return enumValues<T>().find {\n            context.getString(it.stringId) == value\n        } ?: throw IllegalArgumentException()\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/importing/ProjectDetailsCreatorImpl.kt",
    "content": "package org.odk.collect.settings.importing\n\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.collections.CollectionExtensions.itemFromHashOf\nimport org.odk.collect.shared.strings.StringUtils\nimport java.net.URL\nimport java.util.regex.Pattern\n\nclass ProjectDetailsCreatorImpl(private val colors: List<String>, private val defaults: Map<String, Any>) : ProjectDetailsCreator {\n\n    override fun createProjectFromDetails(name: String, icon: String, color: String, connectionIdentifier: String): Project {\n        val projectName = name.ifBlank {\n            getProjectNameFromConnectionIdentifier(connectionIdentifier)\n        }\n\n        val projectIcon = if (icon.isNotBlank()) {\n            StringUtils.firstCharacterOrEmoji(icon)\n        } else {\n            projectName.first().uppercaseChar().toString()\n        }\n\n        val projectColor = if (isProjectColorValid(color)) {\n            color\n        } else {\n            getProjectColorFromProjectName(projectName)\n        }\n\n        return Project.New(projectName, projectIcon, projectColor)\n    }\n\n    private fun getProjectNameFromConnectionIdentifier(connectionIdentifier: String): String {\n        val defaultServer = defaults[ProjectKeys.KEY_SERVER_URL] as String\n\n        return if (connectionIdentifier.isBlank() || connectionIdentifier.startsWith(defaultServer)) {\n            Project.DEMO_PROJECT_NAME\n        } else {\n            try {\n                val projectName = URL(connectionIdentifier).host\n                projectName?.ifBlank {\n                    connectionIdentifier\n                } ?: connectionIdentifier\n            } catch (e: Exception) {\n                connectionIdentifier\n            }\n        }\n    }\n\n    private fun getProjectColorFromProjectName(projectName: String): String {\n        if (projectName == Project.DEMO_PROJECT_NAME) {\n            return Project.DEMO_PROJECT_COLOR\n        }\n\n        return colors.itemFromHashOf(projectName)\n    }\n\n    private fun isProjectColorValid(hexColor: String): Boolean {\n        return Pattern\n            .compile(\"^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\\$\", Pattern.CASE_INSENSITIVE)\n            .matcher(hexColor)\n            .matches()\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/importing/SettingsImporter.kt",
    "content": "package org.odk.collect.settings.importing\n\nimport org.json.JSONObject\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectConfigurationResult\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.SettingsProvider\nimport org.odk.collect.settings.keys.AppConfigurationKeys\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.shared.collections.CollectionExtensions.has\nimport org.odk.collect.shared.settings.Settings\n\ninternal class SettingsImporter(\n    private val settingsProvider: SettingsProvider,\n    private val settingsMigrator: SettingsMigrator,\n    private val settingsValidator: SettingsValidator,\n    private val unprotectedDefaults: Map<String, Any>,\n    private val protectedDefaults: Map<String, Any>,\n    private val settingsChangedHandler: SettingsChangeHandler,\n    private val projectsRepository: ProjectsRepository,\n    private val projectDetailsCreator: ProjectDetailsCreator\n) {\n\n    fun fromJSON(json: String, project: Project.Saved, deviceUnsupportedSettings: JSONObject): ProjectConfigurationResult {\n        if (!settingsValidator.isValid(json)) {\n            return ProjectConfigurationResult.INVALID_SETTINGS\n        }\n\n        val unprotectedSettings = settingsProvider.getUnprotectedSettings(project.uuid)\n        val protectedSettings = settingsProvider.getProtectedSettings(project.uuid)\n\n        val oldUnprotectedSettings = unprotectedSettings.getAll().toMap().let {\n            it.ifEmpty { unprotectedDefaults }\n        }\n        val oldProtectedSettings = protectedSettings.getAll().toMap().let {\n            it.ifEmpty { protectedDefaults }\n        }\n\n        unprotectedSettings.clear()\n        protectedSettings.clear()\n\n        val jsonObject = JSONObject(json)\n\n        if (isGDProject(jsonObject)) {\n            return ProjectConfigurationResult.GD_PROJECT\n        }\n\n        // Import unprotected settings\n        importToPrefs(jsonObject, AppConfigurationKeys.GENERAL, unprotectedSettings, deviceUnsupportedSettings)\n\n        // Import protected settings\n        importToPrefs(jsonObject, AppConfigurationKeys.ADMIN, protectedSettings, deviceUnsupportedSettings)\n\n        // Import project details\n        val projectDetails = if (jsonObject.has(AppConfigurationKeys.PROJECT)) {\n            jsonObject.getJSONObject(AppConfigurationKeys.PROJECT)\n        } else {\n            JSONObject()\n        }\n\n        val connectionIdentifier = unprotectedSettings.getString(ProjectKeys.KEY_SERVER_URL) ?: \"\"\n\n        importProjectDetails(\n            project,\n            projectDetails,\n            connectionIdentifier\n        )\n\n        settingsMigrator.migrate(unprotectedSettings, protectedSettings)\n\n        loadDefaults(unprotectedSettings, unprotectedDefaults)\n        loadDefaults(protectedSettings, protectedDefaults)\n\n        val newUnprotectedSettings = unprotectedSettings.getAll()\n        val newProtectedSettings = protectedSettings.getAll()\n\n        val changedUnprotectedKeys = oldUnprotectedSettings.keys.filter { key ->\n            newUnprotectedSettings[key] != oldUnprotectedSettings[key]\n        }\n        val changedProtectedKeys = oldProtectedSettings.keys.filter { key ->\n            newProtectedSettings[key] != oldProtectedSettings[key]\n        }\n\n        settingsChangedHandler.onSettingsChanged(project.uuid, changedUnprotectedKeys, changedProtectedKeys)\n\n        return ProjectConfigurationResult.SUCCESS\n    }\n\n    private fun isGDProject(jsonObject: JSONObject): Boolean {\n        val unprotectedSettings = jsonObject.getJSONObject(AppConfigurationKeys.GENERAL)\n        return unprotectedSettings.has(ProjectKeys.KEY_PROTOCOL) &&\n            unprotectedSettings.get(ProjectKeys.KEY_PROTOCOL) == ProjectKeys.PROTOCOL_GOOGLE_SHEETS\n    }\n\n    private fun importToPrefs(\n        mainJsonObject: JSONObject,\n        childJsonObjectName: String,\n        preferences: Settings,\n        deviceUnsupportedSettings: JSONObject\n    ) {\n        val childJsonObject = mainJsonObject.getJSONObject(childJsonObjectName)\n        val deviceUnsupportedSettingsForGivenChildJson = if (deviceUnsupportedSettings.has(childJsonObjectName)) deviceUnsupportedSettings.getJSONObject(childJsonObjectName) else JSONObject()\n\n        childJsonObject.keys().forEach {\n            if (settingsValidator.isKeySupported(childJsonObjectName, it)) {\n                val value = childJsonObject[it]\n                if (settingsValidator.isValueSupported(childJsonObjectName, it, value)) {\n                    if (!deviceUnsupportedSettingsForGivenChildJson.has(it) || !deviceUnsupportedSettingsForGivenChildJson.getJSONArray(it).has(value)) {\n                        preferences.save(it, value)\n                    }\n                }\n            }\n        }\n    }\n\n    private fun loadDefaults(preferences: Settings, defaults: Map<String, Any>) {\n        defaults.forEach { (key, value) ->\n            if (!preferences.contains(key)) {\n                preferences.save(key, value)\n            }\n        }\n    }\n\n    private fun importProjectDetails(project: Project.Saved, projectJson: JSONObject, connectionIdentifier: String) {\n        val projectName = if (projectJson.has(AppConfigurationKeys.PROJECT_NAME)) {\n            projectJson.getString(AppConfigurationKeys.PROJECT_NAME)\n        } else {\n            \"\"\n        }\n        val projectIcon = if (projectJson.has(AppConfigurationKeys.PROJECT_ICON)) {\n            projectJson.getString(AppConfigurationKeys.PROJECT_ICON)\n        } else {\n            \"\"\n        }\n        val projectColor = if (projectJson.has(AppConfigurationKeys.PROJECT_COLOR)) {\n            projectJson.getString(AppConfigurationKeys.PROJECT_COLOR)\n        } else {\n            \"\"\n        }\n\n        val newProject = projectDetailsCreator.createProjectFromDetails(projectName, projectIcon, projectColor, connectionIdentifier)\n\n        projectsRepository.save(\n            project.copy(\n                name = newProject.name,\n                icon = newProject.icon,\n                color = newProject.color\n            )\n        )\n    }\n}\n\ninternal interface SettingsValidator {\n    fun isValid(json: String): Boolean\n\n    fun isKeySupported(parentJsonObjectName: String, key: String): Boolean\n\n    fun isValueSupported(parentJsonObjectName: String, key: String, value: Any): Boolean\n}\n\ninterface SettingsChangeHandler {\n    fun onSettingChanged(projectId: String, newValue: Any?, changedKey: String)\n\n    fun onSettingsChanged(projectId: String, changedUnprotectedKeys: List<String>, changedProtectedKeys: List<String>)\n}\n\ninternal fun interface SettingsMigrator {\n    fun migrate(unprotectedSettings: Settings, protectedSettings: Settings)\n}\n\ninterface ProjectDetailsCreator {\n    fun createProjectFromDetails(name: String = \"\", icon: String = \"\", color: String = \"\", connectionIdentifier: String = \"\"): Project\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/keys/AppConfigurationKeys.kt",
    "content": "package org.odk.collect.settings.keys\n\nobject AppConfigurationKeys {\n    const val GENERAL = \"general\"\n    const val ADMIN = \"admin\"\n    const val PROJECT = \"project\"\n\n    const val PROJECT_NAME = \"name\"\n    const val PROJECT_ICON = \"icon\"\n    const val PROJECT_COLOR = \"color\"\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/keys/MetaKeys.kt",
    "content": "package org.odk.collect.settings.keys\n\nobject MetaKeys {\n    const val KEY_INSTALL_ID = \"metadata_installid\"\n    const val KEY_MAPBOX_INITIALIZED = \"mapbox_initialized\"\n    const val KEY_GOOGLE_BUG_154855417_FIXED = \"google_bug_154855417_fixed\"\n    const val LAST_UPDATED_NOTIFICATION = \"last_updated_notification\"\n    const val CURRENT_PROJECT_ID = \"current_project_id\"\n    const val KEY_PROJECTS = \"projects\"\n    const val EXISTING_PROJECT_IMPORTED = \"existing_project_imported\"\n    const val OLD_SAVEPOINTS_IMPORTED = \"old_savepoints_imported\"\n    const val LAST_LAUNCHED = \"last_launched\"\n    const val LAST_USED_PEN_COLOR = \"last_used_pen_color\"\n    const val PERMISSIONS_REQUESTED = \"permissions_requested\"\n    const val LAST_KNOWN_ZOOM_LEVEL = \"last_known_zoom_level\"\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/keys/ProjectKeys.kt",
    "content": "package org.odk.collect.settings.keys\n\nobject ProjectKeys {\n\n    // server_preferences.xml\n    const val KEY_PROTOCOL = \"protocol\"\n\n    // odk_server_preferences.xml\n    const val KEY_SERVER_URL = \"server_url\"\n    const val KEY_USERNAME = \"username\"\n    const val KEY_PASSWORD = \"password\"\n\n    // user_interface_preferences.xml\n    const val KEY_APP_LANGUAGE = \"app_language\"\n    const val KEY_FONT_SIZE = \"font_size\"\n    const val KEY_NAVIGATION = \"navigation\"\n\n    // map_preferences.xml\n    const val KEY_BASEMAP_SOURCE = \"basemap_source\"\n\n    // basemap styles\n    const val KEY_GOOGLE_MAP_STYLE = \"google_map_style\"\n    const val KEY_MAPBOX_MAP_STYLE = \"mapbox_map_style\"\n    const val KEY_USGS_MAP_STYLE = \"usgs_map_style\"\n    const val KEY_CARTO_MAP_STYLE = \"carto_map_style\"\n    const val KEY_REFERENCE_LAYER = \"reference_layer\"\n\n    // form_management_preferences.xml\n    const val KEY_FORM_UPDATE_MODE = \"form_update_mode\"\n    const val KEY_PERIODIC_FORM_UPDATES_CHECK = \"periodic_form_updates_check\"\n    const val KEY_AUTOMATIC_UPDATE = \"automatic_update\"\n    const val KEY_HIDE_OLD_FORM_VERSIONS = \"hide_old_form_versions\"\n    const val KEY_AUTOSEND = \"autosend\"\n    const val KEY_DELETE_AFTER_SEND = \"delete_send\"\n    const val KEY_CONSTRAINT_BEHAVIOR = \"constraint_behavior\"\n    const val KEY_HIGH_RESOLUTION = \"high_resolution\"\n    const val KEY_IMAGE_SIZE = \"image_size\"\n    const val KEY_GUIDANCE_HINT = \"guidance_hint\"\n    const val KEY_EXTERNAL_APP_RECORDING = \"external_app_recording\"\n    const val KEY_INSTANCE_SYNC = \"instance_sync\"\n\n    // identity_preferences.xml\n    const val KEY_ANALYTICS = \"analytics\"\n\n    // form_metadata_preferences.xml\n    const val KEY_METADATA_USERNAME = \"metadata_username\"\n    const val KEY_METADATA_PHONENUMBER = \"metadata_phonenumber\"\n    const val KEY_METADATA_EMAIL = \"metadata_email\"\n    const val KEY_FORM_METADATA = \"form_metadata\"\n    const val KEY_BACKGROUND_LOCATION = \"background_location\"\n    const val KEY_BACKGROUND_RECORDING = \"background_recording\"\n\n    // experimental_preferences.xml\n    const val KEY_DEBUG_FILTERS = \"experimental_debug_filters\"\n    const val KEY_ZXING_SCANNING = \"zxing_scanning\"\n\n    // values\n    const val PROTOCOL_SERVER = \"odk_default\"\n    const val PROTOCOL_GOOGLE_SHEETS = \"google_sheets\"\n    const val NAVIGATION_SWIPE = \"swipe\"\n    const val NAVIGATION_BUTTONS = \"buttons\"\n    const val NAVIGATION_BOTH = \"swipe_buttons\"\n    const val CONSTRAINT_BEHAVIOR_ON_SWIPE = \"on_swipe\"\n\n    // basemap section\n    const val CATEGORY_BASEMAP = \"category_basemap\"\n\n    // basemap source values\n    const val BASEMAP_SOURCE_GOOGLE = \"google\"\n    const val BASEMAP_SOURCE_MAPBOX = \"mapbox\"\n    const val BASEMAP_SOURCE_OSM = \"osm\"\n    const val BASEMAP_SOURCE_USGS = \"usgs\"\n    const val BASEMAP_SOURCE_CARTO = \"carto\"\n\n    // remembered defaults\n    const val KEY_SAVED_FORM_SORT_ORDER = \"instanceUploaderListSortingOrder\"\n    const val KEY_BLANK_FORM_SORT_ORDER = \"formChooserListSortingOrder\"\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt",
    "content": "package org.odk.collect.settings.keys\n\nobject ProtectedProjectKeys {\n    const val KEY_ADMIN_PW = \"admin_pw\"\n\n    // Main Menu Settings\n    const val KEY_EDIT_SAVED = \"edit_saved\"\n    const val KEY_SEND_FINALIZED = \"send_finalized\"\n    const val KEY_VIEW_SENT = \"view_sent\"\n    const val KEY_GET_BLANK = \"get_blank\"\n    const val KEY_DELETE_SAVED = \"delete_saved\"\n\n    // User Settings\n    const val KEY_CHANGE_SERVER = \"change_server\"\n    const val KEY_CHANGE_PROJECT_DISPLAY = \"change_project_display\"\n    const val KEY_APP_LANGUAGE = \"change_app_language\"\n    const val KEY_CHANGE_FONT_SIZE = \"change_font_size\"\n    const val KEY_NAVIGATION = \"change_navigation\"\n    const val KEY_MAPS = \"maps\"\n    const val KEY_FORM_UPDATE_MODE = \"form_update_mode\"\n    const val KEY_PERIODIC_FORM_UPDATES_CHECK = \"periodic_form_updates_check\"\n    const val KEY_AUTOMATIC_UPDATE = \"automatic_update\"\n    const val KEY_HIDE_OLD_FORM_VERSIONS = \"hide_old_form_versions\"\n    const val KEY_AUTOSEND = \"change_autosend\"\n    const val KEY_DELETE_AFTER_SEND = \"delete_after_send\"\n    const val KEY_CONSTRAINT_BEHAVIOR = \"change_constraint_behavior\"\n    const val KEY_HIGH_RESOLUTION = \"high_resolution\"\n    const val KEY_IMAGE_SIZE = \"image_size\"\n    const val KEY_GUIDANCE_HINT = \"guidance_hint\"\n    const val KEY_EXTERNAL_APP_RECORDING = \"external_app_recording\"\n    const val KEY_INSTANCE_FORM_SYNC = \"instance_form_sync\"\n    const val KEY_CHANGE_FORM_METADATA = \"change_form_metadata\"\n    const val KEY_ANALYTICS = \"analytics\"\n\n    // Form Entry Settings\n    const val KEY_MOVING_BACKWARDS = \"moving_backwards\"\n    const val KEY_ACCESS_SETTINGS = \"access_settings\"\n    const val KEY_CHANGE_LANGUAGE = \"change_language\"\n    const val KEY_JUMP_TO = \"jump_to\"\n    const val KEY_SAVE_MID = \"save_mid\"\n    const val KEY_SAVE_AS_DRAFT = \"save_as_draft\"\n    const val KEY_FINALIZE_IN_FORM_ENTRY = \"finalize_in_form_entry\"\n\n    const val ALLOW_OTHER_WAYS_OF_EDITING_FORM = \"allow_other_ways_of_editing_form\"\n    const val KEY_BULK_FINALIZE = \"bulk_finalize\"\n\n    fun allKeys() = listOf(\n        KEY_ADMIN_PW,\n        KEY_EDIT_SAVED,\n        KEY_SEND_FINALIZED,\n        KEY_VIEW_SENT,\n        KEY_GET_BLANK,\n        KEY_DELETE_SAVED,\n        KEY_CHANGE_SERVER,\n        KEY_CHANGE_PROJECT_DISPLAY,\n        KEY_APP_LANGUAGE,\n        KEY_CHANGE_FONT_SIZE,\n        KEY_NAVIGATION,\n        KEY_MAPS,\n        KEY_FORM_UPDATE_MODE,\n        KEY_PERIODIC_FORM_UPDATES_CHECK,\n        KEY_AUTOMATIC_UPDATE,\n        KEY_HIDE_OLD_FORM_VERSIONS,\n        KEY_AUTOSEND,\n        KEY_DELETE_AFTER_SEND,\n        KEY_CONSTRAINT_BEHAVIOR,\n        KEY_HIGH_RESOLUTION,\n        KEY_IMAGE_SIZE,\n        KEY_GUIDANCE_HINT,\n        KEY_EXTERNAL_APP_RECORDING,\n        KEY_INSTANCE_FORM_SYNC,\n        KEY_CHANGE_FORM_METADATA,\n        KEY_ANALYTICS,\n        KEY_MOVING_BACKWARDS,\n        KEY_ACCESS_SETTINGS,\n        KEY_CHANGE_LANGUAGE,\n        KEY_JUMP_TO,\n        KEY_SAVE_MID,\n        KEY_SAVE_AS_DRAFT,\n        KEY_FINALIZE_IN_FORM_ENTRY,\n        ALLOW_OTHER_WAYS_OF_EDITING_FORM,\n        KEY_BULK_FINALIZE\n    )\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/migration/KeyCombiner.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.odk.collect.settings.migration.MigrationUtils.asPairs;\nimport static org.odk.collect.settings.migration.MigrationUtils.replace;\n\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\n\npublic class KeyCombiner implements Migration {\n\n    String[] oldKeys;\n    Object[] tempOldValueArray;\n    List<Object[]> oldValueArrays = new ArrayList<>();\n    List<KeyValuePair[]> newKeyValuePairArrays = new ArrayList<>();\n\n    KeyCombiner(String... oldKeys) {\n        this.oldKeys = oldKeys;\n    }\n\n    public KeyCombiner withValues(Object... oldValues) {\n        tempOldValueArray = oldValues;\n        return this;\n    }\n\n    public KeyCombiner toPairs(Object... keysAndValues) {\n        oldValueArrays.add(tempOldValueArray);\n        newKeyValuePairArrays.add(asPairs(keysAndValues));\n        return this;\n    }\n\n    public void apply(Settings prefs) {\n        Map<String, ?> prefMap = prefs.getAll();\n        Object[] oldValues = new Object[oldKeys.length];\n        for (int i = 0; i < oldKeys.length; i++) {\n            oldValues[i] = prefMap.get(oldKeys[i]);\n        }\n        for (int i = 0; i < oldValueArrays.size(); i++) {\n            if (Arrays.equals(oldValues, oldValueArrays.get(i))) {\n                replace(prefs, oldKeys, newKeyValuePairArrays.get(i));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/migration/KeyExtractor.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class KeyExtractor implements Migration {\n\n    private final String newKey;\n    private final Map<Object, Object> translatedValues = new HashMap<>();\n\n    private String oldKey;\n    private Object tempOldValue;\n\n    public KeyExtractor(String key) {\n        this.newKey = key;\n    }\n\n    public KeyExtractor fromKey(String key) {\n        this.oldKey = key;\n        return this;\n    }\n\n    public KeyExtractor fromValue(Object oldValue) {\n        this.tempOldValue = oldValue;\n        return this;\n    }\n\n    public KeyExtractor toValue(String newKeyValue) {\n        translatedValues.put(tempOldValue, newKeyValue);\n        return this;\n    }\n\n    @Override\n    public void apply(Settings prefs) {\n        if (prefs.contains(newKey)) {\n            return;\n        }\n\n        Object oldValue = prefs.getAll().get(oldKey);\n        Object newValue = translatedValues.get(oldValue);\n\n        if (newValue != null) {\n            prefs.save(newKey, newValue);\n        }\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/migration/KeyMover.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.util.Map;\n\npublic class KeyMover implements Migration {\n    private final String key;\n    private Settings newPrefs;\n\n    public KeyMover(String key) {\n        this.key = key;\n    }\n\n    public KeyMover toPreferences(Settings newPrefs) {\n        this.newPrefs = newPrefs;\n        return this;\n    }\n\n    @Override\n    public void apply(Settings prefs) {\n        if (newPrefs.contains(key) || !prefs.contains(key)) {\n            return;\n        }\n\n        Map<String, ?> all = prefs.getAll();\n        Object value = all.get(key);\n\n        prefs.remove(key);\n        newPrefs.save(key, value);\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/migration/KeyRenamer.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.odk.collect.settings.migration.MigrationUtils.replace;\n\nimport org.odk.collect.shared.settings.Settings;\n\npublic class KeyRenamer implements Migration {\n\n    String oldKey;\n    String newKey;\n\n    KeyRenamer(String oldKey) {\n        this.oldKey = oldKey;\n    }\n\n    public KeyRenamer toKey(String newKey) {\n        this.newKey = newKey;\n        return this;\n    }\n\n    public void apply(Settings prefs) {\n        if (prefs.contains(oldKey) && !prefs.contains(newKey)) {\n            Object value = prefs.getAll().get(oldKey);\n            replace(prefs, oldKey, newKey, value);\n        }\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/migration/KeyTranslator.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.odk.collect.settings.migration.MigrationUtils.replace;\n\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class KeyTranslator implements Migration {\n\n    String oldKey;\n    String newKey;\n    Object tempOldValue;\n    Map<Object, Object> translatedValues = new HashMap<>();\n\n    KeyTranslator(String oldKey) {\n        this.oldKey = oldKey;\n    }\n\n    public KeyTranslator toKey(String newKey) {\n        this.newKey = newKey;\n        return this;\n    }\n\n    public KeyTranslator fromValue(Object oldValue) {\n        this.tempOldValue = oldValue;\n        return this;\n    }\n\n    public KeyTranslator toValue(Object newValue) {\n        translatedValues.put(tempOldValue, newValue);\n        return this;\n    }\n\n    public void apply(Settings prefs) {\n        if (prefs.contains(oldKey) && !prefs.contains(newKey)) {\n            Object oldValue = prefs.getAll().get(oldKey);\n            Object newValue = translatedValues.get(oldValue);\n            if (newValue != null) {\n                replace(prefs, oldKey, newKey, newValue);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/migration/KeyUpdater.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.odk.collect.settings.migration.MigrationUtils.asPairs;\n\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\n\npublic class KeyUpdater implements Migration {\n\n    String[] oldKeys;\n    Object[] tempOldValueArray;\n    List<Object[]> oldValueArrays = new ArrayList<>();\n    List<KeyValuePair[]> newKeyValuePairArrays = new ArrayList<>();\n\n    KeyUpdater(String... oldKeys) {\n        this.oldKeys = oldKeys;\n    }\n\n    public KeyUpdater withValues(Object... oldValues) {\n        tempOldValueArray = oldValues;\n        return this;\n    }\n\n    public KeyUpdater toPairs(Object... keysAndValues) {\n        oldValueArrays.add(tempOldValueArray);\n        newKeyValuePairArrays.add(asPairs(keysAndValues));\n        return this;\n    }\n\n    public void apply(Settings prefs) {\n        Map<String, ?> prefMap = prefs.getAll();\n        Object[] oldValues = new Object[oldKeys.length];\n        for (int i = 0; i < oldKeys.length; i++) {\n            oldValues[i] = prefMap.get(oldKeys[i]);\n        }\n        for (int i = 0; i < oldValueArrays.size(); i++) {\n            if (Arrays.equals(oldValues, oldValueArrays.get(i))) {\n                for (KeyValuePair keyValuePair : newKeyValuePairArrays.get(i)) {\n                    prefs.save(keyValuePair.key, keyValuePair.value);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/migration/KeyValuePair.java",
    "content": "package org.odk.collect.settings.migration;\n\n/** A single preference setting, consisting of a String key and a value of varying type. */\nclass KeyValuePair {\n    final String key;\n    final Object value;\n\n    KeyValuePair(String key, Object value) {\n        this.key = key;\n        this.value = value;\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/migration/Migration.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport org.odk.collect.shared.settings.Settings;\n\npublic interface Migration {\n    void apply(Settings prefs);\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/migration/MigrationUtils.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport android.annotation.SuppressLint;\n\nimport org.odk.collect.shared.settings.Settings;\n\npublic final class MigrationUtils {\n\n    private MigrationUtils() {\n\n    }\n\n    public static ValueTranslator translateValue(String value) {\n        return new ValueTranslator(value);\n    }\n\n    /**\n     * A migration that moves the value of an old preference key over to a new key.\n     * Removes the old key and writes the new key ONLY if the old key is set and\n     * the new key is not set.  Example:\n     *         renameKey(\"color\").toKey(\"couleur\")\n     */\n    public static KeyRenamer renameKey(String oldKey) {\n        return new KeyRenamer(oldKey);\n    }\n\n    /**\n     * A migration that replaces an old preference key with a new key, translating\n     * specific old values to specific new values.  Removes the old key and writes\n     * the new key ONLY if the old key's value exactly matches one of the values\n     * passed to fromValue() and the new key is not set.  Example:\n     *         translateKey(\"color\").toKey(\"couleur\")\n     *             .fromValue(\"red\").toValue(\"rouge\")\n     *             .fromValue(\"yellow\").toValue(\"jaune\")\n     */\n    public static KeyTranslator translateKey(String oldKey) {\n        return new KeyTranslator(oldKey);\n    }\n\n    /**\n     * A migration that combines multiple keys by looking for specific sets of\n     * old values across multiple keys, and replacing them with new key-value pairs.\n     * Removes the old keys and writes the new key-value pairs ONLY if all the\n     * values of the old keys match the set of values passed to withValues().\n     * New key-value pairs MAY overwrite existing keys.  Example:\n     *         combineKeys(\"colour\", \"op\")\n     *             .withValues(\"red\", \"lighten\")\n     *                 .toPairs(\"colour\", \"pink\")  // only if hue = red AND op = lighten\n     *             .withValues(\"yellow\", \"darken\")\n     *                 .toPairs(\"color\", \"brown\")  // only if hue = yellow AND op = darken\n     */\n    public static KeyCombiner combineKeys(String... oldKeys) {\n        return new KeyCombiner(oldKeys);\n    }\n\n    /**\n     * It works like KeyCombiner BUT it does not remove the old keys\n     */\n    public static KeyUpdater updateKeys(String... oldKeys) {\n        return new KeyUpdater(oldKeys);\n    }\n\n    public static KeyMover moveKey(String key) {\n        return new KeyMover(key);\n    }\n\n    public static Migration removeKey(String key) {\n        return prefs -> prefs.remove(key);\n    }\n\n    public static KeyExtractor extractNewKey(String oldKey) {\n        return new KeyExtractor(oldKey);\n    }\n\n    /** Removes an old key and sets a new key. */\n    @SuppressLint(\"ApplySharedPref\")\n    static void replace(Settings prefs, String oldKey, String newKey, Object newValue) {\n        prefs.remove(oldKey);\n        prefs.save(newKey, newValue);\n    }\n\n    /** Removes one or more old keys, then adds one or more new key-value pairs. */\n    @SuppressLint(\"ApplySharedPref\")\n    static void replace(Settings prefs, String[] oldKeys, KeyValuePair... newKeyValuePairs) {\n        for (String key : oldKeys) {\n            prefs.remove(key);\n        }\n        for (KeyValuePair keyValuePair : newKeyValuePairs) {\n            prefs.save(keyValuePair.key, keyValuePair.value);\n        }\n    }\n\n    /** Converts an array of alternating keys and values into an array of Pairs. */\n    static KeyValuePair[] asPairs(Object... args) {\n        KeyValuePair[] keyValuePairs = new KeyValuePair[args.length / 2];\n        for (int i = 0; i * 2 + 1 < args.length; i++) {\n            keyValuePairs[i] = new KeyValuePair((String) args[i * 2], args[i * 2 + 1]);\n        }\n        return keyValuePairs;\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/migration/ValueTranslator.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.util.Map;\n\npublic class ValueTranslator implements Migration {\n\n    private final String oldValue;\n    private String newValue;\n    private String key;\n\n    public ValueTranslator(String oldValue) {\n        this.oldValue = oldValue;\n    }\n\n    public ValueTranslator toValue(String newValue) {\n        this.newValue = newValue;\n        return this;\n    }\n\n    public ValueTranslator forKey(String key) {\n        this.key = key;\n        return this;\n    }\n\n    @Override\n    public void apply(Settings prefs) {\n        if (!prefs.contains(key)) {\n            return;\n        }\n\n        Map<String, ?> all = prefs.getAll();\n        Object prefValue = all.get(key);\n\n        if (prefValue.equals(oldValue)) {\n            prefs.save(key, newValue);\n        }\n    }\n}\n"
  },
  {
    "path": "settings/src/main/java/org/odk/collect/settings/validation/JsonSchemaSettingsValidator.kt",
    "content": "package org.odk.collect.settings.validation\n\nimport com.networknt.schema.Schema\nimport com.networknt.schema.SchemaLocation\nimport com.networknt.schema.SchemaRegistry\nimport com.networknt.schema.dialect.Dialects\nimport com.networknt.schema.keyword.KeywordType\nimport org.json.JSONObject\nimport org.odk.collect.settings.importing.SettingsValidator\nimport org.odk.collect.shared.collections.CollectionExtensions.has\nimport tools.jackson.core.exc.StreamReadException\nimport tools.jackson.databind.ObjectMapper\nimport java.io.InputStream\nimport java.util.Map\n\ninternal class JsonSchemaSettingsValidator(private val schemaProvider: () -> InputStream) :\n    SettingsValidator {\n\n    private val schemaString: String by lazy {\n        schemaProvider().bufferedReader().use { it.readText() }\n    }\n\n    private val schemaJsonObject: JSONObject by lazy {\n        JSONObject(schemaString)\n    }\n\n    override fun isValid(json: String): Boolean {\n        return try {\n            val schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft201909()) {\n                it.schemas(\n                    Map.of(\n                        \"https://example.com/address.schema.json\",\n                        schemaString\n                    )\n                )\n            }\n            val schema: Schema =\n                schemaRegistry.getSchema(SchemaLocation.of(\"https://example.com/address.schema.json\"))\n\n            val errors = schema.validate(ObjectMapper().readTree(json))\n            errors.none { it.keyword != KeywordType.ENUM.value }\n        } catch (_: StreamReadException) {\n            false\n        }\n    }\n\n    override fun isKeySupported(parentJsonObjectName: String, key: String): Boolean {\n        return try {\n            return schemaJsonObject\n                .getJSONObject(\"properties\")\n                .getJSONObject(parentJsonObjectName)\n                .getJSONObject(\"properties\")\n                .has(key)\n        } catch (_: StreamReadException) {\n            false\n        }\n    }\n\n    override fun isValueSupported(parentJsonObjectName: String, key: String, value: Any): Boolean {\n        return try {\n            val settingJsonObject = schemaJsonObject\n                .getJSONObject(\"properties\")\n                .getJSONObject(parentJsonObjectName)\n                .getJSONObject(\"properties\")\n                .getJSONObject(key)\n\n            return if (settingJsonObject.has(\"enum\")) {\n                settingJsonObject.getJSONArray(\"enum\").has(value)\n            } else {\n                true\n            }\n        } catch (_: StreamReadException) {\n            false\n        }\n    }\n}\n"
  },
  {
    "path": "settings/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"auto_send_off\" translatable=\"false\">off</string>\n    <string name=\"auto_send_wifi_only\" translatable=\"false\">wifi_only</string>\n    <string name=\"auto_send_cellular_only\" translatable=\"false\">cellular_only</string>\n    <string name=\"auto_send_wifi_and_cellular\" translatable=\"false\">wifi_and_cellular</string>\n\n    <string name=\"form_update_mode_manual\" translatable=\"false\">manual</string>\n    <string name=\"form_update_mode_previously_downloaded\" translatable=\"false\">previously_downloaded</string>\n    <string name=\"form_update_mode_match_exactly\" translatable=\"false\">match_exactly</string>\n\n    <string name=\"guidance_hint_no\" translatable=\"false\">no</string>\n    <string name=\"guidance_hint_yes\" translatable=\"false\">yes</string>\n    <string name=\"guidance_hint_yes_collapsed\" translatable=\"false\">yes_collapsed</string>\n</resources>\n"
  },
  {
    "path": "settings/src/main/resources/client-settings.schema.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\n  \"$id\": \"https://getodk.org/client-settings.schema.json\",\n  \"title\": \"ODK Client Settings\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"project\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"icon\": {\n          \"type\": \"string\"\n        },\n        \"color\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"general\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"protocol\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"odk_default\",\n            \"google_sheets\"\n          ]\n        },\n        \"server_url\": {\n          \"type\": \"string\"\n        },\n        \"username\": {\n          \"type\": \"string\"\n        },\n        \"password\": {\n          \"type\": \"string\"\n        },\n        \"selected_google_account\": {\n          \"type\": \"string\"\n        },\n        \"google_sheets_url\": {\n          \"type\": \"string\"\n        },\n        \"appTheme\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"system\",\n            \"light_theme\",\n            \"dark_theme\"\n          ]\n        },\n        \"app_language\": {\n          \"type\": \"string\"\n        },\n        \"font_size\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"13\",\n            \"17\",\n            \"21\",\n            \"25\",\n            \"29\"\n          ]\n        },\n        \"navigation\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"swipe\",\n            \"buttons\",\n            \"swipe_buttons\"\n          ]\n        },\n        \"basemap_source\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"google\",\n            \"mapbox\",\n            \"osm\",\n            \"usgs\",\n            \"stamen\",\n            \"carto\"\n          ]\n        },\n        \"google_map_style\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"1\",\n            \"2\",\n            \"3\",\n            \"4\"\n          ]\n        },\n        \"mapbox_map_style\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"mapbox://styles/mapbox/light-v10\",\n            \"mapbox://styles/mapbox/dark-v10\",\n            \"mapbox://styles/mapbox/satellite-v9\",\n            \"mapbox://styles/mapbox/satellite-streets-v11\",\n            \"mapbox://styles/mapbox/streets-v11\",\n            \"mapbox://styles/mapbox/outdoors-v11\"\n          ]\n        },\n        \"usgs_map_style\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"topographic\",\n            \"hybrid\",\n            \"satellite\"\n          ]\n        },\n        \"carto_map_style\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"positron\",\n            \"dark_matter\"\n          ]\n        },\n        \"reference_layer\": {\n          \"type\": \"string\"\n        },\n        \"form_update_mode\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"manual\",\n            \"previously_downloaded\",\n            \"match_exactly\"\n          ]\n        },\n        \"periodic_form_updates_check\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"every_fifteen_minutes\",\n            \"every_one_hour\",\n            \"every_six_hours\",\n            \"every_24_hours\"\n          ]\n        },\n        \"automatic_update\": {\n          \"type\": \"boolean\"\n        },\n        \"hide_old_form_versions\": {\n          \"type\": \"boolean\"\n        },\n        \"autosend\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"off\",\n            \"wifi_only\",\n            \"cellular_only\",\n            \"wifi_and_cellular\"\n          ]\n        },\n        \"delete_send\": {\n          \"type\": \"boolean\"\n        },\n        \"default_completed\": {\n          \"type\": \"boolean\",\n          \"deprecated\": true\n        },\n        \"constraint_behavior\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"on_swipe\",\n            \"on_finalize\"\n          ]\n        },\n        \"high_resolution\": {\n          \"type\": \"boolean\"\n        },\n        \"image_size\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"original_image_size\",\n            \"small\",\n            \"very_small\",\n            \"medium\",\n            \"large\"\n          ]\n        },\n        \"guidance_hint\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"no\",\n            \"yes\",\n            \"yes_collapsed\"\n          ]\n        },\n        \"external_app_recording\": {\n          \"type\": \"boolean\"\n        },\n        \"instance_sync\": {\n          \"type\": \"boolean\"\n        },\n        \"analytics\": {\n          \"type\": \"boolean\"\n        },\n        \"metadata_username\": {\n          \"type\": \"string\"\n        },\n        \"metadata_phonenumber\": {\n          \"type\": \"string\"\n        },\n        \"metadata_email\": {\n          \"type\": \"string\"\n        },\n        \"background_location\": {\n          \"type\": \"boolean\"\n        },\n        \"background_recording\": {\n          \"type\": \"boolean\"\n        }\n      }\n    },\n    \"admin\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"admin_pw\": {\n          \"type\": \"string\"\n        },\n        \"edit_saved\": {\n          \"type\": \"boolean\"\n        },\n        \"send_finalized\": {\n          \"type\": \"boolean\"\n        },\n        \"view_sent\": {\n          \"type\": \"boolean\"\n        },\n        \"get_blank\": {\n          \"type\": \"boolean\"\n        },\n        \"delete_saved\": {\n          \"type\": \"boolean\"\n        },\n        \"change_server\": {\n          \"type\": \"boolean\"\n        },\n        \"change_project_display\": {\n          \"type\": \"boolean\"\n        },\n        \"change_app_theme\": {\n          \"type\": \"boolean\"\n        },\n        \"change_app_language\": {\n          \"type\": \"boolean\"\n        },\n        \"change_font_size\": {\n          \"type\": \"boolean\"\n        },\n        \"change_navigation\": {\n          \"type\": \"boolean\"\n        },\n        \"maps\": {\n          \"type\": \"boolean\"\n        },\n        \"form_update_mode\": {\n          \"type\": \"boolean\"\n        },\n        \"periodic_form_updates_check\": {\n          \"type\": \"boolean\"\n        },\n        \"automatic_update\": {\n          \"type\": \"boolean\"\n        },\n        \"hide_old_form_versions\": {\n          \"type\": \"boolean\"\n        },\n        \"change_autosend\": {\n          \"type\": \"boolean\"\n        },\n        \"delete_after_send\": {\n          \"type\": \"boolean\"\n        },\n        \"change_constraint_behavior\": {\n          \"type\": \"boolean\"\n        },\n        \"high_resolution\": {\n          \"type\": \"boolean\"\n        },\n        \"image_size\": {\n          \"type\": \"boolean\"\n        },\n        \"guidance_hint\": {\n          \"type\": \"boolean\"\n        },\n        \"external_app_recording\": {\n          \"type\": \"boolean\"\n        },\n        \"instance_form_sync\": {\n          \"type\": \"boolean\"\n        },\n        \"change_form_metadata\": {\n          \"type\": \"boolean\"\n        },\n        \"analytics\": {\n          \"type\": \"boolean\"\n        },\n        \"moving_backwards\": {\n          \"type\": \"boolean\"\n        },\n        \"access_settings\": {\n          \"type\": \"boolean\"\n        },\n        \"change_language\": {\n          \"type\": \"boolean\"\n        },\n        \"jump_to\": {\n          \"type\": \"boolean\"\n        },\n        \"save_mid\": {\n          \"type\": \"boolean\"\n        },\n        \"mark_as_finalized\": {\n          \"type\": \"boolean\",\n          \"deprecated\": true\n        },\n        \"save_as_draft\": {\n          \"type\": \"boolean\"\n        },\n        \"finalize\": {\n          \"type\": \"boolean\",\n          \"deprecated\": true\n        },\n        \"finalize_in_form_entry\": {\n          \"type\": \"boolean\"\n        },\n        \"bulk_finalize\": {\n          \"type\": \"boolean\"\n        }\n      }\n    }\n  },\n  \"required\": [\n    \"general\",\n    \"admin\"\n  ]\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/ODKAppSettingsImporterTest.kt",
    "content": "package org.odk.collect.settings\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.json.JSONObject\nimport org.junit.Test\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.projects.InMemProjectsRepository\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectConfigurationResult\nimport org.odk.collect.settings.importing.SettingsChangeHandler\nimport org.odk.collect.settings.support.SettingsUtils.assertSettingsEmpty\nimport java.lang.RuntimeException\n\nclass ODKAppSettingsImporterTest {\n    private val projectsRepository = InMemProjectsRepository()\n    private val settingsProvider = InMemSettingsProvider()\n    private val settingsChangeHandler = mock<SettingsChangeHandler>()\n\n    private val settingsImporter = ODKAppSettingsImporter(\n        projectsRepository,\n        settingsProvider,\n        mapOf(\"server_url\" to \"https://demo.getodk.org\"),\n        emptyMap(),\n        listOf(\"#00000\"),\n        settingsChangeHandler,\n        JSONObject()\n    )\n\n    @Test\n    fun `accepts valid JSON`() {\n        val result = settingsImporter.fromJSON(\n            \"{\\n\" +\n                \"  \\\"general\\\": {\\n\" +\n                \"  },\\n\" +\n                \"  \\\"admin\\\": {\\n\" +\n                \"  }\\n\" +\n                \"}\",\n            projectsRepository.save(Project.New(\"Flat\", \"AS\", \"#ff0000\"))\n        )\n        assertThat(result, equalTo(ProjectConfigurationResult.SUCCESS))\n    }\n\n    @Test\n    fun `rejects JSON without general object`() {\n        val result = settingsImporter.fromJSON(\n            \"{ \\\"admin\\\": {}}\",\n            projectsRepository.save(Project.New(\"Flat\", \"AS\", \"#ff0000\"))\n        )\n        assertThat(result, equalTo(ProjectConfigurationResult.INVALID_SETTINGS))\n        assertSettingsEmpty(settingsProvider.getUnprotectedSettings())\n        assertSettingsEmpty(settingsProvider.getProtectedSettings())\n    }\n\n    @Test\n    fun `rejects JSON without admin object`() {\n        val result = settingsImporter.fromJSON(\n            \"{ \\\"general\\\": {}}\",\n            projectsRepository.save(Project.New(\"Flat\", \"AS\", \"#ff0000\"))\n        )\n        assertThat(result, equalTo(ProjectConfigurationResult.INVALID_SETTINGS))\n        assertSettingsEmpty(settingsProvider.getUnprotectedSettings())\n        assertSettingsEmpty(settingsProvider.getProtectedSettings())\n    }\n\n    @Test\n    fun `rejects invalid JSON`() {\n        val result = settingsImporter.fromJSON(\n            \"{\\\"general\\\":{*},\\\"admin\\\":{}}\",\n            projectsRepository.save(Project.New(\"Flat\", \"AS\", \"#ff0000\"))\n        )\n        assertThat(result, equalTo(ProjectConfigurationResult.INVALID_SETTINGS))\n        assertSettingsEmpty(settingsProvider.getUnprotectedSettings())\n        assertSettingsEmpty(settingsProvider.getProtectedSettings())\n    }\n\n    @Test\n    fun `rejects JSON when exception is thrown during importing`() {\n        whenever(settingsChangeHandler.onSettingsChanged(any(), any(), any())).thenThrow(RuntimeException::class.java)\n\n        val result = settingsImporter.fromJSON(\n            \"{\\n\" +\n                \"  \\\"general\\\": {\\n\" +\n                \"  },\\n\" +\n                \"  \\\"admin\\\": {\\n\" +\n                \"  }\\n\" +\n                \"}\",\n            projectsRepository.save(Project.New(\"Flat\", \"AS\", \"#ff0000\"))\n        )\n        assertThat(result, equalTo(ProjectConfigurationResult.INVALID_SETTINGS))\n    }\n\n    @Test\n    fun `rejects JSON with google_sheets protocol`() {\n        val result = settingsImporter.fromJSON(\n            \"{\\n\" +\n                \"  \\\"general\\\": {\\n\" +\n                \"       \\\"protocol\\\" : \\\"google_sheets\\\"\" +\n                \"  },\\n\" +\n                \"  \\\"admin\\\": {\\n\" +\n                \"  }\\n\" +\n                \"}\",\n            projectsRepository.save(Project.New(\"Flat\", \"AS\", \"#ff0000\"))\n        )\n        assertThat(result, equalTo(ProjectConfigurationResult.GD_PROJECT))\n        assertSettingsEmpty(settingsProvider.getUnprotectedSettings())\n        assertSettingsEmpty(settingsProvider.getProtectedSettings())\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/ODKAppSettingsMigratorTest.java",
    "content": "package org.odk.collect.settings;\n\nimport static org.hamcrest.CoreMatchers.equalTo;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.odk.collect.settings.support.SettingsUtils.assertSettings;\nimport static org.odk.collect.settings.support.SettingsUtils.assertSettingsEmpty;\nimport static org.odk.collect.settings.support.SettingsUtils.initSettings;\nimport static java.util.Arrays.asList;\n\nimport org.junit.Test;\nimport org.odk.collect.settings.keys.ProjectKeys;\nimport org.odk.collect.settings.keys.ProtectedProjectKeys;\nimport org.odk.collect.shared.settings.InMemSettings;\nimport org.odk.collect.shared.settings.Settings;\n\nimport java.util.List;\n\npublic class ODKAppSettingsMigratorTest {\n\n    private final Settings unprotectedSettings = new InMemSettings();\n    private final Settings protectedSettings = new InMemSettings();\n    private final Settings metaSettings = new InMemSettings();\n\n    @Test\n    public void shouldMigrateGoogleMapSettings() {\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"google_maps\", \"map_basemap_behavior\", \"streets\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"google\", \"google_map_style\", String.valueOf(1));\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"google_maps\", \"map_basemap_behavior\", \"satellite\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"google\", \"google_map_style\", String.valueOf(2));\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"google_maps\", \"map_basemap_behavior\", \"terrain\\u200e\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"google\", \"google_map_style\", String.valueOf(3));\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"google_maps\", \"map_basemap_behavior\", \"hybrid\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"google\", \"google_map_style\", String.valueOf(4));\n    }\n\n    @Test\n    public void shouldMigrateMapboxMapSettings() {\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"mapbox_maps\", \"map_basemap_behavior\", \"mapbox_streets\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"mapbox\", \"mapbox_map_style\", \"mapbox://styles/mapbox/streets-v11\");\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"mapbox_maps\", \"map_basemap_behavior\", \"mapbox_light\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"mapbox\", \"mapbox_map_style\", \"mapbox://styles/mapbox/light-v10\");\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"mapbox_maps\", \"map_basemap_behavior\", \"mapbox_dark\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"mapbox\", \"mapbox_map_style\", \"mapbox://styles/mapbox/dark-v10\");\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"mapbox_maps\", \"map_basemap_behavior\", \"mapbox_satellite\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"mapbox\", \"mapbox_map_style\", \"mapbox://styles/mapbox/satellite-v9\");\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"mapbox_maps\", \"map_basemap_behavior\", \"mapbox_satellite_streets\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"mapbox\", \"mapbox_map_style\", \"mapbox://styles/mapbox/satellite-streets-v11\");\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"mapbox_maps\", \"map_basemap_behavior\", \"mapbox_outdoors\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"mapbox\", \"mapbox_map_style\", \"mapbox://styles/mapbox/outdoors-v11\");\n    }\n\n    @Test\n    public void shouldMigrateOsmMapSettings() {\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"osmdroid\", \"map_basemap_behavior\", \"openmap_streets\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", ProjectKeys.BASEMAP_SOURCE_OSM);\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"osmdroid\", \"map_basemap_behavior\", \"openmap_usgs_topo\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"usgs\", \"usgs_map_style\", \"topographic\");\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"osmdroid\", \"map_basemap_behavior\", \"openmap_usgs_sat\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"usgs\", \"usgs_map_style\", \"hybrid\");\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"osmdroid\", \"map_basemap_behavior\", \"openmap_usgs_img\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"usgs\", \"usgs_map_style\", \"satellite\");\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"osmdroid\", \"map_basemap_behavior\", \"openmap_stamen_terrain\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", ProjectKeys.BASEMAP_SOURCE_OSM);\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"osmdroid\", \"map_basemap_behavior\", \"openmap_cartodb_positron\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"carto\", \"carto_map_style\", \"positron\");\n\n        initSettings(unprotectedSettings, \"map_sdk_behavior\", \"osmdroid\", \"map_basemap_behavior\", \"openmap_cartodb_darkmatter\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", \"carto\", \"carto_map_style\", \"dark_matter\");\n    }\n\n    @Test\n    public void shouldMigrateAdminSettings() {\n        initSettings(protectedSettings, \"unrelated\", \"value\");\n        runMigrations();\n        assertSettings(protectedSettings, \"unrelated\", \"value\");\n\n        initSettings(protectedSettings, \"show_map_sdk\", true);\n        runMigrations();\n        assertSettings(protectedSettings, \"show_map_sdk\", true);\n\n        initSettings(protectedSettings, \"show_map_sdk\", false);\n        runMigrations();\n        assertSettings(protectedSettings, \"maps\", false);\n\n        initSettings(protectedSettings, \"show_map_basemap\", true);\n        runMigrations();\n        assertSettings(protectedSettings, \"show_map_basemap\", true);\n\n        initSettings(protectedSettings, \"show_map_basemap\", false);\n        runMigrations();\n        assertSettings(protectedSettings, \"maps\", false);\n    }\n\n    @Test\n    public void migratesMetaKeysToMetaPrefs() {\n        initSettings(unprotectedSettings,\n                \"firstRun\", true,\n                \"lastVersion\", 1L,\n                \"scoped_storage_used\", true,\n                \"metadata_migrated\", true,\n                \"mapbox_initialized\", true\n        );\n\n        runMigrations();\n\n        assertSettingsEmpty(unprotectedSettings);\n        assertSettings(metaSettings,\n                \"scoped_storage_used\", true,\n                \"mapbox_initialized\", true\n        );\n    }\n\n    @Test\n    public void migratesServerType() {\n        initSettings(unprotectedSettings, \"protocol\", \"other_protocol\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"protocol\", \"odk_default\");\n    }\n\n    @Test\n    public void migratesAutosendSettings() {\n        initSettings(unprotectedSettings,\n                \"autosend_wifi\", false,\n                \"autosend_network\", false\n        );\n        runMigrations();\n        assertSettings(unprotectedSettings,\n                \"autosend\", \"off\"\n        );\n\n        initSettings(unprotectedSettings,\n                \"autosend_wifi\", true,\n                \"autosend_network\", false\n        );\n        runMigrations();\n        assertSettings(unprotectedSettings,\n                \"autosend\", \"wifi_only\"\n        );\n\n        initSettings(unprotectedSettings,\n                \"autosend_wifi\", false,\n                \"autosend_network\", true\n        );\n        runMigrations();\n        assertSettings(unprotectedSettings,\n                \"autosend\", \"cellular_only\"\n        );\n\n        initSettings(unprotectedSettings,\n                \"autosend_wifi\", true,\n                \"autosend_network\", true\n        );\n        runMigrations();\n        assertSettings(unprotectedSettings,\n                \"autosend\", \"wifi_and_cellular\"\n        );\n    }\n\n    @Test\n    public void migratesFormUpdateModeSettings() {\n        initSettings(unprotectedSettings,\n                \"periodic_form_updates_check\", \"never\"\n        );\n        runMigrations();\n        assertSettings(unprotectedSettings,\n                \"form_update_mode\", \"manual\",\n                \"periodic_form_updates_check\", \"every_fifteen_minutes\"\n        );\n\n        List<String> periods = asList(\"every_fifteen_minutes\", \"every_one_hour\", \"every_six_hours\", \"every_24_hours\");\n        for (String period : periods) {\n            initSettings(unprotectedSettings,\n                    \"periodic_form_updates_check\", period\n            );\n            runMigrations();\n            assertSettings(unprotectedSettings,\n                    \"periodic_form_updates_check\", period,\n                    \"form_update_mode\", \"previously_downloaded\"\n            );\n        }\n\n        initSettings(unprotectedSettings,\n                \"protocol\", \"google_sheets\"\n        );\n        runMigrations();\n        assertSettings(unprotectedSettings,\n                \"protocol\", \"google_sheets\",\n                \"form_update_mode\", \"manual\"\n        );\n\n        initSettings(unprotectedSettings,\n                \"protocol\", \"google_sheets\",\n                \"periodic_form_updates_check\", \"every_24_hours\"\n        );\n        runMigrations();\n        assertSettings(unprotectedSettings,\n                \"protocol\", \"google_sheets\",\n                \"form_update_mode\", \"manual\",\n                \"periodic_form_updates_check\", \"every_24_hours\"\n        );\n\n        initSettings(unprotectedSettings,\n                \"protocol\", \"google_sheets\",\n                \"periodic_form_updates_check\", \"never\"\n        );\n        runMigrations();\n        assertSettings(unprotectedSettings,\n                \"protocol\", \"google_sheets\",\n                \"form_update_mode\", \"manual\",\n                \"periodic_form_updates_check\", \"every_fifteen_minutes\"\n        );\n    }\n\n    @Test\n    public void migratesServerList() {\n        initSettings(unprotectedSettings,\n                \"knownUrlList\", \"[\\\"http://blah.com\\\"]\"\n        );\n\n        runMigrations();\n        assertSettingsEmpty(unprotectedSettings);\n        assertSettings(metaSettings,\n                \"server_list\", \"[\\\"http://blah.com\\\"]\"\n        );\n    }\n\n    @Test\n    public void when_markAsFinalized_wasDisabled_and_defaultCompleted_wasDisabled_thenDisableNew_finalize_andRemoveOldSettings() {\n        initSettings(protectedSettings, \"mark_as_finalized\", false);\n        initSettings(unprotectedSettings, \"default_completed\", false);\n\n        runMigrations();\n\n        assertSettings(protectedSettings,\n                ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true,\n                ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false,\n                ProtectedProjectKeys.KEY_BULK_FINALIZE, false\n        );\n\n        assertThat(protectedSettings.contains(\"mark_as_finalized\"), equalTo(false));\n        assertThat(protectedSettings.contains(\"default_completed\"), equalTo(false));\n    }\n\n    @Test\n    public void when_markAsFinalized_wasDisabled_and_defaultCompleted_wasEnabled_thenDisableNew_saveAsDraft_andRemoveOldSettings() {\n        initSettings(protectedSettings, \"mark_as_finalized\", false);\n        initSettings(unprotectedSettings, \"default_completed\", true);\n\n        runMigrations();\n\n        assertSettings(protectedSettings,\n                ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false,\n                ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true,\n                ProtectedProjectKeys.KEY_BULK_FINALIZE, true\n        );\n\n        assertThat(protectedSettings.contains(\"mark_as_finalized\"), equalTo(false));\n        assertThat(protectedSettings.contains(\"default_completed\"), equalTo(false));\n    }\n\n    @Test\n    public void when_markAsFinalized_wasDisabled_and_defaultCompleted_wasNotSet_thenDisableNew_finalize_andRemoveOldSettings() {\n        initSettings(protectedSettings, \"mark_as_finalized\", false);\n\n        runMigrations();\n\n        assertSettings(protectedSettings,\n                ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false,\n                ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true,\n                ProtectedProjectKeys.KEY_BULK_FINALIZE, true\n        );\n\n        assertThat(protectedSettings.contains(\"mark_as_finalized\"), equalTo(false));\n        assertThat(protectedSettings.contains(\"default_completed\"), equalTo(false));\n    }\n\n    @Test\n    public void when_markAsFinalized_wasEnabled_and_defaultCompleted_wasDisabled_thenDoNotUpdateSettingsAndRemoveOldOnes() {\n        initSettings(protectedSettings, \"mark_as_finalized\", true);\n        initSettings(unprotectedSettings, \"default_completed\", false);\n\n        runMigrations();\n\n        assertSettingsEmpty(protectedSettings);\n\n        assertThat(protectedSettings.contains(\"mark_as_finalized\"), equalTo(false));\n        assertThat(protectedSettings.contains(\"default_completed\"), equalTo(false));\n    }\n\n    @Test\n    public void when_markAsFinalized_wasEnabled_and_defaultCompleted_wasEnabled_thenDoNotUpdateSettingsAndRemoveOldOnes() {\n        initSettings(protectedSettings, \"mark_as_finalized\", true);\n        initSettings(unprotectedSettings, \"default_completed\", true);\n\n        runMigrations();\n\n        assertSettingsEmpty(protectedSettings);\n\n        assertThat(protectedSettings.contains(\"mark_as_finalized\"), equalTo(false));\n        assertThat(protectedSettings.contains(\"default_completed\"), equalTo(false));\n    }\n\n    @Test\n    public void when_AllowOtherWaysOfEditingFormIsDisabled_thenSaveAsDraftShouldBeDisabledAndFinalizeShouldBeEnabled() {\n        initSettings(protectedSettings,\n                ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, false,\n                ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true,\n                \"finalize\", false\n        );\n\n        runMigrations();\n\n        assertThat(protectedSettings.contains(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(true));\n        assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(false));\n        assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT), equalTo(false));\n        assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(true));\n    }\n\n    @Test\n    public void when_AllowOtherWaysOfEditingFormIsEnabled_thenDoNotUpdateSaveAsDraftOrFinalize() {\n        initSettings(protectedSettings,\n                ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, true,\n                ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true,\n                \"finalize\", false\n        );\n\n        runMigrations();\n\n        assertThat(protectedSettings.contains(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(true));\n        assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(true));\n        assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT), equalTo(true));\n        assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(false));\n    }\n\n    @Test\n    public void migratesFinalizeInFormEntryToNewKey() {\n        initSettings(protectedSettings, \"finalize\", false);\n\n        runMigrations();\n\n        assertThat(protectedSettings.contains(\"finalize\"), equalTo(false));\n        assertThat(protectedSettings.contains(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(true));\n        assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(false));\n    }\n\n    @Test\n    public void whenFinalizeInFormEntryWasDisabledWithOldKey_disablesBulkFinalize() {\n        initSettings(protectedSettings, \"finalize\", false);\n\n        runMigrations();\n\n        assertThat(protectedSettings.contains(ProtectedProjectKeys.KEY_BULK_FINALIZE), equalTo(true));\n        assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_BULK_FINALIZE), equalTo(false));\n    }\n\n    @Test\n    public void migratesStamenMapsToOSM() {\n        initSettings(unprotectedSettings, \"basemap_source\", \"stamen\");\n        runMigrations();\n        assertSettings(unprotectedSettings, \"basemap_source\", ProjectKeys.BASEMAP_SOURCE_OSM);\n    }\n\n    private void runMigrations() {\n        new ODKAppSettingsMigrator(metaSettings).migrate(unprotectedSettings, protectedSettings);\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/importing/ProjectDetailsCreatorImplTest.kt",
    "content": "package org.odk.collect.settings.importing\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.junit.Test\nimport org.odk.collect.projects.Project\nimport org.odk.collect.settings.keys.ProjectKeys\n\nclass ProjectDetailsCreatorImplTest {\n\n    private val projectDetailsCreator = ProjectDetailsCreatorImpl(\n        listOf(\n            \"#EA4633\",\n            \"#9E9E9E\"\n        ),\n        mapOf<String, Any>(\n            ProjectKeys.KEY_SERVER_URL to \"http://default.example.com\"\n        )\n    )\n\n    @Test\n    fun `If project name is included in project details should be used`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(name = \"Project X\").name, `is`(\"Project X\"))\n    }\n\n    @Test\n    fun `When no project name is specified and the connection identifier is a valid URL, the project name is the URL domain`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(connectionIdentifier = \"https://my-project.com\").name, `is`(\"my-project.com\"))\n        assertThat(projectDetailsCreator.createProjectFromDetails(connectionIdentifier = \"https://your-project.com/one\").name, `is`(\"your-project.com\"))\n        assertThat(projectDetailsCreator.createProjectFromDetails(connectionIdentifier = \"http://www.my-project.com\").name, `is`(\"www.my-project.com\"))\n    }\n\n    @Test\n    fun `When no project name is specified and the connection identifier is not a valid URL, the project name is the connection identifier`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(connectionIdentifier = \"https:/\\\\/kc.humanitarianresponse.info\").name, `is`(\"https:/\\\\/kc.humanitarianresponse.info\"))\n        assertThat(projectDetailsCreator.createProjectFromDetails(connectionIdentifier = \"foo@bar.baz\").name, `is`(\"foo@bar.baz\"))\n        assertThat(projectDetailsCreator.createProjectFromDetails(connectionIdentifier = \"something\").name, `is`(\"something\"))\n    }\n\n    @Test\n    fun `When no project name is specified and the connection identifier is the demo project explicitly or by default, the project name is 'Demo project'`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(connectionIdentifier = \"http://default.example.com\").name, `is`(Project.DEMO_PROJECT_NAME))\n        assertThat(projectDetailsCreator.createProjectFromDetails(connectionIdentifier = \"\").name, `is`(Project.DEMO_PROJECT_NAME))\n        assertThat(projectDetailsCreator.createProjectFromDetails().name, `is`(Project.DEMO_PROJECT_NAME))\n    }\n\n    @Test\n    fun `If project name is empty should be generated based on connection identifier`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(name = \" \", connectionIdentifier = \"https://my-server.com\").name, `is`(\"my-server.com\"))\n    }\n\n    @Test\n    fun `If project icon is included in project details and it's a normal char should be used`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(icon = \"X\").icon, `is`(\"X\"))\n    }\n\n    @Test\n    fun `If project icon is included in project details and it's an emoticon should be used`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(icon = \"\\uD83D\\uDC22\").icon, `is`(\"\\uD83D\\uDC22\"))\n    }\n\n    @Test\n    fun `If project icon is not included in project details should be generated based on project name`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(name = \"Project X\").icon, `is`(\"P\"))\n    }\n\n    @Test\n    fun `If project icon is empty should be generated based on project name`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(name = \"My Project X\", icon = \" \").icon, `is`(\"M\"))\n    }\n\n    @Test\n    fun `If project icon is not included in project details and there is no project name should be generated based on the connection identifier`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(connectionIdentifier = \"https://my-server.com\").icon, `is`(\"M\"))\n    }\n\n    @Test\n    fun `If project icon is not included in project details and there is no project and no connection identifier should be generated based on demo name`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails().icon, `is`(\"D\"))\n    }\n\n    @Test\n    fun `If project icon is included in project details but longer than one sign, only the first sign should be used`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(icon = \"XX\").icon, `is`(\"X\"))\n        assertThat(projectDetailsCreator.createProjectFromDetails(icon = \"\\uD83D\\uDC22XX\").icon, `is`(\"\\uD83D\\uDC22\"))\n    }\n\n    @Test\n    fun `If project color is included in project details should be used`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(color = \"#cccccc\").color, `is`(\"#cccccc\"))\n    }\n\n    @Test\n    fun `If project color is not included in project details should be generated based on project name`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(name = \"Project X\").color, `is`(\"#9E9E9E\"))\n    }\n\n    @Test\n    fun `If project color is not included in project details and project name is demo the color should be demo color`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(name = Project.DEMO_PROJECT_NAME).color, `is`(Project.DEMO_PROJECT_COLOR))\n    }\n\n    @Test\n    fun `If project color is included in project details but invalid should be generated based on project name`() {\n        assertThat(projectDetailsCreator.createProjectFromDetails(name = \"Project X\", icon = \"#cc\").color, `is`(\"#9E9E9E\"))\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/importing/SettingsImporterTest.kt",
    "content": "package org.odk.collect.settings.importing\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.`is`\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport org.junit.Before\nimport org.junit.Test\nimport org.mockito.kotlin.any\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.verifyNoMoreInteractions\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.projects.Project\nimport org.odk.collect.projects.ProjectConfigurationResult\nimport org.odk.collect.projects.ProjectsRepository\nimport org.odk.collect.settings.InMemSettingsProvider\nimport org.odk.collect.settings.keys.AppConfigurationKeys\nimport org.odk.collect.settings.keys.ProjectKeys\nimport org.odk.collect.settings.support.SettingsUtils.assertSettings\nimport org.odk.collect.settings.support.SettingsUtils.initSettings\nimport org.odk.collect.shared.settings.Settings\n\nclass SettingsImporterTest {\n\n    private var currentProject = Project.Saved(\"1\", \"Project X\", \"X\", \"#cccccc\")\n\n    private val settingsProvider = InMemSettingsProvider()\n    private val generalSettings = settingsProvider.getUnprotectedSettings(currentProject.uuid)\n    private val adminSettings = settingsProvider.getProtectedSettings(currentProject.uuid)\n\n    private val settingsChangeHandler = mock<SettingsChangeHandler>()\n    private val projectsRepository = mock<ProjectsRepository> {}\n    private var settingsValidator = mock<SettingsValidator> {\n        on { isValid(any()) } doReturn true\n        on { isKeySupported(any(), any()) } doReturn true\n        on { isValueSupported(any(), any(), any()) } doReturn true\n    }\n    private val projectDetailsCreator = mock<ProjectDetailsCreator> {\n        on { createProjectFromDetails() } doReturn Project.DEMO_PROJECT\n    }\n\n    private val generalDefaults: Map<String, Any> = mapOf(\n        \"key1\" to \"default\",\n        \"key2\" to true\n    )\n\n    private val adminDefaults: Map<String, Any> = mapOf(\n        \"key1\" to 5\n    )\n\n    private lateinit var importer: SettingsImporter\n\n    @Before\n    fun setup() {\n        importer = SettingsImporter(\n            settingsProvider,\n            { _: Settings?, _: Settings? -> },\n            settingsValidator,\n            generalDefaults,\n            adminDefaults,\n            settingsChangeHandler,\n            projectsRepository,\n            projectDetailsCreator\n        )\n    }\n\n    @Test\n    fun `when JSON settings are invalid returns 'INVALID_SETTINGS'`() {\n        whenever(settingsValidator.isValid(emptySettings())).thenReturn(false)\n        assertThat(importer.fromJSON(emptySettings(), currentProject, JSONObject()), `is`(ProjectConfigurationResult.INVALID_SETTINGS))\n    }\n\n    @Test\n    fun `unsupported settings should be ignored`() {\n        whenever(settingsValidator.isKeySupported(AppConfigurationKeys.GENERAL, \"key3\")).thenReturn(false)\n        whenever(settingsValidator.isKeySupported(AppConfigurationKeys.ADMIN, \"key3\")).thenReturn(false)\n\n        val json = emptySettingsObject()\n            .put(\n                AppConfigurationKeys.GENERAL,\n                JSONObject().put(\"key3\", \"foo\")\n            )\n            .put(\n                AppConfigurationKeys.ADMIN,\n                JSONObject().put(\"key3\", 5)\n            )\n\n        assertThat(importer.fromJSON(json.toString(), currentProject, JSONObject()), `is`(ProjectConfigurationResult.SUCCESS))\n\n        assertThat(generalSettings.contains(\"key3\"), `is`(false))\n        assertThat(adminSettings.contains(\"key3\"), `is`(false))\n    }\n\n    @Test\n    fun `device unsupported settings should be ignored`() {\n        val json = emptySettingsObject()\n            .put(\n                AppConfigurationKeys.GENERAL,\n                JSONObject()\n                    .put(\"key3\", \"foo\")\n                    .put(\"key4\", \"foo1\")\n            )\n            .put(\n                AppConfigurationKeys.ADMIN,\n                JSONObject()\n                    .put(\"key5\", \"bar\")\n                    .put(\"key6\", \"bar1\")\n            )\n\n        val deviceUnsupportedSettings = emptySettingsObject()\n            .put(\n                AppConfigurationKeys.GENERAL,\n                JSONObject().put(\"key4\", JSONArray(listOf(\"foo1\")))\n            )\n            .put(\n                AppConfigurationKeys.ADMIN,\n                JSONObject().put(\"key5\", JSONArray(listOf(\"bar\")))\n            )\n\n        assertThat(\n            importer.fromJSON(\n                json.toString(),\n                currentProject,\n                deviceUnsupportedSettings\n            ),\n            `is`(ProjectConfigurationResult.SUCCESS)\n        )\n\n        assertThat(generalSettings.contains(\"key3\"), `is`(true))\n        assertThat(generalSettings.contains(\"key4\"), `is`(false))\n        assertThat(adminSettings.contains(\"key5\"), `is`(false))\n        assertThat(adminSettings.contains(\"key6\"), `is`(true))\n    }\n\n    @Test\n    fun `for supported settings that do not exist in json save defaults`() {\n        assertThat(importer.fromJSON(emptySettings(), currentProject, JSONObject()), `is`(ProjectConfigurationResult.SUCCESS))\n        assertSettings(\n            generalSettings,\n            \"key1\",\n            \"default\",\n            \"key2\",\n            true\n        )\n        assertSettings(\n            adminSettings,\n            \"key1\",\n            5\n        )\n    }\n\n    @Test\n    fun forSettingsNotSupported_savesDefaults() {\n        whenever(settingsValidator.isValueSupported(AppConfigurationKeys.GENERAL, \"key1\", \"unsupported_value\")).thenReturn(false)\n        whenever(settingsValidator.isValueSupported(AppConfigurationKeys.ADMIN, \"key1\", 6)).thenReturn(false)\n\n        val json = emptySettingsObject()\n            .put(\n                AppConfigurationKeys.GENERAL,\n                JSONObject().put(\"key1\", \"unsupported_value\")\n            )\n            .put(\n                AppConfigurationKeys.ADMIN,\n                JSONObject().put(\"key1\", 6)\n            )\n\n        assertThat(importer.fromJSON(json.toString(), currentProject, JSONObject()), `is`(ProjectConfigurationResult.SUCCESS))\n        assertSettings(\n            generalSettings,\n            \"key1\",\n            \"default\",\n            \"key2\",\n            true\n        )\n        assertSettings(\n            adminSettings,\n            \"key1\",\n            5\n        )\n    }\n\n    @Test\n    fun whenKeysAlreadyExistInPrefs_overridesWithDefaults() {\n        initSettings(\n            generalSettings,\n            \"key1\",\n            \"existing\",\n            \"key2\",\n            false\n        )\n        initSettings(\n            adminSettings,\n            \"key1\",\n            0\n        )\n        assertThat(importer.fromJSON(emptySettings(), currentProject, JSONObject()), `is`(ProjectConfigurationResult.SUCCESS))\n        assertSettings(\n            generalSettings,\n            \"key1\",\n            \"default\",\n            \"key2\",\n            true\n        )\n        assertSettings(\n            adminSettings,\n            \"key1\",\n            5\n        )\n    }\n\n    @Test // Migrations might add/rename/move keys\n    fun migratesPreferences_beforeLoadingDefaults() {\n        val migrator =\n            SettingsMigrator { _: Settings?, _: Settings? ->\n                if (generalSettings.contains(\"key1\")) {\n                    throw RuntimeException(\"defaults already loaded!\")\n                }\n            }\n        importer = SettingsImporter(\n            settingsProvider,\n            migrator,\n            settingsValidator,\n            generalDefaults,\n            adminDefaults,\n            settingsChangeHandler,\n            projectsRepository,\n            projectDetailsCreator\n        )\n        assertThat(importer.fromJSON(emptySettings(), currentProject, JSONObject()), `is`(ProjectConfigurationResult.SUCCESS))\n    }\n\n    @Test // Migrations might use old keys that are \"unknown\" to the app\n    fun migratesPreferences_beforeClearingUnknowns() {\n        val json = emptySettingsObject()\n            .put(\n                AppConfigurationKeys.GENERAL,\n                JSONObject().put(\"unknown_key\", \"value\")\n            )\n        val migrator =\n            SettingsMigrator { _: Settings?, _: Settings? ->\n                if (!generalSettings.contains(\"unknown_key\")) {\n                    throw RuntimeException(\"unknowns already cleared!\")\n                }\n            }\n        importer = SettingsImporter(\n            settingsProvider,\n            migrator,\n            settingsValidator,\n            generalDefaults,\n            adminDefaults,\n            settingsChangeHandler,\n            projectsRepository,\n            projectDetailsCreator\n        )\n        assertThat(importer.fromJSON(json.toString(), currentProject, JSONObject()), `is`(ProjectConfigurationResult.SUCCESS))\n    }\n\n    @Test\n    fun afterSettingsImportedAndMigrated_runsSettingsChangeHandlerWithUpdatedKeys() {\n        importer = SettingsImporter(\n            settingsProvider,\n            { _: Settings?, _: Settings? -> },\n            settingsValidator,\n            generalDefaults,\n            adminDefaults,\n            settingsChangeHandler,\n            projectsRepository,\n            projectDetailsCreator\n        )\n\n        var generalJson = JSONObject()\n            .put(\"key1\", \"default\")\n            .put(\"key2\", true)\n        var adminJson = JSONObject()\n            .put(\"key2\", 5)\n        var settings = JSONObject()\n            .put(AppConfigurationKeys.GENERAL, generalJson)\n            .put(AppConfigurationKeys.ADMIN, adminJson)\n\n        assertThat(importer.fromJSON(settings.toString(), currentProject, JSONObject()), `is`(ProjectConfigurationResult.SUCCESS))\n        verify(settingsChangeHandler).onSettingsChanged(\"1\", emptyList(), emptyList())\n\n        generalJson = JSONObject()\n            .put(\"key1\", \"foo\")\n            .put(\"key2\", true)\n        adminJson = JSONObject()\n            .put(\"key2\", 10)\n        settings = JSONObject()\n            .put(AppConfigurationKeys.GENERAL, generalJson)\n            .put(AppConfigurationKeys.ADMIN, adminJson)\n\n        assertThat(importer.fromJSON(settings.toString(), currentProject, JSONObject()), `is`(ProjectConfigurationResult.SUCCESS))\n        verify(settingsChangeHandler).onSettingsChanged(\"1\", listOf(\"key1\"), listOf(\"key2\"))\n        verifyNoMoreInteractions(settingsChangeHandler)\n    }\n\n    @Test\n    fun projectDetailsShouldBeImportedIfIncludedInJson() {\n        val newProject = Project.New(\"Project Y\", \"Y\", \"#000000\")\n\n        val projectJson = JSONObject()\n            .put(AppConfigurationKeys.PROJECT_NAME, newProject.name)\n            .put(AppConfigurationKeys.PROJECT_ICON, newProject.icon)\n            .put(AppConfigurationKeys.PROJECT_COLOR, newProject.color)\n        val settings = JSONObject()\n            .put(AppConfigurationKeys.GENERAL, JSONObject())\n            .put(AppConfigurationKeys.ADMIN, JSONObject())\n            .put(AppConfigurationKeys.PROJECT, projectJson)\n\n        whenever(\n            projectDetailsCreator.createProjectFromDetails(\n                newProject.name,\n                newProject.icon,\n                newProject.color,\n                \"\"\n            )\n        ).thenReturn(newProject)\n\n        importer.fromJSON(settings.toString(), currentProject, JSONObject())\n        verify(projectsRepository)\n            .save(\n                Project.Saved(\n                    currentProject.uuid,\n                    newProject.name,\n                    newProject.icon,\n                    newProject.color\n                )\n            )\n    }\n\n    @Test\n    fun `when protocol is server and project name not set, project name falls back to server url`() {\n        val generalJson = JSONObject()\n            .put(ProjectKeys.KEY_SERVER_URL, \"foo\")\n        val settings = JSONObject()\n            .put(AppConfigurationKeys.GENERAL, generalJson)\n            .put(AppConfigurationKeys.ADMIN, JSONObject())\n\n        whenever(\n            projectDetailsCreator.createProjectFromDetails(\n                any(),\n                any(),\n                any(),\n                any()\n            )\n        ).thenReturn(Project.New(\"A\", \"B\", \"C\"))\n\n        importer.fromJSON(settings.toString(), currentProject, JSONObject())\n        verify(projectDetailsCreator).createProjectFromDetails(\"\", \"\", \"\", \"foo\")\n    }\n\n    @Test\n    fun `when protocol is Google Drive returns GD_PROJECT`() {\n        val generalJson = JSONObject().put(ProjectKeys.KEY_PROTOCOL, ProjectKeys.PROTOCOL_GOOGLE_SHEETS)\n\n        val settings = JSONObject()\n            .put(AppConfigurationKeys.GENERAL, generalJson)\n            .put(AppConfigurationKeys.ADMIN, JSONObject())\n\n        assertThat(importer.fromJSON(settings.toString(), currentProject, JSONObject()), `is`(ProjectConfigurationResult.GD_PROJECT))\n    }\n\n    private fun emptySettings(): String {\n        return emptySettingsObject().toString()\n    }\n\n    private fun emptySettingsObject(): JSONObject {\n        return JSONObject()\n            .put(AppConfigurationKeys.GENERAL, JSONObject())\n            .put(AppConfigurationKeys.ADMIN, JSONObject())\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/migration/KeyCombinerTest.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.odk.collect.settings.migration.MigrationUtils.combineKeys;\nimport static org.odk.collect.settings.support.SettingsUtils.assertSettings;\nimport static org.odk.collect.settings.support.SettingsUtils.initSettings;\n\nimport org.junit.Test;\nimport org.odk.collect.shared.settings.InMemSettings;\nimport org.odk.collect.shared.settings.Settings;\n\npublic class KeyCombinerTest {\n\n    private final Settings prefs = new InMemSettings();\n\n    @Test\n    public void combinesValuesOfTwoKeys_andRemovesOldKeys() {\n        initSettings(prefs,\n                \"red\", true,\n                \"blue\", true\n        );\n\n        combineKeys(\"red\", \"blue\")\n                .withValues(true, true)\n                .toPairs(\"color\", \"purple\")\n                .apply(prefs);\n\n        assertSettings(prefs,\n                \"color\", \"purple\"\n        );\n    }\n\n    @Test\n    public void canCombineMultipleValues() {\n        KeyCombiner combiner = combineKeys(\"red\", \"blue\")\n                .withValues(true, true).toPairs(\"color\", \"purple\")\n                .withValues(false, true).toPairs(\"color\", \"blue\")\n                .withValues(true, false).toPairs(\"color\", \"red\");\n\n        initSettings(prefs,\n                \"red\", true,\n                \"blue\", false\n        );\n\n        combiner.apply(prefs);\n\n        assertSettings(prefs,\n                \"color\", \"red\"\n        );\n\n        initSettings(prefs,\n                \"red\", false,\n                \"blue\", true\n        );\n\n        combiner.apply(prefs);\n\n        assertSettings(prefs,\n                \"color\", \"blue\"\n        );\n    }\n\n    @Test\n    public void whenCombinedKeyExists_removesOtherKey_andModifiesExistingKey() {\n        initSettings(prefs,\n                \"direction\", \"north\",\n                \"other-direction\", \"west\"\n        );\n\n        combineKeys(\"direction\", \"other-direction\")\n                .withValues(\"north\", \"west\")\n                .toPairs(\"direction\", \"north-west\")\n                .apply(prefs);\n\n        assertSettings(prefs,\n                \"direction\", \"north-west\"\n        );\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/migration/KeyExtractorTest.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.odk.collect.settings.migration.MigrationUtils.extractNewKey;\nimport static org.odk.collect.settings.support.SettingsUtils.assertSettings;\nimport static org.odk.collect.settings.support.SettingsUtils.initSettings;\n\nimport org.junit.Test;\nimport org.odk.collect.shared.settings.InMemSettings;\nimport org.odk.collect.shared.settings.Settings;\n\npublic class KeyExtractorTest {\n\n    private final Settings prefs = new InMemSettings();\n\n    @Test\n    public void createsNewKeyBasedOnExistingKeysValue() {\n        initSettings(prefs,\n                \"oldKey\", \"blah\"\n        );\n\n        extractNewKey(\"newKey\").fromKey(\"oldKey\")\n                .fromValue(\"blah\").toValue(\"newBlah\")\n                .apply(prefs);\n\n        assertSettings(prefs,\n                \"oldKey\", \"blah\",\n                \"newKey\", \"newBlah\"\n        );\n    }\n\n    @Test\n    public void whenNewKeyExists_doesNothing() {\n        initSettings(prefs,\n                \"oldKey\", \"oldBlah\",\n                \"newKey\", \"existing\"\n        );\n\n        extractNewKey(\"newKey\").fromKey(\"oldKey\")\n                .fromValue(\"oldBlah\").toValue(\"newBlah\")\n                .apply(prefs);\n\n        assertSettings(prefs,\n                \"oldKey\", \"oldBlah\",\n                \"newKey\", \"existing\"\n        );\n    }\n\n    @Test\n    public void whenOldKeyMissing_doesNothing() {\n        initSettings(prefs);\n\n        extractNewKey(\"newKey\").fromKey(\"oldKey\")\n                .fromValue(\"oldBlah\").toValue(\"newBlah\")\n                .apply(prefs);\n\n        assertSettings(prefs);\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/migration/KeyMoverTest.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.odk.collect.settings.migration.MigrationUtils.moveKey;\nimport static org.odk.collect.settings.support.SettingsUtils.assertSettings;\nimport static org.odk.collect.settings.support.SettingsUtils.initSettings;\n\nimport org.junit.Test;\nimport org.odk.collect.shared.settings.InMemSettings;\nimport org.odk.collect.shared.settings.Settings;\n\npublic class KeyMoverTest {\n\n    private final Settings prefs = new InMemSettings();\n    private final Settings other = new InMemSettings();\n\n    @Test\n    public void movesKeyAndValueToOtherPrefs() {\n        initSettings(prefs,\n                \"key\", \"value\"\n        );\n\n        moveKey(\"key\")\n                .toPreferences(other)\n                .apply(prefs);\n\n        assertThat(prefs.getAll().size(), is(0));\n        assertSettings(other,\n                \"key\", \"value\"\n        );\n    }\n\n    @Test\n    public void whenKeyNotInOriginalPrefs_doesNothing() {\n        moveKey(\"key\")\n                .toPreferences(other)\n                .apply(prefs);\n\n        assertThat(prefs.getAll().size(), is(0));\n        assertThat(other.getAll().size(), is(0));\n    }\n\n    @Test\n    public void whenKeyInOtherPrefs_doesNothing() {\n        initSettings(prefs,\n                \"key\", \"value\"\n        );\n\n        initSettings(other,\n                \"key\", \"other-value\"\n        );\n\n        moveKey(\"key\")\n                .toPreferences(other)\n                .apply(prefs);\n\n        assertSettings(prefs,\n                \"key\", \"value\"\n        );\n\n        assertSettings(other,\n                \"key\", \"other-value\"\n        );\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/migration/KeyRemoverTest.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.odk.collect.settings.migration.MigrationUtils.removeKey;\nimport static org.odk.collect.settings.support.SettingsUtils.assertSettingsEmpty;\nimport static org.odk.collect.settings.support.SettingsUtils.initSettings;\n\nimport org.junit.Test;\nimport org.odk.collect.shared.settings.InMemSettings;\nimport org.odk.collect.shared.settings.Settings;\n\npublic class KeyRemoverTest {\n\n    private final Settings prefs = new InMemSettings();\n\n    @Test\n    public void whenKeyDoesNotExist_doesNothing() {\n        initSettings(prefs);\n\n        removeKey(\"blah\").apply(prefs);\n\n        assertSettingsEmpty(prefs);\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/migration/KeyRenamerTest.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.odk.collect.settings.migration.MigrationUtils.renameKey;\nimport static org.odk.collect.settings.support.SettingsUtils.assertSettings;\nimport static org.odk.collect.settings.support.SettingsUtils.initSettings;\n\nimport org.junit.Test;\nimport org.odk.collect.shared.settings.InMemSettings;\nimport org.odk.collect.shared.settings.Settings;\n\npublic class KeyRenamerTest {\n\n    private final Settings prefs = new InMemSettings();\n\n    @Test\n    public void renamesKeys() {\n        initSettings(prefs,\n                \"colour\", \"red\"\n        );\n\n        renameKey(\"colour\")\n                .toKey(\"couleur\")\n                .apply(prefs);\n\n        assertSettings(prefs,\n                \"couleur\", \"red\"\n        );\n    }\n\n    @Test\n    public void whenNewKeyExists_doesNotDoAnything() {\n        initSettings(prefs,\n                \"colour\", \"red\",\n                \"couleur\", \"blue\"\n        );\n\n        renameKey(\"colour\")\n                .toKey(\"couleur\")\n                .apply(prefs);\n\n        assertSettings(prefs,\n                \"colour\", \"red\",\n                \"couleur\", \"blue\"\n        );\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/migration/KeyTranslatorTest.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.odk.collect.settings.migration.MigrationUtils.translateKey;\nimport static org.odk.collect.settings.support.SettingsUtils.assertSettings;\nimport static org.odk.collect.settings.support.SettingsUtils.initSettings;\n\nimport org.junit.Test;\nimport org.odk.collect.shared.settings.InMemSettings;\nimport org.odk.collect.shared.settings.Settings;\n\npublic class KeyTranslatorTest {\n\n    private final Settings prefs = new InMemSettings();\n\n    @Test\n    public void renamesKeyAndTranslatesValues() {\n        initSettings(prefs,\n                \"colour\", \"red\"\n        );\n\n        translateKey(\"colour\")\n                .toKey(\"couleur\")\n                .fromValue(\"red\")\n                .toValue(\"rouge\")\n                .apply(prefs);\n\n        assertSettings(prefs,\n                \"couleur\", \"rouge\"\n        );\n    }\n\n    @Test\n    public void canTranslateMultipleValues() {\n        KeyTranslator translator = translateKey(\"colour\")\n                .toKey(\"couleur\")\n                .fromValue(\"red\")\n                .toValue(\"rouge\")\n                .fromValue(\"green\")\n                .toValue(\"vert\");\n\n        initSettings(prefs,\n                \"colour\", \"red\"\n        );\n\n        translator.apply(prefs);\n\n        assertSettings(prefs,\n                \"couleur\", \"rouge\"\n        );\n\n        initSettings(prefs,\n                \"colour\", \"green\"\n        );\n\n        translator.apply(prefs);\n\n        assertSettings(prefs,\n                \"couleur\", \"vert\"\n        );\n    }\n\n    @Test\n    public void whenKeyHasUnknownValue_doesNotDoAnything() {\n        initSettings(prefs,\n                \"colour\", \"blue\"\n        );\n\n        translateKey(\"color\")\n                .toKey(\"coleur\")\n                .fromValue(\"red\")\n                .toValue(\"rouge\")\n                .apply(prefs);\n\n        assertSettings(prefs,\n                \"colour\", \"blue\"\n        );\n    }\n\n    @Test\n    public void whenNewKeyExists_doesNotDoAnything() {\n        initSettings(prefs,\n                \"colour\", \"red\",\n                \"couleur\", \"bleu\"\n        );\n\n        translateKey(\"color\")\n                .toKey(\"coleur\")\n                .fromValue(\"red\")\n                .toValue(\"rouge\")\n                .apply(prefs);\n\n        assertSettings(prefs,\n                \"colour\", \"red\",\n                \"couleur\", \"bleu\"\n        );\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/migration/ValueTranslatorTest.java",
    "content": "package org.odk.collect.settings.migration;\n\nimport static org.odk.collect.settings.migration.MigrationUtils.translateValue;\nimport static org.odk.collect.settings.support.SettingsUtils.assertSettings;\nimport static org.odk.collect.settings.support.SettingsUtils.initSettings;\n\nimport org.junit.Test;\nimport org.odk.collect.shared.settings.InMemSettings;\nimport org.odk.collect.shared.settings.Settings;\n\npublic class ValueTranslatorTest {\n\n    private final Settings prefs = new InMemSettings();\n\n    @Test\n    public void translatesValueForKey() {\n        initSettings(prefs,\n                \"key\", \"value\"\n        );\n\n        translateValue(\"value\").toValue(\"newValue\").forKey(\"key\").apply(prefs);\n\n        assertSettings(prefs,\n                \"key\", \"newValue\"\n        );\n    }\n\n    @Test\n    public void doesNotTranslateOtherValues() {\n        initSettings(prefs,\n                \"key\", \"otherValue\"\n        );\n\n        translateValue(\"value\").toValue(\"newValue\").forKey(\"key\").apply(prefs);\n\n        assertSettings(prefs,\n                \"key\", \"otherValue\"\n        );\n    }\n\n    @Test\n    public void whenKeyNotInPrefs_doesNothing() {\n        initSettings(prefs,\n                \"otherKey\", \"value\"\n        );\n\n        translateValue(\"value\").toValue(\"newValue\").forKey(\"key\").apply(prefs);\n\n        assertSettings(prefs,\n                \"otherKey\", \"value\"\n        );\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/support/SettingsUtils.kt",
    "content": "package org.odk.collect.settings.support\n\nimport org.junit.Assert.assertEquals\nimport org.odk.collect.shared.settings.Settings\n\nobject SettingsUtils {\n\n    @JvmStatic\n    fun initSettings(settings: Settings, vararg pairs: Any?) {\n        settings.clear()\n\n        for (i in pairs.indices.step(2)) {\n            settings.save((pairs[i] as String?)!!, pairs[i + 1])\n        }\n    }\n\n    @JvmStatic\n    fun assertSettings(settings: Settings, vararg pairs: Any?) {\n        for (i in pairs.indices.step(2)) {\n            assertEquals(pairs[i + 1], settings.getAll()[pairs[i]])\n        }\n\n        assertEquals((pairs.size / 2).toLong(), settings.getAll().size.toLong())\n    }\n\n    @JvmStatic\n    fun assertSettingsEmpty(settings: Settings) {\n        assertEquals(0, settings.getAll().size.toLong())\n    }\n}\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/validation/JsonSchemaSettingsValidatorTest.kt",
    "content": "package org.odk.collect.settings.validation\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\n\nclass JsonSchemaSettingsValidatorTest {\n    @Test\n    fun `isValid returns true when json is valid based on schema`() {\n        val validator = JsonSchemaSettingsValidator {\n            SCHEMA.byteInputStream()\n        }\n\n        assertThat(\n            validator.isValid(\n                \"\"\"\n                    {\n                        \"general\":{\n                            \"foo\":\"option1\"\n                        }\n                    }\n                \"\"\"\n            ),\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `isValid returns false when json is invalid based on schema`() {\n        val validator = JsonSchemaSettingsValidator {\n            SCHEMA.byteInputStream()\n        }\n\n        assertThat(\n            validator.isValid(\n                \"\"\"\n                    {\n                        \"general\":{\n                            \"foo\":false\n                        }\n                    }\n                \"\"\"\n            ),\n            equalTo(false)\n        )\n    }\n\n    @Test\n    fun `isValid returns false when json is invalid`() {\n        val validator = JsonSchemaSettingsValidator {\n            SCHEMA.byteInputStream()\n        }\n\n        assertThat(\n            validator.isValid(\"*\"),\n            equalTo(false)\n        )\n    }\n\n    @Test\n    fun `isValid returns true when json contains values different than those specified in a corresponding enum`() {\n        val validator = JsonSchemaSettingsValidator {\n            SCHEMA.byteInputStream()\n        }\n\n        assertThat(\n            validator.isValid(\n                \"\"\"\n                    {\n                        \"general\":{\n                            \"foo\":\"option3\"\n                        }\n                    }\n                \"\"\"\n            ),\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `isKeySupported returns true for keys allowed by the scheme`() {\n        val validator = JsonSchemaSettingsValidator {\n            SCHEMA.byteInputStream()\n        }\n\n        assertThat(\n            validator.isKeySupported(\"general\", \"foo\"),\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `isKeySupported returns false for keys not allowed by the scheme`() {\n        val validator = JsonSchemaSettingsValidator {\n            SCHEMA.byteInputStream()\n        }\n\n        assertThat(\n            validator.isKeySupported(\"general\", \"baz\"),\n            equalTo(false)\n        )\n    }\n\n    @Test\n    fun `isValueSupported returns true for values allowed by the scheme`() {\n        val validator = JsonSchemaSettingsValidator {\n            SCHEMA.byteInputStream()\n        }\n\n        assertThat(\n            validator.isValueSupported(\"general\", \"foo\", \"option1\"),\n            equalTo(true)\n        )\n    }\n\n    @Test\n    fun `isValueSupported returns false for values not allowed by the scheme`() {\n        val validator = JsonSchemaSettingsValidator {\n            SCHEMA.byteInputStream()\n        }\n\n        assertThat(\n            validator.isValueSupported(\"general\", \"foo\", \"option3\"),\n            equalTo(false)\n        )\n    }\n\n    @Test\n    fun `isValueSupported returns true for any value when the scheme does not specify allowed values`() {\n        val validator = JsonSchemaSettingsValidator {\n            SCHEMA.byteInputStream()\n        }\n\n        assertThat(\n            validator.isValueSupported(\"general\", \"bar\", \"option3\"),\n            equalTo(true)\n        )\n    }\n}\n\nprivate const val SCHEMA = \"\"\"\n            {\n                \"${'$'}schema\": \"https://json-schema.org/draft/2019-09/schema\",\n                \"${'$'}id\": \"https://example.com/example.schema.json\",\n                \"title\": \"Schema\",\n                \"type\": \"object\",\n                \"properties\": {\n                    \"general\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"foo\": {\n                                \"type\": \"string\",\n                                \"enum\": [\n                                    \"option1\",\n                                    \"option2\"\n                                ]\n                            },\n                            \"bar\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    }\n                }\n            }\n            \"\"\"\n"
  },
  {
    "path": "settings/src/test/java/org/odk/collect/settings/validation/OriginalJsonSchemaSettingsValidatorTest.kt",
    "content": "package org.odk.collect.settings.validation\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\n\nclass OriginalJsonSchemaSettingsValidatorTest {\n\n    /*\n     * Some settings end up replaced by new settings in but we need the schema to still\n     * recognize the old fields so that we can migrate them correctly.\n     */\n    @Test\n    fun `isValueSupported returns true for fields we no longer use`() {\n        val validator = JsonSchemaSettingsValidator {\n            javaClass.getResourceAsStream(\"/client-settings.schema.json\")!!\n        }\n\n        removedKeys.forEach {\n            assertThat(\n                validator.isValueSupported(it.first, it.second, \"true\"),\n                equalTo(true)\n            )\n        }\n    }\n\n    private val removedKeys = listOf(\n        Pair(\"admin\", \"mark_as_finalized\"),\n        Pair(\"general\", \"default_completed\"),\n        Pair(\"admin\", \"finalize\")\n    )\n}\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':projects'\ninclude ':forms-test'\ninclude ':shared'\ninclude ':forms'\ninclude ':analytics'\ninclude ':androidshared'\ninclude ':audio-recorder'\ninclude ':test-shared'\ninclude ':audio-clips'\ninclude ':strings'\ninclude ':async'\ninclude ':collect_app'\ninclude ':nbistubs'\ninclude ':material'\ninclude ':fragments-test'\ninclude ':location'\ninclude ':geo'\ninclude ':errors'\ninclude ':external-app'\ninclude ':upgrade'\ninclude ':permissions'\ninclude ':image-loader'\ninclude ':androidtest'\ninclude ':settings'\ninclude ':service-test'\ninclude ':maps'\ninclude ':osmdroid'\ninclude ':icons'\ninclude ':crash-handler'\ninclude ':entities'\ninclude ':selfie-camera'\ninclude ':qr-code'\ninclude ':shadows'\ninclude ':metadata'\ninclude ':google-maps'\ninclude ':test-forms'\ninclude ':printer'\ninclude ':draw'\ninclude ':lists'\ninclude ':web-page'\ninclude ':db'\ninclude ':open-rosa'\ninclude ':mobile-device-management'\ninclude ':timedgrid'\n\napply from: 'secrets.gradle'\nif (getSecrets().getProperty('MAPBOX_DOWNLOADS_TOKEN', '') != '') {\n    include ':mapbox'\n}\n"
  },
  {
    "path": "shadows/.gitignore",
    "content": "/build"
  },
  {
    "path": "shadows/README.md",
    "content": "# shadows\n\nIncludes [custom Robolectric Shadows](https://robolectric.org/extending/) that it doesn't make sense to contribute back to Robolectric itself. An example would be for Shadows of classes in AndroidX libraries, some of which still have similarly untestable interfaces to their Android SDK counterparts.\n"
  },
  {
    "path": "shadows/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.shadows\"\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n}\n\ndependencies {\n    implementation(libs.robolectric)\n    implementation(libs.androidxAppcompat)\n\n    testImplementation(libs.junit)\n    testImplementation(libs.hamcrest)\n}\n"
  },
  {
    "path": "shadows/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "shadows/src/main/java/org/odk/collect/shadows/ShadowAndroidXAlertDialog.kt",
    "content": "package org.odk.collect.shadows\n\nimport android.view.View\nimport androidx.appcompat.app.AlertDialog\nimport org.robolectric.annotation.Implements\nimport org.robolectric.annotation.RealObject\nimport org.robolectric.shadows.ShadowAlertDialog\nimport org.robolectric.shadows.ShadowDialog\nimport org.robolectric.util.ReflectionHelpers\n\n/**\n * Provides similar functionality to [ShadowAlertDialog] but for\n * [androidx.appcompat.app.AlertDialog] instead of [android.app.AlertDialog].\n */\n@Implements(AlertDialog::class)\nclass ShadowAndroidXAlertDialog : ShadowDialog() {\n\n    @RealObject\n    private lateinit var realObject: AlertDialog\n\n    override fun getTitle(): CharSequence {\n        val alertController = ReflectionHelpers.getField<Any>(realObject, \"mAlert\")\n        return ReflectionHelpers.getField(alertController, \"mTitle\")\n    }\n\n    fun getView(): View {\n        val alertController = ReflectionHelpers.getField<Any>(realObject, \"mAlert\")\n        return ReflectionHelpers.getField(alertController, \"mView\")\n    }\n}\n"
  },
  {
    "path": "shadows/src/test/java/org/odk/collect/shadows/ShadowAndroidXAlertDialogTest.kt",
    "content": "package org.odk.collect.shadows\n\nimport android.view.View\nimport androidx.appcompat.app.AlertDialog\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.robolectric.RobolectricTestRunner\nimport org.robolectric.RuntimeEnvironment\nimport org.robolectric.annotation.Config\nimport org.robolectric.shadow.api.Shadow.extract\n\n@RunWith(RobolectricTestRunner::class)\n@Config(shadows = [ShadowAndroidXAlertDialog::class])\nclass ShadowAndroidXAlertDialogTest {\n\n    @Test\n    fun `getView returns view set on builder`() {\n        val context = RuntimeEnvironment.getApplication().also {\n            it.setTheme(androidx.appcompat.R.style.Theme_AppCompat)\n        }\n\n        val view = View(context)\n        val dialog = AlertDialog.Builder(context)\n            .setView(view)\n            .show()\n\n        val shadowDialog = extract<ShadowAndroidXAlertDialog>(dialog)\n        assertThat(shadowDialog.getView(), equalTo(view))\n    }\n}\n"
  },
  {
    "path": "shared/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "shared/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.javaLibrary)\n    alias(libs.plugins.kotlinLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\njava {\n    sourceCompatibility = JavaVersion.VERSION_17\n    targetCompatibility = JavaVersion.VERSION_17\n}\n\ndependencies {\n    implementation(libs.kotlinStdlib)\n    compileOnly(libs.json)\n\n    testImplementation(libs.junit)\n    testImplementation(libs.json)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.mockitoKotlin)\n}\n\ntasks.register(\"testDebug\") {\n    dependsOn(\"test\")\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/DebugLogger.kt",
    "content": "package org.odk.collect.shared\n\ninterface DebugLogger {\n    fun log(tag: String, message: String)\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/PathUtils.kt",
    "content": "package org.odk.collect.shared\n\nobject PathUtils {\n\n    @JvmStatic\n    fun getRelativeFilePath(dirPath: String, filePath: String): String {\n        return if (filePath.startsWith(dirPath)) filePath.substring(dirPath.length + 1) else filePath\n    }\n\n    // https://stackoverflow.com/questions/2679699/what-characters-allowed-in-file-names-on-android\n    @JvmStatic\n    fun getPathSafeFileName(fileName: String) = fileName.replace(\"[\\\"*/:<>?\\\\\\\\|]\".toRegex(), \"_\")\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/Query.kt",
    "content": "package org.odk.collect.shared\n\nimport org.odk.collect.shared.Query.And\nimport org.odk.collect.shared.Query.NumericEq\nimport org.odk.collect.shared.Query.NumericNotEq\nimport org.odk.collect.shared.Query.Or\nimport org.odk.collect.shared.Query.StringEq\nimport org.odk.collect.shared.Query.StringNotEq\n\nsealed class Query {\n    data class StringEq(val column: String, val value: String) : Query()\n    data class StringNotEq(val column: String, val value: String) : Query()\n    data class NumericEq(val column: String, val value: Double) : Query()\n    data class NumericNotEq(val column: String, val value: Double) : Query()\n    data class And(val queryA: Query, val queryB: Query) : Query()\n    data class Or(val queryA: Query, val queryB: Query) : Query()\n}\n\nfun Query.mapColumns(columnMapper: (String) -> String): Query {\n    return when (this) {\n        is StringEq -> StringEq(columnMapper(column), value)\n\n        is StringNotEq -> StringNotEq(columnMapper(column), value)\n\n        is NumericEq -> NumericEq(columnMapper(column), value)\n\n        is NumericNotEq -> NumericNotEq(columnMapper(column), value)\n\n        is And -> And(\n            queryA.mapColumns(columnMapper),\n            queryB.mapColumns(columnMapper)\n        )\n\n        is Or -> Or(\n            queryA.mapColumns(columnMapper),\n            queryB.mapColumns(columnMapper)\n        )\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/TempFiles.kt",
    "content": "package org.odk.collect.shared\n\nimport org.odk.collect.shared.strings.RandomString\nimport java.io.File\n\nobject TempFiles {\n    @JvmStatic\n    @JvmOverloads\n    fun createTempFileWithName(name: String, extension: String = \"\"): File {\n        val tmpDir = getTempDir()\n        return File(tmpDir, name + extension).also {\n            it.createNewFile()\n            it.deleteOnExit()\n        }\n    }\n\n    @JvmStatic\n    fun createTempFile(): File {\n        val tmpDir = getTempDir()\n        return File(tmpDir, getRandomName(tmpDir)).also {\n            it.createNewFile()\n            it.deleteOnExit()\n        }\n    }\n\n    @JvmStatic\n    fun createTempFile(extension: String): File {\n        val tmpDir = getTempDir()\n        return File(tmpDir, getRandomName(tmpDir) + extension).also {\n            it.createNewFile()\n            it.deleteOnExit()\n        }\n    }\n\n    @JvmStatic\n    fun createTempFile(name: String, extension: String): File {\n        val tmpDir = getTempDir()\n        return File(tmpDir, name + getRandomName(tmpDir) + extension).also {\n            it.createNewFile()\n            it.deleteOnExit()\n        }\n    }\n\n    @JvmStatic\n    fun createTempFile(parent: File, name: String): File {\n        return File(parent, name + getRandomName(parent)).also {\n            it.createNewFile()\n            it.deleteOnExit()\n        }\n    }\n\n    @JvmStatic\n    fun createTempFile(parent: File, name: String, extension: String): File {\n        return File(parent, name + extension).also {\n            it.createNewFile()\n            it.deleteOnExit()\n        }\n    }\n\n    @JvmStatic\n    fun createTempFile(parent: File): File {\n        return File(parent, getRandomName(parent)).also {\n            it.createNewFile()\n            it.deleteOnExit()\n        }\n    }\n\n    @JvmStatic\n    fun getPathInTempDir(name: String, extension: String): String {\n        val tmpDir = getTempDir()\n        val file = File(tmpDir, name + extension)\n        file.deleteOnExit()\n        return file.absolutePath\n    }\n\n    @JvmStatic\n    fun getPathInTempDir(): String {\n        val tmpDir = getTempDir()\n        return File(tmpDir, getRandomName(tmpDir)).absolutePath\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun createTempDir(parent: File? = null): File {\n        val dir = if (parent != null) {\n            File(parent, getRandomName(parent))\n        } else {\n            File(getPathInTempDir())\n        }\n\n        dir.mkdir()\n        return dir\n    }\n\n    private fun getTempDir(): File {\n        val tmpDir = File(System.getProperty(\"java.io.tmpdir\", \".\"), \"org.odk.collect.shared.TempFiles\")\n        if (!tmpDir.exists()) {\n            tmpDir.mkdir()\n        }\n\n        return tmpDir\n    }\n\n    private fun getRandomName(parent: File): String {\n        val existing = parent.listFiles()\n\n        var candiate = RandomString.randomString(16)\n        while (existing!!.any { it.name.contains(candiate) }) {\n            candiate = RandomString.randomString(16)\n        }\n\n        return candiate\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/TimeInMs.kt",
    "content": "package org.odk.collect.shared\n\nobject TimeInMs {\n    const val ONE_SECOND = 1000L\n    const val ONE_MINUTE = 60000L\n    const val ONE_HOUR = 3600000L\n    const val ONE_DAY = 86400000L\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/collections/CollectionExtensions.kt",
    "content": "package org.odk.collect.shared.collections\n\nimport org.json.JSONArray\nimport kotlin.math.abs\n\nobject CollectionExtensions {\n\n    fun <T> List<T>.itemFromHashOf(any: Any): T {\n        val index = abs(any.hashCode()) % this.size\n        return this[index]\n    }\n\n    fun JSONArray.has(value: Any): Boolean {\n        for (i in 0 until this.length()) {\n            if (this[i] == value) {\n                return true\n            }\n        }\n        return false\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/files/FileExt.kt",
    "content": "package org.odk.collect.shared.files\n\nimport java.io.File\nimport java.io.IOException\nimport java.io.InputStream\nimport kotlin.jvm.Throws\n\nobject FileExt {\n    /**\n     * Original File.getCanonicalPath() may return paths with inconsistent letter casing for the\n     * /Android/data/ part of the path, such as /storage/emulated/0/android/data/... or /storage/emulated/0/Android/Data/...\n     * instead of the expected /storage/emulated/0/Android/data/...\n     * Since the Android file system is case-sensitive, this behavior appears to be a bug.\n     *\n     * For more details, see the discussion on Stack Overflow:\n     * https://stackoverflow.com/questions/78965720/file-getcanonicalpath-returns-inconsistent-letter-casing-in-path\n     */\n    fun File.sanitizedCanonicalPath(): String {\n        val androidDataSegment = \"/Android/data/\"\n        val regex = Regex(androidDataSegment, RegexOption.IGNORE_CASE)\n\n        return canonicalPath.replace(regex, androidDataSegment)\n    }\n\n    fun File.listFilesRecursively(): List<File> {\n        val listFiles = listFiles() ?: emptyArray()\n        return listFiles.flatMap {\n            if (it.isDirectory) {\n                it.listFilesRecursively()\n            } else {\n                listOf(it)\n            }\n        }\n    }\n\n    @JvmStatic\n    fun File.deleteDirectory() {\n        deleteRecursively()\n    }\n\n    @Throws(IOException::class)\n    @JvmStatic\n    fun File.saveToFile(inputStream: InputStream) {\n        if (exists() && !delete()) {\n            throw IOException(\"Cannot overwrite $absolutePath. Perhaps the file is locked?\")\n        }\n\n        inputStream.use { input ->\n            outputStream().use { output ->\n                input.copyTo(output)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/geometry/Geometry.kt",
    "content": "package org.odk.collect.shared.geometry\n\nimport kotlin.math.abs\nimport kotlin.math.max\nimport kotlin.math.min\n\ndata class Point(val x: Double, val y: Double)\ndata class LineSegment(val start: Point, val end: Point)\ndata class Trace(val points: List<Point>) {\n    fun isClosed(): Boolean {\n        return points.first() == points.last()\n    }\n}\n\nfun Trace.segments(): List<LineSegment> {\n    return points.zipWithNext().flatMap { (start, end) ->\n        if (start != end) {\n            listOf(LineSegment(start, end))\n        } else {\n            emptyList()\n        }\n    }\n}\n\n/**\n * Returns `true` if any segment of the trace intersects with any other and `false` otherwise.\n */\nfun Trace.intersects(epsilon: Double = 0.0): Boolean {\n    val points = this.points\n    return if (points.size >= 3) {\n        val segments = segments()\n        if (segments.size == 2) {\n            segments[0].intersects(segments[1], allowConnection = true, epsilon = epsilon)\n        } else {\n            segments.filterIndexed { line1Index, line1 ->\n                segments.filterIndexed { line2Index, line2 ->\n                    if (isClosed() && line1Index == 0 && line2Index == segments.size - 1) {\n                        false\n                    } else if (line2Index == line1Index + 1) {\n                        line1.intersects(line2, allowConnection = true, epsilon = epsilon)\n                    } else if (line2Index >= line1Index + 2) {\n                        line1.intersects(line2, epsilon = epsilon)\n                    } else {\n                        false\n                    }\n                }.isNotEmpty()\n            }.isNotEmpty()\n        }\n    } else {\n        false\n    }\n}\n\n/**\n * Check if a point is within the bounding box defined by a line between two non-consecutive corners\n */\nfun Point.within(segment: LineSegment): Boolean {\n    val lineXMin = min(segment.start.x, segment.end.x)\n    val lineXMax = max(segment.start.x, segment.end.x)\n    val lineYMin = min(segment.start.y, segment.end.y)\n    val lineYMax = max(segment.start.y, segment.end.y)\n    val xRange = lineXMin..lineXMax\n    val yRange = lineYMin..lineYMax\n\n    return x in xRange && y in yRange\n}\n\n/**\n * Work out whether two line segments intersect by calculating if the endpoints of one segment\n * are on opposite sides (or touching of the other segment **and** vice versa. This is\n * determined by finding the orientation of endpoints relative to the other line.\n *\n * @param allowConnection will allow the end of `this` and the start of `other` to intersect\n * provided they are equivalent (the two segments are \"connected\")\n */\nfun LineSegment.intersects(other: LineSegment, allowConnection: Boolean = false, epsilon: Double = 0.0): Boolean {\n    val (a, b) = this\n    val (c, d) = other\n\n    val orientationA = orientation(a, c, d, epsilon)\n    val orientationD = orientation(a, b, d, epsilon)\n\n    return if (orientationA == Orientation.Collinear && a.within(other)) {\n        true\n    } else if (orientationD == Orientation.Collinear && d.within(this)) {\n        true\n    } else if (b == c && allowConnection) {\n        false\n    } else {\n        val orientationB = orientation(b, c, d, epsilon)\n        val orientationC = orientation(a, b, c, epsilon)\n\n        if (orientationA.isOpposing(orientationB) && orientationC.isOpposing(orientationD)) {\n            true\n        } else if (orientationB == Orientation.Collinear && b.within(other)) {\n            true\n        } else if (orientationC == Orientation.Collinear && c.within(this)) {\n            true\n        } else {\n            false\n        }\n    }\n}\n\n/**\n * Calculate the \"orientation\" (or \"direction\") of three points using the cross product of the\n * vectors of the pairs of points (see\n * [here](https://en.wikipedia.org/wiki/Cross_product#Computational_geometry)). This can\n * either be clockwise, anticlockwise or collinear (the three points form a straight line).\n *\n * @param epsilon the epsilon used to check for collinearity\n *\n */\nfun orientation(a: Point, b: Point, c: Point, epsilon: Double = 0.0): Orientation {\n    val crossProduct = crossProduct(Pair(b.x - a.x, b.y - a.y), Pair(c.x - a.x, c.y - a.y))\n    return if (abs(crossProduct) <= epsilon) {\n        Orientation.Collinear\n    } else if (crossProduct > 0) {\n        Orientation.AntiClockwise\n    } else {\n        Orientation.Clockwise\n    }\n}\n\n/**\n * [https://en.wikipedia.org/wiki/Cross_product](https://en.wikipedia.org/wiki/Cross_product)\n */\nprivate fun crossProduct(x: Pair<Double, Double>, y: Pair<Double, Double>): Double {\n    return (x.first * y.second) - (y.first * x.second)\n}\n\nenum class Orientation {\n    Collinear,\n    Clockwise,\n    AntiClockwise;\n\n    fun isOpposing(other: Orientation): Boolean {\n        return if (this == Collinear) {\n            false\n        } else if (this == Clockwise && other == AntiClockwise) {\n            true\n        } else if (this == AntiClockwise && other == Clockwise) {\n            true\n        } else {\n            false\n        }\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/injection/ObjectProvider.kt",
    "content": "package org.odk.collect.shared.injection\n\nimport java.util.function.Supplier\n\n/**\n * Allows a shared singleton to act as a provider for dependencies by implementing\n * [ObjectProviderHost] and returning a [ObjectProvider] from\n * [ObjectProviderHost.getObjectProvider].\n */\n\ninterface ObjectProvider {\n    fun <T> provide(clazz: Class<T>): T\n}\n\ninterface ObjectProviderHost {\n    fun getObjectProvider(): ObjectProvider\n}\n\n/**\n * Provides a basic implementation of [ObjectProvider] that allows an instantiation of a\n * requested class to be provided by a [Supplier].\n */\nclass SupplierObjectProvider : ObjectProvider {\n\n    private val suppliers: MutableMap<Class<*>, Supplier<*>> = mutableMapOf()\n\n    fun <T> addSupplier(clazz: Class<T>, supplier: Supplier<T>) {\n        suppliers[clazz] = supplier\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    override fun <T> provide(clazz: Class<T>): T {\n        return suppliers[clazz]!!.get() as T\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/locks/BooleanChangeLock.kt",
    "content": "package org.odk.collect.shared.locks\n\nclass BooleanChangeLock : ChangeLock {\n    private var currentToken: Any? = null\n\n    override fun tryLock(token: Any): Boolean {\n        if (currentToken != null) {\n            return false\n        } else {\n            currentToken = token\n            return true\n        }\n    }\n\n    override fun unlock(token: Any) {\n        if (currentToken == token) {\n            currentToken = null\n        }\n    }\n\n    fun lock(token: Any) {\n        if (currentToken != null) {\n            throw IllegalStateException()\n        } else {\n            currentToken = token\n        }\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/locks/ChangeLock.kt",
    "content": "package org.odk.collect.shared.locks\n\nimport java.util.function.Function\n\ninterface ChangeLock {\n    /**\n     * @param function some work to be executed after attempting to acquire the lock. The function\n     * will be passed `true` if the lock was acquired and `false` if not. The\n     * function return value will be returned from the call to this method.\n     */\n    fun <T> withLock(function: Function<Boolean, T>): T {\n        val acquired = tryLock(DEFAULT_TOKEN)\n\n        return try {\n            function.apply(acquired)\n        } finally {\n            if (acquired) {\n                unlock(DEFAULT_TOKEN)\n            }\n        }\n    }\n\n    fun tryLock(token: Any): Boolean\n\n    fun unlock(token: Any)\n\n    companion object {\n        const val DEFAULT_TOKEN = \"default_token\"\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/locks/ThreadSafeBooleanChangeLock.kt",
    "content": "package org.odk.collect.shared.locks\n\nclass ThreadSafeBooleanChangeLock : ChangeLock {\n    private var currentToken: Any? = null\n\n    override fun tryLock(token: Any): Boolean {\n        return synchronized(this) {\n            if (currentToken != null) {\n                false\n            } else {\n                currentToken = token\n                true\n            }\n        }\n    }\n\n    override fun unlock(token: Any) {\n        synchronized(this) {\n            if (currentToken == token) {\n                currentToken = null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/result/Result.kt",
    "content": "package org.odk.collect.shared.result\n\nsealed class Result<S, E> {\n    data class Success<S, E>(val value: S) : Result<S, E>()\n    data class Error<S, E>(val value: E) : Result<S, E>()\n\n    companion object {\n        fun <S, E> S.toSuccess(): Success<S, E> {\n            return Success(this)\n        }\n\n        fun <S, E> E.toError(): Error<S, E> {\n            return Error(this)\n        }\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/settings/InMemSettings.kt",
    "content": "package org.odk.collect.shared.settings\n\nclass InMemSettings : Settings {\n\n    val map = mutableMapOf<String, Any?>()\n\n    override fun save(key: String, value: Any?) {\n        map[key] = value\n    }\n\n    override fun remove(key: String) {\n        map.remove(key)\n    }\n\n    override fun getString(key: String): String? {\n        return map[key] as String?\n    }\n\n    override fun setDefaultForAllSettingsWithoutValues() {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun saveAll(prefs: Map<String, Any?>) {\n        map.putAll(prefs)\n    }\n\n    override fun reset(key: String) {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun clear() {\n        map.clear()\n    }\n\n    override fun contains(key: String): Boolean {\n        return map.containsKey(key)\n    }\n\n    override fun getAll(): Map<String, *> {\n        return map\n    }\n\n    override fun getBoolean(key: String): Boolean {\n        return map[key] as Boolean? ?: false\n    }\n\n    override fun getLong(key: String): Long {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun getInt(key: String): Int {\n        return map[key] as Int? ?: 0\n    }\n\n    override fun getFloat(key: String): Float {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun getStringSet(key: String): Set<String>? {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun registerOnSettingChangeListener(listener: Settings.OnSettingChangeListener) {\n    }\n\n    override fun unregisterOnSettingChangeListener(listener: Settings.OnSettingChangeListener) {\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/settings/Settings.kt",
    "content": "package org.odk.collect.shared.settings\n\ninterface Settings {\n    fun setDefaultForAllSettingsWithoutValues()\n\n    fun save(key: String, value: Any?)\n\n    fun saveAll(prefs: Map<String, Any?>)\n\n    fun remove(key: String)\n\n    fun reset(key: String)\n\n    fun clear()\n\n    fun contains(key: String): Boolean\n\n    fun getAll(): Map<String, *>\n\n    fun getString(key: String): String?\n\n    fun getBoolean(key: String): Boolean\n\n    fun getLong(key: String): Long\n\n    fun getInt(key: String): Int\n\n    fun getFloat(key: String): Float\n\n    fun getStringSet(key: String): Set<String>?\n\n    fun registerOnSettingChangeListener(listener: OnSettingChangeListener)\n\n    fun unregisterOnSettingChangeListener(listener: OnSettingChangeListener)\n\n    interface OnSettingChangeListener {\n        fun onSettingChanged(key: String)\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/strings/Md5.kt",
    "content": "package org.odk.collect.shared.strings\n\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.FileNotFoundException\nimport java.io.IOException\nimport java.io.InputStream\nimport java.math.BigInteger\nimport java.security.MessageDigest\nimport java.security.NoSuchAlgorithmException\n\nobject Md5 {\n\n    @JvmStatic\n    @JvmOverloads\n    fun String.getMd5Hash(bufSize: Int = 16 * 1024): String? {\n        return getMd5Hash(this.byteInputStream(), bufSize)\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun File.getMd5Hash(bufSize: Int = 16 * 1024): String? {\n        val inputStream: InputStream = try {\n            FileInputStream(this)\n        } catch (e: FileNotFoundException) {\n            return null\n        }\n\n        return getMd5Hash(inputStream, bufSize)\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun getMd5Hash(inputStream: InputStream, bufSize: Int = 16 * 1024): String? {\n        return try {\n            val md = MessageDigest.getInstance(\"MD5\")\n            val buffer = ByteArray(bufSize)\n            while (true) {\n                val result = inputStream.read(buffer, 0, bufSize)\n                if (result == -1) {\n                    break\n                }\n                md.update(buffer, 0, result)\n            }\n            val md5 = StringBuilder(BigInteger(1, md.digest()).toString(16))\n            while (md5.length < 32) {\n                md5.insert(0, \"0\")\n            }\n            inputStream.close()\n            md5.toString()\n        } catch (e: NoSuchAlgorithmException) {\n            null\n        } catch (e: IOException) {\n            null\n        }\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/strings/RandomString.java",
    "content": "package org.odk.collect.shared.strings;\n\nimport java.security.SecureRandom;\n\npublic final class RandomString {\n\n    private RandomString() {\n\n    }\n\n    static final String CHARS = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n\n    public static String randomString(int len) {\n        StringBuilder sb = new StringBuilder(len);\n        for (int i = 0; i < len; i++) {\n            sb.append(CHARS.charAt(new SecureRandom().nextInt(CHARS.length())));\n        }\n\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/strings/StringUtils.kt",
    "content": "package org.odk.collect.shared.strings\n\nimport java.text.BreakIterator\n\nobject StringUtils {\n\n    @JvmStatic\n    fun firstCharacterOrEmoji(text: String): String {\n        if (text.isEmpty()) return \"\"\n\n        val iterator = BreakIterator.getCharacterInstance()\n        iterator.setText(text)\n\n        return text.substring(iterator.first(), iterator.next())\n    }\n\n    @JvmStatic\n    fun ellipsizeBeginning(text: String): String {\n        return if (text.length <= 100) {\n            text\n        } else {\n            \"...\" + text.substring(\n                text.length - 97,\n                text.length\n            )\n        }\n    }\n\n    /**\n     * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string\n     * \"null\" will be used as the delimiter.\n     * @param tokens an array objects to be joined. Strings will be formed from the objects by\n     * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If\n     * tokens is empty, an empty string will be returned.\n     */\n    @JvmStatic\n    fun join(delimiter: CharSequence, tokens: Iterable<*>) =\n        tokens.joinToString(separator = delimiter.toString())\n\n    @JvmStatic\n    fun isBlank(string: String): Boolean {\n        val chars = string.toCharArray()\n        for (character in chars) {\n            if (!Character.isWhitespace(character)) {\n                return false\n            }\n        }\n        return true\n    }\n\n    @JvmStatic\n    fun trim(text: CharSequence?): CharSequence? {\n        if (text == null || text.length == 0) {\n            return text\n        }\n        val len = text.length\n        var start = 0\n        var end = len - 1\n        while (start < len && Character.isWhitespace(text[start])) {\n            start++\n        }\n        while (Character.isWhitespace(text[end]) && end > 0) {\n            end--\n        }\n        return if (end >= start) {\n            text.subSequence(start, end + 1)\n        } else {\n            text\n        }\n    }\n\n    @JvmStatic\n    fun removeEnd(str: String?, remove: String): String? {\n        if (str == null || str.isEmpty()) {\n            return str\n        }\n        return if (str.endsWith(remove)) {\n            str.substring(0, str.length - remove.length)\n        } else {\n            str\n        }\n    }\n}\n"
  },
  {
    "path": "shared/src/main/java/org/odk/collect/shared/strings/UUIDGenerator.kt",
    "content": "package org.odk.collect.shared.strings\n\nimport java.util.UUID\n\nclass UUIDGenerator {\n    fun generateUUID() = UUID.randomUUID().toString()\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/Md5Test.kt",
    "content": "package org.odk.collect.shared\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.shared.strings.Md5.getMd5Hash\nimport java.io.File\nimport java.io.FileWriter\n\nclass Md5Test {\n\n    @Test\n    fun md5HashIsCorrect() {\n        val contents = \"Hello, world\"\n        val tempFile = File.createTempFile(\"hello\", \"txt\")\n        tempFile.deleteOnExit()\n\n        FileWriter(tempFile).use {\n            it.write(contents)\n        }\n\n        for (bufSize in listOf(1, contents.length - 1, contents.length, 64 * 1024)) {\n            val expectedResult = \"bc6e6f16b8a077ef5fbc8d59d0b931b9\" // From md5 command-line utility\n            assertThat(tempFile.getMd5Hash(bufSize), equalTo(expectedResult))\n        }\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/PathUtilsTest.kt",
    "content": "package org.odk.collect.shared\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\n\nclass PathUtilsTest {\n    @Test\n    fun `getRelativeFilePath() returns filePath with dirPath removed`() {\n        val path = PathUtils.getRelativeFilePath(\"/root/dir\", \"/root/dir/file\")\n        assertThat(path, equalTo(\"file\"))\n    }\n\n    @Test\n    fun `getRelativeFilePath() returns filePath when it does not start with dirPath`() {\n        val path = PathUtils.getRelativeFilePath(\"/anotherRoot/anotherDir\", \"/root/dir/file\")\n        assertThat(path, equalTo(\"/root/dir/file\"))\n    }\n\n    @Test\n    fun `reserved chars should be removed from file name`() {\n        val result = PathUtils.getPathSafeFileName(\"P\\\"1*ą/ć:!<@>#?$\\\\%|^&[]{}_=+\")\n        assertThat(result, equalTo(\"P_1_ą_ć_!_@_#_\\$_%_^&[]{}_=+\"))\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/QuickCheck.kt",
    "content": "package org.odk.collect.shared\n\n/**\n * Simple helper to allow writing neater [QuickCheck](https://en.wikipedia.org/wiki/QuickCheck)\n * style tests.\n */\nfun <Input, Output> ((Input) -> Output).quickCheck(\n    iterations: Int,\n    generator: Sequence<Input>,\n    checks: (Input, Output) -> Unit\n) {\n    generator.take(iterations).forEach { input ->\n        val output = this(input)\n        checks(input, output)\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/collections/CollectionExtensionsTest.kt",
    "content": "package org.odk.collect.shared.collections\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.json.JSONArray\nimport org.junit.Test\nimport org.odk.collect.shared.collections.CollectionExtensions.has\nimport org.odk.collect.shared.collections.CollectionExtensions.itemFromHashOf\n\nclass CollectionExtensionsTest {\n\n    @Test\n    fun `itemFromHashOf returns item using hashcode as index`() {\n        assertThat(listOf(\"0\", \"1\").itemFromHashOf(HashCode(0)), equalTo(\"0\"))\n        assertThat(listOf(\"0\", \"1\").itemFromHashOf(HashCode(1)), equalTo(\"1\"))\n    }\n\n    @Test\n    fun `itemFromHashOf wraps around when hashcode is greater than index`() {\n        assertThat(listOf(\"0\", \"1\").itemFromHashOf(HashCode(2)), equalTo(\"0\"))\n        assertThat(listOf(\"0\", \"1\").itemFromHashOf(HashCode(5)), equalTo(\"1\"))\n    }\n\n    @Test\n    fun `itemFromHashOf works with negative hashcode`() {\n        assertThat(listOf(\"0\", \"1\").itemFromHashOf(HashCode(-1)), equalTo(\"1\"))\n    }\n\n    @Test\n    fun `has returns true if given value exists in json array`() {\n        assertThat(JSONArray(listOf(\"blah\")).has(\"blah\"), equalTo(true))\n    }\n\n    @Test\n    fun `has returns false if given value does not exist in json array`() {\n        assertThat(JSONArray(listOf(\"blah\")).has(\"blah2\"), equalTo(false))\n    }\n\n    @Test\n    fun `has returns false if json array is empty`() {\n        assertThat(JSONArray().has(\"blah\"), equalTo(false))\n    }\n}\n\nprivate data class HashCode(private val value: Int) {\n    override fun hashCode(): Int {\n        return value\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n\n        other as HashCode\n\n        if (value != other.value) return false\n\n        return true\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/files/FileExtTest.kt",
    "content": "package org.odk.collect.shared.files\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.containsInAnyOrder\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.mockito.Mockito.mock\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.shared.TempFiles\nimport org.odk.collect.shared.files.FileExt.listFilesRecursively\nimport org.odk.collect.shared.files.FileExt.sanitizedCanonicalPath\nimport org.odk.collect.shared.files.FileExt.saveToFile\nimport java.io.File\n\nclass FileExtTest {\n    @Test\n    fun `sanitizedCanonicalPath returns sanitized canonicalPath if androidData path segment is incorrect`() {\n        val file = mock<File>().apply {\n            whenever(canonicalPath).thenReturn(\"/storage/emulated/0/android/Data/org.odk.collect.android/files/projects/DEMO/blah\")\n        }\n\n        assertThat(file.sanitizedCanonicalPath(), equalTo(\"/storage/emulated/0/Android/data/org.odk.collect.android/files/projects/DEMO/blah\"))\n    }\n\n    @Test\n    fun `sanitizedCanonicalPath returns original canonicalPath if androidData path segment is correct`() {\n        val file = mock<File>().apply {\n            whenever(canonicalPath).thenReturn(\"/storage/emulated/0/Android/data/org.odk.collect.android/files/projects/DEMO/blah\")\n        }\n\n        assertThat(file.sanitizedCanonicalPath(), equalTo(file.canonicalPath))\n    }\n\n    @Test\n    fun `sanitizedCanonicalPath returns original canonicalPath if other part of the path than the androidData is incorrect`() {\n        val file = mock<File>().apply {\n            whenever(canonicalPath).thenReturn(\"/Storage/Emulated/0/Android/data/org.odk.collect.android/files/projects/DEMO/blah\")\n        }\n\n        assertThat(file.sanitizedCanonicalPath(), equalTo(file.canonicalPath))\n    }\n\n    @Test\n    fun `sanitizedCanonicalPath returns original canonicalPath if it does not contain the androidData part`() {\n        val file = mock<File>().apply {\n            whenever(canonicalPath).thenReturn(\"/data/data/DEMO/files/blah\")\n        }\n\n        assertThat(file.sanitizedCanonicalPath(), equalTo(file.canonicalPath))\n    }\n\n    @Test\n    fun `saveToFile saves data to file`() {\n        val file = TempFiles.createTempFile().apply {\n            saveToFile(\"blah\".byteInputStream())\n        }\n\n        assertThat(file.readText(), equalTo(\"blah\"))\n    }\n\n    @Test\n    fun listFilesRecursively_withEmptyDirectory_returnsEmptyList() {\n        val files = TempFiles.createTempDir().listFilesRecursively()\n        assertThat(files.size, equalTo(0))\n    }\n\n    @Test\n    fun listFilesRecursively_withFilesInDirectory_returnsFiles() {\n        val directory = TempFiles.createTempDir()\n        val file1 = TempFiles.createTempFile(directory, \"blah1\", \".txt\")\n        val file2 = TempFiles.createTempFile(directory, \"blah2\", \".txt\")\n\n        val files = directory.listFilesRecursively()\n        assertThat(files, containsInAnyOrder(file1, file2))\n    }\n\n    @Test\n    fun listFilesRecursively_withFilesAndEmptyDirectoryInDirectory_returnsFiles() {\n        val directory = TempFiles.createTempDir()\n        val file1 = TempFiles.createTempFile(directory, \"blah1\", \".txt\")\n        val file2 = TempFiles.createTempFile(directory, \"blah2\", \".txt\")\n        File(directory, \"blah\").mkdir()\n\n        val files = directory.listFilesRecursively()\n        assertThat(files, containsInAnyOrder(file1, file2))\n    }\n\n    @Test\n    fun listFilesRecursively_withFilesAndADirectoryWithFilesInDirectory_returnsFilesFromBothDirectories() {\n        val directory1 = TempFiles.createTempDir()\n        val file1 = TempFiles.createTempFile(directory1, \"blah1\", \".txt\")\n        val file2 = TempFiles.createTempFile(directory1, \"blah2\", \".txt\")\n\n        val directory2 = File(directory1, \"blah\").also { it.mkdir() }\n        val file3 = TempFiles.createTempFile(directory2, \"blah3\", \".txt\")\n\n        val files = directory1.listFilesRecursively()\n        assertThat(files, containsInAnyOrder(file1, file2, file3))\n    }\n\n    @Test\n    fun listFilesRecursively_withFilesAndADirectoryContainingAnotherDirectoryWithFilesInDirectory_returnsFilesFromAllDirectories() {\n        val directory1 = TempFiles.createTempDir()\n        val file1 = TempFiles.createTempFile(directory1, \"blah1\", \".txt\")\n        val file2 = TempFiles.createTempFile(directory1, \"blah2\", \".txt\")\n\n        val directory2 = File(directory1, \"blah\").also { it.mkdir() }\n        val file3 = TempFiles.createTempFile(directory2, \"blah3\", \".txt\")\n\n        val directory3 = File(directory2, \"blah\").also { it.mkdir() }\n        val file4 = TempFiles.createTempFile(directory3, \"blah4\", \".txt\")\n\n        val files = directory1.listFilesRecursively()\n        assertThat(files, containsInAnyOrder(file1, file2, file3, file4))\n    }\n\n    @Test\n    fun listFilesRecursively_whenDirectoryDoesNotExist_returnsEmptyList() {\n        val directory = File(TempFiles.getPathInTempDir())\n        val files = directory.listFilesRecursively()\n        assertThat(files.size, equalTo(0))\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/geometry/GeometryTest.kt",
    "content": "package org.odk.collect.shared.geometry\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.shared.geometry.support.GeometryTestUtils.addRandomIntersectingSegment\nimport org.odk.collect.shared.geometry.support.GeometryTestUtils.getTraceGenerator\nimport org.odk.collect.shared.geometry.support.GeometryTestUtils.reverse\nimport org.odk.collect.shared.geometry.support.GeometryTestUtils.scale\nimport org.odk.collect.shared.quickCheck\nimport kotlin.random.Random\n\nclass GeometryTest {\n\n    @Test\n    fun `Trace#intersects returns false for an empty list`() {\n        assertThat(Trace(emptyList()).intersects(), equalTo(false))\n    }\n\n    @Test\n    fun `Trace#intersects returns false when there is only one point`() {\n        val trace = Trace(listOf(Point(0.0, 0.0)))\n        assertThat(trace.intersects(), equalTo(false))\n    }\n\n    @Test\n    fun `Trace#intersects returns false when there is only one segment`() {\n        val trace = Trace(listOf(Point(0.0, 0.0), Point(1.0, 0.0)))\n        assertThat(trace.intersects(), equalTo(false))\n    }\n\n    @Test\n    fun `Trace#intersects returns false when no segment intersects with another`() {\n        val trace = Trace(\n            listOf(\n                Point(0.0, 0.0),\n                Point(1.0, 1.0),\n                Point(2.0, 0.0)\n            )\n        )\n\n        assertThat(trace.intersects(), equalTo(false))\n    }\n\n    @Test\n    fun `Trace#intersects returns false when no segment intersects with another in a closed trace`() {\n        val trace = Trace(\n            listOf(\n                Point(0.0, 0.0),\n                Point(1.0, 1.0),\n                Point(2.0, 0.0),\n                Point(0.0, 0.0)\n            )\n        )\n\n        assertThat(trace.intersects(), equalTo(false))\n    }\n\n    @Test\n    fun `Trace#intersects returns true when a segment intersects with another`() {\n        val trace = Trace(\n            listOf(\n                Point(1.0, 1.0),\n                Point(1.0, 3.0),\n                Point(2.0, 3.0),\n                Point(2.0, 2.0),\n                Point(0.0, 2.0)\n            )\n        )\n\n        assertThat(trace.intersects(), equalTo(true))\n    }\n\n    @Test\n    fun `Trace#intersects returns true when a segment intersects with another in a closed trace`() {\n        val trace = Trace(\n            listOf(\n                Point(1.0, 1.0),\n                Point(1.0, 3.0),\n                Point(2.0, 3.0),\n                Point(2.0, 2.0),\n                Point(0.0, 2.0),\n                Point(1.0, 1.0)\n            )\n        )\n\n        assertThat(trace.intersects(), equalTo(true))\n    }\n\n    @Test\n    fun `Trace#intersects returns false when a segment's end points are both on different sides of another, but the segments do not intersect`() {\n        val trace = Trace(\n            listOf(\n                Point(1.0, 1.0),\n                Point(1.0, 2.0),\n                Point(3.0, 3.0),\n                Point(0.0, 3.0)\n            )\n        )\n\n        assertThat(trace.intersects(), equalTo(false))\n    }\n\n    @Test\n    fun `Trace#intersects returns true when just an endpoint touches another segment`() {\n        val trace = Trace(\n            listOf(\n                Point(0.0, 0.0),\n                Point(1.0, 1.0),\n                Point(2.0, 0.0),\n                Point(-1.0, 0.0)\n            )\n        )\n\n        assertThat(trace.intersects(), equalTo(true))\n    }\n\n    @Test\n    fun `Trace#intersects returns true when two segments and they intersect`() {\n        val endpointWithin = Trace(\n            listOf(\n                Point(0.0, 0.0),\n                Point(0.0, 1.0),\n                Point(0.0, 0.5)\n            )\n        )\n        assertThat(endpointWithin.intersects(), equalTo(true))\n\n        val endpointBeyond = Trace(listOf(\n            Point(0.0, 0.0),\n            Point(0.0, 1.0),\n            Point(0.0, -1.0),\n        ))\n        assertThat(endpointBeyond.intersects(), equalTo(true))\n\n        val endpointMatching = Trace(listOf(\n            Point(0.0, 0.0),\n            Point(0.0, 1.0),\n            Point(0.0, 0.0),\n        ))\n        assertThat(endpointMatching.intersects(), equalTo(true))\n    }\n\n    @Test\n    fun `Trace#intersects returns true when the trace closes on a non-origin vertex`() {\n        val trace = Trace(\n            listOf(\n                Point(0.0, 0.0),\n                Point(0.0, 1.0), // Close back on this point\n                Point(0.0, 2.0),\n                Point(1.0, 2.0),\n                Point(0.0, 1.0)\n            )\n        )\n\n        assertThat(trace.intersects(), equalTo(true))\n    }\n\n    @Test\n    fun `Trace#intersects returns false for right angled triangle`() {\n        val trace = Trace(\n            listOf(\n                Point(0.0, 0.0),\n                Point(10.0, 10.0),\n                Point(0.0, 10.0)\n            )\n        )\n\n        assertThat(trace.intersects(), equalTo(false))\n    }\n\n    @Test\n    fun `Trace#segments returns false for trace with duplicate points`() {\n        val trace = Trace(\n            listOf(\n                Point(0.0, 0.0),\n                Point(1.0, 0.0),\n                Point(1.0, 0.0),\n                Point(2.0, 0.0)\n            )\n        )\n\n        assertThat(trace.intersects(), equalTo(false))\n    }\n\n    @Test\n    fun `Trace#segments does not include zero-length segments`() {\n        val trace = Trace(\n            listOf(\n                Point(0.0, 0.0),\n                Point(1.0, 0.0),\n                Point(1.0, 0.0),\n                Point(2.0, 0.0)\n            )\n        )\n\n        assertThat(trace.segments(), equalTo(listOf(\n            LineSegment(Point(0.0, 0.0), Point(1.0, 0.0)),\n            LineSegment(Point(1.0, 0.0), Point(2.0, 0.0))\n        )))\n    }\n\n    @Test\n    fun `Trace#intersects returns true for 3 segment closed trace reversing on itself`() {\n        val trace = Trace(listOf(\n            Point(0.0, 0.0),\n            Point(2.0, 0.0),\n            Point(1.0, 0.0),\n            Point(0.0, 0.0)\n        ))\n        assertThat(trace.intersects(), equalTo(true))\n    }\n\n    @Test\n    fun `Trace#intersects satisfies metamorphic relationships`() {\n        { trace: Trace -> trace.intersects() }.quickCheck(\n            iterations = 1000,\n            generator = getTraceGenerator()\n        ) { trace, intersects ->\n            // Check intersects is consistent when trace is reversed\n            val reversedTrace = trace.reverse()\n            assertThat(\n                \"Expected intersects=$intersects:\\n$reversedTrace\",\n                reversedTrace.intersects(),\n                equalTo(intersects)\n            )\n\n            // Check intersects is consistent when trace is scaled\n            val scaledTrace = trace.scale(Random.nextDouble(0.1, 10.0))\n            assertThat(\n                \"Expected intersects=$intersects:\\n$scaledTrace\",\n                scaledTrace.intersects(),\n                equalTo(intersects)\n            )\n\n            // Check adding an intersection makes intersects true\n            if (!intersects) {\n                val intersectingTrace = trace.addRandomIntersectingSegment()\n                assertThat(\n                    \"Expected intersects=true:\\n$intersectingTrace\",\n                    intersectingTrace.intersects(),\n                    equalTo(true)\n                )\n            }\n        }\n    }\n\n    @Test\n    fun `LineSegment#intersects detects any endpoint touching the other line`() {\n        val line = LineSegment(Point(0.0, 0.0), Point(0.0, 2.0))\n\n        val aTouching = LineSegment(Point(-1.0, 0.0), Point(1.0, 0.0))\n        assertThat(line.intersects(aTouching), equalTo(true))\n\n        val bTouching = LineSegment(Point(-1.0, 2.0), Point(1.0, 2.0))\n        assertThat(line.intersects(bTouching), equalTo(true))\n\n        val cTouching = LineSegment(Point(0.0, 1.0), Point(1.0, 1.0))\n        assertThat(line.intersects(cTouching), equalTo(true))\n\n        val dTouching = LineSegment(Point(-1.0, 1.0), Point(0.0, 1.0))\n        assertThat(line.intersects(dTouching), equalTo(true))\n    }\n\n    @Test\n    fun `LineSegment#intersects does not detect intersections for collinear endpoints`() {\n        val segment1 = LineSegment(Point(0.0, 0.0), Point(4.0, 0.0))\n        val segment2 = LineSegment(Point(4.0, 4.0), Point(8.0, 0.0))\n\n        assertThat(segment1.intersects(segment2), equalTo(false))\n    }\n\n    @Test\n    fun `LineSegment#intersects with allowConnection true still finds intersections in a cross`() {\n        val segment1 = LineSegment(Point(-1.0, 0.0), Point(1.0, 0.0))\n        val segment2 = LineSegment(Point(0.0, -1.0), Point(0.0, 1.0))\n\n        assertThat(segment1.intersects(segment2, allowConnection = true), equalTo(true))\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/geometry/support/GeometryTestUtils.kt",
    "content": "package org.odk.collect.shared.geometry.support\n\nimport org.odk.collect.shared.geometry.LineSegment\nimport org.odk.collect.shared.geometry.Point\nimport org.odk.collect.shared.geometry.Trace\nimport org.odk.collect.shared.geometry.segments\nimport org.odk.collect.shared.geometry.support.GeometryTestUtils.interpolate\nimport kotlin.random.Random\n\nobject GeometryTestUtils {\n\n    fun getTraceGenerator(maxLength: Int = 10, maxCoordinate: Double = 100.0): Sequence<Trace> {\n        return generateSequence {\n            val length = Random.nextInt(3, maxLength)\n            val trace = Trace(0.until(length).map {\n                Point(\n                    Random.nextDouble(maxCoordinate * -1, maxCoordinate),\n                    Random.nextDouble(maxCoordinate * -1, maxCoordinate)\n                )\n            })\n\n            if (trace.isClosed()) {\n                trace\n            } else {\n                val shouldClose = Random.nextBoolean()\n                if (shouldClose) {\n                    trace.copy(points = trace.points + trace.points.first())\n                } else {\n                    trace\n                }\n            }\n        }\n    }\n\n    fun Trace.reverse(): Trace {\n        return Trace(points.reversed())\n    }\n\n    fun Trace.scale(factor: Double): Trace {\n        return Trace(points.map {\n            Point(it.x * factor, it.y * factor)\n        })\n    }\n\n    /**\n     * Choose random segment, a random (interpolated) point on that segment and then create a new\n     * trace with an additional point just beyond that to create an intersecting trace.\n     *\n     * Never chooses the last segment as a target for intersecting as that can only create a\n     * collinear intersection which is unlikely to be accurate due to inaccuracies in [interpolate].\n     */\n    fun Trace.addRandomIntersectingSegment(): Trace {\n        val intersectionSegment = segments().dropLast(1).random()\n        val intersectPosition = Random.nextDouble(0.1, 1.0)\n        val intersectionPoint = intersectionSegment.interpolate(intersectPosition)\n        val lineSegment = LineSegment(points.last(), intersectionPoint)\n        val intersectingSegment =\n            LineSegment(lineSegment.start, lineSegment.interpolate(1.1))\n        return Trace(points + intersectingSegment.end)\n    }\n\n    /**\n     * Calculate a [Point] on this [LineSegment] based on the `position` using\n     * [Linear interpolation](https://en.wikipedia.org/wiki/Linear_interpolation). `0` will return\n     * [LineSegment.start] and `1` will return [LineSegment.end].\n     */\n    fun LineSegment.interpolate(position: Double): Point {\n        val x = start.x + position * (end.x - start.x)\n        val y = start.y + position * (end.y - start.y)\n        return Point(x, y)\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/geometry/support/GeometryTestUtilsTest.kt",
    "content": "package org.odk.collect.shared.geometry.support\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.odk.collect.shared.geometry.LineSegment\nimport org.odk.collect.shared.geometry.Orientation\nimport org.odk.collect.shared.geometry.Point\nimport org.odk.collect.shared.geometry.orientation\nimport org.odk.collect.shared.geometry.support.GeometryTestUtils.interpolate\nimport org.odk.collect.shared.geometry.within\n\nclass GeometryTestUtilsTest {\n\n    @Test\n    fun `LineSegment#interpolate returns a point on the segment at a proportional distance`() {\n        val segment = LineSegment(Point(0.0, 0.0), Point(1.0, 0.0))\n\n        assertThat(segment.interpolate(0.0), equalTo(Point(0.0, 0.0)))\n        assertThat(segment.interpolate(0.5), equalTo(Point(0.5, 0.0)))\n        assertThat(segment.interpolate(1.0), equalTo(Point(1.0, 0.0)))\n    }\n\n    @Test\n    fun `LineSegment#interpolate returns a collinear point within the line's bounding box`() {\n        val segment = LineSegment(Point(0.0, 0.0), Point(1.0, 1.0))\n        val interpolatedPoint = segment.interpolate(0.5)\n\n        val orientation = orientation(interpolatedPoint, segment.start, segment.end)\n        assertThat(orientation, equalTo(Orientation.Collinear))\n        assertThat(interpolatedPoint.within(segment), equalTo(true))\n    }\n\n    @Test\n    fun `LineSegment#interpolate returns a collinear point within the line's bounding box for higher precision points with a suitable epsilon`() {\n        val segment = LineSegment(Point(56.6029153, 20.2311124), Point(56.6029192, 20.2310467))\n        val interpolatedPoint = segment.interpolate(0.5)\n\n        val orientation = orientation(interpolatedPoint, segment.start, segment.end, epsilon = 0.000001)\n        assertThat(orientation, equalTo(Orientation.Collinear))\n        assertThat(interpolatedPoint.within(segment), equalTo(true))\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/locks/BooleanChangeLockTest.kt",
    "content": "package org.odk.collect.shared.locks\n\nclass BooleanChangeLockTest : ChangeLockTest() {\n    override fun buildSubject(): ChangeLock {\n        return BooleanChangeLock()\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/locks/ChangeLockTest.kt",
    "content": "package org.odk.collect.shared.locks\n\nimport org.hamcrest.CoreMatchers.equalTo\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.junit.Test\n\nabstract class ChangeLockTest {\n    abstract fun buildSubject(): ChangeLock\n\n    @Test\n    fun `tryLock acquires the lock if it is not acquired`() {\n        val changeLock = buildSubject()\n\n        val acquired = changeLock.tryLock(\"foo\")\n\n        assertThat(acquired, equalTo(true))\n    }\n\n    @Test\n    fun `tryLock does not acquire the lock if it is already acquired`() {\n        val changeLock = buildSubject()\n\n        changeLock.tryLock(\"foo\")\n        val acquired = changeLock.tryLock(\"foo\")\n\n        assertThat(acquired, equalTo(false))\n    }\n\n    @Test\n    fun `unlock releases the lock for matching tokens`() {\n        val changeLock = buildSubject()\n\n        changeLock.tryLock(\"foo\")\n        changeLock.unlock(\"foo\")\n        val acquired = changeLock.tryLock(\"foo\")\n\n        assertThat(acquired, equalTo(true))\n    }\n\n    @Test\n    fun `unlock does not release the lock for not matching tokens`() {\n        val changeLock = buildSubject()\n\n        changeLock.tryLock(\"foo\")\n        changeLock.unlock(\"bar\")\n        val acquired = changeLock.tryLock(\"foo\")\n\n        assertThat(acquired, equalTo(false))\n    }\n\n    @Test\n    fun `withLock acquires the lock if it is not acquired`() {\n        val changeLock = buildSubject()\n\n        changeLock.withLock { acquired ->\n            assertThat(acquired, equalTo(true))\n        }\n    }\n\n    @Test\n    fun `withLock does not acquire the lock if it is already acquired`() {\n        val changeLock = buildSubject()\n\n        changeLock.tryLock(\"foo\")\n        changeLock.withLock { acquired ->\n            assertThat(acquired, equalTo(false))\n        }\n    }\n\n    @Test\n    fun `withLock releases the lock after performing the job`() {\n        val changeLock = buildSubject()\n\n        changeLock.withLock {}\n\n        val acquired = changeLock.tryLock(\"foo\")\n\n        assertThat(acquired, equalTo(true))\n    }\n\n    @Test\n    fun `withLock does not release the lock it it was not able to acquire it`() {\n        val changeLock = buildSubject()\n\n        changeLock.tryLock(\"foo\")\n        changeLock.withLock {}\n\n        val acquired = changeLock.tryLock(\"foo\")\n\n        assertThat(acquired, equalTo(false))\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/locks/ThreadSafeBooleanChangeLockTest.kt",
    "content": "package org.odk.collect.shared.locks\n\nclass ThreadSafeBooleanChangeLockTest : ChangeLockTest() {\n    override fun buildSubject(): ChangeLock {\n        return ThreadSafeBooleanChangeLock()\n    }\n}\n"
  },
  {
    "path": "shared/src/test/java/org/odk/collect/shared/strings/StringUtilsTest.kt",
    "content": "package org.odk.collect.shared.strings\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Assert.assertEquals\nimport org.junit.Assert.assertFalse\nimport org.junit.Assert.assertTrue\nimport org.junit.Test\nimport org.odk.collect.shared.strings.StringUtils.ellipsizeBeginning\nimport org.odk.collect.shared.strings.StringUtils.firstCharacterOrEmoji\nimport org.odk.collect.shared.strings.StringUtils.isBlank\nimport org.odk.collect.shared.strings.StringUtils.trim\n\nclass StringUtilsTest {\n\n    @Test\n    fun firstCharacterOrEmoji_whenStringIsOneCharacter_returnsString() {\n        assertThat(firstCharacterOrEmoji(\"N\"), equalTo(\"N\"))\n    }\n\n    @Test\n    fun firstCharacterOrEmoji_whenStringIsMoreThanOneCharacter_returnsFirstCharacter() {\n        assertThat(firstCharacterOrEmoji(\"Nosferatu\"), equalTo(\"N\"))\n    }\n\n    @Test\n    fun firstCharacterOrEmoji_whenStringIsEmoji_returnString() {\n        assertThat(firstCharacterOrEmoji(VAMPIRE_EMOJI), equalTo(VAMPIRE_EMOJI))\n    }\n\n    @Test\n    fun firstCharacterOrEmoji_whenStringIsComplexEmoji_returnString() {\n        assertThat(firstCharacterOrEmoji(MAN_VAMPIRE_EMOJI), equalTo(MAN_VAMPIRE_EMOJI))\n    }\n\n    @Test\n    fun firstCharacterOrEmoji_whenStringIsMultipleComplexEmojis_returnsFirstEmoji() {\n        assertThat(\n            firstCharacterOrEmoji(MAN_VAMPIRE_EMOJI + VAMPIRE_EMOJI),\n            equalTo(MAN_VAMPIRE_EMOJI)\n        )\n    }\n\n    @Test\n    fun firstCharacterOrEmoji_whenStringIsTextFollowedByEmoji_returnsEmoji() {\n        assertThat(firstCharacterOrEmoji(\"Dracula$MAN_VAMPIRE_EMOJI\"), equalTo(\"D\"))\n    }\n\n    @Test\n    fun firstCharacterOrEmoji_whenStringIsRepeatedEmojis_returnsEmoji() {\n        assertThat(firstCharacterOrEmoji(VAMPIRE_EMOJI + VAMPIRE_EMOJI), equalTo(VAMPIRE_EMOJI))\n    }\n\n    @Test\n    fun firstCharacterOrEmoji_whenStringIsEmpty_returnsEmptyString() {\n        assertThat(firstCharacterOrEmoji(\"\"), equalTo(\"\"))\n    }\n\n    @Test\n    fun ellipsizeBeginningTest() {\n        // 50 chars\n        assertEquals(\n            \"Lorem ipsum dolor sit amet, consectetur massa nunc\",\n            ellipsizeBeginning(\"Lorem ipsum dolor sit amet, consectetur massa nunc\")\n        )\n\n        // 100 chars\n        assertEquals(\n            \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer tempus, risus ac cursus turpis duis\",\n            ellipsizeBeginning(\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer tempus, risus ac cursus turpis duis\")\n        )\n\n        // 101 chars\n        assertEquals(\n            \"...m ipsum dolor sit amet, consectetur adipiscing elit. Cras finibus, augue a imperdiet orci aliquam\",\n            ellipsizeBeginning(\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras finibus, augue a imperdiet orci aliquam\")\n        )\n\n        // 150 chars\n        assertEquals(\n            \"...it. Donec cursus condimentum sagittis. Ut condimentum efficitur libero, vitae volutpat dui nullam\",\n            ellipsizeBeginning(\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec cursus condimentum sagittis. Ut condimentum efficitur libero, vitae volutpat dui nullam\")\n        )\n    }\n\n    @Test\n    fun whenStringIsJustWhitespace_returnsTrue() {\n        assertTrue(isBlank(\" \"))\n    }\n\n    @Test\n    fun whenStringContainsWhitespace_returnsFalse() {\n        assertFalse(isBlank(\" hello \"))\n    }\n\n    @Test\n    fun whenCharSequenceContainWhitespaces_shouldTrimReturnTrimmedCharSequence() {\n        val result = trim(\"\\n\\t <p style=\\\"text-align:center\\\">Text</p> \\t\\n\")\n        assertThat(result, equalTo(\"<p style=\\\"text-align:center\\\">Text</p>\"))\n    }\n\n    @Test\n    fun whenCharSequenceContainOnlyWhitespaces_shouldTrimReturnOriginalCharSequence() {\n        val result = trim(\"\\n\\t \\t\\n\")\n        assertThat(result, equalTo(\"\\n\\t \\t\\n\"))\n    }\n\n    @Test\n    fun whenCharSequenceIsNull_shouldTrimReturnNull() {\n        val result = trim(null)\n        assertThat(result, equalTo(null))\n    }\n\n    @Test\n    fun whenCharSequenceIsEmpty_shouldTrimReturnEmptyCharSequence() {\n        val result = trim(\"\")\n        assertThat(result, equalTo(\"\"))\n    }\n\n    companion object {\n        // Unicode for https://emojipedia.org/vampire/\n        private const val VAMPIRE_EMOJI = \"\\ud83e\\udddb\"\n\n        // Unicode for https://emojipedia.org/man-vampire/\n        private const val MAN_VAMPIRE_EMOJI = \"\\ud83e\\udddb\\u200d\\u2642\\ufe0f\"\n    }\n}\n"
  },
  {
    "path": "strings/.gitignore",
    "content": "/build"
  },
  {
    "path": "strings/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    namespace = \"org.odk.collect.strings\"\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n}\n\ndependencies {\n    implementation(libs.androidxCoreKtx)\n\n    /*\n     * We need to expose [AppCompatActivity] for classes in separate modules that\n     * extend [LocalizedActivity].\n     */\n    api(libs.androidxAppcompat)\n\n    testImplementation(libs.junit)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.robolectric)\n}\n\nrepositories {\n    mavenCentral()\n}\n"
  },
  {
    "path": "strings/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "strings/src/main/java/org/odk/collect/strings/format/LengthFormatter.kt",
    "content": "package org.odk.collect.strings.format\n\nimport java.util.Locale\n\nprivate const val ONE_HOUR = 3600000\nprivate const val ONE_MINUTE = 60000\nprivate const val ONE_SECOND = 1000\n\nfun formatLength(milliseconds: Long): String {\n    val hours = milliseconds / ONE_HOUR\n    val minutes = milliseconds % ONE_HOUR / ONE_MINUTE\n    val seconds = milliseconds % ONE_MINUTE / ONE_SECOND\n    return if (milliseconds < ONE_HOUR) {\n        String.format(Locale.getDefault(), \"%02d:%02d\", minutes, seconds)\n    } else {\n        String.format(Locale.getDefault(), \"%02d:%02d:%02d\", hours, minutes, seconds)\n    }\n}\n"
  },
  {
    "path": "strings/src/main/java/org/odk/collect/strings/localization/LocalizedActivity.kt",
    "content": "package org.odk.collect.strings.localization\n\nimport android.content.Context\nimport android.content.res.Configuration\nimport androidx.appcompat.app.AppCompatActivity\n\nopen class LocalizedActivity : AppCompatActivity() {\n\n    override fun attachBaseContext(base: Context?) {\n        super.attachBaseContext(base)\n        applyOverrideConfiguration(Configuration())\n    }\n\n    override fun applyOverrideConfiguration(newConfig: Configuration) {\n        super.applyOverrideConfiguration(updateConfigurationIfSupported(newConfig))\n    }\n\n    private fun updateConfigurationIfSupported(config: Configuration): Configuration? {\n        if (!config.locales.isEmpty) {\n            return config\n        }\n\n        val locale = when (applicationContext) {\n            is LocalizedApplication -> (applicationContext as LocalizedApplication).locale\n            else -> config.locales[0]\n        }\n\n        config.setLocale(locale)\n        config.setLayoutDirection(locale)\n        return config\n    }\n}\n"
  },
  {
    "path": "strings/src/main/java/org/odk/collect/strings/localization/LocalizedApplication.kt",
    "content": "package org.odk.collect.strings.localization\n\nimport android.content.Context\nimport android.content.res.Configuration\nimport android.content.res.Resources\nimport android.view.View\nimport java.util.Locale\n\ninterface LocalizedApplication {\n\n    val locale: Locale\n}\n\nfun Context.getLocalizedString(stringId: Int, vararg formatArgs: Any): String {\n    val locale = when (applicationContext) {\n        is LocalizedApplication -> (applicationContext as LocalizedApplication).locale\n\n        // Don't explode if the application doesn't implement LocalizedApplication. Useful\n        // when testing modules in isolation\n        else -> resources.configuration.locales[0]\n    }\n\n    return getLocalizedResources(locale).getString(stringId, *formatArgs)\n}\n\nfun Context.getLocalizedResources(locale: Locale): Resources {\n    val newConfig = Configuration(resources.configuration).apply {\n        setLocale(locale)\n    }\n\n    return createConfigurationContext(newConfig).resources\n}\n\nfun Context.getLocalizedQuantityString(stringId: Int, quantity: Int, vararg formatArgs: Any): String {\n    val locale = when (applicationContext) {\n        is LocalizedApplication -> (applicationContext as LocalizedApplication).locale\n\n        // Don't explode if the application doesn't implement LocalizedApplication. Useful\n        // when testing modules in isolation\n        else -> resources.configuration.locales[0]\n    }\n\n    val newConfig = Configuration(resources.configuration).apply {\n        setLocale(locale)\n    }\n\n    return createConfigurationContext(newConfig)\n        .resources\n        .getQuantityString(stringId, quantity, *formatArgs)\n}\n\nfun Context.isLTR(): Boolean {\n    return resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR\n}\n"
  },
  {
    "path": "strings/src/main/res/values/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?><!--\n    Copyright (C) 2009 University of Washington Licensed under the Apache\n    License, Version 2.0 (the \"License\"); you may not use this file except\n    in compliance with the License. You may obtain a copy of the License\n    at http://www.apache.org/licenses/LICENSE-2.0 Unless required by\n    applicable law or agreed to in writing, software distributed under the\n    License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\n    CONDITIONS OF ANY KIND, either express or implied. See the License for\n    the specific language governing permissions and limitations under the\n    License.\n-->\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\" tools:ignore=\"MissingTranslation\" tools:locale=\"en\">\n    <!-- Text for the topmost button on the main screen. It should be short and action-oriented. -->\n    <string name=\"enter_data\">Start new form</string>\n    <!-- Text for an action button on the main screen. -->\n    <string name=\"review_data\">Drafts</string>\n    <!-- Text for an action button on the main screen. -->\n    <string name=\"send_data\">Ready to send</string>\n    <!-- Text for an action button on the main screen. -->\n    <string name=\"view_sent_forms\">Sent</string>\n    <!-- Text for an action button on the main screen. -->\n    <string name=\"get_forms\">Download form</string>\n    <!-- Text for an action button on the main screen. -->\n    <string name=\"manage_files\">Delete form</string>\n\n    <string name=\"saved_forms\">Saved forms</string>\n\n    <string name=\"version_number\">Version: %s</string>\n    <string name=\"id_number\">ID: %s</string>\n    <!-- Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"added_on_date_at_time\">\\'Added on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n    <!-- Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"updated_on_date_at_time\">\\'Updated on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n    <!-- Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"saved_on_date_at_time\">\\'Saved on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n    <!-- Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"finalized_on_date_at_time\">\\'Finalized on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n    <!-- Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"sent_on_date_at_time\">\\'Sent on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n    <!-- Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"sending_failed_on_date_at_time\">\\'Sending failed on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n    <!-- Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"deleted_on_date_at_time\">\\'Deleted on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n    <string name=\"submission_deleted\">Submission deleted</string>\n    <!-- Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"modified_on_date_at_time\">\\'Modified on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n    <string name=\"encrypted_form\">Encrypted form</string>\n    <string name=\"deleted_form\">Deleted form</string>\n\n    <string name=\"sort_the_list\">Sort the list</string>\n    <string name=\"sort_by\">Sort by</string>\n    <string name=\"sort_by_name_asc\">Name, A-Z</string>\n    <string name=\"sort_by_name_desc\">Name, Z-A</string>\n    <string name=\"sort_by_date_desc\">Date, newest first</string>\n    <string name=\"sort_by_date_asc\">Date, oldest first</string>\n    <!-- A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first. -->\n    <string name=\"sort_by_last_saved\">Last saved first</string>\n    <string name=\"filter_the_list\">Filter the list</string>\n    <!-- Displayed in search boxes for form lists or question types that involve search -->\n    <string name=\"search\">Search</string>\n\n    <string name=\"not_granted_permission\">The activity you are trying to start requires a permission which is not granted.</string>\n    <string name=\"storage_runtime_permission_denied_title\">Storage permissions</string>\n    <string name=\"storage_runtime_permission_denied_desc\">Without these permissions Collect can\\'t access your forms or save answers. Reopen the app when you are ready to grant them.</string>\n    <string name=\"location_runtime_permissions_denied_title\">Location permissions</string>\n    <!-- Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted. -->\n    <string name=\"location_runtime_permissions_denied_desc\">Without the <b>Precise Location</b> permission Collect can\\'t record your location. Please try again when you are ready to grant it.</string>\n    <string name=\"camera_runtime_permission_denied_title\">Camera permission</string>\n    <string name=\"camera_runtime_permission_denied_desc\">Without this permission Collect can\\'t access the camera. Please try again when you are ready to grant it.</string>\n    <string name=\"record_audio_runtime_permission_denied_title\">Record audio permission</string>\n    <string name=\"record_audio_runtime_permission_denied_desc\">Without this permission Collect can\\'t access the microphone. Please try again when you are ready to grant it.</string>\n\n    <!--\n    ##############################################\n    # FILL BLANK FORM\n    ##############################################\n    -->\n    <!-- Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown. -->\n    <string name=\"open_form_map\">Open form map</string>\n    <string name=\"instance_scan_count\">%1$d forms added.</string>\n    <string name=\"finished_disk_scan\">Finished scanning. All forms loaded.</string>\n    <string name=\"xform_parse_error\">XForm parse error for %1$s: %2$s is missing or invalid.</string>\n\n    <string name=\"loading_form\">Loading Form</string>\n    <string name=\"survey_loading_reading_form_message\">Reading form definition…</string>\n    <string name=\"survey_loading_reading_data_message\">Reading survey data…</string>\n    <string name=\"survey_loading_reading_csv_message\">Reading CSV files…</string>\n    <string name=\"parse_error\">Sorry, unable to parse form.</string>\n    <string name=\"loading_form_failed\">An error occurred while loading the form. Please try again.</string>\n    <string name=\"cannot_edit_completed_form\">This form cannot be edited once it has been marked as finalized. It may be encrypted.</string>\n\n    <!-- Displayed when loading a form if that form was previously filled but closed because of a crash or system failure -->\n    <string name=\"savepoint_used\">Unsaved changes recovered from savepoint!</string>\n\n    <!-- Displayed in the form filling menu when a form definition specifies multiple languages -->\n    <string name=\"change_language\">Change Language</string>\n    <!-- Text for button displayed in the form filling menu to see the form's table of contents -->\n    <string name=\"view_hierarchy\">Go To Prompt</string>\n    <!-- Text for button displayed in the form filling menu to save the form -->\n    <string name=\"save_all_answers\">Save as draft</string>\n\n    <!-- Text of button from the form's table of contents to jump to the beginning of the form -->\n    <string name=\"jump_to_beginning\">Go To Start</string>\n    <!-- Text of button from the form's table of contents to jump to the end of the form -->\n    <string name=\"jump_to_end\">Go To End</string>\n    <!-- Text of button from the form's table of contents to navigate out of a group or a repeat -->\n    <string name=\"jump_to_previous\">Go Up</string>\n    <!-- Text of button from the form's table of contents to edit a finalized form -->\n    <string name=\"edit_finalized_form\">Edit finalized form</string>\n    <!-- Displayed below group names in a form's table of contents -->\n    <string name=\"group_label\">Group</string>\n    <!-- Displayed below repeat names in a form's table of contents -->\n    <string name=\"repeatable_group_label\">Repeatable Group</string>\n    <string name=\"exit\">Exit</string>\n\n    <!-- This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n    <string name=\"add_repeat_question\">Add \\\"%s\\\"?</string>\n    <!-- Text of button from the 'Add new repeat' dialog to confirm adding a repeat -->\n    <string name=\"add_repeat\">Add</string>\n    <!-- Text of button from the 'Add new repeat' dialog to cancel adding a repeat -->\n    <string name=\"do_not_add_repeat\">Don\\'t add</string>\n    <string name=\"add_another_menu\">Add another</string>\n    <string name=\"add_another_question\">Add another?</string>\n\n    <!-- Title of overlay menu displayed when long pressing on a question in a form -->\n    <string name=\"edit_prompt\">Edit Prompt</string>\n\n    <string name=\"clearanswer_confirm\">Remove the response to \\\"%s\\\"?</string>\n    <string name=\"clear_answer\">Remove response</string>\n    <string name=\"clear_answer_ask\">Remove This Response?</string>\n    <string name=\"clear_answer_no\">Cancel</string>\n    <string name=\"discard_answer\">Remove response</string>\n    <string name=\"discard_group\">Remove group</string>\n\n    <string name=\"delete_repeat\">Remove group</string>\n    <string name=\"delete_repeat_ask\">Remove This Group?</string>\n    <string name=\"delete_repeat_confirm\">Remove group \\\"%s\\\" and all of its sub-groups?</string>\n    <string name=\"delete_repeat_no\">Cancel</string>\n\n    <string name=\"invalid_answer_error\">Sorry, this response is invalid!</string>\n    <string name=\"required_answer_error\">Sorry, this response is required!</string>\n    <string name=\"success_form_validation\">Success! No errors found in your form.</string>\n    <string name=\"invalid_space_in_answer_singular\">Warning: underlying value %s has spaces</string>\n    <string name=\"invalid_space_in_answer_plural\">Warning: underlying values %s have spaces</string>\n\n    <string name=\"too_complex_form\">This form is too complex for this device. Try simplifying expressions or ask for help on the forum.</string>\n\n    <string name=\"survey_internal_error\">Internal error: step to prompt failed.</string>\n    <!-- Title of the end of form screen -->\n    <string name=\"save_enter_data_description\">You are at the end of %s.</string>\n    <!-- Text shown in an information box on the end of form screen when auto-send is off or the device is offline -->\n    <string name=\"form_editing_disabled_after_finalizing\">Can\\'t edit after finalize</string>\n    <!-- Text shown in an information box on the end of form screen when auto-send is on and the device is online -->\n    <string name=\"form_editing_disabled_after_sending\">Can\\'t edit after send</string>\n    <!-- Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send -->\n    <string name=\"form_editing_enabled_after_finalizing\">Can edit after finalize</string>\n    <!-- Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send -->\n    <string name=\"form_editing_enabled_after_sending\">Can edit after send</string>\n    <!-- Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send -->\n    <string name=\"form_editing_disabled_hint\">If you need to make edits to your form, \\\"Save as draft\\\" until you\\'re ready to send.</string>\n    <!-- Details shown in an information box on the end of form screen when the form allows edits after finalization -->\n    <string name=\"form_editing_enabled_after_finalizing_hint\">To make changes after finalizing your form, go to \\\"Ready to send\\\", open the form, and tap the edit icon.</string>\n    <!-- Details shown in an information box on the end of form screen when the form allows edits after sending -->\n    <string name=\"form_editing_enabled_after_sending_hint\">To make changes after sending your form, go to \\\"Sent\\\", open the form, and tap the edit icon.</string>\n    <!-- Button text for saving as draft -->\n    <string name=\"save_as_draft\">Save as draft</string>\n    <!-- Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline -->\n    <string name=\"finalize\">Finalize</string>\n    <!-- Button text for sending a submission. -->\n    <string name=\"send\">Send</string>\n\n    <string name=\"data_saved_error\">Sorry, form save failed!</string>\n    <string name=\"data_saved_ok\">Form successfully saved!</string>\n    <string name=\"save_point_error\">Error saving recovery point: %s</string>\n\n    <!-- Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded> -->\n    <string name=\"do_not_save\">Discard Form</string>\n    <string name=\"discard_changes\">Discard Changes</string>\n    <string name=\"keep_changes\">Save Changes</string>\n    <string name=\"keep_editing\">Keep editing</string>\n    <string name=\"quit_form_title\">Save form?</string>\n    <string name=\"save_explanation\">You can save this form and access it from your drafts at any time.</string>\n    <!-- Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"save_explanation_with_last_saved\">\\'This form was last saved on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm\\'. Save as draft to keep changes.\\'</string>\n    <string name=\"quit_form_continue_title\">Continue editing?</string>\n    <string name=\"discard_form_warning\">If you discard form, you will lose any changes you have made so far.</string>\n    <!-- Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"discard_changes_warning\">\\'This form was last saved on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm\\'. If you discard changes, you will lose any changes you have made since then.\\'</string>\n\n    <string name=\"saving_form\">Saving Form</string>\n    <string name=\"survey_saving_validating_message\">Validating answers…</string>\n    <string name=\"survey_saving_collecting_message\">Collecting data…</string>\n    <string name=\"survey_saving_saving_message\">Saving to SD card…</string>\n    <string name=\"survey_saving_finalizing_message\">Finalizing to SD card…</string>\n\n    <string name=\"survey_saving_encrypting_message\">Encrypting data…</string>\n    <string name=\"not_exactly_one_record_for_this_instance\">Not exactly one record for this instance!</string>\n    <string name=\"not_exactly_one_blank_form_for_this_form_id\">Not exactly one blank form matches this jr_form_id.</string>\n    <string name=\"no_form_id_specified\">No FormId specified???</string>\n    <string name=\"phone_does_not_support_rsa\">Phone does not support RSA encryption.</string>\n    <string name=\"invalid_rsa_public_key\">Invalid RSA public key.</string>\n    <string name=\"encryption_error_message\">%s Form has not been saved as finalized.</string>\n\n    <!-- Displayed when an attempt is made to start form filling, but the form is broken -->\n    <string name=\"form_cannot_be_used\">Form can\\'t be used</string>\n\n    <!-- Text displayed when audio recording fails to start -->\n    <string name=\"start_recording_failed\">Could not start recording.</string>\n\n    <!-- Text displayed when user tries to start recording while the mic is already in use -->\n    <string name=\"mic_in_use\">Microphone is already in use.</string>\n\n    <!-- Error message displayed when copying media from a temporary directory fails -->\n    <string name=\"answer_file_copy_failed_message\">Media could not be attached to form but is available at %1$s</string>\n\n    <!-- Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu. -->\n    <string name=\"recording_disabled\">Recording disabled. Enable in %s</string>\n\n    <!-- Error displayed when something bad happens during updating widgets in a field-list group -->\n    <string name=\"update_widgets_error\">Failure trying to update values. This is generally because of incorrect usage of calculations in the form design.\\n\\nIf you keep having this problem, report it to the person who asked you to collect data.</string>\n\n    <!--\n    ##############################################\n    # FORM FEATURES\n    ##############################################\n    -->\n    <!-- Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields -->\n    <string name=\"launch_app\">Launch</string>\n    <string name=\"activity_not_found\">No activity found to handle: %s</string>\n    <string name=\"no_app\">The requested application is missing. Please manually enter the reading.</string>\n    <string name=\"null_intent_value\">The external application did not provide expected information.</string>\n    <!-- Text on button that is part of print questions. Tapping the button prints the value of the question. -->\n    <string name=\"print\">Print</string>\n\n    <string name=\"open_url\">Open Url</string>\n\n    <string name=\"capture_osm\">Launch OpenMapKit</string>\n    <string name=\"recapture_osm\">Redo OSM Tagging</string>\n    <string name=\"edited_osm_file\">Edited OSM XML File:</string>\n    <string name=\"invalid_osm_data\">Something went wrong. We did not get valid OSM data.</string>\n    <string name=\"alert\">Alert</string>\n    <string name=\"install_openmapkit\">Please install OpenMapKit!</string>\n\n    <string name=\"get_barcode\">Get Barcode</string>\n    <string name=\"replace_barcode\">Replace Barcode</string>\n\n    <!-- Prompt under view finder to describe where to place the camera to scan a barcode -->\n    <string name=\"barcode_scanner_prompt\">Place the barcode inside the rectangle</string>\n\n    <!-- Label for button that rotates camera orientation when scanning barcodes -->\n    <string name=\"rotate_device\">Rotate device</string>\n\n    <string name=\"open_file\">Open file</string>\n\n    <string name=\"rank_items\">Rank items</string>\n\n    <string name=\"number_picker_title\">Number Picker</string>\n    <string name=\"invalid_range_widget\">This widget has invalid parameters!</string>\n\n    <string name=\"svg_file_does_not_exist\">SVG file does not exist!</string>\n\n    <string name=\"capture_image\">Take Picture</string>\n    <string name=\"choose_image\">Choose Image</string>\n    <string name=\"selected_invalid_image\">Selected file is not a valid image</string>\n    <string name=\"take_picture_instruction\">Tap the screen to take a picture</string>\n    <string name=\"error_front_camera_unavailable\">Front camera not available on this device</string>\n    <string name=\"camera_error\">There was an error taking picture</string>\n    <string name=\"camera_failed_to_initialize\">Couldn\\'t initialize camera!</string>\n    <string name=\"annotate_image\">Annotate Image</string>\n    <string name=\"gif_not_supported\">Gif files are not supported</string>\n    <string name=\"invalid_file_type\">Application returned an invalid file type</string>\n    <!-- Title displayed in a dialog for saving a signature, drawing, or annotated image -->\n    <string name=\"save_drawing_title\">Save changes?</string>\n    <!-- Explanation message displayed in a dialog for saving a signature, drawing, or annotated image -->\n    <string name=\"save_drawing_explanation\">Do you want to save changes before you exit?</string>\n    <string name=\"save_and_close\">Save and Close</string>\n    <string name=\"signature_capture\">Signature Capture</string>\n    <string name=\"sign_button\">Gather Signature</string>\n    <string name=\"markup_image\">Markup Image</string>\n    <string name=\"draw_image\">Sketch Image</string>\n    <string name=\"reset_image\">Reset</string>\n    <string name=\"set_color\">Set Color</string>\n\n    <string name=\"capture_video\">Record Video</string>\n    <string name=\"choose_video\">Choose Video</string>\n    <string name=\"play_video\">Play Video</string>\n\n    <string name=\"choose_file\">Choose File</string>\n\n    <!-- Text for button that deletes a file that is the answer to a question -->\n    <string name=\"delete_answer_file\">Delete</string>\n\n    <!-- Text for button that starts recording sound -->\n    <string name=\"capture_audio\">Record Sound</string>\n\n    <!-- Text for button that lets the user choose a sound file on their device -->\n    <string name=\"choose_sound\">Choose Sound</string>\n\n    <!-- Description text for button that that pauses recording sound -->\n    <string name=\"pause_recording\">Pause</string>\n\n    <!-- Description text for button that that resumes recording sound -->\n    <string name=\"resume_recording\">Resume</string>\n\n    <!-- Displayed in the form filling menu when a form has background audio recording enabled -->\n    <string name=\"record_audio_on\">Record audio: on</string>\n\n    <!-- Displayed in the form filling menu when a form has background audio recording disabled -->\n    <string name=\"record_audio_off\">Record audio: off</string>\n\n    <string name=\"background_audio_permission_explanation\">This form records audio in the background. You must grant permission to use your microphone. If you do not, you will not be able to open this form.</string>\n\n    <!-- Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file) -->\n    <string name=\"stop_recording_confirmation\">This form requests background audio recording. Disabling it will stop recording and discard existing audio. Are you sure you want to proceed?</string>\n\n    <!-- Button text for the dialog confirming whether the user wants to disable background recording -->\n    <string name=\"disable_recording\">Disable recording</string>\n\n    <!-- Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form -->\n    <string name=\"background_audio_recording_enabled_explanation\">Recording will not begin immediately. You must re-open the form to record.</string>\n\n    <!-- Title for dialog asking the user if they want to delete a file -->\n    <string name=\"delete_answer_file_question\">Delete file?</string>\n\n    <!-- Dialog text warning the user they won't be able to recover a file once deleted -->\n    <string name=\"answer_file_delete_warning\">You will not be able to recover this file once it is deleted. Are you sure you want to proceed?</string>\n\n    <!-- Text for button that stops audio recording  -->\n    <string name=\"stop_recording\">Stop</string>\n    <!-- Text to let the user know a recording is in progress  -->\n    <string name=\"recording\">Recording…</string>\n    <!-- Title of Android Notification Channel for audio recording notifications  -->\n    <string name=\"recording_channel\">Audio recording</string>\n    <!-- Text for button that plays audio clip  -->\n    <string name=\"play_audio\">Play audio</string>\n    <!-- Text to let the user know a file is being saved  -->\n    <string name=\"saving_file\">Saving file</string>\n    <!-- Text in dialog that instructs the user to stop recording audio before leaving the form -->\n    <string name=\"recording_warning\">You must stop recording before leaving this screen.</string>\n\n    <string name=\"error_attaching_binary_file\">An error occurred while attaching the media: %s</string>\n\n    <!-- Displayed in form questions for which the user needs to select a value -->\n    <string name=\"select_value\">Select value</string>\n    <string name=\"select_answer\">Select Answer</string>\n    <!-- Displayed in form questions for which the user needs to select a value -->\n    <string name=\"edit_value\">Edit value</string>\n    <!-- Displayed in form questions for which the user needs to select a value -->\n    <string name=\"no_value_selected\">No value selected</string>\n    <string name=\"selected_title\">Selected: </string>\n\n    <string name=\"trigger\">OK. Please continue.</string>\n\n    <string name=\"ext_file_no_data_error\">The file contains no data!</string>\n    <string name=\"ext_conflicting_columns_error\">Columns %s match!</string>\n    <string name=\"ext_sortBy_numeric_error\">Your sortby column should contain only numeric values. Conflicting value was \\'%s\\'.</string>\n    <string name=\"ext_search_wrong_arguments_error\">Syntax error in search() function: The function needs 1, 4 or 6 arguments.</string>\n    <string name=\"ext_search_wrong_function_error\">Syntax error in search() function : Unrecognised function \\'%s\\'.</string>\n    <string name=\"ext_search_bad_function_error\">Syntax error in search() function: \\'%s\\' was not evaluated as a function.</string>\n    <string name=\"ext_search_return_error\">The search handler returned a object of type \\'%s\\'.</string>\n    <string name=\"ext_import_generic_error\">Could not import data from %1$s. Reason: %2$s</string>\n    <string name=\"ext_import_progress_message\">Pre-loading data from \\'%1$s\\', please wait… %2$s</string>\n    <string name=\"ext_import_cancelled_message\">Reading data canceled!</string>\n    <string name=\"ext_import_finalizing_message\">Finalizing pre-loaded data…</string>\n    <string name=\"ext_import_completed_message\">Reading data completed!</string>\n    <string name=\"ext_not_initialized_error\">The ExternalDataManager has not been initialized.</string>\n    <string name=\"ext_import_csv_missing_error\">External data for %1$s has not been imported. Perhaps you forgot to include the %2$s.csv file with your form?</string>\n    <string name=\"ext_search_generic_error\">Syntax error in search() function: %s</string>\n    <string name=\"ext_assign_value_error\">Cannot assign the value at \\'%s\\'.</string>\n\n    <string name=\"parser_exception\">XPathParser Exception: \\\"%s\\\"</string>\n\n    <string name=\"get_bearing\">Record Bearing</string>\n    <string name=\"replace_bearing\">Replace Bearing</string>\n    <string name=\"getting_bearing\">Loading Bearing</string>\n    <string name=\"accept_bearing\">Record Bearing</string>\n    <string name=\"bearing\">Bearing: %.3f</string>\n    <string name=\"direction\">Direction: %s</string>\n    <string name=\"bearing_lack_of_sensors\">Bearing cannot be collected: device is missing accelerometer, magnetic field sensor or both.</string>\n\n    <string name=\"select_date\">Select date</string>\n    <string name=\"select_time\">Select time</string>\n    <string name=\"no_date_selected\">No date selected</string>\n    <string name=\"no_time_selected\">No time selected</string>\n\n    <string name=\"ethiopian_month_1\">Meskerem</string>\n    <string name=\"ethiopian_month_2\">Tikimt</string>\n    <string name=\"ethiopian_month_3\">Hidar</string>\n    <string name=\"ethiopian_month_4\">Tahsas</string>\n    <string name=\"ethiopian_month_5\">Tir</string>\n    <string name=\"ethiopian_month_6\">Yekatit</string>\n    <string name=\"ethiopian_month_7\">Megabit</string>\n    <string name=\"ethiopian_month_8\">Miazia</string>\n    <string name=\"ethiopian_month_9\">Ginbot</string>\n    <string name=\"ethiopian_month_10\">Senie</string>\n    <string name=\"ethiopian_month_11\">Hamlie</string>\n    <string name=\"ethiopian_month_12\">Nehasie</string>\n    <string name=\"ethiopian_month_13\">Pagumien</string>\n    <string name=\"coptic_month_1\">Thout</string>\n    <string name=\"coptic_month_2\">Paopi</string>\n    <string name=\"coptic_month_3\">Hathor</string>\n    <string name=\"coptic_month_4\">Koiak</string>\n    <string name=\"coptic_month_5\">Tobi</string>\n    <string name=\"coptic_month_6\">Meshir</string>\n    <string name=\"coptic_month_7\">Paremhat</string>\n    <string name=\"coptic_month_8\">Parmouti</string>\n    <string name=\"coptic_month_9\">Pashons</string>\n    <string name=\"coptic_month_10\">Paoni</string>\n    <string name=\"coptic_month_11\">Epip</string>\n    <string name=\"coptic_month_12\">Mesori</string>\n    <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n    <string name=\"islamic_month_1\">Muharram</string>\n    <string name=\"islamic_month_2\">Safar</string>\n    <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n    <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n    <string name=\"islamic_month_5\">Jumada al-awwal</string>\n    <string name=\"islamic_month_6\">Jumada al-thani</string>\n    <string name=\"islamic_month_7\">Rajab</string>\n    <string name=\"islamic_month_8\">Sha\\'ban</string>\n    <string name=\"islamic_month_9\">Ramadan</string>\n    <string name=\"islamic_month_10\">Shawwal</string>\n    <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n    <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n    <string name=\"persian_month_1\">Farvardin</string>\n    <string name=\"persian_month_2\">Ordibehesht</string>\n    <string name=\"persian_month_3\">Khordad</string>\n    <string name=\"persian_month_4\">Tir</string>\n    <string name=\"persian_month_5\">Mordad</string>\n    <string name=\"persian_month_6\">Shahrivar</string>\n    <string name=\"persian_month_7\">Mehr</string>\n    <string name=\"persian_month_8\">Aban</string>\n    <string name=\"persian_month_9\">Azar</string>\n    <string name=\"persian_month_10\">Dey</string>\n    <string name=\"persian_month_11\">Bahman</string>\n    <string name=\"persian_month_12\">Esfand</string>\n\n    <!-- 1st month in the Buddhist calendar -->\n    <string name=\"buddhist_month_1\">มกราคม</string>\n    <!-- 2nd month in the Buddhist calendar -->\n    <string name=\"buddhist_month_2\">กุมภาพันธ์</string>\n    <!-- 3rd month in the Buddhist calendar -->\n    <string name=\"buddhist_month_3\">มีนาคม</string>\n    <!-- 4th month in the Buddhist calendar -->\n    <string name=\"buddhist_month_4\">เมษายน</string>\n    <!-- 5th month in the Buddhist calendar -->\n    <string name=\"buddhist_month_5\">พฤษภาคม</string>\n    <!-- 6th month in the Buddhist calendar -->\n    <string name=\"buddhist_month_6\">มิถุนายน</string>\n    <!-- 7th month in the Buddhist calendar -->\n    <string name=\"buddhist_month_7\">กรกฎาคม</string>\n    <!-- 8th month in the Buddhist calendar -->\n    <string name=\"buddhist_month_8\">สิงหาคม</string>\n    <!-- 9th month in the Buddhist calendar -->\n    <string name=\"buddhist_month_9\">กันยายน</string>\n    <!-- 10th month in the Buddhist calendar -->\n    <string name=\"buddhist_month_10\">ตุลาคม</string>\n    <!-- 11th month in the Buddhist calendar -->\n    <string name=\"buddhist_month_11\">พฤศจิกายน</string>\n    <!-- 12th month in the Buddhist calendar -->\n    <string name=\"buddhist_month_12\">ธันวาคม</string>\n\n    <!-- Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order -->\n    <string name=\"custom_date\">%1$s (%2$s)</string>\n\n    <!-- Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid -->\n    <string name=\"file_invalid\">File: %s is invalid.</string>\n    <!-- Displayed when a form media file such as an image for a question label or an audio file for a choice is missing -->\n    <string name=\"file_missing\">File: %s is missing.</string>\n\n    <!-- Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification. -->\n    <string name=\"enter_identity\">Enter identity</string>\n    <!-- Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification. -->\n    <string name=\"identity\">Identity</string>\n    <!-- Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification. -->\n    <string name=\"enter_identity_description\">This form requires you to identify yourself</string>\n    <!-- Action to close a full screen dialog -->\n    <string name=\"close\">Close</string>\n\n    <!-- Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking. -->\n    <string name=\"reason_for_changes\">Reason for changes</string>\n    <!-- Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking. -->\n    <string name=\"reason\">Reason</string>\n    <!-- Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking. -->\n    <string name=\"reason_for_changes_description\">You need to explain the reason for changes to this form</string>\n    <!-- Action to save the contents of a full screen dialog -->\n    <string name=\"save\">Save</string>\n\n    <!-- Button text to select a place from a list presented on a map -->\n    <string name=\"select_place\">Select place</string>\n    <string name=\"choices\">Choices</string>\n\n    <!--\n    ##############################################\n    # GEO\n    ##############################################\n    -->\n    <!--\n   TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.\n    -->\n    <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n    <string name=\"openmap_usgs_sat\">USGS National Map Hybrid</string>\n    <string name=\"openmap_usgs_img\">USGS National Map Imagery</string>\n    <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n    <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n\n    <string name=\"basemap\">Basemap</string>\n    <string name=\"basemap_source\">Source</string>\n    <string name=\"basemap_source_unavailable\">Sorry, %s basemaps are not available on this device. Please choose another basemap in Settings &gt; Maps.</string>\n    <string name=\"map_style_label\">%s map style</string>\n\n    <string name=\"streets\">Streets</string>\n    <string name=\"terrain\">Terrain</string>\n    <string name=\"hybrid\">Hybrid</string>\n    <string name=\"satellite\">Satellite</string>\n    <string name=\"light\" tools:ignore=\"UnusedResources\">Light</string>\n    <string name=\"dark\" tools:ignore=\"UnusedResources\">Dark</string>\n    <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Outdoors</string>\n    <string name=\"topographic\">Topographic</string>\n    <string name=\"google_play_services_error_occured\">Unable to access Google Maps. Is Google Play Services installed?</string>\n\n    <string name=\"carto_map_style_positron\">Positron</string>\n    <string name=\"carto_map_style_dark_matter\">Dark Matter</string>\n\n    <!-- Title for the offline layer section of settings-->\n    <string name=\"offline_layer\">Offline layers</string>\n\n    <!-- Label for the offline layer setting -->\n    <string name=\"layer_data_file\">Layer</string>\n\n    <!-- Title for the screen that allows the user to select a offline layer for maps -->\n    <string name=\"offline_layer_dialog_title\">Select offline layer</string>\n\n    <!-- Description of how to add offline layers to the project -->\n    <string name=\"offline_layer_dialog_description\">Select the offline layer to use for all maps in this project. You can add options to the list by selecting an MBTiles file from your device and also delete existing layers from the list.</string>\n\n    <!-- Text of button user can press to get help when setting up Offline Layers -->\n    <string name=\"get_help_with_offline_layers\">Learn more about adding MBTiles.</string>\n\n    <!-- Text for button that lets the user select a offline layer on their device to add -->\n    <string name=\"add_layer\">Add layers</string>\n\n    <!-- Text for button that lets the user delete a offline layer -->\n    <string name=\"delete_layer\">Delete layer</string>\n\n    <!-- Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported -->\n    <plurals name=\"non_mbtiles_files_selected_title\">\n        <item quantity=\"one\">%d layer can\\'t be added</item>\n        <item quantity=\"other\">%d layers can\\'t be added</item>\n    </plurals>\n\n    <!-- Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported -->\n    <string name=\"all_non_mbtiles_files_selected_message\">The files you selected are not MBTiles. You can only add MBTiles files.</string>\n\n    <!-- Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported -->\n    <string name=\"some_non_mbtiles_files_selected_message\">Some of the files you selected are not MBTiles. You can only add MBTiles files.</string>\n\n    <!-- Text for title above list of layers -->\n    <string name=\"layers_list_title\">Layers</string>\n\n    <!-- Text for title and subtitle above options for how the layer can be access -->\n    <string name=\"select_layer_access_title\">Select layers access</string>\n    <string name=\"select_layer_access_subtitle\">Do you want the layers available in all projects or your current project only?</string>\n\n    <!-- Text for option where layer should be available to all projects -->\n    <string name=\"all_projects_option\">All projects</string>\n\n    <!-- Text for option where layer should be available to the current project only -->\n    <string name=\"current_project_option\">Current project only</string>\n\n    <!-- Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list -->\n    <string name=\"delete_layer_confirmation_message\">Are you sure you want to delete the %1$s offline layer?</string>\n\n    <string name=\"record_geopoint\">Record a point</string>\n    <string name=\"location_accuracy\">Accuracy: %1$s</string>\n    <string name=\"location_accuracy_unacceptable\">⚠️ Accuracy: %1$s (unacceptable)</string>\n    <string name=\"provider_disabled_error\">Sorry, Location providers are disabled!</string>\n    <string name=\"gps_result\">Latitude: %1$s\\nLongitude: %2$s\\nAltitude: %3$s m\\nAccuracy: %4$s m</string>\n    <string name=\"google_play_services_not_available\">This form wants to track your location but Google Play Services are not available.</string>\n    <string name=\"location_providers_disabled_dialog_message\">This form wants to track your location but location providers are disabled. Please enable in Android settings.</string>\n    <!-- Action to go to Android settings. Displayed when location providers are disabled. -->\n    <string name=\"go_to_settings\">Go to settings</string>\n    <!-- Displayed in the form filling menu when a form has background location enabled -->\n    <string name=\"track_location_on\">Track location: on</string>\n    <!-- Displayed in the form filling menu when a form has background location disabled -->\n    <string name=\"track_location_off\">Track location: off</string>\n    <string name=\"background_location_disabled\">This form wants to track your location but tracking is disabled. Please enable in the %1$s menu above.</string>\n    <string name=\"background_location_enabled\">This form tracks your location. You can disable tracking in the %1$s menu above.</string>\n\n    <!-- Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error. -->\n    <string name=\"validate\">Check for errors</string>\n\n    <!-- %1$s is the time taken so far in hh:mm:ss -->\n    <string name=\"time_elapsed\">Time elapsed: %1$s</string>\n\n    <!-- %1$s is the accuracy required for a point to be automatically saved-->\n    <string name=\"point_will_be_saved\">Point will be saved at %1$s</string>\n\n    <!-- %1$s is the number of satellites being used to get location -->\n    <string name=\"satellites\">Satellites: %1$s</string>\n\n    <string name=\"waiting_for_location\">Trying to get location. Please wait.</string>\n    <string name=\"poor_accuracy\">Poor accuracy. Please wait.</string>\n    <string name=\"unacceptable_accuracy\">Unacceptable accuracy. Please wait.</string>\n    <string name=\"improving_accuracy\">Improving accuracy. Please wait.</string>\n    <string name=\"accuracy_status_title\">Getting Location</string>\n\n    <!-- The accuracy of a location fix in meters -->\n    <string name=\"accuracy_m\">%1$s m</string>\n\n    <!-- %1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\" -->\n    <string name=\"west\">W %1$s%2$s%3$s</string>\n    <!-- %1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\" -->\n    <string name=\"east\">E %1$s%2$s%3$s</string>\n    <!-- %1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\" -->\n    <string name=\"south\">S %1$s%2$s%3$s</string>\n    <!-- %1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\" -->\n    <string name=\"north\">N %1$s%2$s%3$s</string>\n\n    <string name=\"input_method\">Input method</string>\n    <string name=\"start\">Start</string>\n    <string name=\"enable_gps\">Enable GPS</string>\n    <string name=\"gps_enable_message\">GPS is disabled on your device. Would you like to enable it?</string>\n    <string name=\"geo_clear_warning\">Feature already created. Would you like to clear the feature?</string>\n    <string name=\"geo_exit_warning\">Discard changes and return to ODK?</string>\n    <string name=\"polygon_validator\">Must have at least 3 points to create Polygon</string>\n    <string name=\"polyline_validator\">Must have at least 2 points to create Polyline</string>\n    <string name=\"clear\">Clear</string>\n    <string name=\"placement_mode\">Placement by tapping</string>\n    <string name=\"manual_mode\">Manual location recording</string>\n    <string name=\"automatic_mode\">Automatic location recording</string>\n    <string name=\"recording_interval\">Recording interval:</string>\n    <!-- Used to display choices for location recording interval in geotrace and geoshape questions -->\n    <plurals name=\"number_of_seconds\">\n        <item quantity=\"one\">%d second</item>\n        <item quantity=\"other\">%d seconds</item>\n    </plurals>\n    <!-- Used to display choices for location recording interval in geotrace and geoshape questions -->\n    <plurals name=\"number_of_minutes\">\n        <item quantity=\"one\">%d minute</item>\n        <item quantity=\"other\">%d minutes</item>\n    </plurals>\n    <string name=\"accuracy_requirement\">Accuracy requirement:</string>\n    <string name=\"none\">None</string>\n    <!-- Used to display choices for accuracy requirements in geotrace and geoshape questions -->\n    <plurals name=\"number_of_meters\">\n        <item quantity=\"one\">%d meter</item>\n        <item quantity=\"other\">%d meters</item>\n    </plurals>\n    <string name=\"collection_status_paused\">Points entered: %d</string>\n    <string name=\"collection_status_placement\">Points entered: %d (tap to place points)</string>\n    <string name=\"collection_status_manual\">Points entered: %d (manual recording)</string>\n    <string name=\"collection_status_auto_seconds\">Points entered: %1$d (recording every %2$d sec)</string>\n    <string name=\"collection_status_auto_minutes\">Points entered: %1$d (recording every %2$d min)</string>\n    <string name=\"collection_status_auto_seconds_accuracy\">Points entered: %1$d (recording every %2$d sec, accuracy %3$d m)</string>\n    <string name=\"collection_status_auto_minutes_accuracy\">Points entered: %1$d (recording every %2$d min, accuracy %3$d m)</string>\n    <string name=\"geopoint_instruction\">Long press to place mark or tap add marker button</string>\n    <string name=\"geopoint_no_draggable_instruction\">Tap add marker button.</string>\n    <string name=\"discard\">Discard</string>\n    <string name=\"select_item_count\">%s: %d (%d shown on map)</string>\n    <string name=\"select_item\">Select</string>\n    <string name=\"new_item\">New item</string>\n    <!-- Name of an action button -->\n    <string name=\"edit_data\">Edit Draft</string>\n    <string name=\"view_data\">View Saved Form</string>\n\n    <!-- Name for the location tracking notifications. Will be seen in Android settings. -->\n    <string name=\"location_tracking_notification_channel_name\">Location tracking</string>\n\n    <!-- Title for notification that lets user know their location is currently being tracked -->\n    <string name=\"location_tracking_notification_title\">Tracking location…</string>\n\n    <!-- Description for button that zooms to the user's current location on a map -->\n    <string name=\"show_my_location\">Show my location</string>\n\n    <!-- Description for button that zooms a map so that it shows all items -->\n    <string name=\"zoom_to_fit_all\">Zoom to fit all</string>\n\n    <!-- Description for button that pauses automatic location recording -->\n    <string name=\"pause_location_recording\">Pause</string>\n\n    <!-- Description for button that removes the last recorded point -->\n    <string name=\"remove_last_point\">Remove last point</string>\n\n    <!-- Message shown when an error with the trace/shape has been fixed  -->\n    <string name=\"error_fixed\">✅ Error fixed</string>\n\n    <!-- Description for button that explains how to modify the map -->\n    <string name=\"show_how_to_modify_map\">Show how to modify the map</string>\n\n    <!-- Title for instructions on how to modify map features -->\n    <string name=\"how_to_modify_map\">How to modify the map</string>\n\n    <!-- Instruction in a list on how to move a point -->\n    <string name=\"long_press_to_move_point_info_item\">Long press to move point</string>\n\n    <!-- Instruction in a list on how to remove the most recently added point -->\n    <string name=\"remove_last_point_info_item\">Remove last point</string>\n\n    <!-- Instruction in a list on how to delete the current shape to start over -->\n    <string name=\"delete_shape_to_start_over_info_item\">Delete shape to start over</string>\n\n    <!-- Instruction in a list on how to add a new point -->\n    <string name=\"add_point_info_item\">Add point</string>\n\n    <!-- Instruction in a list on how to tap to add a point -->\n    <string name=\"tap_to_add_a_point_info_item\">Tap to add a point</string>\n\n    <!-- Instruction in a list on how to delete the entire shape -->\n    <string name=\"delete_entire_shape_info_item\">Delete entire shape</string>\n\n    <!-- Button text to close the instructions dialog -->\n    <string name=\"done\">Done</string>\n\n    <!-- Button text to open the instructions dialog from the error snackbar -->\n    <string name=\"how_to_modify\">How to modify</string>\n\n    <!--\n    ##############################################\n    # SEND FINALIZED FORM\n    ##############################################\n    -->\n    <string name=\"uploading_data\">Sending Form</string>\n\n    <string name=\"noselect_error\">Sorry, you have not selected any forms!</string>\n    <string name=\"no_connection\">No network connection available</string>\n\n    <string name=\"sending_items\">Sending %1$s of %2$s form(s)</string>\n    <string name=\"send_selected_data\">Send Selected</string>\n\n    <string name=\"server_auth_credentials\">Invalid username or password for server: %s</string>\n    <string name=\"server_requires_auth\">Server Requires Authentication</string>\n\n    <string name=\"no_forms_uploaded\">Sorry, no form was uploaded.</string>\n\n    <string name=\"upload_results\">Upload Results</string>\n\n    <!-- Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms -->\n    <string name=\"change_view\">Change View</string>\n    <string name=\"show_sent_and_unsent_forms\">Show Sent and Unsent Forms</string>\n    <string name=\"show_unsent_forms\">Show Unsent Forms</string>\n\n    <string name=\"send_in_progress\">Background send running, please try again shortly</string>\n\n    <string name=\"forms_upload_succeeded\">Forms upload succeeded</string>\n    <string name=\"forms_upload_failed\">Forms upload failed</string>\n    <string name=\"all_uploads_succeeded\">All uploads succeeded!</string>\n    <string name=\"some_uploads_failed\">%1$s of %2$s uploads failed!</string>\n\n    <!--\n    ##############################################\n    # GET BLANK FORM\n    ##############################################\n    -->\n    <string name=\"refresh\">Refresh</string>\n    <string name=\"download\">Get Selected</string>\n\n    <string name=\"downloading_data\">Connecting to Server</string>\n\n    <string name=\"fetching_file\">Getting %1$s.\\n\\nForm %2$s of %3$s form(s).</string>\n    <string name=\"form_download_progress\">%1$s. Getting media files: %2$s of %3$s</string>\n\n    <string name=\"version\">Version:</string>\n\n    <string name=\"form_updates_available\">Form updates available</string>\n    <string name=\"newer_version_of_a_form_info\">This is an update to a form you have</string>\n    <string name=\"form_update_error\">Form update failed</string>\n    <string name=\"form_update_error_massage\">Open ODK Collect to try again</string>\n\n    <!-- Text for notification shown while syncing forms with the server -->\n    <string name=\"form_update_notification_title\">Syncing forms</string>\n\n    <string name=\"load_remote_form_error\">Form list download failed</string>\n\n    <string name=\"forms_download_succeeded\">Forms download succeeded</string>\n    <string name=\"forms_download_failed\">Forms download failed</string>\n    <string name=\"all_downloads_succeeded\">All downloads succeeded!</string>\n    <string name=\"some_downloads_failed\">%1$s of %2$s downloads failed!</string>\n    <!-- Part of form download dialog. Clicking it displays additional information about a form that failed -->\n    <string name=\"form_details\">ID: %1$s Version: %2$s</string>\n\n    <!--\n    ##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################\n    -->\n\n    <string name=\"report_to_project_lead\">If you keep having this problem, report it to the person who asked you to collect data.</string>\n\n    <!-- Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data -->\n    <string name=\"unreachable_error\">Collect can\\'t reach the server at %s. Did you enter the URL correctly?</string>\n\n    <!-- Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data -->\n    <string name=\"security_error\">Collect can\\'t connect securely to the server at %s. Did you enter the URL correctly?</string>\n\n    <!-- Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data -->\n    <string name=\"server_error\">The server %1$s returned status code %2$s.</string>\n\n    <!-- Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data -->\n    <string name=\"invalid_response\">The server %s provided an invalid response.</string>\n\n    <!-- Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data -->\n    <string name=\"form_with_no_hash_error\">The server did not provide a hash for this form.</string>\n\n    <!-- Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data -->\n    <string name=\"form_parsing_error\">This form cannot be processed:</string>\n\n    <!-- Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data -->\n    <string name=\"form_save_disk_error\">A disk error occurred on device while downloading this form.</string>\n\n    <!-- Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data -->\n    <string name=\"form_with_invalid_submission_error\">This form has an invalid submission URL.</string>\n\n    <!--\n    ##############################################\n    # DELETE SAVED FORM\n    ##############################################\n    -->\n    <!-- Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms) -->\n    <string name=\"data\">Saved Forms</string>\n    <!-- Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms) -->\n    <string name=\"forms\">Blank Forms</string>\n    <string name=\"delete_confirm\">Delete %s form(s)?</string>\n    <string name=\"delete_file\">Delete Selected</string>\n    <string name=\"delete_no\">Do Not Delete</string>\n    <string name=\"delete_yes\">Delete Forms</string>\n    <string name=\"form_delete_message\">Deleting selected forms</string>\n    <string name=\"file_deleted_ok\">%s form(s) successfully deleted!</string>\n\n    <!--\n    ##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################\n    -->\n    <string name=\"project_settings\">Project settings</string>\n\n    <!-- Title of a settings section -->\n    <string name=\"server_settings_title\">Server</string>\n    <!-- Subtext of a settings section -->\n    <string name=\"server_settings_subtext\">URL, username, password</string>\n    <!-- Title of a settings section -->\n    <string name=\"project_display_title\">Project display</string>\n    <!-- Subtext of a settings section -->\n    <string name=\"project_display_subtext\">Name, icon, color</string>\n    <!-- Title of a settings section -->\n    <string name=\"client\">User interface</string>\n    <!-- Subtext of a settings section -->\n    <string name=\"user_interface_settings_subtext\">App language, font size, navigation</string>\n    <!-- Title of a settings section -->\n    <string name=\"maps\">Maps</string>\n    <!-- Subtext of a settings section -->\n    <string name=\"maps_settings_subtext\">Basemap, styles, layers</string>\n    <!-- Title of a settings section -->\n    <string name=\"form_management_preferences\">Form management</string>\n    <!-- Subtext of a settings section -->\n    <string name=\"form_management_settings_subtext\">Auto-update, auto-send, auto-delete</string>\n    <!-- Title of a settings section -->\n    <string name=\"user_and_device_identity_title\">User and device identity</string>\n    <!-- Subtext of a settings section -->\n    <string name=\"user_and_device_identity_settings_subtext\">Username, phone number, device ID</string>\n    <!-- Title of a settings section only available in beta or development builds. -->\n    <string name=\"experimental\">Experimental</string>\n\n    <string name=\"protected_settings\">Protected</string>\n    <!-- Title of a settings section -->\n    <string name=\"unlock_protected_settings_section_title\">Unlock protected settings</string>\n    <!-- Subtext of a settings section -->\n    <string name=\"unlock_protected_settings_section_subtext\">Change password, set access controls</string>\n    <string name=\"set_admin_password\">Set admin password</string>\n    <!-- Title of a settings section -->\n    <string name=\"project_management_section_title\">Project management</string>\n    <!-- Subtext of a settings section -->\n    <string name=\"project_management_section_subtext\">Reconfigure, reset, delete</string>\n    <!-- Title of a settings section -->\n    <string name=\"access_control_section_title\">Access control</string>\n    <!-- Subtext of a settings section -->\n    <string name=\"access_control_section_subtext\">Limit user interface</string>\n\n    <string name=\"server_preferences\">Server Settings</string>\n    <!-- Label for server type setting -->\n    <string name=\"server_url\">URL</string>\n    <!-- Title of dialog for configuring server URL -->\n    <string name=\"change_server_url\">Server URL</string>\n    <string name=\"url_error\">Sorry, invalid URL!</string>\n    <string name=\"username\">Username</string>\n    <string name=\"change_username\">ODK Username</string>\n    <string name=\"password\">Password</string>\n    <string name=\"change_password\">ODK Password</string>\n    <string name=\"password_error_whitespace\">Leading or trailing whitespace not allowed in passwords</string>\n    <string name=\"username_error_whitespace\">Leading or trailing whitespace not allowed in usernames</string>\n\n    <string name=\"app_theme\">Theme</string>\n\n    <string name=\"language\">Language</string>\n    <string name=\"use_device_language\">Use device language</string>\n\n    <string name=\"font_size\">Text font size</string>\n    <string name=\"change_font_size\">Text font size</string>\n    <string name=\"font_size_extra_large\">Extra Large</string>\n    <string name=\"font_size_large\">Large</string>\n    <string name=\"font_size_medium\">Medium</string>\n    <string name=\"font_size_small\">Small</string>\n    <string name=\"font_size_extra_small\">Extra Small</string>\n\n    <string name=\"navigation\">Navigation</string>\n    <string name=\"swipe_navigation\">Use horizontal swipes</string>\n    <string name=\"buttons_navigation\">Use forward/backward buttons</string>\n    <string name=\"swipe_buttons_navigation\">Use swipes and buttons</string>\n    <!-- Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation. -->\n    <string name=\"form_forward\">Next</string>\n    <!-- Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation. -->\n    <string name=\"form_backward\">Back</string>\n    <!-- Heading for the settings category about updating blank forms -->\n    <string name=\"form_update_category\">Form update</string>\n    <!-- Name of the setting that defines how blank forms are updated from the server. -->\n    <string name=\"form_update_mode_title\">Blank form update mode</string>\n    <!-- One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms. -->\n    <string name=\"manual\">Manual</string>\n    <!-- One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date. -->\n    <string name=\"previously_downloaded_only\">Previously downloaded forms only</string>\n    <!-- One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed. -->\n    <string name=\"match_exactly\">Exactly match server</string>\n\n    <!-- Name of the setting that defines how often blank forms are updated from the server. -->\n    <string name=\"form_update_frequency_title\">Automatic update frequency</string>\n    <!-- Frequency for requesting blank form updates from the server -->\n    <string name=\"every_fifteen_minutes\">Every fifteen minutes</string>\n    <!-- Frequency for requesting blank form updates from the server -->\n    <string name=\"every_one_hour\">Every hour</string>\n    <!-- Frequency for requesting blank form updates from the server -->\n    <string name=\"every_six_hours\">Every six hours</string>\n    <!-- Frequency for requesting blank form updates from the server -->\n    <string name=\"every_24_hours\">Every 24 hours</string>\n    <!-- Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode -->\n    <string name=\"automatic_download\">Automatic download</string>\n    <string name=\"automatic_download_summary\">Automatically download updated versions of forms</string>\n    <string name=\"hide_old_form_versions_setting_title\">Hide old form versions</string>\n    <string name=\"hide_old_form_versions_setting_summary\">Only the newest version will appear in Fill Blank Form</string>\n\n    <string name=\"form_submission_category\">Form submission</string>\n    <string name=\"autosend_selector_title\">Auto send</string>\n    <string name=\"autosend\">Auto send</string>\n    <string name=\"off\">Off</string>\n    <string name=\"wifi_autosend\">Wifi only</string>\n    <string name=\"cellular_autosend\">Cellular only</string>\n    <string name=\"wifi_cellular_autosend\">Wifi or cellular</string>\n\n    <string name=\"delete_after_send\">Delete after send</string>\n    <string name=\"delete_after_send_summary\">Deletes finalized forms and media after sending to server</string>\n\n    <string name=\"form_filling_category\">Form filling</string>\n\n    <string name=\"constraint_behavior_title\">Constraint processing</string>\n    <string name=\"constraint_behavior\">Constraint processing behavior</string>\n    <string name=\"constraint_behavior_on_swipe\">Validate upon forward swipe</string>\n    <string name=\"constraint_behavior_on_finalize\">Defer validation until finalized</string>\n\n    <string name=\"high_resolution_title\">High res video</string>\n    <string name=\"high_resolution_summary\">Enable high-resolution video recordings</string>\n\n    <string name=\"image_size_title\">Image size</string>\n    <string name=\"image_size_dialog_title\">Maximum pixels of the long edge of the image</string>\n    <string name=\"image_size_large\">Large (3072px)</string>\n    <string name=\"image_size_medium\">Medium (2048px)</string>\n    <string name=\"image_size_small\">Small (1024px)</string>\n    <string name=\"image_size_very_small\">Very small (640px)</string>\n    <string name=\"image_size_original\">Original size from camera (default)</string>\n\n    <!-- Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts. -->\n    <string name=\"guidance_hint_title\">Show guidance for questions</string>\n    <string name=\"guidance_no\">No</string>\n    <string name=\"guidance_yes\">Yes - always shown</string>\n    <!-- Guidance hints can be displayed behind an icon that must be tapped to reveal the hint. -->\n    <string name=\"guidance_yes_collapsed\">Yes - collapsed</string>\n\n    <!-- Title for setting to let the user switch between using Collect or another app (on the device) for recording audio -->\n    <string name=\"external_app_recording\">Use external app for audio recording</string>\n\n    <string name=\"form_import_category\">Form import</string>\n    <string name=\"instance_sync\">Finalize forms on import</string>\n    <string name=\"instance_sync_summary\">Affects forms added directly to instances folder.</string>\n\n    <string name=\"analytics_preferences\">Usage data</string>\n    <string name=\"analytics\">Collect anonymous usage data</string>\n    <string name=\"analytics_summary\">Anonymous usage data helps the ODK team prioritize fixes and features.</string>\n\n    <string name=\"form_metadata_title\">Form Metadata</string>\n    <string name=\"form_metadata\">Form metadata</string>\n    <string name=\"form_metadata_summary\">These values will be added to forms that have username, email and/or telephone preloaded fields to identify the person submitting data.</string>\n    <string name=\"user_defined\">User-defined</string>\n    <string name=\"phone_number\">Phone number</string>\n    <string name=\"email\">Email address</string>\n    <string name=\"invalid_email_address\">Invalid email address!</string>\n    <string name=\"device_defined\">Device-defined</string>\n    <string name=\"device_id\">Device ID</string>\n    <!-- Displayed when a device property such as the phone number is not available -->\n    <string name=\"preference_not_available\">Not available</string>\n\n    <!-- Label for an item displayed in the project settings menu to unlock settings -->\n    <string name=\"unlock_settings\">Unlock settings</string>\n    <!-- Label for an item displayed in the project settings menu to indicate that settings are unlocked -->\n    <string name=\"settings_unlocked\">Settings unlocked</string>\n\n    <!-- Summary on setting that's currently not available because the user has entered settings from form entry -->\n    <string name=\"setting_not_available_during_form_entry\">Setting not available during form entry</string>\n\n    <!--\n     ##############################################\n     # PROTECTED SETTINGS\n     ##############################################\n     -->\n    <string name=\"enter_new_password\">Enter new password</string>\n    <string name=\"admin_password\">Admin Password</string>\n    <string name=\"admin_password_changed\">Admin password successfully changed</string>\n    <string name=\"admin_password_disabled\">Admin password disabled</string>\n    <string name=\"admin_password_incorrect\">Sorry, password is incorrect!</string>\n    <string name=\"enter_admin_password\">Enter Admin Password</string>\n    <string name=\"change_admin_password\">Change Admin Password</string>\n    <string name=\"show_password\">Show password</string>\n    <string name=\"found_in_main\">Uncheck to hide from Main Menu</string>\n    <string name=\"found_in_settings\">Uncheck to hide from Unprotected Settings</string>\n    <string name=\"form_entry\">Uncheck to hide from Form Entry</string>\n    <string name=\"form_end\">Uncheck to hide from Form End</string>\n\n    <string name=\"reset_project_settings_title\">Reset</string>\n    <string name=\"reset_project_settings_subtext\">Choose from settings, forms, data</string>\n    <string name=\"delete_project\">Delete</string>\n    <string name=\"select_all\">Select All</string>\n    <string name=\"clear_all\">Clear All</string>\n    <string name=\"reset_settings\">All settings (internal settings, saved settings)</string>\n    <string name=\"reset_saved_forms\">Saved forms (instances folder, instances database)</string>\n    <string name=\"reset_blank_forms\">Blank forms (forms folder, forms database, itemsets database)</string>\n    <string name=\"reset_layers\">Map layers (layers folder)</string>\n    <string name=\"reset_cache\">Form load cache (.cache folder)</string>\n    <string name=\"reset_settings_dialog_title\">Select what to reset</string>\n    <string name=\"reset_settings_button_reset\">Reset</string>\n    <string name=\"reset_in_progress\">Resetting…</string>\n    <string name=\"reset_app_state_result\">Reset results</string>\n    <string name=\"reset_app_state_warning\">All selected data will be deleted permanently. There is no undo.</string>\n    <string name=\"reset_settings_result\">All settings :: %s</string>\n    <string name=\"reset_saved_forms_result\">Saved forms :: %s</string>\n    <string name=\"reset_blank_forms_result\">Blank forms :: %s</string>\n    <string name=\"reset_layers_result\">Map layers :: %s</string>\n    <string name=\"reset_cache_result\">Form load cache :: %s</string>\n\n    <string name=\"main_menu_settings\">Main Menu Settings</string>\n    <string name=\"main_menu_settings_summary\">Show or hide buttons</string>\n    <string name=\"user_settings\">User Settings</string>\n    <string name=\"user_setting_summary\">Show or hide settings</string>\n    <string name=\"form_entry_setting\">Form Entry Settings</string>\n    <string name=\"form_entry_settings_summary\">Show or hide actions</string>\n\n    <!-- Name of a setting that determines whether users can navigate backwards in a form -->\n    <string name=\"moving_backwards_title\">Moving backwards</string>\n    <string name=\"moving_backwards_disabled_title\">Moving backwards disabled</string>\n    <string name=\"moving_backwards_disabled_message\">Configure the device to prevent users from bypassing this setting?\\n\\nThe changes are:\\n\\u2022 Disable Edit Draft\\n\\u2022 Disable Save as draft\\n\\u2022 Disable Go To Prompt\\n\\u2022 Set Constraint processing to Validate upon forward swipe</string>\n    <string name=\"yes\">Yes</string>\n    <string name=\"no\">No</string>\n    <string name=\"moving_backwards_enabled_title\">Moving backwards enabled</string>\n    <string name=\"moving_backwards_enabled_message\">You may wish to review the following settings:\\n\\n\\u2022 Edit Draft\\n\\u2022 Save as draft\\n\\u2022 Go To Prompt\\n\\u2022 Constraint processing</string>\n\n    <!-- Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end -->\n    <string name=\"save_mid\">Save as draft</string>\n    <string name=\"save_mid_summary\">Save icon in top bar and \\\"Save as draft\\\" button when exiting form</string>\n    <!--\n     ##############################################\n     # ABOUT\n     ##############################################\n     -->\n    <string name=\"about_preferences\">About</string>\n    <string name=\"tell_your_friends\">Share ODK Collect</string>\n    <string name=\"tell_your_friends_msg\">Are your colleagues still collecting data on paper? Share ODK Collect with them.</string>\n    <string name=\"leave_a_review\">Leave a Play Store review</string>\n    <string name=\"leave_a_review_msg\">Your (hopefully positive) review increases the visibility of the App in the Play Store.</string>\n    <string name=\"odk_website\">Visit the ODK website</string>\n    <string name=\"odk_website_summary\">ODK is used to collect data for social good in challenging environments.</string>\n    <string name=\"odk_forum\">Join the ODK forum</string>\n    <string name=\"odk_forum_summary\">Join the forum to get support, request features, contribute code/translations! </string>\n    <string name=\"all_open_source_licenses\">Open source libraries/licenses</string>\n    <string name=\"all_open_source_licenses_msg\">We stand on the shoulders of giants!</string>\n\n    <!--\n     ##############################################\n     # SETTINGS IMPORT\n     ##############################################\n     -->\n    <string name=\"reconfigure_with_qr_code_settings_title\">Reconfigure with QR code</string>\n    <string name=\"reconfigure_with_qr_code_settings_subtext\">Replace all existing settings</string>\n    <string name=\"qr_code\">QR code configuration</string>\n    <string name=\"scan_qr_code_fragment_title\">Scan</string>\n    <string name=\"view_qr_code_fragment_title\">QR Code</string>\n    <string name=\"import_qrcode_sd\">Import QR code</string>\n    <string name=\"successfully_imported_settings\">Settings imported successfully</string>\n    <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Maximum characters limit reached: QR code can\\'t be generated for all settings</string>\n    <string name=\"share\">Share</string>\n    <string name=\"turn_on_flashlight\">Turn on flashlight</string>\n    <string name=\"turn_off_flashlight\">Turn off flashlight</string>\n    <string name=\"qrcode_with_both_passwords\">Contains sensitive information: <b>admin</b> and <b>server passwords</b></string>\n    <string name=\"qrcode_without_passwords\">QR code does not contain <b>admin</b> or <b>server</b> passwords. Tap to configure</string>\n    <string name=\"qrcode_with_admin_password\">Contains sensitive information: <b>admin</b> password</string>\n    <string name=\"qrcode_with_server_password\">Contains sensitive information: <b>server</b> password</string>\n    <string name=\"server_password\">Server Password</string>\n    <string name=\"include_password_dialog\">Passwords Included in Code</string>\n    <string name=\"generate\">Generate</string>\n    <string name=\"invalid_qrcode\">QR code does not contain valid settings</string>\n    <string name=\"qr_code_not_found\">QR Code not found in the selected image</string>\n    <string name=\"corrupt_imported_preferences_error\">Current settings are corrupt. From project management settings, reset settings or import working ones.</string>\n    <string name=\"settings_with_gd_protocol\">Google Drive/Sheets projects can no longer be created</string>\n\n    <!--\n     ##############################################\n     # MISC\n     ##############################################\n     -->\n    <string name=\"ok\">OK</string>\n    <!-- Used in many dialogs to dismiss the dialog without taking any action -->\n    <string name=\"cancel\">Cancel</string>\n\n    <string name=\"cancel_loading_form\">Cancel</string>\n    <string name=\"cancel_location\">Cancel</string>\n    <string name=\"do_not_change\">Cancel</string>\n    <string name=\"canceling\">Canceling</string>\n\n    <string name=\"notification_channel_name\">General notifications</string>\n\n    <string name=\"success\">Success</string>\n    <string name=\"error_occured\">Error Occurred</string>\n\n    <string name=\"please_wait\">Please wait a few moments.</string>\n    <string name=\"please_wait_long\">Please wait. This could take a few minutes.</string>\n\n    <string name=\"fs_delete_media_path_if_file_error\">Could not delete \\'%s\\'. Please delete the file manually and re-download the form.</string>\n    <string name=\"fs_create_media_folder_error\">Could not create media folder \\'%s\\'.</string>\n    <string name=\"fs_file_copy_error\">Could not copy \\'%1$s\\' over \\'%2$s\\'. Reason: %3$s</string>\n\n    <string name=\"select_odk_shortcut\">Select ODK Shortcut</string>\n    <string name=\"background_audio_help\">This form records the audio from your device\\'s microphone.\\n\\nYou can use the volume indicator to make sure the microphone is close enough to the sound you need to record.\\n\\nTo stop the recording please exit this form.\\n\\nFor more information please contact the person who asked you to collect data.</string>\n\n    <string name=\"loading\">Loading…</string>\n\n    <!--\n     ##############################################\n     # Projects\n     ##############################################\n     -->\n    <string name=\"projects\">Projects</string>\n    <string name=\"settings\">Settings</string>\n    <string name=\"add_project\">Add project</string>\n    <string name=\"add\">Add</string>\n    <!-- Title of the dialog shown when attempting to create a project with connection settings that match an existing project -->\n    <string name=\"duplicate_project\">Duplicate project</string>\n    <!-- Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project -->\n    <string name=\"duplicate_project_details\">You already have a project with these connection settings. Do you want to switch to the existing project or add a new one?</string>\n    <!-- Button text for adding a duplicate project -->\n    <string name=\"add_duplicate_project\">Add duplicate project</string>\n    <!-- Buton text for switching to an existing project -->\n    <string name=\"switch_to_existing\">Switch to existing project</string>\n\n    <string name=\"project_name\">Project name</string>\n    <string name=\"project_icon\">Project icon</string>\n    <string name=\"project_color\">Project color</string>\n    <!-- Toast text shown when the current project is changed -->\n    <string name=\"switched_project\">Switched to %s</string>\n    <!-- Description for screen readers of which project is in use. This is part of the project selection dialog -->\n    <string name=\"using_project\">Using %s</string>\n    <!-- Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog -->\n    <string name=\"switch_to_project\">Switch to %s</string>\n    <!-- Text for the dialog title displayed when a user attempts to delete the current project -->\n    <string name=\"delete_project_dialog_title\">Delete %s</string>\n\n    <!-- Text that appears above the list of counts of different kinds of forms shown before deleting a project -->\n    <string name=\"delete_project_message\">By deleting this project, you will permanently delete the following</string>\n    <!-- Instructions telling the user they need to type a specific word to confirm deletion of the project -->\n    <string name=\"delete_project_instructions\">Type %1$s to confirm this action.</string>\n    <!-- The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching -->\n    <string name=\"delete_trigger\">delete</string>\n    <!-- The list item for form definition count in the project delete confirmation dialog -->\n    <string name=\"form_definitions_count\">Form definitions: %1$d</string>\n    <!-- The list item for sent count in the project delete confirmation dialog -->\n    <string name=\"sent_count\">Sent: %1$d</string>\n    <!-- The list item for unsent count in the project delete confirmation dialog -->\n    <string name=\"unsent_count\">Unsent: %1$d</string>\n    <!-- The list item for draft count in the project delete confirmation dialog -->\n    <string name=\"drafts_count\">Drafts: %1$d</string>\n\n    <!-- Text for the button that confirms deleting the project -->\n    <string name=\"delete_project_confirm_button_text\">Delete project</string>\n    <string name=\"or\">OR</string>\n    <string name=\"hex_color\">Hex color</string>\n    <string name=\"invalid_hex_code\">Invalid hex code</string>\n\n    <!--\n     ##############################################\n     # First launch and initial configuration\n     ##############################################\n    -->\n    <!-- Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language. -->\n    <string name=\"tagline\">Collect data\\nanywhere</string>\n    <!-- Text for the button that allows for configuring a project by scanning a QR code. Should be short -->\n    <string name=\"configure_with_qr_code\">Configure with QR code</string>\n    <!-- Text for the button that allows for manually entering project details. Should be short -->\n    <string name=\"configure_manually\">Manually enter project details</string>\n    <string name=\"project_config_tip\">After you add your project, you can configure it in Settings</string>\n    <!-- Immediately followed by \"Try Collect\" which is a link to create a demo project -->\n    <string name=\"dont_have_project\">Don\\'t have a project yet?</string>\n    <!-- Preceded by \"Don't have a project yet?\" question -->\n    <string name=\"try_demo\">Try a demo</string>\n    <string name=\"scan_configuration_qr_code\">Scan configuration QR code</string>\n\n    <!--\n    ##############################################\n    # Errors\n    ##############################################\n    -->\n    <string name=\"show_details\">Show details</string>\n    <string name=\"errors\">Errors</string>\n\n    <!-- Error shown when the user tries to open a web page from the app, but they don't have a browser installed -->\n    <string name=\"no_browser_installed\">No browser installed!</string>\n\n    <!--\n    ##############################################\n    # Form opening errors\n    ##############################################\n    -->\n    <!-- Error shown when a user tries to open a form that's in a different project -->\n    <string name=\"wrong_project_selected_for_form\">To open this form, you must first open ODK Collect and switch to the project that contains it.</string>\n    <!-- Error shown when a user tries to open a form when there are no projects -->\n    <string name=\"app_not_configured\">ODK Collect has not been configured. Try to open ODK Collect and configure it.\\n\\nIf you tapped on a shortcut, you might need to recreate it after configuring ODK Collect.</string>\n    <!-- Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown -->\n    <string name=\"unrecognized_uri\">Unrecognized URI: %s</string>\n    <!-- Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect -->\n    <string name=\"bad_uri\">Bad URI: %s</string>\n    <!-- Error displayed when opened filled form has been deleted on disk -->\n    <string name=\"instance_deleted_message\">Filled form has been deleted!</string>\n    <!-- Error shown when a user tries to edit a form but there is no corresponding blank form -->\n    <string name=\"parent_form_not_present\">Unable to edit this draft because the corresponding blank form is not present or was deleted.\\n\\nForm ID: %1$s</string>\n    <!-- Error shown when a user tries to open a form but there is more than one form with the same form ID -->\n    <string name=\"survey_multiple_forms_error\">You have downloaded two different forms with the same form ID and version. Maybe they were the same form uploaded at different times or to different servers. In any case, you should delete one.</string>\n    <!-- Error shown when a user tries to open a form when forms update is being applied in the background -->\n    <string name=\"cannot_open_form_because_of_forms_update\">This form cannot be opened right now because forms are being updated. Please try again shortly.</string>\n\n    <!--\n    ##############################################\n    # Permissions\n    ##############################################\n    -->\n    <string name=\"open_settings\">Open Settings</string>\n\n    ##############################################\n    # Entities\n    ##############################################\n\n    <!-- An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project. -->\n    <string name=\"entities_title\">Entity lists</string>\n\n    <!-- Text for button that leads to browser for the user to view and manage entity lists -->\n    <string name=\"entity_browser_button\">View entity lists</string>\n\n    <!-- Label for entity that has been created offline and is not on the server yet -->\n    <string name=\"offline\">Offline</string>\n\n    <!-- Error message displayed when a user opens a form with an unrecognized entity version -->\n    <string name=\"unrecognized_entity_version\">This form is not supported by this version of ODK Collect. Please upgrade Collect. If you keep having this problem, report it to the person who asked you to collect data.\\n\\nForm Entities spec version: %s</string>\n\n    <!--\n    ##############################################\n    # Crash handling\n    ##############################################\n    -->\n\n    <string name=\"crash_last_run\">There was a crash last time the app was running!</string>\n    <string name=\"cant_start_app\">Cannot start the app!</string>\n\n    <!--\n    ##############################################\n    # Google Drive deprecation\n    ##############################################\n    -->\n    <string name=\"google_drive_removed_message\">This project was previously connected to a Google Drive account. Google Drive support has been removed. You can configure a server or pull submissions to a computer.</string>\n    <string name=\"learn_more_button_text\">Learn more</string>\n\n    <!--\n    ##############################################\n    # Developer tools\n    ##############################################\n    -->\n    <!-- Section title in settings -->\n    <string name=\"dev_tools\">Developer tools</string>\n    <!-- Option provided in the Developer tools settings to force an application crash -->\n    <string name=\"crash_app\">Crash app</string>\n    <!-- An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close -->\n    <string name=\"crash_app_summary\">Force an uncaught exception causing the app to crash</string>\n\n    <!-- Option provided in the Developer tools settings to allow debugging filters during form entry -->\n    <string name=\"debug_filters\">Debug filters</string>\n    <string name=\"debug_filters_summary\">Show debug information for filter expression executions during form entry</string>\n\n    <!-- Option provided in the Experimental settings to revert to the old ZXing based barcode scanner -->\n    <string name=\"use_zxing_for_barcode_scanning\">Revert to older barcode scanner</string>\n\n    <string name=\"permission_dialog_title\">About permissions</string>\n    <string name=\"permission_dialog_text\">You will be asked to allow ODK Collect access to the features below, select “allow” if you want to use them.</string>\n    <string name=\"notifications\">Notifications</string>\n    <string name=\"required_for_notifications\">Required for showing updates when forms are downloaded, updated and sent.</string>\n\n    <!--\n    ##############################################\n    # Form saved Snackbar\n    ##############################################\n    -->\n    <string name=\"form_saved_as_draft\">Your form was saved as a draft.</string>\n    <string name=\"form_saved\">Your form was saved.</string>\n    <string name=\"form_sending\">Sending form…</string>\n    <string name=\"edit_form\">Edit</string>\n    <string name=\"view_form\">View</string>\n    <string name=\"close_snackbar\">Close snackbar</string>\n\n    <!--\n    ##############################################\n    # Forms ready to send banner\n    ##############################################\n    -->\n    <plurals name=\"last_form_sent_seconds_ago\">\n        <item quantity=\"one\">Last form sent: %d second ago</item>\n        <item quantity=\"other\">Last form sent: %d seconds ago</item>\n    </plurals>\n    <plurals name=\"last_form_sent_minutes_ago\">\n        <item quantity=\"one\">Last form sent: %d minute ago</item>\n        <item quantity=\"other\">Last form sent: %d minutes ago</item>\n    </plurals>\n    <plurals name=\"last_form_sent_hours_ago\">\n        <item quantity=\"one\">Last form sent: %d hour ago</item>\n        <item quantity=\"other\">Last form sent: %d hours ago</item>\n    </plurals>\n    <plurals name=\"last_form_sent_days_ago\">\n        <item quantity=\"one\">Last form sent: %d day ago</item>\n        <item quantity=\"other\">Last form sent: %d days ago</item>\n    </plurals>\n    <plurals name=\"forms_ready_to_send\">\n        <item quantity=\"one\">%d form ready to send</item>\n        <item quantity=\"other\">%d forms ready to send</item>\n    </plurals>\n\n    <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">In later versions, finalized forms will no longer be editable. Save forms as draft to edit them later.\\n\\nYou can check for errors in a draft form by tapping the three dots (⋮) and then Check for errors.</string>\n\n    <!-- Message showed on main menu to inform users that the ability to edit finalized forms is going away -->\n    <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">In later versions, finalized forms will no longer be editable.</string>\n\n    <!-- Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend -->\n    <string name=\"finalize_all_drafts\">Finalize all drafts</string>\n\n    <!-- Dialog title that asks if the user wants to finalize all drafts -->\n    <plurals name=\"bulk_finalize_confirmation\">\n        <item quantity=\"one\">Do you want to finalize %d draft?</item>\n        <item quantity=\"other\">Do you want to finalize %d drafts?</item>\n    </plurals>\n\n    <!-- Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel -->\n    <string name=\"bulk_finalize_explanation\">Once you finalize all drafts, they will be in \"Ready to send\" and you will not be able to make edits. Any drafts that have errors will not be finalized.\\n\\nYou will not be able to undo this action.</string>\n\n    <!-- Message shown after bulk finalization when finalizing all drafts has succeeded -->\n    <plurals name=\"bulk_finalize_success\">\n        <item quantity=\"one\">Success! %d draft finalized.</item>\n        <item quantity=\"other\">Success! %d drafts finalized.</item>\n    </plurals>\n\n    <!-- Message shown after bulk finalization when no drafts finalized successfully -->\n    <plurals name=\"bulk_finalize_failure\">\n        <item quantity=\"one\">%d draft has errors that must be addressed before finalizing.</item>\n        <item quantity=\"other\">%d drafts have errors that must be addressed before finalizing.</item>\n    </plurals>\n\n    <!-- Message shown after bulk finalization when some drafts finalize successfully and others fail -->\n    <string name=\"bulk_finalize_partial_success\">%d drafts finalized. %d drafts have errors that must be addressed before finalizing.</string>\n    <!-- Message shown when some drafts finalize successfully but others need to be finalized manually -->\n    <string name=\"bulk_finalize_unsupported\">%d drafts finalized. Drafts that are left need to be finalized manually.</string>\n\n    <!-- Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature. -->\n    <string name=\"draft_errors\">Errors</string>\n\n    <!-- Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature. -->\n    <string name=\"draft_no_errors\">No errors</string>\n\n    <!-- Title of section of features that the user can uncheck to hide from the Drafts screen -->\n    <string name=\"uncheck_to_hide_from_drafts\">Uncheck to hide from Drafts</string>\n\n    <!-- Message displayed when the list of questions is empty -->\n    <string name=\"empty_list_of_questions\">Nothing available to display</string>\n\n    <!-- Message displayed when the list of blank forms is empty -->\n    <string name=\"empty_list_of_blank_forms_title\">No blank forms</string>\n    <!-- Subtext displayed when the list of blank forms is empty -->\n    <string name=\"empty_list_of_blank_forms_subtitle\">Download form to get started</string>\n\n    <!-- Message displayed when the list of drafts is empty -->\n    <string name=\"empty_list_of_drafts_title\">Nothing in drafts</string>\n    <!-- Subtext displayed when the list of drafts is empty -->\n    <string name=\"empty_list_of_drafts_subtitle\">When you save as draft, forms will appear here</string>\n\n    <!-- Message displayed when the list of ready to send forms is empty -->\n    <string name=\"empty_list_of_finalized_forms_title\">Nothing ready to send</string>\n    <!-- Subtext displayed when the list of ready to send forms is empty -->\n    <string name=\"empty_list_of_finalized_forms_subtitle\">When you finalize drafts, they will appear here</string>\n\n    <!-- Message displayed when the list of sent forms is empty -->\n    <string name=\"empty_list_of_sent_forms_title\">Nothing in sent</string>\n    <!-- Subtext of message displayed when the list of sent forms is empty -->\n    <string name=\"empty_list_of_sent_forms_subtitle\">When you send finalized forms, they will appear here</string>\n\n    <!-- Message displayed when the list of forms to download is empty -->\n    <string name=\"empty_list_of_forms_to_download_title\">No forms to download</string>\n    <string name=\"empty_list_of_forms_to_download_subtitle\">Download form to get started</string>\n\n    <!-- Message displayed when the list of forms to delete is empty -->\n    <string name=\"empty_list_of_forms_to_delete_title\">No forms to delete</string>\n    <!-- Subtext displayed when the list of blank forms to delete is empty -->\n    <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">When you have downloaded blank forms, they will appear here</string>\n    <!-- Subtext displayed when the list of filled forms to delete is empty -->\n    <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">When you have saved forms, they will appear here</string>\n\n    <!--\n    ##############################################\n    # Savepoint recovery dialog\n    ##############################################\n    -->\n    <!-- Title displayed in a dialog before starting a form if there is a work to recover -->\n    <string name=\"savepoint_recovery_dialog_title\">Recover your work?</string>\n    <!-- Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n    <string name=\"savepoint_recovery_dialog_message\">\\'Collect closed unexpectedly on \\'EEE, MMM dd, yyyy \\'at\\' HH:mm\\' and saved your work.\\n\\nWould you like to recover or discard it?\\'</string>\n    <!-- Text for the button that accepts the recovery of the work -->\n    <string name=\"recover\">Recover</string>\n    <!-- Text for the button that declines the recovery of the work -->\n    <string name=\"do_not_recover\">Discard</string>\n\n    <!-- Text for the button that initiates getting user location -->\n    <string name=\"get_point\">Get point</string>\n    <!-- Text for the button that displays a map for viewing or changing an already existing location -->\n    <string name=\"view_or_change_point\">View or change point</string>\n    <!-- Text for the button that displays a map for viewing an already existing location -->\n    <string name=\"view_point\">View point</string>\n    <!-- Text for the button that initiates getting user location when one is already present -->\n    <string name=\"change_point\">Change point</string>\n    <!-- Text for the button that displays a map for getting a new line -->\n    <string name=\"get_line\">Get line</string>\n    <!-- Text for the button that displays a map for viewing or changing an already existing line -->\n    <string name=\"view_or_change_line\">View or change line</string>\n    <!-- Text for the button that displays a map for viewing an already existing line -->\n    <string name=\"view_line\">View line</string>\n    <!-- Text for the button that displays a map for getting a new polygon -->\n    <string name=\"get_polygon\">Get polygon</string>\n    <!-- Text for the button that displays a map for viewing or changing an already existing polygon -->\n    <string name=\"view_or_change_polygon\">View or change polygon</string>\n    <!-- Text for the button that displays a map for viewing an already existing polygon -->\n    <string name=\"view_polygon\">View polygon</string>\n    <!-- Text to describe a field for setting settings JSON in a mobile device management platform (MDM) -->\n    <string name=\"settings_json\">Settings JSON</string>\n    <!-- Text for the dialog that is displayed while the form is being prepared for editing -->\n    <string name=\"preparing_form_edit\">Preparing the form for editing…</string>\n    <!-- Instance name with a suffix indicating the edit number, if this is an edited instance -->\n    <string name=\"user_visible_instance_name\">%s (Edit %d)</string>\n    <!-- Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists -->\n    <string name=\"newer_draft_edit_found_dialog_title\">Continue editing draft?</string>\n    <!-- Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"newer_draft_edit_found_dialog_message\">\\'A draft edit was saved on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm\\'\\n\\nDo you want to resume this edit?\\'</string>\n    <!-- Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists -->\n    <string name=\"newer_draft_edit_found_dialog_positive_button\">Resume editing</string>\n    <!-- Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists -->\n    <string name=\"newer_finalized_edit_found_dialog_title\">Edit latest version?</string>\n    <!-- Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->\n    <string name=\"newer_finalized_edit_found_dialog_message\">\\'This submission was previously edited.\\n\\nWould you like to edit the latest version finalized on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm\\'?\\'</string>\n    <!-- Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists -->\n    <string name=\"newer_finalized_edit_found_dialog_positive_button\">Edit latest form</string>\n    <!-- Text for the snackbar shown when editing of a finalized form begins -->\n    <string name=\"finalized_form_edit_started\">You are now editing this form. Tap on a question to start editing.</string>\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values/untranslated.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Copyright (C) 2009 University of Washington Licensed under the Apache\nLicense, Version 2.0 (the \"License\"); you may not use this file except in\ncompliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software distributed\nunder the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES\nOR CONDITIONS OF ANY KIND, either express or implied. See the License for\nthe specific language governing permissions and limitations under the License. -->\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- strings that should not be translated -->\n    <string name=\"collect_app_name\" translatable=\"false\">ODK Collect</string>\n    <string name=\"app_url\" translatable=\"false\">https://getodk.org</string>\n    <string name=\"forum_url\" translatable=\"false\">\"https://forum.getodk.org\"</string>\n    <string name=\"never_value\" translatable=\"false\">never</string>\n    <string name=\"every_fifteen_minutes_value\" translatable=\"false\">every_fifteen_minutes</string>\n    <string name=\"every_one_hour_value\" translatable=\"false\">every_one_hour</string>\n    <string name=\"every_six_hours_value\" translatable=\"false\">every_six_hours</string>\n    <string name=\"every_24_hours_value\" translatable=\"false\">every_24_hours</string>\n\n    <!-- For consistency, we don't translate these brand/organization names. -->\n    <string name=\"basemap_source_google\" translatable=\"false\">Google</string>\n    <string name=\"basemap_source_mapbox\" translatable=\"false\">Mapbox</string>\n    <string name=\"basemap_source_osm\" translatable=\"false\">OpenStreetMap</string>\n    <string name=\"basemap_source_usgs\" translatable=\"false\">USGS</string>\n    <string name=\"basemap_source_carto\" translatable=\"false\">Carto</string>\n\n    <string name=\"hash\" translatable=\"false\">#</string>\n    <string name=\"hex\" translatable=\"false\">Hex</string>\n\n    <string name=\"empty_accuracy\" translatable=\"false\">--</string>\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-af/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Bygevoeg op\\' EEE, MMM dd, yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Gestoor op\\' EEE, MMM dd, yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Gefinaliseer op\\' EEE, MMM dd, yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Gestuur op\\' EEE, MMM dd, yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Stuur het misluk op\\' EEE, MMM dd, yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Verander op\\' EEE, MMM dd, yyyy \\'om\\' HH:mm</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Klaar ge-scan. Alle vorms is gelaai.</string>\n  <string name=\"xform_parse_error\">XForm verwerkingsfout. %1$s vermis element %2$s</string>\n  <string name=\"loading_form\">Besig om vorm te laai</string>\n  <string name=\"survey_loading_reading_form_message\">Lees vorm definisie…</string>\n  <string name=\"survey_loading_reading_data_message\">Lees opname data…</string>\n  <string name=\"survey_loading_reading_csv_message\">Lees CSV leêrs…</string>\n  <string name=\"parse_error\">Jammer, kon nie vorm verwerk nie.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Ongestoorde veranderinge is herstel vanaf stoor-punt!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Verander Taal</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Gaan Na Vraag</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Gaan na Begin</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Gaan na Einde</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Gaan op</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Wysig Vraag</string>\n  <string name=\"clearanswer_confirm\">Verwyer die antwoord op \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Verwyder antwoord</string>\n  <string name=\"clear_answer_ask\">Verwyder hierdie antwoord?</string>\n  <string name=\"clear_answer_no\">Kanselleer</string>\n  <string name=\"discard_answer\">Verwyder antwoord</string>\n  <string name=\"discard_group\">Verwyder groep</string>\n  <string name=\"delete_repeat\">Verwyder groep</string>\n  <string name=\"delete_repeat_ask\">Veryder Hierdie Groep?</string>\n  <string name=\"delete_repeat_confirm\">Verwyder groep \\\"%s\\\" en al sy sub-groepe?</string>\n  <string name=\"delete_repeat_no\">Kanselleer</string>\n  <string name=\"invalid_answer_error\">Jammer, hierdie antwoord is nie geldig nie!</string>\n  <string name=\"required_answer_error\">Jammer, hierdie antwoord word benodig!</string>\n  <string name=\"survey_internal_error\">Interne fout: aanstap na die vraag het misluk</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">U is aan die einde van %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Jammer, stoor van vorm het misluk!</string>\n  <string name=\"data_saved_ok\">Vorm suksesvol gestoor!</string>\n  <string name=\"save_point_error\">Fout tydens stoor van herstel-punt: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Stoor Veranderinge</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Besig om vorm te stoor</string>\n  <string name=\"survey_saving_validating_message\">Toets antwoorde…</string>\n  <string name=\"survey_saving_collecting_message\">Versamel data…</string>\n  <string name=\"survey_saving_saving_message\">Stoor na SD kaart…</string>\n  <string name=\"survey_saving_finalizing_message\">Finaliseer na SD kaart…</string>\n  <string name=\"survey_saving_encrypting_message\">Enkripteer data…</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Loods</string>\n  <string name=\"activity_not_found\">Geen aktiwiteit om te hanteer nie: %s</string>\n  <string name=\"no_app\">Die program of app wat u aangevra het word vermis. Tik asb self die lesing in.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Maak Url oop</string>\n  <string name=\"capture_osm\">Loods OpenMapKit</string>\n  <string name=\"recapture_osm\">Doen OSM-tagging oor</string>\n  <string name=\"get_barcode\">Lees Barcode In</string>\n  <string name=\"replace_barcode\">Vervang Barcode</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Neem foto</string>\n  <string name=\"choose_image\">Kies Prentjie</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Stoor en maak toe</string>\n  <string name=\"sign_button\">Verkry Handtekening</string>\n  <string name=\"markup_image\">Edit Prentjie</string>\n  <string name=\"draw_image\">Teken Prentjie</string>\n  <string name=\"reset_image\">Reset</string>\n  <string name=\"set_color\">Verstel Kleur</string>\n  <string name=\"capture_video\">Neem video</string>\n  <string name=\"choose_video\">Kies Video</string>\n  <string name=\"play_video\">Speel Video</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Neem klank op</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Kies Klank</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">\\'n Fout het gebeur tydens aanheg van media: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Kies Antwoord</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Gekies:</string>\n  <string name=\"trigger\">OK. Gaan asseblief voort.</string>\n  <string name=\"ext_file_no_data_error\">Die leêr het niks data nie!</string>\n  <string name=\"ext_conflicting_columns_error\">Kolomme %s pas!</string>\n  <string name=\"ext_sortBy_numeric_error\">U sortby kolom mag net numeriese waardes bevat. Probleemwaarde was \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Sintaks fout in search() funksie: Die funksie benodig 1, 4 of 6 velde (Eng:arguments).</string>\n  <string name=\"ext_search_wrong_function_error\">Sintaks fout in search() funksie: Herken nie funksie \\'%s\\' nie.</string>\n  <string name=\"ext_search_bad_function_error\">Sintaks fout in search() funksie: \\'%s\\' is nie as \\'n funksie ge-evalueer nie.</string>\n  <string name=\"ext_search_return_error\">Die search handler het \\'n voorwerp van tipe \\'%s\\' teruggestuur.</string>\n  <string name=\"ext_import_generic_error\">Kon nie data van %1$s intrek nie. Rede: %2$s</string>\n  <string name=\"ext_import_progress_message\">Laai data vanaf \\'%1$s\\', wag asseblief… %2$s</string>\n  <string name=\"ext_import_finalizing_message\">Besig om vooraf-gelaaide data te finaliseer…</string>\n  <string name=\"ext_import_completed_message\">Lees van data voltooi!</string>\n  <string name=\"ext_not_initialized_error\">Die ExternalDataManager is nie geïnisialiseer nie.</string>\n  <string name=\"ext_import_csv_missing_error\">Eksterne data vir %1$s is nie ingetrek nie. Miskien het u vergeet om die %2$s.csv leêr met u vorm in te sluit?</string>\n  <string name=\"ext_search_generic_error\">Sintaks fout in search() funksie: %s</string>\n  <string name=\"ext_assign_value_error\">Kan nie die waarde by \\'%s\\' toeken nie</string>\n  <string name=\"get_bearing\">Stoor rigting van beweging</string>\n  <string name=\"replace_bearing\">Vervang Rigting van beweging</string>\n  <string name=\"getting_bearing\">Besig om rigting van beweging te laai</string>\n  <string name=\"accept_bearing\">Stoor rigting van beweging</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Leêr: %s is ongeldig.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Leêr: %s word vermis.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Jammer, Posisie-verskaffers is afgeskakel!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Besig om vorm te stuur</string>\n  <string name=\"noselect_error\">Jammer, u het nie enige vorms geselekteer nie!</string>\n  <string name=\"no_connection\">Geen netwerk konneksie beskikbaar nie</string>\n  <string name=\"sending_items\">Besig om %1$s van %2$s vorm(s) te stuur</string>\n  <string name=\"send_selected_data\">Stuur Geselekteerde</string>\n  <string name=\"server_requires_auth\">Server Benodig Authentication</string>\n  <string name=\"no_forms_uploaded\">Jammer, geen vorm is opgelaai nie.</string>\n  <string name=\"upload_results\">Oplaai Resultate</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Verander Vertoning</string>\n  <string name=\"show_sent_and_unsent_forms\">Wys Gestuurde en Ongestuurde Vorms</string>\n  <string name=\"show_unsent_forms\">Wys Ongestuurde Vorms</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Verfris</string>\n  <string name=\"download\">Haal geselekteerdes</string>\n  <string name=\"downloading_data\">Besig om aan server te konnekteer</string>\n  <string name=\"fetching_file\">Besig om %1$s te haal.\\n\\nVorm %2$s van %3$s vorm(s).</string>\n  <string name=\"form_download_progress\">%1$s. Besig om media leërs te haal: %2$s van %3$s</string>\n  <string name=\"version\">Weergawe:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Gestoorde Vorms</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Leë Vorms</string>\n  <string name=\"delete_confirm\">Verwyder %s vorm(s)?</string>\n  <string name=\"delete_file\">Verwyder Geselekteerde</string>\n  <string name=\"delete_no\">Moenie Verwyder Nie</string>\n  <string name=\"delete_yes\">Verwyder Vorms</string>\n  <string name=\"file_deleted_ok\">%s vorm(s) suksesvol verwyder!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Server Verstellings</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Server URL</string>\n  <string name=\"url_error\">Jammer, ongeldige URL!</string>\n  <string name=\"username\">Gebruikersnaam</string>\n  <string name=\"change_username\">ODK Gebruikersnaam</string>\n  <string name=\"password\">Wagwoord</string>\n  <string name=\"change_password\">ODK Wagwoord</string>\n  <string name=\"font_size\">Teks font grootte</string>\n  <string name=\"change_font_size\">Teks font grootte</string>\n  <string name=\"font_size_extra_large\">Extra Large</string>\n  <string name=\"font_size_large\">Large</string>\n  <string name=\"font_size_medium\">Medium</string>\n  <string name=\"font_size_small\">Small</string>\n  <string name=\"font_size_extra_small\">Extra Small</string>\n  <string name=\"navigation\">Navigasie</string>\n  <string name=\"swipe_navigation\">Gebruik horisontale swipes</string>\n  <string name=\"buttons_navigation\">Gebruik vorentoe/agtertoe knoppies</string>\n  <string name=\"swipe_buttons_navigation\">Gebruik swipes en knoppies</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Volgende</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"delete_after_send\">Verwyder na stuur</string>\n  <string name=\"delete_after_send_summary\">Verwyder gefinaliseerde vorms en media nadat dit server toe gestuur is</string>\n  <string name=\"constraint_behavior\">Constraint proseseringsgedrag</string>\n  <string name=\"constraint_behavior_on_swipe\">Toets tydens vorentoe swipe</string>\n  <string name=\"constraint_behavior_on_finalize\">Stel toetsing uit tot finalisering</string>\n  <string name=\"high_resolution_summary\">Laat hoë-resolusie video opnames toe</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Voer nuwe wagwoord in</string>\n  <string name=\"admin_password_changed\">Admin wagwoord suksesvol verander</string>\n  <string name=\"admin_password_disabled\">Admin wagwoord afgeskakel.</string>\n  <string name=\"admin_password_incorrect\">Jammer, wagwoord is verkeerd!</string>\n  <string name=\"enter_admin_password\">Voer Admin wagwoord in</string>\n  <string name=\"found_in_main\">Deselekteer om van Hoofkieslys af te haal</string>\n  <string name=\"reset_project_settings_title\">Reset</string>\n  <string name=\"reset_settings_button_reset\">Reset</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Kanselleer</string>\n  <string name=\"cancel_loading_form\">Kanselleer</string>\n  <string name=\"cancel_location\">Kanselleer</string>\n  <string name=\"do_not_change\">Kanselleer</string>\n  <string name=\"success\">Sukses</string>\n  <string name=\"error_occured\">Daar was \\'n fout</string>\n  <string name=\"please_wait\">Wag asseblief \\'n paar sekondes.</string>\n  <string name=\"please_wait_long\">Wag asseblief. Hierdie kan dalk \\'n paar minute neem.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Kon nie \\'%s\\' verwyder nie. Verwyder self die leêr en laai die vorm weer af. </string>\n  <string name=\"fs_create_media_folder_error\">Kon nie media-folder \\'%s\\' skep nie</string>\n  <string name=\"fs_file_copy_error\">Kon nie \\'%1$s\\' oor \\'%2$s\\' skryf nie. Rede: %3$s</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">U het twee verskillende vorms afgelaai met dieselfde Vorm ID en weergawe. Miskien is dit dieselfde vorm wat of op verskillende tye of na verskillende servers opgelaai is. U moet in enige geval een verwyder.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-am/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">ሥሪት: 1 %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'ታክሏል በ\\' EEE፣ ወወወ ቀቀ፣ ዓዓዓዓ \\'በ\\' ሰሰ:ደደ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'የተመዘገበዉ በ\\' EEE፣ ወወወ ቀቀ ዓዓዓዓ \\'በ\\' ሰሰ:ደደ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'የተጠናቀቀዉ በ\\' EEE፣ ወወወ ቀቀ ዓዓዓዓ \\'በ\\' ሰሰ:ደደ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'የተላከወዉ በ\\' EEE፣ ወወወ ቀቀ ዓዓዓዓ \\'በ\\' ሰሰ:ደደ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'በመላክ ላይ ያልተሳካዉ በ\\'  EEE፣ ወወወ ቀቀ ዓዓዓዓ \\'በ\\' ሰሰ:ደደ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'የተሰረዘዉ በ\\' EEE፣ ወወወ ቀቀ ዓዓዓዓ \\'በ\\' ሰሰ:ደደ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"encrypted_form\">የተመሳጠረ ቅጽ</string>\n  <string name=\"deleted_form\">የተሰረዘ ቅጽ</string>\n  <string name=\"sort_by\">ቅደምተከተሉ የተስተካከለው</string>\n  <string name=\"sort_by_name_asc\">ስም፣ ሀ-ፐ</string>\n  <string name=\"sort_by_name_desc\">ስም፣ ፐ-ሀ</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">ፈልግ</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">ስካን ተጠናቋል፡፡ ሁሉም ቅጾች ተጭነዋል፡፡</string>\n  <string name=\"loading_form\">ቅጽ በመጫን ላይ</string>\n  <string name=\"survey_loading_reading_form_message\">የቅፅ ፍቺ በማንበብ ላይ …</string>\n  <string name=\"survey_loading_reading_data_message\">የዳሰሳ ጥናት ውሂብ በማንበብ ላይ …</string>\n  <string name=\"parse_error\">ይቅርታ፣ ቅፁን ለመተንተን አልተቻለም</string>\n  <string name=\"cannot_edit_completed_form\">ይህ ቅጽ እንደተጠናቀቀ ምልክት ከተደረገ በኋላ አርትዕ ሊደረግ አይችልም. ቅፁ ምናልባት የተመሳጠረ ሊሆን ይችላል፡፡</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">ያልተቀመጡ ለውጦች በጥንቃቄ ተመልሰዋል!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">ቋንቋ ይቀይሩ</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">ወደ መጀመሪያ ይሂዱ</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">ወደ መጨረሻ ይሂዱ</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">ወደ ላይ ውጣ</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"exit\">ውጣ</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"clearanswer_confirm\">የ \\\" 1 %s \\\" ምላሽ ይወግድ?</string>\n  <string name=\"clear_answer\">ምላሹን አስወግድ</string>\n  <string name=\"clear_answer_ask\">ይህ ምላሽ ይወገድ?</string>\n  <string name=\"clear_answer_no\">ሰርዝ</string>\n  <string name=\"discard_answer\">ምላሹን አስወግድ</string>\n  <string name=\"discard_group\">ቡድን አስወግድ</string>\n  <string name=\"delete_repeat\">ቡድን አስወግድ</string>\n  <string name=\"delete_repeat_ask\">ይህ ቡድን ይወገድ?</string>\n  <string name=\"delete_repeat_confirm\">ቡድን \\\" 1%s \\\" ን እና ሁሉንም ንዑስ ቡድኖቹን አስወግድ?</string>\n  <string name=\"delete_repeat_no\">ሰርዝ</string>\n  <string name=\"invalid_answer_error\">ይቅርታ፣ ይህ ምላሽ ልክ አይደለም!</string>\n  <string name=\"required_answer_error\">ይቅርታ፣ ይህ ምላሽ ያስፈልጋል!</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">በ 1 %s መጨረሻ ላይ ነዎት</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">ይቅርታ፣ ቅፁን ማስቀመጥ አልተሳካም!</string>\n  <string name=\"data_saved_ok\">ቅፁ በተሳካ ሁኔታ ተቀምጧል!</string>\n  <string name=\"save_point_error\">የመልሶ ማግኛ ነጥብ ማስቀመጥ አልተቻለም፡ 1 %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">ለውጦችን አስቀምጥ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">ቅጽን በማስቀመጥ ላይ</string>\n  <string name=\"survey_saving_validating_message\">መልሶችን በማረጋገጥ ላይ …</string>\n  <string name=\"survey_saving_collecting_message\">ውሂብ በመሰብሰብ ላይ …</string>\n  <string name=\"survey_saving_saving_message\">ወደ SD ካርድ በማስቀመጥ ላይ …</string>\n  <string name=\"survey_saving_finalizing_message\">ወደ SD ካርድ በማጠናቀቅ ላይ …</string>\n  <string name=\"survey_saving_encrypting_message\">ውሂብ በማመስጠር ላይ …</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">አስጀምር</string>\n  <string name=\"activity_not_found\">የሚስተናገዱ ምንም እንቅስቃሴዎች አልተገኙም: 1 %s</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">ዩአርኤል ይክፈቱ</string>\n  <string name=\"capture_osm\">OpenMapKitን አስጀምር</string>\n  <string name=\"alert\">ማንቂያ</string>\n  <string name=\"install_openmapkit\">እባክዎ OpenMapKitን ይጫኑ!</string>\n  <string name=\"get_barcode\">ባርኮድ ያግኙ</string>\n  <string name=\"replace_barcode\">ባርኮዱን ተካ</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">ፎቶ አንሳ</string>\n  <string name=\"choose_image\">ምስል ይምረጡ</string>\n  <string name=\"selected_invalid_image\">የተመረጠው ፋይል ትክክለኛ ምስል አይደለም</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">አስቀምጥ እና ዝጋ</string>\n  <string name=\"sign_button\">ፊርማ ያሰባስቡ</string>\n  <string name=\"draw_image\">ንድፍ ምስል</string>\n  <string name=\"reset_image\">ዳግም አስጀምር</string>\n  <string name=\"set_color\">ቀለም አዘጋጅ</string>\n  <string name=\"capture_video\">ቪዲዮ ቅዳ</string>\n  <string name=\"choose_video\">ቪዲዮ ይምረጡ</string>\n  <string name=\"play_video\">ቪድዮ አጫውት</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">ድምጽ ቅረፅ</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">ድምጽ ይምረጡ</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">ሚደያዉን በማያያዝ ላይ ስህተት ተከስቷል 1 %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">መልሱን ይምረጡ</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">ተመርጧል:</string>\n  <string name=\"trigger\">እሺ:: እባክዎ ይቀጥሉ::</string>\n  <string name=\"ext_file_no_data_error\">ፋይሉ ምንም ውሂብ አልያዘም!</string>\n  <string name=\"ext_conflicting_columns_error\">አምዶች 1 %s ይመሳሰላሉ!</string>\n  <string name=\"ext_import_generic_error\">ከ 1 %1$s ውሂብ ማስመጣት አልተቻለም፡፡ ምክንያት፣ 2 %2$s</string>\n  <string name=\"ext_import_finalizing_message\">ቀድሞ የተጫነ ውሂብ በማጠናቀቅ ላይ …</string>\n  <string name=\"ext_import_completed_message\">የንባብ ውሂብ ተጠናቅቋል!</string>\n  <string name=\"ext_not_initialized_error\">ውጫዊ ውሂብ አደራጅ አልተነሳም</string>\n  <string name=\"direction\">አቅጣጫ: 1 %s</string>\n  <string name=\"select_date\">ቀን ምረጥ</string>\n  <string name=\"select_time\">ሰዓት ምረጥ</string>\n  <string name=\"ethiopian_month_1\">መስከረም</string>\n  <string name=\"ethiopian_month_2\">ጥቅምት</string>\n  <string name=\"ethiopian_month_3\">ኅዳር</string>\n  <string name=\"ethiopian_month_4\">ታኅሣሥ</string>\n  <string name=\"ethiopian_month_5\">ጥር</string>\n  <string name=\"ethiopian_month_6\">የካቲት</string>\n  <string name=\"ethiopian_month_7\">መጋቢት</string>\n  <string name=\"ethiopian_month_8\">ሚያዝያ</string>\n  <string name=\"ethiopian_month_9\">ግንቦት</string>\n  <string name=\"ethiopian_month_10\">ሰኔ</string>\n  <string name=\"ethiopian_month_11\">ሐምሌ</string>\n  <string name=\"ethiopian_month_12\">ነሐሴ</string>\n  <string name=\"ethiopian_month_13\">ጳጉሜን</string>\n  <string name=\"persian_month_4\">ጥር</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">ፋይል፡ 1 %s ልክ ያልኾነ ነው</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">ፋይል፡ 1 %s ይጎድላል</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"streets\">መንገዶች</string>\n  <string name=\"terrain\">መልከአ ምድር</string>\n  <string name=\"satellite\">ሳተላይት</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">ይቅርታ፣ የአካባቢ አቅራቢዎች አይሰሩም፡፡</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">ም 1 %1$s  2 %2$s  3%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"start\">ይጀምሩ</string>\n  <string name=\"enable_gps\">GPSን አንቃ</string>\n  <string name=\"clear\">አጽዳ</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"discard\">አስወግድ</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">ቅጹን በመላክ ላይ</string>\n  <string name=\"noselect_error\">ይቅርታ፣ ምንም ቅጽ አልመረጡም!</string>\n  <string name=\"no_connection\">ምንም የአውታረ መረብ ግንኙነት የለም</string>\n  <string name=\"sending_items\">1 %1$s ከ 2 %2$s ቅጽ (ዎች) በመላክ ላይ</string>\n  <string name=\"send_selected_data\">የተመረጠውን ላክ</string>\n  <string name=\"server_auth_credentials\">ልክ ያልሆነ የተጠቃሚ ስም ወይም የይለፍ ቃል ለሰርቨሩ፣ 1 %s</string>\n  <string name=\"server_requires_auth\">ሰርቨር ማረጋገጫ ያስፈልገዋል</string>\n  <string name=\"no_forms_uploaded\">ይቅርታ፣ ምንም ቅጽ አልተጫነም፡፡</string>\n  <string name=\"upload_results\">ውጤቶችን ጫን</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">እይታን ለውጥ</string>\n  <string name=\"show_sent_and_unsent_forms\">የተላኩ እና ያልተላኩ ቅጾችን አሳይ</string>\n  <string name=\"show_unsent_forms\">ያልተላኩ ቅጾችን አሳይ</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">አድስ</string>\n  <string name=\"download\">የተመረጠውን ያግኙ</string>\n  <string name=\"downloading_data\">ከአገልጋይ ኮምፒውተር ጋር በመገናኘት ላይ</string>\n  <string name=\"version\">ስሪት:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">የተቀመጡ ቅጾች</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">ባዶ ቅጾች</string>\n  <string name=\"delete_confirm\">የ 1 %s ቅጽ (ዎች) ይወገድ?</string>\n  <string name=\"delete_file\">የተመረጠውን ሰርዝ</string>\n  <string name=\"delete_no\">አትሰርዝ</string>\n  <string name=\"delete_yes\">ቅጾችን ሰርዝ</string>\n  <string name=\"file_deleted_ok\">1 %s ቅጽ (ዎች) በተሳካ ሁኔታ ተሰርዘዋል!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">የተጠቃሚ በይነገጽ</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">የቅጽ ማስተዳደር</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">የሰርቨር ቅንብሮች</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"url_error\">ይቅርታ፣ ልክ ያልሆነ URL!</string>\n  <string name=\"username\">የተጠቃሚ ስም</string>\n  <string name=\"change_username\">ODK የተጠቃሚ ስም</string>\n  <string name=\"password\">የይለፍ ቃል</string>\n  <string name=\"change_password\">ODK የይለፍ ቃል</string>\n  <string name=\"language\">ቋንቋ</string>\n  <string name=\"font_size\">የጽሑፍ ቅርጸ ቁምፊ መጠን</string>\n  <string name=\"change_font_size\">የጽሑፍ ቅርጸ ቁምፊ መጠን</string>\n  <string name=\"font_size_extra_large\">በጣም ትልቅ</string>\n  <string name=\"font_size_large\">ትልቅ</string>\n  <string name=\"font_size_medium\">መካከለኛ</string>\n  <string name=\"font_size_small\">ትንሽ</string>\n  <string name=\"font_size_extra_small\">በጣም ትንሽ</string>\n  <string name=\"navigation\">ዳሰሳ</string>\n  <string name=\"swipe_navigation\">አግድም ማጠፊያዎችን ይጠቀሙ</string>\n  <string name=\"buttons_navigation\">ወደፊት/ወደኋላ አዝራሮችን ይጠቀሙ</string>\n  <string name=\"swipe_buttons_navigation\">ማጠፊያዎችን እና አዝራሮችን ይጠቀሙ</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">ቀጣይ</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"form_submission_category\">የቅጽ ማስረከብ</string>\n  <string name=\"off\">ጠፍቷል</string>\n  <string name=\"delete_after_send\">ከላክ በኋላ ይሰርዙ</string>\n  <string name=\"delete_after_send_summary\">ወደ ሰርቨር ከተላኩ በኋላ የተጠናቀቁ ቅጾችን እና ሚዲያዎችን ይሰርዛል</string>\n  <string name=\"form_filling_category\">የቅጽ መሙላት</string>\n  <string name=\"high_resolution_title\">ከፍተኛ ጥራት ቪዲዮ</string>\n  <string name=\"high_resolution_summary\">ባለከፍተኛ ጥራት ቪዲዮ ቀረጻዎችን ያንቁ</string>\n  <string name=\"image_size_title\">የምስል መጠን</string>\n  <string name=\"image_size_large\">ትልቅ (3072 ፒክስል)</string>\n  <string name=\"image_size_medium\">መካከለኛ (2048 ፒክሰል)</string>\n  <string name=\"image_size_small\">ትንሽ (1024 ፒክሰል)</string>\n  <string name=\"image_size_very_small\">በጣም ትንሽ (640 ፒክሰል)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_no\">አይ</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"analytics_preferences\">የአጠቃቀም ውሂብ</string>\n  <string name=\"phone_number\">ስልክ ቁጥር</string>\n  <string name=\"email\">የ ኢሜል አድራሻ</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">አዲስ የይለፍ ቃል ያስገቡ</string>\n  <string name=\"admin_password\">የአስተዳዳሪ ይለፍ ቃል</string>\n  <string name=\"admin_password_changed\">የአስተዳዳሪ የይለፍ ቃል በተሳካ ሁኔታ ተለውጧል</string>\n  <string name=\"admin_password_disabled\">የአስተዳዳሪ ይለፍ ቃል ተዘግቷል</string>\n  <string name=\"admin_password_incorrect\">ይቅርታ፣ የይለፍ ቃል ትክክል አይደለም!</string>\n  <string name=\"enter_admin_password\">የአስተዳዳሪ የይለፍ ቃል ያስገቡ</string>\n  <string name=\"change_admin_password\">የአስተዳዳሪ የይለፍ ቃል ለውጥ</string>\n  <string name=\"show_password\">የይለፍ ቃሉን አሳይ</string>\n  <string name=\"found_in_main\">ከመደበኛው ምናሌ ለመደበቅ አትምረጥ</string>\n  <string name=\"reset_project_settings_title\">ዳግም አስጀምር</string>\n  <string name=\"select_all\">ሁሉንም ምረጥ</string>\n  <string name=\"clear_all\">ሁሉንም ያፅዱ</string>\n  <string name=\"reset_settings_dialog_title\">ምን ዳግም ለመጀመር ይምረጡ</string>\n  <string name=\"reset_settings_button_reset\">ዳግም አስጀምር</string>\n  <string name=\"reset_in_progress\">እንደገና በማስጀመር ላይ …</string>\n  <string name=\"reset_app_state_result\">ውጤቶችን ዳግም ያስጀምሩ</string>\n  <string name=\"reset_settings_result\">ሁሉም ቅንብሮች :: %s</string>\n  <string name=\"reset_saved_forms_result\">የተቀመጡ ቅጾች :: %s</string>\n  <string name=\"reset_blank_forms_result\">ባዶ ቅጾች :: %s</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">ወደኋላ መሄድ</string>\n  <string name=\"moving_backwards_disabled_title\">ወደኋላ መሄድ አልተመረጠም</string>\n  <string name=\"yes\">አዎ</string>\n  <string name=\"no\">አይ</string>\n  <string name=\"moving_backwards_enabled_title\">ወደኋላ መሄድ ተመርጧል</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">ስለ</string>\n  <string name=\"odk_website\">የODKን ድረ ገፅ ይጎብኙ</string>\n  <string name=\"odk_forum\">የODKን መድረክ ይቀላቀሉ</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"share\">አጋራ</string>\n  <string name=\"generate\">ፍጠር</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">እሺ</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">ሰርዝ</string>\n  <string name=\"cancel_loading_form\">ሰርዝ</string>\n  <string name=\"cancel_location\">ሰርዝ</string>\n  <string name=\"do_not_change\">ሰርዝ</string>\n  <string name=\"success\">ስኬት</string>\n  <string name=\"error_occured\">ስህተት ተከስቷል</string>\n  <string name=\"please_wait\">እባክዎ ጥቂት ጊዜ ይጠብቁ</string>\n  <string name=\"please_wait_long\">እባክዎ ትንሽ ይጠብቁ፡፡ ይሄ ጥቂት ደቂቃዎችን ሊወስድ ይችላል፡፡</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">አስወግድ</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ar/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">املأ نموذج جديد</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">مسودّات</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">جاهز للإرسال</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">مرسل</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">حمّل نموذج</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">امسح نموذج</string>\n  <string name=\"saved_forms\">نماذج محفوظة</string>\n  <string name=\"version_number\">الإصدار: %s</string>\n  <string name=\"id_number\">المعرف : %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'تمت الاضافة ب\\' yyyy, dd MMM, EEE  \\'في الساعة\\' mm:HH</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'حدث \\' EEE, MMM dd, yyyy \\'في\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'تم الحفظ ب\\' yyyy, dd MMM, EEE  \\'في الساعة\\' mm:HH</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'اكتملت ب\\' yyyy, dd MMM, EEE  \\'في الساعة\\' mm:HH</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'ارسلت ب\\' yyyy, dd MMM, EEE  \\'في الساعة\\' mm:HH</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'فشل الارسال ب\\' yyyy, dd MMM, EEE  \\'في الساعة\\' mm:HH</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'تمت عملية الحذف في\\' yyyy, dd MMM, EEE  \\'الساعة\\' mm:HH</string>\n  <string name=\"submission_deleted\">تم حذف التقديم</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'تم التعديل ب \\' yyyy, dd MMM, EEE  \\'في الساعة\\' mm:HH</string>\n  <string name=\"encrypted_form\">استمارة مشفرة</string>\n  <string name=\"deleted_form\">استمارة محذوفة</string>\n  <string name=\"sort_the_list\">ترتيب القائمة</string>\n  <string name=\"sort_by\">ترتيب حسب</string>\n  <string name=\"sort_by_name_asc\">ترتيب حسب الاسم تصاعدياً</string>\n  <string name=\"sort_by_name_desc\">ترتيب حسب الاسم تنازلياً</string>\n  <string name=\"sort_by_date_desc\">ترتيب حسب التاريخ تصاعدياً</string>\n  <string name=\"sort_by_date_asc\">ترتيب حسب التاريخ تنازلياً</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">عرض آخر المحفوظات أولا</string>\n  <string name=\"filter_the_list\">فرز القائمة</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">بحث</string>\n  <string name=\"not_granted_permission\">النشاط الذي تحاول بدء القيام به يحتاج إلى إذن لم تحصل عليه بعد.</string>\n  <string name=\"storage_runtime_permission_denied_title\">أذونات التخزين</string>\n  <string name=\"storage_runtime_permission_denied_desc\">بدون هذه الأذونات لا يمكن لبرنامج ODK Collect الوصول إلى الاستمارات أو حفظ الإجابات. أعد فتح التطبيق عندما تكون مستعدًا لمنحهم.</string>\n  <string name=\"location_runtime_permissions_denied_title\">الأذونات الخاصة بالموقع</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">إذن الكاميرا</string>\n  <string name=\"camera_runtime_permission_denied_desc\">بدون هذا الإذن لا يمكن لبرنامج ODK Collect الوصول إلى الكاميرا. يرجى المحاولة مرة أخرى عندما تكون مستعدًا لمنحه.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">إذن تسجيل الصوت</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">بدون هذا الإذن لا يمكن لبرنامج ODK Collect الوصول إلى الميكروفون. يرجى المحاولة مرة أخرى عندما تكون مستعدًا لمنحه.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">فتح من الخريطة</string>\n  <string name=\"instance_scan_count\">تم إضافة %1$d إستمارة</string>\n  <string name=\"finished_disk_scan\">تم انهاء عملية البحث، جميع النماذج تم تحميلها.</string>\n  <string name=\"xform_parse_error\">خطأ في محلل XForm من أجل %1$s: %2$s مفقود أو غير صالح.</string>\n  <string name=\"loading_form\">تحميل الاستمارة</string>\n  <string name=\"survey_loading_reading_form_message\">القراءة من التعريف…</string>\n  <string name=\"survey_loading_reading_data_message\">قراءة بيانات الدراسة…</string>\n  <string name=\"survey_loading_reading_csv_message\">قراءة ملفات CSV..</string>\n  <string name=\"parse_error\">عذراً لا يمكن تحليل الاستمارة.</string>\n  <string name=\"loading_form_failed\">حدث خطأ أثناء تحميل الاستمارة. الرجاء المحاولة مرة أخرى.</string>\n  <string name=\"cannot_edit_completed_form\">يتعذر تعديل هذه الاستمارة عندما يتم وضع علامة على أنها مكتملة. قد تكون مشفرة.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">التغييرات غير محفوظة يرجى استعادتها من نقطة الحفظ!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">تغيير اللغة</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">الذهاب الى الموجه</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">حفظ كمسودة</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">الذهاب إلى البداية</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">الذهاب إلى النهاية</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">الذهاب صعودًا</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">مجموعة</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">مجموعة متكررة</string>\n  <string name=\"exit\">خروج</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">أضف \\\"%s\\\"؟</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">أضف</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">إضافة أخرى</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">تعديل التوجيه</string>\n  <string name=\"clearanswer_confirm\">حذف الإجابة ل \\\"%s\\\"؟</string>\n  <string name=\"clear_answer\">حذف الإجابة</string>\n  <string name=\"clear_answer_ask\">حذف هذه الإجابة؟</string>\n  <string name=\"clear_answer_no\">إلغاء</string>\n  <string name=\"discard_answer\">حذف الإجابة</string>\n  <string name=\"discard_group\">حذف المجموعة</string>\n  <string name=\"delete_repeat\">حذف المجموعة</string>\n  <string name=\"delete_repeat_ask\">حذف هذه المجموعة؟</string>\n  <string name=\"delete_repeat_confirm\">حذف المجموعة \\\"%s\\\" وجميع المجموعات الفرعية ذات الصلة؟</string>\n  <string name=\"delete_repeat_no\">إلغاء</string>\n  <string name=\"invalid_answer_error\">عذراً، الإجابة غير صالحة.</string>\n  <string name=\"required_answer_error\">عذرًا، يجب الإجابة على هذا السؤال!</string>\n  <string name=\"success_form_validation\">تمت العملية بنجاح! لا توجد أخطاء في النموذج.</string>\n  <string name=\"invalid_space_in_answer_singular\">تحذير: القيمة أدناه %s تحتوي على مسافات</string>\n  <string name=\"invalid_space_in_answer_plural\">تحذير: القيم أدناه %s تحتوي على مسافات</string>\n  <string name=\"too_complex_form\">هذه الاستمارة معقدة جدًا لهذا الجهاز. حاول تبسيط التعبيرات أو طلب المساعدة في المنتدى.</string>\n  <string name=\"survey_internal_error\">خطأ داخلي: فشل الخطأ الموجه.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">إنّك في نهاية الـ%s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">حفظ كمسودة</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">إنهاء</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">إرسال</string>\n  <string name=\"data_saved_error\">عذرًا، لم يتمّ حفظ الاستمارة!</string>\n  <string name=\"data_saved_ok\">تمّ حفظ الاستمارة بنجاح!</string>\n  <string name=\"save_point_error\">خطأ في حفظ نقطة الاسترداد %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">التخلص من النموذج</string>\n  <string name=\"discard_changes\">تجاهل التعديلات</string>\n  <string name=\"keep_changes\">حفظ التعديلات</string>\n  <string name=\"keep_editing\">الاستمرار في التعديل</string>\n  <string name=\"quit_form_title\">حفظ النموذج؟</string>\n  <string name=\"save_explanation\">يمكنك حفظ هذا النموذج والوصول إليه من المسودات في أي وقت.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"quit_form_continue_title\">الإستمرار في التعديل؟</string>\n  <string name=\"discard_form_warning\">إذا قمت بالتخلص من النموذج, فسوف تخسر جميع التعديلات التي قمت بها إلى هذه اللحظة.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">جاري حفظ الاستمارة</string>\n  <string name=\"survey_saving_validating_message\">تدقيق الاجابات…</string>\n  <string name=\"survey_saving_collecting_message\">جمع البيانات</string>\n  <string name=\"survey_saving_saving_message\">جاري الحفظ في بطاقة SD</string>\n  <string name=\"survey_saving_finalizing_message\">جاري  انهاء الاستمارات  في بطاقة </string>\n  <string name=\"survey_saving_encrypting_message\">تشفير البيانات</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">لايوجد سجل مطابق لهذا النموذج</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">معرف الاستمارة لا يتطابق مع أي نموذج الفارغ</string>\n  <string name=\"no_form_id_specified\">لم يتم تحديد معرف لنموذج الاستمارة؟؟؟</string>\n  <string name=\"phone_does_not_support_rsa\">الهاتف لايدعم تشفير RSA</string>\n  <string name=\"invalid_rsa_public_key\">مفتاح RSA العام غير صالح</string>\n  <string name=\"encryption_error_message\">الاستمارة لم يتم حفظها كنهائية %s </string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">تعذر بدء التسجيل</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">الميكرفون قيد الإستخدام</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">لا يمكن إرفاق الوسائط بالنموذج ولكنها متوفرة على %1$s</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">التسجيل معطل. تمكين في %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">تشغيل</string>\n  <string name=\"activity_not_found\">لا يوجد مهمات للمعالجة: %s</string>\n  <string name=\"no_app\">التطبيق المطلوب مفقود. يرجى ادخال القراءة يدويا.</string>\n  <string name=\"null_intent_value\">التطبيق الخارجي لم يرسل البيانات المتوقعة.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">فتح الرابط</string>\n  <string name=\"capture_osm\">شغل ال OpenMapKit</string>\n  <string name=\"recapture_osm\">أعادة تعليم ال OSM</string>\n  <string name=\"edited_osm_file\">تعديل ملف OSM XML:</string>\n  <string name=\"invalid_osm_data\">هناك شيء ما خاطئ. لم نستطع الحصول على بيانات OSM صحيحة</string>\n  <string name=\"alert\">تحذير</string>\n  <string name=\"install_openmapkit\">رجاء قم بتنصيب OpenMapKit</string>\n  <string name=\"get_barcode\">الحصول على الباركود</string>\n  <string name=\"replace_barcode\">استبدال الباركود</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">ضع الباركود داخل المستطيل</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">فتح الملف</string>\n  <string name=\"rank_items\">رتب العناصر</string>\n  <string name=\"number_picker_title\">منتقي الأرقام</string>\n  <string name=\"invalid_range_widget\">هذة الميزة غير موجودة</string>\n  <string name=\"svg_file_does_not_exist\">ملف SVG غير موجود!</string>\n  <string name=\"capture_image\">التقط صورة</string>\n  <string name=\"choose_image\">إختر صورة</string>\n  <string name=\"selected_invalid_image\">الملف المحدد ليس صورة بتنسيق مقبول</string>\n  <string name=\"take_picture_instruction\">انقر على الشاشة لإلتقاط صورة</string>\n  <string name=\"error_front_camera_unavailable\">الكاميرا الأمامية غير متوفرة على هذا الجهاز</string>\n  <string name=\"camera_error\">حدث خطأ أثناء أخذ صورة</string>\n  <string name=\"camera_failed_to_initialize\">فشل في تهيئة الكاميرا!</string>\n  <string name=\"annotate_image\">تعليق على الصورة</string>\n  <string name=\"gif_not_supported\">ملفات صيغة الــ GIF غير مدعومة</string>\n  <string name=\"invalid_file_type\">أرجع التطبيق نوع ملف غير صالح</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">الحفظ و الاغلاق</string>\n  <string name=\"signature_capture\">صورة التوقيع</string>\n  <string name=\"sign_button\">جمع التوقيع</string>\n  <string name=\"markup_image\">حدد الصورة</string>\n  <string name=\"draw_image\">تخطيط الصورة</string>\n  <string name=\"reset_image\">اعادة تعيين</string>\n  <string name=\"set_color\">تحديد اللون</string>\n  <string name=\"capture_video\">سجل فيديو</string>\n  <string name=\"choose_video\">إختر فيديو</string>\n  <string name=\"play_video\">تشغيل تسجيل الفيديو</string>\n  <string name=\"choose_file\">إختر ملف</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">حذف</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">تسجيل الصوت</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">إختر تسجيل صوتي</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">إيقاف مؤقت</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">استئناف</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">مسجل الصوت: يعمل</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">مسجل الصوت: مقفل</string>\n  <string name=\"background_audio_permission_explanation\">هذا النموذج يسجل الصوت في الخلفية. يجب أن تمنح الإذن لاستخدام الميكروفون الخاص بك. إذا لم تقم بذلك ، فلن تتمكن من فتح هذا النموذج.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">يطلب هذا النموذج تسجيل صوت في الخلفية. سيؤدي تعطيله إلى إيقاف التسجيل وتجاهل الصوت الموجود. هل انت متأكد انك تريد المتابعة؟</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">تعطيل التسجيل</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">التسجيل لن يبدأ على الفور. يجب عليك إعادة فتح النموذج للتسجيل.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">حذف الملف؟</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">لن تتمكن من استعادة هذا الملف بعد حذفه. هل انت متأكد انك تريد المتابعة؟</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">توقف</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">تسجيل…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">تسجيل الصوت</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">تشغيل الصوت</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">حفظ الملف</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">عليك ايقاف التسجيل قبل مغادرة الشاشة</string>\n  <string name=\"error_attaching_binary_file\">حدث خطأ أثناء محاولة ارفاق الملف: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">اختر القيمة</string>\n  <string name=\"select_answer\">إختر إجابة</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">عدل القيمة</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">لم يتم اختيار قيمة</string>\n  <string name=\"selected_title\">مختارة:</string>\n  <string name=\"trigger\">حسنًا. الرجاء المواصلة.</string>\n  <string name=\"ext_file_no_data_error\">لا يحتوي الملف على بيانات!</string>\n  <string name=\"ext_conflicting_columns_error\">اعمدة  %s متوافقة ! </string>\n  <string name=\"ext_sortBy_numeric_error\">العمود الخاص بك يجب ان يحتوي على قيم رقمية فقط.. القيم كانت متضاربة \\'%s\\'</string>\n  <string name=\"ext_search_wrong_arguments_error\">خطأ في عملية البحث: تحتاج العملية الى ارفاق القيم 1,4,6</string>\n  <string name=\"ext_search_wrong_function_error\">خطأ في عملية البحث: وظيفة: عملية غير معترف بها  \\'%s\\'</string>\n  <string name=\"ext_search_bad_function_error\">خطا في عملية البحث:  \\'%s\\' لم يتم تقييمها كعملية</string>\n  <string name=\"ext_search_return_error\">معالج البحث استعاد نوع الموضوع  \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">لم يمكن استيراد البيانات من  %1$s السبب: %2$s</string>\n  <string name=\"ext_import_progress_message\">التحميل المسبق للبيانات من \\'%1$s\\'،  يرجى الانتظار…  %2$s</string>\n  <string name=\"ext_import_cancelled_message\">إلغاء قراءة البيانات!</string>\n  <string name=\"ext_import_finalizing_message\">جاري انهاء التحميل المسبق للبيانات.</string>\n  <string name=\"ext_import_completed_message\">اكتملت قراءة البيانات!</string>\n  <string name=\"ext_not_initialized_error\">مدير البيانات الخارجية لم يتم تفعيله.</string>\n  <string name=\"ext_import_csv_missing_error\">لم يتم استيراد البيانات الخارجية %1$s. ربما قد نسيت تضمين ملف  %2$s.csv مع استمارتك؟</string>\n  <string name=\"ext_search_generic_error\">خطأ في عملية البحث: %s</string>\n  <string name=\"ext_assign_value_error\">لا يمكن تعيين القيمة في   \\'%s\\'.</string>\n  <string name=\"parser_exception\">استثناء XPathParser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">تسجيل الاتجاه</string>\n  <string name=\"replace_bearing\">استبدال الاتجاه</string>\n  <string name=\"getting_bearing\">تحميل الاتجاه</string>\n  <string name=\"accept_bearing\">تسجيل الاتجاه</string>\n  <string name=\"bearing\">تحمل %.3f</string>\n  <string name=\"direction\">الاتجاه: %s</string>\n  <string name=\"bearing_lack_of_sensors\">لا يمكن جمع المحامل: يفتقد الجهاز إلى مقياس التسارع أو مستشعر المجال المغناطيسي أو كليهما.</string>\n  <string name=\"select_date\">اختر التاريخ</string>\n  <string name=\"select_time\">اختر الوقت</string>\n  <string name=\"no_date_selected\">لم يتم اختيار تاريخ</string>\n  <string name=\"no_time_selected\">لم يتم اختيار وقت</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">محرم</string>\n  <string name=\"islamic_month_2\">صفر</string>\n  <string name=\"islamic_month_3\">ربيع الأول</string>\n  <string name=\"islamic_month_4\">ربيع الثاني</string>\n  <string name=\"islamic_month_5\">جمادى الأولى</string>\n  <string name=\"islamic_month_6\">جمادى الثانية</string>\n  <string name=\"islamic_month_7\">رجب</string>\n  <string name=\"islamic_month_8\">شعبان</string>\n  <string name=\"islamic_month_9\">رمضان</string>\n  <string name=\"islamic_month_10\">شوال</string>\n  <string name=\"islamic_month_11\">ذو القعدة</string>\n  <string name=\"islamic_month_12\">ذو الحجة</string>\n  <string name=\"persian_month_1\">فروردین</string>\n  <string name=\"persian_month_2\">اردیبهشت</string>\n  <string name=\"persian_month_3\">خرداد</string>\n  <string name=\"persian_month_4\">تير</string>\n  <string name=\"persian_month_5\">مرداد</string>\n  <string name=\"persian_month_6\">شهریور</string>\n  <string name=\"persian_month_7\">مهر</string>\n  <string name=\"persian_month_8\">آبان</string>\n  <string name=\"persian_month_9\">آذر</string>\n  <string name=\"persian_month_10\">دى</string>\n  <string name=\"persian_month_11\">بهمن</string>\n  <string name=\"persian_month_12\">اسفند</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">الملف: %s غير صالح</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">الملف: %s مفقود</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">أدخل الهوية</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">هوية</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">هذه الاستمارة تتطلب منك تحديد هويتك</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">أغلق</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">سبب التغيير</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">السبب</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">أنت بحاجة إلى توضيح سبب التغييرات في هذه الاستمارة</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">حفظ</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">أختر مكان</string>\n  <string name=\"choices\">خيارات</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">خرطة البنية</string>\n  <string name=\"openmap_usgs_sat\">خريطة وطنية هجين USGS</string>\n  <string name=\"openmap_usgs_img\">صور الخريطة الوطنية USGS</string>\n  <string name=\"openmap_cartodb_positron\">خريطة CartoDB</string>\n  <string name=\"openmap_cartodb_darkmatter\">خريطة CartoDB</string>\n  <string name=\"basemap\">الخريطة الأساسية</string>\n  <string name=\"basemap_source\">المصدر</string>\n  <string name=\"map_style_label\">%s نمط الخريطة</string>\n  <string name=\"streets\">الطرق</string>\n  <string name=\"terrain\">التضاريس</string>\n  <string name=\"hybrid\">الكل</string>\n  <string name=\"satellite\">أقمار صناعية</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">ضوء</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">داكن</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">في الخارج</string>\n  <string name=\"topographic\">طبوغرافي</string>\n  <string name=\"google_play_services_error_occured\">غير قادر على الوصول إلى خرائط الجوجل. هل خدمات Google Play مثبتة؟</string>\n  <string name=\"carto_map_style_positron\">بوزيترون</string>\n  <string name=\"carto_map_style_dark_matter\">المادة المظلمة</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">سجل نقطة</string>\n  <string name=\"provider_disabled_error\">عذراً، مزود خدمة الموقع معطّل.</string>\n  <string name=\"google_play_services_not_available\">تريد هذه الاستمارة تتبع موقعك ولكن خدمات Google Play غير متوفرة.</string>\n  <string name=\"location_providers_disabled_dialog_message\">تريد هذه الاستمارة تتبع موقعك ولكن موفري الموقع معطلين. يرجى التمكين في إعدادات أندرويد.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">إذهب للاعدادات</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">تعقب المكان: يعمل</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">تعقب المكان: مقفل</string>\n  <string name=\"background_location_disabled\">تريد هذه الاستمارة تتبع موقعك ولكن تم تعطيل التتبع. يرجى التمكين في القائمة %1$s أعلاه.</string>\n  <string name=\"background_location_enabled\">هذه الاستمارة تقوم بتتبع موقعك. يمكنك تعطيل التتبع في القائمة %1$s أعلاه.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">تحقق من الأخطاء</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"waiting_for_location\">محاولة الوصول للموقع. الرجاء الإنتظار.</string>\n  <string name=\"poor_accuracy\">جودة ضعيفة الرجاء الإنتظار.</string>\n  <string name=\"unacceptable_accuracy\">جودة غير مقبولة. الرجاء الإنتظار.</string>\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">غرب %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">شرق %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">جنوب %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">شمال %1$s%2$s%3$s</string>\n  <string name=\"input_method\">طريقة الإدخال</string>\n  <string name=\"start\">إبدأ</string>\n  <string name=\"enable_gps\">تفعيل GPS</string>\n  <string name=\"gps_enable_message\">GPS غير مفعلة على جهازك. هل تريد تفعيلها؟</string>\n  <string name=\"geo_clear_warning\">الميزة تم إنشاؤها مسبقاً. هل ترغب في إزالتها؟</string>\n  <string name=\"geo_exit_warning\">تجاهل التعديلات والعودة إلى ODK؟</string>\n  <string name=\"polygon_validator\">يجب توفر 3 نقاط لإنشاء مضلع</string>\n  <string name=\"polyline_validator\">يجب توفر نقطتين لإنشاء خط</string>\n  <string name=\"clear\">إلغاء</string>\n  <string name=\"placement_mode\">التنسيب عن طريق النقر</string>\n  <string name=\"manual_mode\">تسجيل يدوي للموقع</string>\n  <string name=\"automatic_mode\">تسجيل تلقائي للموقع</string>\n  <string name=\"recording_interval\">فترة التسجيل:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"zero\">%dثانية </item>\n    <item quantity=\"one\">%dثانية </item>\n    <item quantity=\"two\">%dثواني </item>\n    <item quantity=\"few\">%dثواني </item>\n    <item quantity=\"many\">%dثانية </item>\n    <item quantity=\"other\">%d ثانية</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"zero\">%dدقيقة </item>\n    <item quantity=\"one\">%dدقيقة</item>\n    <item quantity=\"two\">%dدقائق </item>\n    <item quantity=\"few\">%d دقائق</item>\n    <item quantity=\"many\">%dدقيقة </item>\n    <item quantity=\"other\">%dدقيقة </item>\n  </plurals>\n  <string name=\"accuracy_requirement\">متطلبات الدقة:</string>\n  <string name=\"none\">لا شيء</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"zero\">%d متر</item>\n    <item quantity=\"one\">%d متر</item>\n    <item quantity=\"two\">%dأمتار </item>\n    <item quantity=\"few\">%dأمتار </item>\n    <item quantity=\"many\">%dمتر </item>\n    <item quantity=\"other\">%dمتر </item>\n  </plurals>\n  <string name=\"collection_status_paused\">النقاط التي تم إدخالها: %d</string>\n  <string name=\"collection_status_placement\">النقاط التي تم إدخالها: %d (انقر لوضع نقاط)</string>\n  <string name=\"collection_status_manual\">النقاط التي تم إدخالها: %d (التسجيل اليدوي)</string>\n  <string name=\"collection_status_auto_seconds\">النقاط التي تم إدخالها: %1$d (تسجيل كل %2$d ثانية)</string>\n  <string name=\"collection_status_auto_minutes\">النقاط التي تم إدخالها: %1$d (تسجيل كل %2$d دقيقة)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">النقاط التي تم إدخالها: %1$d (تسجيل كل %2$d ثانية ، دقة %3$d متر)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">النقاط التي تم إدخالها: %1$d (تسجيل كل %2$d دقيقة ، دقة %3$d متر)</string>\n  <string name=\"geopoint_no_draggable_instruction\">انقر على زر مؤشر الإضافة</string>\n  <string name=\"discard\">تجاهل</string>\n  <!--Name of an action button-->\n  <string name=\"view_data\">عرض الإستمارات المحفوظة</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">إيقاف مؤقت</string>\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">جاري ارسال الاستمارة</string>\n  <string name=\"noselect_error\">عذرًا، لم تختَر أيّ استمارة!</string>\n  <string name=\"no_connection\">لا يوجد اتصال بالشبكة</string>\n  <string name=\"sending_items\">إرسال %1$s من أصل %2$s استمارة</string>\n  <string name=\"send_selected_data\">إرسال الاستمارات المُختارة</string>\n  <string name=\"server_auth_credentials\">اسم المستخدم أو كلمة المرور للمخدم غير صحيحة: %s</string>\n  <string name=\"server_requires_auth\">المخدم يتطلب المصادقة</string>\n  <string name=\"no_forms_uploaded\">عذرا، لا يوجد استمارة للتحميل.</string>\n  <string name=\"upload_results\">تحميل النتائج</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">تغيير المشهد</string>\n  <string name=\"show_sent_and_unsent_forms\">عرض الاستمارات المرسلة وغير المرسلة</string>\n  <string name=\"show_unsent_forms\">عرض الاستمارات الغير مرسلة</string>\n  <string name=\"send_in_progress\">خلفية الارسال تعمل. يرجى المحاولة مرة أخرى في وقت قريب</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">تحديث</string>\n  <string name=\"download\">الحصول على الاستمارات المحددة</string>\n  <string name=\"downloading_data\">الإتصال بالخادم</string>\n  <string name=\"fetching_file\">الحصول على %1$s.\\n\\nالنموذج %2$s من %3$s نماذج</string>\n  <string name=\"form_download_progress\">%1$s. الحصول على ملفات الوسائط المتعددة:  %2$s  من %3$s</string>\n  <string name=\"version\">رقم الاصدار:</string>\n  <string name=\"form_updates_available\">تحديثات الاستمارة متاحة</string>\n  <string name=\"newer_version_of_a_form_info\">هذا تحديث للإستمارة التي لديك</string>\n  <string name=\"form_update_error\">فشل تحديث الإستمارة</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">خطأ في تنزيل قائمة الإستمارة</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">إذا استمرت هذه المشكلة، فأبلغ عنها الشخص الذي طلب منك جمع البيانات.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">التطبيق لا يمكن أن يصل إلى الخادم في %s. هل أدخلت عنوان URL بشكل صحيح؟</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">التطبيق لا يمكن أن يصل إلى الخادم في %s بشكل آمن. هل أدخلت عنوان URL بشكل صحيح؟</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">الخادم %1$s ارجع خطأ بكود %2$s</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">قدم الخادم %s بإرجاع استجابة غير صالحة.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">الاستمارات المحفوظة</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">استمارات فارغة</string>\n  <string name=\"delete_confirm\">حذف %s الاستمارة/ات؟</string>\n  <string name=\"delete_file\">إحذف الخيارات المحددة</string>\n  <string name=\"delete_no\">لا تحذف</string>\n  <string name=\"delete_yes\">حذف الاستمارات</string>\n  <string name=\"form_delete_message\">جاري حذف الاستمارات المحددة</string>\n  <string name=\"file_deleted_ok\">%s تم حذف الاستمارة /ت!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">الخادم</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">إعدادات واجهة المستخدم</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">خرائط</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">إدارة الاستمارة</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">معرف المستخدم والجهاز</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">تجريبي</string>\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">إعدادات الخادم</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">الرابط</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">رابط الخادم</string>\n  <string name=\"url_error\">غدراً، الرابط خاطئ!</string>\n  <string name=\"username\">إسم المستخدم</string>\n  <string name=\"change_username\">إسم المستخدم</string>\n  <string name=\"password\">كلمة المرور</string>\n  <string name=\"change_password\">ODK كلمة المرور</string>\n  <string name=\"password_error_whitespace\">الفراغات في البداية أو النهاية غير مسموح بها في كلمات المرور</string>\n  <string name=\"username_error_whitespace\">الفراغات في البداية أو النهاية غير مسموح بها في اسم المستخدم</string>\n  <string name=\"app_theme\">المظهر</string>\n  <string name=\"language\">اللغة</string>\n  <string name=\"use_device_language\">استخدم لغة الجهاز</string>\n  <string name=\"font_size\">حجم الخط</string>\n  <string name=\"change_font_size\">حجم الخط</string>\n  <string name=\"font_size_extra_large\">كبير جدا</string>\n  <string name=\"font_size_large\">كبير</string>\n  <string name=\"font_size_medium\">متوسط</string>\n  <string name=\"font_size_small\">صغير</string>\n  <string name=\"font_size_extra_small\">صغير جدا</string>\n  <string name=\"navigation\">التنقل</string>\n  <string name=\"swipe_navigation\">استخدم السحب الافقي</string>\n  <string name=\"buttons_navigation\">استخدم ازرار التنقل للأمام و الخلف</string>\n  <string name=\"swipe_buttons_navigation\">استخدم السحب والازرار</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">التالي</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">رجوع</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">تحديث الاستمارة</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">وضع تحديث الإستمارات الفارغة</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">يدوي</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">فقط الإستمارات التي تم تنزيلها مسبقًا</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">تتطابق مع السيرفر تمامًا</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">تكرار التحديث التلقائي</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">كل 15 دقيقة</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">كل ساعة</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">كل 6 ساعات</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">كل 24 ساعة</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">تحميل آلي</string>\n  <string name=\"automatic_download_summary\">قم بتنزيل الإصدارات المحدّثة من الاستمارات تلقائيًا</string>\n  <string name=\"hide_old_form_versions_setting_title\">إخفاء الإصدارات القديمة من الاستمارة</string>\n  <string name=\"hide_old_form_versions_setting_summary\">سيظهر الإصدار الأحدث فقط في ملء استمارة فارغة</string>\n  <string name=\"form_submission_category\">إرسال الاستمارة</string>\n  <string name=\"autosend_selector_title\">الإرسال التلقائي</string>\n  <string name=\"autosend\">الإرسال التلقائي</string>\n  <string name=\"off\">مغلق</string>\n  <string name=\"wifi_autosend\">فقط عبر Wifi</string>\n  <string name=\"cellular_autosend\">فقط عبر شبكة الاتصال</string>\n  <string name=\"wifi_cellular_autosend\">عبر أي من Wifi أو شبكة الاتصال</string>\n  <string name=\"delete_after_send\">حذف بعد الارسال</string>\n  <string name=\"delete_after_send_summary\">حذف الاستمارة النهائية والوسائط المتعددة بعد ارسالها الى الخادم</string>\n  <string name=\"form_filling_category\">تعبئة الاستمارة</string>\n  <string name=\"constraint_behavior_title\">معالجة القيود</string>\n  <string name=\"constraint_behavior\">معالجة القيود جارية</string>\n  <string name=\"constraint_behavior_on_swipe\">التدقيق عند السحب باتجاه الامام</string>\n  <string name=\"constraint_behavior_on_finalize\">تأجيل المصادقة حتى الانتهاء</string>\n  <string name=\"high_resolution_title\">دقة عالية للفيديو</string>\n  <string name=\"high_resolution_summary\">تمكين تسجيلات الفيديو عالية الدقة</string>\n  <string name=\"image_size_title\">حجم الصورة</string>\n  <string name=\"image_size_dialog_title\">أقصى بكسلات للحافة الطويلة للصورة</string>\n  <string name=\"image_size_large\">كبيرة (3072 بكسل)</string>\n  <string name=\"image_size_medium\">متوسطة (2048 بكسل)</string>\n  <string name=\"image_size_small\">صغيرة (1024 بكسل)</string>\n  <string name=\"image_size_very_small\">صغيرة جدا (640 بكسل)</string>\n  <string name=\"image_size_original\">الحجم الأصلي من الكاميرا (افتراضي)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">عرض التوجيه للأسئلة</string>\n  <string name=\"guidance_no\">لا</string>\n  <string name=\"guidance_yes\">نعم - يظهر دائما</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">نعم - مطوية</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">استخدام تطبيق خارجي لتسجيل الصوت</string>\n  <string name=\"form_import_category\">استيراد الاستمارات</string>\n  <string name=\"instance_sync\">حدد الاستمارات كمنتهية عند الاستيراد</string>\n  <string name=\"instance_sync_summary\">يؤثر على الاستمارات المضافة مباشرة إلى مجلد النماذج.</string>\n  <string name=\"analytics_preferences\">بيانات الاستخدام</string>\n  <string name=\"analytics\">جمع بيانات الاستخدام دون تحديد هوية المستخدم</string>\n  <string name=\"form_metadata_title\">معلومات حول النموذج</string>\n  <string name=\"form_metadata\">معلومات حول النموذج</string>\n  <string name=\"form_metadata_summary\">هذه القيم سيتم إضافتها بشكل افتراضي إلى النماذج التي فيها حقول لتحديد اسم المستخدم، بريده الإلكتروني و/أو رقم الهاتف وذلك لتحديد الشخص الذي يقوم بإرسال البيانات.</string>\n  <string name=\"user_defined\">معرف المستخدم</string>\n  <string name=\"phone_number\">رقم الهاتف المحمول</string>\n  <string name=\"email\">البريد الالكتروني</string>\n  <string name=\"invalid_email_address\">عنوان بريد إلكتروني غير صحيح!</string>\n  <string name=\"device_defined\">معرف الجهاز</string>\n  <string name=\"device_id\">رقم معرف الجهاز</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">غير متوفر</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">أدخل كلمة مرور جديدة</string>\n  <string name=\"admin_password\">كلمة مرور المدير</string>\n  <string name=\"admin_password_changed\">تمّ تغيير كلمة مرور المدير بنجاح</string>\n  <string name=\"admin_password_disabled\">تعطيل كلمة السر للمدير</string>\n  <string name=\"admin_password_incorrect\">عذرًا، كلمة المرور غير صحيحة!</string>\n  <string name=\"enter_admin_password\">أدخل كلمة مرور المدير</string>\n  <string name=\"change_admin_password\">تغيير كلمة مرور المدير</string>\n  <string name=\"show_password\">عرض كلمة المرور</string>\n  <string name=\"found_in_main\">الغاء القفل للاخفاء من القائمة الرئيسية</string>\n  <string name=\"form_entry\">خيارات تعبئة الاستمارة</string>\n  <string name=\"reset_project_settings_title\">إعادة ضبط</string>\n  <string name=\"delete_project\">حذف</string>\n  <string name=\"select_all\">تحديد الكل</string>\n  <string name=\"clear_all\">إلغاء التحديد</string>\n  <string name=\"reset_settings\">كافة الإعدادات (الإعدادات الداخلية، والإعدادات المحفوظة)</string>\n  <string name=\"reset_saved_forms\">الاستمارات المحفوظة (مجلد الاستمارات، وقاعدة بيانات الاستمارات)</string>\n  <string name=\"reset_blank_forms\">النماذج الفارغة (مجلد النماذج، وقاعدة بيانات النماذج، وقاعدة بيانات مجموعات العناصر)</string>\n  <string name=\"reset_layers\">طبقات الخريطة (مجلد الطبقات)</string>\n  <string name=\"reset_cache\">الذاكرة المخبئية المؤقتة للنماذج (مجلد cache.)</string>\n  <string name=\"reset_settings_dialog_title\">اختر ما تريد إعادته للضبط الافتراضي</string>\n  <string name=\"reset_settings_button_reset\">إعادة ضبط</string>\n  <string name=\"reset_in_progress\">جاري إعادة الضبط</string>\n  <string name=\"reset_app_state_result\">نتائج عملية إعادة الضبط</string>\n  <string name=\"reset_app_state_warning\">سيتم حذف البيانات المحددة نهائيا. لا إمكانية للتراجع.</string>\n  <string name=\"reset_settings_result\">كل الإعدادات :: %s</string>\n  <string name=\"reset_saved_forms_result\">الاستمارات المحفوظة  :: %s</string>\n  <string name=\"reset_blank_forms_result\">الاستمارات الفارغة  :: %s</string>\n  <string name=\"reset_layers_result\">طبقات الخرائط  :: %s</string>\n  <string name=\"reset_cache_result\">التخزين المؤقت للنماذج المحملة  :: %s</string>\n  <string name=\"main_menu_settings\">إعدادات القائمة الرئيسة</string>\n  <string name=\"user_settings\">إعدادات المستخدم</string>\n  <string name=\"form_entry_setting\">إعدادات تعبئة الاستمارة</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">العودة إلى الوراء</string>\n  <string name=\"moving_backwards_disabled_title\">تم تعطيل العودة إلى الوراء</string>\n  <string name=\"yes\">نعم</string>\n  <string name=\"no\">لا</string>\n  <string name=\"moving_backwards_enabled_title\">تم تمكين التحرك إلى الوراء</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">حفظ كمسودة</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">حول التطبيق</string>\n  <string name=\"tell_your_friends\">مشاركة رابط ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">هل ما زال زملائك يجمعون البيانات على الورق؟ شارك ODK Collect معهم.</string>\n  <string name=\"leave_a_review\">اترك تقييم في Play Store</string>\n  <string name=\"leave_a_review_msg\">تقييمك (الذي نأمل أن يكون إيجابيا) يزيد من رؤية التطبيق في Play Store.</string>\n  <string name=\"odk_website\">زيارة موقع ODK</string>\n  <string name=\"odk_website_summary\">يستخدم ODK لجمع البيانات من أجل الصالح الاجتماعي في البيئات الصعبة.</string>\n  <string name=\"odk_forum\">انضم إلى منتدى ODK</string>\n  <string name=\"odk_forum_summary\">انضم إلى المنتدى للحصول على الدعم وطلب الميزات والمساهمة بالكود / الترجمات!</string>\n  <string name=\"all_open_source_licenses\">مكتبات و تراخيص مفتوحة المصدر</string>\n  <string name=\"all_open_source_licenses_msg\">نقف على أكتاف العمالقة!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"qr_code\">إعدادات رمز QR</string>\n  <string name=\"scan_qr_code_fragment_title\">مسح</string>\n  <string name=\"view_qr_code_fragment_title\">رمز QR</string>\n  <string name=\"import_qrcode_sd\">استيراد رمز QR</string>\n  <string name=\"successfully_imported_settings\">تم استيراد الإعدادات بنجاح</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">تم الوصول إلى الحد الأقصى من عدد المحارف: لا يمكن توليد كود QR لجميع الإعدادات</string>\n  <string name=\"share\">مشاركة</string>\n  <string name=\"turn_on_flashlight\">تشغيل الإضاءة الخلفية</string>\n  <string name=\"turn_off_flashlight\">إيقاف الإضاءة الخلفية</string>\n  <string name=\"qrcode_with_both_passwords\">يحوي معلومات حساسة: <b> المدير </b> و <b> كلمات مرور المخدم </b></string>\n  <string name=\"qrcode_without_passwords\">رمز الـ QR لا يحوي <b> المدير </b> أو <b> المخدم </b> كلمات مرور، انقر للقيام بالإعداد</string>\n  <string name=\"qrcode_with_admin_password\">يحتوي على معلومات حساسة: <b> كلمة مرور </b> المدير </string>\n  <string name=\"qrcode_with_server_password\">يحوي معلومات حساسة: <b> سيرفر </b> كلمة السر</string>\n  <string name=\"server_password\">كلمة مرور الخادم</string>\n  <string name=\"include_password_dialog\">تضمين كلمات المرور في الكود</string>\n  <string name=\"generate\">توليد</string>\n  <string name=\"invalid_qrcode\">كود QR لا يتضمن إعدادات صحيحة</string>\n  <string name=\"qr_code_not_found\">كود QR غير موجود في الصورة المحددة</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">حسناً</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">إلغاء</string>\n  <string name=\"cancel_loading_form\">إلغاء</string>\n  <string name=\"cancel_location\">إلغاء</string>\n  <string name=\"do_not_change\">إلغاء</string>\n  <string name=\"canceling\">جاري الإلغاء</string>\n  <string name=\"notification_channel_name\">الإشعارات العامة</string>\n  <string name=\"success\">نجاح</string>\n  <string name=\"error_occured\">حدث خطأ</string>\n  <string name=\"please_wait\">الرجاء الانتظار للحظات قليلة.</string>\n  <string name=\"please_wait_long\">الرجاء الانتظار. قد يتطلّب ذلك بضع دقائق.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">لا يمكن حذف  \\'%s\\'.  يرجى حذف الملف يدويا وقم باعادة تحميل الاستمارة.</string>\n  <string name=\"fs_create_media_folder_error\">لا يمكن انشاء ملف وسائط متعددة \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">لا يمكن النسخ  \\'%1$s\\' فوق  \\'%2$s\\'. السبب: \\'%3$s\\'</string>\n  <string name=\"select_odk_shortcut\">اختيار اختصار ODK</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">أضف</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">عنوان غير معروف: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">عنوان غير صحيح: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">لقد تم حذف نموذج معبأ!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">قمت بتحميل استمارتين مختلفتين بنفس رقم النموذج والاصدار. ربما قد يكون نفس النموذج تم تحميله في اوقات مختلفة او في خودام مختلفة. بأي حال يجب عليك حذف واحدة.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">تجاهل</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-bg/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Създай нов формуляр</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Чернови</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Готови за изпращане</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Изпратени</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Свали формуляр</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Изтрий формуляр</string>\n  <string name=\"saved_forms\">Запазени формуляри</string>\n  <string name=\"version_number\">Версия: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Добавено на\\' EEE, MMM dd, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Обновено на\\' EEE, MMM dd, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Записано на\\' EEE, MMM dd, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Завършено на\\' EEE, MMM dd, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Изпратено на\\' EEE, MMM dd, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Неуспешно изпращане на\\' EEE, MMM dd, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Изтрито на\\' EEE, MMM dd, yyyy \\'в\\' HH:mm</string>\n  <string name=\"submission_deleted\">Подаденият формуляр е изтрит</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Редактирано на\\' EEE, MMM dd, yyyy \\'в\\' HH:mm</string>\n  <string name=\"encrypted_form\">Криптиран формуляр</string>\n  <string name=\"deleted_form\">Изтрит формуляр</string>\n  <string name=\"sort_the_list\">Подреждане на списъка</string>\n  <string name=\"sort_by\">Подреди по</string>\n  <string name=\"sort_by_name_asc\">Име, А-Я</string>\n  <string name=\"sort_by_name_desc\">Име, Я-А</string>\n  <string name=\"sort_by_date_desc\">Дата, първо най-новите</string>\n  <string name=\"sort_by_date_asc\">Дата, първо най-старите</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Първо последно записаните</string>\n  <string name=\"filter_the_list\">Филтриране на списъка</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Търсене</string>\n  <string name=\"not_granted_permission\">Тази дейност изисква допълнително разрешение, което не е одобрено.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Резрешения за хранилището</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Без тези разрешения, приложението няма достъп до вашите формуляри и не може да попълва такива. </string>\n  <string name=\"location_runtime_permissions_denied_title\">Рарешения за местоположението</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Без разрешението за <b>Точно местоположение</b> Collect не може да запише вашето местоположение. Моля, опитайте пак, когато сте готови да го одобрите.</string>\n  <string name=\"camera_runtime_permission_denied_title\">Разрешение за камера</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Без това разрешение Collect няма достъп до камерата на устройството. Моля, опитайте пак, когато сте готови да го одобрите.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Разрешение за запис на аудио</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Без това разрешение Collect няма достъп до микрофона на устройството. Моля, опитайте пак, когато сте готови да го одобрите.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Зареди от карта</string>\n  <string name=\"instance_scan_count\">Добавени са %1$d формуляри.</string>\n  <string name=\"finished_disk_scan\">Сканирането приключи. Всички формуляри са заредени.</string>\n  <string name=\"xform_parse_error\">XForm грешка  %1$s: %2$s липсва или е невалидно.</string>\n  <string name=\"loading_form\">Зареждане на формуляр</string>\n  <string name=\"survey_loading_reading_form_message\">Прочитане дефиницията на формуляра…</string>\n  <string name=\"survey_loading_reading_data_message\">Прочитане на данните за проучването…</string>\n  <string name=\"survey_loading_reading_csv_message\">Прочитане на CSV файловете…</string>\n  <string name=\"parse_error\">Съжаляваме. Формулярът не може да бъде анализиран</string>\n  <string name=\"loading_form_failed\">Възникна грешка при зареждане на формуляра. Моля опитайте пак.</string>\n  <string name=\"cannot_edit_completed_form\">Този формуляр не може да се редактира понеже е маркиран като приключен. Възможно е и да е криптиран.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Незапазените промени са възстановени от момента на запис!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Смени език</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Премини до напомнянето</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Запази чернова</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Към началото</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Към края</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Нагоре</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Група</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Повтаряща се група</string>\n  <string name=\"exit\">Изход</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Добавяне на \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Добави</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Добавябе ба друг</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Редактиране на напомняне</string>\n  <string name=\"clearanswer_confirm\">Премахване отговора на \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Премахни отговора</string>\n  <string name=\"clear_answer_ask\">Премахване на този отговор?</string>\n  <string name=\"clear_answer_no\">Отмяна</string>\n  <string name=\"discard_answer\">Премахни отговора</string>\n  <string name=\"discard_group\">Премахни група</string>\n  <string name=\"delete_repeat\">Премахни група</string>\n  <string name=\"delete_repeat_ask\">Премахване на тази група?</string>\n  <string name=\"delete_repeat_confirm\">Премахванае на група \\\"%s\\\" и всички подгрупи?</string>\n  <string name=\"delete_repeat_no\">Отмяна</string>\n  <string name=\"invalid_answer_error\">Oтговорът на този въпрос е невалиден!</string>\n  <string name=\"required_answer_error\">Oтговорът на този въпрос е задължителен!</string>\n  <string name=\"success_form_validation\">Успех! Не са открити грешки във вашият формуляр!</string>\n  <string name=\"invalid_space_in_answer_singular\">Предупреждение: стойността %s съдържа празни символи </string>\n  <string name=\"invalid_space_in_answer_plural\">Предупреждение: стойностите %s съдържат празни символи</string>\n  <string name=\"too_complex_form\">Този формуляр е прекалено сложен за това устройство. Опитайте да го опростите или потърсете помощ във форума.</string>\n  <string name=\"survey_internal_error\">Вътрешна грешка: преминавенето към подсказката е неуспешно.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Вие сте в края на %s</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Ако се налага да правите промени във формуляра, използвайте \\\"Запази като чернова\\\" докато сте готови да го изпратите.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Запази като чернова</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Финализирай</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Изпрати</string>\n  <string name=\"data_saved_error\">Запазването на формуляра беше неуспешно!</string>\n  <string name=\"data_saved_ok\">Успешно запазване на формуляра!</string>\n  <string name=\"save_point_error\">Грешка при запис на точка за възстановяване: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Отхвърли формуляра</string>\n  <string name=\"discard_changes\">Отхвърли промените</string>\n  <string name=\"keep_changes\">Запис на промените</string>\n  <string name=\"keep_editing\">Продължи с редакцията</string>\n  <string name=\"quit_form_title\">Запази формуляра?</string>\n  <string name=\"save_explanation\">Може да запишeте този формуляр и той ще бъде достъпен във вашите чернови по всяко време.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">Този формуляр бе последно записан на\\' EEE, MMM dd, yyyy \\' в \\' HH:mm\\'. Запишете като чернова за да запазите промените.</string>\n  <string name=\"quit_form_continue_title\">Продължи с редактриането?</string>\n  <string name=\"discard_form_warning\">Ако отхвърлите формуляр ще загубите всички промени.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">Този формуляр бе последно записан на\\' EEE, MMM dd, yyyy \\' в \\' HH:mm\\'. Ако отхвърлите промените, ще загубите всички промени направени след това.</string>\n  <string name=\"saving_form\">Запис на формуляр</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Label for server type setting-->\n  <!--Title of dialog for configuring server URL-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Запиши чернова</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Отмяна</string>\n  <string name=\"cancel_loading_form\">Отмяна</string>\n  <string name=\"cancel_location\">Отмяна</string>\n  <string name=\"do_not_change\">Отмяна</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">Добави</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">Черновате не може да бъде редактирана защото съответната бланка не е налична, или е изтрита.\\n\\nID на формуляра: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"learn_more_button_text\">Научи повече</string>\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-bn/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">নিদিষ্ট সময় তারিখ যোগ করেন</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">নিদিষ্ট সময় তারিখ সংরক্ষিত করেন</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">নিদিষ্ট সময় তারিখ ঠিক করেন</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">নিদিষ্ট সময় তারিখ এর মধ্যে পাঠান</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">নিদিষ্ট সময় তারিখ এর মধ্যে পাঠানো সফল হয়নি</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">খোজা শেষ হয়েছে। সকল ফরম যুক্ত হয়েছে।</string>\n  <string name=\"loading_form\">ফরম লোড হচ্ছে </string>\n  <string name=\"parse_error\">দুঃখিত, ফরম উদ্ধার করা সম্ভব হয়নি।</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">ভাষা পরিবর্তন </string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">অনুক্রম  অনুসারে দেখা</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">ফরম শুরুর স্থানে যান</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">ফরম শেষের স্থানে যান</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">আগের প্রশ্নে যান</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"clearanswer_confirm\"> উত্তর মুছে \\\"%s\\\" ফেলতে চান?</string>\n  <string name=\"clear_answer\">উত্তর মুছে ফেলুন</string>\n  <string name=\"clear_answer_ask\">উত্তর মুছে ফেলতে চান?</string>\n  <string name=\"clear_answer_no\">বাতিল</string>\n  <string name=\"discard_answer\">জবাটাকে সরান</string>\n  <string name=\"discard_group\">গ্রুপটাকে সরান </string>\n  <string name=\"delete_repeat\">রিপিট অপসারণ</string>\n  <string name=\"delete_repeat_ask\">রিপিট অপসারন করবেন?</string>\n  <string name=\"delete_repeat_confirm\">\\\"%s\\\" গ্রুপ এবং এর সকল সাব-গ্রুপ অপসারণ করতে চান?</string>\n  <string name=\"delete_repeat_no\">বাতিল</string>\n  <string name=\"invalid_answer_error\">দুঃখিত,উত্তর গ্রহণযোগ্য নয়।</string>\n  <string name=\"required_answer_error\">দুঃখিত, এই উত্তরটি অপরিহার্য ।</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">আপনি %s ফরম শেষে দিকে রয়েছে </string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">দু:খিত, ফরম সংরক্ষিত হয়নি</string>\n  <string name=\"data_saved_ok\">ফরম সংরক্ষিত হয়েছে</string>\n  <string name=\"save_point_error\">ত্রুটি সংরক্ষণ উদ্ধার পয়েন্টঃ %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">পরিবর্তন সংরক্ষণকরুন</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">ফরম সংরক্ষন করা হচ্ছে।</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">launch_app</string>\n  <string name=\"activity_not_found\">পরিচালনা করার মত কোন কর্মকাণ্ড পাওয়া যায় নিঃ %s</string>\n  <string name=\"no_app\">অনুরোধকৃত  application অনুপস্থিত. অনুগ্রহ করে ম্যানোয়েলি তত্ত্ব নিন।</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"capture_osm\">OpenMapKit চালু করুন</string>\n  <string name=\"recapture_osm\">পুনরায় OSM ট্যাগিং করুন</string>\n  <string name=\"get_barcode\">বারকোড নিন</string>\n  <string name=\"replace_barcode\">বারকোড প্রতিস্থাপন করুন</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">ছবি তোলা </string>\n  <string name=\"choose_image\">ছবি পছন্দ করুন</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">সংরক্ষণ করেন এবং বন্ধ করেন</string>\n  <string name=\"sign_button\">সাক্ষর নিন।</string>\n  <string name=\"markup_image\">মার্কআপ চিত্র</string>\n  <string name=\"draw_image\">স্কেচ চিত্র</string>\n  <string name=\"reset_image\">রিসেট</string>\n  <string name=\"set_color\">কালার সেট</string>\n  <string name=\"capture_video\">ভিডিও রেকর্ড</string>\n  <string name=\"choose_video\">ভি ডি ও পছন্দ করুন</string>\n  <string name=\"play_video\">ভিডিও চালান</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">শব্দ রেকর্ড</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">শব্দ নির্বাচন করুন।</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">মিডিয়া \\\"%s\\\" সংযুক্ত করার সময় ত্রুটি সংঘটিত হয়েছে</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">উত্তর নির্বাচন করুন।</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">নির্বাচিত</string>\n  <string name=\"trigger\">ঠিক আছে। দয়া করে সামনের দিকে যান।</string>\n  <string name=\"direction\">অভিমুখ: %s</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">ফাইলঃ %s অকার্যকর।</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">ফাইলঃ %s অনুপস্থিত.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"terrain\">ভূখণ্ড</string>\n  <string name=\"hybrid\">হাইব্রিড</string>\n  <string name=\"satellite\">উপগ্রহ</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">দুঃখিত, অবস্থান প্রদানকারী সেবা বন্ধ আছে!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"start\">শুরু</string>\n  <string name=\"enable_gps\">GPS সক্ষম</string>\n  <string name=\"clear\">মুছে ফেলুন।</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">পাঠানো হচ্ছে ফরম </string>\n  <string name=\"noselect_error\">দুঃখিত, আপনি কোন ফরম  নির্বাচন করেন নি!</string>\n  <string name=\"no_connection\">কোন নেটওয়ার্ক সংযোগ পাওয়া যায় নি</string>\n  <string name=\"sending_items\">%1$s এর %2$s ফরম/(ফরমগুলো) পাঠানো হচ্ছে</string>\n  <string name=\"send_selected_data\">নির্বাচিত ফরমটি পাঠান </string>\n  <string name=\"server_requires_auth\">সার্ভারে প্রমাণীকরণ অপরিহার্য আবশ্যক</string>\n  <string name=\"upload_results\">পাঠানোর ফলাফল</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">View পরিবর্তন</string>\n  <string name=\"show_sent_and_unsent_forms\">sent ফর্ম ও Unsent ফর্ম দেখা</string>\n  <string name=\"show_unsent_forms\">Unsent ফর্ম দেখা</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">সতেজ করা</string>\n  <string name=\"download\">ডাউনলোড করুন </string>\n  <string name=\"downloading_data\">ডাটা সার্ভারের থেকে ডাউনলোড হচ্ছে</string>\n  <string name=\"fetching_file\">নামানো হচ্ছে %1$s.\\n\\n ফরম %2$s এর %3$s ফরম/(ফরমগুলি)</string>\n  <string name=\"version\">ভার্সন</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">ডাটা</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">ফরমগুলি </string>\n  <string name=\"delete_confirm\">ফরম %s মুছে দিন </string>\n  <string name=\"delete_file\">ফাইল ডিলিট</string>\n  <string name=\"delete_no\">মুছবেন না</string>\n  <string name=\"delete_yes\">ফরম মুছে দিন </string>\n  <string name=\"file_deleted_ok\">%s ফরম/(ফরমগুলি) সফলভাবে মুছে গেছে!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">সার্ভার নির্দিষ্টকরণ</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">ইউআরএল </string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"url_error\">দুঃখিত।ইউ আর এল সঠিক নয়</string>\n  <string name=\"username\">ব্যবহারকারীর নাম।</string>\n  <string name=\"change_username\">ব্যবহারকারীর নাম পরিবর্তন </string>\n  <string name=\"password\">পাসওয়ার্ড</string>\n  <string name=\"change_password\">পাসওয়ার্ড পরিবর্তন </string>\n  <string name=\"font_size\">ফন্ট আকার</string>\n  <string name=\"change_font_size\">ফন্ট আকার পরিবর্তন</string>\n  <string name=\"font_size_extra_large\">বিশাল বৃহৎ।</string>\n  <string name=\"font_size_large\">বৃহৎ</string>\n  <string name=\"font_size_medium\">মাঝারি</string>\n  <string name=\"font_size_small\">ক্ষুদ্র</string>\n  <string name=\"font_size_extra_small\">অতি ক্ষুদ্র।</string>\n  <string name=\"navigation\">নেভিগেশান</string>\n  <string name=\"swipe_navigation\">swipe নেভিগেশান</string>\n  <string name=\"buttons_navigation\">buttons নেভিগেশান</string>\n  <string name=\"swipe_buttons_navigation\">swipe buttons নেভিগেশান</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">ফর্মের সামনের দিকে</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"constraint_behavior\">constraint_behavior</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">পাসওয়ার্ড দিন।</string>\n  <string name=\"admin_password_changed\">অ্যাডমিনের পাসওয়ার্ড পরিবর্তন</string>\n  <string name=\"admin_password_disabled\">অ্যাডমিনের পাসওয়ার্ড disabled</string>\n  <string name=\"admin_password_incorrect\">পাসওয়ার্ড সঠিক হয়নি।</string>\n  <string name=\"enter_admin_password\">এডমিন পাসওয়ার্ড দিন।</string>\n  <string name=\"found_in_main\">প্রধান মেনু্তে পাবেন</string>\n  <string name=\"reset_project_settings_title\">রিসেট</string>\n  <string name=\"reset_settings_button_reset\">রিসেট</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">ঠিক আছে</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">বাতিল</string>\n  <string name=\"cancel_loading_form\">বাতিল</string>\n  <string name=\"cancel_location\">বাতিল</string>\n  <string name=\"do_not_change\">বাতিল</string>\n  <string name=\"success\">সাফল্য </string>\n  <string name=\"error_occured\">ত্রুটি ঘটেছে</string>\n  <string name=\"please_wait\">দয়া করে কিছু সময় অপেক্ষা করুন।</string>\n  <string name=\"please_wait_long\">দয়া করে অপেক্ষা করুন। এটি শেষ হতে কয়েক মিনিট সময় লাগতে পারে।</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ca/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">Versió: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Afegit el\\' EEE dd MMM yyyy, HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Desat el\\' EEE dd MMM yyyy, HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Finalitzat el\\' EEE dd MMM yyyy, HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Enviat el\\' EEE dd MMM yyyy, HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Enviament fallat el\\' EEE dd MMM yyyy, HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Esborrat el\\' EEE, dd MMM, yyyy \\'a\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Modificat el\\' EEE, dd-MMM-yyyy \\'a les\\' HH:mm</string>\n  <string name=\"encrypted_form\">Formulari encriptat</string>\n  <string name=\"deleted_form\">Formulari esborrat</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">Filtra la llista</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Exploració finalitzada. Formularis carregats.</string>\n  <string name=\"loading_form\">Carregant formulari</string>\n  <string name=\"survey_loading_reading_form_message\">Llegint definició de formulari…</string>\n  <string name=\"survey_loading_reading_data_message\">LLegint dades de enquesta…</string>\n  <string name=\"survey_loading_reading_csv_message\">LLegint arxius CSV…</string>\n  <string name=\"parse_error\">Ho sentim, incapaç d\\'interpretar el formulari.</string>\n  <string name=\"cannot_edit_completed_form\">No es pot editar un formulari un cop finalitzat.  Pot estar encriptat.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Canvis no desats llegits del punt de recuperació!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Canvia idioma</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Ves a entrada de dades</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Anar a l\\'inici</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Anar al final</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Ves enrera</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"exit\">Sortir</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Editar entrada:</string>\n  <string name=\"clearanswer_confirm\">Eliminar la resposta a \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Eliminar resposta</string>\n  <string name=\"clear_answer_ask\">Eliminar aquesta resposta?</string>\n  <string name=\"clear_answer_no\">Cancel·lar</string>\n  <string name=\"discard_answer\">Descartar resposta</string>\n  <string name=\"discard_group\">Esborrar grup</string>\n  <string name=\"delete_repeat\">Esborrar grup</string>\n  <string name=\"delete_repeat_ask\">Esborrar aquest grup?</string>\n  <string name=\"delete_repeat_confirm\">Esborrar grup \\\"%s\\\" i tots els seus subgrups?</string>\n  <string name=\"delete_repeat_no\">Cancel·lar</string>\n  <string name=\"invalid_answer_error\">Atenció, resposta invàlida!</string>\n  <string name=\"required_answer_error\">Aquesta dada és d\\'entrada obligada!</string>\n  <string name=\"survey_internal_error\">Error intern al demanar la següent dada.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Formulari \\\"%s\\\" finalitzat.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Ho sentim, el formulari no s\\'ha desat be!</string>\n  <string name=\"data_saved_ok\">Formulari desat correctament!</string>\n  <string name=\"save_point_error\">Error al guardar el punt de recuperació: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Desar els canvis</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Desant formulari</string>\n  <string name=\"survey_saving_validating_message\">Validant respostes…</string>\n  <string name=\"survey_saving_collecting_message\">Recollint dades…</string>\n  <string name=\"survey_saving_saving_message\">Salvant a tarja SD …</string>\n  <string name=\"survey_saving_finalizing_message\">Acabant a tarja SD…</string>\n  <string name=\"survey_saving_encrypting_message\">Encriptant dades…</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Llançament</string>\n  <string name=\"activity_not_found\">Cap activitat pendent trobada: %s</string>\n  <string name=\"no_app\">L\\'aplicació sol·licitada no s\\'ha trobat.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Obrir Url</string>\n  <string name=\"capture_osm\">Executa OpenMapKit</string>\n  <string name=\"recapture_osm\">Refer signatura OSM</string>\n  <string name=\"edited_osm_file\">Arxiu OSM XML editat:</string>\n  <string name=\"invalid_osm_data\">Alguna cosa ha fallat. No s\\'han llegit dades OSM vàlides.</string>\n  <string name=\"alert\">Alerta</string>\n  <string name=\"install_openmapkit\">Si us plau, instal·la OpenMapKit!</string>\n  <string name=\"get_barcode\">Llegir Codi de Barres</string>\n  <string name=\"replace_barcode\">Reemplaça Codi de Barres</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Pren foto</string>\n  <string name=\"choose_image\">Escollir imatge</string>\n  <string name=\"selected_invalid_image\">L\\'arxiu seleccionat no és una imatge vàlida</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Salva i Tanca</string>\n  <string name=\"sign_button\">Afegir Signatura</string>\n  <string name=\"markup_image\">Anotacions Imatge</string>\n  <string name=\"draw_image\">Esbós</string>\n  <string name=\"reset_image\">Reset</string>\n  <string name=\"set_color\">Definir Color</string>\n  <string name=\"capture_video\">Enregistrar Vídeo</string>\n  <string name=\"choose_video\">Escollir vídeo</string>\n  <string name=\"play_video\">Reprodueix Vídeo</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Enregistrar so</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Escollir so</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">Error detectat al connectar medi: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Seleccionar resposta:</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Seleccionat:</string>\n  <string name=\"trigger\">D\\'acord. Si us plau, continuar.</string>\n  <string name=\"ext_file_no_data_error\">L\\'arxiu no conté dades!</string>\n  <string name=\"ext_conflicting_columns_error\">Columnes %s coincideixen!</string>\n  <string name=\"ext_sortBy_numeric_error\">La columna de classificació ha de tenir només valors numèrics. Valor conflictiu: \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Error sintàctic a la funció de cercar(): La funció necessita 1, 4 o 6 arguments.</string>\n  <string name=\"ext_search_wrong_function_error\">Error sintàctic a funció cercar(): Funció no reconeguda \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Error sintàctic a la funció de cercar(): \\'%s\\' no s\\'ha avaluat com a funció.</string>\n  <string name=\"ext_search_return_error\">El cercador ha retornat un objecte de tipus \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">No pot importar dades des de %1$s. Raó: %2$s</string>\n  <string name=\"ext_import_progress_message\">Precàrregant dades des de \\'%1$s\\', espereu si us plau… %2$s</string>\n  <string name=\"ext_import_finalizing_message\">Acabant dades precarregades…</string>\n  <string name=\"ext_import_completed_message\">Lectura de dades completada!</string>\n  <string name=\"ext_not_initialized_error\">GestorDadesExternes no s\\'ha inicialitzat</string>\n  <string name=\"ext_import_csv_missing_error\">Dades externes per %1$s no s\\'han importat. Pot ser heu oblidat incloure l\\'arxiu %2$s.csv amb el formulari?</string>\n  <string name=\"ext_search_generic_error\">Error sintàctic a la funció cerca(): %s</string>\n  <string name=\"ext_assign_value_error\">No pot assignar el valor a \\'%s\\'</string>\n  <string name=\"parser_exception\">Exceptio XPathParser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Gravar rumb</string>\n  <string name=\"replace_bearing\">Reemplaçar rumb</string>\n  <string name=\"getting_bearing\">Carregant rumb</string>\n  <string name=\"accept_bearing\">Gravar rumb</string>\n  <string name=\"bearing\">Rumb: %.3f</string>\n  <string name=\"direction\">Direcció: %s</string>\n  <string name=\"select_date\">Selecciona data</string>\n  <string name=\"select_time\">Selecciona hora</string>\n  <string name=\"no_date_selected\">Data no seleccionada</string>\n  <string name=\"no_time_selected\">Hora no seleccionada</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Arxiu: %s és invàlid.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Arxiu: %s no s\\'ha trobat.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"streets\">Carrers</string>\n  <string name=\"terrain\">Terreny</string>\n  <string name=\"hybrid\">Híbrid</string>\n  <string name=\"satellite\">Satel·lit</string>\n  <string name=\"google_play_services_error_occured\">No pot accedir a Google Maps. Teniu el servei instal·lat ?</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Atenció, la Locatització està desactivada!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"start\">Comença</string>\n  <string name=\"enable_gps\">Activa GPS</string>\n  <string name=\"geo_clear_warning\">Característica ja creada. Vols netejar-la?</string>\n  <string name=\"geo_exit_warning\">Descartar canvis i tornar a ODK?</string>\n  <string name=\"polygon_validator\">Per crear un polígon calen mínim 3 punts </string>\n  <string name=\"clear\">Neteja</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"geopoint_no_draggable_instruction\">Toca el botó afegir fita</string>\n  <string name=\"discard\">Descarta</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Enviant formulari</string>\n  <string name=\"noselect_error\">Atenció, no hi ha cap formulari seleccionat!</string>\n  <string name=\"no_connection\">No hi ha connexió a xarxa disponible</string>\n  <string name=\"sending_items\">Enviant %1$s de %2$s formulari(s)</string>\n  <string name=\"send_selected_data\">Enviar seleccionats</string>\n  <string name=\"server_requires_auth\">El servidor necessita Autenticació</string>\n  <string name=\"no_forms_uploaded\">No s\\'ha carregat cap formulari.</string>\n  <string name=\"upload_results\">Resultats de la càrrega</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Canviar vista</string>\n  <string name=\"show_sent_and_unsent_forms\">Formularis enviats i pendents</string>\n  <string name=\"show_unsent_forms\">Nomes Formularis pendents d\\'enviar</string>\n  <string name=\"send_in_progress\">Enviament funcionant, si us plau, re-intenteu en un moment.</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Refrescar</string>\n  <string name=\"download\">Descarregar seleccionats</string>\n  <string name=\"downloading_data\">Connectant a servidor</string>\n  <string name=\"fetching_file\">Obtenint %1$s.\\n\\nForm %2$s de %3$s formulari(s).</string>\n  <string name=\"form_download_progress\">%1$s. Obtenint arxius multimèdia: %2$s de %3$s</string>\n  <string name=\"version\">Versió:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">F.desats</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">F.buits</string>\n  <string name=\"delete_confirm\">Eliminar %s formulari(s)?</string>\n  <string name=\"delete_file\">Eliminar seleccionats</string>\n  <string name=\"delete_no\">No eliminar</string>\n  <string name=\"delete_yes\">Esborrar formularis</string>\n  <string name=\"file_deleted_ok\">%s formularis(s) eliminats correctament!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Servidor</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Servidor</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL Servidor</string>\n  <string name=\"url_error\">La URL és invàlida!</string>\n  <string name=\"username\">Nom d\\'usuari</string>\n  <string name=\"change_username\">nom d\\'usuari ODK</string>\n  <string name=\"password\">Clau d\\'accés</string>\n  <string name=\"change_password\">Clau d\\'accés ODK</string>\n  <string name=\"password_error_whitespace\">No es permeten espais davant o darrera una clau d\\'accés</string>\n  <string name=\"username_error_whitespace\">No es permeten espais davant o darrera un nom d\\'usuari</string>\n  <string name=\"language\">Idioma</string>\n  <string name=\"font_size\">Mida del text</string>\n  <string name=\"change_font_size\">Mida del text</string>\n  <string name=\"font_size_extra_large\">Extra gran</string>\n  <string name=\"font_size_large\">Gran</string>\n  <string name=\"font_size_medium\">Mitjana</string>\n  <string name=\"font_size_small\">Petita</string>\n  <string name=\"font_size_extra_small\">Extra petita</string>\n  <string name=\"navigation\">Navegació</string>\n  <string name=\"swipe_navigation\">Usar desplaçament horizontal</string>\n  <string name=\"buttons_navigation\">Usar botons avança/enrere</string>\n  <string name=\"swipe_buttons_navigation\">Usar desplaçaments i botons</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Següent</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"delete_after_send\">Esborra despres d\\'enviar</string>\n  <string name=\"delete_after_send_summary\">Esborra formularis tancats i arxius multimedia després d\\'enviar a servidor</string>\n  <string name=\"constraint_behavior\">Comportament del processament de restriccions</string>\n  <string name=\"constraint_behavior_on_swipe\">Validar al moure endavant</string>\n  <string name=\"constraint_behavior_on_finalize\">Ajornar validació fins al final</string>\n  <string name=\"high_resolution_summary\">Activant enregistrament de  vídeos HD</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"analytics_preferences\">Us de dades</string>\n  <string name=\"user_defined\">definit per l\\'usuari</string>\n  <string name=\"phone_number\">Telèfon</string>\n  <string name=\"email\">Direcció email</string>\n  <string name=\"device_defined\">definit pel dispositiu</string>\n  <string name=\"device_id\">ID dispositiu</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Entrar nova clau d\\'accés</string>\n  <string name=\"admin_password\">Clau d\\'administrador</string>\n  <string name=\"admin_password_changed\">Clau d\\'accés d\\'Administrador canviat</string>\n  <string name=\"admin_password_disabled\">Clau d\\'accés d\\'Administrador desactivat</string>\n  <string name=\"admin_password_incorrect\">La clau d\\'accés és incorrecte!</string>\n  <string name=\"enter_admin_password\">Entrar clau d\\'accés Administrador</string>\n  <string name=\"change_admin_password\">Canviar clau d\\'administrador</string>\n  <string name=\"show_password\">Mostra clau d\\'accés</string>\n  <string name=\"found_in_main\">Desmarcar per amagar en el Menú Principal</string>\n  <string name=\"reset_project_settings_title\">Reinicialitza</string>\n  <string name=\"select_all\">Selecciona tot</string>\n  <string name=\"clear_all\">Neteja-ho tot</string>\n  <string name=\"reset_settings_button_reset\">Reinicialitza</string>\n  <string name=\"reset_in_progress\">Reinicialitzant…</string>\n  <string name=\"reset_app_state_result\">Reinicialitzar resultats</string>\n  <string name=\"reset_settings_result\">Totes les característiques:: %s</string>\n  <string name=\"reset_saved_forms_result\">Formularis desats :: %s</string>\n  <string name=\"main_menu_settings\">Menú principal de configuració</string>\n  <string name=\"user_settings\">Configuració d\\'usuari</string>\n  <string name=\"form_entry_setting\">Característiques d\\'omplir formulari</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Sobre</string>\n  <string name=\"odk_website\">Visita la web d\\'ODK</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"successfully_imported_settings\">Configuració importada correctament</string>\n  <string name=\"share\">Comparteix</string>\n  <string name=\"server_password\">Clau d\\'accés servidor</string>\n  <string name=\"include_password_dialog\">Claus d\\'accés incloses al codi</string>\n  <string name=\"generate\">Genera</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">D\\'acord</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Cancel·lar</string>\n  <string name=\"cancel_loading_form\">Cancel·lar</string>\n  <string name=\"cancel_location\">Cancel·lar</string>\n  <string name=\"do_not_change\">Cancel·lar</string>\n  <string name=\"success\">Correcte</string>\n  <string name=\"error_occured\">S\\'ha produït un error</string>\n  <string name=\"please_wait\">Un moment si us plau.</string>\n  <string name=\"please_wait_long\">Un moment si us plau. Això pot trigar uns minuts.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">No pot esborrar \\'%s\\': Si us plau, esborrar l\\'arxiu manualment i recarregar el formulari.</string>\n  <string name=\"fs_create_media_folder_error\">No pot crear la carpeta de medis \\'%s\\'</string>\n  <string name=\"fs_file_copy_error\">No pot copiar \\'%1$s\\' sobre \\'%2$s\\'. Raó: %3$s</string>\n  <string name=\"select_odk_shortcut\">Selecciona drecera ODK</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">URI no reconeguda: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">URI dolenta: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Heu descarregat dos formularis diferents amb el mateix ID i versió. Potser son formularis repetits o procedents de dos servidors diferents. Cal esborrar-ne un.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Descarta</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-cs/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Začít nový formulář</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Návrhy</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Připraveno k odeslání</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Odeslánoo</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Stáhout formulář</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Smazat formulář</string>\n  <string name=\"saved_forms\">Uložené formuláře</string>\n  <string name=\"version_number\">Verze: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Přidáno\\' EEE, dd MMM, yyyy \\'v\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Aktualizováno dne\\' EEE, MMM dd, yyyy \\'v\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Uloženo\\' EEE, dd MMM, yyyy \\'v\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Dokončeno\\' EEE, dd MMM, yyyy \\'v\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Odesláno\\' EEE, dd MMM, yyyy \\'v\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Odesílání selhalo\\' EEE, dd MMM, yyyy \\'v\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Smazáno\\' EEE, MMM dd, yyyy \\'v\\' HH:mm</string>\n  <string name=\"submission_deleted\">Odeslání bylo smazáno</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Upraveno v \\' EEE, dd MMM, yyyy \\'v\\' HH:mm</string>\n  <string name=\"encrypted_form\">Šifrovaný formulář</string>\n  <string name=\"deleted_form\">Smazaný formulář</string>\n  <string name=\"sort_the_list\">Řazení seznamu</string>\n  <string name=\"sort_by\">Seřazeno podle</string>\n  <string name=\"sort_by_name_asc\">Název, A-Z</string>\n  <string name=\"sort_by_name_desc\">Název, Z-A</string>\n  <string name=\"sort_by_date_desc\">Datum, nejnovější první</string>\n  <string name=\"sort_by_date_asc\">Datum, nejstarší první</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Poslední uložený jako první</string>\n  <string name=\"filter_the_list\">Filtrovat výpis</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Hledat</string>\n  <string name=\"not_granted_permission\">Aktivita, kterou se pokoušíte spustit, vyžaduje oprávnění, které není uděleno.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Povolení pro ukládání dat</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Bez těchto oprávnění nelze přistupovat k formulářům nebo ukládat odpovědi. Opětovně otevřete aplikaci, až budete připraveni je udělit.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Oprávnění umístění</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Bez povolení <b>Přesné lokace</b>Collect nemůže zaznamenávat vaši polohu. Zkuste to znovu, až budete připraveni povolení udělit.</string>\n  <string name=\"camera_runtime_permission_denied_title\">Oprávnění k fotoaparátu</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Bez tohoto povolení nelze přistupovat k fotoaparátu. Zkuste to prosím znovu, když jste připraveni jej udělit.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Oprávnění k nahrávání zvuku</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Bez tohoto povolení nelze získat přístup k mikrofonu. Zkuste to prosím znovu, když jste připraveni jej udělit.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Otevřít mapu formulářů</string>\n  <string name=\"instance_scan_count\">%1$d formulářů přidáno. </string>\n  <string name=\"finished_disk_scan\">Prohledávání dokončeno. Všechny dotazníky načteny.</string>\n  <string name=\"xform_parse_error\">Chyba analýzy XForm pro %1$s : %2$s chybí nebo je neplatná.</string>\n  <string name=\"loading_form\">Načítám dotazník</string>\n  <string name=\"survey_loading_reading_form_message\">Načítám nastavení dotazníku…</string>\n  <string name=\"survey_loading_reading_data_message\">Načítám data dotazníků…</string>\n  <string name=\"survey_loading_reading_csv_message\">Načítám CSV soubory…</string>\n  <string name=\"parse_error\">Dotazník nejde načíst.</string>\n  <string name=\"loading_form_failed\">Během načítání formuláře došlo k chybě. Prosím zkuste to znovu.</string>\n  <string name=\"cannot_edit_completed_form\">Tento formulář nelze upravit, jakmile byl označen jako dokončený. Může být zašifrovaný.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Neuložené změny obnoveny z bodu obnovy!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Změnit jazyk</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Přejít k otázce</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Uložit jako návrh</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Přejít na začátek</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Přejít na konec</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Přejít nahoru</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <string name=\"edit_finalized_form\">Upravit finální formulář</string>\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Skupina</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Opakovatelná skupina</string>\n  <string name=\"exit\">Odejít</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Přidat \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Přidat</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"do_not_add_repeat\">Nepřidávat</string>\n  <string name=\"add_another_menu\">Přidat další</string>\n  <string name=\"add_another_question\">Přidat další?</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Editovat otázku</string>\n  <string name=\"clearanswer_confirm\">Odstranit odpověď na \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Odstranit odpověď</string>\n  <string name=\"clear_answer_ask\">Odstranit tuto odpověď?</string>\n  <string name=\"clear_answer_no\">Zrušit</string>\n  <string name=\"discard_answer\">Smazat odpověď</string>\n  <string name=\"discard_group\">Odstranit skupinu</string>\n  <string name=\"delete_repeat\">Odstranit skupinu</string>\n  <string name=\"delete_repeat_ask\">Odstranit tuto skupinu?</string>\n  <string name=\"delete_repeat_confirm\">Odstranit skupinu \\\"%s\\\" a všechny její podskupiny?</string>\n  <string name=\"delete_repeat_no\">Zrušit</string>\n  <string name=\"invalid_answer_error\">Tato odpověď je neplatná!</string>\n  <string name=\"required_answer_error\">Tato odpověď je povinná!</string>\n  <string name=\"success_form_validation\">Úspěch! Ve formuláři nebyly nalezeny žádné chyby.</string>\n  <string name=\"invalid_space_in_answer_singular\">Upozornění: základní hodnota %s má mezery</string>\n  <string name=\"invalid_space_in_answer_plural\">Upozornění: základní hodnoty %s mají mezery</string>\n  <string name=\"too_complex_form\">Tento formulář je pro toto zařízení příliš složitý. Zkuste zjednodušit výrazy nebo požádat o pomoc na fóru.</string>\n  <string name=\"survey_internal_error\">Vnitřní chyba: přechod k otázce selhal.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Jste na konci %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <string name=\"form_editing_disabled_after_finalizing\">Po dokončení nelze provádět žádné úpravy.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <string name=\"form_editing_disabled_after_sending\">Po odeslání nelze provádět žádné úpravy.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_finalizing\">Úpravy lze provádět i po dokončení.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_sending\">Úpravy lze provádět i po odeslání.</string>\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Pokud potřebujete formulář upravit, uložte jej jako návrh, dokud nebudete připraveni k odeslání.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <string name=\"form_editing_enabled_after_finalizing_hint\">Chcete-li provést změny po dokončení formuláře, přejděte do části „Připraveno k odeslání“, otevřete formulář a klepněte na ikonu úprav.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <string name=\"form_editing_enabled_after_sending_hint\">Chcete-li provést změny po odeslání formuláře, přejděte do sekce „Odeslané“, otevřete formulář a klepněte na ikonu úprav.</string>\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Uložit jako návrh</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Dokončit</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Odeslat</string>\n  <string name=\"data_saved_error\">Uložení dotazníku selhalo!</string>\n  <string name=\"data_saved_ok\">Dotazník úspěšně uložen!</string>\n  <string name=\"save_point_error\">Chyba při ukládání bodu obnovení: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Zrušit formulář</string>\n  <string name=\"discard_changes\">Zrušit změny</string>\n  <string name=\"keep_changes\">Uložit změny</string>\n  <string name=\"keep_editing\">Pokračujte v úpravách</string>\n  <string name=\"quit_form_title\">Uložit formulář?</string>\n  <string name=\"save_explanation\">Tento formulář můžete uložit a kdykoli k němu přistupovat z návrhů.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'Tento formulář byl naposledy uložen dne\\' EEE, MMM dd, yyyy \\'v\\' HH:mm\\'. Uložit jako návrh pro zachování změn.\\'</string>\n  <string name=\"quit_form_continue_title\">Pokračovat v úpravách?</string>\n  <string name=\"discard_form_warning\">Pokud formulář zahodíte, ztratíte všechny dosud provedené změny.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'Tento formulář byl naposledy uložen dne\\' EEE, MMM dd, yyyy \\'v\\' HH:mm\\'. Pokud změny zahodíte, ztratíte všechny změny, které jste od té doby provedli.\\'</string>\n  <string name=\"saving_form\">Ukládám dotazník</string>\n  <string name=\"survey_saving_validating_message\">Ověřuji odpovědi…</string>\n  <string name=\"survey_saving_collecting_message\">Sbírám data…</string>\n  <string name=\"survey_saving_saving_message\">Ukládám na SD kartu…</string>\n  <string name=\"survey_saving_finalizing_message\">Dokončuji ukládání na SD kartu…</string>\n  <string name=\"survey_saving_encrypting_message\">Šifruji data…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Není přesně jeden záznam pro tento případ!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Neexistuje přesně jeden prázdný formulář, který odpovídá tomuto jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Nezadán žádný FormId ?</string>\n  <string name=\"phone_does_not_support_rsa\">Telefon nepodporuje šifrování RSA.</string>\n  <string name=\"invalid_rsa_public_key\">Neplatný veřejný klíč RSA.</string>\n  <string name=\"encryption_error_message\">%s Formulář nebyl uložen jako dokončený.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">Formulář nelze použít</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Záznam se nepodařilo spustit.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Mikrofon se již používá.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">Médium nelze připojit k formuláři, ale je k dispozici na %1$s</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Nahrávání zakázáno. Povolit v %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Selhání při pokusu o aktualizaci hodnot. Důvodem je obvykle nesprávné použití výpočtů v návrhu formuláře.\\n\\nPokud se tento problém opakuje, nahlaste jej osobě, která vás požádala o sběr dat.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Spustit</string>\n  <string name=\"activity_not_found\">Nenalezena žádná aktivita k řešení: %s</string>\n  <string name=\"no_app\">Požadovaná aplikace chybí. Zadejte prosím údaje ručně.</string>\n  <string name=\"null_intent_value\">Externí aplikace neposkytla očekávané informace.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">Tisk</string>\n  <string name=\"open_url\">Otevřít URL</string>\n  <string name=\"capture_osm\">Spustit OpenMapKit</string>\n  <string name=\"recapture_osm\">Znovu provést OSM tagování</string>\n  <string name=\"edited_osm_file\">Upravený soubor XML OSM:</string>\n  <string name=\"invalid_osm_data\">Něco se pokazilo. Nezískali jsme platné údaje OSM.</string>\n  <string name=\"alert\">Výstraha</string>\n  <string name=\"install_openmapkit\">Prosím nainstalujte OpenMapKit!</string>\n  <string name=\"get_barcode\">Získat čárový kód</string>\n  <string name=\"replace_barcode\">Změnit čárový kód</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Vložte čárový kód do obdélníku</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Otevřít soubor</string>\n  <string name=\"rank_items\">Pořadí položek</string>\n  <string name=\"number_picker_title\">Výběr čísla</string>\n  <string name=\"invalid_range_widget\">Tento widget obsahuje neplatné parametry!</string>\n  <string name=\"svg_file_does_not_exist\">Soubor SVG neexistuje!</string>\n  <string name=\"capture_image\">Udělat fotku</string>\n  <string name=\"choose_image\">Vyberte obrázek</string>\n  <string name=\"selected_invalid_image\">Vybraný soubor není platný obrázek</string>\n  <string name=\"take_picture_instruction\">Klepnutím na obrazovku udělejte snímek</string>\n  <string name=\"error_front_camera_unavailable\">Přední fotoaparát není v tomto zařízení k dispozici</string>\n  <string name=\"camera_error\">Při pořizování snímku došlo k chybě</string>\n  <string name=\"camera_failed_to_initialize\">Nelze inicializovat fotoaparát!</string>\n  <string name=\"annotate_image\">Přidat popisek obrázku</string>\n  <string name=\"gif_not_supported\">Soubory Gif nejsou podporovány</string>\n  <string name=\"invalid_file_type\">Aplikace vrátila neplatný typ souboru</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_explanation\">Chcete před ukončením uložit změny?</string>\n  <string name=\"save_and_close\">Uložit a zavřít</string>\n  <string name=\"signature_capture\">Zachycení podpisu</string>\n  <string name=\"sign_button\">Získat podpis</string>\n  <string name=\"markup_image\">Markup Image</string>\n  <string name=\"draw_image\">Sketch Image</string>\n  <string name=\"reset_image\">Resetovat</string>\n  <string name=\"set_color\">Nastavit barvu</string>\n  <string name=\"capture_video\">Nahrát video</string>\n  <string name=\"choose_video\">Vyberte video</string>\n  <string name=\"play_video\">Přehrát video</string>\n  <string name=\"choose_file\">Vybrat soubor</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Vymazat</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Nahrát zvuk</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Vyberte zvuk</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Pauza</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Pokračovat</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">Záznam zvuku: Zapnuto</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">Záznam zvuku: Vypnuto</string>\n  <string name=\"background_audio_permission_explanation\">This form records audio in the background. You must grant permission to use your microphone. If you do not, you will not be able to open this form.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Tento formulář požaduje záznam zvuku na pozadí. Jeho deaktivací se zastaví nahrávání a zahodí se stávající zvuk. Opravdu chcete pokračovat?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Zakázat nahrávání</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">Nahrávání nezačne okamžitě. Chcete-li nahrávat, musíte znovu otevřít formulář.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Smazat soubor?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">Jakmile tento soubor odstraníte, nebudete jej moci obnovit. Opravdu chcete pokračovat?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Stop</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Záznam…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Záznam zvuku</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Přehrát audio</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Ukládání souboru</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Před opuštěním této obrazovky musíte zastavit nahrávání.</string>\n  <string name=\"error_attaching_binary_file\">Při vkládání médií: %s nastala chyba</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Vybrat hodnotu</string>\n  <string name=\"select_answer\">Zvolte odpověď</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Upravit hodnotu</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Žádná hodnota nebyla vybrána</string>\n  <string name=\"selected_title\">Vybráno:</string>\n  <string name=\"trigger\">OK. Pokračujte prosím.</string>\n  <string name=\"ext_file_no_data_error\">Soubor neobsahuje žádná data!</string>\n  <string name=\"ext_conflicting_columns_error\">Sloupce %s souhlasí!</string>\n  <string name=\"ext_sortBy_numeric_error\">Sloupec pro řazení smí obsahovat jen číselné hodnoty. Problematická hodnota je \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Chyba zadání ve funkci search() : Funkce potřebuje 1, 4 nebo 6 parametrů</string>\n  <string name=\"ext_search_wrong_function_error\">Chyba zadání ve funkci search() : Neznámá funkce \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Chyba zadání ve funkci search() : \\'%s\\' není funkce.</string>\n  <string name=\"ext_search_return_error\">Vyhledávání vrátilo objekt typu \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Nemohu importovat data z %1$s. Důvod: %2$s</string>\n  <string name=\"ext_import_progress_message\">Nahrávám data z \\'%1$s\\', počkejte prosím… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Čtení dat zrušeno!</string>\n  <string name=\"ext_import_finalizing_message\">Dokončuji nahraná data…</string>\n  <string name=\"ext_import_completed_message\">Načtení dat dokončeno!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager nebyl spuštěn.</string>\n  <string name=\"ext_import_csv_missing_error\">Externí data pro %1$s nebyla importována. Možná jste zapomněl/a přidat k dotazníku soubor %2$s.csv?</string>\n  <string name=\"ext_search_generic_error\">Chyba zadání ve funkci search() : %s</string>\n  <string name=\"ext_assign_value_error\">Nemohu přidělit hodnotu k \\'%s\\'.</string>\n  <string name=\"parser_exception\">Výjimka XPathParser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Zaznamenat orientaci</string>\n  <string name=\"replace_bearing\">Nahradit orientaci</string>\n  <string name=\"getting_bearing\">Nahrávám orientaci</string>\n  <string name=\"accept_bearing\">Zaznamenat orientaci</string>\n  <string name=\"bearing\">Orientace: %.3f</string>\n  <string name=\"direction\">Pokyny: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Orientaci nelze zjistit: na zařízení chybí akcelerometr, senzor magnetického pole nebo obojí.</string>\n  <string name=\"select_date\">Vybrat datum</string>\n  <string name=\"select_time\">Vybrat čas</string>\n  <string name=\"no_date_selected\">Nevybrán žádný datum</string>\n  <string name=\"no_time_selected\">Nevybrán žádný čas</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <string name=\"buddhist_month_1\">มกราคม</string>\n  <!--2nd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_2\">กุมภาพันธ์</string>\n  <!--3rd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_3\">มีนาคม</string>\n  <!--4th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_4\">เมษายน</string>\n  <!--5th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_5\">พฤษภาคม</string>\n  <!--6th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_6\">มิถุนายน</string>\n  <!--7th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_7\">กรกฎาคม</string>\n  <!--8th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_8\">สิงหาคม</string>\n  <!--9th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_9\">กันยายน</string>\n  <!--10th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_10\">ตุลาคม</string>\n  <!--11th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_11\">พฤศจิกายน</string>\n  <!--12th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_12\">ธันวาคม</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Soubor: %s není platný.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Soubor: %s chybí.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Zadat identitu</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Identita</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Tento formulář vyžaduje, abyste se identifikovali</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Zavřít</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Důvod změn</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Důvod</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Musíte vysvětlit důvod změn tohoto formuláře</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Uložit</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">Vybrat místo</string>\n  <string name=\"choices\">Možnosti</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">Topologická mapa USGS National Map </string>\n  <string name=\"openmap_usgs_sat\">Hybridní mapa USGS National Map</string>\n  <string name=\"openmap_usgs_img\">Snímková mapa USGS National Map</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Základní mapa</string>\n  <string name=\"basemap_source\">Zdroj</string>\n  <string name=\"basemap_source_unavailable\">Je nám líto, ale v tomto zařízení nejsou k dispozici základní mapy %s. Vyberte prosím jinou podkladovou mapu v Nastavení &gt; Mapy.</string>\n  <string name=\"map_style_label\">%s styl mapy</string>\n  <string name=\"streets\">Ulice</string>\n  <string name=\"terrain\">Terén</string>\n  <string name=\"hybrid\">Hybridní</string>\n  <string name=\"satellite\">Satelitní</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Světlý</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Tmavý</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Přírodní</string>\n  <string name=\"topographic\">Topografický</string>\n  <string name=\"google_play_services_error_occured\">Nelze přistupovat k Mapám Google. Je služba Google Play nainstalována?</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Temná hmota</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">Offline vrstvy</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">Vrstva</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">Vyberte offline vrstvu</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">Vyberte offline vrstvu, která se má použít pro všechny mapy v tomto projektu. Do seznamu můžete přidat možnosti výběrem souboru MBTiles ze zařízení a také odstranit stávající vrstvy ze seznamu.</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">Další informace o přidání dlaždic MBTiles.</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">Přidat vrstvy</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">Smazat vrstvu</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <plurals name=\"non_mbtiles_files_selected_title\">\n    <item quantity=\"one\">%d  vrstvu nelze přidat</item>\n    <item quantity=\"few\">%d  vrstvy nelze přidat</item>\n    <item quantity=\"many\">%d  vrstev nelze přidat</item>\n    <item quantity=\"other\">%d  vrstev nelze přidat</item>\n  </plurals>\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">Vybrané soubory nejsou MBTiles. Můžete přidávat pouze soubory MBTiles.</string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">Některé z vybraných souborů nejsou dlaždice MBT. Můžete přidávat pouze soubory MBTiles.</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">Vrstvy</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">Zvolte přístup k vrstvám</string>\n  <string name=\"select_layer_access_subtitle\">Chcete, aby byly vrstvy dostupné ve všech projektech, nebo pouze v aktuálním projektu?</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">Všechny projekty</string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">Pouze aktuální projekt</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">Opravdu chcete odstranit %1$s vrstvu offline?</string>\n  <string name=\"record_geopoint\">Zaznamenat bod</string>\n  <string name=\"provider_disabled_error\">Nástroje pro získání polohy jsou zakázané!</string>\n  <string name=\"google_play_services_not_available\">Tento formulář chce sledovat vaši polohu, ale služby Google Play nejsou k dispozici.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Tento formulář chce sledovat vaši polohu, ale poskytovatelé polohy jsou zakázáni. Zvolte prosím nastavení v systému Android.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Jít do nastavení</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">Sledování polohy: zapnuto</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">Sledování polohy: vypnuto</string>\n  <string name=\"background_location_disabled\">Tento formulář chce sledovat vaši polohu, ale sledování je zakázáno. Aktivujte prosím v nabídce %1$s výše.</string>\n  <string name=\"background_location_enabled\">Tento formulář sleduje vaši polohu. Sledování můžete zakázat v nabídce %1$s výše.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">Kontrola chyb</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">Uplynulý čas: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">Bod bude uložen na %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">Satelity: %1$s</string>\n  <string name=\"waiting_for_location\">Snažíme se zjistit polohu. Počkejte prosím.</string>\n  <string name=\"poor_accuracy\">Špatná přesnost. Počkejte prosím.</string>\n  <string name=\"unacceptable_accuracy\">Nepřijatelná přesnost. Počkejte prosím.</string>\n  <string name=\"improving_accuracy\">Zlepšení přesnosti. Počkejte prosím.</string>\n  <string name=\"accuracy_status_title\">Zjištění polohy</string>\n  <!--The accuracy of a location fix in meters-->\n  <string name=\"accuracy_m\">%1$s m</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">Západně %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">Východně %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">Jižně %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">Severně %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Vstupní metoda</string>\n  <string name=\"start\">Začít</string>\n  <string name=\"enable_gps\">Zapnout GPS</string>\n  <string name=\"gps_enable_message\">GPS je ve vašem zařízení deaktivováno. Chcete ho povolit?</string>\n  <string name=\"geo_clear_warning\">Funkce již vytvořena. Chcete tuto funkci vymazat?</string>\n  <string name=\"geo_exit_warning\">Zrušit změny a vrátit se do ODK?</string>\n  <string name=\"polygon_validator\">Musí mít alespoň 3 body pro vytvoření polygonu</string>\n  <string name=\"polyline_validator\">Musí mít alespoň 2 body k vytvoření křivky</string>\n  <string name=\"clear\">Vymazat</string>\n  <string name=\"placement_mode\">Umístění poklepáním</string>\n  <string name=\"manual_mode\">Ruční záznam polohy</string>\n  <string name=\"automatic_mode\">Automatické nahrávání polohy</string>\n  <string name=\"recording_interval\">Interval záznamu:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d sekunda</item>\n    <item quantity=\"few\">%d sekundy</item>\n    <item quantity=\"many\">%d sekund</item>\n    <item quantity=\"other\">%d sekund</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d minuta</item>\n    <item quantity=\"few\">%d minuty</item>\n    <item quantity=\"many\">%d minut</item>\n    <item quantity=\"other\">%d minut</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Požadavek na přesnost:</string>\n  <string name=\"none\">Žádný</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d metr</item>\n    <item quantity=\"few\">%d metry</item>\n    <item quantity=\"many\">%d metrů</item>\n    <item quantity=\"other\">%d metrů</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Zadané body: %d</string>\n  <string name=\"collection_status_placement\">Zadané body: %d (klepnutím umístíte body)</string>\n  <string name=\"collection_status_manual\">Zadané body: %d (ruční nahrávání)</string>\n  <string name=\"collection_status_auto_seconds\">Zadané body: %1$d (záznam každých %2$d sec)</string>\n  <string name=\"collection_status_auto_minutes\">Zadané body: %1$d (záznam každých %2$d min)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Zadané body: %1$d (záznam každé %2$d sec, přesnost %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Zadané body: %1$d (záznam každé %2$d min, přesnost %3$d m)</string>\n  <string name=\"geopoint_instruction\">Dlouhým stisknutím umístíte značku nebo klepnutím přidáte značku.</string>\n  <string name=\"geopoint_no_draggable_instruction\">Klepnutím na tlačítko přidat značku.</string>\n  <string name=\"discard\">Vyřadit</string>\n  <string name=\"select_item_count\">%s: %d (%d zobrazeno na mapě)</string>\n  <string name=\"select_item\">Vybrat</string>\n  <string name=\"new_item\">Nová položka</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">Upravit návrh</string>\n  <string name=\"view_data\">Zobrazit uložený formulář</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">Sledování polohy</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">Sledování polohy…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Zobrazit mou polohu</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">Přiblížení pro plné zobrazení</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Pauza</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">Odebrat poslední bod</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">Odebrat poslední bod</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Odesílám dotazník</string>\n  <string name=\"noselect_error\">Nevybral/a jste žádný dotazník!</string>\n  <string name=\"no_connection\">Není k dispozici připojení k internetu!</string>\n  <string name=\"sending_items\">Odesílám %1$s z %2$s formulář(ů)</string>\n  <string name=\"send_selected_data\">Odeslat vybrané</string>\n  <string name=\"server_auth_credentials\">Neplatné uživatelské jméno nebo heslo pro server: %s</string>\n  <string name=\"server_requires_auth\">Server vyžaduje přihlášení</string>\n  <string name=\"no_forms_uploaded\">Žádný dotazník nebyl nahrán.</string>\n  <string name=\"upload_results\">Výsledky nahrávání</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Změnit pohled</string>\n  <string name=\"show_sent_and_unsent_forms\">Zobrazit odeslané a neodeslané dotazníky</string>\n  <string name=\"show_unsent_forms\">Zobrazit neodeslané dotazníky</string>\n  <string name=\"send_in_progress\">Odesílání na pozadí běží, zkuste to znovu</string>\n  <string name=\"forms_upload_succeeded\">Nahrání formulářů bylo úspěšné</string>\n  <string name=\"forms_upload_failed\">Nahrávání formulářů selhalo</string>\n  <string name=\"all_uploads_succeeded\">Všechna nahrání se podařila!</string>\n  <string name=\"some_uploads_failed\">%1$s z %2$s nahrávání selhalo!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Obnovit</string>\n  <string name=\"download\">Stáhnout vybrané</string>\n  <string name=\"downloading_data\">Připojuji se k serveru</string>\n  <string name=\"fetching_file\">Stahuji %1$s.\\n\\nForm %2$s z %3$s dotazníků.</string>\n  <string name=\"form_download_progress\">%1$s. Stahuji mediální soubory: %2$s z %3$s</string>\n  <string name=\"version\">Verze:</string>\n  <string name=\"form_updates_available\">Dostupné aktualizace formuláře</string>\n  <string name=\"newer_version_of_a_form_info\">Toto je aktualizace formuláře, který máte</string>\n  <string name=\"form_update_error\">Aktualizace formuláře se nezdařila</string>\n  <string name=\"form_update_error_massage\">Otevřete ODK Collect a zkuste to znovu</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"form_update_notification_title\">Synchronizace formulářů</string>\n  <string name=\"load_remote_form_error\">Stažení seznamu formulářů se nezdařilo</string>\n  <string name=\"forms_download_succeeded\">Formuláře úspěšně staženy</string>\n  <string name=\"forms_download_failed\">Stažení formulářů se nezdařilo</string>\n  <string name=\"all_downloads_succeeded\">Všechna stažení se podařila!</string>\n  <string name=\"some_downloads_failed\">%1$sz %2$s stažení se nepodařilo!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Verze: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Pokud problém přetrvává, nahlaste jej osobě, která vás požádala o sběr dat.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect nemůže kontaktovat server na %s. Zadali jste adresu URL správně?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect se nemůže bezpečně připojit k serveru na %s. Zadali jste URL správně?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">Server %1$s vrátil stavový kód %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">Server %s poskytl neplatnou odpověď.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">Server neposkytl hash pro tento formulář.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_parsing_error\">Tento formulář nelze zpracovat:</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">Při stahování tohoto formuláře došlo na zařízení k chybě disku.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">Tento formulář má neplatnou adresu URL pro odeslání.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Uložené dotazníky</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Dotazníky k vyplnění</string>\n  <string name=\"delete_confirm\">Smazat \\\"%s\\\" dotazník(ů)</string>\n  <string name=\"delete_file\">Smazat označené</string>\n  <string name=\"delete_no\">Nemazat</string>\n  <string name=\"delete_yes\">Smazat dotazníky</string>\n  <string name=\"form_delete_message\">Smazání vybraných formulářů</string>\n  <string name=\"file_deleted_ok\">%s dotazníků úspěšně smazáno!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Nastavení projektu</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Server</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, uživatelské jméno, heslo</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Zobrazení projektu</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Název, ikona, barva</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Uživatelské rozhraní</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_interface_settings_subtext\">Jazyk aplikace, velikost písma, navigace</string>\n  <!--Title of a settings section-->\n  <string name=\"maps\">Mapy</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Podkladová mapa, styly, vrstvy</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Správa formulářů</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">Automatická aktualizace, automatické odesílání, automatické mazání</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Identita uživatele a zařízení</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">Uživatelské jméno, telefonní číslo, ID zařízení</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Experimentální</string>\n  <string name=\"protected_settings\">Chráněné</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">Odemknutí chráněných nastavení</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">Změna hesla, nastavení kontroly přístupu</string>\n  <string name=\"set_admin_password\">Nastavení hesla správce</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Řízení projektu</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">Překonfigurování, resetování, odstranění</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Řízení přístupu</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">Omezení uživatelského rozhraní</string>\n  <string name=\"server_preferences\">Nastavení serveru</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL Serveru</string>\n  <string name=\"url_error\">Neplatné URL!</string>\n  <string name=\"username\">Uživatelské jméno</string>\n  <string name=\"change_username\">Uživatelské jméno do ODK</string>\n  <string name=\"password\">Heslo</string>\n  <string name=\"change_password\">Heslo do ODK</string>\n  <string name=\"password_error_whitespace\">Počáteční nebo koncové mezery v heslech nejsou povoleny </string>\n  <string name=\"username_error_whitespace\">Počáteční nebo koncové mezery v uživatelských jménech nejsou povoleny</string>\n  <string name=\"app_theme\">Vizuální styl</string>\n  <string name=\"language\">Jazyk</string>\n  <string name=\"use_device_language\">Použít jazyk zařízení</string>\n  <string name=\"font_size\">Velikost písma</string>\n  <string name=\"change_font_size\">Velikost písma</string>\n  <string name=\"font_size_extra_large\">Extra velký</string>\n  <string name=\"font_size_large\">Velký</string>\n  <string name=\"font_size_medium\">Střední</string>\n  <string name=\"font_size_small\">Malý</string>\n  <string name=\"font_size_extra_small\">Extra malý</string>\n  <string name=\"navigation\">Navigace</string>\n  <string name=\"swipe_navigation\">Pohybem prstu do strany (swipe)</string>\n  <string name=\"buttons_navigation\">Pomocí tlačítek se šipkami</string>\n  <string name=\"swipe_buttons_navigation\">Pomocí swipu a tlačítek</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Další</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Zpět</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Aktualizace formuláře</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Režim aktualizace prázdného formuláře</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Ruční</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Pouze dříve stažené formuláře</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Přesně se shodující server</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Frekvence automatické aktualizace</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Každých patnáct minut</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Každou hodinu</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Každých šest hodin</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Každých 24 hodin</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Automatické stahování</string>\n  <string name=\"automatic_download_summary\">Automaticky stahovat aktualizované verze formulářů</string>\n  <string name=\"hide_old_form_versions_setting_title\">Skrýt staré verze formuláře</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Pouze nejnovější verze se zobrazí v formuláře Vyplnit prázdný formulář</string>\n  <string name=\"form_submission_category\">Odeslání formuláře</string>\n  <string name=\"autosend_selector_title\">Automatické odeslání</string>\n  <string name=\"autosend\">Automatické odeslání</string>\n  <string name=\"off\">Vypnuto</string>\n  <string name=\"wifi_autosend\">Pouze Wifi</string>\n  <string name=\"cellular_autosend\">Pouze mobilní</string>\n  <string name=\"wifi_cellular_autosend\">Wifi nebo mobilní</string>\n  <string name=\"delete_after_send\">Vymazat po odeslání</string>\n  <string name=\"delete_after_send_summary\">Vymaže dokončené dotazníky a média po odeslání na server</string>\n  <string name=\"form_filling_category\">Plnění formuláře</string>\n  <string name=\"constraint_behavior_title\">Omezování zpracování</string>\n  <string name=\"constraint_behavior\">Ověření správnosti odpovědi</string>\n  <string name=\"constraint_behavior_on_swipe\">Ověření při přechodu na další otázku</string>\n  <string name=\"constraint_behavior_on_finalize\">Provést validaci až při dokončení</string>\n  <string name=\"high_resolution_title\">Vysoké rozlišení videa</string>\n  <string name=\"high_resolution_summary\">Povolit vysoké rozlišení videonahrávek</string>\n  <string name=\"image_size_title\">Velikost obrázku</string>\n  <string name=\"image_size_dialog_title\">Maximální počet pixelů delšího okraje obrázku</string>\n  <string name=\"image_size_large\">Velká (3072px)</string>\n  <string name=\"image_size_medium\">Střední (2048px)</string>\n  <string name=\"image_size_small\">Malá (1024px)</string>\n  <string name=\"image_size_very_small\">Velmi malá (640px)</string>\n  <string name=\"image_size_original\">Původní velikost z fotoaparátu (výchozí)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Zobrazit pokyny pro otázky</string>\n  <string name=\"guidance_no\">Ne</string>\n  <string name=\"guidance_yes\">Ano - vždy zobrazeno</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Ano - sbaleno</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">K nahrávání zvuku použít externí aplikaci</string>\n  <string name=\"form_import_category\">Import formuláře</string>\n  <string name=\"instance_sync\">Dokončení formulářů při importu</string>\n  <string name=\"instance_sync_summary\">Ovlivňuje formuláře přidávané přímo do složky instance.</string>\n  <string name=\"analytics_preferences\">Údaje o použití</string>\n  <string name=\"analytics\">Shromažďujte anonymní údaje o použití</string>\n  <string name=\"analytics_summary\">Anonymní údaje o používání pomáhají týmu ODK stanovit priority oprav a funkcí.</string>\n  <string name=\"form_metadata_title\">Metadata formulářů</string>\n  <string name=\"form_metadata\">Metadata formulářů</string>\n  <string name=\"form_metadata_summary\">Tyto hodnoty budou přidány do formulářů, které obsahují uživatelské jméno, e-mail a / nebo telefonem předem vyplněná pole pro identifikaci odesílající osoby.</string>\n  <string name=\"user_defined\">Uživatelsky definované</string>\n  <string name=\"phone_number\">Telefonní číslo</string>\n  <string name=\"email\">Emailová adresa</string>\n  <string name=\"invalid_email_address\">Neplatná emailová adresa!</string>\n  <string name=\"device_defined\">Definované podle zařízení</string>\n  <string name=\"device_id\">ID zařízení</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Není dostupný</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">Odemknutí nastavení</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">Odemčené nastavení</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <string name=\"setting_not_available_during_form_entry\">Nastavení není při zadávání formuláře k dispozici</string>\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Zadejte nové heslo</string>\n  <string name=\"admin_password\">Heslo správce</string>\n  <string name=\"admin_password_changed\">Administrátorské heslo úspěšně změněno</string>\n  <string name=\"admin_password_disabled\">Administrátorské heslo vypnuto</string>\n  <string name=\"admin_password_incorrect\">Heslo není správné!</string>\n  <string name=\"enter_admin_password\">Zadejte administrátorské heslo</string>\n  <string name=\"change_admin_password\">Změnit heslo správce</string>\n  <string name=\"show_password\">Zobrazit heslo</string>\n  <string name=\"found_in_main\">Zobrazit v Hlavní nabídce</string>\n  <string name=\"found_in_settings\">Zrušení zaškrtnutí pro skrytí z nechráněných nastavení</string>\n  <string name=\"form_entry\">Odškrtnout, chcete-li skrýt z nabídky Formulářů</string>\n  <string name=\"form_end\">Zrušte zaškrtnutí pro skrytí konce formuláře</string>\n  <string name=\"reset_project_settings_title\">Reset</string>\n  <string name=\"reset_project_settings_subtext\">Vyberte si z nastavení, formulářů, dat</string>\n  <string name=\"delete_project\">Vymazat</string>\n  <string name=\"select_all\">Vybrat vše</string>\n  <string name=\"clear_all\">Vymazat vše</string>\n  <string name=\"reset_settings\">Všechna nastavení (interní nastavení, uložená nastavení)</string>\n  <string name=\"reset_saved_forms\">Uložené formuláře (složka instance, databáze instancí)</string>\n  <string name=\"reset_blank_forms\">Prázdné formuláře (složka formulářů, databáze formulářů, databáze položek)</string>\n  <string name=\"reset_layers\">Mapové vrstvy (složka layers)</string>\n  <string name=\"reset_cache\">Mezipaměť načítání formulářů (složka .cache)</string>\n  <string name=\"reset_settings_dialog_title\">Vybrat co resetovat</string>\n  <string name=\"reset_settings_button_reset\">Reset</string>\n  <string name=\"reset_in_progress\">Resetování…</string>\n  <string name=\"reset_app_state_result\">Výsledky resetu</string>\n  <string name=\"reset_app_state_warning\">Všechna vybraná data budou trvale smazána. Neexistuje žádné vrácení.</string>\n  <string name=\"reset_settings_result\">Všechna nastavení: %s</string>\n  <string name=\"reset_saved_forms_result\">Uložené formuláře: %s</string>\n  <string name=\"reset_blank_forms_result\">Prázdné formuláře: %s</string>\n  <string name=\"reset_layers_result\">Mapové vrstvy :: %s</string>\n  <string name=\"reset_cache_result\">Mezipaměť pro načtení formuláře :: %s</string>\n  <string name=\"main_menu_settings\">Nastavení hlavního menu</string>\n  <string name=\"main_menu_settings_summary\">Zobrazení nebo skrytí tlačítek</string>\n  <string name=\"user_settings\">Uživatelské nastavení</string>\n  <string name=\"user_setting_summary\">Zobrazení nebo skrytí nastavení</string>\n  <string name=\"form_entry_setting\">Nastavení vstupních formulářů</string>\n  <string name=\"form_entry_settings_summary\">Zobrazení nebo skrytí akcí</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Posun zpět</string>\n  <string name=\"moving_backwards_disabled_title\">Posun zpět je zakázán</string>\n  <string name=\"moving_backwards_disabled_message\">Nakonfigurujte zařízení tak, aby uživatelé nemohli toto nastavení obejít?\\n\\nZměny jsou:\\n\\u2022 Zakázat Upravit návrh\\n\\u2022 Zakázat Uložit jako návrh\\n\\u2022 Zakázat Přejít na výzvu\\n\\u2022 Nastavit zpracování omezení na Ověřit při přejetí prstem dopředu.</string>\n  <string name=\"yes\">Ano</string>\n  <string name=\"no\">Ne</string>\n  <string name=\"moving_backwards_enabled_title\">Posun zpět povolen</string>\n  <string name=\"moving_backwards_enabled_message\">Možná budete chtít zkontrolovat následující nastavení:\\n\\n\\u2022 Upravit návrh\\n\\u2022 Uložit jako návrh\\n\\u2022 Přejít na výzvu\\n\\u2022 Zpracování omezení</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Uložit jako návrh</string>\n  <string name=\"save_mid_summary\">Ikona pro uložení v horním panelu a tlačítko \\\"Uložit jako návrh\\\" při ukončení formuláře</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Více o….</string>\n  <string name=\"tell_your_friends\">Sdílet ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Shromažďují vaši kolegové data jen na papíře? Sdílejte s nimi ODK Collect.</string>\n  <string name=\"leave_a_review\">Nechte recenzi v Play Store</string>\n  <string name=\"leave_a_review_msg\">Vaše recenze (snad pozitivní) zvyšuje viditelnost aplikace v Play Store.</string>\n  <string name=\"odk_website\">Navštivte web ODK</string>\n  <string name=\"odk_website_summary\">ODK se používá ke sběru dat pro sociální dobro v náročných prostředích.</string>\n  <string name=\"odk_forum\">Připojit se k fóru ODK</string>\n  <string name=\"odk_forum_summary\">Připojte se na fórum, abyste získali podporu, žádali o nové funkce aplikace, přispěli kódem / překlady!</string>\n  <string name=\"all_open_source_licenses\">Open source knihovny / licence</string>\n  <string name=\"all_open_source_licenses_msg\">Stojíme na ramenou obrů!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">Rekonfigurace pomocí kódu QR</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">Nahrazení všech stávajících nastavení</string>\n  <string name=\"qr_code\">QR kódová konfigurace</string>\n  <string name=\"scan_qr_code_fragment_title\">Snímat</string>\n  <string name=\"view_qr_code_fragment_title\">QR kód</string>\n  <string name=\"import_qrcode_sd\">Import QR kódu</string>\n  <string name=\"successfully_imported_settings\">Nastavení úspěšně importováno</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Maximální limit počtu znaků: QR kód nelze generovat pro všechna nastavení</string>\n  <string name=\"share\">Sdílet</string>\n  <string name=\"turn_on_flashlight\">Zapnout svítilnu</string>\n  <string name=\"turn_off_flashlight\">Vypnout svítilnu</string>\n  <string name=\"qrcode_with_both_passwords\">Obsahuje citlivé informace: hesla <b>správce</b> a <b>serveru</b></string>\n  <string name=\"qrcode_without_passwords\">QR kód neobsahuje hesla <b>správce</b> nebo <b>serveru</b>. Klepnutím nakonfigurujte</string>\n  <string name=\"qrcode_with_admin_password\">Obsahuje citlivé informace: heslo <b>správce</b></string>\n  <string name=\"qrcode_with_server_password\">Obsahuje citlivé informace: heslo <b>serveru</b></string>\n  <string name=\"server_password\">Heslo serveru</string>\n  <string name=\"include_password_dialog\">Hesla zahrnuta v kódu</string>\n  <string name=\"generate\">Vytvořit</string>\n  <string name=\"invalid_qrcode\">Kód QR neobsahuje platná nastavení</string>\n  <string name=\"qr_code_not_found\">QR kód nebyl nalezen ve vybraném snímku</string>\n  <string name=\"corrupt_imported_preferences_error\">Aktuální nastavení je poškozené. V nastavení správy projektů obnovte nastavení nebo importujte funkční nastavení.</string>\n  <string name=\"settings_with_gd_protocol\">Projekty na Disku Google/Sheets již nelze vytvářet</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Zrušit</string>\n  <string name=\"cancel_loading_form\">Zrušit</string>\n  <string name=\"cancel_location\">Zrušit</string>\n  <string name=\"do_not_change\">Zrušit</string>\n  <string name=\"canceling\">Zrušení</string>\n  <string name=\"notification_channel_name\">Obecná upozornění</string>\n  <string name=\"success\">Úspěch</string>\n  <string name=\"error_occured\">Vyskytla se chyba</string>\n  <string name=\"please_wait\">Prosím chvilku počkejte.</string>\n  <string name=\"please_wait_long\">Prosím čekejte. Může to trvat několik minut.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Nemohu vymazat \\'%s\\'. Vymažte soubor ručně a stáhněte dotazník znovu.</string>\n  <string name=\"fs_create_media_folder_error\">Nemohu vytvořit adresář pro mediální soubory \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Nezdařilo se nakopírovat \\'%1$s\\' přes \\'%2$s\\'. Důvod: %3$s </string>\n  <string name=\"select_odk_shortcut\">Vybrat ODK zástupce</string>\n  <string name=\"background_audio_help\">Tento formulář zaznamenává zvuk z mikrofonu zařízení.\\n\\nMůžete použít indikátor hlasitosti a ujistit se, že je mikrofon dostatečně blízko zvuku, který potřebujete zaznamenat.\\n\\nChcete-li zastavit nahrávání, ukončete tento formulář.\\n\\nDalší informace získáte od osoby, která vás požádala o shromažďování údajů.</string>\n  <string name=\"loading\">Načítání…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Projekty</string>\n  <string name=\"settings\">Nastavení</string>\n  <string name=\"add_project\">Přidat projekt</string>\n  <string name=\"add\">Přidat</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Duplicitní projekt</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">Již máte projekt s těmito nastaveními připojení. Chcete přejít na stávající projekt nebo přidat nový?</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Přidat duplicitní projekt</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Přepnutí na stávající projekt</string>\n  <string name=\"project_name\">Název projektu</string>\n  <string name=\"project_icon\">Ikona projektu</string>\n  <string name=\"project_color\">Barva projektu</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Přepnuto na %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">S pomocí %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Přepnout na %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <string name=\"delete_project_dialog_title\">Smazat %s</string>\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <string name=\"delete_project_message\">Smazáním tohoto projektu trvale smažete následující položky</string>\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <string name=\"delete_trigger\">smazat</string>\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"delete_project_confirm_button_text\">Smazat projekt</string>\n  <string name=\"or\">OR</string>\n  <string name=\"hex_color\">Hex color</string>\n  <string name=\"invalid_hex_code\">Neplatný hexadecimální kód</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Sběr dat\\nkdekoli</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Nakonfigurujte pomocí QR kódu</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Ručně zadejte podrobnosti projektu</string>\n  <string name=\"project_config_tip\">Po přidání projektu jej můžete nakonfigurovat v části Nastavení</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Ještě nemáte žádný projekt?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Vyzkoušejte si demo</string>\n  <string name=\"scan_configuration_qr_code\">Naskenování konfiguračního kódu QR</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Zobrazit detaily</string>\n  <string name=\"errors\">Chyby</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <string name=\"no_browser_installed\">Není nainstalován žádný prohlížeč!</string>\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">Chcete-li otevřít tento formulář, musíte nejprve otevřít ODK Collect a přepnout na projekt, který jej obsahuje.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">ODK Collect nebyl nakonfigurován. Zkuste otevřít ODK Collect a nakonfigurovat jej.\\n\\nPokud jste klepli na zástupce, možná jej budete muset po konfiguraci ODK Collect znovu vytvořit.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Nerozpoznaný identifikátor URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Špatný identifikátor URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Vyplněný formulář byl smazán!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">Tento návrh nelze upravit, protože příslušný prázdný formulář není k dispozici nebo byl smazán.\\n\\nForm ID: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Stáhli jste dva různé formuláře se shodným ID a verzí. Možná jste nahráli stejný dotazník dvakrát nebo na jiné servery. Každopádně, jeden byste měli smazat.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <string name=\"cannot_open_form_because_of_forms_update\">Tento formulář nelze nyní otevřít, protože probíhá aktualizace formulářů. Zkuste to prosím brzy znovu.</string>\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Otevřít nastavení</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <string name=\"entities_title\">Seznamy entit</string>\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <string name=\"entity_browser_button\">Zobrazit seznamy entit</string>\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">Offline</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">Tento formulář není v této verzi ODK Collect podporován. Upgradujte prosím aplikaci Collect. Pokud máte tento problém i nadále, nahlaste ho osobě, která vás požádala o sběr dat. \\n\\nVerze specifikace formulářových entit: %s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">Při posledním spuštění aplikace došlo k pádu!</string>\n  <string name=\"cant_start_app\">Nelze spustit aplikaci!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">Tento projekt byl dříve propojen s účtem Google Drive. Podpora pro Google Drive byla odstraněna. Můžete nakonfigurovat server nebo stahovat příspěvky na počítač.</string>\n  <string name=\"learn_more_button_text\">Více informací</string>\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">Vývojářské nástroje</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">Aplikace Crash</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Vynucení nezachycené výjimky, která způsobí pád aplikace</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <string name=\"debug_filters_summary\">Zobrazit informace o ladění pro provádění výrazů filtru během zadávání formuláře</string>\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"use_zxing_for_barcode_scanning\">Vrátit se ke staršímu skeneru čárových kódů</string>\n  <string name=\"permission_dialog_title\">O oprávněních</string>\n  <string name=\"permission_dialog_text\">Budete požádáni o povolení přístupu ODK Collect k níže uvedeným funkcím, pokud je chcete používat, vyberte možnost \\\"povolit\\\".</string>\n  <string name=\"notifications\">Oznámení</string>\n  <string name=\"required_for_notifications\">Vyžaduje se pro zobrazení aktualizací při stahování, aktualizaci a odesílání formulářů.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">Váš formulář byl uložen jako návrh.</string>\n  <string name=\"form_saved\">Váš formulář byl uložen.</string>\n  <string name=\"form_sending\">Odesílání formuláře…</string>\n  <string name=\"edit_form\">Upravit</string>\n  <string name=\"view_form\">Zobrazit</string>\n  <string name=\"close_snackbar\">Zavřít snackbar</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">Poslední formulář odeslán: před %d sekundou</item>\n    <item quantity=\"few\">Poslední formulář odeslán: před %d sekundami</item>\n    <item quantity=\"many\">Poslední formulář odeslán: před %d sekundami</item>\n    <item quantity=\"other\">Poslední formulář odeslán: před %d sekundami</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">V pozdějších verzích nebudou dokončené formuláře již upravitelné. Uložte formuláře jako koncept a později je upravte.\\n\\nChyby v konceptu formuláře můžete zkontrolovat klepnutím na tři tečky (⋮) a poté Klepněte na Zkontrolovat chyby.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">V pozdějších verzích nebudou moci být dokončené formuláře upraveny.</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">Dokončete všechny koncepty</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">Jakmile dokončíte všechny koncepty, budou v \\\"Připraveno k odeslání\\\" a nebudete moci provádět úpravy. Jakékoliv koncepty s chybami nebudou dokončeny.\\n\\nTuto akci nebudete moci vrátit zpět.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d návrhy byly dokončeny. %d návrhy obsahují chyby, které je třeba před dokončením odstranit.</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d  dokončené návrhy. Zbývající návrhy je třeba dokončit ručně.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Chyby</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">Žádné chyby</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">Zrušte zaškrtnutím, aby se skrylo z konceptů</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">Nic není  k zobrazení</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">Žádné prázdné formuláře</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">Stáhněte si formulář, abyste mohli začít.</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">V návrzích nic není</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">Když uložíte jako koncept, formuláře se zde zobrazí.</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">Není nic připraveno k odeslání</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">Když dokončíte návrhy, zobrazí se zde</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">Nic v odeslaných</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">Když odešlete dokončené formuláře, objeví se zde</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">Žádné formuláře ke stažení</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">Stáhněte si formulář, abyste mohli začít.</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">Žádné formuláře k odstranění.</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">Když si stáhnete prázdné formuláře, zobrazí se zde</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">Když jste uložili formuláře, zobrazí se zde</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">Obnovit svou práci?</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"savepoint_recovery_dialog_message\">\\'Sběr se neočekávaně ukončil dne \\'EEE, MMM dd, yyyy \\'at\\' HH:mm\\' a uložil vaši práci.\\n\\nChcete ji obnovit nebo zahodit?\\'</string>\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">Obnovit</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Vyřadit</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">Získat bod místa</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">Zobrazit nebo změnit bod</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">Zobrazit bod</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">Změnit bod</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">Získat čáru</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">Zobrazit nebo změnit čáru</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">Zobrazit čáru</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">Získat polygon</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">Zobrazit nebo změnit polygon</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">Zobrazit polygon</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <string name=\"settings_json\">Nastavení JSON</string>\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <string name=\"preparing_form_edit\">Připravuji formulář k úpravám…</string>\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-da/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Tilføjet til\\' EEE, MMM dd, yyyy \\'kl\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Gemt på\\' EEE, MMM dd, yyyy \\'kl\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Afsluttet på\\' EEE, MMM dd, yyyy \\'kl\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Sendt på\\' EEE, MMM dd, yyyy \\'kl\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Sending mislykkedes til\\' EEE, MMM dd, yyyy \\'kl\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Slettet på\\' EEE, MMM dd, yyyy \\'kl\\' HH:mm</string>\n  <string name=\"submission_deleted\">Indberetning slettet</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sort_the_list\">Sortér liste</string>\n  <string name=\"sort_by_name_asc\">Navn, A-Z</string>\n  <string name=\"sort_by_name_desc\">Navn, A-Z</string>\n  <string name=\"sort_by_date_desc\">Dato - nyeste først</string>\n  <string name=\"sort_by_date_asc\">Dato - ældste først</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">Filtrér liste</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Søg</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Skanning fuldendt. Alle formularer hentet.</string>\n  <string name=\"xform_parse_error\">XForm-læsefejl for 1%1$s: 2%2$s mangler eller er invalid </string>\n  <string name=\"loading_form\">Henter formular</string>\n  <string name=\"survey_loading_reading_data_message\">Læser undersøgelsesdata…</string>\n  <string name=\"survey_loading_reading_csv_message\">Læser CSV-filer…</string>\n  <string name=\"parse_error\">Ikke muligt at læse formular</string>\n  <string name=\"cannot_edit_completed_form\">Formularen kan ikke ændres når den er markeret som endelig. Formularen er muligvis krypteret</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Ikke-gemte ændringer genskabt fra gendannelsespunkt.</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Skift sprog</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Gå til prompt</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Gå til start</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Gå til slutning</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Gå op</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Gruppe</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Gentagelig gruppe</string>\n  <string name=\"exit\">Forlad</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Tilføj \\\"1%s \\\"?l</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Tilføj</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Tilføj anden</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Rediger prompt</string>\n  <string name=\"clearanswer_confirm\">Fjern indmelding til \\\"1%s \\\"?</string>\n  <string name=\"clear_answer\">Fjern input</string>\n  <string name=\"clear_answer_ask\">Fjern inputtet</string>\n  <string name=\"clear_answer_no\">Afbryd</string>\n  <string name=\"discard_answer\">Fjern input</string>\n  <string name=\"discard_group\">Fjern gruppe</string>\n  <string name=\"delete_repeat\">Fjern gruppe</string>\n  <string name=\"delete_repeat_ask\">Vil du fjerne gruppen?</string>\n  <string name=\"delete_repeat_confirm\">Vil du fjerne gruppe \\\"1%s\\\" og dens undergrupper?</string>\n  <string name=\"delete_repeat_no\">Afbryd</string>\n  <string name=\"invalid_answer_error\"> Opfylder ikke svarmuligheder</string>\n  <string name=\"required_answer_error\">Skal udfyldes</string>\n  <string name=\"invalid_space_in_answer_singular\">OBS! Indtastede værdi 1%s har mellemrum</string>\n  <string name=\"invalid_space_in_answer_plural\">OBS! Indtastede værdier 1 %s har mellemrum</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Du er ved slutningen af %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Formularen kunne ikke gemmes</string>\n  <string name=\"data_saved_ok\">Formular gemt </string>\n  <string name=\"save_point_error\">Kunne ikke gemme gendannelsespunkt: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Gem ændringer</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Gemmer formular</string>\n  <string name=\"survey_saving_validating_message\">Validerer svar…</string>\n  <string name=\"survey_saving_collecting_message\">Indsamler data…</string>\n  <string name=\"survey_saving_saving_message\">Gemmer på SD-kort…</string>\n  <string name=\"survey_saving_encrypting_message\">Krypterer data…</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Start</string>\n  <string name=\"activity_not_found\">Ingen aktivitet at handle på 1%s</string>\n  <string name=\"no_app\">Den efterspurgte applikation mangler. Prøv manuelt at tilgå den.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Åbn webadresse</string>\n  <string name=\"capture_osm\">Åbn OpenMapKit</string>\n  <string name=\"recapture_osm\">Omgør OSM-tagging</string>\n  <string name=\"alert\">Advarsel</string>\n  <string name=\"install_openmapkit\">Installer venligst OpenMapKit!</string>\n  <string name=\"get_barcode\">Hent stregkode</string>\n  <string name=\"replace_barcode\">Erstat stregkode</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Åben fil</string>\n  <string name=\"capture_image\">Tag billede</string>\n  <string name=\"choose_image\">Vælg billede</string>\n  <string name=\"error_front_camera_unavailable\">Frontkamera er ikke tilgængeligt på dette device</string>\n  <string name=\"annotate_image\">Kommentér billede</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Gem og luk</string>\n  <string name=\"signature_capture\">Underskrift</string>\n  <string name=\"sign_button\">T</string>\n  <string name=\"reset_image\">Nulstil</string>\n  <string name=\"set_color\">Definér farve</string>\n  <string name=\"capture_video\">Optag video</string>\n  <string name=\"choose_video\">Vælg video</string>\n  <string name=\"play_video\">Afspil video</string>\n  <string name=\"choose_file\">Vælg fil</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Optag lyd</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Vælg lyd</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">Fejl opstod ved upload af af medie: 1%s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Vælg svarmulighed</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Valgt:</string>\n  <string name=\"trigger\">Fortsæt</string>\n  <string name=\"ext_file_no_data_error\">Filen indeholder intet data.</string>\n  <string name=\"ext_conflicting_columns_error\">Kolonner 1%s stemmer overens</string>\n  <string name=\"ext_sortBy_numeric_error\">Din sorteringskolonne bør kun indeholde numeriske værdier. Der er konflikt på \\'%s \\' </string>\n  <string name=\"ext_search_wrong_arguments_error\">Indtastningsfejl i funktionen search(): Funktionen kræver 1, 4 eller 6 \\\"arguments\\\".</string>\n  <string name=\"ext_search_wrong_function_error\">Indtastningsfejl i funktionen search(): Ukendt function \\'%s\\'</string>\n  <string name=\"ext_search_bad_function_error\">Indtastningsfejl i funktionen search():%s blev ikke evalueret som en funktion.</string>\n  <string name=\"ext_search_return_error\">Søgeværktøjet returnerede med et objekt af typen \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Kunne ikke importere data fra%1$s.  Forklaring: %2$s</string>\n  <string name=\"ext_import_progress_message\">Forudindlæser data fra\\' %1$s\\'.  Vent venligst… %2$s.</string>\n  <string name=\"ext_import_cancelled_message\">Læsning af data afbrudt.</string>\n  <string name=\"ext_import_finalizing_message\">Færdiggører forudindlæst data…</string>\n  <string name=\"ext_import_completed_message\">Læsning af data gennemført.</string>\n  <string name=\"ext_import_csv_missing_error\">Eksternt data for %1$s blev ikke importeret. Måske har du glemt at tilføje %2$s.csv-filen til din fomular?</string>\n  <string name=\"ext_search_generic_error\">Indtastningsfejl i search()-funktion: %s</string>\n  <string name=\"select_date\">Vælg dato</string>\n  <string name=\"select_time\">Vælg tidspunkt</string>\n  <string name=\"no_date_selected\">Ingen dato valgt</string>\n  <string name=\"no_time_selected\">Intet tidspunkt valgt</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Fil 1%s er ugyldig </string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Fil 1%s mangler </string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Luk</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Gem</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Basemap</string>\n  <string name=\"basemap_source\">Kilde</string>\n  <string name=\"streets\">Gader</string>\n  <string name=\"terrain\">Terræn</string>\n  <string name=\"hybrid\">Hybrid</string>\n  <string name=\"satellite\">Satellit</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Lys</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Mørk</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Udendørs</string>\n  <string name=\"topographic\">Topografisk</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Geolokation er slået fra</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">V %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">Ø %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Inputmetode</string>\n  <string name=\"start\">Start</string>\n  <string name=\"enable_gps\">Tænd GPS</string>\n  <string name=\"gps_enable_message\">GPS er slukket på dit udstyr. Har du lyst til at tænde den?</string>\n  <string name=\"geo_clear_warning\">Funktion allerede oprettet. Vil du fjerne funktionen?</string>\n  <string name=\"geo_exit_warning\">Fortryd ændringer og returnér til ODK?</string>\n  <string name=\"polygon_validator\">Du skal have mindst 3 punkter for at lave et polygon</string>\n  <string name=\"polyline_validator\">Du skal have mindst 2 punkter for at lave en polylinje</string>\n  <string name=\"clear\">Ryd</string>\n  <string name=\"placement_mode\">Placering ved tapping</string>\n  <string name=\"manual_mode\">Manuel optagelse af lokation</string>\n  <string name=\"automatic_mode\">Automatisk optagelse af lokation</string>\n  <string name=\"recording_interval\">Optagelsesinterval:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d sekunder</item>\n    <item quantity=\"other\">%d sekunder</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d minutter</item>\n    <item quantity=\"other\">%d minutter</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Nødvendig præcision:</string>\n  <string name=\"none\">Ingen</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d meter</item>\n    <item quantity=\"other\">%d meter</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Punkter indtastet: %d</string>\n  <string name=\"collection_status_placement\">Punkter indtastet: %d(Tryk for at placere punkter)</string>\n  <string name=\"collection_status_manual\">Punkter indtastet: %d(manuel optagelse)</string>\n  <string name=\"collection_status_auto_seconds\">Punkter indtastet: %1$d(optager hver %2$dsekund)</string>\n  <string name=\"discard\">Kassér</string>\n  <!--Name of an action button-->\n  <string name=\"view_data\">Inpicér gemt formular</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Uploader formular</string>\n  <string name=\"noselect_error\">Du har ikke valgt nogle formularer</string>\n  <string name=\"no_connection\">Ingen netværksforbindelse tilgængelig</string>\n  <string name=\"sending_items\">Uploader %1$s af %2$s formularer</string>\n  <string name=\"send_selected_data\">Upload valgte</string>\n  <string name=\"server_auth_credentials\">Invalid brugernavn eller adgangskode til server: %s</string>\n  <string name=\"server_requires_auth\">Server kræver identifikation</string>\n  <string name=\"no_forms_uploaded\">Ingen formular blev uploaded</string>\n  <string name=\"upload_results\">Upload-resultater</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Skift vinkel</string>\n  <string name=\"show_sent_and_unsent_forms\">Vis afsendte(uploadede) og ikke-afsendte formularer</string>\n  <string name=\"show_unsent_forms\">Vis ikke-afsendte formularer</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Opdatér</string>\n  <string name=\"download\">Hent valgte</string>\n  <string name=\"downloading_data\">Forbinder til server</string>\n  <string name=\"fetching_file\">Henter 1 %1$s. Formular 2%2$s af 3%3$s formularer. </string>\n  <string name=\"form_download_progress\">1%1$s. Henter mediefiler: 2%2$s af 3%3$s</string>\n  <string name=\"version\">Version:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">Hentning af formular-oversigt fejlede</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Gemte formularer</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Ikke-udfyldte(tomme) skabeloner</string>\n  <string name=\"delete_confirm\">Slet 1%s formular(er)?</string>\n  <string name=\"delete_file\">Slet valgte</string>\n  <string name=\"delete_no\">Undlad at slette</string>\n  <string name=\"delete_yes\">Fjern formularer</string>\n  <string name=\"file_deleted_ok\">1%s formular slettet </string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">Brugergrænseflade</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">Kort</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Serverindstilinger</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">Webadresse</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Serveradresse</string>\n  <string name=\"url_error\">Invalid webadresse</string>\n  <string name=\"username\">Brugernavn</string>\n  <string name=\"change_username\">ODK brugernavn</string>\n  <string name=\"password\">Adgangskode</string>\n  <string name=\"change_password\">ODK kodeord</string>\n  <string name=\"app_theme\">Tema</string>\n  <string name=\"language\">Sprog</string>\n  <string name=\"font_size\">Skriftstørrelse</string>\n  <string name=\"change_font_size\">Skriftstørrelse</string>\n  <string name=\"font_size_extra_large\">Ekstra stor</string>\n  <string name=\"font_size_large\">Stor</string>\n  <string name=\"font_size_medium\">Medium</string>\n  <string name=\"font_size_small\">Lille</string>\n  <string name=\"font_size_extra_small\">Ekstra lille</string>\n  <string name=\"navigation\">Navigation</string>\n  <string name=\"swipe_navigation\">Anvend horisontal swipe</string>\n  <string name=\"buttons_navigation\">Anvend tilbage- og frem-knapper</string>\n  <string name=\"swipe_buttons_navigation\">Anvend swipes og knapper</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Næste</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Tilbage</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Hver 15 minutter</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Hver time</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Hver sjette time</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Hver 24. time</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Automatisk download</string>\n  <string name=\"autosend_selector_title\">Auto-send</string>\n  <string name=\"autosend\">Auto-send</string>\n  <string name=\"delete_after_send\">Slet efter afsendelse</string>\n  <string name=\"delete_after_send_summary\">Sletter færdiggjorte formularer og tilhørende mediefiler efter afsendelse til server.</string>\n  <string name=\"constraint_behavior_on_swipe\">Validér ved brug af fremad-swipe</string>\n  <string name=\"constraint_behavior_on_finalize\">Udsæt validering indtil færdiggørelsen</string>\n  <string name=\"high_resolution_summary\">Aktivér højt-opløselige video-indstillnger</string>\n  <string name=\"image_size_title\">Billedstørrelse</string>\n  <string name=\"image_size_large\">Stor (3072px)</string>\n  <string name=\"image_size_medium\">Mellem (2048px)</string>\n  <string name=\"image_size_small\">Lille (1024px)</string>\n  <string name=\"image_size_very_small\">Meget lille (640px)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"analytics_preferences\">Brugerdata</string>\n  <string name=\"form_metadata_title\">Metadata(formular)</string>\n  <string name=\"form_metadata\">Metadata(formular)</string>\n  <string name=\"form_metadata_summary\">Disse værdier vil blive tilføjet til formularer der har brugernavn, email og/eller telefonnr. til ved forudindlæste felter for at identificere inventøreren.</string>\n  <string name=\"user_defined\">Brugerdefineret</string>\n  <string name=\"phone_number\">Telefonnummer</string>\n  <string name=\"email\">Mailadresse</string>\n  <string name=\"device_defined\">Defineret af enhed</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Angiv ny adgangskode</string>\n  <string name=\"admin_password\">Administrator-adgangskode</string>\n  <string name=\"admin_password_changed\">Administrator-adgangskode ændret</string>\n  <string name=\"admin_password_disabled\">Administrator-adgangskode deaktiveret</string>\n  <string name=\"admin_password_incorrect\">Adgangskode ikke korrekt</string>\n  <string name=\"enter_admin_password\">Angiv administrator-adgangskode</string>\n  <string name=\"change_admin_password\">Skift administrator-adgangskode</string>\n  <string name=\"show_password\">Vis kodeord</string>\n  <string name=\"found_in_main\">Fjern markering for at skjule fra hovedmenu </string>\n  <string name=\"reset_project_settings_title\">Nulstil</string>\n  <string name=\"select_all\">Vælg alle</string>\n  <string name=\"clear_all\">Ryd alle</string>\n  <string name=\"reset_settings_button_reset\">Nulstil</string>\n  <string name=\"reset_in_progress\">Nulstiller…</string>\n  <string name=\"user_settings\">Brugerindstillinger</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Om</string>\n  <string name=\"tell_your_friends\">Del ODK Collect</string>\n  <string name=\"odk_website\">Besøg ODK websiden</string>\n  <string name=\"all_open_source_licenses\">Open source biblioteker/licenser</string>\n  <string name=\"all_open_source_licenses_msg\">Vi står på skuldrene af giganter!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"scan_qr_code_fragment_title\">Scan</string>\n  <string name=\"view_qr_code_fragment_title\">QR kode</string>\n  <string name=\"share\">Del</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Afbryd</string>\n  <string name=\"cancel_loading_form\">Afbryd</string>\n  <string name=\"cancel_location\">Afbryd</string>\n  <string name=\"do_not_change\">Afbryd</string>\n  <string name=\"canceling\">Afbryder</string>\n  <string name=\"success\">Voila!</string>\n  <string name=\"error_occured\">Der opstod en fejl</string>\n  <string name=\"please_wait\">Vent et øjeblik</string>\n  <string name=\"please_wait_long\">Vent et øjeblik. Det kan vare et par minutter.</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">Tilføj</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Du har downloadet to forskellige formularer med samme formular-id og version. Måske var de den samme form, der blev uploadet på forskellige tidspunkter eller til forskellige servere. Under alle omstændigheder skal du slette en.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Kassér</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-de/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Neues Formular ausfüllen</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Entwürfe</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Bereit zum Versenden</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Gesendet</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Formular downloaden</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Formular löschen</string>\n  <string name=\"saved_forms\">Gespeicherte Formulare</string>\n  <string name=\"version_number\">Version: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Hinzugefügt am\\' EEE, dd.MM.yyyy \\'um\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Aktualisiert am\\' EEE, dd. MMM yyyy \\'um\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Gespeichert am\\' EEE, dd.MM.yyyy \\'um\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Fertig gestellt am\\' EEE, dd.MM.yyyy \\'um\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Gesendet am\\' EEE, dd.MM.yyyy \\'um\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Senden gescheitert am\\' EEE, dd.MM.yyyy \\'um\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Gelöscht am\\' EEE, dd.MM.yyyy \\'um\\' HH:mm</string>\n  <string name=\"submission_deleted\">Übermittlung gelöscht</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Geändert am\\' EEE, dd.MM.yyyy \\'um\\' HH:mm</string>\n  <string name=\"encrypted_form\">Verschlüsseltes Formular</string>\n  <string name=\"deleted_form\">Formular gelöscht</string>\n  <string name=\"sort_the_list\">Die Liste sortieren</string>\n  <string name=\"sort_by\">Sortieren nach</string>\n  <string name=\"sort_by_name_asc\">Name, A-Z</string>\n  <string name=\"sort_by_name_desc\">Name, Z-A</string>\n  <string name=\"sort_by_date_desc\">Datum, neuestes zuerst</string>\n  <string name=\"sort_by_date_asc\">Datum, ältestes zuerst</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Zuletzt gespeichertes zuerst</string>\n  <string name=\"filter_the_list\">Liste filtern</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Suche</string>\n  <string name=\"not_granted_permission\">Die Aktivität, die Sie versuchen zu starten, benötigt eine Berechtigung, die nicht gewährt wurde. </string>\n  <string name=\"storage_runtime_permission_denied_title\">Speicher-Berechtigungen</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Ohne diese Berechtigungen kann Collect nicht auf Formulare zugreifen und Antworten speichern. Starten Sie die App erneut, wenn sie bereit dazu sind die Berechtigung zu erteilen.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Standort-Berechtigungen</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Ohne die Berechtigung <b>Genauer Standort</b> kann Collect Ihren Standort nicht erfassen. Bitte versuchen Sie es erneut, wenn Sie bereit dazu sind die Berechtigung zu erteilen.</string>\n  <string name=\"camera_runtime_permission_denied_title\">Kamera-Berechtigung</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Ohne diese Berechtigung kann Collect nicht auf die Kamera zugreifen. Bitte versuchen Sie es erneut, wenn Sie dazu bereit sind die Berechtigung zu erteilen.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Audioaufnahmeberechtigung</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Ohne diese Berechtigungen kann Collect nicht auf das Mikrofon zugreifen. Bitte versuchen Sie es erneut, wenn Sie bereit dazu sind die Berechtigungen zu erteilen.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Formularkarte öffnen</string>\n  <string name=\"instance_scan_count\">%1$d Formular(e) hinzugefügt.</string>\n  <string name=\"finished_disk_scan\">Suche beendet. Alle Formulare wurden geladen.</string>\n  <string name=\"xform_parse_error\">XForm parse error. Bei %1$s fehlt Element %2$s oder ist ungültig.</string>\n  <string name=\"loading_form\">Formular wird geladen</string>\n  <string name=\"survey_loading_reading_form_message\">Formular-Definition wird gelesen…</string>\n  <string name=\"survey_loading_reading_data_message\">Umfragedaten werden gelesen…</string>\n  <string name=\"survey_loading_reading_csv_message\">Lese CSV Dateien …</string>\n  <string name=\"parse_error\">Formularverarbeitung nicht möglich.</string>\n  <string name=\"loading_form_failed\">Beim Laden des Formulars ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.</string>\n  <string name=\"cannot_edit_completed_form\">Dieses Formular kann nicht mehr geändert werden, nachdem es als vollständig markiert wurde. Möglicherweise ist es verschlüsselt.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Nicht-gespeicherte Einträge aus dem letzten automatischen Speichern wiederhergestellt!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Sprache ändern</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Zur Eingabe</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Als Entwurf speichern</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Zum Start</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Zum Ende</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Zurück</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <string name=\"edit_finalized_form\">Abgeschlossenes Formular bearbeiten</string>\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Gruppe</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Wiederholbare Gruppe</string>\n  <string name=\"exit\">Beenden</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">\\\"%s\\\" hinzufügen?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Hinzufügen</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"do_not_add_repeat\">Nicht hinzufügen</string>\n  <string name=\"add_another_menu\">Weitere hinzufügen</string>\n  <string name=\"add_another_question\">Weitere hinzufügen?</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Eingabe ändern</string>\n  <string name=\"clearanswer_confirm\">Antwort zu \\\"%s\\\" entfernen?</string>\n  <string name=\"clear_answer\">Antwort entfernen</string>\n  <string name=\"clear_answer_ask\">Diese Antwort entfernen?</string>\n  <string name=\"clear_answer_no\">Abbrechen</string>\n  <string name=\"discard_answer\">Antwort löschen</string>\n  <string name=\"discard_group\">Gruppenelement löschen</string>\n  <string name=\"delete_repeat\">Gruppenelement löschen</string>\n  <string name=\"delete_repeat_ask\">Dieses Gruppenelement löschen?</string>\n  <string name=\"delete_repeat_confirm\">Das Gruppenelement \\\"%s\\\" und alle möglichen Unterelemente löschen?</string>\n  <string name=\"delete_repeat_no\">Nicht löschen</string>\n  <string name=\"invalid_answer_error\">Diese Antwort ist ungültig!</string>\n  <string name=\"required_answer_error\">Diese Antwort ist erforderlich!</string>\n  <string name=\"success_form_validation\">Erfolgreich! Keine Fehler im Formular gefunden.</string>\n  <string name=\"invalid_space_in_answer_singular\">Warnung: hinterlegter Wert %s enthält Leerzeichen</string>\n  <string name=\"invalid_space_in_answer_plural\">Warnung: hinterlegte Werte %s enthalten Leerzeichen</string>\n  <string name=\"too_complex_form\">Dieses Formular ist zu komplex für dieses Gerät. Versuche Ausdrücke zu vereinfachen oder bitte im Forum um Hilfe.</string>\n  <string name=\"survey_internal_error\">Interner Fehler: Sprung zur Eingabe fehlgeschlagen.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Sie sind am Ende von \\\"%s\\\" angelangt.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <string name=\"form_editing_disabled_after_finalizing\">Änderungen sind nach dem Abschließen nicht möglich.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <string name=\"form_editing_disabled_after_sending\">Änderungen sind nach dem Senden nicht möglich.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_finalizing\">Änderungen sind nach dem Abschließen möglich.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_sending\">Änderungen sind nach dem Senden möglich.</string>\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Wenn Sie Änderungen an Ihrem Formular vornehmen müssen, \\\"Als Entwurf speichern\\\", bis Sie bereit sind, es zu senden.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <string name=\"form_editing_enabled_after_finalizing_hint\">Um Änderung nach dem Abschließen des Formulars durchzuführen, auf \\\"Bereit zum Senden\\\" gehen, das Formular öffnen und das Bearbeiten-Icon betätigen.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <string name=\"form_editing_enabled_after_sending_hint\">Um Änderung nach dem Senden des Formulars durchzuführen, auf \\\"Gesendet\\\" gehen, das Formular öffnen und das Bearbeiten-Icon betätigen.</string>\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Als Entwurf speichern</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Abschließen</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Senden</string>\n  <string name=\"data_saved_error\">Speichervorgang gescheitert!</string>\n  <string name=\"data_saved_ok\">Speichervorgang erfolgreich!</string>\n  <string name=\"save_point_error\">Fehler beim Speichern des Wiederherstellungspunkts: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Formular verwerfen</string>\n  <string name=\"discard_changes\">Änderungen verwerfen</string>\n  <string name=\"keep_changes\">Änderungen speichern</string>\n  <string name=\"keep_editing\">Bearbeiten fortsetzen</string>\n  <string name=\"quit_form_title\">Formular speichern?</string>\n  <string name=\"save_explanation\">Sie können dieses Formular speichern und jederzeit in Ihren Entwürfen darauf zugreifen.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'Dieses Formular wurde zuletzt gespeichert am\\' EEE, dd. MMM yyyy \\'um\\' HH:mm\\'. Als Entwurf speichern um Änderungen zu behalten.\\'</string>\n  <string name=\"quit_form_continue_title\">Bearbeiten fortsetzen?</string>\n  <string name=\"discard_form_warning\">Wenn Sie das Formular verwerfen, gehen alle bisherigen Änderungen verloren.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'Dieses Formular wurde zuletzt gespeichert am\\' EEE, dd. MMM yyyy \\'um\\' HH:mm\\'. Wenn die Änderungen verworfen werden, gehen alle Änderungen seit diesem Zeitpunkt verloren.\\'</string>\n  <string name=\"saving_form\">Formular wird gespeichert</string>\n  <string name=\"survey_saving_validating_message\">Antworten werden geprüft…</string>\n  <string name=\"survey_saving_collecting_message\">Daten werden gesammelt…</string>\n  <string name=\"survey_saving_saving_message\">Speichere auf SD-Karte…</string>\n  <string name=\"survey_saving_finalizing_message\">Abschließen auf SD-Karte…</string>\n  <string name=\"survey_saving_encrypting_message\">Daten werden verschlüsselt…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Mehr als genau ein Datensatz für diese Instanz!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Mehr als genau ein leeres Formular passt auf diese jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Keine FormId angegeben?</string>\n  <string name=\"phone_does_not_support_rsa\">Gerät unterstützt RSA-Verschlüsselung nicht.</string>\n  <string name=\"invalid_rsa_public_key\">Ungültiger RSA public key.</string>\n  <string name=\"encryption_error_message\">%s Formular wurde nicht als vollständig gespeichert.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">Formular kann nicht verwendet werden</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Aufnahme konnte nicht gestartet werden. </string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Das Mikrofon wird bereits verwendet.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">Mediendatei konnte nicht an das Formular angehängt werden. Aber ist hier verfügbar: %1$s</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Aufnahme deaktiviert. Aktivieren über %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Fehler beim Versuch Werte zu aktualisieren. Dies passiert normalerweise wegen fehlerhafter Verwendung von Berechnungen im Formulardesign.\\n\\nWenn das Problem weiterhin besteht, wenden Sie sich an die Person, für die Sie die Daten erfassen.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Starten</string>\n  <string name=\"activity_not_found\">Folgende App wurde nicht gefunden: %s</string>\n  <string name=\"no_app\">Die gewünschte App steht nicht zur Verfügung. Bitte tragen sie den Wert manuell ein.</string>\n  <string name=\"null_intent_value\">Die externe App hat die erwartete Information nicht geliefert.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">Drucken</string>\n  <string name=\"open_url\">URL öffnen</string>\n  <string name=\"capture_osm\">OpenMapKit starten</string>\n  <string name=\"recapture_osm\">OSM-Tagging ändern</string>\n  <string name=\"edited_osm_file\">Geänderte OSM XML-Datei:</string>\n  <string name=\"invalid_osm_data\">Fehler: Keine gültigen OSM-Daten erhalten.</string>\n  <string name=\"alert\">Warnung</string>\n  <string name=\"install_openmapkit\">Bitte OpenMapKit installieren!</string>\n  <string name=\"get_barcode\">Barcode erfassen</string>\n  <string name=\"replace_barcode\">Barcode ersetzen</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Positionieren Sie den Barcode innerhalb des Rechtecks.</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"rotate_device\">Gerät drehen</string>\n  <string name=\"open_file\">Datei öffnen</string>\n  <string name=\"rank_items\">Rangfolge festlegen</string>\n  <string name=\"number_picker_title\">Zahlenwähler</string>\n  <string name=\"invalid_range_widget\">Dieses Widget hat ungültige Parameter!</string>\n  <string name=\"svg_file_does_not_exist\">SVG-Datei existiert nicht!</string>\n  <string name=\"capture_image\">Foto machen</string>\n  <string name=\"choose_image\">Bild auswählen</string>\n  <string name=\"selected_invalid_image\">Ausgewählte Datei ist kein gültiges Bild.</string>\n  <string name=\"take_picture_instruction\">Bildschirm berühren um ein Foto aufzunehmen</string>\n  <string name=\"error_front_camera_unavailable\">Frontkamera bei diesem Gerät nicht verfügbar</string>\n  <string name=\"camera_error\">Es gab einen Fehler beim Fotografieren.</string>\n  <string name=\"camera_failed_to_initialize\">Kamera konnte nicht initialisiert werden!</string>\n  <string name=\"annotate_image\">Bild beschreiben</string>\n  <string name=\"gif_not_supported\">Gif-Dateien werden nicht unterstützt</string>\n  <string name=\"invalid_file_type\">App gab einen ungültigen Dateityp zurück</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_title\">Änderungen speichern?</string>\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_explanation\">Möchten Sie Änderungen vor dem Beenden speichern?</string>\n  <string name=\"save_and_close\">Speichern und schließen</string>\n  <string name=\"signature_capture\">Unterschrift erfassen</string>\n  <string name=\"sign_button\">Unterschrift erfassen</string>\n  <string name=\"markup_image\">Zeichnung bearbeiten</string>\n  <string name=\"draw_image\">Zeichnung erstellen</string>\n  <string name=\"reset_image\">Zurücksetzen</string>\n  <string name=\"set_color\">Farbe wählen</string>\n  <string name=\"capture_video\">Video aufnehmen</string>\n  <string name=\"choose_video\">Video auswählen</string>\n  <string name=\"play_video\">Video abspielen</string>\n  <string name=\"choose_file\">Datei auswählen</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Löschen</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Ton aufnehmen</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Tonaufnahme auswählen</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Pause</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Fortsetzen</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">Audioaufzeichnung: an</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">Audioaufzeichnung: aus</string>\n  <string name=\"background_audio_permission_explanation\">Dieses Formular nimmt im Hintergrund Audio auf. Sie müssen die Berechtigung erteilen das Mikrofon zu benutzen. Wenn nicht, können Sie dieses Formular nicht öffnen.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Dieses Formular verlangt Hintergrundaudioaufnahme. Deaktivieren beendet die Aufnahme und löscht existierende Audioaufnahmen. Sind Sie sicher, dass Sie fortfahren möchten?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Aufnahme deaktivieren</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">Die Aufnahme beginnt nicht sofort. Sie müssen das Formular erneut öffnen, um aufzunehmen.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Datei löschen? </string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">Sie können diese Datei nicht wiederherstellen, sobald Sie gelöscht wurde. Sind Sie sicher, dass Sie fortfahren möchten? </string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Stop</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Aufnehmen…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Audio-Aufnahme</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Audio abspielen</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Datei wird gespeichert</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Sie müssen die Aufnahme stoppen, bevor Sie diesen Bildschirm verlassen. </string>\n  <string name=\"error_attaching_binary_file\">Fehler beim Anhängen des Mediums: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Wert auswählen</string>\n  <string name=\"select_answer\">Antwort wählen</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Wert bearbeiten</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Kein Wert ausgewählt</string>\n  <string name=\"selected_title\">Ausgewählt: </string>\n  <string name=\"trigger\">OK. Bitte fortfahren.</string>\n  <string name=\"ext_file_no_data_error\">Die Datei enthält keine Daten!</string>\n  <string name=\"ext_conflicting_columns_error\">Spalten %s stimmen überein!</string>\n  <string name=\"ext_sortBy_numeric_error\">Ihre sortby-Spalte sollte nur numerische Werte enthalten. Fehlerhafter Wert war \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Syntaxfehler in der search()-Funktion: Die Funktion braucht 1, 4 oder 6 Argumente.</string>\n  <string name=\"ext_search_wrong_function_error\">Syntaxfehler in der search()-Funktion: Unbekannte Funktion \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Syntaxfehler in der search()-Funktion: \\'%s\\' wurde nicht als Funktion ausgewertet.</string>\n  <string name=\"ext_search_return_error\">Der search-Handler gab ein Objekt vom Typ \\'%s\\' zurück.</string>\n  <string name=\"ext_import_generic_error\">Konnten die Daten von %1$s nicht importieren. Grund: %2$s</string>\n  <string name=\"ext_import_progress_message\">Daten von \\'%1$s\\' werden geladen. Bitte warten… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Lesen der Daten abgebrochen!</string>\n  <string name=\"ext_import_finalizing_message\">Abschluss des Ladevorgangs…</string>\n  <string name=\"ext_import_completed_message\">Lesevorgang abgeschlossen!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager wurde nicht initialisiert.</string>\n  <string name=\"ext_import_csv_missing_error\">Externe Daten für %1$s wurden nicht importiert. Haben Sie vergessen die Datei %2$s.csv in Ihrem Formular anzugeben?</string>\n  <string name=\"ext_search_generic_error\">Syntaxfehler in der search()-Funktion: %s</string>\n  <string name=\"ext_assign_value_error\">Der Wert bei \\'%s\\' kann nicht zugewiesen werden.</string>\n  <string name=\"parser_exception\">XPathParser Exception: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Kompass-Peilung erfassen</string>\n  <string name=\"replace_bearing\">Kompass-Peilung ändern</string>\n  <string name=\"getting_bearing\">Kompass-Peilung erfassen</string>\n  <string name=\"accept_bearing\">Kompass-Peilung übernehmen</string>\n  <string name=\"bearing\">Kompass-Peilung: %.3f</string>\n  <string name=\"direction\">Himmelsrichtung: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Kompass-Peilung konnte nicht erfasst werden: Gerät besitzt keinen Kompass oder Beschleunigungssensor.</string>\n  <string name=\"select_date\">Datum auswählen</string>\n  <string name=\"select_time\">Uhrzeit auswählen</string>\n  <string name=\"no_date_selected\">Kein Datum ausgewählt</string>\n  <string name=\"no_time_selected\">Keine Uhrzeit ausgewählt</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farwardin</string>\n  <string name=\"persian_month_2\">Ordibehescht</string>\n  <string name=\"persian_month_3\">Chordād</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordād</string>\n  <string name=\"persian_month_6\">Schahriwar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Ābān</string>\n  <string name=\"persian_month_9\">Āsar</string>\n  <string name=\"persian_month_10\">Déi</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Essfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <string name=\"buddhist_month_1\">มกราคม</string>\n  <!--2nd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_2\">กุมภาพันธ์</string>\n  <!--3rd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_3\">มีนาคม</string>\n  <!--4th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_4\">เมษายน</string>\n  <!--5th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_5\">พฤษภาคม</string>\n  <!--6th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_6\">มิถุนายน</string>\n  <!--7th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_7\">กรกฎาคม</string>\n  <!--8th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_8\">สิงหาคม</string>\n  <!--9th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_9\">กันยายน</string>\n  <!--10th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_10\">ตุลาคม</string>\n  <!--11th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_11\">พฤศจิกายน</string>\n  <!--12th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_12\">ธันวาคม</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Datei: %s ist ungültig.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Datei: %s kann nicht gefunden werden.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Identität eingeben</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Identität</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Dieses Formular erfordert, dass Sie sich identifizieren</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Schließen</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Begründung für Änderungen</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Begründung</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Sie müssen den Grund für die Änderungen am Formular erklären</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Speichern</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">Ort auswählen</string>\n  <string name=\"choices\">Auswahl</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS Topografische Karte</string>\n  <string name=\"openmap_usgs_sat\">USGS National Karte Hybrid</string>\n  <string name=\"openmap_usgs_img\">USGS National Karte Imagery</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Basiskarte</string>\n  <string name=\"basemap_source\">Quelle</string>\n  <string name=\"basemap_source_unavailable\">%s Basiskarten sind leider nicht auf diesem Gerät verfügbar. Bitte eine andere Basiskarte unter Einstellungen &gt; Karten wählen.</string>\n  <string name=\"map_style_label\">%s Kartentyp</string>\n  <string name=\"streets\">Straßen</string>\n  <string name=\"terrain\">Gelände</string>\n  <string name=\"hybrid\">Hybrid</string>\n  <string name=\"satellite\">Satellit</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Hell</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Dunkel</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Draußen</string>\n  <string name=\"topographic\">Topographisch</string>\n  <string name=\"google_play_services_error_occured\">Zugriff auf Google Maps nicht möglich. Sind die Google Play Services installiert?</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Dunkle Materie</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">Offline-Ebenen</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">Ebene</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">Offline-Ebene auswählen</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">Wählen Sie die Offline-Ebene aus, die für alle Karten in diesem Projekt verwendet werden soll. Sie können der Liste Optionen hinzufügen, indem Sie eine MBTiles-Datei von Ihrem Gerät auswählen, und vorhandene Ebenen aus der Liste löschen.\\\"</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">Mehr über das Hinzufügen von MBTiles erfahren?</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">Ebenen hinzufügen</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">Ebene löschen</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <plurals name=\"non_mbtiles_files_selected_title\">\n    <item quantity=\"one\">%d Ebene kann nicht hinzugefügt werden</item>\n    <item quantity=\"other\">%d Ebenen können nicht hinzugefügt werden</item>\n  </plurals>\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">Die ausgewählten Dateien sind keine MBTiles. Es können nur MBTiles-Dateien hinzugefügt werden.</string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">Einige der ausgewählten Dateien sind keine MBTiles. Es können nur MBTiles-Dateien hinzugefügt werden.</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">Ebenen</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">Ebenenzugriff auswählen</string>\n  <string name=\"select_layer_access_subtitle\">Sollen die Ebenen in allen Projekten oder nur im aktuellen verfügbar sein?</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">Alle Projekte</string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">Nur aktuelles Projekt</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">Möchten sie wirklich die %1$s Offline-Ebene löschen?</string>\n  <string name=\"record_geopoint\">Punkt erfassen</string>\n  <string name=\"location_accuracy\">Genauigkeit: %1$s</string>\n  <string name=\"location_accuracy_unacceptable\">⚠️ Genauigkeit: %1$s (inakzeptabel)</string>\n  <string name=\"provider_disabled_error\">Standortdienste sind nicht aktiviert!</string>\n  <string name=\"gps_result\">Breite (Lat.): %1$s\\nLänge (Long.): %2$s\\nHöhe (Alt.): %3$sm\\nGenauigkeit: %4$sm</string>\n  <string name=\"google_play_services_not_available\">Dieses Formular möchte Ihren Standort aufzeichnen, aber die Google Play-Dienste sind nicht verfügbar.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Dieses Formular möchte Ihren Standort aufzeichnen, aber die Standortdienste sind deaktiviert. Bitte den Standortzugriff in den Android-Einstellungen aktivieren.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Zu den Einstellungen wechseln</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">Standortnachverfolgung: an</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">Standortnachverfolgung: aus</string>\n  <string name=\"background_location_disabled\">Dieses Formular möchte Ihren Standort aufzeichnen, aber die Standortaufzeichnung ist deaktiviert. Bitte oben im %1$s Menü aktivieren.</string>\n  <string name=\"background_location_enabled\">Dieses Formular zeichnet Ihren Standort auf. Sie können die Standortaufzeichnung oben im %1$s Menü deaktivieren.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">Auf Fehler überprüfen</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">Vergangene Zeit: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">Punkt wird gespeichert bei %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">Satelliten: %1$s</string>\n  <string name=\"waiting_for_location\">Versuche den Standort zu ermitteln. Bitte warten.</string>\n  <string name=\"poor_accuracy\">Schlechte Genauigkeit. Bitte warten.</string>\n  <string name=\"unacceptable_accuracy\">Inakzeptable Genauigkeit. Bitte warten.</string>\n  <string name=\"improving_accuracy\">Verbessere Genauigkeit. Bitte warten.</string>\n  <string name=\"accuracy_status_title\">Ermittle Standort</string>\n  <!--The accuracy of a location fix in meters-->\n  <string name=\"accuracy_m\">%1$s m</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">W %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">O %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Eingabemethode</string>\n  <string name=\"start\">Start</string>\n  <string name=\"enable_gps\">GPS aktivieren</string>\n  <string name=\"gps_enable_message\">GPS ist auf Ihrem Gerät deaktiviert. Möchten Sie es aktivieren?</string>\n  <string name=\"geo_clear_warning\">Merkmal bereits erzeugt. Möchten Sie das Merkmal löschen?</string>\n  <string name=\"geo_exit_warning\">Änderungen verwerfen und zu ODK zurückkehren?</string>\n  <string name=\"polygon_validator\">Min. 3 Punkte notwendig um ein Polygon zu erzeugen</string>\n  <string name=\"polyline_validator\">Min. 2 Punkte notwendig um eine Polylinie zu erzeugen</string>\n  <string name=\"clear\">Löschen</string>\n  <string name=\"placement_mode\">Platzierung durch Tippen</string>\n  <string name=\"manual_mode\">Manuelle Standorterfassung</string>\n  <string name=\"automatic_mode\">Automatische Standorterfassung</string>\n  <string name=\"recording_interval\">Aufzeichnungsintervall:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d Sekunde</item>\n    <item quantity=\"other\">%d Sekunden</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d Minute</item>\n    <item quantity=\"other\">%d Minuten</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Genauigkeitsanforderung:</string>\n  <string name=\"none\">Keine</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d Meter</item>\n    <item quantity=\"other\">%d Meter</item>\n  </plurals>\n  <string name=\"collection_status_paused\">erfasste Punkte: %d</string>\n  <string name=\"collection_status_placement\">erfasste Punkte: %d (tippen zum Platzieren von Punkten)</string>\n  <string name=\"collection_status_manual\">erfasste Punkte: %d (manuelle Standorterfassung)</string>\n  <string name=\"collection_status_auto_seconds\">erfasste Punkte: %1$d (automatische Erfassung alle %2$d s)</string>\n  <string name=\"collection_status_auto_minutes\">erfasste Punkte: %1$d (automatische Erfassung alle %2$d min)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">erfasste Punkte: %1$d (automatische Erfassung alle%2$d s, Genauigkeit %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">erfasste Punkte: %1$d (automatische Erfassung alle %2$d min, Genauigkeit %3$d m)</string>\n  <string name=\"geopoint_instruction\">Lange drücken um Markierung zu platzieren oder Button Markierung hinzufügen antippen.</string>\n  <string name=\"geopoint_no_draggable_instruction\">Button Markierung hinzufügen antippen.</string>\n  <string name=\"discard\">Verwerfen</string>\n  <string name=\"select_item_count\">%s: %d (%d auf der Karte angezeigt)</string>\n  <string name=\"select_item\">Auswählen</string>\n  <string name=\"new_item\">Neues Element</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">Entwurf bearbeiten </string>\n  <string name=\"view_data\">Gespeichertes Formular anzeigen</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">Standortverlauf</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">Standort wird aufgezeichnet… </string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Meinen Standort anzeigen</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">Zoom auf alles</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Pause</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">Letzten Punkt entfernen</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <string name=\"error_fixed\">✅ Fehler behoben</string>\n  <!--Description for button that explains how to modify the map-->\n  <string name=\"show_how_to_modify_map\">Zeigen Sie, wie man die Karte ändert.</string>\n  <!--Title for instructions on how to modify map features-->\n  <string name=\"how_to_modify_map\">Wie man die Karte ändert</string>\n  <!--Instruction in a list on how to move a point-->\n  <string name=\"long_press_to_move_point_info_item\">Lang drücken, um den Punkt zu verschieben</string>\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">Letzten Punkt entfernen</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <string name=\"delete_shape_to_start_over_info_item\">Form löschen, um neu zu beginnen</string>\n  <!--Instruction in a list on how to add a new point-->\n  <string name=\"add_point_info_item\">Punkt hinzufügen</string>\n  <!--Instruction in a list on how to tap to add a point-->\n  <string name=\"tap_to_add_a_point_info_item\">Tippen Sie, um einen Punkt hinzuzufügen</string>\n  <!--Instruction in a list on how to delete the entire shape-->\n  <string name=\"delete_entire_shape_info_item\">Gesamte Form löschen</string>\n  <!--Button text to close the instructions dialog-->\n  <string name=\"done\">Fertig</string>\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <string name=\"how_to_modify\">Wie man modifiziert</string>\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Formular wird gesendet</string>\n  <string name=\"noselect_error\">Keine Formulare ausgewählt!</string>\n  <string name=\"no_connection\">Keine Netzwerkverbindung vorhanden</string>\n  <string name=\"sending_items\">%1$s von %2$s Formular(en) werden hochgeladen</string>\n  <string name=\"send_selected_data\">Auswahl hochladen</string>\n  <string name=\"server_auth_credentials\">Ungültiger Benutzername oder Passwort für Server: %s</string>\n  <string name=\"server_requires_auth\">Server verlangt Authentifizierung</string>\n  <string name=\"no_forms_uploaded\">Kein Formular wurde hochgeladen.</string>\n  <string name=\"upload_results\">Upload-Ergebnisse</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Ansicht ändern</string>\n  <string name=\"show_sent_and_unsent_forms\">Gesendete und nicht gesendete Formulare</string>\n  <string name=\"show_unsent_forms\">Nur nicht gesendete Formulare</string>\n  <string name=\"send_in_progress\">Sendevorgang läuft im Hintergrund. Bitte versuchen Sie es später nochmal.</string>\n  <string name=\"forms_upload_succeeded\">Formular-Upload erfolgreich</string>\n  <string name=\"forms_upload_failed\">Formular-Upload fehlgeschlagen</string>\n  <string name=\"all_uploads_succeeded\">Alle Uploads erfolgreich!</string>\n  <string name=\"some_uploads_failed\">%1$s von %2$s Uploads fehlgeschlagen!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Aktualisieren</string>\n  <string name=\"download\">Auswahl herunterladen</string>\n  <string name=\"downloading_data\">Verbindung mit dem Server wird aufgebaut</string>\n  <string name=\"fetching_file\">Lade %1$s.\\n\\nFormular %2$s von %3$s Formular(en).</string>\n  <string name=\"form_download_progress\">%1$s. Media-Dateien werden geladen: %2$s von %3$s</string>\n  <string name=\"version\">Version:</string>\n  <string name=\"form_updates_available\">Formular-Updates verfügbar</string>\n  <string name=\"newer_version_of_a_form_info\">Dies ist eine aktualisierte Version eines bereits vorhandenen Formulars!</string>\n  <string name=\"form_update_error\">Formular-Update fehlgeschlagen</string>\n  <string name=\"form_update_error_massage\">ODK Collect öffnen um es erneut zu versuchen</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"form_update_notification_title\">Formulare synchronisieren</string>\n  <string name=\"load_remote_form_error\">Formularlisten-Download fehlgeschlagen</string>\n  <string name=\"forms_download_succeeded\">Formular-Download erfolgreich</string>\n  <string name=\"forms_download_failed\">Formular-Download fehlgeschlagen</string>\n  <string name=\"all_downloads_succeeded\">Alle Downloads erfolgreich!</string>\n  <string name=\"some_downloads_failed\">%1$s von %2$s Downloads fehlgeschlagen!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Version: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Wenn Sie weiterhin dieses Problem haben, berichten Sie es an die Person, die Sie zum Daten sammeln beauftragt hat.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect kann den Server unter %s nicht erreichen. Haben Sie die URL korrekt eingegeben? </string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect kann sich nicht sicher mit dem Server unter %s verbinden. Haben Sie die URL korrekt eingegeben? </string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">Der Server %1$s antwortete mit Statuscode %2$s. </string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">Der Server %s gab eine ungültige Antwort.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">Der Server hat keinen Hash für dieses Formular zur Verfügung gestellt.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_parsing_error\">Dieses Formular kann nicht verarbeitet werden:</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">Während des Speicherns des Formulars ist ein Laufwerksfehler aufgetreten.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">Das Formular hat eine ungültige Übermittlung-URL.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Gespeicherte Formulare</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Leere Formulare</string>\n  <string name=\"delete_confirm\">%s Formular(e) löschen?</string>\n  <string name=\"delete_file\">Auswahl löschen</string>\n  <string name=\"delete_no\">Nicht löschen</string>\n  <string name=\"delete_yes\">Formulare löschen</string>\n  <string name=\"form_delete_message\">Ausgewählte Formulare löschen</string>\n  <string name=\"file_deleted_ok\">%s Formular(e) erfolgreich gelöscht!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Projekt-Einstellungen</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Server</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, Benutzername, Passwort</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Projekt-Anzeige</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Name, Icon, Farbe</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Bedienung</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_interface_settings_subtext\">App-Sprache, Schriftgröße, Navigation</string>\n  <!--Title of a settings section-->\n  <string name=\"maps\">Karten</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Basiskarte, Stile, Ebenen</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Formularverwaltung</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">Auto-Update, Auto-Senden, Auto-Löschen</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Benutzer- und Geräteidentität</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">Benutzername, Telefonnummer, Geräte-ID</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Experimentell</string>\n  <string name=\"protected_settings\">Geschützt</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">Geschützte Einstellungen freigeben</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">Passwort ändern, Zugriffssteuerung einstellen</string>\n  <string name=\"set_admin_password\">Admin-Passwort setzen</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Projektverwaltung</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">Umkonfigurieren, Reset, Löschen</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Zugriffskontrolle</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">Bedienung beschränken</string>\n  <string name=\"server_preferences\">Server-Einstellungen</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Server URL</string>\n  <string name=\"url_error\">Ungültige URL!</string>\n  <string name=\"username\">Benutzername</string>\n  <string name=\"change_username\">ODK Benutzername</string>\n  <string name=\"password\">Passwort</string>\n  <string name=\"change_password\">ODK Passwort</string>\n  <string name=\"password_error_whitespace\">Leerzeichen am Anfang und Ende von Passwörtern nicht erlaubt</string>\n  <string name=\"username_error_whitespace\">Leerzeichen am Anfang und Ende von Benutzernamen nicht erlaubt</string>\n  <string name=\"app_theme\">Theme</string>\n  <string name=\"language\">Sprache</string>\n  <string name=\"use_device_language\">Gerätesprache benutzen</string>\n  <string name=\"font_size\">Schriftgröße</string>\n  <string name=\"change_font_size\">Schriftgröße</string>\n  <string name=\"font_size_extra_large\">Sehr gross</string>\n  <string name=\"font_size_large\">Groß</string>\n  <string name=\"font_size_medium\">Mittel</string>\n  <string name=\"font_size_small\">Klein</string>\n  <string name=\"font_size_extra_small\">Sehr klein</string>\n  <string name=\"navigation\">Navigation</string>\n  <string name=\"swipe_navigation\">Horizontal wischen</string>\n  <string name=\"buttons_navigation\">Vor-/Zurück-Buttons verwenden</string>\n  <string name=\"swipe_buttons_navigation\">Wischen und Buttons verwenden</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Weiter</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Zurück</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Formular-Update</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Leerformular-Update-Modus</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Manuell</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Nur zuvor heruntergeladene Formulare</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Exakt übereinstimmender Server</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Automatische Update-Frequenz</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Alle 15 Minuten</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Jede Stunde</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Alle 6 Stunden</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Alle 24 Stunden</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Automatischer Download</string>\n  <string name=\"automatic_download_summary\">Automatisch aktualisierte Versionen von Formularen herunterladen</string>\n  <string name=\"hide_old_form_versions_setting_title\">Alte Formular-Versionen ausblenden</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Nur die neueste Version wird in \\\"Leeres Formular ausfüllen\\\" angezeigt</string>\n  <string name=\"form_submission_category\">Formularübermittlung</string>\n  <string name=\"autosend_selector_title\">Auto-Send</string>\n  <string name=\"autosend\">Auto-Send</string>\n  <string name=\"off\">Aus</string>\n  <string name=\"wifi_autosend\">Nur WLAN</string>\n  <string name=\"cellular_autosend\">Nur Mobilfunk</string>\n  <string name=\"wifi_cellular_autosend\">WLAN oder Mobilfunk</string>\n  <string name=\"delete_after_send\">Löschen nach dem Senden</string>\n  <string name=\"delete_after_send_summary\">Löscht abgeschlossene Formulare und Medien nach dem Senden zum Server</string>\n  <string name=\"form_filling_category\">Formulareingabe</string>\n  <string name=\"constraint_behavior_title\">Validierung</string>\n  <string name=\"constraint_behavior\">Validierungszeitpunkt</string>\n  <string name=\"constraint_behavior_on_swipe\">Validierung beim Vorwärts-Wischen</string>\n  <string name=\"constraint_behavior_on_finalize\">Validierung beim Abschließen</string>\n  <string name=\"high_resolution_title\">HD-Video</string>\n  <string name=\"high_resolution_summary\">HD-Videoaufnahmen aktivieren</string>\n  <string name=\"image_size_title\">Bildgröße</string>\n  <string name=\"image_size_dialog_title\">Max. Anzahl Pixel der langen Seite des Bildes</string>\n  <string name=\"image_size_large\">Groß (3072px)</string>\n  <string name=\"image_size_medium\">Mittel (2048px)</string>\n  <string name=\"image_size_small\">Klein (1024px)</string>\n  <string name=\"image_size_very_small\">Sehr klein (640px)</string>\n  <string name=\"image_size_original\">Originalgröße von der Kamera (Standard)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Hilfe zu Fragen anzeigen</string>\n  <string name=\"guidance_no\">Nein</string>\n  <string name=\"guidance_yes\">Ja - immer anzeigen</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Ja - ausgeblendet</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">Externe App für Audioaufnahme verwenden</string>\n  <string name=\"form_import_category\">Formular-Import</string>\n  <string name=\"instance_sync\">Formulare beim Import abschließen</string>\n  <string name=\"instance_sync_summary\">Betrifft Formulare, die direkt dem instances-Ordner hinzugefügt werden.</string>\n  <string name=\"analytics_preferences\">Nutzungsdaten</string>\n  <string name=\"analytics\">Anonyme Nutzungsdaten sammeln</string>\n  <string name=\"analytics_summary\">Anonyme Nutzungsdaten helfen dem ODK-Team Fehlerbehebungen und neue Funktionen zu priorisieren.</string>\n  <string name=\"form_metadata_title\">Formular-Metadaten</string>\n  <string name=\"form_metadata\">Formular-Metadaten</string>\n  <string name=\"form_metadata_summary\">Diese Werte werden Formularen hinzugefügt, die Usernamen, E-Mail und/oder durch das Telefon vorbelegte Felder beinhalten, um die Person zu identifizieren, die Daten eingibt.</string>\n  <string name=\"user_defined\">Benutzerdefiniert</string>\n  <string name=\"phone_number\">Telefonnummer</string>\n  <string name=\"email\">E-Mail-Adresse</string>\n  <string name=\"invalid_email_address\">Ungültige E-Mail-Adresse!</string>\n  <string name=\"device_defined\">Gerätedefiniert</string>\n  <string name=\"device_id\">Geräte-ID</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Nicht verfügbar</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">Einstellungen freigeben</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">Einstellungen freigegeben</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <string name=\"setting_not_available_during_form_entry\">Einstellung nicht verfügbar während Formularbearbeitung</string>\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Neues Passwort eingeben</string>\n  <string name=\"admin_password\">Admin-Passwort</string>\n  <string name=\"admin_password_changed\">Admin-Passwort erfolgreich geändert</string>\n  <string name=\"admin_password_disabled\">Admin-Passwort deaktiviert</string>\n  <string name=\"admin_password_incorrect\">Passwort ist falsch!</string>\n  <string name=\"enter_admin_password\">Admin-Passwort eingeben</string>\n  <string name=\"change_admin_password\">Admin-Passwort ändern</string>\n  <string name=\"show_password\">Passwort anzeigen</string>\n  <string name=\"found_in_main\">Deaktivieren, um aus dem Haupt-Menü auszublenden</string>\n  <string name=\"found_in_settings\">Abwählen, um unter Ungeschützten Einstellungen zu verbergen.</string>\n  <string name=\"form_entry\">Deaktivieren, um in der Formular-Eingabe auszublenden</string>\n  <string name=\"form_end\">Abwählen um vom Formularende verbergen</string>\n  <string name=\"reset_project_settings_title\">Zurücksetzen (Reset)</string>\n  <string name=\"reset_project_settings_subtext\">Wähle aus Einstellungen, Formulare, Daten</string>\n  <string name=\"delete_project\">Löschen</string>\n  <string name=\"select_all\">Alle auswählen</string>\n  <string name=\"clear_all\">Auswahl aufheben</string>\n  <string name=\"reset_settings\">Alle Einstellungen (interne Einstellungen, gespeicherte Einstellungen)</string>\n  <string name=\"reset_saved_forms\">Gespeicherte Formulare (Instanz-Ordner, Instanz-Datenbank)</string>\n  <string name=\"reset_blank_forms\">Leere Formulare (Formular-Ordner, Formular-Datenbank, Itemsets-Datenbank)</string>\n  <string name=\"reset_layers\">Kartenebenen (layers-Ordner)</string>\n  <string name=\"reset_cache\">Formular-Cache (.cache-Ordner)</string>\n  <string name=\"reset_settings_dialog_title\">Auswählen, was zurückgesetzt werden soll</string>\n  <string name=\"reset_settings_button_reset\">Zurücksetzen (Reset)</string>\n  <string name=\"reset_in_progress\">Zurücksetzen…</string>\n  <string name=\"reset_app_state_result\">Zurücksetzen-Ergebnisse</string>\n  <string name=\"reset_app_state_warning\">Alle ausgewählten Daten werden dauerhaft gelöscht. Es gibt kein Rückgängig-Machen.</string>\n  <string name=\"reset_settings_result\">Alle Einstellungen :: %s</string>\n  <string name=\"reset_saved_forms_result\">Gespeicherte Formulare :: %s</string>\n  <string name=\"reset_blank_forms_result\">Leere Formulare :: %s</string>\n  <string name=\"reset_layers_result\">Kartenebenen :: %s</string>\n  <string name=\"reset_cache_result\">Formular-Cache :: %s</string>\n  <string name=\"main_menu_settings\">Haupt-Menü-Einstellungen</string>\n  <string name=\"main_menu_settings_summary\">Buttons anzeigen oder ausblenden</string>\n  <string name=\"user_settings\">Benutzer-Einstellungen</string>\n  <string name=\"user_setting_summary\">Einstellungen anzeigen oder ausblenden</string>\n  <string name=\"form_entry_setting\">Eingabe-Einstellungen</string>\n  <string name=\"form_entry_settings_summary\">Aktionen anzeigen oder ausblenden</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Rückwärts bewegen</string>\n  <string name=\"moving_backwards_disabled_title\">Rückwärts bewegen deaktiviert</string>\n  <string name=\"moving_backwards_disabled_message\">Das Gerät so konfigurieren, dass Anwender diese Einstellung nicht umgehen können?\\n\\nDie Änderungen sind:\\n\\n\\u2022 Deaktiviere \\\"Entwurf bearbeiten\\\"\\n\\u2022 Deaktiviere \\\"Als Entwurf speichern\\\"\\n\\u2022 Deaktiviere \\\"Gehe zum Prompt\\\"\\n\\u2022 Aktiviere \\\"Validierung beim Vorwärts-Wischen\\\"</string>\n  <string name=\"yes\">Ja</string>\n  <string name=\"no\">Nein</string>\n  <string name=\"moving_backwards_enabled_title\">Rückwärts bewegen aktiviert</string>\n  <string name=\"moving_backwards_enabled_message\">Bitte überprüfen Sie die folgenden Einstellungen:\\n\\n\\u2022 Entwurf bearbeiten\\n\\u2022 Als Entwurf speichern\\n\\u2022 Gehe zum Prompt\\n\\u2022 Validierung</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Als Entwurf speichern</string>\n  <string name=\"save_mid_summary\">Speichern-Icon in oberer Leiste und \\\"Als Entwurf speichern\\\"-Button beim Verlassen des Formulars</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Über</string>\n  <string name=\"tell_your_friends\">ODK Collect teilen</string>\n  <string name=\"tell_your_friends_msg\">Erfassen Ihre Kollegen Daten immer noch auf Papier? Empfehlen Sie Ihnen ODK Collect.</string>\n  <string name=\"leave_a_review\">Geben Sie eine Play Store Bewertung ab</string>\n  <string name=\"leave_a_review_msg\">Ihre (hoffentlich positive) Bewertung steigert die Bekanntheit der App im Play Store.</string>\n  <string name=\"odk_website\">ODK-Webseite öffnen</string>\n  <string name=\"odk_website_summary\">ODK wird benutzt um Daten für soziale Zwecke in herausfordernden Umgebungen zu sammeln.</string>\n  <string name=\"odk_forum\">Dem ODK-Forum beitreten</string>\n  <string name=\"odk_forum_summary\">Treten Sie dem Forum bei, um Hilfe zu bekommen, Funktionen vorzuschlagen, Code/Übersetzungen beizutragen!</string>\n  <string name=\"all_open_source_licenses\">Open Source Libraries/Lizenzen</string>\n  <string name=\"all_open_source_licenses_msg\">Wir stehen auf den Schultern von Riesen!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">Mit QR-Code rekonfigurieren</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">Alle bestehenden Einstellungen ersetzen</string>\n  <string name=\"qr_code\">QR-Code-Konfiguration</string>\n  <string name=\"scan_qr_code_fragment_title\">Scannen</string>\n  <string name=\"view_qr_code_fragment_title\">QR-Code</string>\n  <string name=\"import_qrcode_sd\">QR-Code importieren</string>\n  <string name=\"successfully_imported_settings\">Einstellungen erfolgreich importiert</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Max. Zeichen-Limit erreicht: QR-Code kann nicht für alle Einstellungen generiert werden</string>\n  <string name=\"share\">Teilen</string>\n  <string name=\"turn_on_flashlight\">Taschenlampe anschalten</string>\n  <string name=\"turn_off_flashlight\">Taschenlampe ausschalten</string>\n  <string name=\"qrcode_with_both_passwords\">Enthält vertrauliche Informationen: <b>Admin-</b> und <b>Server-Passwörter</b></string>\n  <string name=\"qrcode_without_passwords\">QR-Code enthält keine <b>Admin-</b> oder <b>Server-</b>Passwörter. Tippen Sie um zu konfigurieren</string>\n  <string name=\"qrcode_with_admin_password\">Enthält vertrauliche Informationen: <b>Admin-</b>Passwort </string>\n  <string name=\"qrcode_with_server_password\">Enthält vertrauliche Informationen: <b>Server-</b>Passwort </string>\n  <string name=\"server_password\">Server-Passwort</string>\n  <string name=\"include_password_dialog\">Passwörter im Code enthalten</string>\n  <string name=\"generate\">Generieren</string>\n  <string name=\"invalid_qrcode\">QR-Code enthält keine gültigen Einstellungen</string>\n  <string name=\"qr_code_not_found\">Kein QR-Code im ausgewählten Bild gefunden</string>\n  <string name=\"corrupt_imported_preferences_error\">Die aktuellen Einstellungen sind beschädigt. Setze die Einstellungen in den Projektmanagement-Einstellungen zurück oder importiere funktionierende.</string>\n  <string name=\"settings_with_gd_protocol\">Google Drive/Sheets Projekte können nicht mehr erzeugt werden</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Abbrechen</string>\n  <string name=\"cancel_loading_form\">Abbrechen</string>\n  <string name=\"cancel_location\">Abbrechen</string>\n  <string name=\"do_not_change\">Abbrechen</string>\n  <string name=\"canceling\">Abbrechen</string>\n  <string name=\"notification_channel_name\">Allgemeine Benachrichtigungen</string>\n  <string name=\"success\">Erfolg</string>\n  <string name=\"error_occured\">Fehler</string>\n  <string name=\"please_wait\">Bitte haben Sie einen Moment Geduld.</string>\n  <string name=\"please_wait_long\">Bitte warten. Dies kann einige Minuten dauern.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">\\'%s\\' konnte nicht gelöscht werden. Bitte löschen Sie die Datei manuell und laden Sie das Formular erneut herunter.</string>\n  <string name=\"fs_create_media_folder_error\">Das Medien-Verzeichnis \\'%s\\' kann nicht angelegt werden.</string>\n  <string name=\"fs_file_copy_error\">Konnte \\'%1$s\\' nicht über \\'%2$s\\' kopieren. Grund: %3$s</string>\n  <string name=\"select_odk_shortcut\">ODK Verknüpfung auswählen</string>\n  <string name=\"background_audio_help\">Dieses Formular zeichnet Audio über das Mikrofon ihres Gerätes auf.\\n\\nSie können die Lautstärkeanzeige nutzen, um sicher zu gehen, dass das Mikrofon nah genug am Geräusch ist, das Sie aufzeichnen möchten.\\n\\nUm die Aufzeichnung zu beenden, verlassen Sie das Formular.\\n\\nFür weitere Informationen kontaktieren Sie bitte die Person, die Sie gebeten hat die Daten zu sammeln.</string>\n  <string name=\"loading\">Lade…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Projekte</string>\n  <string name=\"settings\">Einstellungen</string>\n  <string name=\"add_project\">Projekt hinzufügen</string>\n  <string name=\"add\">Hinzufügen</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Duplikat-Projekt</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">Sie haben schon ein Projekt mit diesen Verbindungseinstellungen. Wollen Sie zum existierenden Projekt wechseln oder ein neues hinzufügen?</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Duplikat-Projekt hinzufügen</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Zum existierenden Projekt wechseln</string>\n  <string name=\"project_name\">Projekt-Name</string>\n  <string name=\"project_icon\">Projekt-Icon</string>\n  <string name=\"project_color\">Projekt-Farbe</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Gewechselt zu %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">Verwende %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Wechseln zu %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <string name=\"delete_project_dialog_title\">Lösche %s</string>\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <string name=\"delete_project_message\">Durch Löschen dieses Projektes, wird Folgendes permanent gelöscht</string>\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <string name=\"delete_project_instructions\">Geben Sie %1$s ein um diese Aktion zu bestätigen.</string>\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <string name=\"delete_trigger\">löschen</string>\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <string name=\"form_definitions_count\">Formulardefinitionen: %1$d</string>\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <string name=\"sent_count\">Versendet: %1$d</string>\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <string name=\"unsent_count\">Unversendet: %1$d</string>\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <string name=\"drafts_count\">Entwürfe: %1$d</string>\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"delete_project_confirm_button_text\">Projekt löschen</string>\n  <string name=\"or\">ODER</string>\n  <string name=\"hex_color\">Hex-Farbe</string>\n  <string name=\"invalid_hex_code\">Ungültiger Hex-Code</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Erfasse Daten\\nüberall</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Mit QR-Code konfigurieren</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Projektdetails manuell eingeben</string>\n  <string name=\"project_config_tip\">Nachdem Sie Ihr Projekt hinzugefügt haben, können Sie es in den konfigurieren.</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Noch kein eigenes Projekt?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Versuchen Sie es mit einer Demo</string>\n  <string name=\"scan_configuration_qr_code\">Konfigurations-QR-Code scannen</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Details anzeigen</string>\n  <string name=\"errors\">Fehler</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <string name=\"no_browser_installed\">Kein Browser installiert!</string>\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">Um dieses Formular zu öffnen, müssen Sie zuerst ODK Collect öffnen und in das Projekt wechseln, in dem es enthalten ist.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">ODK Collect wurde nicht konfiguriert. Versuchen Sie ODK Collect zu öffnen und zu konfigurieren.\\n\\nWenn Sie auf eine Verknüpfung getippt haben, müssen Sie diese vielleicht nach der Konfiguration neu erstellen.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Unbekannte URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Fehlerhafte URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Ausgefülltes Formular wurde gelöscht!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">Der Entwurf kann nicht bearbeitet werden, weil das zugehörige leere Formular nicht vorhanden ist oder gelöscht wurde.\\n\\nForm ID: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Zwei unterschiedliche Formulare mit derselben Form ID und Version heruntergeladen. Vielleicht wurde das gleiche Formular zu unterschiedlichen Zeiten oder auf unterschiedliche Server hochgeladen. In jedem Fall sollten Sie ein Formular löschen.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <string name=\"cannot_open_form_because_of_forms_update\">Dieses Formular kann zur Zeit nicht geöffnet werden, weil Formulare gerade aktualisiert werden. Bitte versuchen Sie es in Kürze erneut.</string>\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Einstellungen öffnen</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <string name=\"entities_title\">Objektliste</string>\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <string name=\"entity_browser_button\">Objektliste anzeigen</string>\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">Offline</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">Dieses Formular wird von dieser ODK Collect Version nicht unterstützt. Bitte Collect updaten. Falls dieses Problem weiterhin auftritt, melden Sie es der Person, die sie zur Datenerfassung beauftragt hat.\\n\\nForm Entities Spec Version: %s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">Es gab einen Absturz beim letzten Lauf der App!</string>\n  <string name=\"cant_start_app\">Kann die App nicht starten!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">Dieses Projekt war zuvor mit einem Google Drive-Konto verbunden. Die Unterstützung für Google Drive wurde entfernt. Sie können einen Server konfigurieren oder Übermittlungen auf einen Computer laden.</string>\n  <string name=\"learn_more_button_text\">Mehr erfahren</string>\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">Entwicklerwerkzeuge</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">App abstürzen lassen</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Unbehandelte Ausnahme erzwingen um die App abstürzen zu lassen</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <string name=\"debug_filters\">Debug-Filter</string>\n  <string name=\"debug_filters_summary\">Debuginformationen für Filterausdruck-Ausführungen während des Ausfüllens des Formulars anzeigen </string>\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"use_zxing_for_barcode_scanning\">Wechsele zu altem Barcode-Scanner</string>\n  <string name=\"permission_dialog_title\">Über Berechtigungen</string>\n  <string name=\"permission_dialog_text\">Sie werden aufgefordert, ODK Collect den Zugriff auf die unten aufgeführten Funktionen zu erlauben. Wählen Sie \\\"Zulassen\\\", wenn Sie sie nutzen möchten.</string>\n  <string name=\"notifications\">Benachrichtigungen</string>\n  <string name=\"required_for_notifications\">Wird benötigt um Aktualisierungen anzuzeigen, wenn Formulare heruntergeladen, aktualisiert und übertragen werden.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">Ihr Formular wurde als Entwurf gespeichert.</string>\n  <string name=\"form_saved\">Ihr Formular wurde gespeichert.</string>\n  <string name=\"form_sending\">Formular wird übertragen…</string>\n  <string name=\"edit_form\">Bearbeiten</string>\n  <string name=\"view_form\">Ansicht</string>\n  <string name=\"close_snackbar\">Snackbar schließen</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">Letztes Formular versandt: vor %d Sekunde</item>\n    <item quantity=\"other\">Letztes Formular versandt: vor %d Sekunden</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"one\">Letztes Formular versandt: vor %d Minute</item>\n    <item quantity=\"other\">Letztes Formular versandt: vor %d Minuten</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"one\">Letztes Formular versandt: vor %d Stunde</item>\n    <item quantity=\"other\">Letztes Formular versandt: vor %d Stunden</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"one\">Letztes Formular versandt: vor %d Tag</item>\n    <item quantity=\"other\">Letztes Formular versandt: vor %d Tagen</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"one\">%dFormular bereit zum Senden</item>\n    <item quantity=\"other\">%d Formulare bereit zum Senden</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">In zukünftigen Versionen werden abgeschlossene Formulare nicht mehr editierbar sein. Speichern Sie Formulare als Entwurf, um sie später zu bearbeiten.\\n\\nSie können einen Entwurf auf Fehler überprüfen, in dem Sie auf die drei Punkte (⋮) und dann Auf Fehler überprüfen klicken.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">In zukünftigen Versionen werden abgeschlossene Formulare nicht mehr editierbar sein.</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">Alle Entwürfe abschließen</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"one\">Wollen Sie %d Entwurf abschließen?</item>\n    <item quantity=\"other\">Wollen Sie %d Entwürfe abschließen?</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">Sobald alle Entwürfe abgeschlossen wurden, sind sie in \\\"Bereit zum Senden\\\" und sind nicht mehr editierbar. Entwürfe, die noch Fehler enthalten werden nicht abgeschlossen.\\n\\nDieser Vorgang lässt sich nicht rückgängig machen.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"one\">Fertig! %d Entwurf abgeschlossen.</item>\n    <item quantity=\"other\">Fertig! %d Entwürfe abgeschlossen.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"one\">%d Entwurf hat Fehler, die vor dem Abschließen korrigiert werden müssen.</item>\n    <item quantity=\"other\">%d Entwürfe haben Fehler, die vor dem Abschließen korrigiert werden müssen.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d Entwürfe abgeschlossen. %d Entwürfe haben Fehler, die vor dem Abschließen korrigiert werden müssen.</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d Entwürfe abgeschlossen. Verbleibende Entwürfe müssen manuell abgeschlossen werden.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Fehler</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">Keine Fehler</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">Abwählen um von Entwürfen zu verbergen</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">Keine Inhalte vorhanden.</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">Keine leeren Formulare</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">Formular herunterladen um zu starten</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">Keine Inhalte vorhanden.</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">Nach dem Speichern als Entwurf, erscheinen die Formulare hier</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">Keine Inhalte vorhanden.</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">Nach dem Abschließen, erscheinen die Formulare hier</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">Keine Inhalte vorhanden.</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">Nach dem Versenden abgeschlossener Formulare, erscheinen die Formulare hier</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">Keine Formulare zum Download</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">Formular herunterladen um zu starten</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">Keine Formulare zum Löschen</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">Wenn sie Leerformulare heruntergeladen haben, erscheinen sie hier.</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">Wenn Sie gespeicherte Formulare haben, erscheinen sie hier.</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">Ihre Arbeit Wiederherstellen?</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"savepoint_recovery_dialog_message\">\\'Collect wurde am \\'EEE, MMM dd, yyyy \\'um\\' HH:mm\\' unerwartet beendet und hat ihre Arbeit gespeichert.\\n\\nWollen Sie die gespeicherte Arbeit wiederherstellen oder verwerfen?\\'</string>\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">Wiederherstellen</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Verwerfen</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">Punkt erhalten</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">Punkt anzeigen oder ändern</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">Punkt anzeigen</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">Punkt ändern</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">Linie erhalten</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">Linie anzeigen oder ändern</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">Linie anzeigen</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">Polygon erhalten</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">Polygon anzeigen oder ändern</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">Polygon anzeigen</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <string name=\"settings_json\">Einstellungen-JSON</string>\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <string name=\"preparing_form_edit\">Formular für die Bearbeitung vorbereiten…</string>\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <string name=\"user_visible_instance_name\">%s (Änderung %d)</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_title\">Bearbeiten des Entwurfs fortsetzen?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_draft_edit_found_dialog_message\">\\'Ein bearbeiteter Entwurf wurde gespeichert am\\' EEE, MMM dd, yyyy \\'um\\' HH:mm\\'\\n\\nDiese Bearbeitung fortsetzen?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_positive_button\">Bearbeiten fortsetzen</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_title\">Aktuellste Version bearbeiten?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_finalized_edit_found_dialog_message\">\\'Diese Einsendung wurde zuvor bearbeitet.\\n\\nDie aktuellste Version abgeschlossen am\\' EEE, MMM dd, yyyy \\'um\\' HH:mm\\' bearbeiten?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_positive_button\">Aktuellstes Formular bearbeiten</string>\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n  <string name=\"finalized_form_edit_started\">Sie bearbeiten jetzt dieses Formular. Tippen sie auf eine Frage um die Bearbeitung zu starten.</string>\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-es/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Llenar nuevo formulario</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Borradores</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Listo para enviar</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Enviado</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Descargar formulario</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Borrar formulario</string>\n  <string name=\"saved_forms\">Formularios guardados</string>\n  <string name=\"version_number\">Versión: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Agregado:\\' EEE, dd MMM, yyyy \\'a las\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Actualizado\\' EEE dd \\'de\\' MMM \\'de\\' yyyy \\'a las\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Guardado:\\' EEE, dd MMM, yyyy \\'a las\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Finalizado:\\' EEE, dd MMM, yyyy \\'a las\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Enviado:\\' EEE, dd MMM, yyyy \\'a las\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Envio fallado:\\' EEE, dd MMM, yyyy \\'a las\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Borrado el\\' EEE dd \\'de\\' MMM \\'de\\' yyyy \\'a las\\' HH:mm</string>\n  <string name=\"submission_deleted\">Envío borrado</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Modificado el\\' EEE, MMM dd, yyyy \\'a las\\' HH:mm</string>\n  <string name=\"encrypted_form\">Formulario encriptado</string>\n  <string name=\"deleted_form\">Formulario borrado</string>\n  <string name=\"sort_the_list\">Ordenar la lista</string>\n  <string name=\"sort_by\">Ordenar por</string>\n  <string name=\"sort_by_name_asc\">Ascendente por Nombre, A-Z</string>\n  <string name=\"sort_by_name_desc\">Descendente por Nombre, Z-A</string>\n  <string name=\"sort_by_date_desc\">Fecha, más reciente de primero</string>\n  <string name=\"sort_by_date_asc\">Fecha, más antiguo de primero</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Último guardado primero</string>\n  <string name=\"filter_the_list\">Filtrar la lista</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Buscar</string>\n  <string name=\"not_granted_permission\">La actividad que está intentando iniciar requiere un permiso que no se ha concedido.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Permisos de Almacenamiento</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Sin estos permisos no se puede accesar los formularios o guardar las respuetas. Abra la aplicación de nuevo cuando esté listo para dar acceso</string>\n  <string name=\"location_runtime_permissions_denied_title\">Permisos de Ubicación</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Sin el permiso de <b>Localización precisa</b> no puede registrar su ubicación. Intente de nuevo cuando esté el permiso.</string>\n  <string name=\"camera_runtime_permission_denied_title\">Permiso de Cámara</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Sin este permiso la aplicación no puede acceder a la cámara. Intente de nuevo cuando esté el permiso.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Permiso de Grabación de Audio</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Sin este permiso la aplicación no puede acceder al micrófono. Intente de nuevo cuando esté el permiso.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Abrir mapa de formulario</string>\n  <string name=\"instance_scan_count\">%1$d formularios añadidos.</string>\n  <string name=\"finished_disk_scan\">Exploración finalizada. Todos los formularios cargados.</string>\n  <string name=\"xform_parse_error\">XForm error de análisis para %1$s: %2$s es inválido o no se encuentra.</string>\n  <string name=\"loading_form\">Cargando Formulario</string>\n  <string name=\"survey_loading_reading_form_message\">Leyendo definición de formulario…</string>\n  <string name=\"survey_loading_reading_data_message\">Leyendo datos sondeo…</string>\n  <string name=\"survey_loading_reading_csv_message\">Leyendo archivos CSV…</string>\n  <string name=\"parse_error\">No se pudo interpretar el formulario.</string>\n  <string name=\"loading_form_failed\">Ocurrió un error al cargar el formulario. Por favor intente de nuevo.</string>\n  <string name=\"cannot_edit_completed_form\">Este formulario no puede ser editado una vez que ha sido marcado como finalizado. Puede estar encriptado.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">¡Cambios no guardados recuperados del punto de recuperación!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Cambiar el Idioma</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Ir a la Pregunta</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Guardar como borrador</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Ir al Inicio</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Ir al Final</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Subir</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <string name=\"edit_finalized_form\">Editar formulario completado</string>\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Grupo</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Grupo Repetible</string>\n  <string name=\"exit\">Salir</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Agregar \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Agregar</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"do_not_add_repeat\">No agregar</string>\n  <string name=\"add_another_menu\">Agregar otro</string>\n  <string name=\"add_another_question\">Agregar otro</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Editar entrada</string>\n  <string name=\"clearanswer_confirm\">¿Borrar la respuesta a \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Borrar respuesta</string>\n  <string name=\"clear_answer_ask\">¿Borrar esta respuesta?</string>\n  <string name=\"clear_answer_no\">Cancelar</string>\n  <string name=\"discard_answer\">Borrar respuesta</string>\n  <string name=\"discard_group\">Borrar grupo</string>\n  <string name=\"delete_repeat\">Borrar grupo</string>\n  <string name=\"delete_repeat_ask\">¿Borrar este grupo?</string>\n  <string name=\"delete_repeat_confirm\">¿Borrar el grupo \\\"%s\\\" y todos sus subgrupos?</string>\n  <string name=\"delete_repeat_no\">Cancelar</string>\n  <string name=\"invalid_answer_error\">¡Respuesta invalida!</string>\n  <string name=\"required_answer_error\">¡Respuesta obligatoria!</string>\n  <string name=\"success_form_validation\">¡Éxito! No se encontraron errores en su formulario.</string>\n  <string name=\"invalid_space_in_answer_singular\">Advertencia: valor subyacente %s contiene espacios</string>\n  <string name=\"invalid_space_in_answer_plural\">Advertencia: valores subyacentes %s contienen espacios</string>\n  <string name=\"too_complex_form\">Este formulario es muy complejo para este dispositivo. Intente simplificar las expresiones o busque ayuda en el Foro.</string>\n  <string name=\"survey_internal_error\">Error interno: Ha fallado la entrada de datos.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Estás al final de \\\"%s\\\".</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <string name=\"form_editing_disabled_after_finalizing\">No se pueden realizar modificaciones una vez finalizado.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <string name=\"form_editing_disabled_after_sending\">No se pueden realizar modificaciones después del envío.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_finalizing\">Se pueden realizar modificaciones después de finalizar.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_sending\">No se pueden realizar modificaciones después del envío.</string>\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Si necesita hacer cambios, \\\"Guardar como borrador\\\" hasta que esté listo para enviar.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <string name=\"form_editing_enabled_after_finalizing_hint\">Para realizar cambios después de finalizar su formulario, vaya a \\\"Listo para enviar\\\", abra el formulario y toque el ícono de edición.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <string name=\"form_editing_enabled_after_sending_hint\">Para realizar cambios después de enviar su formulario, vaya a \\\"Enviados\\\", abra el formulario y toque el ícono de edición.</string>\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Guardar como borrador</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Finalizar</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Enviar</string>\n  <string name=\"data_saved_error\">¡Atención, el formulario no se ha guardado correctamente!</string>\n  <string name=\"data_saved_ok\">Formulario guardado correctamente</string>\n  <string name=\"save_point_error\">Error al guardar punto de recuperación: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Descartar formulario</string>\n  <string name=\"discard_changes\">Descartar los cambios</string>\n  <string name=\"keep_changes\">Guardar los cambios</string>\n  <string name=\"keep_editing\">Seguir editando</string>\n  <string name=\"quit_form_title\">Guardar formulario?</string>\n  <string name=\"save_explanation\">Puede guardar este formulario y acceder a él desde sus borradores en cualquier momento.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'Este formulario se guardó por última vez el\\' EEE, MMM dd, yyyy \\'a las\\' HH:mm\\'. Guardar como borrador para conservar los cambios.\\'</string>\n  <string name=\"quit_form_continue_title\">Seguir editando?</string>\n  <string name=\"discard_form_warning\">Si descarta el formulario, perderá todos los cambios que haya realizado hasta el momento.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'Este formulario se guardó por última vez el\\' EEE, MMM dd, yyyy \\'a las\\' HH:mm\\'. Si descarta los cambios, perderá los cambios que haya realizado desde entonces.\\'</string>\n  <string name=\"saving_form\">Guardando el formulario</string>\n  <string name=\"survey_saving_validating_message\">Validando respuestas…</string>\n  <string name=\"survey_saving_collecting_message\">Colectando datos…</string>\n  <string name=\"survey_saving_saving_message\">Guardando en tarjeta SD </string>\n  <string name=\"survey_saving_finalizing_message\">FInalizando en tarjeta SD</string>\n  <string name=\"survey_saving_encrypting_message\">Encriptando datos…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">No hay exactamente un registro para esta instancia!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">No hay exactamente un formulario en blanco que corresponda a este jr_form_id.</string>\n  <string name=\"no_form_id_specified\">No se especifo el FormID???</string>\n  <string name=\"phone_does_not_support_rsa\">El teléfono no es compatible con el cifrado RSA.</string>\n  <string name=\"invalid_rsa_public_key\">Clave publica RSA Invalida.</string>\n  <string name=\"encryption_error_message\">%s del Formulario no ha sido guardada como finalizado.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">El formulario no se puede utilizar.</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">No se pudo iniciar la grabación.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">El micrófono ya está en uso.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">Los archivos multimedia no se pudieron adjuntar al formulario, pero están disponibles en %1$s</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Grabación deshabilitada. Habilitar en %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Error al intentar actualizar los valores. Esto generalmente se debe a un uso incorrecto de los cálculos en el diseño del formulario.\\n\\nSi sigue teniendo este problema, infórmelo a la persona que le pidió que recopilara los datos.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Iniciar</string>\n  <string name=\"activity_not_found\">No se encontró actividad para manejar: %s</string>\n  <string name=\"no_app\">Esta aplicación no está. Por favor ingrese el dato manualmente.</string>\n  <string name=\"null_intent_value\">La aplicación externa no suministra la información esperada</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">Imprimir</string>\n  <string name=\"open_url\">Abrir URL</string>\n  <string name=\"capture_osm\">Iniciar OpenMapKit</string>\n  <string name=\"recapture_osm\">Rehacer OSM Tagging</string>\n  <string name=\"edited_osm_file\">Archivo OSM XML editado:</string>\n  <string name=\"invalid_osm_data\">Algo salió mal. No obtuvimos datos válidos de OSM.</string>\n  <string name=\"alert\">Alerta</string>\n  <string name=\"install_openmapkit\">Por favor instalar OpenMapKit!</string>\n  <string name=\"get_barcode\">Escanear Codigo de Barras</string>\n  <string name=\"replace_barcode\">Reemplazar el Codigo de Barras</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Coloque el código de barras dentro del rectángulo</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"rotate_device\">Girar dispositivo</string>\n  <string name=\"open_file\">Abrir archivo</string>\n  <string name=\"rank_items\">Ranking de Items</string>\n  <string name=\"number_picker_title\">Selector de números</string>\n  <string name=\"invalid_range_widget\">Esta opción tiene parámetros inválidos</string>\n  <string name=\"svg_file_does_not_exist\">Archivo SVG no existe</string>\n  <string name=\"capture_image\">Tomar la Foto</string>\n  <string name=\"choose_image\">Escoja la Imagen</string>\n  <string name=\"selected_invalid_image\">El archivo seleccionado no es una imagen válida</string>\n  <string name=\"take_picture_instruction\">Toca la pantalla para tomar una foto</string>\n  <string name=\"error_front_camera_unavailable\">Este dispositivo no tiene Cámara frontal disponible </string>\n  <string name=\"camera_error\">Hubo un error al tomar la foto</string>\n  <string name=\"camera_failed_to_initialize\">¡No se pudo inicializar la cámara!</string>\n  <string name=\"annotate_image\">Anotar en Imagen</string>\n  <string name=\"gif_not_supported\">No se admiten archivos GIF</string>\n  <string name=\"invalid_file_type\">La aplicación devolvió un tipo de archivo no válido</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_title\">Guardar los cambios?</string>\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_explanation\">¿Desea guardar los cambios antes de salir?</string>\n  <string name=\"save_and_close\">Guardar y Cerrar</string>\n  <string name=\"signature_capture\">Firma</string>\n  <string name=\"sign_button\">Adjuntar firma</string>\n  <string name=\"markup_image\">Anotar en imagen</string>\n  <string name=\"draw_image\">Boceto</string>\n  <string name=\"reset_image\">Restablecer</string>\n  <string name=\"set_color\">Seleccionar Color</string>\n  <string name=\"capture_video\">Grabar el Video</string>\n  <string name=\"choose_video\">Escoja el Video</string>\n  <string name=\"play_video\">Reproducir el Video</string>\n  <string name=\"choose_file\">Escoger Archivo</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Eliminar</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Grabar sonido</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Escoja el Sonido</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Pausar</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Reanudar</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">Grabar audio: on</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">Grabar audio: off</string>\n  <string name=\"background_audio_permission_explanation\">Este formulario graba audio en segundo plano. Debe otorgar permiso para usar su micrófono. Si no lo hace, no podrá abrir este formulario.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Este formulario solicita una grabación de audio de fondo. Si lo desactiva, se detendrá la grabación y se descartará el audio existente. ¿Estas seguro que deseas continuar?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Desactivar la grabación</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">La grabación no comenzará inmediatamente. Debe volver a abrir el formulario para grabar.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Borrar archivo?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">No podrá recuperar este archivo una vez que se elimine. ¿Estas seguro que deseas continuar?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Parar</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Grabando…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Grabación de audio</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Reproducir audio</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Guardando archivo</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Debe detener la grabación antes de salir de esta pantalla.</string>\n  <string name=\"error_attaching_binary_file\">Error detectado al conectar medio: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Seleccionar valor</string>\n  <string name=\"select_answer\">Seleccione una respuesta</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Editar valor</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Ningún valor seleccionado</string>\n  <string name=\"selected_title\">Seleccionada:</string>\n  <string name=\"trigger\">OK. Por favor continúe.</string>\n  <string name=\"ext_file_no_data_error\">¡El archivo no contiene datos!</string>\n  <string name=\"ext_conflicting_columns_error\">¡Columnas %s coinciden!</string>\n  <string name=\"ext_sortBy_numeric_error\">Su columna de clasificación debe contener solo valores numéricos. El valor conflictivo fue \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Error sintáctico en función search(): La función necesita 1, 4 o 6 argumentos.</string>\n  <string name=\"ext_search_wrong_function_error\">Error sintáctico en función search(): Función \\'%s\\' desconocida.</string>\n  <string name=\"ext_search_bad_function_error\">Error sintáctico en función search(): \\'%s\\' no fue evaluada como una función.</string>\n  <string name=\"ext_search_return_error\">El gestor de búsqueda ha devuelto un objeto de tipo \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">No puede importar datos desde %1$s. Razón: %2$s</string>\n  <string name=\"ext_import_progress_message\">Precarga datos desde \\'%1$s\\'. Por favor espere… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Lectura de datos cancelada!</string>\n  <string name=\"ext_import_finalizing_message\">Acabando datos precargados…</string>\n  <string name=\"ext_import_completed_message\">¡Lectura de datos completada!</string>\n  <string name=\"ext_not_initialized_error\">El ExternalDataManager no ha sido inicializado.</string>\n  <string name=\"ext_import_csv_missing_error\">Los datos externos para %1$s no ha sido importados. ¿Ha olvidado incluir el archivo %2$s.csv con su formulario?</string>\n  <string name=\"ext_search_generic_error\">Error sintáctico en función search(): %s</string>\n  <string name=\"ext_assign_value_error\">No puede asignar el valor a \\'%s\\'.</string>\n  <string name=\"parser_exception\">Excepción XPathParser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Registrar Orientación</string>\n  <string name=\"replace_bearing\">Cambiar Orientación</string>\n  <string name=\"getting_bearing\">Cargando Orientación</string>\n  <string name=\"accept_bearing\">Guardar Orientación</string>\n  <string name=\"bearing\">Orientación: %.3f</string>\n  <string name=\"direction\">Dirección: %s</string>\n  <string name=\"bearing_lack_of_sensors\">La Orientación no puede ser registrada: El dispositivo no tiene acelerómetro, o sensor de campo magnético o ambos.</string>\n  <string name=\"select_date\">Seleccione la fecha</string>\n  <string name=\"select_time\">Seleccione la hora</string>\n  <string name=\"no_date_selected\">No se ha seleccionado una fecha</string>\n  <string name=\"no_time_selected\">No se ha seleccionado una hora</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <string name=\"buddhist_month_1\">Enero</string>\n  <!--2nd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_2\">Febrero</string>\n  <!--3rd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_3\">Marzo</string>\n  <!--4th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_4\">Abril</string>\n  <!--5th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_5\">Mayo</string>\n  <!--6th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_6\">Junio</string>\n  <!--7th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_7\">Julio</string>\n  <!--8th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_8\">Agosto</string>\n  <!--9th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_9\">Septiembre</string>\n  <!--10th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_10\">Octubre</string>\n  <!--11th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_11\">Noviembre</string>\n  <!--12th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_12\">Diciembre</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Archivo: %s es invalido.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Archivo: %s falta.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Ingrese su Identidad</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Identidad</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Este formulario requiere que te identifiques</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Cerrar</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Motivo de los cambios</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Motivo</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Favor explicar el motivo de los cambios para este formulario</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Guardar</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">Seleccionar lugar</string>\n  <string name=\"choices\">Opciones</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS Mapa Topográfico Nacional</string>\n  <string name=\"openmap_usgs_sat\">USGS Mapa Nacional Hibrido</string>\n  <string name=\"openmap_usgs_img\">USGS Mapa Nacional Satélite</string>\n  <string name=\"openmap_cartodb_positron\">Positron CartoDB</string>\n  <string name=\"openmap_cartodb_darkmatter\">Materia Obscura CartoDB</string>\n  <string name=\"basemap\">Mapa Base</string>\n  <string name=\"basemap_source\">Fuente</string>\n  <string name=\"basemap_source_unavailable\">Lo sentimos, los mapas base de %s  no están disponibles en este dispositivo. Elija otro mapa base en Configuración &gt; Mapas.</string>\n  <string name=\"map_style_label\">Estilo de Mapa %s </string>\n  <string name=\"streets\">Calles</string>\n  <string name=\"terrain\">Terreno</string>\n  <string name=\"hybrid\">Híbrido</string>\n  <string name=\"satellite\">Satélite</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Claro</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Oscuro</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Exteriores</string>\n  <string name=\"topographic\">Topográfico</string>\n  <string name=\"google_play_services_error_occured\">No se puede acceder a Google Maps. ¿Están los servicios de Google Play instalados?</string>\n  <string name=\"carto_map_style_positron\">Positrón</string>\n  <string name=\"carto_map_style_dark_matter\">Materia oscura</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">Capas sin conexión</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">Capa</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">Seleccione capa sin conexión</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">Seleccione la capa sin conexión para usar en todos los mapas de este proyecto. Puede agregar opciones a la lista seleccionando un archivo MBTiles de su dispositivo y también eliminar capas existentes de la lista.</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">Obtenga más información sobre cómo agregar MBTiles.</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">Agregar capas</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">Borrar capa</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <plurals name=\"non_mbtiles_files_selected_title\">\n    <item quantity=\"one\">%d No se puede agregar la capa</item>\n    <item quantity=\"many\">%d No se pueden agregar las capas</item>\n    <item quantity=\"other\">%d no se pueden agregar capas</item>\n  </plurals>\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">Los archivos seleccionados no son MBTiles. Sólo puede agregar archivos MBTiles.</string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">Los archivos seleccionados no son MBTiles. Sólo puede agregar archivos MBTiles.</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">Capas</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">Seleccionar acceso a capas</string>\n  <string name=\"select_layer_access_subtitle\">¿Quiere que las capas estén disponibles en todos los proyectos o solo en su proyecto actual?</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">Todos los proyectos </string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">Sólo proyecto actual</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">¿Está seguro de que desea eliminar la capa sin conexión %1$s ?</string>\n  <string name=\"record_geopoint\">Registrando un punto</string>\n  <string name=\"location_accuracy\">Precisión: %1$s</string>\n  <string name=\"location_accuracy_unacceptable\">Precisión de ubicación: %1$s (inaceptable)</string>\n  <string name=\"provider_disabled_error\">¡Los proveedores de ubicación están deshabilitados!</string>\n  <string name=\"gps_result\">Latitud: %1$s\\nLongitud: %2$s\\nAltitud: %3$sm\\nPrecisión: %4$sm</string>\n  <string name=\"google_play_services_not_available\">Este formulario quiere rastrear su ubicación, pero los servicios de Google Play no están disponibles.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Este formulario quiere rastrear su ubicación pero los proveedores de ubicación están deshabilitados Por favor, habilite en la configuración de Android.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Ir a ajustes</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">Rastrear ubicación: on</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">Rastrear ubicación: off</string>\n  <string name=\"background_location_disabled\">Este formulario requiere registrar su localización, pero el rastreo esta deshabilitado. Favor habilitar en el menú de arriba %1$s .</string>\n  <string name=\"background_location_enabled\">Este formulario registra su localización. Puede deshabilitar el rastreo en el menú de arriba %1$s.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">Comprobar si hay errores</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">Tiempo transcurrido: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">Punto se guardará en %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">Satélites: %1$s</string>\n  <string name=\"waiting_for_location\">Tratando de obtener la ubicación. Espere por favor.</string>\n  <string name=\"poor_accuracy\">Mala precisión. Espere por favor.</string>\n  <string name=\"unacceptable_accuracy\">Precisión inaceptable. Espere por favor.</string>\n  <string name=\"improving_accuracy\">Mejorando precisión. Espere por favor.</string>\n  <string name=\"accuracy_status_title\">Obteniendo ubicación</string>\n  <!--The accuracy of a location fix in meters-->\n  <string name=\"accuracy_m\">%1$s m</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">O %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Método de ingreso</string>\n  <string name=\"start\">Iniciar</string>\n  <string name=\"enable_gps\">Activar el GPS</string>\n  <string name=\"gps_enable_message\">El GPS está deshabilitado en este dispositivo. Le gustaría habilitarlo?</string>\n  <string name=\"geo_clear_warning\">Función ya creada. ¿Desea borrar la función?</string>\n  <string name=\"geo_exit_warning\">¿Ignorar los cambios y regresar?</string>\n  <string name=\"polygon_validator\">Debe tener al menos 3 puntos para crear un polígono</string>\n  <string name=\"polyline_validator\">Deben existir al menos 2 puntos para crear una Polilínea</string>\n  <string name=\"clear\">Limpiar</string>\n  <string name=\"placement_mode\">Ubicación con toque</string>\n  <string name=\"manual_mode\">Registro de localización manual</string>\n  <string name=\"automatic_mode\">Registro de localización automática</string>\n  <string name=\"recording_interval\">Intervalo de registro:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d segundo</item>\n    <item quantity=\"many\">%d segundos</item>\n    <item quantity=\"other\">%d segundos</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d minuto</item>\n    <item quantity=\"many\">%d minutos</item>\n    <item quantity=\"other\">%d minutos</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Requisito de precisión</string>\n  <string name=\"none\">Ninguno</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d metro</item>\n    <item quantity=\"many\">%d metros</item>\n    <item quantity=\"other\">%d metros</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Puntos ingresados: %d</string>\n  <string name=\"collection_status_placement\">Puntos ingresados: %d (toque para ubicar puntos)</string>\n  <string name=\"collection_status_manual\">Puntos ingresados: %d (registro manual)</string>\n  <string name=\"collection_status_auto_seconds\">Puntos ingresados: %1$d (registrando cada %2$d seg)</string>\n  <string name=\"collection_status_auto_minutes\">Puntos ingresados: %1$d (registrando cada %2$d min)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Puntos ingresados: %1$d (registrando cada %2$d seg, precisión %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Puntos ingresados: %1$d (registrando cada %2$d min, precisión %3$d m)</string>\n  <string name=\"geopoint_instruction\">Mantenga pulsado para colocar una marca o toque el botón para agregar marcador</string>\n  <string name=\"geopoint_no_draggable_instruction\">Tap botón de agregar marker</string>\n  <string name=\"discard\">Descartar</string>\n  <string name=\"select_item_count\">%s: %d (%d mostrar en el mapa)</string>\n  <string name=\"select_item\">Seleccionar</string>\n  <string name=\"new_item\">Nuevo ítem</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">Editar borrador</string>\n  <string name=\"view_data\">Ver formulario guardado</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">Seguimiento de ubicación</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">Monitoreando ubicación…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Mostrar mi ubicación</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">Zoom para mostrar todo</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Pausar</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">Remover el último punto</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <string name=\"error_fixed\">✅ Error corregido</string>\n  <!--Description for button that explains how to modify the map-->\n  <string name=\"show_how_to_modify_map\">Muestra cómo modificar el mapa</string>\n  <!--Title for instructions on how to modify map features-->\n  <string name=\"how_to_modify_map\">Cómo modificar el mapa</string>\n  <!--Instruction in a list on how to move a point-->\n  <string name=\"long_press_to_move_point_info_item\">Mantenga pulsado para mover el punto</string>\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">Remover el último punto</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <string name=\"delete_shape_to_start_over_info_item\">Borrar forma para empezar de nuevo</string>\n  <!--Instruction in a list on how to add a new point-->\n  <string name=\"add_point_info_item\">Añadir punto</string>\n  <!--Instruction in a list on how to tap to add a point-->\n  <string name=\"tap_to_add_a_point_info_item\">Toque para agregar un punto</string>\n  <!--Instruction in a list on how to delete the entire shape-->\n  <string name=\"delete_entire_shape_info_item\">Borrar toda la forma</string>\n  <!--Button text to close the instructions dialog-->\n  <string name=\"done\">Realizado</string>\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <string name=\"how_to_modify\">Cómo modificar</string>\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Enviando el formulario</string>\n  <string name=\"noselect_error\">¡No ha escogido ningún formulario!</string>\n  <string name=\"no_connection\">No hay conexión a internet</string>\n  <string name=\"sending_items\">Enviando %1$s de %2$s formulario(s)</string>\n  <string name=\"send_selected_data\">Enviar seleccionado</string>\n  <string name=\"server_auth_credentials\">Usuario o contraseña inválidos para el servidor: %s</string>\n  <string name=\"server_requires_auth\">El servidor requiere autenticacion</string>\n  <string name=\"no_forms_uploaded\">No se ha cargado ningún formulario.</string>\n  <string name=\"upload_results\">Resultados de la carga</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Cambiar Vista</string>\n  <string name=\"show_sent_and_unsent_forms\">Formularios enviados y no enviados</string>\n  <string name=\"show_unsent_forms\">Solo formularios no enviados</string>\n  <string name=\"send_in_progress\">Otro envío funcionando, reintentar en breve</string>\n  <string name=\"forms_upload_succeeded\">Carga de formularios exitosa</string>\n  <string name=\"forms_upload_failed\">Error al cargar formularios</string>\n  <string name=\"all_uploads_succeeded\">¡Todas las cargas se realizaron correctamente!</string>\n  <string name=\"some_uploads_failed\">%1$s de %2$s cargas fallaron</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Actualizar</string>\n  <string name=\"download\">Obtener los Seleccionados</string>\n  <string name=\"downloading_data\">Conectandose al Servidor</string>\n  <string name=\"fetching_file\">Obteniendo %1$s.\\n\\nFormulario %2$s de %3$s formulario(s).</string>\n  <string name=\"form_download_progress\">%1$s. Obteniendo los archivos: %2$s de %3$s</string>\n  <string name=\"version\">Version:</string>\n  <string name=\"form_updates_available\">Formularios actualizados disponibles</string>\n  <string name=\"newer_version_of_a_form_info\">Esta es una actualización a un formulario que tiene</string>\n  <string name=\"form_update_error\">Error al actualizar formulario</string>\n  <string name=\"form_update_error_massage\">Abre ODK Collect para volver a intentarlo.</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"form_update_notification_title\">Sincronización de formularios</string>\n  <string name=\"load_remote_form_error\">Error al descargar la lista de formularios</string>\n  <string name=\"forms_download_succeeded\">Descarga de formularios exitosa</string>\n  <string name=\"forms_download_failed\">Error al descargar formularios</string>\n  <string name=\"all_downloads_succeeded\">¡Todas las descargas se realizaron correctamente!</string>\n  <string name=\"some_downloads_failed\">%1$s de %2$s descargas fallidas!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Version: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Si sigue teniendo este problema, infórmelo a la persona que le pidió que recopile los datos.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">La aplicación no se ha podido conectar al servidor %s. ¿Ingresó la URL correctamente?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">La aplicación no se puede conectar de forma segura al servidor en %s. ¿Ingresaste la URL correctamente?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">El servidor %1$s devolvió código de estado %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">El servidor %s proporcionó una respuesta no válida.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">El servidor no proporcionó un hash para este formulario.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_parsing_error\">Este formulario no puede ser procesado:</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">Se produjo un error de disco en el dispositivo al descargar este formulario.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">Este formulario tiene una URL de envío inválida.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">F. LLENOS</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">F. VACIOS</string>\n  <string name=\"delete_confirm\">¿Borrar %s formulario(s)?</string>\n  <string name=\"delete_file\">Borrar los Seleccionados</string>\n  <string name=\"delete_no\">No Borrar</string>\n  <string name=\"delete_yes\">Borrar los Formularios</string>\n  <string name=\"form_delete_message\">Eliminando formularios seleccionados</string>\n  <string name=\"file_deleted_ok\">¡%s formulario(s) se borraron correctamente!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Ajustes de proyecto</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Servidor - Usuario</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, usuario, contraseña</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Visualización del proyecto</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Nombre, icono, color</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Interfaz de usuario</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_interface_settings_subtext\">Idioma de la aplicación, tamaño de fuente, navegación</string>\n  <!--Title of a settings section-->\n  <string name=\"maps\">Mapas</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Mapa base, estilo, capas</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Manejo de formularios</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">Actualización automática, autoenvío, autoborrado</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Metadatos del formulario</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">Usuario, teléfono, ID dispositivo</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Experimental</string>\n  <string name=\"protected_settings\">Protegido</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">Desbloquear configuraciones protegidas</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">Cambiar contrasela, ajustar controles de acceso</string>\n  <string name=\"set_admin_password\">Establecer contraseña de administrador</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Gestión de proyectos</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">Reconfigurar, restablecer, borrar</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Control de acceso</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">Limitar la interfaz de usuario</string>\n  <string name=\"server_preferences\">Configuración del Servidor</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL del Servidor</string>\n  <string name=\"url_error\">¡URL invalida!</string>\n  <string name=\"username\">Nombre del Usuario</string>\n  <string name=\"change_username\">Nombre de Usuario</string>\n  <string name=\"password\">Contraseña</string>\n  <string name=\"change_password\">Contraseña</string>\n  <string name=\"password_error_whitespace\">Los espacios al inicio o al final no están permitidos en las contraseñas</string>\n  <string name=\"username_error_whitespace\">No se permiten espacios al inicio o al final en el nombre de usuario.</string>\n  <string name=\"app_theme\">Tema</string>\n  <string name=\"language\">Idioma</string>\n  <string name=\"use_device_language\">Usar el idioma del dispositivo</string>\n  <string name=\"font_size\">Tamaño del Texto</string>\n  <string name=\"change_font_size\">Tamaño del Texto</string>\n  <string name=\"font_size_extra_large\">Extra Grande</string>\n  <string name=\"font_size_large\">Grande</string>\n  <string name=\"font_size_medium\">Medio</string>\n  <string name=\"font_size_small\">Pequeño</string>\n  <string name=\"font_size_extra_small\">Muy pequeño</string>\n  <string name=\"navigation\">Navegación</string>\n  <string name=\"swipe_navigation\">Use deslizamiento horizontal</string>\n  <string name=\"buttons_navigation\">Use los botones siguiente/atrás</string>\n  <string name=\"swipe_buttons_navigation\">Use deslizar y botones</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Siguiente</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Atrás</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Actualizar Formulario</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Modo de actualización de formulario en blanco</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Manual</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Solo formularios descargados previamente</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Coincidencia exacta de servidor</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Frecuencia de actualización automática</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Cada 15 minutos</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Cada hora</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Cada 6 horas</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Cada 24 horas</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Descarga automática</string>\n  <string name=\"automatic_download_summary\">Nuevas versiones de formularios descargadas automáticamente</string>\n  <string name=\"hide_old_form_versions_setting_title\">Ocultar formularios anteriores </string>\n  <string name=\"hide_old_form_versions_setting_summary\">Sólo la versión mas nueva aparecerá en Llenar Nuevo Formulario</string>\n  <string name=\"form_submission_category\">Enviar formulario</string>\n  <string name=\"autosend_selector_title\">Envío Automático</string>\n  <string name=\"autosend\">Envío Automático</string>\n  <string name=\"off\">Desactivado</string>\n  <string name=\"wifi_autosend\">Solo con WIFI</string>\n  <string name=\"cellular_autosend\">Solo con Datos Móviles</string>\n  <string name=\"wifi_cellular_autosend\">Wifi o Datos Móviles</string>\n  <string name=\"delete_after_send\">Eliminar después de enviar</string>\n  <string name=\"delete_after_send_summary\">Eliminar formularios finalizados y archivos multimedia después de enviar al servidor</string>\n  <string name=\"form_filling_category\">Llenado de Formularios</string>\n  <string name=\"constraint_behavior_title\">Manejo de restricciones</string>\n  <string name=\"constraint_behavior\">Manejo de las restricciones</string>\n  <string name=\"constraint_behavior_on_swipe\">Validar al desplazar hacia delante.</string>\n  <string name=\"constraint_behavior_on_finalize\">Postponer validación hasta el final</string>\n  <string name=\"high_resolution_title\">Video de alta resolución</string>\n  <string name=\"high_resolution_summary\">Grabar vídeos en HD</string>\n  <string name=\"image_size_title\">Tamaño de imagen</string>\n  <string name=\"image_size_dialog_title\">Número de pixeles máximo del lado más largo de la imagen</string>\n  <string name=\"image_size_large\">Grande (3072px)</string>\n  <string name=\"image_size_medium\">Mediana (2048px)</string>\n  <string name=\"image_size_small\">Pequeña (1024px)</string>\n  <string name=\"image_size_very_small\">Muy pequeña (640px)</string>\n  <string name=\"image_size_original\">Tamaño original de la cámara (por defecto)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Mostrar guías de preguntas</string>\n  <string name=\"guidance_no\">No</string>\n  <string name=\"guidance_yes\">Si - Mostrar Siempre</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Si - Colapsado</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">Use una aplicación externa para grabación de audio</string>\n  <string name=\"form_import_category\">Importar formulario</string>\n  <string name=\"instance_sync\">Finalizar formularios en importación</string>\n  <string name=\"instance_sync_summary\">Afecta los formularios agregados directamente a la carpeta de instances</string>\n  <string name=\"analytics_preferences\">Datos de uso</string>\n  <string name=\"analytics\">Recolectar datos de uso anónimos</string>\n  <string name=\"analytics_summary\">Los datos de uso anónimos ayudan al equipo de ODK a priorizar las correcciones y las funciones.</string>\n  <string name=\"form_metadata_title\">Metadatos del formulario</string>\n  <string name=\"form_metadata\">Metadatos del formulario</string>\n  <string name=\"form_metadata_summary\">Estos valores se incluyen en los formularios que tienen precargados los campos de nombre de usuario, email y/o teléfono para identificar a la persona que envía los datos.</string>\n  <string name=\"user_defined\">Definido por el usuario</string>\n  <string name=\"phone_number\">Numero de teléfono</string>\n  <string name=\"email\">Dirección de correo electrónico</string>\n  <string name=\"invalid_email_address\">Email inválido</string>\n  <string name=\"device_defined\">Definido por el dispositivo</string>\n  <string name=\"device_id\">ID de dispositivo</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">No disponible</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">Desbloquear ajustes</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">Ajustes desbloqueados</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <string name=\"setting_not_available_during_form_entry\">Configuración no disponible durante la entrada del formulario</string>\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Entrar nueva contraseña</string>\n  <string name=\"admin_password\">Contraseña de Administrador</string>\n  <string name=\"admin_password_changed\">Contraseña Administrador cambiada correctamente</string>\n  <string name=\"admin_password_disabled\">Contraseña Administrador deshabilitada</string>\n  <string name=\"admin_password_incorrect\">¡Contraseña incorrecta!</string>\n  <string name=\"enter_admin_password\">Entrar contraseña de Administrador</string>\n  <string name=\"change_admin_password\">Cambiar Contraseña de Administrador</string>\n  <string name=\"show_password\">Mostrar contraseña</string>\n  <string name=\"found_in_main\">Desmarque para ocultar del Menú Principal</string>\n  <string name=\"found_in_settings\">Desmarque para ocultar la configuración desprotegida</string>\n  <string name=\"form_entry\">Desmarque para ocultar el formulario</string>\n  <string name=\"form_end\">Desmarque para ocultar del Fin del formulario</string>\n  <string name=\"reset_project_settings_title\">Restablecer</string>\n  <string name=\"reset_project_settings_subtext\">Elija entre configuraciones, formularios, datos</string>\n  <string name=\"delete_project\">Eliminar</string>\n  <string name=\"select_all\">Seleccionar todo</string>\n  <string name=\"clear_all\">Limpiar todo</string>\n  <string name=\"reset_settings\">Todos los ajustes  (Ajustes internos, ajustes guardados)</string>\n  <string name=\"reset_saved_forms\">Formularios guardados (carpeta de instancias, base de datos de instancias)</string>\n  <string name=\"reset_blank_forms\">Formularios Vacíos (carpeta forms, base de datos forms, base de datos itemsets)</string>\n  <string name=\"reset_layers\">Capas cartográficas (carpeta de las capas)</string>\n  <string name=\"reset_cache\">Cache de formularios cargados (carpeta .cache)</string>\n  <string name=\"reset_settings_dialog_title\">Seleccione que restablecer</string>\n  <string name=\"reset_settings_button_reset\">Restablecer</string>\n  <string name=\"reset_in_progress\">Restableciendo</string>\n  <string name=\"reset_app_state_result\">Reestablecer resultados</string>\n  <string name=\"reset_app_state_warning\">Los datos seleccionados serán borrados permanentemente. No se puede deshacer.</string>\n  <string name=\"reset_settings_result\">Todos los ajustes :: %s</string>\n  <string name=\"reset_saved_forms_result\">Formularios guardados :: %s</string>\n  <string name=\"reset_blank_forms_result\">Formularios en blanco :: %s</string>\n  <string name=\"reset_layers_result\">Capas de mapa :: %s</string>\n  <string name=\"reset_cache_result\">Cache de carga de formularios :: %s</string>\n  <string name=\"main_menu_settings\">Menú principal de ajustes</string>\n  <string name=\"main_menu_settings_summary\">Mostrar u ocultar botones</string>\n  <string name=\"user_settings\">Ajustes de usuario</string>\n  <string name=\"user_setting_summary\">Mostrar u ocultar ajustes</string>\n  <string name=\"form_entry_setting\">Configuración de entrada de formularios</string>\n  <string name=\"form_entry_settings_summary\">Mostrar u ocultar acciones</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Retroceder en Preguntas</string>\n  <string name=\"moving_backwards_disabled_title\">Retroceder en Preguntas Desactivado</string>\n  <string name=\"moving_backwards_disabled_message\">Configure el dispositivo para prevenir que los usuarios se salten esta configuración?\\n\\nLos cambios son:\\n\\u2022 Desactivar Editar Borrador\\n\\u2022 Desactivar Guardar como Borrador\\n\\u2022 Desactivar Ir a Pregunta\\n\\u2022 Establecer Manejo de Restricciones como Validar al desplazar hacia adelante</string>\n  <string name=\"yes\">Si</string>\n  <string name=\"no\">No</string>\n  <string name=\"moving_backwards_enabled_title\">Retroceder en Preguntas Activado</string>\n  <string name=\"moving_backwards_enabled_message\">Se recomienda revisar la siguiente configuración:\\n\\n\\u2022 Editar Borrador\\n\\u2022 Guardar como Borrador\\n\\u2022 Ir a la Pregunta\\n\\u2022 Manejo de Restricciones</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Guardar como borrador</string>\n  <string name=\"save_mid_summary\">Icono Guardar en la barra superior y botón \\\"Guardar como borrador\\\" al salir del formulario</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Acerca de</string>\n  <string name=\"tell_your_friends\">Compartir ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">¿Sus colegas aún recogen datos en papel? Comparte con ellos la aplicación</string>\n  <string name=\"leave_a_review\">Ir a Play Store - Comentar y/o Actualizar</string>\n  <string name=\"leave_a_review_msg\">Su reseña (ojalá positiva) incrementa la visibilidad de la aplicación en Play Store</string>\n  <string name=\"odk_website\">Visitar el sitio web</string>\n  <string name=\"odk_website_summary\">ODK se utiliza para recopilar datos para el bien social en entornos desafiantes.</string>\n  <string name=\"odk_forum\">Unirse al foro de ODK</string>\n  <string name=\"odk_forum_summary\">Únase al foro para obtener soporte, solicitar mejoras, contribuir con el código, traducir!</string>\n  <string name=\"all_open_source_licenses\">Licencias/librerias de Código Abierto</string>\n  <string name=\"all_open_source_licenses_msg\">Sostenidos a hombros de gigantes!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">Reconfigurar con código QR</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">Reemplazar todos los ajustes existentes</string>\n  <string name=\"qr_code\">Configuración del Código QR</string>\n  <string name=\"scan_qr_code_fragment_title\">Escanear</string>\n  <string name=\"view_qr_code_fragment_title\">Código QR</string>\n  <string name=\"import_qrcode_sd\">Importar código QR</string>\n  <string name=\"successfully_imported_settings\">Ajustes importados exitosamente</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Se alcanzó el límite máximo de caracteres: No se puede generar código QR para todos los ajustes</string>\n  <string name=\"share\">Compartir</string>\n  <string name=\"turn_on_flashlight\">Encender flash</string>\n  <string name=\"turn_off_flashlight\">Apagar flash</string>\n  <string name=\"qrcode_with_both_passwords\">Contiene información sensible: <b>contraseña de administrador</b> y <b>servidor</b></string>\n  <string name=\"qrcode_without_passwords\">Código QR no contiene contraseñas de <b>administrador</b> o <b>servidor</b>. Pulse para configurar</string>\n  <string name=\"qrcode_with_admin_password\">Contiene información sensible: contraseña de <b>administrador</b> </string>\n  <string name=\"qrcode_with_server_password\">Contiene información sensible: contraseña de <b>servidor</b> </string>\n  <string name=\"server_password\">Contraseña del servidor</string>\n  <string name=\"include_password_dialog\">Contraseñas incluidas en el Código</string>\n  <string name=\"generate\">Generar</string>\n  <string name=\"invalid_qrcode\">El código QR no contiene ajustes válidos</string>\n  <string name=\"qr_code_not_found\">Código QR no encontrado en la imagen seleccionada</string>\n  <string name=\"corrupt_imported_preferences_error\">La configuración actual está dañada. Desde la configuración de gestión de proyectos, restablezca la configuración o importe las que funcionan.</string>\n  <string name=\"settings_with_gd_protocol\">Ya no se pueden crear proyectos de Google Drive/Sheets</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">De acuerdo</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Cancelar</string>\n  <string name=\"cancel_loading_form\">Cancelar</string>\n  <string name=\"cancel_location\">Cancelar</string>\n  <string name=\"do_not_change\">Cancelar</string>\n  <string name=\"canceling\">Cancelando</string>\n  <string name=\"notification_channel_name\">Notificaciones generales</string>\n  <string name=\"success\">Correcto</string>\n  <string name=\"error_occured\">Se ha producido un error</string>\n  <string name=\"please_wait\">Por favor, espere un momento.</string>\n  <string name=\"please_wait_long\">Por favor espere. Esto podria demorarse unos minutos.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">No puede borrar \\'%s\\'. Por favor, borre manualmente el archivo y recargue el formulario.</string>\n  <string name=\"fs_create_media_folder_error\">No pudo crear la carpeta de medios \\'%s\\'</string>\n  <string name=\"fs_file_copy_error\">No ha podido copiar \\'%1$s\\' sobre \\'%2$s\\'. Razón: %3$s</string>\n  <string name=\"select_odk_shortcut\">Seleccionar acceso directo</string>\n  <string name=\"background_audio_help\">Este formulario graba el audio del micrófono de su dispositivo.\\n\\nY puede usar el indicador de volumen para asegurarse de que el micrófono esté lo suficientemente cerca del sonido que necesita grabar.\\n\\nPara detener la grabación, salga de este formulario.\\n\\nPara obtener más información, comuníquese con la persona que le pidió que recopilara datos.</string>\n  <string name=\"loading\">Cargando…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Proyectos</string>\n  <string name=\"settings\">Ajustes</string>\n  <string name=\"add_project\">Agregar proyecto</string>\n  <string name=\"add\">Agregar</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Proyecto duplicado</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">Ya tienes un proyecto con estas configuraciones de conexión. ¿Quieres cambiar al proyecto existente o agregar uno nuevo?</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Agregar proyecto duplicado</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Cambiar a proyecto existente</string>\n  <string name=\"project_name\">Nombre del proyecto</string>\n  <string name=\"project_icon\">Icono del proyecto</string>\n  <string name=\"project_color\">Color del proyecto</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Cambiado a %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">Usando %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Cambiar a %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <string name=\"delete_project_dialog_title\">Eliminar %s</string>\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <string name=\"delete_project_message\">Al eliminar este proyecto, se eliminarán de forma permanente los siguientes elementos:</string>\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <string name=\"delete_project_instructions\">Escriba %1$s para confirmar esta acción.</string>\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <string name=\"delete_trigger\">eliminar</string>\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <string name=\"form_definitions_count\">Formularios en blanco: %1$d</string>\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <string name=\"sent_count\">Enviados: %1$d</string>\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <string name=\"unsent_count\">No enviados: %1$d</string>\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <string name=\"drafts_count\">Borradores: %1$d</string>\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"delete_project_confirm_button_text\">Eliminar proyecto</string>\n  <string name=\"or\">O</string>\n  <string name=\"hex_color\">Color en Hex</string>\n  <string name=\"invalid_hex_code\">Código hexadecimal no válido</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Capture datos\\nen cualquier lugar</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Configure mediante código QR</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Ingrese manualmente los detalles del proyecto</string>\n  <string name=\"project_config_tip\">Después de agregar su proyecto, puede configurarlo en Configuración</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">¿Aún no tienes un proyecto?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Prueba una demostración</string>\n  <string name=\"scan_configuration_qr_code\">Escanear el código QR de configuración</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Mostrar detalles</string>\n  <string name=\"errors\">Errores</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <string name=\"no_browser_installed\">¡No hay ningún browser instalado!</string>\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">Para abrir este formulario, primero debe abrir la aplicación m´vil y cambiar al proyecto que lo contiene.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">No se ha configurado la aplicación móvil. Intente abrir la aplicación y configurarla.\\n\\nIfSi tocó un acceso directo, es posible que deba volver a crearlo después de configurarla.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">URI no reconocido: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">URI erroneo: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Formulario lleno ha sido eliminado!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">No se pudo editar este borrador por que el formulario en blanco que le corresponde no esta o fue borrado.\\n\\nID de Formulario: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Ha descargado dos formularios distintos con el mismo ID y versión. Puede que los cargara en diferentes momentos o de distintas procedencias. Debe de borrar uno.\n</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <string name=\"cannot_open_form_because_of_forms_update\">Este formulario no puede abrirse en este momento porque se están actualizando los formularios. Vuelva a intentarlo en breve.</string>\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Abrir Configuración</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <string name=\"entities_title\">Listas de entidades</string>\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <string name=\"entity_browser_button\">Ver listas de entidades</string>\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">Desconectado</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">Este formulario no es compatible con esta versión de ODK Collect. Por favor, actualice Collect. Si sigues teniendo este problema, informa a la persona que te pidió que recopilaras los datos.\\n\\nForm Entities spec version: %s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">¡Hubo un colapso la última vez que se ejecutó la aplicación!</string>\n  <string name=\"cant_start_app\">¡No se puede iniciar la aplicación!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">Este proyecto estaba previamente conectado a una cuenta de Google Drive. Se ha eliminado la compatibilidad con Google Drive. Puede configurar un servidor o conectar a una computadora.</string>\n  <string name=\"learn_more_button_text\">Aprende más</string>\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">Herramientas de desarrollo</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">Aplicación bloqueada</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Se forzó una excepción no detectada que hace que la aplicación se bloquee</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <string name=\"debug_filters\">Filtros de depuración</string>\n  <string name=\"debug_filters_summary\">Mostrar información de depuración para las ejecuciones de expresiones de filtro durante la entrada del formulario</string>\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"use_zxing_for_barcode_scanning\">Volver al escáner de códigos de barras anterior</string>\n  <string name=\"permission_dialog_title\">Acerca de los permisos</string>\n  <string name=\"permission_dialog_text\">Se le pedirá que permita el acceso de ODK Collect a las siguientes funciones, seleccione \\\"permitir\\\" si desea usarlas.</string>\n  <string name=\"notifications\">Notificaciones</string>\n  <string name=\"required_for_notifications\">Requerido para mostrar actualizaciones cuando los formularios se descargan, actualizan y envían.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">Su formulario fue guardado como borrador.</string>\n  <string name=\"form_saved\">Su formulario fue guardado.</string>\n  <string name=\"form_sending\">Enviando el formulario…</string>\n  <string name=\"edit_form\">Editar</string>\n  <string name=\"view_form\">Mirar</string>\n  <string name=\"close_snackbar\">Cerrar notificación</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">Último formulario enviado: hace %d segundo</item>\n    <item quantity=\"many\">Último formulario enviado: hace %d segundos</item>\n    <item quantity=\"other\">Último formulario enviado: hace %d segundos</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"one\">Último formulario enviado: hace %d minuto</item>\n    <item quantity=\"many\">Último formulario enviado: hace %d minutos</item>\n    <item quantity=\"other\">Último formulario enviado: hace %d minuto</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"one\">Último formulario enviado: hace %d hora</item>\n    <item quantity=\"many\">Último formulario enviado: hace %d horas</item>\n    <item quantity=\"other\">Último formulario enviado: hace %d horas</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"one\">Último formulario enviado: hace %d día</item>\n    <item quantity=\"many\">Último formulario enviado: hace %d días</item>\n    <item quantity=\"other\">Último formulario enviado: hace %d días</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"one\">%d formulario listo para enviar</item>\n    <item quantity=\"many\">%d formularios listos para enviar</item>\n    <item quantity=\"other\">%dformularios listos para enviar</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">En versiones posteriores, los formularios finalizados ya no serán editables. Guarde los formularios como borradores para editarlos más tarde.\\n\\nPuede comprobar si hay errores en un borrador de formulario tocando los tres puntos (⋮) y luego Comprobar si hay errores.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">En versiones posteriores, los formularios finalizados ya no serán editables.</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">Finalizar todos los borradores</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"one\">¿Quiere finalizar %d borrador?</item>\n    <item quantity=\"many\">¿Quiere finalizar %d borradores?</item>\n    <item quantity=\"other\">¿Quiere finalizar %d borradores?</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">Una vez que finalice todos los borradores, estarán en \\\"Listo para enviar\\\" y no podrá realizar modificaciones. Los borradores que tengan errores no se finalizarán.\\n\\nNo podrás deshacer esta acción.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"one\">¡Bien! %d borrador finalizado.</item>\n    <item quantity=\"many\">¡Bien! %d borradores finalizados.</item>\n    <item quantity=\"other\">¡Bien! %d borradores finalizados.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"one\"> %d borrador tiene errores, debe corregirlo antes de finalizar.</item>\n    <item quantity=\"many\">%d borradores tiene errores, debe corregirlos antes de finalizar.</item>\n    <item quantity=\"other\">%d borradores tiene errores, debe corregirlo antes de finalizar.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d  borradores finalizados. %d borradores tienen errores que deben corregirse antes de finalizarlos.</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d borradores finalizados. Los borradores que quedan deben finalizarse manualmente.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Errores</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">Sin errores</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">Desmarque para ocultarse de los borradores</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">No hay nada disponible para mostrar.</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">No hay formularios en blanco</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">Descargue el formulario para comenzar</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">Nada en borradores</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">Cuando guardes como borrador, los formularios aparecerán aquí</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">Nada listo para enviar</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">Cuando finalices los borradores, aparecerán aquí.</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">No hay nada enviado</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">Cuando envíe formularios finalizados, aparecerán aquí</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">No hay formularios para descargar</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">Descargue el formulario para comenzar</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">no hay formularios para eliminar</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">Cuando descargue formularios en blanco, aparecerán aquí</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">Cuando tenga formularios guardados, aparecerán aquí</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">¿Recuperar tu trabajo?</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"savepoint_recovery_dialog_message\">\\'Recolección cerrada inesperadamente el \\'EEE, MMM dd, yyyy \\'a las\\' HH:mm\\' y  se guardó su trabajo. \\n\\n¿Quiere recuperarlo o descartarlo?\\'</string>\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">Recuperar</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Descartar</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">Obtener punto</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">Ver o cambiar punto</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">Ver punto</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">Cambiar punto</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">Obtener línea</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">Ver o cambiar línea</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">Ver línea</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">Obtener polígono </string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">Ver o cambiar polígono</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">Ver polígono</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <string name=\"settings_json\">Configuración JSON</string>\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <string name=\"preparing_form_edit\">Preparando el formulario para editarlo…</string>\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <string name=\"user_visible_instance_name\">%s (Editar %d)</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_title\">¿Continuar editando el borrador?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_draft_edit_found_dialog_message\">\\'Se guardó un borrador de edición el\\' EEE, MMM dd, aaaa \\'a las\\' HH:mm\\'\\n\\n¿Desea reanudar esta edición?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_positive_button\">Seguir editando</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_title\">¿Editar la última versión?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_finalized_edit_found_dialog_message\">\\'Este envío fue editado previamente.\\n\\n¿Desea editar la última versión finalizada el \\'EEE, MMM dd, yyyy\\' a las \\'HH:mm\\'?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_positive_button\">Editar último formulario</string>\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n  <string name=\"finalized_form_edit_started\">Estás editando este formulario. Pulsa una pregunta para empezar a editarla.</string>\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-es-rSV/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources>\n  <string name=\"cannot_edit_completed_form\">Este formulario no podrá ser editado una vez que se haya marcado como completado. Este puede ser encriptado.</string>\n  <string name=\"capture_video\">Grabar video</string>\n  <string name=\"password\">Contraseña</string>\n  <!--http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Geo string added in feature-geofeatures branch-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-et/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">Versoon: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Lisatud\\' EEE, MMM dd, yyyy \\'kell\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Salvestatud\\' EEE, MMM dd, yyyy \\'kell\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Lõpetatud\\' EEE, MMM dd, yyyy \\'kell\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Saadetud\\' EEE, MMM dd, yyyy \\'kell\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Saatmine ebaõnnestus\\' EEE, MMM dd, yyyy \\'kell\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Kustutatud\\' EEE, MMM dd, yyyy \\' \\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Muudetud\\' EEE, MMM dd, yyyy \\'kell\\' HH:mm</string>\n  <string name=\"encrypted_form\">Krüptitud vorm</string>\n  <string name=\"deleted_form\">Kustutatud vorm</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Skanneerimine lõpetatud. Kõik vormid on laaditud.</string>\n  <string name=\"loading_form\">Vormide laadimine</string>\n  <string name=\"survey_loading_reading_form_message\">Vormi definitsooni lugemine…</string>\n  <string name=\"survey_loading_reading_data_message\">Uuringu andmete lugemine…</string>\n  <string name=\"survey_loading_reading_csv_message\">CSV failide lugemine…</string>\n  <string name=\"parse_error\">Kahjuks ei ole vormi töötlemine võimalik.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Salvestamata muudatused on taastatud!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Muuda keelt</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Liigu küsimusele</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Liigu algusesse</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Liigu lõppu</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Liigu üles</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"exit\">Välju</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Muuda küsimust</string>\n  <string name=\"clearanswer_confirm\">Eemalda vastus \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Eemalda vastus</string>\n  <string name=\"clear_answer_ask\">Eemalda see vastus?</string>\n  <string name=\"clear_answer_no\">Tühista</string>\n  <string name=\"discard_answer\">Eemalda vastus</string>\n  <string name=\"discard_group\">Eemalda grupp</string>\n  <string name=\"delete_repeat\">Eemalda grupp</string>\n  <string name=\"delete_repeat_ask\">Eemalda see grupp?</string>\n  <string name=\"delete_repeat_confirm\">Eemalda grupp \\\"%s\\\" ja kõik tema alamgrupid?</string>\n  <string name=\"delete_repeat_no\">Tühista</string>\n  <string name=\"invalid_answer_error\">Kahjuks on antud vastus ei sobimatu!</string>\n  <string name=\"required_answer_error\">Vabandage, vastus on kohustuslik!</string>\n  <string name=\"survey_internal_error\">Rakenduse sisene viga: liikumine erkaanile/valjale ebaõnnestus.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Olete %s lõpus</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Kahjuks ebaõnnestus vormi salvestamine!</string>\n  <string name=\"data_saved_ok\">Vorm on edukalt salvestatud!</string>\n  <string name=\"save_point_error\">Vea parndamise taastuspunkt %s </string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Muudatuste salvestamine</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Vormi salvestamine</string>\n  <string name=\"survey_saving_validating_message\">Vastuste valideerimine…</string>\n  <string name=\"survey_saving_collecting_message\">Andmete kogumine…</string>\n  <string name=\"survey_saving_saving_message\">SD kaardile salvestamine…</string>\n  <string name=\"survey_saving_finalizing_message\">SD kaardi tegevuste lõpetamine…</string>\n  <string name=\"survey_saving_encrypting_message\">Andmete krüpteerimine</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Ei leidu täpselt üks kirje!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Ei leidu täpselt ühte tühja vormi, mis vastaks jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Vormi ID määramata?</string>\n  <string name=\"phone_does_not_support_rsa\">Telefon ei toeta RSA krüpteerimist.</string>\n  <string name=\"invalid_rsa_public_key\">RSA avalik võti on sobimatu.</string>\n  <string name=\"encryption_error_message\">%s vorm ei ole salvestatud ja lõpetatud.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Alusta</string>\n  <string name=\"activity_not_found\">%s teostamiseks ei leidu toimingut</string>\n  <string name=\"no_app\">Soovitud rakendust ei ole. Palun sisestage käsitsi.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Ava veebiaadress</string>\n  <string name=\"capture_osm\">Ava OpenMapKit</string>\n  <string name=\"recapture_osm\">Korda OSM sildistamist</string>\n  <string name=\"edited_osm_file\">Muudetud OSM XML fail:</string>\n  <string name=\"invalid_osm_data\">Midagi läks valesti. Me ei saanud korrektseid OSM andmeid.</string>\n  <string name=\"alert\">Alarm</string>\n  <string name=\"install_openmapkit\">Palun paigaldage OpenMapKit!</string>\n  <string name=\"get_barcode\">Skaneeri triipkood</string>\n  <string name=\"replace_barcode\">Asenda triipkood</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Pildista</string>\n  <string name=\"choose_image\">Vali pilt</string>\n  <string name=\"selected_invalid_image\">Valitud fail ei ole pilt</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Salvesta ja sulge</string>\n  <string name=\"sign_button\">Allkirja kogumine</string>\n  <string name=\"markup_image\">Markup pilt</string>\n  <string name=\"draw_image\">Joonista pilt</string>\n  <string name=\"reset_image\">Lähtesta</string>\n  <string name=\"set_color\">Värvi määramine</string>\n  <string name=\"capture_video\">Video salvestamine</string>\n  <string name=\"choose_video\">Vali video</string>\n  <string name=\"play_video\">Mängi videot</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Heli salvestamine</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Vali heli</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">Meedia %s lisamisel esines viga </string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Vali vastus</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Valitud:</string>\n  <string name=\"trigger\">Ok. Palun jätkake.</string>\n  <string name=\"ext_file_no_data_error\">Fail on tühi!</string>\n  <string name=\"ext_conflicting_columns_error\">%s tulbad sobivad!</string>\n  <string name=\"ext_sortBy_numeric_error\">Veerg peaks sisaldama ainult numbrilisi väärtusi. Konfliktis on väärtused: \\'%s\\'</string>\n  <string name=\"ext_search_wrong_arguments_error\">Süntaksi viga funktsoonis search(): funktsoon vajab 1, 4 või 6 argumenti.</string>\n  <string name=\"ext_search_wrong_function_error\">Süntaksi viga funktsoonis search(): tundmatu funktsoon \\'%s\\'</string>\n  <string name=\"ext_search_bad_function_error\">Süntaksi viga funktsoonis search(): \\'%s\\' ei oma õiget funktsoon vormigut.</string>\n  <string name=\"ext_search_return_error\">Otsing tagastas \\'%s\\' tüüpi objekti</string>\n  <string name=\"ext_import_generic_error\">Import %1$s ebaõnnestus. Põhjus %2$s</string>\n  <string name=\"ext_import_progress_message\">Vormi eellaadimine \\'%1$s\\', palun oodake… %2$s</string>\n  <string name=\"ext_import_finalizing_message\">Lõpetan andmete eellaadimist…</string>\n  <string name=\"ext_import_completed_message\">Andmete lugemine sai valmis!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager ei ole algseadistatud.</string>\n  <string name=\"ext_import_csv_missing_error\">Väiline andmefail %1$s ei ole imporditud. Äkki unustasite vormile lisada %2$s.csv faili?</string>\n  <string name=\"ext_search_generic_error\">Süntaksi viga funktsioonis search(): %s</string>\n  <string name=\"ext_assign_value_error\">\\'%s\\' ebaõnnestus väärtuse määramine.</string>\n  <string name=\"parser_exception\">XPathParser Viga: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Salvesta suund</string>\n  <string name=\"replace_bearing\">Asenda suund</string>\n  <string name=\"getting_bearing\">Laen suunda</string>\n  <string name=\"accept_bearing\">Salvesta suund</string>\n  <string name=\"bearing\">Suund: %.3f</string>\n  <string name=\"direction\">Siht: %s</string>\n  <string name=\"select_date\">Vali kuupäev</string>\n  <string name=\"no_date_selected\">Kuupäev valimata</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Fail %s on sobimatu.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Fail %s on puudu.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"streets\">Tänavad</string>\n  <string name=\"terrain\">Maastik</string>\n  <string name=\"hybrid\">Hübriid</string>\n  <string name=\"satellite\">Satelliit</string>\n  <string name=\"google_play_services_error_occured\">Puudub ligipääs Google Maps-ile. Veenduge, et Google Play teenused on paigaldatud.</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Kahjuks on asukoha küsimine maha keeratud!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">W %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"start\">Alusta</string>\n  <string name=\"enable_gps\">Luba GPS</string>\n  <string name=\"geo_clear_warning\">Kujutis on juba loodud. Kas soovite kujutise eemaldada?</string>\n  <string name=\"geo_exit_warning\">Loobu muudatustest ja ava ODK?</string>\n  <string name=\"polygon_validator\">Polügooni loomiseks on vaja vähemalt 3 punkti</string>\n  <string name=\"clear\">Eemalda</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"geopoint_no_draggable_instruction\">Puuduta lisa marke nuppu.</string>\n  <string name=\"discard\">Loobu</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Saadan vormi</string>\n  <string name=\"noselect_error\">Kahjuks ei ole te ühtegi vormi valinud!</string>\n  <string name=\"no_connection\">Võrguõhendus puudub</string>\n  <string name=\"sending_items\">%1$s / %2$s vormist saadetud</string>\n  <string name=\"send_selected_data\">Saada valitud</string>\n  <string name=\"server_requires_auth\">Server nõuab autentimist</string>\n  <string name=\"no_forms_uploaded\">Vabandage, ühtegi vormi ei ole üles laetud.</string>\n  <string name=\"upload_results\">Tulemuste üles laadimine</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Muuda vaadet</string>\n  <string name=\"show_sent_and_unsent_forms\">Näita saadetud ja saatmata vorme</string>\n  <string name=\"show_unsent_forms\">Näita </string>\n  <string name=\"send_in_progress\">Taustal jookseb andmete saatmine. Palun proovige varsti uuesti</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Värskenda</string>\n  <string name=\"download\">Lae alla</string>\n  <string name=\"downloading_data\">Serveriga ühendumine</string>\n  <string name=\"fetching_file\">Laen alla %1$s.\\n\\nVormi(id) (%2$s/%3$s).</string>\n  <string name=\"form_download_progress\">%1$s. Laeb faile: %2$s of %3$s</string>\n  <string name=\"version\">Versioon:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Salvestatud vormid</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Sisestusvormid</string>\n  <string name=\"delete_confirm\">Kustuta vorm(id) %s?</string>\n  <string name=\"delete_file\">Kustuta valitu</string>\n  <string name=\"delete_no\">Ei kustuta</string>\n  <string name=\"delete_yes\">Kustuta vormid</string>\n  <string name=\"file_deleted_ok\">%s vorm(id) edukalt kustutatud!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Serveri seaded</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">Veebiaadress</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Serveri veebiaadress</string>\n  <string name=\"url_error\">Vabandage, vale veebiaadress!</string>\n  <string name=\"username\">Kasutajanimi</string>\n  <string name=\"change_username\">ODK kasutajanimi</string>\n  <string name=\"password\">Salasõna</string>\n  <string name=\"change_password\">ODK salasõna</string>\n  <string name=\"password_error_whitespace\">Salasõna alguses või lõpus ei ole lubatud kasutada tühikut</string>\n  <string name=\"username_error_whitespace\">Kasutajanime alguses või lõpus ei ole lubatud kasutada tühikut</string>\n  <string name=\"font_size\">Teksti suurus</string>\n  <string name=\"change_font_size\">Teksti suurus</string>\n  <string name=\"font_size_extra_large\">Väga suur</string>\n  <string name=\"font_size_large\">Suur</string>\n  <string name=\"font_size_medium\">Keskmine</string>\n  <string name=\"font_size_small\">Väike</string>\n  <string name=\"font_size_extra_small\">Väga väike</string>\n  <string name=\"navigation\">Rakenduses liikumine</string>\n  <string name=\"swipe_navigation\">Liikumiseks kasuta libistamist (swipe) käeliigutust</string>\n  <string name=\"buttons_navigation\">Liikumiseks kasuta edasi-tagasi nuppe</string>\n  <string name=\"swipe_buttons_navigation\">Liikumiseks kasuta mõlemat libistamist (swipe) ja nuppe </string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Järgmine</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"delete_after_send\">Kustuta peale saatmist</string>\n  <string name=\"delete_after_send_summary\">Peale serverisse saatmist kustutab lõpetatud vormid ja meedia</string>\n  <string name=\"constraint_behavior\">Piira vormi töötlemise käitumist</string>\n  <string name=\"constraint_behavior_on_swipe\">Valideeri vormis edasi liikudes</string>\n  <string name=\"constraint_behavior_on_finalize\">Vormi valideerimine toimub sisestuse lõpetamisel</string>\n  <string name=\"high_resolution_summary\">Kõrge resolutiooniga videosalvestuse võimaldamine</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"analytics_preferences\">Kasutusinfo</string>\n  <string name=\"analytics\">Anonüümne kasutusinfo kogumine</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Sisesta uus salasõna</string>\n  <string name=\"admin_password\">Administraatori salasõna</string>\n  <string name=\"admin_password_changed\">Administraatori salasõna muutmine õnnestus</string>\n  <string name=\"admin_password_disabled\">Administraatori salasõna on keelatud</string>\n  <string name=\"admin_password_incorrect\">Vabandust, salasõna on vale!</string>\n  <string name=\"enter_admin_password\">Sisestage administraatori salasõna</string>\n  <string name=\"change_admin_password\">Muuda administraatori salasõna</string>\n  <string name=\"found_in_main\">Peamenüüst eemaldamiseks puhasta väli</string>\n  <string name=\"reset_project_settings_title\">Lähtesta</string>\n  <string name=\"select_all\">Vali kõik</string>\n  <string name=\"clear_all\">Eemalda kõik</string>\n  <string name=\"reset_settings\">Kõik seaded (sisemised seaded, salvestatud seaded)</string>\n  <string name=\"reset_saved_forms\">Salvestatud vormid (kirjete kaust, kirjete andmebaas)</string>\n  <string name=\"reset_blank_forms\">Sisestusvormid (vormide kaust, vormide andmebaas, andmekogud)</string>\n  <string name=\"reset_layers\">Kaardikihid (kihtide kaust)</string>\n  <string name=\"reset_cache\">Vormi laadimise vahemälu (.cache kaust)</string>\n  <string name=\"reset_settings_dialog_title\">Vali mida lähtestada</string>\n  <string name=\"reset_settings_button_reset\">Lähtesta</string>\n  <string name=\"reset_in_progress\">Lähtestamine…</string>\n  <string name=\"reset_app_state_result\">Lähtesta tulemused</string>\n  <string name=\"reset_app_state_warning\">Kogu valitud andmestik kustutatakse jäädavalt. </string>\n  <string name=\"reset_settings_result\">Kõik seaded :: %s</string>\n  <string name=\"reset_saved_forms_result\">Salvestatud vormid :: %s</string>\n  <string name=\"reset_blank_forms_result\">Tühjad vormid :: %s</string>\n  <string name=\"reset_layers_result\">Kaardikihid :: %s</string>\n  <string name=\"reset_cache_result\">Vormi laadimise vahemälu :: %s</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Teave</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Tühista</string>\n  <string name=\"cancel_loading_form\">Tühista</string>\n  <string name=\"cancel_location\">Tühista</string>\n  <string name=\"do_not_change\">Tühista</string>\n  <string name=\"success\">Õnnestus</string>\n  <string name=\"error_occured\">Esines viga</string>\n  <string name=\"please_wait\">Palun oodake.</string>\n  <string name=\"please_wait_long\">Palun oodake. See võib võtta mõne minuti.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">\\'%s\\' kustutamine ebaõnnestus. Palun kusutage fail käsitsi ja laadige vorm uuesti alla.</string>\n  <string name=\"fs_create_media_folder_error\">\\'%s\\' meediakausta loomine ebaõnnestus.</string>\n  <string name=\"fs_file_copy_error\">\\'%1$s\\' kopeerimine \\'%2$s\\' ebaõnnestus. Põhjuseks: %3$s</string>\n  <string name=\"select_odk_shortcut\">Vali ODK otsetee</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Tundmatu veebiaadress: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Vale veebiaadress: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Olete alla laadinud kaks eri vormi millel on sama ID ja versioon. Võis juhtuda, et sama vorm oli üles laetud erinevatel aegadel või erinevatesse serveritesse. Igatahes peaksite ühe neist eemaldama.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Loobu</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-fa/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">شروع با فرم جدید</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">پیش نویس</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">آماده حهت ارسال</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">ارسال شد</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">دانلود فرم</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">حذف فرم</string>\n  <string name=\"saved_forms\">ذخیره فvم ها</string>\n  <string name=\"version_number\">نسخه: %s</string>\n  <string name=\"id_number\">شماره شناسایی: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">اضافه شد EEE,MMM,dd,yyyy در HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\' بارگذاری \\' EEE, MMM dd, yyyy \\'at\\' HH:mm\n </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">ذخیره شد EEE,MMM,dd,yyyy در HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">نهایی شد EEE,MMM,dd,yyyy در HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">ارسال شده EEE,MMM,dd,yyyy در HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'ارسال ناموفق بود\\' EEE, MMM dd, yyyy\\'در\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">حذف شده EEE, MMM dd, yyyy در  HH:mm</string>\n  <string name=\"submission_deleted\">ارسال حذف شد</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">اصلاح شد EEE,MMM,dd,yyyy در HH:mm</string>\n  <string name=\"encrypted_form\">فرم رمز گذاری شده</string>\n  <string name=\"deleted_form\">حذف فرم</string>\n  <string name=\"sort_the_list\">لیست را مرتب کنید</string>\n  <string name=\"sort_by\">دسته بندی بر اساس</string>\n  <string name=\"sort_by_name_asc\">اسم، الف-ی</string>\n  <string name=\"sort_by_name_desc\">اسم ی-الف</string>\n  <string name=\"sort_by_date_desc\">تاریخ،  جدیدترین در اولویت</string>\n  <string name=\"sort_by_date_asc\">تاریخ،  قدیمی ترها در اولویت</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">فیلتر لیست</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">جستجو</string>\n  <string name=\"not_granted_permission\">فعالیتی که شما میخواهید آنرا انجام دهید به مجوزی ضرورت دارد که به شما اعطا نشده است. </string>\n  <string name=\"storage_runtime_permission_denied_title\">مجوزهای برای ذخیره سازی</string>\n  <string name=\"storage_runtime_permission_denied_desc\">بدون مجوزها، مجموعه نمی‌تواند به فرم‌های شما دسترسی داشته باشد یا پاسخ‌ها را ذخیره کند. زمانی که برای اعطای برنامه آماده شدید، دوباره آن را باز کنید.</string>\n  <string name=\"location_runtime_permissions_denied_title\">دسترسی لوکیشن</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">بدون اجازه <b>Precise Location</b> نمی تواند موقعیت مکانی شما را ضبط کند. لطفاً هنگامی که  اجازه دسترسی آن فعال شد، دوباره امتحان کنید.</string>\n  <string name=\"camera_runtime_permission_denied_title\">دسترسی دوربین</string>\n  <string name=\"camera_runtime_permission_denied_desc\">بدون این مجوز Collect نمی تواند به دوربین دسترسی داشته باشد. لطفاً هنگامی که آماده اعطای آن هستید دوباره امتحان کنید.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">دسترسی ضبط صدا</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">بدون این مجوز، Collect نمی تواند به میکروفون دسترسی داشته باشد. لطفاً هنگامی که آماده اعطای آن هستید دوباره امتحان کنید.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">نقشه فرم را باز کنید</string>\n  <string name=\"instance_scan_count\">%1$dفرم ها اضافه شدند.</string>\n  <string name=\"finished_disk_scan\">اسکن به پایان رسید. همه فرم ها بارگذاری شدند.</string>\n  <string name=\"xform_parse_error\">خطای تجزیه XForm برای %1$s: %2$s وجود ندارد یا نامعتبر است.</string>\n  <string name=\"loading_form\">در حال آماده سازی فرم</string>\n  <string name=\"survey_loading_reading_form_message\">خواندن تعریف فرم…</string>\n  <string name=\"survey_loading_reading_data_message\">خواندن داده های نظر سنجی </string>\n  <string name=\"survey_loading_reading_csv_message\">خواندن اسناد CSV</string>\n  <string name=\"parse_error\">با عرض پوزش، نمی توان فرم را تجزیه کرد.</string>\n  <string name=\"loading_form_failed\">هنگام  آماده سازی فرم خطایی روی داد. لطفا دوباره تلاش کنید.</string>\n  <string name=\"cannot_edit_completed_form\">این فرم بعد از نهایی شدن آن قابل ویرایش نیست. ممکن است رمزگذاری شده باشد. </string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">تغییرات ذخیره نشده از savepoint بازیابی شد!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">تغییر زبان</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">به Prompt بروید</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">ذخیره پیش نویس</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">رفتن به ابتدا</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">رفتن به انتها</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">بالا برو</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">گروه</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">گروه تکرار شونده</string>\n  <string name=\"exit\">خروج</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">\\\"%s\\\" اضافه شود؟</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">اضافه کردن</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">اضافه کردن جدید</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">ویرایش سریع</string>\n  <string name=\"clearanswer_confirm\">آیا برای \\\"%s\\\" پاسخ حذف شود؟</string>\n  <string name=\"clear_answer\">حذف پاسخ</string>\n  <string name=\"clear_answer_ask\">آیا این پاسخ حذف شود؟</string>\n  <string name=\"clear_answer_no\">لغو</string>\n  <string name=\"discard_answer\">حذف پاسخ</string>\n  <string name=\"discard_group\">حذف گروه</string>\n  <string name=\"delete_repeat\">حذف گروه</string>\n  <string name=\"delete_repeat_ask\">این گروه را حذف کنید؟</string>\n  <string name=\"delete_repeat_confirm\">حذف گروه \\\"%s\\\" با تمام زیر گروپ های آن</string>\n  <string name=\"delete_repeat_no\">انصراف</string>\n  <string name=\"invalid_answer_error\">متاسفانه، این پاسخ غیر معتبر است.</string>\n  <string name=\"required_answer_error\">ببخشید، این پاسخ الزامی است!</string>\n  <string name=\"success_form_validation\">موفق! هیچ خطایی در فرم شما یافت نشد.</string>\n  <string name=\"invalid_space_in_answer_singular\">هشدار: مقدار اساسی %s دارای فاصله است</string>\n  <string name=\"invalid_space_in_answer_plural\">هشدار: مقادیر اساسی %s دارای فاصله هستند</string>\n  <string name=\"too_complex_form\">این فرم برای این دستگاه بسیار پیچیده است. سعی کنید برای ساده سازی فرم  یا اصطلاحات کمک بخواهید.</string>\n  <string name=\"survey_internal_error\">خطای داخلی: گام به اعلان ناموفق بود.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">شما در پایان %sقرار دارید.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">ذخیره پیش نویس</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">نهایی</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">ارسال شد</string>\n  <string name=\"data_saved_error\">متاسفانه فرم ذخیره نشد!</string>\n  <string name=\"data_saved_ok\">فرم با موفقیت ذخیره شد!</string>\n  <string name=\"save_point_error\">خطا در ذخیره نقطه یابی: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">نادیده گرفتن فرم</string>\n  <string name=\"discard_changes\">نادیده گرفتن تغییرات</string>\n  <string name=\"keep_changes\">ذخیره تغییرات</string>\n  <string name=\"keep_editing\">به ویرایش ادامه دهید</string>\n  <string name=\"quit_form_title\">ذخبره فرم؟</string>\n  <string name=\"save_explanation\">شما می توانید این فرم را ذخیره کرده و در هر زمان که بخواهید از پیش نویس های خود به آن دسترسی داشته باشید.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\\"این فرم آخرین بار در\\\" EEE، MMM dd، yyyy \\\"در\\\" HH:mm ذخیره شده است. ذخیره به عنوان پیش نویس برای حفظ تغییرات.</string>\n  <string name=\"quit_form_continue_title\">به ویرایش ادامه دهید؟</string>\n  <string name=\"discard_form_warning\">اگر فرم را نادیده بگیرید، تغییراتی را که تاکنون انجام داده اید از دست خواهید داد.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">در حال ذخیره فرم</string>\n  <string name=\"survey_saving_validating_message\">در حال اعتبار سنجی پاسخ ها….</string>\n  <string name=\"survey_saving_collecting_message\">در حال جمع آوری داده ها…</string>\n  <string name=\"survey_saving_saving_message\">در حال ذخیره در کارت حافظه…</string>\n  <string name=\"survey_saving_finalizing_message\">در حال نهایی کردن کارت حافظه</string>\n  <string name=\"survey_saving_encrypting_message\">رمزگذاری داده ها…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">نه دقیقاً یک رکورد برای این مثال!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">دقیقاً یک فرم خالی با این jr_form_id مطابقت ندارد.</string>\n  <string name=\"no_form_id_specified\">FormId مشخص نشده؟؟؟</string>\n  <string name=\"phone_does_not_support_rsa\">تلفن از رمزگذاری RSA پشتیبانی نمی کند.</string>\n  <string name=\"invalid_rsa_public_key\">کلید عمومی RSA نامعتبر است.</string>\n  <string name=\"encryption_error_message\">فورم %s به عنوان نهایی ذخیره نشده است.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">ضبط شروع نشد.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">میکروفون در حال حاضر در حال استفاده است.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">رسانه را نمی توان به فورم پیوست کرد اما در %1$s موجود است</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">ضبط غیرفعال شد در %s فعال کنید</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">شکست در تلاش برای به روزرسانی مقادیر\\\" این عمومآ به دلیل استفاده نادرست از حسابات در طرح فورم می باشد. اگر این مشگل به شکل دوامدار مواجه شدید: پس این موضوع را شخص که از شما خواسته تا معلومات را جمع آوری کنید به اشتراک بگدازید.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">راه اندازی</string>\n  <string name=\"activity_not_found\">برای انجام %s هیچ فعالیتی دریافت نشد:</string>\n  <string name=\"no_app\"> درخواستی برای فورمه موجود نیست. لطفاً خواندن را به صورت دستی وارد کنید.</string>\n  <string name=\"null_intent_value\">درخواست نامه خارجی اطلاعات مورد انتظار را ارائه نکرد.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">باز نمودن آدرس انترنتی</string>\n  <string name=\"capture_osm\">راه اندازی ابزار نقشه</string>\n  <string name=\"recapture_osm\">برچسپ گذاری مجدد OSM</string>\n  <string name=\"edited_osm_file\">فایل OSM XML ویرایش شده</string>\n  <string name=\"invalid_osm_data\">مشکلی پیش آمد. ما داده OSM معتبر دریافت نکردیم.</string>\n  <string name=\"alert\">هشدار</string>\n  <string name=\"install_openmapkit\">لطفاْ OpenMapKit را نصب کنید!</string>\n  <string name=\"get_barcode\">دریافت بارکد</string>\n  <string name=\"replace_barcode\">جاگزینی بارکود</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">بارکد را داخل مستطیل قرار دهید</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">فایل را باز کنید</string>\n  <string name=\"rank_items\">رده بندی اقلام</string>\n  <string name=\"number_picker_title\">شماره جمع کننده</string>\n  <string name=\"invalid_range_widget\">این ویجت دارای پارامترهای نامعتبر است!</string>\n  <string name=\"svg_file_does_not_exist\">فایل SVG موجود نیست!</string>\n  <string name=\"capture_image\">گرفتن عکس</string>\n  <string name=\"choose_image\">انتخاب تصویر</string>\n  <string name=\"selected_invalid_image\">فایل انتخاب شده تصویر قابل اعتبار ندارد</string>\n  <string name=\"take_picture_instruction\">برای گرفتن عکس روی صفحه ضربه بزنید</string>\n  <string name=\"error_front_camera_unavailable\">این دستگاه دوربین جلو موجود نیست</string>\n  <string name=\"camera_error\">هنگام عکس گرفتن خطایی روی داد.</string>\n  <string name=\"camera_failed_to_initialize\">دوربین راه اندازی نشد!</string>\n  <string name=\"annotate_image\">نوشتن بر روی تصویر</string>\n  <string name=\"gif_not_supported\">فایل های Gif پشتیبانی نمی شوند</string>\n  <string name=\"invalid_file_type\">برنامه یک نوع فایل نامعتبر را برگرداند</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">ذخیره کردن و خروج</string>\n  <string name=\"signature_capture\">ثبت امضا</string>\n  <string name=\"sign_button\">جمع آوری امضا</string>\n  <string name=\"markup_image\">نشانه گذاری تصویر</string>\n  <string name=\"draw_image\">تصویر طرح</string>\n  <string name=\"reset_image\">تنظیم مجدد</string>\n  <string name=\"set_color\">تنظیم رنگ</string>\n  <string name=\"capture_video\">ضبط ویدیو</string>\n  <string name=\"choose_video\">انتخاب ویدیو</string>\n  <string name=\"play_video\">پخش ویدیو</string>\n  <string name=\"choose_file\">انتخاب فایل</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">حذف شده</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">ضبط صدا</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">انتخاب صدا</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">توقف </string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">از سر گرفتن</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"background_audio_permission_explanation\">این فورم صدا را در پس زمینه ثبت میکند شما باید اجاره بدهید تا از میکروفون استفاده صورت بگیرد در غیر آن نمی توانید از فورم استفاد کنید.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">این فورم درخواست می دهد تا در پس زمنیه صدا ثیت گردد. غیر فعال ساختن این امکان ثیت را غیر فعال میسازد و صدای ثیت شده قبلی را از بین میبرد. آیا شما مطمن هستید که میخواهید ادامه بدهید.</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">غیرفعال کردن ضبط</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">ضبط بلافاصله شروع نمی شود. برای ضبط باید دوباره فورم را باز کنید.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">فایل حذف شود؟</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">پس از حذف نمی توانید این فایل را بازیابی کنید. آیا مطمئن هستید که می خواهید ادامه دهید؟</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">متوقف کردن</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">ثبت کردن</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">ضبط صدا</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">پخش صدا</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">در حال ذخیره فایل</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">قبل از ترک این صفحه باید ضبط را متوقف کنید.</string>\n  <string name=\"error_attaching_binary_file\">در زمان ضمیمه کردن فایل رسانه ای خطا رخ داد: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">انتخاب مقدار</string>\n  <string name=\"select_answer\">انتخاب پاسخ</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">ویرایش مقدار</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">هیچ مقدار را انتخاب نگردیده است</string>\n  <string name=\"selected_title\">انتخاب شد:</string>\n  <string name=\"trigger\">درست است، لطفاً ادامه بدهید.</string>\n  <string name=\"ext_file_no_data_error\">فایل حاوی هیچ data نیست!</string>\n  <string name=\"ext_conflicting_columns_error\">ستون ها %s  برای مطابقت</string>\n  <string name=\"ext_sortBy_numeric_error\">ستون مرتب سازی شما باید فقط حاوی مقادیر عددی باشد. مقدار متناقض \\\"%s\\\" بود.</string>\n  <string name=\"ext_search_wrong_arguments_error\">خطای نحوی در تابع search (): این تابع به 1، 4 یا 6 آرگومان نیاز دارد.</string>\n  <string name=\"ext_search_wrong_function_error\">خطای نحوی در تابع search(): تابع ناشناخته \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">خطای نحوی در تابع search(): \\'%s\\' به عنوان یک تابع ارزیابی نشد.</string>\n  <string name=\"ext_search_return_error\">کنترل کننده جستجو یک موضوع از نوع \\'%s\\' را برگرداند.</string>\n  <string name=\"ext_import_generic_error\">Data ها از %1$s وارد نشد. به دلیل: %2$s</string>\n  <string name=\"ext_import_progress_message\">در حال جاگیری data از «%1$s»، لطفاً صبر کنید… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">خواندن Data لغو شد!</string>\n  <string name=\"ext_import_finalizing_message\">در حال نهایی کردن data از پیش بارگیری شده…</string>\n  <string name=\"ext_import_completed_message\">خوانش داده ها تکمیل شد!</string>\n  <string name=\"ext_not_initialized_error\">مدیر data خارجی راه اندازی نشده است.</string>\n  <string name=\"ext_import_csv_missing_error\">معلومات خارجی %1$s وارد نشده است. شاید فراموش کرده اید که فایل %2$s.csv را به فورم خود اضافه کنید؟</string>\n  <string name=\"ext_search_generic_error\">خطای نحوی در تابع search(): %s</string>\n  <string name=\"ext_assign_value_error\">نمی توان مقدار \\\"%s\\\" را اختصاص داد.</string>\n  <string name=\"parser_exception\">XPathParser استثنا: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">ثبت بلبرینگ</string>\n  <string name=\"replace_bearing\">بلبرینگ را تعویض کنید</string>\n  <string name=\"getting_bearing\">در حال بار کردن بلبرینگ</string>\n  <string name=\"accept_bearing\">ثبت بلبرینگ</string>\n  <string name=\"bearing\">بلبرینگ: %.3f</string>\n  <string name=\"direction\">مسیر %s</string>\n  <string name=\"bearing_lack_of_sensors\">بلبرینگ را نمی توان جمع آوری کرد: دستگاه شتاب سنج، حسگر میدان مغناطیسی یا هر دو را ندارد.</string>\n  <string name=\"select_date\">انتخاب تاریخ</string>\n  <string name=\"select_time\">انتخاب زمان</string>\n  <string name=\"no_date_selected\">هیچ تاریخی انتخاب نشده است</string>\n  <string name=\"no_time_selected\">هیچ زمانی انتخاب نشده است</string>\n  <string name=\"ethiopian_month_1\">مسکرم</string>\n  <string name=\"ethiopian_month_5\">تیر</string>\n  <string name=\"ethiopian_month_7\">مگابیت</string>\n  <string name=\"islamic_month_1\">محرم</string>\n  <string name=\"islamic_month_2\">صفر</string>\n  <string name=\"islamic_month_3\">ربیع الاول</string>\n  <string name=\"islamic_month_4\">ربیع الثانی</string>\n  <string name=\"islamic_month_5\">جمادی الاول</string>\n  <string name=\"islamic_month_6\">جمادی الثانی</string>\n  <string name=\"islamic_month_7\">رجب</string>\n  <string name=\"islamic_month_8\">شعبان</string>\n  <string name=\"islamic_month_9\">رمضان</string>\n  <string name=\"islamic_month_10\">شوال</string>\n  <string name=\"islamic_month_11\">ذی القضه</string>\n  <string name=\"islamic_month_12\">ذی الحجه</string>\n  <string name=\"persian_month_1\">فروردین</string>\n  <string name=\"persian_month_2\">اردیبهشت</string>\n  <string name=\"persian_month_3\">خرداد</string>\n  <string name=\"persian_month_4\">تیر</string>\n  <string name=\"persian_month_5\">مرداد</string>\n  <string name=\"persian_month_6\">شهریور</string>\n  <string name=\"persian_month_7\">مهر</string>\n  <string name=\"persian_month_8\">آبان</string>\n  <string name=\"persian_month_9\">آذر</string>\n  <string name=\"persian_month_10\">دی</string>\n  <string name=\"persian_month_11\">بهمن</string>\n  <string name=\"persian_month_12\">اسفند</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">فایل: %s قابل اعتبار نیست.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">فایل: %sمفقود است.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">شناسایی را وارد کنید</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">شناسایی</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">این فورم از شما می خواهد که خودتان را شناسایی کنید</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">مسدود کردن</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">دلیل برای تغییرات</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">علت</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">باید دلیل تغییرات این فورم را توضیح دهید</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">ذخیره</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">مکان را انتخاب کنید</string>\n  <string name=\"choices\">برگزیده</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">نقشه ملی USGS Topo</string>\n  <string name=\"basemap\">نقشه اصلی</string>\n  <string name=\"basemap_source\">منبع</string>\n  <string name=\"basemap_source_unavailable\">متأسفیم، %s نقشه پایه در این دستگاه موجود نیست. لطفاً نقشه پایه دیگری را در تنظیمات &gt; نقشه ها.</string>\n  <string name=\"map_style_label\">%s سبک نقشه</string>\n  <string name=\"streets\">سرک ها</string>\n  <string name=\"terrain\">ناحیه</string>\n  <string name=\"hybrid\">ترکیبی</string>\n  <string name=\"satellite\">ستلایت</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">روشن</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">تاریک</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">در فضای بیرونی </string>\n  <string name=\"topographic\">توپوگرافی</string>\n  <string name=\"google_play_services_error_occured\">دسترسی به نقشه های گوگل امکان پذیر نیست. آیا خدمات Google Play نصب شده است؟</string>\n  <string name=\"carto_map_style_positron\">پوزیترون</string>\n  <string name=\"carto_map_style_dark_matter\">ماده تاریک</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">یک نقطه را ثبت کنید</string>\n  <string name=\"provider_disabled_error\">با معذرت، نشان دهنده های مکان غیرفعال هستند!</string>\n  <string name=\"google_play_services_not_available\">این فورم می خواهد موقعیت مکانی شما را ردیابی کند اما خدمات Google Play در دسترس نیست.</string>\n  <string name=\"location_providers_disabled_dialog_message\">این فورم می خواهد موقعیت مکانی شما را ردیابی کند اما ارائه دهندگان موقعیت مکانی غیرفعال هستند. لطفا در تنظیمات اندروید فعال کنید.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">رفتن به تنظیمات </string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"background_location_disabled\">این فورم می خواهد موقعیت مکانی شما را ردیابی کند اما ردیابی غیرفعال است. لطفاً در منوی %1$s بالا فعال کنید.</string>\n  <string name=\"background_location_enabled\">این فورم موقعیت مکانی شما را ردیابی می کند. می‌توانید ردیابی را در منوی %1$s بالا غیرفعال کنید.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">بررسی خطا</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">زمان سپری شده: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">نقطه در %1$s ذخیره خواهد شد</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">ماهواره ها: %1$s</string>\n  <string name=\"waiting_for_location\">تلاش برای بدست آوردن مکان لطفا منتظر باشید</string>\n  <string name=\"poor_accuracy\">دقت ضعیف لطفا منتظر باشید</string>\n  <string name=\"unacceptable_accuracy\">دقت غیر قابل قبول لطفا منتظر باشید</string>\n  <string name=\"improving_accuracy\">بهبود دقت لطفا منتظر باشید</string>\n  <string name=\"accuracy_status_title\">دریافت موقعیت مکانی</string>\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">W %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">شمال %1$s%2$s%3$s</string>\n  <string name=\"input_method\">روش وروردی</string>\n  <string name=\"start\">آغاز</string>\n  <string name=\"enable_gps\">فعال ساختن GPS</string>\n  <string name=\"gps_enable_message\">در دستگاه شما GPS غیرفعال گردید. آیا میخواهید دوباره آنرا فعال کنید؟</string>\n  <string name=\"geo_clear_warning\">ویژگی قبلا ایجاد شده است. آیا می خواهید ویژگی را پاک کنید؟</string>\n  <string name=\"geo_exit_warning\">تغییرات کنار بگذرید و به ODK برگردید؟</string>\n  <string name=\"polygon_validator\">برای ایجاد Polygon باید حداقل 3 امتیاز داشته باشد</string>\n  <string name=\"polyline_validator\">برای ایجاد Polyline باید حداقل 2 امتیاز داشته باشد</string>\n  <string name=\"clear\">پاک کردن</string>\n  <string name=\"placement_mode\">قرار دادن با گشیدن انگشت</string>\n  <string name=\"manual_mode\">ثبت دستی مکان</string>\n  <string name=\"automatic_mode\">ثبت خودکار موقعیت مکانی</string>\n  <string name=\"recording_interval\">فاصله ثبت:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">1%d ثانیه</item>\n    <item quantity=\"other\">%d ثانیه</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">1%d دقیقه</item>\n    <item quantity=\"other\">%d دقیقه</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">دقت مورد نیاز:</string>\n  <string name=\"none\">هیچ کدام</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"collection_status_paused\">امتیازهای وارد شده: %d</string>\n  <string name=\"collection_status_placement\">نقاط وارد شده: %d (برای قرار دادن نقاط ضربه بزنید)</string>\n  <string name=\"collection_status_manual\">امتیازهای وارد شده: %d (ثبت دستی)</string>\n  <string name=\"collection_status_auto_seconds\">امتیازهای وارد شده: %1$d (ثبت هر %2$d ثانیه)</string>\n  <string name=\"collection_status_auto_minutes\">امتیازهای وارد شده: %1$d (ثبت هر %2$d دقیقه)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">امتیازهای وارد شده: %1$d (ثبت هر %2$d ثانیه، دقت %3$d متر)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">امتیازهای وارد شده: %1$d (ثبت هر %2$d ثانیه، دقت %3$d متر)</string>\n  <string name=\"geopoint_no_draggable_instruction\">روی دکمه افزودن نشانگر انگشیتان را بکشید</string>\n  <string name=\"discard\">دور انداختن</string>\n  <string name=\"select_item_count\">%s: %d (%d روی نقشه نشان داده شده است)</string>\n  <string name=\"select_item\">برگزیدن</string>\n  <string name=\"new_item\">گزینه جدید</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">بازنگری پیش نویس</string>\n  <string name=\"view_data\">مشاهده فورم ذخیره شده</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">ردیابی موقعیت مکانی</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">ردیابی مکان…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">نمایش لوکیشن من</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">بزرگنمایی به تناسب همه</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">توقف </string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">حذف نقطه آخر.</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">حذف نقطه آخر.</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">در حال فرستادن فورم</string>\n  <string name=\"noselect_error\">با عرض پوزش، شما هیچ فورمی را انتخاب نکرده اید!</string>\n  <string name=\"no_connection\">اتصال شبکه در دسترس نیست</string>\n  <string name=\"sending_items\">ارسال %1$s از %2$s فورم(های)</string>\n  <string name=\"send_selected_data\">ارسال مورد انتخاب شده</string>\n  <string name=\"server_auth_credentials\">پسورد و یا نام کاربری اشتباه برای سرور: %s</string>\n  <string name=\"server_requires_auth\">سرور نیاز به احراز هویت دارد</string>\n  <string name=\"no_forms_uploaded\">ببخشید، هیچ فورمی آپلود نشده است</string>\n  <string name=\"upload_results\">نتایج آپلود</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">تغییر چشم انداز</string>\n  <string name=\"show_sent_and_unsent_forms\">نمایش فورم های ارسال شده و ارسال ناشده</string>\n  <string name=\"show_unsent_forms\">نمایش فورم های ارسال نشده</string>\n  <string name=\"send_in_progress\">ارسال در پس‌زمینه اجرا می‌شود، لطفاً به زودی دوباره امتحان کنید</string>\n  <string name=\"forms_upload_succeeded\">آپلود فورم ها با موفقیت انجام شد</string>\n  <string name=\"forms_upload_failed\">بارگذاری فورم ها انجام نشد</string>\n  <string name=\"all_uploads_succeeded\">همه آپلودها با موفقیت انجام شد!</string>\n  <string name=\"some_uploads_failed\">%1$s از %2$s بارگیره انجام نشد!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">تازه</string>\n  <string name=\"download\">انتخاب کنید</string>\n  <string name=\"downloading_data\">در حال اتصال به سرور</string>\n  <string name=\"fetching_file\">در حال دریافت %1$s. \\n\\n فورم %2$s از فورم (های) %3$s.</string>\n  <string name=\"form_download_progress\">%1$s. دریافت فایل‌های رسانه: %2$s از %3$s</string>\n  <string name=\"version\">نسخه:</string>\n  <string name=\"form_updates_available\">آپدیت های فورم موجود است</string>\n  <string name=\"newer_version_of_a_form_info\">این یک آپدیت به فورمی است که شما در اختیار دارید. </string>\n  <string name=\"form_update_error\">آپدیت فورم ناموفق بود</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">دانلود فهرست فورم ناموفق بود</string>\n  <string name=\"forms_download_succeeded\">دانلود فورم ها با موفقیت انجام شد</string>\n  <string name=\"forms_download_failed\">دانلود فورم ها انجام نشد</string>\n  <string name=\"all_downloads_succeeded\">همه دانلودها با موفقیت انجام شد!</string>\n  <string name=\"some_downloads_failed\">%1$s از %2$s بارکردن انجام نشد!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">شناسایی: %1$s نسخه: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">اگر همچنان با این مشکل مواجه هستید، آن را به فردی که از شما خواسته است معلومات جمع آوری کنید، گزارش دهید.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect نمی تواند به سرور در %s برسد. آیا URL را درست وارد کردید؟</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect نمی تواند به طور ایمن به سرور در %s متصل شود. آیا URL را درست وارد کردید؟</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">سرور %1$s کد وضعیت %2$s را برگرداند.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">سرور %s پاسخ نادرست ارائه کرد.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">سرور برای این فورم hash ارائه نکرده است.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">هنگام بارکردن این فورم، خطای دیسک در دستگاه روخ داد.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">این فورم دارای URL ارسال نادرست است.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">ذخیره فرم</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">فورم های خالی</string>\n  <string name=\"delete_confirm\">حذف فورم (های) %s</string>\n  <string name=\"delete_file\">حذف مورد انتخاب شده</string>\n  <string name=\"delete_no\">حذف نکنید</string>\n  <string name=\"delete_yes\">حذف فورم ها</string>\n  <string name=\"form_delete_message\">حذف کردن فورم های انتخاب شده</string>\n  <string name=\"file_deleted_ok\">%sفورم(ها) موفقانه حذف گردید!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">تنظیمات پروژه</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">سرور</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, نام کاربری، پسورد</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">نمایش پروژه</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">نام، نشان، رنگ</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">رابط کاربری</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">نقشه ها</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">نقشه پایه، سبک ها، لایه ها</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">از مدیریت</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">جدید سازی خودکار، ارسال خودکار، حذف خودکار</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">شناسایی کاربر و دستگاه</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">نام کاربری، شماره تلفن، شناسایی دستگاه</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">ازمایشی</string>\n  <string name=\"protected_settings\">محافظت شده</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">تنظیمات محافظت شده را باز کنید</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">پسورد را تغییر دهید، کنترل های دسترسی را تنظیم کنید</string>\n  <string name=\"set_admin_password\">پسورد مدیریت را تنظیم کنید</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">مدیریت پروژه</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">تنظیم مجدد، دوباره شروع مردن، حذف کردن</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">کنترل دسترسی</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">رابط کاربری را محدود کنید</string>\n  <string name=\"server_preferences\">تنظیمات سرور</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">آدرس وب سایت </string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">سرور آدرس وب سایت</string>\n  <string name=\"url_error\">ببخشید، آدرس وب سایت اشتباه است!</string>\n  <string name=\"username\">نام کاربر</string>\n  <string name=\"change_username\">اسم کاربری</string>\n  <string name=\"password\">پسورد</string>\n  <string name=\"change_password\">پسورد ODK</string>\n  <string name=\"password_error_whitespace\">فضای خالی پیشرو یا انتهایی در پسورد مجاز نیست</string>\n  <string name=\"username_error_whitespace\">فضای خالی پیشرو یا انتهایی در نام کاربری مجاز نیست</string>\n  <string name=\"app_theme\">ظاهر</string>\n  <string name=\"language\">زبان</string>\n  <string name=\"use_device_language\">از زبان دستگاه استفاده کنید</string>\n  <string name=\"font_size\">اندازه فونت متن</string>\n  <string name=\"change_font_size\">اندازه فونت متن</string>\n  <string name=\"font_size_extra_large\">بسیار بزرگ</string>\n  <string name=\"font_size_large\">بزرگ</string>\n  <string name=\"font_size_medium\">متوسط</string>\n  <string name=\"font_size_small\">کوچک</string>\n  <string name=\"font_size_extra_small\">بسیار کوچک</string>\n  <string name=\"navigation\">جهت یابی</string>\n  <string name=\"swipe_navigation\">از سوایپ های افقی استفاده کنید</string>\n  <string name=\"buttons_navigation\">از دکمه های جلو/عقب استفاده کنید</string>\n  <string name=\"swipe_buttons_navigation\">از سوایپ ها و دکمه ها استفاده کنید</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">بعدی</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">قبلی</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">آپدیت فورم</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">حالت خودکار آپدیت فورم خالی</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">کتابچه راهنمای</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">فقط فورم‌های دانلود شده قبلی</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">دقیقا مطابق با سرور</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">آپدیت خودکار فرکانس </string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">هر پانزده دقیقه بعد</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">هر ساعت</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">هر شش ساعت</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">هر 24 ساعت</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">دانلود خودکار</string>\n  <string name=\"automatic_download_summary\">نسخه های جدید فورم ها را به صورت خودکار دانلود کنید</string>\n  <string name=\"hide_old_form_versions_setting_title\">مخفی کردن نسخه های قدیمی</string>\n  <string name=\"hide_old_form_versions_setting_summary\">فقط جدیدترین نسخه در فورم خالی ظاهر می شود</string>\n  <string name=\"form_submission_category\">ارسال فورم</string>\n  <string name=\"autosend_selector_title\">ارسال خودکار</string>\n  <string name=\"autosend\">ارسال خودکار</string>\n  <string name=\"off\">خاموش</string>\n  <string name=\"wifi_autosend\">تنها وای فای</string>\n  <string name=\"cellular_autosend\">تنها تلفن همراه</string>\n  <string name=\"wifi_cellular_autosend\">وای فای یا تلفن همراه</string>\n  <string name=\"delete_after_send\">بعد از ارسال حذف کنید</string>\n  <string name=\"delete_after_send_summary\">فورم های نهایی و رسانه یی را بعد از ارسال به سرور حذف کنید</string>\n  <string name=\"form_filling_category\">فورم را پر کنید</string>\n  <string name=\"constraint_behavior_title\"> محدودیت پردازش</string>\n  <string name=\"constraint_behavior\">محدودیت رفتار پردازش </string>\n  <string name=\"constraint_behavior_on_swipe\">با کشیدن انگشت به جلو اعتبارسنجی کنید</string>\n  <string name=\"constraint_behavior_on_finalize\">اعتبارسنجی را تا نهایی شدن به تعویق بگذرید</string>\n  <string name=\"high_resolution_title\">ویدیو با وضوح بالا</string>\n  <string name=\"high_resolution_summary\">فعال ساختن ظبط ویدیو با رزولوشن بالا</string>\n  <string name=\"image_size_title\">اندازه تصویر</string>\n  <string name=\"image_size_dialog_title\">حداکثر پیکسل حاشیه بلند عکس</string>\n  <string name=\"image_size_large\">بزرگ (3072 پکسل)</string>\n  <string name=\"image_size_medium\">متوسط (2048 پکسل)</string>\n  <string name=\"image_size_small\">کوچک (1024 پکسل)</string>\n  <string name=\"image_size_very_small\">بسیار کوچک (640 پکسل)</string>\n  <string name=\"image_size_original\">اندازه اصلی از دوربین (فرض کنید)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">برای سوالات راهنمایی نشان دهید</string>\n  <string name=\"guidance_no\">نخیر</string>\n  <string name=\"guidance_yes\">بلی - همیشه نمایش داده شود</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">بلی - فرو پاشیده</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">از برنامه خارجی برای ثبت صدا استفاده کنید</string>\n  <string name=\"form_import_category\">فورم را وارد کنید</string>\n  <string name=\"instance_sync\">فورم ها را در هنگام واردات نهایی کنید</string>\n  <string name=\"instance_sync_summary\">فورم هایی که مستقیماً به فلودر پنهانی اضافه شده اند تأثیر می گذارد.</string>\n  <string name=\"analytics_preferences\">استفاده معلومات</string>\n  <string name=\"analytics\">معلومات استفاده ناشناس را جمع آوری کنید</string>\n  <string name=\"analytics_summary\">استفاده معلومات ناشناس به تیم ODK کمک می‌کند تا اصلاحات و ویژگی‌ها را اولویت‌بندی کند.</string>\n  <string name=\"form_metadata_title\">فورم metadata</string>\n  <string name=\"form_metadata\">فورم metadata</string>\n  <string name=\"form_metadata_summary\">این مقادیر به فورم هایی اضافه می شود که دارای فیلدهای از پیش بار شده نام کاربری، ایمیل و/یا تلفن هستند تا فرد ارسال کننده معلومات را شناسایی کند.</string>\n  <string name=\"user_defined\">تعریف شده توسط کاربر</string>\n  <string name=\"phone_number\">شماره تماس</string>\n  <string name=\"email\">ایمیل آدرس</string>\n  <string name=\"invalid_email_address\">آدرس ایمیل اعتبار ندارد</string>\n  <string name=\"device_defined\">تعریف شده توسط دستگاه</string>\n  <string name=\"device_id\">نمبر شناسایی دستگاه</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">در دسترس نیست</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">تنظیمات را باز کنید</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">تنظیمات باز شد</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">پسورد جدید را داخل کنید</string>\n  <string name=\"admin_password\">پسورد Admin</string>\n  <string name=\"admin_password_changed\">پسورد Admin با موفقیت تغییر کرد</string>\n  <string name=\"admin_password_disabled\">پسورد Admin غیر فعال گردید</string>\n  <string name=\"admin_password_incorrect\">ببخشید، پسورد نادرست است</string>\n  <string name=\"enter_admin_password\">پسورد عبور مدیریت را وارد کنید</string>\n  <string name=\"change_admin_password\">پسورد عبور مدیریت را تغییر دهید</string>\n  <string name=\"show_password\">نشان دادن پسورد</string>\n  <string name=\"found_in_main\">برای پنهان شدن از منوی اصلی، علامت را بردارید</string>\n  <string name=\"found_in_settings\">برای پنهان شدن از تنظیمات محافظت نشده، علامت را بردارید</string>\n  <string name=\"form_entry\">برای پنهان شدن از ورود فورم، علامت را بردارید</string>\n  <string name=\"form_end\">برای پنهان شدن از پایان فرم، علامت را بردارید</string>\n  <string name=\"reset_project_settings_title\">تنظیم مجدد</string>\n  <string name=\"reset_project_settings_subtext\">از میان تنظیمات، فورم‌ها، معلومات را انتخاب کنید</string>\n  <string name=\"delete_project\">حذف شده</string>\n  <string name=\"select_all\">انتخاب همه</string>\n  <string name=\"clear_all\">پاک کردن همه</string>\n  <string name=\"reset_settings\">تمام تنظیمات (تنظیمات داخلی، تنظیمات حفظ شده)</string>\n  <string name=\"reset_saved_forms\">فورم های ذخیره شده ( نمونه ها، پایگاه معلومات نمونه ها)</string>\n  <string name=\"reset_blank_forms\">فورم های خالی ( فورم ها، پایگاه معلومات فورم ها، پایگاه معلومات مجموعه اقلام )</string>\n  <string name=\"reset_layers\">لایه های نقشه (پوشه لایه ها)</string>\n  <string name=\"reset_cache\">بار کردن فورم حافظه پنهان (پوشه کش.)</string>\n  <string name=\"reset_settings_dialog_title\">انتخاب موارد جهت تنظیم مجدد</string>\n  <string name=\"reset_settings_button_reset\">تنظیم مجدد</string>\n  <string name=\"reset_in_progress\">در حال تنظیم مجدد</string>\n  <string name=\"reset_app_state_result\">نتایج تنظیم مجدد</string>\n  <string name=\"reset_app_state_warning\">همه معلومات های انتخاب شده برای همیشه حذف می شوند. لغو وجود ندارد.</string>\n  <string name=\"reset_settings_result\">تمام تنظیمات:: %s</string>\n  <string name=\"reset_saved_forms_result\">فورم های ذخیره شده:: %s</string>\n  <string name=\"reset_blank_forms_result\">قورم های خالی:: %s</string>\n  <string name=\"reset_layers_result\">لایه های نقشه:: %s</string>\n  <string name=\"reset_cache_result\">حافظه پنهان بار کردن فورم :: %s</string>\n  <string name=\"main_menu_settings\">تنظیمات منوی اصلی</string>\n  <string name=\"main_menu_settings_summary\">نمایش یا پنهان کردن دکمه ها</string>\n  <string name=\"user_settings\">تنظیمات کاربر</string>\n  <string name=\"user_setting_summary\">نمایش یا پنهان کردن تنظیمات</string>\n  <string name=\"form_entry_setting\">تنظیمات ورود به فورم</string>\n  <string name=\"form_entry_settings_summary\">نمایش یا پنهان کردن اقدامات</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">حرکت به سمت عقب</string>\n  <string name=\"moving_backwards_disabled_title\">حرکت به عقب غیرفعال است</string>\n  <string name=\"yes\">بلی</string>\n  <string name=\"no\">نخیر</string>\n  <string name=\"moving_backwards_enabled_title\">حرکت به عقب فعال شد</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">ذخیره پیش نویس</string>\n  <string name=\"save_mid_summary\">نماد ذخیره در نوار بالا و دکمه \\\"ذخیره به عنوان پیش نویس\\\" هنگام خروج از فرم</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">در باره</string>\n  <string name=\"tell_your_friends\">ODK Collect را به اشتراک بگذارید</string>\n  <string name=\"tell_your_friends_msg\">آیا همکاران شما هنوز در حال جمع آوری معلومات بر روی کاغذ هستند؟ ODK Collect را با آنها به اشتراک بگذارید.</string>\n  <string name=\"leave_a_review\">نظریه خود را راجع به Play Store بنویسید</string>\n  <string name=\"leave_a_review_msg\"> (امیدوارم) بررسی ما باعث افزایش دید برنامه در  Play Store می شود.</string>\n  <string name=\"odk_website\">به وبسایت ODK مراجعه کنید</string>\n  <string name=\"odk_website_summary\">ODK برای جمع آوری data برای منافع اجتماعی در محیط های چالش برانگیز استفاده می شود.</string>\n  <string name=\"odk_forum\">با انجمن ODK یکجا شوید</string>\n  <string name=\"odk_forum_summary\">برای دریافت کمک، درخواست ویژگی‌ها، مشارکت کد/ترجمه به انجمن بپیوندید!</string>\n  <string name=\"all_open_source_licenses\">منابع باز کتابخانه ها/مجوز ها</string>\n  <string name=\"all_open_source_licenses_msg\">ما روی شانه های غول ها ایستاده ایم!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">با کد QR تنظیم مجدد کنید</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">تمام تنظیمات موجود را جایگزین کنید</string>\n  <string name=\"qr_code\">تنظیم QR کد</string>\n  <string name=\"scan_qr_code_fragment_title\">اسکن کنید</string>\n  <string name=\"view_qr_code_fragment_title\">ًQR کد</string>\n  <string name=\"import_qrcode_sd\">QR کد را وارد شد</string>\n  <string name=\"successfully_imported_settings\">تنظیمات با موفقیت وارد شد</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">به حداکثر محدودیت رقم ها رسیده‌اید: QR کد نمی‌تواند برای همه تنظیمات ایجاد شود</string>\n  <string name=\"share\">اشتراک گذاری</string>\n  <string name=\"turn_on_flashlight\">روشن نمودن چراغ </string>\n  <string name=\"turn_off_flashlight\">خاموش نمودن چراغ</string>\n  <string name=\"qrcode_with_both_passwords\">حاوی اطلاعات حساس:<b> پسورد </b>مدیریت و <b>پسورد سرور</b></string>\n  <string name=\"qrcode_without_passwords\">کد QR حاوی<b> پسورد عبور مدیریت</b> یا <b>سرور</b> نیست. برای تنظیم کردن انگشتات را بکشید</string>\n  <string name=\"qrcode_with_admin_password\">حاوی اطلاعات حساس:<b> پسورد عبور </b>مدیریت</string>\n  <string name=\"qrcode_with_server_password\">حاوی اطلاعات حساس:<b> پسورد عبور </b>سرور</string>\n  <string name=\"server_password\">رمز سرور</string>\n  <string name=\"include_password_dialog\">رمزهای عبور با code یکجاست</string>\n  <string name=\"generate\">تولید کردن</string>\n  <string name=\"invalid_qrcode\">QR کد شامل تنظیمات معتبری نیست</string>\n  <string name=\"qr_code_not_found\">QR کد در تصویر انتخابی یافت نشد</string>\n  <string name=\"corrupt_imported_preferences_error\">تنظیمات فعلی خراب است. از تنظیمات مدیریت پروژه، تنظیمات را بازنشانی کنید یا تنظیمات درست را وارد کنید.</string>\n  <string name=\"settings_with_gd_protocol\">پروژه‌های Google Drive/Sheets دیگر نمی‌توانند ایجاد شوند</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">درست است</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">لغو </string>\n  <string name=\"cancel_loading_form\">لغو</string>\n  <string name=\"cancel_location\">لغو</string>\n  <string name=\"do_not_change\">انصراف</string>\n  <string name=\"canceling\">لغو کردن</string>\n  <string name=\"notification_channel_name\">اطلاعیه های عمومی</string>\n  <string name=\"success\">موفقیت </string>\n  <string name=\"error_occured\">خطا اتفاق افتاده است</string>\n  <string name=\"please_wait\">لطفاً برای چند لحظه منتظر باشید</string>\n  <string name=\"please_wait_long\">لطفاً منتظر باشید: این میتواند چند دقیقه را در بر بگیرد.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">\\\"%s\\\" حذف نشد. لطفا فایل را به صورت دستی حذف کنید و دوباره فورم را دانلود کنید.</string>\n  <string name=\"fs_create_media_folder_error\">folder رسانه \\'%s\\' ایجاد نشد.</string>\n  <string name=\"fs_file_copy_error\">\\\"%1$s\\\" روی \\\"%2$s\\\" کپی نشد. دلیل: %3$s</string>\n  <string name=\"select_odk_shortcut\">میانبر ODK را انتخاب کنید</string>\n  <string name=\"background_audio_help\">این فورم صدا را از میکروفون دستگاه شما ضبط می‌کند. \\n\\n می‌توانید از نشانگر میزان صدا استفاده کنید تا مطمئن شوید میکروفون به اندازه کافی به صدایی که باید ضبط کنید نزدیک است. \\n\\n برای توقف ضبط لطفاً از این فورم خارج شوید. \\n\\ n برای اطلاعات بیشتر، لطفاً با شخصی که از شما خواسته است اطلاعات را جمع آوری کنید، تماس بگیرید.</string>\n  <string name=\"loading\">بارگذاری…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">پروژه ها</string>\n  <string name=\"settings\">تنظیمات</string>\n  <string name=\"add_project\">اضافه کردن پروژه</string>\n  <string name=\"add\">اضافه</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">پروژه تکراری</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">شما قبلاً پروژه ای با این تنظیمات اتصال دارید. آیا می خواهید به پروژه موجود تغییر دهید یا یک پروژه جدید اضافه کنید</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">اضافه کردن پروژه تکراری</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">تغییر به پروژه موجود</string>\n  <string name=\"project_name\">نام پروژه</string>\n  <string name=\"project_icon\">نماد پروژه</string>\n  <string name=\"project_color\">رنگ پروژه</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">به %s تغییر وضعیت داد</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">با استفاده از %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">به %s تغییر دهید</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"or\">یا</string>\n  <string name=\"hex_color\">رنگ Hex</string>\n  <string name=\"invalid_hex_code\">hex code نادرست است</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">جمع‌آوری data در هر کجا</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">تنظیم کردن با QR code</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">جزئیات پروژه را به صورت دستی وارد کنید</string>\n  <string name=\"project_config_tip\">پس از اینکه پروژه خود را اضافه کردید، می توانید آن را در تنظیمات تنظیم کنید</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">هنوز پروژه ای ندارید؟</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">نسخه ی نمایشی را امتحان کنید</string>\n  <string name=\"scan_configuration_qr_code\"> QR کد پیکربندی را اسکن کنید</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">نمایش جزئیات</string>\n  <string name=\"errors\">اشتباه</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">برای باز کردن این فرم، ابتدا باید ODK Collect را باز کنید و به پروژه ای که مربوط آن است بروید.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">او دی کی کلیکت تنظیم نگردیده است، کوشش کنید تا او دی کی کلیکت را باز کنید و آنرا تنظیم کنید. اگر شما بالای میانبر فشار وارد کنید، پس از تنظیم کردن ODK collect شما نیاز پیدا خواهد کرد تا انرا دوباره ایجاد کنید.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">URI شناسایی نشده: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">URI: %s : بی اعتبار</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">فورم پر شده حذف شد!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">ویرایش این پیش نویس امکان پذیر نیست زیرا فرم خالی مربوطه وجود ندارد یا حذف شده است.\\n\\nشناسه فرم: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">شما دو فورم مختلف را باشناسایی و نسخه یکسان دانلود کرده اید. شاید همان فورم بارگذاری شده در زمان های مختلف یا در سرورهای مختلف باشد. در هر صورت باید یکی را حذف کنید.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">تنظیمات را باز کنید</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">آخرین باری که برنامه در حال اجرا بود یک خرابی وجود داشت!</string>\n  <string name=\"cant_start_app\">نمی توان app را راه اندازی کرد!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"learn_more_button_text\">بیشتر بدانید</string>\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">توسعه ابزارها</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">از کار افتاد app</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">اجبار یک استثناء کشف نشده که باعث از کار افتادن برنامه می شود</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"permission_dialog_title\">درباره محدودیت ها</string>\n  <string name=\"permission_dialog_text\">از شما خواسته می شود که به ODK Collect اجازه دسترسی به ویژگی های زیر را بدهید، اگر می خواهید از آنها استفاده کنید، \\\"اجازه\\\" را انتخاب کنید.</string>\n  <string name=\"notifications\">اعلان ها</string>\n  <string name=\"required_for_notifications\">برای نمایش به‌روزرسانی‌ها هنگام بارگیری، به‌روزرسانی و ارسال فرم‌ها ضروری است.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">فرم شما به عنوان پیش نویس ذخیره شد.</string>\n  <string name=\"form_saved\">فرم شما ذخیره شد.</string>\n  <string name=\"form_sending\">در حال ارسال فرم</string>\n  <string name=\"edit_form\">بازنگری</string>\n  <string name=\"view_form\">نمایش</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">آخرین فرم ارسال شده: 1%d ثانیه قبل</item>\n    <item quantity=\"other\">آخرین فرم ارسال شده: 1%d ثانیه قبل</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"one\">آخرین فرم ارسال شده: 1%d دقیقه قبل</item>\n    <item quantity=\"other\">آخرین فرم ارسال شده: 1%d دقیقه قبل</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"one\">آخرین فرم ارسال شده: 1%d ساعت قبل</item>\n    <item quantity=\"other\">آخرین فرم ارسال شده: 1%d ثانیه قبل</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"one\">آخرین فرم ارسال شده: 1%d روز قبل</item>\n    <item quantity=\"other\">آخرین فرم ارسال شده: 1%d روز قبل</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"one\">1%d فرم آماده ارسال است</item>\n    <item quantity=\"other\">1%d فرم آماده ارسال است</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">در نسخه های بعدی، فرم های نهایی شده دیگر قابل ویرایش نخواهند بود. فرم‌ها را به‌عنوان پیش‌نویس ذخیره کنید تا بعداً آن‌ها را ویرایش کنید. \\n\\nمی‌توانید با ضربه زدن روی سه نقطه (⋮) و سپس خطاها را در فرم پیش‌نویس بررسی کنید.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">در نسخه های بعدی، فرم های نهایی شده دیگر قابل ویرایش نخواهند بود.</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">تمام پیش نویس ها را نهایی کنید.</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"one\">آیا می‌خواهید 1%d پیش‌نویس را نهایی کنید؟</item>\n    <item quantity=\"other\">آیا می‌خواهید 1%d پیش‌نویس ها را نهایی کنید؟</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">وقتی همه پیش‌نویس‌ها را نهایی کردید، در «آماده ارسال» قرار می‌گیرند و نمی‌توانید ویرایش کنید. هر پیش‌نویسی که دارای خطا باشد، نهایی نخواهد شد.\\n\\nشما نمی‌توانید این عمل را واگرد کنید.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"one\">موفق! 1%d پیش‌نویس نهایی شد.</item>\n    <item quantity=\"other\">موفق! 1%d پیش‌نویس نهایی شد.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"one\">1%d پیش نویس دارای خطاهایی است که باید قبل از نهایی شدن برطرف شوند.</item>\n    <item quantity=\"other\">1%d پیش نویس دارای خطاهایی است که باید قبل از نهایی شدن برطرف شوند.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">1%d پیش‌نویس نهایی شد. 1%d پیش نویس دارای خطاهایی است که باید قبل از نهایی شدن برطرف شوند.</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">1%d پیش‌نویس نهایی شد. پیش نویس هایی که باقی مانده اند باید به صورت دستی نهایی شوند.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">اشتباه</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">بدون خطا</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">برای پنهان شدن از پیش نویس ها، علامت را بردارید.</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">چیزی برای نمایش در دسترس نیست</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">هیچ چیزی در پیش نویس ها وجود ندارد</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">وقتی به عنوان پیش‌نویس ذخیره می‌کنید، فرم‌ها در اینجا ظاهر می‌شوند</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">چیزی برای ارسال آماده نیست</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">وقتی پیش‌نویس‌ها را نهایی کردید، در اینجا ظاهر می‌شوند</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">چیزی ارسال نشد</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">هنگامی که فرم های نهایی شده را ارسال می کنید، آنها در اینجا ظاهر می شوند</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">دور انداختن</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-fa-rAF/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">شروع با فرم جدید</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">پیش نویس</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">آماده حهت ارسال</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">ارسال شد</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">دانلود فرم</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">حذف فرم</string>\n  <string name=\"saved_forms\">ذخیره فvم ها</string>\n  <string name=\"version_number\">نسخه: %s</string>\n  <string name=\"id_number\">شماره شناسایی: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">اضافه شد EEE,MMM,dd,yyyy در HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\' بارگذاری \\' EEE, MMM dd, yyyy \\'at\\' HH:mm\n </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">ذخیره شد EEE,MMM,dd,yyyy در HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">نهایی شد EEE,MMM,dd,yyyy در HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">ارسال شده EEE,MMM,dd,yyyy در HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'ارسال ناموفق بود\\' EEE, MMM dd, yyyy\\'در\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">حذف شده EEE, MMM dd, yyyy در  HH:mm</string>\n  <string name=\"submission_deleted\">ارسال حذف شد</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">اصلاح شد EEE,MMM,dd,yyyy در HH:mm</string>\n  <string name=\"encrypted_form\">فرم رمز گذاری شده</string>\n  <string name=\"deleted_form\">حذف فرم</string>\n  <string name=\"sort_the_list\">لیست را مرتب کنید</string>\n  <string name=\"sort_by\">دسته بندی بر اساس</string>\n  <string name=\"sort_by_name_asc\">اسم، الف-ی</string>\n  <string name=\"sort_by_name_desc\">اسم ی-الف</string>\n  <string name=\"sort_by_date_desc\">تاریخ،  جدیدترین در اولویت</string>\n  <string name=\"sort_by_date_asc\">تاریخ،  قدیمی ترها در اولویت</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">فیلتر لیست</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">جستجو</string>\n  <string name=\"not_granted_permission\">فعالیتی که شما میخواهید آنرا انجام دهید به مجوزی ضرورت دارد که به شما اعطا نشده است. </string>\n  <string name=\"storage_runtime_permission_denied_title\">مجوزهای برای ذخیره سازی</string>\n  <string name=\"storage_runtime_permission_denied_desc\">بدون مجوزها، مجموعه نمی‌تواند به فرم‌های شما دسترسی داشته باشد یا پاسخ‌ها را ذخیره کند. زمانی که برای اعطای برنامه آماده شدید، دوباره آن را باز کنید.</string>\n  <string name=\"location_runtime_permissions_denied_title\">دسترسی لوکیشن</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">بدون اجازه <b>Precise Location</b> نمی تواند موقعیت مکانی شما را ضبط کند. لطفاً هنگامی که  اجازه دسترسی آن فعال شد، دوباره امتحان کنید.</string>\n  <string name=\"camera_runtime_permission_denied_title\">دسترسی دوربین</string>\n  <string name=\"camera_runtime_permission_denied_desc\">بدون این مجوز Collect نمی تواند به دوربین دسترسی داشته باشد. لطفاً هنگامی که آماده اعطای آن هستید دوباره امتحان کنید.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">دسترسی ضبط صدا</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">بدون این مجوز، Collect نمی تواند به میکروفون دسترسی داشته باشد. لطفاً هنگامی که آماده اعطای آن هستید دوباره امتحان کنید.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">نقشه فرم را باز کنید</string>\n  <string name=\"instance_scan_count\">%1$dفرم ها اضافه شدند.</string>\n  <string name=\"finished_disk_scan\">اسکن به پایان رسید. همه فرم ها بارگذاری شدند.</string>\n  <string name=\"xform_parse_error\">خطای تجزیه XForm برای %1$s: %2$s وجود ندارد یا نامعتبر است.</string>\n  <string name=\"loading_form\">در حال آماده سازی فرم</string>\n  <string name=\"survey_loading_reading_form_message\">خواندن تعریف فرم…</string>\n  <string name=\"survey_loading_reading_data_message\">خواندن داده های نظر سنجی </string>\n  <string name=\"survey_loading_reading_csv_message\">خواندن اسناد CSV</string>\n  <string name=\"parse_error\">با عرض پوزش، نمی توان فرم را تجزیه کرد.</string>\n  <string name=\"loading_form_failed\">هنگام  آماده سازی فرم خطایی روی داد. لطفا دوباره تلاش کنید.</string>\n  <string name=\"cannot_edit_completed_form\">این فرم بعد از نهایی شدن آن قابل ویرایش نیست. ممکن است رمزگذاری شده باشد. </string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">تغییرات ذخیره نشده از savepoint بازیابی شد!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">تغییر زبان</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">به Prompt بروید</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">ذخیره پیش نویس</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">رفتن به ابتدا</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">رفتن به انتها</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">بالا برو</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">گروه</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">گروه تکرار شونده</string>\n  <string name=\"exit\">خروج</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">\\\"%s\\\" اضافه شود؟</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">اضافه کردن</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">اضافه کردن جدید</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">ویرایش سریع</string>\n  <string name=\"clearanswer_confirm\">آیا برای \\\"%s\\\" پاسخ حذف شود؟</string>\n  <string name=\"clear_answer\">حذف پاسخ</string>\n  <string name=\"clear_answer_ask\">آیا این پاسخ حذف شود؟</string>\n  <string name=\"clear_answer_no\">لغو</string>\n  <string name=\"discard_answer\">حذف پاسخ</string>\n  <string name=\"discard_group\">حذف گروه</string>\n  <string name=\"delete_repeat\">حذف گروه</string>\n  <string name=\"delete_repeat_ask\">این گروه را حذف کنید؟</string>\n  <string name=\"delete_repeat_confirm\">حذف گروه \\\"%s\\\" با تمام زیر گروپ های آن</string>\n  <string name=\"delete_repeat_no\">انصراف</string>\n  <string name=\"invalid_answer_error\">متاسفانه، این پاسخ غیر معتبر است.</string>\n  <string name=\"required_answer_error\">ببخشید، این پاسخ الزامی است!</string>\n  <string name=\"success_form_validation\">موفق! هیچ خطایی در فرم شما یافت نشد.</string>\n  <string name=\"invalid_space_in_answer_singular\">هشدار: مقدار اساسی %s دارای فاصله است</string>\n  <string name=\"invalid_space_in_answer_plural\">هشدار: مقادیر اساسی %s دارای فاصله هستند</string>\n  <string name=\"too_complex_form\">این فرم برای این دستگاه بسیار پیچیده است. سعی کنید برای ساده سازی فرم  یا اصطلاحات کمک بخواهید.</string>\n  <string name=\"survey_internal_error\">خطای داخلی: گام به اعلان ناموفق بود.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">شما در پایان %sقرار دارید.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">ذخیره پیش نویس</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">نهایی</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">ارسال شد</string>\n  <string name=\"data_saved_error\">متاسفانه فرم ذخیره نشد!</string>\n  <string name=\"data_saved_ok\">فرم با موفقیت ذخیره شد!</string>\n  <string name=\"save_point_error\">خطا در ذخیره نقطه یابی: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">نادیده گرفتن فرم</string>\n  <string name=\"discard_changes\">نادیده گرفتن تغییرات</string>\n  <string name=\"keep_changes\">ذخیره تغییرات</string>\n  <string name=\"keep_editing\">به ویرایش ادامه دهید</string>\n  <string name=\"quit_form_title\">ذخبره فرم؟</string>\n  <string name=\"save_explanation\">شما می توانید این فرم را ذخیره کرده و در هر زمان که بخواهید از پیش نویس های خود به آن دسترسی داشته باشید.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\\"این فرم آخرین بار در\\\" EEE، MMM dd، yyyy \\\"در\\\" HH:mm ذخیره شده است. ذخیره به عنوان پیش نویس برای حفظ تغییرات.</string>\n  <string name=\"quit_form_continue_title\">به ویرایش ادامه دهید؟</string>\n  <string name=\"discard_form_warning\">اگر فرم را نادیده بگیرید، تغییراتی را که تاکنون انجام داده اید از دست خواهید داد.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">در حال ذخیره فرم</string>\n  <string name=\"survey_saving_validating_message\">در حال اعتبار سنجی پاسخ ها….</string>\n  <string name=\"survey_saving_collecting_message\">در حال جمع آوری داده ها…</string>\n  <string name=\"survey_saving_saving_message\">در حال ذخیره در کارت حافظه…</string>\n  <string name=\"survey_saving_finalizing_message\">در حال نهایی کردن کارت حافظه</string>\n  <string name=\"survey_saving_encrypting_message\">رمزگذاری داده ها…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">نه دقیقاً یک رکورد برای این مثال!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">دقیقاً یک فرم خالی با این jr_form_id مطابقت ندارد.</string>\n  <string name=\"no_form_id_specified\">FormId مشخص نشده؟؟؟</string>\n  <string name=\"phone_does_not_support_rsa\">تلفن از رمزگذاری RSA پشتیبانی نمی کند.</string>\n  <string name=\"invalid_rsa_public_key\">کلید عمومی RSA نامعتبر است.</string>\n  <string name=\"encryption_error_message\">فورم %s به عنوان نهایی ذخیره نشده است.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">ضبط شروع نشد.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">میکروفون در حال حاضر در حال استفاده است.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">رسانه را نمی توان به فورم پیوست کرد اما در %1$s موجود است</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">ضبط غیرفعال شد در %s فعال کنید</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">شکست در تلاش برای به روزرسانی مقادیر\\\" این عمومآ به دلیل استفاده نادرست از حسابات در طرح فورم می باشد. اگر این مشگل به شکل دوامدار مواجه شدید: پس این موضوع را شخص که از شما خواسته تا معلومات را جمع آوری کنید به اشتراک بگدازید.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">راه اندازی</string>\n  <string name=\"activity_not_found\">برای انجام %s هیچ فعالیتی دریافت نشد:</string>\n  <string name=\"no_app\"> درخواستی برای فورمه موجود نیست. لطفاً خواندن را به صورت دستی وارد کنید.</string>\n  <string name=\"null_intent_value\">درخواست نامه خارجی اطلاعات مورد انتظار را ارائه نکرد.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">باز نمودن آدرس انترنتی</string>\n  <string name=\"capture_osm\">راه اندازی ابزار نقشه</string>\n  <string name=\"recapture_osm\">برچسپ گذاری مجدد OSM</string>\n  <string name=\"edited_osm_file\">فایل OSM XML ویرایش شده</string>\n  <string name=\"invalid_osm_data\">مشکلی پیش آمد. ما داده OSM معتبر دریافت نکردیم.</string>\n  <string name=\"alert\">هشدار</string>\n  <string name=\"install_openmapkit\">لطفاْ OpenMapKit را نصب کنید!</string>\n  <string name=\"get_barcode\">دریافت بارکد</string>\n  <string name=\"replace_barcode\">جاگزینی بارکود</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">بارکد را داخل مستطیل قرار دهید</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">فایل را باز کنید</string>\n  <string name=\"rank_items\">رده بندی اقلام</string>\n  <string name=\"number_picker_title\">شماره جمع کننده</string>\n  <string name=\"invalid_range_widget\">این ویجت دارای پارامترهای نامعتبر است!</string>\n  <string name=\"svg_file_does_not_exist\">فایل SVG موجود نیست!</string>\n  <string name=\"capture_image\">گرفتن عکس</string>\n  <string name=\"choose_image\">انتخاب تصویر</string>\n  <string name=\"selected_invalid_image\">فایل انتخاب شده تصویر قابل اعتبار ندارد</string>\n  <string name=\"take_picture_instruction\">برای گرفتن عکس روی صفحه ضربه بزنید</string>\n  <string name=\"error_front_camera_unavailable\">این دستگاه دوربین جلو موجود نیست</string>\n  <string name=\"camera_error\">هنگام عکس گرفتن خطایی روی داد.</string>\n  <string name=\"camera_failed_to_initialize\">دوربین راه اندازی نشد!</string>\n  <string name=\"annotate_image\">نوشتن بر روی تصویر</string>\n  <string name=\"gif_not_supported\">فایل های Gif پشتیبانی نمی شوند</string>\n  <string name=\"invalid_file_type\">برنامه یک نوع فایل نامعتبر را برگرداند</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">ذخیره کردن و خروج</string>\n  <string name=\"signature_capture\">ثبت امضا</string>\n  <string name=\"sign_button\">جمع آوری امضا</string>\n  <string name=\"markup_image\">نشانه گذاری تصویر</string>\n  <string name=\"draw_image\">تصویر طرح</string>\n  <string name=\"reset_image\">تنظیم مجدد</string>\n  <string name=\"set_color\">تنظیم رنگ</string>\n  <string name=\"capture_video\">ضبط ویدیو</string>\n  <string name=\"choose_video\">انتخاب ویدیو</string>\n  <string name=\"play_video\">پخش ویدیو</string>\n  <string name=\"choose_file\">انتخاب فایل</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">حذف شده</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">ضبط صدا</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">انتخاب صدا</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">توقف </string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">از سر گرفتن</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"background_audio_permission_explanation\">این فورم صدا را در پس زمینه ثبت میکند شما باید اجاره بدهید تا از میکروفون استفاده صورت بگیرد در غیر آن نمی توانید از فورم استفاد کنید.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">این فورم درخواست می دهد تا در پس زمنیه صدا ثیت گردد. غیر فعال ساختن این امکان ثیت را غیر فعال میسازد و صدای ثیت شده قبلی را از بین میبرد. آیا شما مطمن هستید که میخواهید ادامه بدهید.</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">غیرفعال کردن ضبط</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">ضبط بلافاصله شروع نمی شود. برای ضبط باید دوباره فورم را باز کنید.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">فایل حذف شود؟</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">پس از حذف نمی توانید این فایل را بازیابی کنید. آیا مطمئن هستید که می خواهید ادامه دهید؟</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">متوقف کردن</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">ثبت کردن</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">ضبط صدا</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">پخش صدا</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">در حال ذخیره فایل</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">قبل از ترک این صفحه باید ضبط را متوقف کنید.</string>\n  <string name=\"error_attaching_binary_file\">در زمان ضمیمه کردن فایل رسانه ای خطا رخ داد: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">انتخاب مقدار</string>\n  <string name=\"select_answer\">انتخاب پاسخ</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">ویرایش مقدار</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">هیچ مقدار را انتخاب نگردیده است</string>\n  <string name=\"selected_title\">انتخاب شد:</string>\n  <string name=\"trigger\">درست است، لطفاً ادامه بدهید.</string>\n  <string name=\"ext_file_no_data_error\">فایل حاوی هیچ data نیست!</string>\n  <string name=\"ext_conflicting_columns_error\">ستون ها %s  برای مطابقت</string>\n  <string name=\"ext_sortBy_numeric_error\">ستون مرتب سازی شما باید فقط حاوی مقادیر عددی باشد. مقدار متناقض \\\"%s\\\" بود.</string>\n  <string name=\"ext_search_wrong_arguments_error\">خطای نحوی در تابع search (): این تابع به 1، 4 یا 6 آرگومان نیاز دارد.</string>\n  <string name=\"ext_search_wrong_function_error\">خطای نحوی در تابع search(): تابع ناشناخته \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">خطای نحوی در تابع search(): \\'%s\\' به عنوان یک تابع ارزیابی نشد.</string>\n  <string name=\"ext_search_return_error\">کنترل کننده جستجو یک موضوع از نوع \\'%s\\' را برگرداند.</string>\n  <string name=\"ext_import_generic_error\">Data ها از %1$s وارد نشد. به دلیل: %2$s</string>\n  <string name=\"ext_import_progress_message\">در حال جاگیری data از «%1$s»، لطفاً صبر کنید… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">خواندن Data لغو شد!</string>\n  <string name=\"ext_import_finalizing_message\">در حال نهایی کردن data از پیش بارگیری شده…</string>\n  <string name=\"ext_import_completed_message\">خوانش داده ها تکمیل شد!</string>\n  <string name=\"ext_not_initialized_error\">مدیر data خارجی راه اندازی نشده است.</string>\n  <string name=\"ext_import_csv_missing_error\">معلومات خارجی %1$s وارد نشده است. شاید فراموش کرده اید که فایل %2$s.csv را به فورم خود اضافه کنید؟</string>\n  <string name=\"ext_search_generic_error\">خطای نحوی در تابع search(): %s</string>\n  <string name=\"ext_assign_value_error\">نمی توان مقدار \\\"%s\\\" را اختصاص داد.</string>\n  <string name=\"parser_exception\">XPathParser استثنا: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">ثبت بلبرینگ</string>\n  <string name=\"replace_bearing\">بلبرینگ را تعویض کنید</string>\n  <string name=\"getting_bearing\">در حال بار کردن بلبرینگ</string>\n  <string name=\"accept_bearing\">ثبت بلبرینگ</string>\n  <string name=\"bearing\">بلبرینگ: %.3f</string>\n  <string name=\"direction\">مسیر %s</string>\n  <string name=\"bearing_lack_of_sensors\">بلبرینگ را نمی توان جمع آوری کرد: دستگاه شتاب سنج، حسگر میدان مغناطیسی یا هر دو را ندارد.</string>\n  <string name=\"select_date\">انتخاب تاریخ</string>\n  <string name=\"select_time\">انتخاب زمان</string>\n  <string name=\"no_date_selected\">هیچ تاریخی انتخاب نشده است</string>\n  <string name=\"no_time_selected\">هیچ زمانی انتخاب نشده است</string>\n  <string name=\"ethiopian_month_1\">مسکرم</string>\n  <string name=\"ethiopian_month_5\">تیر</string>\n  <string name=\"ethiopian_month_7\">مگابیت</string>\n  <string name=\"islamic_month_1\">محرم</string>\n  <string name=\"islamic_month_2\">صفر</string>\n  <string name=\"islamic_month_3\">ربیع الاول</string>\n  <string name=\"islamic_month_4\">ربیع الثانی</string>\n  <string name=\"islamic_month_5\">جمادی الاول</string>\n  <string name=\"islamic_month_6\">جمادی الثانی</string>\n  <string name=\"islamic_month_7\">رجب</string>\n  <string name=\"islamic_month_8\">شعبان</string>\n  <string name=\"islamic_month_9\">رمضان</string>\n  <string name=\"islamic_month_10\">شوال</string>\n  <string name=\"islamic_month_11\">ذی القضه</string>\n  <string name=\"islamic_month_12\">ذی الحجه</string>\n  <string name=\"persian_month_1\">فروردین</string>\n  <string name=\"persian_month_2\">اردیبهشت</string>\n  <string name=\"persian_month_3\">خرداد</string>\n  <string name=\"persian_month_4\">تیر</string>\n  <string name=\"persian_month_5\">مرداد</string>\n  <string name=\"persian_month_6\">شهریور</string>\n  <string name=\"persian_month_7\">مهر</string>\n  <string name=\"persian_month_8\">آبان</string>\n  <string name=\"persian_month_9\">آذر</string>\n  <string name=\"persian_month_10\">دی</string>\n  <string name=\"persian_month_11\">بهمن</string>\n  <string name=\"persian_month_12\">اسفند</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">فایل: %s قابل اعتبار نیست.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">فایل: %sمفقود است.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">شناسایی را وارد کنید</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">شناسایی</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">این فورم از شما می خواهد که خودتان را شناسایی کنید</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">مسدود کردن</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">دلیل برای تغییرات</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">علت</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">باید دلیل تغییرات این فورم را توضیح دهید</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">ذخیره</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">مکان را انتخاب کنید</string>\n  <string name=\"choices\">برگزیده</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">نقشه ملی USGS Topo</string>\n  <string name=\"basemap\">نقشه اصلی</string>\n  <string name=\"basemap_source\">منبع</string>\n  <string name=\"basemap_source_unavailable\">متأسفیم، %s نقشه پایه در این دستگاه موجود نیست. لطفاً نقشه پایه دیگری را در تنظیمات &gt; نقشه ها.</string>\n  <string name=\"map_style_label\">%s سبک نقشه</string>\n  <string name=\"streets\">سرک ها</string>\n  <string name=\"terrain\">ناحیه</string>\n  <string name=\"hybrid\">ترکیبی</string>\n  <string name=\"satellite\">ستلایت</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">روشن</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">تاریک</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">در فضای بیرونی </string>\n  <string name=\"topographic\">توپوگرافی</string>\n  <string name=\"google_play_services_error_occured\">دسترسی به نقشه های گوگل امکان پذیر نیست. آیا خدمات Google Play نصب شده است؟</string>\n  <string name=\"carto_map_style_positron\">پوزیترون</string>\n  <string name=\"carto_map_style_dark_matter\">ماده تاریک</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">یک نقطه را ثبت کنید</string>\n  <string name=\"provider_disabled_error\">با معذرت، نشان دهنده های مکان غیرفعال هستند!</string>\n  <string name=\"google_play_services_not_available\">این فورم می خواهد موقعیت مکانی شما را ردیابی کند اما خدمات Google Play در دسترس نیست.</string>\n  <string name=\"location_providers_disabled_dialog_message\">این فورم می خواهد موقعیت مکانی شما را ردیابی کند اما ارائه دهندگان موقعیت مکانی غیرفعال هستند. لطفا در تنظیمات اندروید فعال کنید.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">رفتن به تنظیمات </string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"background_location_disabled\">این فورم می خواهد موقعیت مکانی شما را ردیابی کند اما ردیابی غیرفعال است. لطفاً در منوی %1$s بالا فعال کنید.</string>\n  <string name=\"background_location_enabled\">این فورم موقعیت مکانی شما را ردیابی می کند. می‌توانید ردیابی را در منوی %1$s بالا غیرفعال کنید.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">بررسی خطا</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">زمان سپری شده: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">نقطه در %1$s ذخیره خواهد شد</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">ماهواره ها: %1$s</string>\n  <string name=\"waiting_for_location\">تلاش برای بدست آوردن مکان لطفا منتظر باشید</string>\n  <string name=\"poor_accuracy\">دقت ضعیف لطفا منتظر باشید</string>\n  <string name=\"unacceptable_accuracy\">دقت غیر قابل قبول لطفا منتظر باشید</string>\n  <string name=\"improving_accuracy\">بهبود دقت لطفا منتظر باشید</string>\n  <string name=\"accuracy_status_title\">دریافت موقعیت مکانی</string>\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">W %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">شمال %1$s%2$s%3$s</string>\n  <string name=\"input_method\">روش وروردی</string>\n  <string name=\"start\">آغاز</string>\n  <string name=\"enable_gps\">فعال ساختن GPS</string>\n  <string name=\"gps_enable_message\">در دستگاه شما GPS غیرفعال گردید. آیا میخواهید دوباره آنرا فعال کنید؟</string>\n  <string name=\"geo_clear_warning\">ویژگی قبلا ایجاد شده است. آیا می خواهید ویژگی را پاک کنید؟</string>\n  <string name=\"geo_exit_warning\">تغییرات کنار بگذرید و به ODK برگردید؟</string>\n  <string name=\"polygon_validator\">برای ایجاد Polygon باید حداقل 3 امتیاز داشته باشد</string>\n  <string name=\"polyline_validator\">برای ایجاد Polyline باید حداقل 2 امتیاز داشته باشد</string>\n  <string name=\"clear\">پاک کردن</string>\n  <string name=\"placement_mode\">قرار دادن با گشیدن انگشت</string>\n  <string name=\"manual_mode\">ثبت دستی مکان</string>\n  <string name=\"automatic_mode\">ثبت خودکار موقعیت مکانی</string>\n  <string name=\"recording_interval\">فاصله ثبت:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">1%d ثانیه</item>\n    <item quantity=\"other\">%d ثانیه</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">1%d دقیقه</item>\n    <item quantity=\"other\">%d دقیقه</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">دقت مورد نیاز:</string>\n  <string name=\"none\">هیچ کدام</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"collection_status_paused\">امتیازهای وارد شده: %d</string>\n  <string name=\"collection_status_placement\">نقاط وارد شده: %d (برای قرار دادن نقاط ضربه بزنید)</string>\n  <string name=\"collection_status_manual\">امتیازهای وارد شده: %d (ثبت دستی)</string>\n  <string name=\"collection_status_auto_seconds\">امتیازهای وارد شده: %1$d (ثبت هر %2$d ثانیه)</string>\n  <string name=\"collection_status_auto_minutes\">امتیازهای وارد شده: %1$d (ثبت هر %2$d دقیقه)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">امتیازهای وارد شده: %1$d (ثبت هر %2$d ثانیه، دقت %3$d متر)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">امتیازهای وارد شده: %1$d (ثبت هر %2$d ثانیه، دقت %3$d متر)</string>\n  <string name=\"geopoint_no_draggable_instruction\">روی دکمه افزودن نشانگر انگشیتان را بکشید</string>\n  <string name=\"discard\">دور انداختن</string>\n  <string name=\"select_item_count\">%s: %d (%d روی نقشه نشان داده شده است)</string>\n  <string name=\"select_item\">برگزیدن</string>\n  <string name=\"new_item\">گزینه جدید</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">بازنگری پیش نویس</string>\n  <string name=\"view_data\">مشاهده فورم ذخیره شده</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">ردیابی موقعیت مکانی</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">ردیابی مکان…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">نمایش لوکیشن من</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">بزرگنمایی به تناسب همه</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">توقف </string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">حذف نقطه آخر.</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">حذف نقطه آخر.</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">در حال فرستادن فورم</string>\n  <string name=\"noselect_error\">با عرض پوزش، شما هیچ فورمی را انتخاب نکرده اید!</string>\n  <string name=\"no_connection\">اتصال شبکه در دسترس نیست</string>\n  <string name=\"sending_items\">ارسال %1$s از %2$s فورم(های)</string>\n  <string name=\"send_selected_data\">ارسال مورد انتخاب شده</string>\n  <string name=\"server_auth_credentials\">پسورد و یا نام کاربری اشتباه برای سرور: %s</string>\n  <string name=\"server_requires_auth\">سرور نیاز به احراز هویت دارد</string>\n  <string name=\"no_forms_uploaded\">ببخشید، هیچ فورمی آپلود نشده است</string>\n  <string name=\"upload_results\">نتایج آپلود</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">تغییر چشم انداز</string>\n  <string name=\"show_sent_and_unsent_forms\">نمایش فورم های ارسال شده و ارسال ناشده</string>\n  <string name=\"show_unsent_forms\">نمایش فورم های ارسال نشده</string>\n  <string name=\"send_in_progress\">ارسال در پس‌زمینه اجرا می‌شود، لطفاً به زودی دوباره امتحان کنید</string>\n  <string name=\"forms_upload_succeeded\">آپلود فورم ها با موفقیت انجام شد</string>\n  <string name=\"forms_upload_failed\">بارگذاری فورم ها انجام نشد</string>\n  <string name=\"all_uploads_succeeded\">همه آپلودها با موفقیت انجام شد!</string>\n  <string name=\"some_uploads_failed\">%1$s از %2$s بارگیره انجام نشد!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">تازه</string>\n  <string name=\"download\">انتخاب کنید</string>\n  <string name=\"downloading_data\">در حال اتصال به سرور</string>\n  <string name=\"fetching_file\">در حال دریافت %1$s. \\n\\n فورم %2$s از فورم (های) %3$s.</string>\n  <string name=\"form_download_progress\">%1$s. دریافت فایل‌های رسانه: %2$s از %3$s</string>\n  <string name=\"version\">نسخه:</string>\n  <string name=\"form_updates_available\">آپدیت های فورم موجود است</string>\n  <string name=\"newer_version_of_a_form_info\">این یک آپدیت به فورمی است که شما در اختیار دارید. </string>\n  <string name=\"form_update_error\">آپدیت فورم ناموفق بود</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">دانلود فهرست فورم ناموفق بود</string>\n  <string name=\"forms_download_succeeded\">دانلود فورم ها با موفقیت انجام شد</string>\n  <string name=\"forms_download_failed\">دانلود فورم ها انجام نشد</string>\n  <string name=\"all_downloads_succeeded\">همه دانلودها با موفقیت انجام شد!</string>\n  <string name=\"some_downloads_failed\">%1$s از %2$s بارکردن انجام نشد!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">شناسایی: %1$s نسخه: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">اگر همچنان با این مشکل مواجه هستید، آن را به فردی که از شما خواسته است معلومات جمع آوری کنید، گزارش دهید.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect نمی تواند به سرور در %s برسد. آیا URL را درست وارد کردید؟</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect نمی تواند به طور ایمن به سرور در %s متصل شود. آیا URL را درست وارد کردید؟</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">سرور %1$s کد وضعیت %2$s را برگرداند.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">سرور %s پاسخ نادرست ارائه کرد.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">سرور برای این فورم hash ارائه نکرده است.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">هنگام بارکردن این فورم، خطای دیسک در دستگاه روخ داد.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">این فورم دارای URL ارسال نادرست است.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">ذخیره فرم</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">فورم های خالی</string>\n  <string name=\"delete_confirm\">حذف فورم (های) %s</string>\n  <string name=\"delete_file\">حذف مورد انتخاب شده</string>\n  <string name=\"delete_no\">حذف نکنید</string>\n  <string name=\"delete_yes\">حذف فورم ها</string>\n  <string name=\"form_delete_message\">حذف کردن فورم های انتخاب شده</string>\n  <string name=\"file_deleted_ok\">%sفورم(ها) موفقانه حذف گردید!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">تنظیمات پروژه</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">سرور</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, نام کاربری، پسورد</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">نمایش پروژه</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">نام، نشان، رنگ</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">رابط کاربری</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">نقشه ها</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">نقشه پایه، سبک ها، لایه ها</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">از مدیریت</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">جدید سازی خودکار، ارسال خودکار، حذف خودکار</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">شناسایی کاربر و دستگاه</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">نام کاربری، شماره تلفن، شناسایی دستگاه</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">ازمایشی</string>\n  <string name=\"protected_settings\">محافظت شده</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">تنظیمات محافظت شده را باز کنید</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">پسورد را تغییر دهید، کنترل های دسترسی را تنظیم کنید</string>\n  <string name=\"set_admin_password\">پسورد مدیریت را تنظیم کنید</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">مدیریت پروژه</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">تنظیم مجدد، دوباره شروع مردن، حذف کردن</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">کنترل دسترسی</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">رابط کاربری را محدود کنید</string>\n  <string name=\"server_preferences\">تنظیمات سرور</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">آدرس وب سایت </string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">سرور آدرس وب سایت</string>\n  <string name=\"url_error\">ببخشید، آدرس وب سایت اشتباه است!</string>\n  <string name=\"username\">نام کاربر</string>\n  <string name=\"change_username\">اسم کاربری</string>\n  <string name=\"password\">پسورد</string>\n  <string name=\"change_password\">پسورد ODK</string>\n  <string name=\"password_error_whitespace\">فضای خالی پیشرو یا انتهایی در پسورد مجاز نیست</string>\n  <string name=\"username_error_whitespace\">فضای خالی پیشرو یا انتهایی در نام کاربری مجاز نیست</string>\n  <string name=\"app_theme\">ظاهر</string>\n  <string name=\"language\">زبان</string>\n  <string name=\"use_device_language\">از زبان دستگاه استفاده کنید</string>\n  <string name=\"font_size\">اندازه فونت متن</string>\n  <string name=\"change_font_size\">اندازه فونت متن</string>\n  <string name=\"font_size_extra_large\">بسیار بزرگ</string>\n  <string name=\"font_size_large\">بزرگ</string>\n  <string name=\"font_size_medium\">متوسط</string>\n  <string name=\"font_size_small\">کوچک</string>\n  <string name=\"font_size_extra_small\">بسیار کوچک</string>\n  <string name=\"navigation\">جهت یابی</string>\n  <string name=\"swipe_navigation\">از سوایپ های افقی استفاده کنید</string>\n  <string name=\"buttons_navigation\">از دکمه های جلو/عقب استفاده کنید</string>\n  <string name=\"swipe_buttons_navigation\">از سوایپ ها و دکمه ها استفاده کنید</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">بعدی</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">قبلی</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">آپدیت فورم</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">حالت خودکار آپدیت فورم خالی</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">کتابچه راهنمای</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">فقط فورم‌های دانلود شده قبلی</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">دقیقا مطابق با سرور</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">آپدیت خودکار فرکانس </string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">هر پانزده دقیقه بعد</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">هر ساعت</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">هر شش ساعت</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">هر 24 ساعت</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">دانلود خودکار</string>\n  <string name=\"automatic_download_summary\">نسخه های جدید فورم ها را به صورت خودکار دانلود کنید</string>\n  <string name=\"hide_old_form_versions_setting_title\">مخفی کردن نسخه های قدیمی</string>\n  <string name=\"hide_old_form_versions_setting_summary\">فقط جدیدترین نسخه در فورم خالی ظاهر می شود</string>\n  <string name=\"form_submission_category\">ارسال فورم</string>\n  <string name=\"autosend_selector_title\">ارسال خودکار</string>\n  <string name=\"autosend\">ارسال خودکار</string>\n  <string name=\"off\">خاموش</string>\n  <string name=\"wifi_autosend\">تنها وای فای</string>\n  <string name=\"cellular_autosend\">تنها تلفن همراه</string>\n  <string name=\"wifi_cellular_autosend\">وای فای یا تلفن همراه</string>\n  <string name=\"delete_after_send\">بعد از ارسال حذف کنید</string>\n  <string name=\"delete_after_send_summary\">فورم های نهایی و رسانه یی را بعد از ارسال به سرور حذف کنید</string>\n  <string name=\"form_filling_category\">فورم را پر کنید</string>\n  <string name=\"constraint_behavior_title\"> محدودیت پردازش</string>\n  <string name=\"constraint_behavior\">محدودیت رفتار پردازش </string>\n  <string name=\"constraint_behavior_on_swipe\">با کشیدن انگشت به جلو اعتبارسنجی کنید</string>\n  <string name=\"constraint_behavior_on_finalize\">اعتبارسنجی را تا نهایی شدن به تعویق بگذرید</string>\n  <string name=\"high_resolution_title\">ویدیو با وضوح بالا</string>\n  <string name=\"high_resolution_summary\">فعال ساختن ظبط ویدیو با رزولوشن بالا</string>\n  <string name=\"image_size_title\">اندازه تصویر</string>\n  <string name=\"image_size_dialog_title\">حداکثر پیکسل حاشیه بلند عکس</string>\n  <string name=\"image_size_large\">بزرگ (3072 پکسل)</string>\n  <string name=\"image_size_medium\">متوسط (2048 پکسل)</string>\n  <string name=\"image_size_small\">کوچک (1024 پکسل)</string>\n  <string name=\"image_size_very_small\">بسیار کوچک (640 پکسل)</string>\n  <string name=\"image_size_original\">اندازه اصلی از دوربین (فرض کنید)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">برای سوالات راهنمایی نشان دهید</string>\n  <string name=\"guidance_no\">نخیر</string>\n  <string name=\"guidance_yes\">بلی - همیشه نمایش داده شود</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">بلی - فرو پاشیده</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">از برنامه خارجی برای ثبت صدا استفاده کنید</string>\n  <string name=\"form_import_category\">فورم را وارد کنید</string>\n  <string name=\"instance_sync\">فورم ها را در هنگام واردات نهایی کنید</string>\n  <string name=\"instance_sync_summary\">فورم هایی که مستقیماً به فلودر پنهانی اضافه شده اند تأثیر می گذارد.</string>\n  <string name=\"analytics_preferences\">استفاده معلومات</string>\n  <string name=\"analytics\">معلومات استفاده ناشناس را جمع آوری کنید</string>\n  <string name=\"analytics_summary\">استفاده معلومات ناشناس به تیم ODK کمک می‌کند تا اصلاحات و ویژگی‌ها را اولویت‌بندی کند.</string>\n  <string name=\"form_metadata_title\">فورم metadata</string>\n  <string name=\"form_metadata\">فورم metadata</string>\n  <string name=\"form_metadata_summary\">این مقادیر به فورم هایی اضافه می شود که دارای فیلدهای از پیش بار شده نام کاربری، ایمیل و/یا تلفن هستند تا فرد ارسال کننده معلومات را شناسایی کند.</string>\n  <string name=\"user_defined\">تعریف شده توسط کاربر</string>\n  <string name=\"phone_number\">شماره تماس</string>\n  <string name=\"email\">ایمیل آدرس</string>\n  <string name=\"invalid_email_address\">آدرس ایمیل اعتبار ندارد</string>\n  <string name=\"device_defined\">تعریف شده توسط دستگاه</string>\n  <string name=\"device_id\">نمبر شناسایی دستگاه</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">در دسترس نیست</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">تنظیمات را باز کنید</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">تنظیمات باز شد</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">پسورد جدید را داخل کنید</string>\n  <string name=\"admin_password\">پسورد Admin</string>\n  <string name=\"admin_password_changed\">پسورد Admin با موفقیت تغییر کرد</string>\n  <string name=\"admin_password_disabled\">پسورد Admin غیر فعال گردید</string>\n  <string name=\"admin_password_incorrect\">ببخشید، پسورد نادرست است</string>\n  <string name=\"enter_admin_password\">پسورد عبور مدیریت را وارد کنید</string>\n  <string name=\"change_admin_password\">پسورد عبور مدیریت را تغییر دهید</string>\n  <string name=\"show_password\">نشان دادن پسورد</string>\n  <string name=\"found_in_main\">برای پنهان شدن از منوی اصلی، علامت را بردارید</string>\n  <string name=\"found_in_settings\">برای پنهان شدن از تنظیمات محافظت نشده، علامت را بردارید</string>\n  <string name=\"form_entry\">برای پنهان شدن از ورود فورم، علامت را بردارید</string>\n  <string name=\"form_end\">برای پنهان شدن از پایان فرم، علامت را بردارید</string>\n  <string name=\"reset_project_settings_title\">تنظیم مجدد</string>\n  <string name=\"reset_project_settings_subtext\">از میان تنظیمات، فورم‌ها، معلومات را انتخاب کنید</string>\n  <string name=\"delete_project\">حذف شده</string>\n  <string name=\"select_all\">انتخاب همه</string>\n  <string name=\"clear_all\">پاک کردن همه</string>\n  <string name=\"reset_settings\">تمام تنظیمات (تنظیمات داخلی، تنظیمات حفظ شده)</string>\n  <string name=\"reset_saved_forms\">فورم های ذخیره شده ( نمونه ها، پایگاه معلومات نمونه ها)</string>\n  <string name=\"reset_blank_forms\">فورم های خالی ( فورم ها، پایگاه معلومات فورم ها، پایگاه معلومات مجموعه اقلام )</string>\n  <string name=\"reset_layers\">لایه های نقشه (پوشه لایه ها)</string>\n  <string name=\"reset_cache\">بار کردن فورم حافظه پنهان (پوشه کش.)</string>\n  <string name=\"reset_settings_dialog_title\">انتخاب موارد جهت تنظیم مجدد</string>\n  <string name=\"reset_settings_button_reset\">تنظیم مجدد</string>\n  <string name=\"reset_in_progress\">در حال تنظیم مجدد</string>\n  <string name=\"reset_app_state_result\">نتایج تنظیم مجدد</string>\n  <string name=\"reset_app_state_warning\">همه معلومات های انتخاب شده برای همیشه حذف می شوند. لغو وجود ندارد.</string>\n  <string name=\"reset_settings_result\">تمام تنظیمات:: %s</string>\n  <string name=\"reset_saved_forms_result\">فورم های ذخیره شده:: %s</string>\n  <string name=\"reset_blank_forms_result\">قورم های خالی:: %s</string>\n  <string name=\"reset_layers_result\">لایه های نقشه:: %s</string>\n  <string name=\"reset_cache_result\">حافظه پنهان بار کردن فورم :: %s</string>\n  <string name=\"main_menu_settings\">تنظیمات منوی اصلی</string>\n  <string name=\"main_menu_settings_summary\">نمایش یا پنهان کردن دکمه ها</string>\n  <string name=\"user_settings\">تنظیمات کاربر</string>\n  <string name=\"user_setting_summary\">نمایش یا پنهان کردن تنظیمات</string>\n  <string name=\"form_entry_setting\">تنظیمات ورود به فورم</string>\n  <string name=\"form_entry_settings_summary\">نمایش یا پنهان کردن اقدامات</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">حرکت به سمت عقب</string>\n  <string name=\"moving_backwards_disabled_title\">حرکت به عقب غیرفعال است</string>\n  <string name=\"yes\">بلی</string>\n  <string name=\"no\">نخیر</string>\n  <string name=\"moving_backwards_enabled_title\">حرکت به عقب فعال شد</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">ذخیره پیش نویس</string>\n  <string name=\"save_mid_summary\">نماد ذخیره در نوار بالا و دکمه \\\"ذخیره به عنوان پیش نویس\\\" هنگام خروج از فرم</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">در باره</string>\n  <string name=\"tell_your_friends\">ODK Collect را به اشتراک بگذارید</string>\n  <string name=\"tell_your_friends_msg\">آیا همکاران شما هنوز در حال جمع آوری معلومات بر روی کاغذ هستند؟ ODK Collect را با آنها به اشتراک بگذارید.</string>\n  <string name=\"leave_a_review\">نظریه خود را راجع به Play Store بنویسید</string>\n  <string name=\"leave_a_review_msg\"> (امیدوارم) بررسی ما باعث افزایش دید برنامه در  Play Store می شود.</string>\n  <string name=\"odk_website\">به وبسایت ODK مراجعه کنید</string>\n  <string name=\"odk_website_summary\">ODK برای جمع آوری data برای منافع اجتماعی در محیط های چالش برانگیز استفاده می شود.</string>\n  <string name=\"odk_forum\">با انجمن ODK یکجا شوید</string>\n  <string name=\"odk_forum_summary\">برای دریافت کمک، درخواست ویژگی‌ها، مشارکت کد/ترجمه به انجمن بپیوندید!</string>\n  <string name=\"all_open_source_licenses\">منابع باز کتابخانه ها/مجوز ها</string>\n  <string name=\"all_open_source_licenses_msg\">ما روی شانه های غول ها ایستاده ایم!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">با کد QR تنظیم مجدد کنید</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">تمام تنظیمات موجود را جایگزین کنید</string>\n  <string name=\"qr_code\">تنظیم QR کد</string>\n  <string name=\"scan_qr_code_fragment_title\">اسکن کنید</string>\n  <string name=\"view_qr_code_fragment_title\">ًQR کد</string>\n  <string name=\"import_qrcode_sd\">QR کد را وارد شد</string>\n  <string name=\"successfully_imported_settings\">تنظیمات با موفقیت وارد شد</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">به حداکثر محدودیت رقم ها رسیده‌اید: QR کد نمی‌تواند برای همه تنظیمات ایجاد شود</string>\n  <string name=\"share\">اشتراک گذاری</string>\n  <string name=\"turn_on_flashlight\">روشن نمودن چراغ </string>\n  <string name=\"turn_off_flashlight\">خاموش نمودن چراغ</string>\n  <string name=\"qrcode_with_both_passwords\">حاوی اطلاعات حساس:<b> پسورد </b>مدیریت و <b>پسورد سرور</b></string>\n  <string name=\"qrcode_without_passwords\">کد QR حاوی<b> پسورد عبور مدیریت</b> یا <b>سرور</b> نیست. برای تنظیم کردن انگشتات را بکشید</string>\n  <string name=\"qrcode_with_admin_password\">حاوی اطلاعات حساس:<b> پسورد عبور </b>مدیریت</string>\n  <string name=\"qrcode_with_server_password\">حاوی اطلاعات حساس:<b> پسورد عبور </b>سرور</string>\n  <string name=\"server_password\">رمز سرور</string>\n  <string name=\"include_password_dialog\">رمزهای عبور با code یکجاست</string>\n  <string name=\"generate\">تولید کردن</string>\n  <string name=\"invalid_qrcode\">QR کد شامل تنظیمات معتبری نیست</string>\n  <string name=\"qr_code_not_found\">QR کد در تصویر انتخابی یافت نشد</string>\n  <string name=\"corrupt_imported_preferences_error\">تنظیمات فعلی خراب است. از تنظیمات مدیریت پروژه، تنظیمات را بازنشانی کنید یا تنظیمات درست را وارد کنید.</string>\n  <string name=\"settings_with_gd_protocol\">پروژه‌های Google Drive/Sheets دیگر نمی‌توانند ایجاد شوند</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">درست است</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">لغو </string>\n  <string name=\"cancel_loading_form\">لغو</string>\n  <string name=\"cancel_location\">لغو</string>\n  <string name=\"do_not_change\">انصراف</string>\n  <string name=\"canceling\">لغو کردن</string>\n  <string name=\"notification_channel_name\">اطلاعیه های عمومی</string>\n  <string name=\"success\">موفقیت </string>\n  <string name=\"error_occured\">خطا اتفاق افتاده است</string>\n  <string name=\"please_wait\">لطفاً برای چند لحظه منتظر باشید</string>\n  <string name=\"please_wait_long\">لطفاً منتظر باشید: این میتواند چند دقیقه را در بر بگیرد.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">\\\"%s\\\" حذف نشد. لطفا فایل را به صورت دستی حذف کنید و دوباره فورم را دانلود کنید.</string>\n  <string name=\"fs_create_media_folder_error\">folder رسانه \\'%s\\' ایجاد نشد.</string>\n  <string name=\"fs_file_copy_error\">\\\"%1$s\\\" روی \\\"%2$s\\\" کپی نشد. دلیل: %3$s</string>\n  <string name=\"select_odk_shortcut\">میانبر ODK را انتخاب کنید</string>\n  <string name=\"background_audio_help\">این فورم صدا را از میکروفون دستگاه شما ضبط می‌کند. \\n\\n می‌توانید از نشانگر میزان صدا استفاده کنید تا مطمئن شوید میکروفون به اندازه کافی به صدایی که باید ضبط کنید نزدیک است. \\n\\n برای توقف ضبط لطفاً از این فورم خارج شوید. \\n\\ n برای اطلاعات بیشتر، لطفاً با شخصی که از شما خواسته است اطلاعات را جمع آوری کنید، تماس بگیرید.</string>\n  <string name=\"loading\">بارگذاری…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">پروژه ها</string>\n  <string name=\"settings\">تنظیمات</string>\n  <string name=\"add_project\">اضافه کردن پروژه</string>\n  <string name=\"add\">اضافه</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">پروژه تکراری</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">شما قبلاً پروژه ای با این تنظیمات اتصال دارید. آیا می خواهید به پروژه موجود تغییر دهید یا یک پروژه جدید اضافه کنید</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">اضافه کردن پروژه تکراری</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">تغییر به پروژه موجود</string>\n  <string name=\"project_name\">نام پروژه</string>\n  <string name=\"project_icon\">نماد پروژه</string>\n  <string name=\"project_color\">رنگ پروژه</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">به %s تغییر وضعیت داد</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">با استفاده از %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">به %s تغییر دهید</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"or\">یا</string>\n  <string name=\"hex_color\">رنگ Hex</string>\n  <string name=\"invalid_hex_code\">hex code نادرست است</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">جمع‌آوری data در هر کجا</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">تنظیم کردن با QR code</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">جزئیات پروژه را به صورت دستی وارد کنید</string>\n  <string name=\"project_config_tip\">پس از اینکه پروژه خود را اضافه کردید، می توانید آن را در تنظیمات تنظیم کنید</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">هنوز پروژه ای ندارید؟</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">نسخه ی نمایشی را امتحان کنید</string>\n  <string name=\"scan_configuration_qr_code\"> QR کد پیکربندی را اسکن کنید</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">نمایش جزئیات</string>\n  <string name=\"errors\">اشتباه</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">برای باز کردن این فرم، ابتدا باید ODK Collect را باز کنید و به پروژه ای که مربوط آن است بروید.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">او دی کی کلیکت تنظیم نگردیده است، کوشش کنید تا او دی کی کلیکت را باز کنید و آنرا تنظیم کنید. اگر شما بالای میانبر فشار وارد کنید، پس از تنظیم کردن ODK collect شما نیاز پیدا خواهد کرد تا انرا دوباره ایجاد کنید.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">URI شناسایی نشده: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">URI: %s : بی اعتبار</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">فورم پر شده حذف شد!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">ویرایش این پیش نویس امکان پذیر نیست زیرا فرم خالی مربوطه وجود ندارد یا حذف شده است.\\n\\nشناسه فرم: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">شما دو فورم مختلف را باشناسایی و نسخه یکسان دانلود کرده اید. شاید همان فورم بارگذاری شده در زمان های مختلف یا در سرورهای مختلف باشد. در هر صورت باید یکی را حذف کنید.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">تنظیمات را باز کنید</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">آخرین باری که برنامه در حال اجرا بود یک خرابی وجود داشت!</string>\n  <string name=\"cant_start_app\">نمی توان app را راه اندازی کرد!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"learn_more_button_text\">بیشتر بدانید</string>\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">توسعه ابزارها</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">از کار افتاد app</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">اجبار یک استثناء کشف نشده که باعث از کار افتادن برنامه می شود</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"permission_dialog_title\">درباره محدودیت ها</string>\n  <string name=\"permission_dialog_text\">از شما خواسته می شود که به ODK Collect اجازه دسترسی به ویژگی های زیر را بدهید، اگر می خواهید از آنها استفاده کنید، \\\"اجازه\\\" را انتخاب کنید.</string>\n  <string name=\"notifications\">اعلان ها</string>\n  <string name=\"required_for_notifications\">برای نمایش به‌روزرسانی‌ها هنگام بارگیری، به‌روزرسانی و ارسال فرم‌ها ضروری است.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">فرم شما به عنوان پیش نویس ذخیره شد.</string>\n  <string name=\"form_saved\">فرم شما ذخیره شد.</string>\n  <string name=\"form_sending\">در حال ارسال فرم</string>\n  <string name=\"edit_form\">بازنگری</string>\n  <string name=\"view_form\">نمایش</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">آخرین فرم ارسال شده: 1%d ثانیه قبل</item>\n    <item quantity=\"other\">آخرین فرم ارسال شده: 1%d ثانیه قبل</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"one\">آخرین فرم ارسال شده: 1%d دقیقه قبل</item>\n    <item quantity=\"other\">آخرین فرم ارسال شده: 1%d دقیقه قبل</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"one\">آخرین فرم ارسال شده: 1%d ساعت قبل</item>\n    <item quantity=\"other\">آخرین فرم ارسال شده: 1%d ثانیه قبل</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"one\">آخرین فرم ارسال شده: 1%d روز قبل</item>\n    <item quantity=\"other\">آخرین فرم ارسال شده: 1%d روز قبل</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"one\">1%d فرم آماده ارسال است</item>\n    <item quantity=\"other\">1%d فرم آماده ارسال است</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">در نسخه های بعدی، فرم های نهایی شده دیگر قابل ویرایش نخواهند بود. فرم‌ها را به‌عنوان پیش‌نویس ذخیره کنید تا بعداً آن‌ها را ویرایش کنید. \\n\\nمی‌توانید با ضربه زدن روی سه نقطه (⋮) و سپس خطاها را در فرم پیش‌نویس بررسی کنید.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">در نسخه های بعدی، فرم های نهایی شده دیگر قابل ویرایش نخواهند بود.</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">تمام پیش نویس ها را نهایی کنید.</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"one\">آیا می‌خواهید 1%d پیش‌نویس را نهایی کنید؟</item>\n    <item quantity=\"other\">آیا می‌خواهید 1%d پیش‌نویس ها را نهایی کنید؟</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">وقتی همه پیش‌نویس‌ها را نهایی کردید، در «آماده ارسال» قرار می‌گیرند و نمی‌توانید ویرایش کنید. هر پیش‌نویسی که دارای خطا باشد، نهایی نخواهد شد.\\n\\nشما نمی‌توانید این عمل را واگرد کنید.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"one\">موفق! 1%d پیش‌نویس نهایی شد.</item>\n    <item quantity=\"other\">موفق! 1%d پیش‌نویس نهایی شد.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"one\">1%d پیش نویس دارای خطاهایی است که باید قبل از نهایی شدن برطرف شوند.</item>\n    <item quantity=\"other\">1%d پیش نویس دارای خطاهایی است که باید قبل از نهایی شدن برطرف شوند.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">1%d پیش‌نویس نهایی شد. 1%d پیش نویس دارای خطاهایی است که باید قبل از نهایی شدن برطرف شوند.</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">1%d پیش‌نویس نهایی شد. پیش نویس هایی که باقی مانده اند باید به صورت دستی نهایی شوند.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">اشتباه</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">بدون خطا</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">برای پنهان شدن از پیش نویس ها، علامت را بردارید.</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">چیزی برای نمایش در دسترس نیست</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">هیچ چیزی در پیش نویس ها وجود ندارد</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">وقتی به عنوان پیش‌نویس ذخیره می‌کنید، فرم‌ها در اینجا ظاهر می‌شوند</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">چیزی برای ارسال آماده نیست</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">وقتی پیش‌نویس‌ها را نهایی کردید، در اینجا ظاهر می‌شوند</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">چیزی ارسال نشد</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">هنگامی که فرم های نهایی شده را ارسال می کنید، آنها در اینجا ظاهر می شوند</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">دور انداختن</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-fi/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Aloita uusi lomake</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Luonnokset</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Valmiina lähettämään</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Lähetetty</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Lataa lomake</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Poista lomake</string>\n  <string name=\"saved_forms\">Tallennetut lomakkeet</string>\n  <string name=\"version_number\">Versio: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Lisätty\\' EEE dd.MMM.yyyy \\'kello\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Päivitetty\\' EEE dd.MM.yyyy \\'kello\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Tallennettu\\' EEE dd.MM.yyyy \\'kello\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Viimeistelty\\' EEE dd.MM.yyyy \\'kello\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Lähetetty\\' EEE dd.MM.yyyy \\'kello\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Lähettäminen epäonnistunut\\' EEE dd.MM.yyyy \\'kello\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Poistettu\\' EEE dd.MM.yyyy \\'kello\\' HH:mm</string>\n  <string name=\"submission_deleted\">Lähetys poistettu</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Muokattu\\' EEE dd.MM.yyyy \\'kello\\' HH:mm</string>\n  <string name=\"encrypted_form\">Salattu lomake</string>\n  <string name=\"deleted_form\">Poistettu lomake</string>\n  <string name=\"sort_the_list\">Lajittele lista</string>\n  <string name=\"sort_by\">Lajitteluperuste</string>\n  <string name=\"sort_by_name_asc\">Nimi, A-Ö</string>\n  <string name=\"sort_by_name_desc\">Nimi, Ö-A</string>\n  <string name=\"sort_by_date_desc\">Päivämäärä, uusin ensin</string>\n  <string name=\"sort_by_date_asc\">Päivämäärä, vanhin ensin</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Viimeisenä tallennettu ensimmäisenä</string>\n  <string name=\"filter_the_list\">Suodata listaa</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Etsi</string>\n  <string name=\"not_granted_permission\">Toiminto, jota yrität käynnistää, vaatii käyttöluvan jota ei ole myönnetty.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Talletusluvat</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Ilman näitä lupia Collect ei pysty lukemaan lomakemäärittelyitä tai tallentamaan vastauksia. Avaa sovellus uudestaan kun olet valmis antamaan luvat.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Sijaintitiedon käyttöluvat</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Ilman lupaa <b>Tarkka sijainti</b> Collect ei pysty tallentamaan sijaintiasi. Yritä uudestaan kun olet valmis sallimaan sen.</string>\n  <string name=\"camera_runtime_permission_denied_title\">Kameran käyttölupa</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Ilman tätä käyttölupaa Collect ei pysty käyttämään kameraa. Ole hyvä ja yritä uudestaa kun olet valmis myöntämään sen.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Äänen tallentamisen käyttölupa</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Ilman tätä käyttölupaa Collect ei pysty käyttämään mikrofonia. Ole hyvä ja yritä uudestaa kun olet valmis myöntämään sen.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Avaa lomakekartta</string>\n  <string name=\"instance_scan_count\">%1$d lomaketta lisätty.</string>\n  <string name=\"finished_disk_scan\">Skannaus valmis. Kaikki lomakkeet ladattu.</string>\n  <string name=\"xform_parse_error\">XForm jäsennysvirhe %1$s: %2$s puuttuu tai ei kelpaa.</string>\n  <string name=\"loading_form\">Ladataan lomaketta</string>\n  <string name=\"survey_loading_reading_form_message\">Luetaan lomakemäärittelyä…</string>\n  <string name=\"survey_loading_reading_data_message\">Luetaan kyselydataa…</string>\n  <string name=\"survey_loading_reading_csv_message\">Luetaan CSV-tiedostoja…</string>\n  <string name=\"parse_error\">Pahoittelen, lomake ei ole tulkittavissa.</string>\n  <string name=\"loading_form_failed\">Lomaketta ladatessa tapahtui virhe. Ole hyvä ja yritä uudestaan.</string>\n  <string name=\"cannot_edit_completed_form\">Tätä lomaketta ei voi muokata, kun se on merkitty vimeistellyksi. Se saattaa olla salattu.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Tallentamattomat muutokset palautettu tallennuspisteestä!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Vaihda kieltä</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Siirry kehotteeseen</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Tallenna luonnoksena</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Alkuun</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Loppuun</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Ylös</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <string name=\"edit_finalized_form\">Muokkaa viimeisteltyä lomaketta</string>\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Ryhmä</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Toistettava ryhmä</string>\n  <string name=\"exit\">Poistu</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Lisätäänkö \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Lisätään</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"do_not_add_repeat\">Älä lisää</string>\n  <string name=\"add_another_menu\">Lisää toinen</string>\n  <string name=\"add_another_question\">Lisää vielä yksi?</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Kehotteen muokkaus</string>\n  <string name=\"clearanswer_confirm\">Poistetaanko vastaus kysymykseen \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Poista vastaus</string>\n  <string name=\"clear_answer_ask\">Poistetaanko tämä vastaus?</string>\n  <string name=\"clear_answer_no\">Peruuta</string>\n  <string name=\"discard_answer\">Poista vastaus</string>\n  <string name=\"discard_group\">Poista ryhmä</string>\n  <string name=\"delete_repeat\">Poista ryhmä</string>\n  <string name=\"delete_repeat_ask\">Poistetaanko tämä ryhmä?</string>\n  <string name=\"delete_repeat_confirm\">Poistetaanko ryhmä \\\"%s\\\" ja kaikki sen alaryhmät?</string>\n  <string name=\"delete_repeat_no\">Peruuta</string>\n  <string name=\"invalid_answer_error\">Pahoittelut, vastaus ei kelpaa!</string>\n  <string name=\"required_answer_error\">Pahoittelut, tämä vastaus on pakollinen!</string>\n  <string name=\"success_form_validation\">Onnistui! Lomakkeestasi ei löytynyt virheitä.</string>\n  <string name=\"invalid_space_in_answer_singular\">Varoitus: taustalla olevassa arvossa %s on välilyöntejä</string>\n  <string name=\"invalid_space_in_answer_plural\">Varoitus: taustalla olevissa arvoissa %s on välilyöntejä</string>\n  <string name=\"too_complex_form\">Tämä lomake on liian monimutkainen tälle laitteelle. Yritä lausekkeiden yksinkertaistamista tai pyydä apua verkon keskusteluryhmästä.</string>\n  <string name=\"survey_internal_error\">Sisäinen virhe: siirtyminen kehotteeseen epäonnistui.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Olet lomakkeen %s lopussa.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <string name=\"form_editing_disabled_after_finalizing\">Muokkauksia ei voi tehdä viimeistelyn jälkeen.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <string name=\"form_editing_disabled_after_sending\">Muokkauksia ei voi tehdä lähettämisen jälkeen.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_finalizing\">Muokkauksia voi tehdä viimeistelyn jälkeen.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_sending\">Muokkauksia voi tehdä lähettämisen jälkeen.</string>\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Jos sinun tarvitsee tehdä muutoksia lomakkeeseesi, \\\"Tallenna luonnoksena\\\" kunnes olet valmis lähettämään.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <string name=\"form_editing_enabled_after_finalizing_hint\">Tehdäksesi muutoksia kun olet viimeistellyt lomakkeesi, mene kohtaan \\\"Valmis lähetettäväksi\\\", avaa lomake ja täppää muokkauskuvaketta.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <string name=\"form_editing_enabled_after_sending_hint\">Tehdäksesi muutoksia kun olet lähettänyt lomakkeesi, mene kohtaan \\\"Lähetetty\\\", avaa lomake ja täppää muokkauskuvaketta.</string>\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Tallenna luonnoksena</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Viimeistele</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Lähetä</string>\n  <string name=\"data_saved_error\">Pahoittelut, lomakkeen tallennus epäonnistui!</string>\n  <string name=\"data_saved_ok\">Lomake tallennettu!</string>\n  <string name=\"save_point_error\">Virhe tallentaessa palautuspistettä: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Hylkää lomake</string>\n  <string name=\"discard_changes\">Hylkää muutokset</string>\n  <string name=\"keep_changes\">Tallenna muutokset</string>\n  <string name=\"keep_editing\">Jatka muokkausta</string>\n  <string name=\"quit_form_title\">Tallennetaanko lomake?</string>\n  <string name=\"save_explanation\">Voit tallentaa tämän lomakkeen ja löytää sen milloin tahansa luonnostesi joukosta.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'Tämä lomake tallennettiin viimeksi\\' EEE dd.MM.yyyy \\'kello\\' HH:mm\\'. Tallenna luonnos säilyttääksesi muutokset.\\'</string>\n  <string name=\"quit_form_continue_title\">Jatketaanko muokkausta?</string>\n  <string name=\"discard_form_warning\">Jos hylkäät lomakkeen, tulet menettämään kaikki tähän mennessä tekemäsi muutokset.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'Tämä lomake tallennettiin viimeksi\\' EEE dd.MM.yyyy \\'kello\\' HH:mm\\'. Jos hylkäät muutokset, tulet menettämään kaikki tähän mennessä tekemäsi muutokset.\\'</string>\n  <string name=\"saving_form\">Tallentamassa lomaketta</string>\n  <string name=\"survey_saving_validating_message\">Tarkistetaan vastauksia…</string>\n  <string name=\"survey_saving_collecting_message\">Kerätää dataa…</string>\n  <string name=\"survey_saving_saving_message\">Tallennetaan SD-kortille…</string>\n  <string name=\"survey_saving_finalizing_message\">Viimeistellään SD-kortille…</string>\n  <string name=\"survey_saving_encrypting_message\">Salataan dataa…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Tällä instanssilla ei ole täsmälleen yhtä tietuetta!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Tätä jr_form_id:tä ei vastaa täsmälleen yksi tyhjä lomake.</string>\n  <string name=\"no_form_id_specified\">FormId määrittämättä???</string>\n  <string name=\"phone_does_not_support_rsa\">Laite ei tue RSA-salausta.</string>\n  <string name=\"invalid_rsa_public_key\">Kelvoton julkinen RSA-avain.</string>\n  <string name=\"encryption_error_message\">%s Lomaketta ei ole tallennettu viimeisteltynä.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">Lomaketta ei voi käyttää.</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Äänityksen aloittaminen ei onnistunut.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Mikrofoni on jo käytössä.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">Mediatiedostoa ei voitu liittää lomakkeeseen, mutta se on saatavilla sijainnissa %1$s</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Taustaäänitys estetty. Käynnistä valikossa %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Arvojen päivittäminen epäonnistui. Tämä johtuu yleensä virheellisestä laskutoimitusten käytöstä lomakkeen määrittelyssä.\\n\\nJos ongelma esiintyy toistuvasti, ilmoita siitä henkilölle, joka pyysi sinua keräämään dataa.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Käynnistä</string>\n  <string name=\"activity_not_found\">Ei toimintoa tunnuksella: %s</string>\n  <string name=\"no_app\">Toivottu sovellus puuttuu. Ole hyvä ja syötä lukema manuaalisesti.</string>\n  <string name=\"null_intent_value\">Ulkoinen sovellus ei toimittanut odotettua tietoa.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">Tulosta</string>\n  <string name=\"open_url\">Avaa URL</string>\n  <string name=\"capture_osm\">Käynnistä OpenMapKit</string>\n  <string name=\"recapture_osm\">Uusi OSM-tägi</string>\n  <string name=\"edited_osm_file\">Muokattu OSM XML -tiedosto:</string>\n  <string name=\"invalid_osm_data\">Jokin meni pieleen. Kelvollista OSM-dataa ei saatu.</string>\n  <string name=\"alert\">Huomio</string>\n  <string name=\"install_openmapkit\">Ole hyvä ja asenna OpenMapKit!</string>\n  <string name=\"get_barcode\">Hae viivakoodi</string>\n  <string name=\"replace_barcode\">Korvaa viivakoodi</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Aseta viivakoodi suorakaiteen sisään</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"rotate_device\">Laitteen kääntäminen</string>\n  <string name=\"open_file\">Avaa tiedosto</string>\n  <string name=\"rank_items\">Järjestä</string>\n  <string name=\"number_picker_title\">Numerovalitsin</string>\n  <string name=\"invalid_range_widget\">Tällä widgetillä on väärät asetukset!</string>\n  <string name=\"svg_file_does_not_exist\">SVG-tiedosto puuttuu!</string>\n  <string name=\"capture_image\">Ota kuva</string>\n  <string name=\"choose_image\">Valitse kuva</string>\n  <string name=\"selected_invalid_image\">Valittu tiedosto ei ole kelvollinen kuva</string>\n  <string name=\"take_picture_instruction\">Napsauta näyttöä ottaaksesi kuvan</string>\n  <string name=\"error_front_camera_unavailable\">Etukamera ei ole käytettävissä tällä laitteella</string>\n  <string name=\"camera_error\">Virhe ottaessa kuvaa</string>\n  <string name=\"camera_failed_to_initialize\">Kameran alustaminen ei onnistunut!</string>\n  <string name=\"annotate_image\">Lisää merkinnät kuvaan</string>\n  <string name=\"gif_not_supported\">Gif-tiedostoja ei tueta</string>\n  <string name=\"invalid_file_type\">Sovellus ilmoitti epäkelvon tiedostotyypin</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_title\">Tallennetaanko muutokset?</string>\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_explanation\">Haluatko tallentaa muutokset ennen lopettamista?</string>\n  <string name=\"save_and_close\">Tallenna ja sulje</string>\n  <string name=\"signature_capture\">Allekirjoituksen kuvankaappaus</string>\n  <string name=\"sign_button\">Tallenna allekirjoitus</string>\n  <string name=\"markup_image\">Kommentoi kuvaa</string>\n  <string name=\"draw_image\">Luonnostele kuva</string>\n  <string name=\"reset_image\">Nollaus (Reset)</string>\n  <string name=\"set_color\">Aseta väri</string>\n  <string name=\"capture_video\">Tallenna video</string>\n  <string name=\"choose_video\">Valitse video</string>\n  <string name=\"play_video\">Näytä video</string>\n  <string name=\"choose_file\">Valitse tiedosto</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Poista</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Äänitä</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Valitse ääni</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Tauko</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Jatka</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">Äänen tallennus: päällä</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">Äänen tallennus: pois päältä</string>\n  <string name=\"background_audio_permission_explanation\">Tämä lomake tallentaa taustalla ääntä. Sinun pitää antaa lupa käyttää mikrofonia. Jos et anna lupaa, et voi avata lomaketta.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Tämä lomake pyytää lupaa tallentaa taustalla ääntä. Kytkeminen pois lopettaa tallennuksen ja poistaa olemassa olevan äänityksen. Haluatko varmasti tehdä näin?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Poista taustaäänitys käytöstä</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">Äänitys ei ala heti. Sinun täytyy avata lomake uudelleen tallentaaksesi ääntä.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Poistetaanko tiedosto?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">Tiedostoa ei voida palauttaa poistamisen jälkeen. Haluatko varmasti jatkaa?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Pysäytä</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Äänitetään…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Äänen tallennus</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Soita äänitiedosto</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Tallennetaan tiedostoa</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Pysäytä tallennus ennen poistumista tältä näytöltä.</string>\n  <string name=\"error_attaching_binary_file\">Virhe liittäessä mediaa %s </string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Valitse arvo</string>\n  <string name=\"select_answer\">Valitse vastaus</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Muokkaa arvoa</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Ei valittua arvoa</string>\n  <string name=\"selected_title\">Valittu:</string>\n  <string name=\"trigger\">OK. Ole hyvä ja jatka.</string>\n  <string name=\"ext_file_no_data_error\">Tiedostossa ei ole tietoja!</string>\n  <string name=\"ext_conflicting_columns_error\">Sarakkeet %s täsmäävät!</string>\n  <string name=\"ext_sortBy_numeric_error\">Järjestyssarakkeen kuuluu sisältää vain numeroarvoja. Ongelma-arvo \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Syntaksivirhe funktiossa search(): funktiolle annettava 1, 4 tai 6 argumenttia.</string>\n  <string name=\"ext_search_wrong_function_error\">Syntaksivirhe funktiossa search(): tunnistamaton funktio \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Syntaksivirhe funktiossa search(): \\'%s\\' ei evaluoitu funktiona.</string>\n  <string name=\"ext_search_return_error\">Etsimistoiminto palautti objektin tyyppiä \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Ei tuotu dataa lähteestä %1$s. Syy: %2$s</string>\n  <string name=\"ext_import_progress_message\">Esiladataan dataa lähteestä \\'%1$s\\', odota… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Datan lukeminen peruttu!</string>\n  <string name=\"ext_import_finalizing_message\">Viimeistellään esiladattua dataa…</string>\n  <string name=\"ext_import_completed_message\">Datan lukeminen valmis!</string>\n  <string name=\"ext_not_initialized_error\"> ExternalDataManager alustamatta.</string>\n  <string name=\"ext_import_csv_missing_error\">Lomakkeen %1$s tarvitsemaa ulkoista dataa ei ole tuotu. Ehkä tiedoston %2$s.csv tuominen lomakkeen ohessa unohtui?</string>\n  <string name=\"ext_search_generic_error\">Syntaksivirhe funktiossa search(): %s.</string>\n  <string name=\"ext_assign_value_error\">Arvoa ei voida asettaa kohdassa \\'%s\\'.</string>\n  <string name=\"parser_exception\">XPathParser-virhe: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Tallenna suuntima</string>\n  <string name=\"replace_bearing\">Korvaa suuntima</string>\n  <string name=\"getting_bearing\">Ladataan suuntimaa</string>\n  <string name=\"accept_bearing\">Tallenna suuntima</string>\n  <string name=\"bearing\">Suuntima: %.3f</string>\n  <string name=\"direction\">Suunta: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Suuntimaa ei voida hakea: laitteesta puuttuu kiihtyvyysanturi, magneettikenttäanturi tai molemmat.</string>\n  <string name=\"select_date\">Valitse päivämäärä</string>\n  <string name=\"select_time\">Valitse aika</string>\n  <string name=\"no_date_selected\">Päivämäärä valitsematta</string>\n  <string name=\"no_time_selected\">Aika valitsematta</string>\n  <string name=\"ethiopian_month_1\">Mäskäräm</string>\n  <string name=\"ethiopian_month_2\">Ṭəqəmt</string>\n  <string name=\"ethiopian_month_3\">Ḫədar</string>\n  <string name=\"ethiopian_month_4\">Taḫśaś</string>\n  <string name=\"ethiopian_month_5\">Ṭərr</string>\n  <string name=\"ethiopian_month_6\">Yäkatit</string>\n  <string name=\"ethiopian_month_7\">Mägabit</string>\n  <string name=\"ethiopian_month_8\">Miyazya</string>\n  <string name=\"ethiopian_month_9\">Gənbo</string>\n  <string name=\"ethiopian_month_10\">Säne</string>\n  <string name=\"ethiopian_month_11\">Ḥamle</string>\n  <string name=\"ethiopian_month_12\">Nähase</string>\n  <string name=\"ethiopian_month_13\">Ṗagʷəmen</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">muhárram</string>\n  <string name=\"islamic_month_2\">sáfar</string>\n  <string name=\"islamic_month_3\">rabi’ al-áwwal</string>\n  <string name=\"islamic_month_4\">rabi’ al-ákhir</string>\n  <string name=\"islamic_month_5\">džumada-l-úla</string>\n  <string name=\"islamic_month_6\">džumada-l-ákhira</string>\n  <string name=\"islamic_month_7\">radžab</string>\n  <string name=\"islamic_month_8\">ša’bán</string>\n  <string name=\"islamic_month_9\">ramadán</string>\n  <string name=\"islamic_month_10\">šawwal</string>\n  <string name=\"islamic_month_11\">dhu-l-qada</string>\n  <string name=\"islamic_month_12\">dhu-l-hiddža</string>\n  <string name=\"persian_month_1\">Farvardīn</string>\n  <string name=\"persian_month_2\">Ordībehešt</string>\n  <string name=\"persian_month_3\">Khordād</string>\n  <string name=\"persian_month_4\">Tīr</string>\n  <string name=\"persian_month_5\">A-Mordād</string>\n  <string name=\"persian_month_6\">Shahrīvar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <string name=\"buddhist_month_1\">tammikuu</string>\n  <!--2nd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_2\">helmikuu</string>\n  <!--3rd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_3\">maaliskuu</string>\n  <!--4th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_4\">huhtikuu</string>\n  <!--5th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_5\">toukokuu</string>\n  <!--6th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_6\">kesäkuu</string>\n  <!--7th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_7\">heinäkuu</string>\n  <!--8th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_8\">elokuu</string>\n  <!--9th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_9\">syyskuu</string>\n  <!--10th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_10\">lokakuu</string>\n  <!--11th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_11\">marraskuu</string>\n  <!--12th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_12\">joulukuu</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Tiedosto %s ei kelpaa.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Tiedostoa %s ei löydy.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Syötä käyttäjätunnus</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Käyttäjätunnus</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Tämä lomake vaatii tunnistautumisen</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Sulje</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Muutosten selitys</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Selitys</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Tämän lomakkeen muutoksille tarvitaan selitys</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Tallenna</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">Valitse paikka</string>\n  <string name=\"choices\">Valinnan</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map topografinen</string>\n  <string name=\"openmap_usgs_sat\">USGS National Map hybridi</string>\n  <string name=\"openmap_usgs_img\">USGS National Map kuvat</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Peruskartta</string>\n  <string name=\"basemap_source\">Lähde</string>\n  <string name=\"basemap_source_unavailable\">Pahoittelut, %s pohjakartat eivät ole käytettävissä tällä laitteella. Ole hyvä ja valitse toinen pohjakartta kohdassa Asetukset &gt; Kartat.</string>\n  <string name=\"map_style_label\">%s kartan tyyli</string>\n  <string name=\"streets\">Kadut</string>\n  <string name=\"terrain\">Maasto</string>\n  <string name=\"hybrid\">Hybridi</string>\n  <string name=\"satellite\">Satelliitti</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Vaalea</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Tumma</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Ulkoilma</string>\n  <string name=\"topographic\">Topografinen</string>\n  <string name=\"google_play_services_error_occured\">Google Maps -palveluun ei saada yhteyttä. Onko Google Play Services asennettu?</string>\n  <string name=\"carto_map_style_positron\">Positroni</string>\n  <string name=\"carto_map_style_dark_matter\">Pimeä aine</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">Offlinetasot</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">Kerros</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">Valitse offlinetaso</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">Valitse offline-taso kaikkiin karttoihin tässä projektissa. Voit lisätä vaihtoehtoja listaan valitsemalla MBTile-tiedoston laitteellasi tai poistaa aikaisemmin lisättyjä tasoja listalta.</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">Opi lisää MBTile-tiedostojen lisäämisestä.</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">Lisää tasoja</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">Poista taso</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <plurals name=\"non_mbtiles_files_selected_title\">\n    <item quantity=\"one\">%d tasoa ei voi lisätä</item>\n    <item quantity=\"other\">%d tasoa ei voi lisätä</item>\n  </plurals>\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">Valitsemasi tiedostot eivät ole MBTiles-tiedostoja. Voit ainoastaan lisätä MBTiles-tiedostoja.</string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">Jotkut valitsemistasi tiedostoista eivät ole MBTiles-tiedostoja. Voit ainoastaan lisätä MBTiles-tiedostoja.</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">Tasot</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">Valitse tasojen saatavuus</string>\n  <string name=\"select_layer_access_subtitle\">Haluatko tasojen olevan käytettävissä kaikissa projekteissa vai ainoastaan nykyisessä?</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">Kaikissa projekteissa</string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">Vain tässä projektissa</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">Haluatko varmasti poistaa offline-tason %1$s?</string>\n  <string name=\"record_geopoint\">Tallenna piste</string>\n  <string name=\"location_accuracy\">Tarkkuus: %1$s</string>\n  <string name=\"location_accuracy_unacceptable\">⚠️ Tarkkuus: %1$s (ei hyväksyttävä)</string>\n  <string name=\"provider_disabled_error\">Pahoittelut, sijaintitiedot eivät ole käytössä!</string>\n  <string name=\"gps_result\">Leveysasteet: %1$s\\nPituusasteet: %2$s\\nKorkeus: %3$s m\\nTarkkuus: %4$s m</string>\n  <string name=\"google_play_services_not_available\">Tämä lomake haluaa seurata sijaintiasi mutta Google Play Services ei ole käytettävissä.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Tämä lomake haluaa seurata sijaintiasi mutta sijaintipalvelut (location providers) eivät ole käytettävissä. Ole hyvä ja aktivoi ne Androidin asetuksissa.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Asetuksiin</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">Sijainnin seuranta: päällä</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">Sijainnin seuranta: pois päältä</string>\n  <string name=\"background_location_disabled\">Tämä lomake haluaa seurata sijaintiasi, mutta seuranta on poissa päältä. Ole hyvä ja laita se päälle valikossa %1$s yllä.</string>\n  <string name=\"background_location_enabled\">Tämä lomake seuraa sijaintiasi. Voit laittaa seurannan pois päältä valikossa %1$s yllä.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">Tarkista virheiden varalta</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">Kulunut aika: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">Piste tallennetaan tarkkuudella %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">Satelliitteja: %1$s</string>\n  <string name=\"waiting_for_location\">Haetaan sijaintia, ole hyvä ja odota.</string>\n  <string name=\"poor_accuracy\">Heikko tarkkuus, ole hyvä ja odota.</string>\n  <string name=\"unacceptable_accuracy\">Kelpaamaton tarkkuus, ole hyvä ja odota.</string>\n  <string name=\"improving_accuracy\">Parannetaan tarkkuutta, ole hyvä ja odota.</string>\n  <string name=\"accuracy_status_title\">Sijainnin hakeminen</string>\n  <!--The accuracy of a location fix in meters-->\n  <string name=\"accuracy_m\">%1$s m</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">W %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Syöttötapa</string>\n  <string name=\"start\">Aloita</string>\n  <string name=\"enable_gps\">Ota GPS käyttöön</string>\n  <string name=\"gps_enable_message\">GPS ei ole päällä laitteessasi. Haluaisitko kytkeä sen päälle?</string>\n  <string name=\"geo_clear_warning\">Ominaisuus on jo luotu. Haluatko tyhjentää ominaisuuden?</string>\n  <string name=\"geo_exit_warning\">Hylätäänkö muutokset ja palataan ODK:hon?</string>\n  <string name=\"polygon_validator\">Monikulmion luomiseen tarvitaan vähintään 3 pistettä</string>\n  <string name=\"polyline_validator\">Polyline-kuvion luomiseen tarvitaan vähintään 2 pistettä</string>\n  <string name=\"clear\">Tyhjennä</string>\n  <string name=\"placement_mode\">Sijoittaminen napsauttamalla</string>\n  <string name=\"manual_mode\">Manuaalinen sijainnin tallennus</string>\n  <string name=\"automatic_mode\">Automaattinen sijainnin tallennus</string>\n  <string name=\"recording_interval\">Tallennusväli:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d sekunti</item>\n    <item quantity=\"other\">%d sekuntia</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d minuutti</item>\n    <item quantity=\"other\">%d minuuttia</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Tarkkuusvaatimus:</string>\n  <string name=\"none\">Ei mitään</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d metri</item>\n    <item quantity=\"other\">%d metriä</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Syötettyjä pisteitä: %d</string>\n  <string name=\"collection_status_placement\">Syötettyjä pisteitä: %d (napsauta sijoittaaksesi pisteitä)</string>\n  <string name=\"collection_status_manual\">Syötettyjä pisteitä: %d (manuaalinen tallennus)</string>\n  <string name=\"collection_status_auto_seconds\">Syötettyjä pisteitä: %1$d (tallennus joka %2$d s)</string>\n  <string name=\"collection_status_auto_minutes\">Tallennettuja pisteitä: %1$d (tallennus joka %2$d min)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Tallennettuja pisteitä: %1$d (tallennus joka %2$d s, tarkkuus %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Tallennettuja pisteitä: %1$d (tallennus joka %2$d min, tarkkuus %3$d m)</string>\n  <string name=\"geopoint_instruction\">Paina pitkään asettaaksesi merkin tai näpäytä merkkinäppäintä</string>\n  <string name=\"geopoint_no_draggable_instruction\">Paina lisää merkki -näppäintä.</string>\n  <string name=\"discard\">Hylkää</string>\n  <string name=\"select_item_count\">%s: %d (%d näytetty kartalla)</string>\n  <string name=\"select_item\">Valitse</string>\n  <string name=\"new_item\">Uusi kohde</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">Muokkaa luonnosta</string>\n  <string name=\"view_data\">Tallennetun lomakkeen tarkastelu</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">Sijainnin seuranta</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">Sijaintia seurataan…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Näytä sijaintini</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">Suurenna sisällyttämään kaikki</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Tauko</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">Poista viimeinen sijainti</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <string name=\"error_fixed\">✅ Virhe korjattu</string>\n  <!--Description for button that explains how to modify the map-->\n  <string name=\"show_how_to_modify_map\">Miten karttaa muokataan</string>\n  <!--Title for instructions on how to modify map features-->\n  <string name=\"how_to_modify_map\">Miten karttaa muokataan</string>\n  <!--Instruction in a list on how to move a point-->\n  <string name=\"long_press_to_move_point_info_item\">Pitkä painallus pisteen siirtämiseksi</string>\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">Poista viimeinen sijainti</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <string name=\"delete_shape_to_start_over_info_item\">Poista kuvio aloittaaksesi alusta</string>\n  <!--Instruction in a list on how to add a new point-->\n  <string name=\"add_point_info_item\">Lisää piste</string>\n  <!--Instruction in a list on how to tap to add a point-->\n  <string name=\"tap_to_add_a_point_info_item\">Täppää lisätäksesi pisteen</string>\n  <!--Instruction in a list on how to delete the entire shape-->\n  <string name=\"delete_entire_shape_info_item\">Poista koko kuvio</string>\n  <!--Button text to close the instructions dialog-->\n  <string name=\"done\">Valmis</string>\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <string name=\"how_to_modify\">Muokkausohje</string>\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Lähetetään lomaketta.</string>\n  <string name=\"noselect_error\">Pahoittelen, et ole valinnut yhtään lomaketta!</string>\n  <string name=\"no_connection\">Ei verkkoyhteyttä käytettävissä</string>\n  <string name=\"sending_items\">Lähetetään %1$s / %2$s lomaketta.</string>\n  <string name=\"send_selected_data\">Lähetä valitut</string>\n  <string name=\"server_auth_credentials\">Väärä käyttäjätunnus tai salasana palvelimelle: %s</string>\n  <string name=\"server_requires_auth\">Palvelin vaatii tunnistautumista</string>\n  <string name=\"no_forms_uploaded\">Pahoittelut, lomaketta ei siirretty.</string>\n  <string name=\"upload_results\">Siirrä tulokset.</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Vaihda näkymää</string>\n  <string name=\"show_sent_and_unsent_forms\">Näytä lähetetyt ja lähettämättömät lomakkeet</string>\n  <string name=\"show_unsent_forms\">Näytä lähettämättömät lomakkeet</string>\n  <string name=\"send_in_progress\">Taustalähetys käynnissä, yritä hetken kuluttua uudestaan</string>\n  <string name=\"forms_upload_succeeded\">Lomakkeiden lähetys onnistui</string>\n  <string name=\"forms_upload_failed\">Lomakkeiden lähetys epäonnistui</string>\n  <string name=\"all_uploads_succeeded\">Kaikki lähetykset onnistuivat!</string>\n  <string name=\"some_uploads_failed\">%1$s latausta epäonnistui (yhteensä %2$s)!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Päivitä</string>\n  <string name=\"download\">Hae valittu</string>\n  <string name=\"downloading_data\">Yhdistetään palvelimeen</string>\n  <string name=\"fetching_file\">Haetaan %1$s.\\n\\nLomake %2$s / %3$s.</string>\n  <string name=\"form_download_progress\">%1$s. Haetaan mediatiedostoja: %2$s / %3$s</string>\n  <string name=\"version\">Versio:</string>\n  <string name=\"form_updates_available\">Lomakepäivityksiä saatavilla</string>\n  <string name=\"newer_version_of_a_form_info\">Tämä on päivitys lomakkeeseen, joka sinulla on</string>\n  <string name=\"form_update_error\">Lomakkeen päivitys epäonnistui</string>\n  <string name=\"form_update_error_massage\">Avaa ODK Collect yrittääksesi uudestaan</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"form_update_notification_title\">Lomakkeita synkronoidaan</string>\n  <string name=\"load_remote_form_error\">Lomakeluettelon lataaminen epäonnistui</string>\n  <string name=\"forms_download_succeeded\">Lomakkeiden hakeminen onnistui</string>\n  <string name=\"forms_download_failed\">Lomakkeiden hakeminen epäonnistui</string>\n  <string name=\"all_downloads_succeeded\">Kaikki lataukset onnistuivat!</string>\n  <string name=\"some_downloads_failed\">%1$s latausta epäonnistui (yhteensä %2$s)!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Versio: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Jos ongelma jatkuu, ilmoita siitä tiedonkeruusta vastaavalle.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect ei tavoita palvelinta osoitteessa %s. Syötitkö URL:in oikein?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect ei saa turvattua yhteyttä palvelimeen %s. Syötitkö URL:in oikein?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">Palvelin %1$s vastasi statuskoodilla %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">Palvelin %s antoi epäkelvon vastauksen.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">Palvelin ei antanut tiivistettä tälle lomakkeelle.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_parsing_error\">Tätä lomaketta ei voida käsitellä:</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">Levyvirhe laitteella ladatessa tätä lomaketta.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">Tällä lomakkeella on epäkelpo lähetys-URL.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Tallennetut lomakkeet</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Tyhjät lomakkeet</string>\n  <string name=\"delete_confirm\">Poista %s lomake/lomakkeet?</string>\n  <string name=\"delete_file\">Poista valittu</string>\n  <string name=\"delete_no\">Älä poista</string>\n  <string name=\"delete_yes\">Poista lomakkeita</string>\n  <string name=\"form_delete_message\">Valittuja lomakkeita poistetaan</string>\n  <string name=\"file_deleted_ok\">%s lomakkeen poistaminen onnistui!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Projektin asetukset</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Palvelin</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, käyttäjänimi, tunnussana</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Projektin näyttöasetukset</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Nimi, kuvake, väri</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Käyttöliittymä</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_interface_settings_subtext\">Sovelluksen kieli, kirjasinkoko, navigointi</string>\n  <!--Title of a settings section-->\n  <string name=\"maps\">Kartat</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Kartasto, tyylit, tasot</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Lomakkeen hallinta</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">Automaattinen päivitys, automaattinen lähetys, automaattinen poistaminen</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Käyttäjän ja laitteen tunnistaminen</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">Käyttäjänimi, puhelinnumero, laitetunnus</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Kokeellinen</string>\n  <string name=\"protected_settings\">Suojattu</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">Avaa suojattujen asetusten lukitus</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">Salasanan vaihtaminen, pääsyvalvonnan asettaminen</string>\n  <string name=\"set_admin_password\">Aseta pääkäyttäjän salasana</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Projektihallinta</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">Uudelleenkonfigurointi, palauttaminen, poistaminen</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Pääsynvalvonta</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">Käyttöliittymän rajoittaminen</string>\n  <string name=\"server_preferences\">Palvelinasetukset</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Palvelimen URL</string>\n  <string name=\"url_error\">Pahoittelut, URL ei kelpaa! </string>\n  <string name=\"username\">Käyttäjätunnus</string>\n  <string name=\"change_username\">ODK käyttäjätunnus</string>\n  <string name=\"password\">Salasana</string>\n  <string name=\"change_password\">ODK salasana</string>\n  <string name=\"password_error_whitespace\">Salasanan alussa tai lopussa ei saa olla välilyöntimerkkejä</string>\n  <string name=\"username_error_whitespace\">Käyttäjänimen alussa tai lopussa ei saa olla välilyöntimerkkejä</string>\n  <string name=\"app_theme\">Teema</string>\n  <string name=\"language\">Kieli</string>\n  <string name=\"use_device_language\">Käytä laitteen oletuskieltä</string>\n  <string name=\"font_size\">Tekstin koko</string>\n  <string name=\"change_font_size\">Tekstin koko</string>\n  <string name=\"font_size_extra_large\">Erittäin suuri</string>\n  <string name=\"font_size_large\">Suuri</string>\n  <string name=\"font_size_medium\">Keskikokoinen</string>\n  <string name=\"font_size_small\">Pieni</string>\n  <string name=\"font_size_extra_small\">Erittäin pieni</string>\n  <string name=\"navigation\">Navigointi</string>\n  <string name=\"swipe_navigation\">Käytä vaakapyyhkäisyjä</string>\n  <string name=\"buttons_navigation\">Käytä painikkeita eteenpäin/taaksepäin</string>\n  <string name=\"swipe_buttons_navigation\">Käytä pyyhkäisyjä ja painikkeita</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Seuraava</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Takaisin</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Lomakepäivitys</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Tyhjien lomakkeiden päivitystila</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Manuaalinen</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Vain aikaisemmin ladatut lomakkeet</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Palvelimen tarkka täsmääminen</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Automaattisen päivityksen väli</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Vartin välein</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Tunnin välein</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Kuuden tunnin välein</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Vuorokauden välein</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Automaattinen haku</string>\n  <string name=\"automatic_download_summary\">Hae automaattisesti lomakkeiden päivitettyjä versioita</string>\n  <string name=\"hide_old_form_versions_setting_title\">Piilota vanhat lomakeversiot</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Vain uusin versio tulee näkymään \\\"Täytä tyhjä lomake\\\" -näkymässä.</string>\n  <string name=\"form_submission_category\">Lomakkeen lähetys</string>\n  <string name=\"autosend_selector_title\">Automaattinen lähetys</string>\n  <string name=\"autosend\">Automaattinen lähetys</string>\n  <string name=\"off\">Pois</string>\n  <string name=\"wifi_autosend\">Vain Wifi</string>\n  <string name=\"cellular_autosend\">Vain puhelinverkko</string>\n  <string name=\"wifi_cellular_autosend\">Wifi tai puhelinverkko</string>\n  <string name=\"delete_after_send\">Poista lähettämisen jälkeen</string>\n  <string name=\"delete_after_send_summary\">Poistaa valmiiksi merkityt lomakkeet ja mediatiedostot kun ne on lähetetty palvelimelle</string>\n  <string name=\"form_filling_category\">Lomakkeen täyttö</string>\n  <string name=\"constraint_behavior_title\">Rajoitekäsittely</string>\n  <string name=\"constraint_behavior\">Rajoitekäsittely</string>\n  <string name=\"constraint_behavior_on_swipe\">Validointi pyyhkäistäessä eteepäin</string>\n  <string name=\"constraint_behavior_on_finalize\">Validoi vasta kun lomake on valmis</string>\n  <string name=\"high_resolution_title\">Korkean resoluution video</string>\n  <string name=\"high_resolution_summary\">Aktivoi korkean resoluution videotallennukset</string>\n  <string name=\"image_size_title\">Kuvakoko</string>\n  <string name=\"image_size_dialog_title\">Maksimipikselit kuvan pitkältä reunalta</string>\n  <string name=\"image_size_large\">Suuri (3072 pikseliä)</string>\n  <string name=\"image_size_medium\">Keskisuuri (2048 pikseliä)</string>\n  <string name=\"image_size_small\">Pieni (1024 pikseliä)</string>\n  <string name=\"image_size_very_small\">Hyvin pieni (640 pikseliä)</string>\n  <string name=\"image_size_original\">Kameran alkuperäinen koko (oletus)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Näytä kysymysopastus</string>\n  <string name=\"guidance_no\">Ei</string>\n  <string name=\"guidance_yes\">Kyllä - näytetään aina</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Kyllä - tiivistettynä</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">Käytä ulkoista sovellusta äänen tallentamiseen</string>\n  <string name=\"form_import_category\">Lomakkeen tuonti</string>\n  <string name=\"instance_sync\">Lomakkeet viimeistellään tuodessa</string>\n  <string name=\"instance_sync_summary\">Vaikuttaa lomakkeisiin jotka lisätään suoraan Instances -kansioon.</string>\n  <string name=\"analytics_preferences\">Käyttötiedot</string>\n  <string name=\"analytics\">Kerää anonyymejä käyttötietoja</string>\n  <string name=\"analytics_summary\">Anonyymi käyttäjädata auttaa ODK-tiimiä priosrisoimaan korjauksia ja toiminnallisuuksia.</string>\n  <string name=\"form_metadata_title\">Lomakkeen metadata</string>\n  <string name=\"form_metadata\">Lomakkeen metadata</string>\n  <string name=\"form_metadata_summary\">Nämä arvot lisätään lomakkeisiin joissa on käyttäjätunnus, sähköpostiosoite ja/tai puhelinnumero valmiiksi täytettynä lähettäjän tunnistamiseksi.</string>\n  <string name=\"user_defined\">Käyttäjän määrittelemä</string>\n  <string name=\"phone_number\">Puhelinnumero</string>\n  <string name=\"email\">Sähköpostiosoite</string>\n  <string name=\"invalid_email_address\">Virheellinen sähköpostiosoite!</string>\n  <string name=\"device_defined\">Laitteen määrittelemä</string>\n  <string name=\"device_id\">Laitetunnus</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Ei käytettävissä</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">Avaa asetusten lukitus</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">Asetusten lukitus avattu</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <string name=\"setting_not_available_during_form_entry\">Asetus ei ole käytettävissä lomakesyötön aikana</string>\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Syötä uusi salasana</string>\n  <string name=\"admin_password\">Pääkäyttäjän salasana</string>\n  <string name=\"admin_password_changed\">Pääkäyttäjän salasana vaihdettu</string>\n  <string name=\"admin_password_disabled\">Pääkäyttäjän salasana pois päältä</string>\n  <string name=\"admin_password_incorrect\">Pahoittelen, väärä salasana!</string>\n  <string name=\"enter_admin_password\">Syötä pääkäyttäjätunnus</string>\n  <string name=\"change_admin_password\">Vaihda pääkäyttäjän salasana</string>\n  <string name=\"show_password\">Näytä salasana</string>\n  <string name=\"found_in_main\">Poista rasti piilottaaksesi päävalikosta</string>\n  <string name=\"found_in_settings\">Poista rasti piilottaaksesi kohdasta Suojaamattomat asetukset</string>\n  <string name=\"form_entry\">Poista rasti piiloittaaksesi lomakesyötöstä</string>\n  <string name=\"form_end\">Poista rasti piiloittaaksesi lomakkeen lopusta</string>\n  <string name=\"reset_project_settings_title\">Palautus alkutilaan</string>\n  <string name=\"reset_project_settings_subtext\">Valitse asetukset, lomakkeet tai data</string>\n  <string name=\"delete_project\">Poista</string>\n  <string name=\"select_all\">Valitse kaikki</string>\n  <string name=\"clear_all\">Tyhjennä kaikki</string>\n  <string name=\"reset_settings\">Kaikki asetukset (sisäiset asetukset, tallennetut asetukset)</string>\n  <string name=\"reset_saved_forms\">Tallennetut lomakkeet (instances -kansio, instances -tietokanta)</string>\n  <string name=\"reset_blank_forms\">Tyhjät lomakkeet (forms -kansio, forms -tietokanta, itemsets -tietokanta)</string>\n  <string name=\"reset_layers\">Karttatasot (layers -kansio)</string>\n  <string name=\"reset_cache\">Lomakkeiden latausvälimuisti (.cache -kansio)</string>\n  <string name=\"reset_settings_dialog_title\">Valitse mitä palautetaan alkutilaan</string>\n  <string name=\"reset_settings_button_reset\">Palautus alkutilaan</string>\n  <string name=\"reset_in_progress\">Palautetaan alkutilaan…</string>\n  <string name=\"reset_app_state_result\">Alkutilaan palauttamisen tulokset</string>\n  <string name=\"reset_app_state_warning\">Kaikki valittu data poistetaan pysyvästi. Peruutusmahdollisuutta ei ole.</string>\n  <string name=\"reset_settings_result\">Kaikki asetukset :: %s</string>\n  <string name=\"reset_saved_forms_result\">Tallennetut lomakkeet :: %s</string>\n  <string name=\"reset_blank_forms_result\">Tyhjät lomakkeet :: %s</string>\n  <string name=\"reset_layers_result\">Karttatasot :: %s</string>\n  <string name=\"reset_cache_result\">Lomakkeiden latausvälimuisti :: %s</string>\n  <string name=\"main_menu_settings\">Päävalikkoasetukset</string>\n  <string name=\"main_menu_settings_summary\">Näytä tai piilota painikkeita</string>\n  <string name=\"user_settings\">Käyttäjäasetukset</string>\n  <string name=\"user_setting_summary\">Näytä tai piilota asetuksia</string>\n  <string name=\"form_entry_setting\">Lomakesyöttöasetukset</string>\n  <string name=\"form_entry_settings_summary\">Näytä tai piilota toimintoja</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Siirtyminen taaksepäin</string>\n  <string name=\"moving_backwards_disabled_title\">Siirtyminen taaksepäin pois päältä</string>\n  <string name=\"moving_backwards_disabled_message\">Määritelläänkö laite estämään käyttäjiä ohittamasta tätä asetusta?\\n\\nMuutokset ovat:\\n\\u2022 Poista käytöstä Muokkaa luonnasta\\n\\u2022 Poista käytöstä Tallenna luonnoksena\\n\\u2022 Poista käytöstä Siirry kehotteeseen\\n\\u2022 Aseta rajoiteprosessointi tilaan Tarkista eteenpäin pyyhkäistäessä</string>\n  <string name=\"yes\">Kyllä</string>\n  <string name=\"no\">Ei</string>\n  <string name=\"moving_backwards_enabled_title\">Siirtyminen taaksepäin päällä</string>\n  <string name=\"moving_backwards_enabled_message\">Voit halutessasi tarkistaa seuraavat asetukset:\\n\\n\\u2022 Muokkaa luonnosta\\n\\u2022 Tallenna luonnoksena\\n\\u2022 Siirry kehotteeseen\\n\\u2022 Rajoitekäsittely</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Tallenna luonnoksena</string>\n  <string name=\"save_mid_summary\">Tallennusikoni yläpalkissa ja \\\"Tallenna luonnoksena\\\" -painike lomakkeelta poistuessa</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Lisätietoja</string>\n  <string name=\"tell_your_friends\">Jaa ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Keräävätkö kollegasi vielä dataa paperilla? Jaa ODK Collect heidän kanssaan.</string>\n  <string name=\"leave_a_review\">Kirjoita Play Store -arvio</string>\n  <string name=\"leave_a_review_msg\">Sinun (toivottavasti myönteinen) arviosi lisää sovelluksen näkyvyyttä Play Store -kaupassa.</string>\n  <string name=\"odk_website\">Käy ODK:n verkkosivulla</string>\n  <string name=\"odk_website_summary\">ODK:ta käytetään vaativien olosuhteiden tiedonkeruuseen sosiaalisen hyvän tarkoituksissa.</string>\n  <string name=\"odk_forum\">Liity ODK-foorumiin</string>\n  <string name=\"odk_forum_summary\">Liity foorumiin saadaksesi tukea, pyytääksesi toimintoja, ja auttaaksesi koodaamisessa tai kääntämisessä!</string>\n  <string name=\"all_open_source_licenses\">Avoimen lähdekoodin kirjastot/lisenssit</string>\n  <string name=\"all_open_source_licenses_msg\">Seisomme jättiläisten harteilla!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">Asetusten uudelleenmäärittely QR-koodilla</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">Korvaa kaikki nykyiset asetukset</string>\n  <string name=\"qr_code\">QR-koodi-asetukset</string>\n  <string name=\"scan_qr_code_fragment_title\">Skannaus</string>\n  <string name=\"view_qr_code_fragment_title\">QR-koodi</string>\n  <string name=\"import_qrcode_sd\">Tuo QR-koodi</string>\n  <string name=\"successfully_imported_settings\">Asetusten hakeminen onnistui</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Merkkien maksimimäärä ylitetty: QR-koodia ei voida luoda kaikille asetuksille</string>\n  <string name=\"share\">Jaa</string>\n  <string name=\"turn_on_flashlight\">Taskulamppu päälle</string>\n  <string name=\"turn_off_flashlight\">Taskulamppu pois</string>\n  <string name=\"qrcode_with_both_passwords\">Sisältää arkaluonteista tietoa: <b>pääkäyttäjä-</b> ja <b>palvelinsalasanat</b></string>\n  <string name=\"qrcode_without_passwords\">QR-koodi ei sisällä <b>pääkäyttäjä-</b> tai <b>palvelin</b>salasanaa. Tästä asetuksiin</string>\n  <string name=\"qrcode_with_admin_password\">Sisältää arkaluonteista tietoa: <b>pääkäyttäjän</b> salasana</string>\n  <string name=\"qrcode_with_server_password\">Sisältää arkaluonteista tietoa: <b>palvelimen</b> salasana</string>\n  <string name=\"server_password\">Palvelinsalasana</string>\n  <string name=\"include_password_dialog\">Koodiin sisältyvät salasanat</string>\n  <string name=\"generate\">Luo</string>\n  <string name=\"invalid_qrcode\">QR-koodi ei sisällä kelvollisia asetuksia</string>\n  <string name=\"qr_code_not_found\">Valitusta kuvasta ei löytynyt QR-koodia</string>\n  <string name=\"corrupt_imported_preferences_error\">Nykyiset asetukset ovat viallisia. Nollaa asetukset tai tuo toimivat Projektihallinta-asetuksissa.</string>\n  <string name=\"settings_with_gd_protocol\">Google Drive/Sheets -projekteja ei enää voi luoda</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Peruuta</string>\n  <string name=\"cancel_loading_form\">Peruuta</string>\n  <string name=\"cancel_location\">Peruuta</string>\n  <string name=\"do_not_change\">Peruuta</string>\n  <string name=\"canceling\">Perutaan</string>\n  <string name=\"notification_channel_name\">Yleiset ilmoitukset</string>\n  <string name=\"success\">Onnistui</string>\n  <string name=\"error_occured\">Virhe</string>\n  <string name=\"please_wait\">Odota hetki.</string>\n  <string name=\"please_wait_long\">Ole hyvä ja odota. Tämä voi kestää muutaman minuutin.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">\\'%s\\' ei voitu poistaa. Poista tiedosto manuaalisesti ja hae lomake uudelleen.</string>\n  <string name=\"fs_create_media_folder_error\">Ei voitu luoda mediakansiota \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Ei voitu kopioida \\'%1$s\\' yli \\'%2$s\\'. Syy: %3$s</string>\n  <string name=\"select_odk_shortcut\">Valitse ODK-oikotie</string>\n  <string name=\"background_audio_help\">Tämä lomake tallentaa ääntä laitteesi mikrofonin avulla.\\n\\nVoit käyttää äänitason ilmaisinta varmistaaksesi, että mikrofoni on riittävän lähellä ääntä, jota haluat äänittää.\\n\\nSulje tämä lomake lopettaaksesi äänityksen.\\n\\nOle yhteydessä henkilöön joka pyysi sinua keräämään tietoja saadaksesi lisätietoja.</string>\n  <string name=\"loading\">Ladataan…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Projektit</string>\n  <string name=\"settings\">Asetukset</string>\n  <string name=\"add_project\">Lisää projekti</string>\n  <string name=\"add\">Lisätään</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Kopioi projekti</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">Sinulla on jo projekti näillä yhteysasetuksilla. Haluatko vaihtaa olemassa olevaan projektiin vai luoda uuden?</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Lisää projektin kopio</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Vaihda olemassa olevaan projektiin</string>\n  <string name=\"project_name\">Projektin nimi</string>\n  <string name=\"project_icon\">Projektikuvake</string>\n  <string name=\"project_color\">Projektiväri</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Projekti vaihdettu, nyt %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">%s käytössä</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Vaihda projektiin %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <string name=\"delete_project_dialog_title\">Poista %s</string>\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <string name=\"delete_project_message\">Poistamalla tämän projektin, poistat pysyvästi seuraavat</string>\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <string name=\"delete_project_instructions\">Kirjoita %1$s vahvistaaksesi tämän.</string>\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <string name=\"delete_trigger\">poista</string>\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <string name=\"form_definitions_count\">Lomakemäärittelyjä: %1$d</string>\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <string name=\"sent_count\">Lähetettyjä: %1$d</string>\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <string name=\"unsent_count\">Lähettämättömiä: %1$d</string>\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <string name=\"drafts_count\">Luonnoksia: %1$d</string>\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"delete_project_confirm_button_text\">Poista projekti</string>\n  <string name=\"or\">OR</string>\n  <string name=\"hex_color\">Heksadesimaalinen väri</string>\n  <string name=\"invalid_hex_code\">Heksadesimaalinen koodi ei kelpaa</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Kerää dataa\\nmissä vain</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Asetusten määrittely QR-koodilla</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Syötä projektin yksityiskohdat manuaalisesti</string>\n  <string name=\"project_config_tip\">Lisättyäsi projektin voit konfiguroida sen Asetuksissa.</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Puuttuuko projekti vielä?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Kokeile demoa</string>\n  <string name=\"scan_configuration_qr_code\">Skannaa asetukset QR-koodista</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Yksityiskohdat</string>\n  <string name=\"errors\">Virheitä</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <string name=\"no_browser_installed\">Ei asennettua selainta!</string>\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">Avataksesi tämän lomakkeen, avaa ensin ODK Collect ja vaihda projektiin johon se sisältyy.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">ODK Collect ei ole konfiguroitu. Yritä avata ODK Collect ja konfiguroida sitä.\\n\\nJos käytit pikakuvaketta, saatat joutua luomaan sen uudelleen kun ODK Collect on konfiguroitu.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Tunnistamaton URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Kelvoton URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Täytetty lomake on poistettu!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">Tätä luonnosta ei voi muokata, koska vastaavaa tyhjää lomaketta ei ole tai se on poistettu.\\n\\nLomaketunnus: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Olet ladannut kaksi eri lomaketta, joilla on sama lomake tunnus ja versio. Ehkä ne olivat samat lomake ladattuna eri aikoina tai eri palvelimille. Joka tapauksessa, sinun pitäisi poistaa toinen.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <string name=\"cannot_open_form_because_of_forms_update\">Tätä lomaketta ei voi avata juuri nyt koska lomakkeita päivitetään parhaillaan. Yritä kohta uudelleen.</string>\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Avaa asetukset</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <string name=\"entities_title\">Entiteettilistat</string>\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <string name=\"entity_browser_button\">Tarkastele entiteettilistoja</string>\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">Offline</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">Tätä lomaketta ei tueta tässä ODK Collect -versiossa. Ole hyvä ja päivitä Collect. Jos ongelma jatkuu, ilmoita siitä henkilölle, joka pyysi sinua keräämään dataa.\\n\\nForm Entities spec versio: %s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">Sovellus kaatui viime käyttökerralla!</string>\n  <string name=\"cant_start_app\">Sovelluksen käynnistys ei onnistu!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">Tämä projekti oli aikaisemmin kytketty Google Drive -tiliin. Google Drive -tuki on lopetettu. Voit määritellä palvelimen tai tallentaa lähetyksiä tietokoneelle.</string>\n  <string name=\"learn_more_button_text\">Lisätietoja</string>\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">Kehitystyökalut</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">Kaada sovellus</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Pakota havaitsematon poikkeus, joka aiheuttaa sovelluksen kaatumisen</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <string name=\"debug_filters\">Vianetsinnän suodattimet</string>\n  <string name=\"debug_filters_summary\">Näytä suodatinlausekesuorittamisen vianetsintätietoa lomaketta täyttäessä</string>\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"use_zxing_for_barcode_scanning\">Paluu vanhempaan viivakoodilukijaan</string>\n  <string name=\"permission_dialog_title\">Tietoa luvista</string>\n  <string name=\"permission_dialog_text\">Sinulta tullaan pyytämään lupaa ODK Collectille pääsy toimintoihin alla, valitse \\\"Salli\\\" jos haluat käyttää niitä.</string>\n  <string name=\"notifications\">Ilmoitukset</string>\n  <string name=\"required_for_notifications\">Tarvitaan päivitysten näyttämiseen kun lomakkeita ladataan, päivitetään tai lähetetään.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">Lomakkeesi tallennettiin luonnoksena.</string>\n  <string name=\"form_saved\">Lomakkeesi tallennettiin.</string>\n  <string name=\"form_sending\">Lähetetään lomaketta…</string>\n  <string name=\"edit_form\">Muokkaa</string>\n  <string name=\"view_form\">Tarkastele</string>\n  <string name=\"close_snackbar\">Sulje ilmoitus</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">Viime lomakelähetys: %d sekunti sitten</item>\n    <item quantity=\"other\">Viime lomakelähetys: %d sekuntia sitten</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"one\">Viime lomakelähetys: %d minuutti sitten</item>\n    <item quantity=\"other\">Viime lomakelähetys: %d minuuttia sitten</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"one\">Viime lomakelähetys: %d tunti sitten</item>\n    <item quantity=\"other\">Viime lomakelähetys: %d tuntia sitten</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"one\">Viime lomakelähetys: %d päivä sitten</item>\n    <item quantity=\"other\">Viime lomakelähetys: %d päivää sitten</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"one\">%d lomake valmiina lähetettäväksi</item>\n    <item quantity=\"other\">%d lomaketta valmiina lähetettäväksi</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">Myöhemmissä versioissa viimeistellyt lomakkeet eivät enää tule olemaan muokattavissa. Tallenna lomakkeet luonnoksina muokataksesi niitä myöhemmin.\\n\\nVoit tarkistaa luonnoslomaketta virheiden varalta napsauttamalla kolmea pistettä (⋮) ja sitten Tarkista virheiden varalta.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">Myöhemmissä versioissa viimeistellyt lomakkeet eivät enää tule olemaan muokattavissa. </string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">Viimeistele kaikki lomakkeet</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"one\">Haluatko viimeistellä %d luonnoksen?</item>\n    <item quantity=\"other\">Haluatko viimeistellä %d luonnosta?</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">Kun viimeistelet kaikki luonnokset, ne tulevat olemaan \\\"Valmiita lähetykseen\\\" etkä voi enää muokata niitä. Jos lomakkeessa on virheitä sitä ei viimeistellä.\\n\\nTätä toimintoa ei voi perua.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"one\">Onnistui! %d luonnos viimeistelty.</item>\n    <item quantity=\"other\">Onnistui! %d luonnosta viimeistelty.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"one\">%d luonnoksessa on virheitä jotka pitää korjata ennen viimeistelyä.</item>\n    <item quantity=\"other\">%d luonnoksessa on virheitä jotka pitää korjata ennen viimeistelyä.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d luonnosta viimeistelty. %d luonnoksessa on virheitä, jotka pitää korjata ennen viimeistelyä..</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d luonnosta viimeistelty. Jäljelle jääneet luonnokset pitää viimeistellä manuaalisesti.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Virheitä</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">Ei virheitä</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">Poista rasti piilottaaksesi Luonnos-näkymästä</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">Ei näytettävää</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">Ei tyhjiä lomakkeita</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">Lataa lomake aloittaaksesi</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">Ei luonnoksia</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">Kun tallennat luonnoksen, lomakkeet näkyvät tässä</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">Ei valmista lähetettävää</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">Kun viimeistelet luonnoksia, ne näkyvät tässä</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">Ei lähetettyjä lomakkeita</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">Kun lähetät viimeisteltyjä lomakkeita, ne näkyvät tässä</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">Ei ladattavia lomakkeita</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">Lataa lomake aloittaaksesi</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">Ei lomakkeita poistettavaksi</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">Kun olet ladannut tyhjiä lomakkeita ne tulevat ilmestymään tänne</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">Kun olet tallentanut lomakkeita ne tulevat ilmestymään tänne</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">Haluatko palauttaa työsi?</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"savepoint_recovery_dialog_message\">\\'Collect sulkeutui odottamattomasti\\' EEE dd.MM.yyyy \\'kello\\' HH:mm\\' ja tallensi työsi.\\n\\nHaluatko palauttaa työsi?\\'</string>\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">Palauta</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Hylkää</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">Hae piste</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">Tarkastele tai muuta pistettä</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">Tarkastele pistettä</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">Muuta pistettä</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">Hae viiva</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">Tarkastele tai muuta viivaa</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">Tarkastele viivaa</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">Hae monikulmio</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">Tarkastele tai muuta monikulmiota</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">Tarkastele monikulmiota</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <string name=\"settings_json\">JSON-asetukset</string>\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <string name=\"preparing_form_edit\">Lomaketta valmistellaan muokkausta varten…</string>\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <string name=\"user_visible_instance_name\">%s (Muokkaus %d)</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_title\">Jatketaanko luonnoksen muokkausta?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_draft_edit_found_dialog_message\">\\'Luonnos tallennettu\\' EEE dd MMM yyyy \\'klo\\' HH:mm\\'\\n\\nHaluatko jatkaa luonnoksen muokkaamista?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_positive_button\">Jatka muokkausta</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_title\">Muokataanko viimeistä versiota?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_finalized_edit_found_dialog_message\">\\'Tätä lähetystä on aikaisemmin muokattu.\\n\\nHaluatko muokata viimeistä versiota, joka on viimeistelty\\' EEE dd MMM yyyy \\'klo\\' HH:mm\\'?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_positive_button\">Muokkaa viimeistä lomaketta</string>\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n  <string name=\"finalized_form_edit_started\">Olet nyt muokkaamassa tätä lomaketta. Täppää kysymystä aloittaaksesi muokkauksen.</string>\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-fr/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Remplir un formulaire </string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Brouillons</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Prêts à envoyer</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Envoyés</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Télécharger formulaire</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Supprimer des formulaires</string>\n  <string name=\"saved_forms\">Formulaires enregistrés</string>\n  <string name=\"version_number\">Version: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Ajouté le\\' EEE MMM dd yyyy \\'à\\' HH\\'h\\'mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Mis à jour le\\' EEE dd MMM yyyy \\'à\\' HH\\'h\\'mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Enregistré le\\' EEE MMM dd yyyy \\'à\\' HH\\'h\\'mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Finalisé le\\' EEE MMM dd yyyy \\'à\\' HH\\'h\\'mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Envoyé le\\' EEE MMM dd yyyy \\'à\\' HH\\'h\\'mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'L\\'\\'envoi a échoué le\\' EEE dd MMM yyyy \\'à\\' HH\\'h\\'mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Supprimé le\\' EEE dd MMM yyyy \\'à\\' HH\\'h\\'mm</string>\n  <string name=\"submission_deleted\">Soumission supprimée</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Modifié\\' EEE MMM dd yyyy \\'à\\' HH\\'h\\'mm</string>\n  <string name=\"encrypted_form\">Formulaires encryptés</string>\n  <string name=\"deleted_form\">Formulaire supprimé</string>\n  <string name=\"sort_the_list\">Trier la liste</string>\n  <string name=\"sort_by\">Trier par</string>\n  <string name=\"sort_by_name_asc\">Nom, A-Z</string>\n  <string name=\"sort_by_name_desc\">Nom, Z-A</string>\n  <string name=\"sort_by_date_desc\">Date, la plus récente en premier</string>\n  <string name=\"sort_by_date_asc\">Date, la plus ancienne en premier</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Dernière sauvegarde en premier</string>\n  <string name=\"filter_the_list\">Filtrer la liste</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Rechercher</string>\n  <string name=\"not_granted_permission\">Vous n\\'avez pas les permissions requises pour démarrer l\\'activité demandée</string>\n  <string name=\"storage_runtime_permission_denied_title\">Autorisations de stockage</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Sans ces permissions Collect ne peut pas accéder à vos formulaires ou enregistrer des réponses. Ré-ouvrez l\\'application quand vous êtes prêt à donner les permissions requises. </string>\n  <string name=\"location_runtime_permissions_denied_title\">Autorisations liées à la localisation</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Sans la permission de <b>Position Exacte</b>, Collect ne peut pas enregistrer votre localisation. Veuillez réessayer lorsque vous êtes prêt à l\\'accorder.</string>\n  <string name=\"camera_runtime_permission_denied_title\">Autorisation d\\'enregistrement vidéo</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Sans cette permission, Collect ne peut pas accéder à la caméra. Veuillez réessayer lorsque vous êtes prêt à l\\'accorder.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Autorisation d\\'enregistrement audio</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Sans cette permission, Collect ne peut pas accéder au microphone. Veuillez réessayer lorsque vous êtes prêt à l\\'accorder.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Voir les formulaires remplis sur une carte</string>\n  <string name=\"instance_scan_count\">%1$d formulaires ajoutés.</string>\n  <string name=\"finished_disk_scan\">Le scan est fini. Tous les formulaires sont chargés.</string>\n  <string name=\"xform_parse_error\">Erreur d\\'analyse de l\\'XForm %1$s: l\\'élément %2$s manque ou est invalide.</string>\n  <string name=\"loading_form\">Chargement du formulaire…</string>\n  <string name=\"survey_loading_reading_form_message\">Lecture du formulaire…</string>\n  <string name=\"survey_loading_reading_data_message\">Lecture des données du sondage…</string>\n  <string name=\"survey_loading_reading_csv_message\">Lecture des fichiers CSV…</string>\n  <string name=\"parse_error\">Désolé. Impossible d\\'analyser le formulaire.</string>\n  <string name=\"loading_form_failed\">Une erreur s\\'est produite lors du chargement du formulaire. Veuillez réessayer.</string>\n  <string name=\"cannot_edit_completed_form\">Ce formulaire ne peut pas être modifié une fois finalisé. Il est peut-être encrypté.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Les modifications non enregistrées ont été récupérées à partir du point de sauvegarde !</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Changer la langue</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Aller à la ligne de saisie</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Enregistrer comme brouillon</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Aller au début</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Aller à la fin</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Monter</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <string name=\"edit_finalized_form\">Éditer le formulaire finalisé</string>\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Groupe</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Groupe répétitif</string>\n  <string name=\"exit\">Sortir</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Ajouter «%s» ?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Ajouter</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"do_not_add_repeat\">Ne pas ajouter</string>\n  <string name=\"add_another_menu\">Ajouter un autre </string>\n  <string name=\"add_another_question\">Ajouter un autre ?</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Ligne de modification</string>\n  <string name=\"clearanswer_confirm\">Enlever la réponse à «%s» ?</string>\n  <string name=\"clear_answer\">Enlever la réponse</string>\n  <string name=\"clear_answer_ask\">Enlever cette réponse ?</string>\n  <string name=\"clear_answer_no\">Annuler</string>\n  <string name=\"discard_answer\">Enlever la réponse</string>\n  <string name=\"discard_group\">Enlever le groupe</string>\n  <string name=\"delete_repeat\">Supprimer le groupe</string>\n  <string name=\"delete_repeat_ask\">Supprimer ce groupe ?</string>\n  <string name=\"delete_repeat_confirm\">Supprimer le groupe \\\"%s\\\" et tous ses sous-groupes ?</string>\n  <string name=\"delete_repeat_no\">Annuler</string>\n  <string name=\"invalid_answer_error\">Désolé, la réponse n\\'est pas valide.</string>\n  <string name=\"required_answer_error\">Réponse obligatoire!</string>\n  <string name=\"success_form_validation\">Succès! Aucune erreur trouvée.</string>\n  <string name=\"invalid_space_in_answer_singular\">Attention: le nom 1%s contient des espaces</string>\n  <string name=\"invalid_space_in_answer_plural\">Attention: les noms 1%s contiennent des espaces</string>\n  <string name=\"too_complex_form\">Ce formulaire est trop complexe pour cet appareil. Essayez de simplifier les expressions ou demandez de l\\'aide sur le forum.</string>\n  <string name=\"survey_internal_error\">Erreur interne : un ou plusieurs champs de saisie peuvent avoir été ignorés.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Vous êtes à la fin de \\\"%s\\\".</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <string name=\"form_editing_disabled_after_finalizing\">Vous ne pourrez pas éditer ce formulaire après sa finalisation.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <string name=\"form_editing_disabled_after_sending\">Vous ne pourrez pas éditer ce formulaire après envoi.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_finalizing\">Vous pourrez éditer ce formulaire après qu\\'il soit finalisé.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_sending\">Vous pourrez éditer ce formulaire après envoi.</string>\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Si vous devrez modifier ce formulaire prochainement, choisissez \\\"Enregistrer comme brouillon\\\" jusqu\\'à ce que vous soyez prêt à l\\'envoyer.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <string name=\"form_editing_enabled_after_finalizing_hint\">Pour modifier les données après avoir finalisé votre formulaire, trouvé le dans \\\"Prêt à envoyer\\\" et appuyez sur l\\'icône d\\'édition.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <string name=\"form_editing_enabled_after_sending_hint\">Pour modifier les données après avoir envoyé votre formulaire, allez à \\\"Envoyé\\\" et appuyez sur l\\'icône d\\'édition.</string>\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Enregistrer comme brouillon</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Finaliser</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Envoyer</string>\n  <string name=\"data_saved_error\">Désolé, l\\'enregistrement du formulaire a échoué.</string>\n  <string name=\"data_saved_ok\">Le formulaire a été enregistré avec succès.</string>\n  <string name=\"save_point_error\">Erreur lors de l\\'enregistrement du point de récupération: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Supprimer le formulaire</string>\n  <string name=\"discard_changes\">Supprimer les modifications</string>\n  <string name=\"keep_changes\">Enregistrer les modifications</string>\n  <string name=\"keep_editing\">Continuer l\\'édition</string>\n  <string name=\"quit_form_title\">Enregistrer le formulaire ?</string>\n  <string name=\"save_explanation\">Vous pouvez enregistrer ce formulaire et y accéder depuis vos brouillons à tout moment.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'Ce formulaire a été enregistré pour la dernière fois le\\' EEE MMM dd yyyy \\'à\\' HH\\'h\\'mm\\'. Enregistrer comme brouillon pour conserver les modifications.\\'</string>\n  <string name=\"quit_form_continue_title\">Continuer l\\'édition ?</string>\n  <string name=\"discard_form_warning\">Si vous fermez le formulaire, vous perdrez toutes les modifications que vous y avez apportées.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'Ce formulaire a été enregistré pour la dernière fois le\\' EEE MMM dd yyyy \\'à\\' HH\\'h\\'mm\\'. Si vous abandonnez les modifications, vous perdrez toutes celles apportées depuis.\\'</string>\n  <string name=\"saving_form\">Sauvegarde du formulaire…</string>\n  <string name=\"survey_saving_validating_message\">Validation des réponses…</string>\n  <string name=\"survey_saving_collecting_message\">Récupération des données…</string>\n  <string name=\"survey_saving_saving_message\">Enregistrement sur ​​la carte SD…</string>\n  <string name=\"survey_saving_finalizing_message\">Finalisation de la carte SD…</string>\n  <string name=\"survey_saving_encrypting_message\">Chiffrement des données…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Pas exactement un enregistrement pour cette instance !</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Aucun formulaire vierge ne correspond à ce jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Pas de FormId spécifié ???</string>\n  <string name=\"phone_does_not_support_rsa\">Le téléphone ne supporte pas le chiffrement RSA.</string>\n  <string name=\"invalid_rsa_public_key\">Clé publique RSA invalide.</string>\n  <string name=\"encryption_error_message\">Le formulaire %s n\\'a pas été enregistré comme finalisé.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">Le formulaire ne peut être utilisé</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Impossible de commencer l\\'enregistrement.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Le microphone est déjà en utilisation.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">Le fichier n\\'a pas pu être attaché au formulaire mais est disponible à %1$s</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Enregistrement désactivé. Activez depuis %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Échec de mise à jour des données. Ceci est généralement causé par un mauvais usage de \\\"calculates\\\" dans la définition du formulaire.\\n\\nSi vous continuez à avoir ce problème, avertissez la personne qui vous a demandé de collecter les données.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Démarrer</string>\n  <string name=\"activity_not_found\">Aucune activité à gérer: %s</string>\n  <string name=\"no_app\">L\\'application demandée n\\'existe pas. Saisissez les informations manuellement.</string>\n  <string name=\"null_intent_value\">L\\'application externe n\\'a pas fourni les informations attendues.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">Imprimer</string>\n  <string name=\"open_url\">Ouvrir l\\'URL</string>\n  <string name=\"capture_osm\">Lancer OpenMapKit</string>\n  <string name=\"recapture_osm\">Refaire le balisage d\\'OSM</string>\n  <string name=\"edited_osm_file\">Fichier OSM XML édité:</string>\n  <string name=\"invalid_osm_data\">Une erreur s\\'est produite. Les données OSM valides ne peuvent être obtenues.</string>\n  <string name=\"alert\">Alerte</string>\n  <string name=\"install_openmapkit\">Veuillez installer OpenMapKit.</string>\n  <string name=\"get_barcode\">Scanner un code-barres</string>\n  <string name=\"replace_barcode\">Remplacer le code-barre</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Mettre le code-barres dans le rectangle</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"rotate_device\">Tourner l\\'appareil</string>\n  <string name=\"open_file\">Ouvrir fichier</string>\n  <string name=\"rank_items\">Choisir l\\'ordre des éléments</string>\n  <string name=\"number_picker_title\">Sélecteur de numéro</string>\n  <string name=\"invalid_range_widget\">Les paramètres de ce widget sont invalides!</string>\n  <string name=\"svg_file_does_not_exist\">Le fichier SVG n\\'existe pas !</string>\n  <string name=\"capture_image\">Prendre une photo</string>\n  <string name=\"choose_image\">Choisir une Image</string>\n  <string name=\"selected_invalid_image\">Le fichier choisi n\\'est pas une image valide</string>\n  <string name=\"take_picture_instruction\">Toucher l\\'écran pour prendre une photo</string>\n  <string name=\"error_front_camera_unavailable\">Caméra frontale non disponible sur cet appareil</string>\n  <string name=\"camera_error\">Il y a eu un erreur lors de la prise d\\'image</string>\n  <string name=\"camera_failed_to_initialize\">Collect n\\'a pas pu initialiser l\\'appareil photo !</string>\n  <string name=\"annotate_image\">Annoter l\\'image</string>\n  <string name=\"gif_not_supported\">Les fichiers GIF ne sont pas supportés</string>\n  <string name=\"invalid_file_type\">Le type de fichier fourni par l\\'application n\\'es pas valide</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_title\">Enregistrer les modifications?</string>\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_explanation\">Voulez-vous enregistrer les modifications avant de quitter?</string>\n  <string name=\"save_and_close\">Enregistrer et fermer</string>\n  <string name=\"signature_capture\">Capture d\\'une signature</string>\n  <string name=\"sign_button\">Obtenir une signature</string>\n  <string name=\"markup_image\">écrire sur l\\'image</string>\n  <string name=\"draw_image\">Dessiner une image</string>\n  <string name=\"reset_image\">Remise à zéro</string>\n  <string name=\"set_color\">Sélectionner la couleur</string>\n  <string name=\"capture_video\">Enregistrer une vidéo</string>\n  <string name=\"choose_video\">Choisir une Vidéo</string>\n  <string name=\"play_video\">Jouer la vidéo</string>\n  <string name=\"choose_file\">Choisir un fichier</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Supprimer</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Enregistrer de l\\'audio</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Choisir un fichier audio</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Pause</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Reprendre</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">Enregistrement audio : activé</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">Enregistrement audio : désactivé</string>\n  <string name=\"background_audio_permission_explanation\">Ce formulaire crée un enregistrement audio en tâche de fond. Vous devez accorder l\\'accès à votre microphone. Si vous ne l\\'accordez pas, vous ne pourrez pas ouvrir ce formulaire.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Ce formulaire demande un enregistrement audio en tâche de fond. Si vous désactivez l\\'enregistrement, l\\'enregistrement en cours sera arrêté et l\\'audio déjà enregistré sera supprimé. Êtes-vous sûr de vouloir procéder?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Désactiver enregistrement</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">L\\'enregistrement ne recommencera pas tout de suite. Vous devez quitter et réouvrir le formulaire pour enregistrer.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Supprimer fichier?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">Vous ne pourrez pas récupérer ce fichier une fois qu\\'il sera supprimé. Êtes-vous sûr de vouloir continuer?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Arrêt</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Enregistrement en cours…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Enregistrement audio</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Jouer audio</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Sauvegarde du fichier en cours</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Veuillez arrêter l\\'enregistrement avant de quitter cet écran.</string>\n  <string name=\"error_attaching_binary_file\">Erreur pendant l\\'attachement du média: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Choisir valeur</string>\n  <string name=\"select_answer\">Sélectionnez la Réponse</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Modifier valeur</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Aucune valeur choisie</string>\n  <string name=\"selected_title\">Sélectionné: </string>\n  <string name=\"trigger\">OK. Continuer.</string>\n  <string name=\"ext_file_no_data_error\">Le fichier ne contient pas de données !</string>\n  <string name=\"ext_conflicting_columns_error\">Colonnes %s correspondent !</string>\n  <string name=\"ext_sortBy_numeric_error\">Le trie par colonne ne doit contenir que des caractères numériques. La valeur en conflit était: \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Erreur de syntaxe dans la fonction search(): La fonction requiert 1,4 ou 6 arguments:</string>\n  <string name=\"ext_search_wrong_function_error\">Erreur de syntaxe dans la fonction search() : Fonction non reconnue \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Erreur de syntaxe dans la fonction search(): \\'%s\\' n\\'a pas été évaluée comme une fonction.</string>\n  <string name=\"ext_search_return_error\">Le gestionnaire de recherche a retourné un objet de type \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Impossible d\\'importer des données depuis %1$s. Raison: %2$s</string>\n  <string name=\"ext_import_progress_message\">Pré-chargement des données depuis \\'%1$s\\', merci de patienter… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Lecture des données annulée!</string>\n  <string name=\"ext_import_finalizing_message\">Finalisation des données pré-chargées…</string>\n  <string name=\"ext_import_completed_message\">Lecture des données terminée !</string>\n  <string name=\"ext_not_initialized_error\">Le Gestionnaire des données externes n\\'a pas été initialisé.</string>\n  <string name=\"ext_import_csv_missing_error\">Les données externes pour %1$s n\\'ont pas été importées. Vous avez peux-être oublié d\\'inclure le fichier %2$s.csv avec votre formulaire ?</string>\n  <string name=\"ext_search_generic_error\">Erreur de syntaxe dans la fonction search(): %s</string>\n  <string name=\"ext_assign_value_error\">Impossible d\\'attribuer la valeur à \\'%s\\'.</string>\n  <string name=\"parser_exception\">Exception XPathParser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Enregistrer la Position</string>\n  <string name=\"replace_bearing\">Remplacer la Position</string>\n  <string name=\"getting_bearing\">Chargement de la Position</string>\n  <string name=\"accept_bearing\">Enregistrer la Position</string>\n  <string name=\"bearing\">Palier: %.3f</string>\n  <string name=\"direction\">Direction: %s</string>\n  <string name=\"bearing_lack_of_sensors\">L\\'orientement n\\'est pas disponible: l\\'appareil n\\'a pas d\\'accéléromètre ou de capteur de champ magnétique.</string>\n  <string name=\"select_date\">Choisir la date</string>\n  <string name=\"select_time\">Choisir l\\'heure</string>\n  <string name=\"no_date_selected\">Aucune date choisie</string>\n  <string name=\"no_time_selected\">Aucune heure choisie</string>\n  <string name=\"ethiopian_month_1\">Mäskäräm</string>\n  <string name=\"ethiopian_month_2\">Teqemt</string>\n  <string name=\"ethiopian_month_3\">Hedar</string>\n  <string name=\"ethiopian_month_4\">Tahesas</string>\n  <string name=\"ethiopian_month_5\">Ter</string>\n  <string name=\"ethiopian_month_6\">Yäkatit</string>\n  <string name=\"ethiopian_month_7\">Mägabit</string>\n  <string name=\"ethiopian_month_8\">Miyazya</string>\n  <string name=\"ethiopian_month_9\">Guenbot</string>\n  <string name=\"ethiopian_month_10\">Säné</string>\n  <string name=\"ethiopian_month_11\">Hamlé</string>\n  <string name=\"ethiopian_month_12\">Nähasé</string>\n  <string name=\"ethiopian_month_13\">Pagumén</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Ter</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <string name=\"buddhist_month_1\">makara</string>\n  <!--2nd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_2\">kumbha</string>\n  <!--3rd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_3\">mīna</string>\n  <!--4th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_4\">meṣa</string>\n  <!--5th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_5\">vṛṣabha</string>\n  <!--6th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_6\">mithuna</string>\n  <!--7th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_7\">karkaṭa</string>\n  <!--8th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_8\">sinha</string>\n  <!--9th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_9\">kanyā</string>\n  <!--10th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_10\">tulā</string>\n  <!--11th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_11\">vṛścika</string>\n  <!--12th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_12\">dhanu</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Le fichier %s n\\'est pas valide.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Fichier %s manquant</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Entrez l\\'identité</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Identité</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Le formulaire requiert votre identification</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Fermer</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Raison du changement</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Raison</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Vous devez expliquer les raisons du changement de ce formulaire</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Enregistrer</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">Sélectionner un emplacement</string>\n  <string name=\"choices\">Choix</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_usgs_sat\">USGS Carte nationale hybride</string>\n  <string name=\"openmap_usgs_img\">Les images de la carte nationale USGS</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Fonds de carte</string>\n  <string name=\"basemap_source\">Source</string>\n  <string name=\"basemap_source_unavailable\">Désolé, %s carte de base ne sont pas disponibles sur ce terminal. Merci d\\'en choisir une autre dans Paramètres &gt; Cartes.</string>\n  <string name=\"map_style_label\">Style de carte %s</string>\n  <string name=\"streets\">Streets</string>\n  <string name=\"terrain\">Terrain</string>\n  <string name=\"hybrid\">Hybride</string>\n  <string name=\"satellite\">Satellite</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Clair</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Foncé</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">En exterieur</string>\n  <string name=\"topographic\">Topographique</string>\n  <string name=\"google_play_services_error_occured\">Impossible d’accéder à Google Maps. Google Play Services est-il installé ?</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Matière Sombre</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">Couches hors ligne</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">Couche</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">Choisir la couche hors ligne</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">Choisissez la couche hors ligne à utiliser pour toutes les cartes de ce projet. Vous pouvez ajouter des options à la liste en choisissant un fichier MBTiles de votre appareil. Vous pouvez également supprimer des couches de la liste.</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">En apprendre plus à propos de l\\'ajout de MBTiles.</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">Ajouter des couches</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">Supprimer une couche</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <plurals name=\"non_mbtiles_files_selected_title\">\n    <item quantity=\"one\">%d couche n\\'a pas pu être ajoutée.</item>\n    <item quantity=\"many\">%d couches n\\'ont pas pu être ajoutées.</item>\n    <item quantity=\"other\">%d couches n\\'ont pas pu être ajoutées.</item>\n  </plurals>\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">Les fichiers que vous avez sélectionnés ne sont pas des fichiers MBTiles. Vous pouvez seulement ajouter des fichiers MBTiles. </string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">Certains des fichiers que vous avez sélectionnés ne sont pas des fichiers MBTiles. Vous pouvez seulement ajouter des fichiers MBTiles.</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">Couches</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">Choisir l\\'accès aux couches</string>\n  <string name=\"select_layer_access_subtitle\">Voulez vous que les couches soient disponibles dans tous les projets ou seulement dans le projet courant ?</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">Tous les projets</string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">Projet courant seulement</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">Voulez-vous vraiment supprimer la couche hors ligne %1$s?</string>\n  <string name=\"record_geopoint\">Enregistrer un point</string>\n  <string name=\"location_accuracy\">Précision : %1$s</string>\n  <string name=\"location_accuracy_unacceptable\">⚠️ Précision : %1$s (insuffisante)</string>\n  <string name=\"provider_disabled_error\">Désolé, le fournisseur de localisation est désactivé.</string>\n  <string name=\"gps_result\">Latitude: %1$s\\nLongitude: %2$s\\nAltitude: %3$s m\\nPrécision: %4$s m</string>\n  <string name=\"google_play_services_not_available\">Ce formulaire souhaite suivre votre localisation, mais les Services de Google Play ne sont pas disponible</string>\n  <string name=\"location_providers_disabled_dialog_message\">Ce formulaire souhaite suivre votre localisation, mais les fournisseurs d\\'emplacements sont désactivés. Merci de vérifier les paramétres Android</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Aller aux paramètres</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">Enregistrement de la localisation : activé</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">Enregistrement de la localisation : désactivé</string>\n  <string name=\"background_location_disabled\">Ce formulaire veut suivre votre position mais le suivi est désactivé. S\\'il vous plaît activer dans le %1$s menu ci-dessus.</string>\n  <string name=\"background_location_enabled\">Ce formulaire suit votre position. Vous pouvez désactiver le suivi dans le %1$s menu ci-dessus.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">Vérifier s\\'il y a des erreurs</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">Temps écoulé : %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">Le point sera enregistré dés que la précision sera supérieure à %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">Satellites: %1$s</string>\n  <string name=\"waiting_for_location\">Tentative de localisation. Veuillez patienter.</string>\n  <string name=\"poor_accuracy\">Faible précision. Veuillez patienter.</string>\n  <string name=\"unacceptable_accuracy\">Précision inacceptable. Veuillez patienter.</string>\n  <string name=\"improving_accuracy\">Amélioration de la précision. Veuillez patienter.</string>\n  <string name=\"accuracy_status_title\">Localisation en cours.</string>\n  <!--The accuracy of a location fix in meters-->\n  <string name=\"accuracy_m\">%1$s m.</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">O %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Méthode de saisie</string>\n  <string name=\"start\">Démarrer</string>\n  <string name=\"enable_gps\">Activer le GPS</string>\n  <string name=\"gps_enable_message\">Le GPS est désactivé sur votre appareil. Voulez vous l\\'activer ?</string>\n  <string name=\"geo_clear_warning\">Objet déjà créé. Souhaitez-vous l\\'effacer ?</string>\n  <string name=\"geo_exit_warning\">Abandoner les changements et retourner au formulaire?</string>\n  <string name=\"polygon_validator\">3 points minimum sont nécessaires pour créer un Polygone</string>\n  <string name=\"polyline_validator\">Il faut au moins 2 points pour créer une ligne</string>\n  <string name=\"clear\">Effacer</string>\n  <string name=\"placement_mode\">Placement en tapotant</string>\n  <string name=\"manual_mode\">Enregistrement manuel de la localisation</string>\n  <string name=\"automatic_mode\">Enregistrement automatique de la localisation</string>\n  <string name=\"recording_interval\">Intervalle d\\'enregistrement:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d seconde</item>\n    <item quantity=\"many\">%d secondes</item>\n    <item quantity=\"other\">%d secondes</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d minute</item>\n    <item quantity=\"many\">%d minutes</item>\n    <item quantity=\"other\">%d minutes</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Exigence de précision:</string>\n  <string name=\"none\">Aucun</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d mètre</item>\n    <item quantity=\"many\">%d mètres</item>\n    <item quantity=\"other\">%d mètres</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Points entrés: %d</string>\n  <string name=\"collection_status_placement\">Points entrés: %d (appuyez pour placer des points)</string>\n  <string name=\"collection_status_manual\">Points entrés: %d (enregistrement manuel)</string>\n  <string name=\"collection_status_auto_seconds\">Points entrés: %1$d (enregistrement chaque %2$d sec)</string>\n  <string name=\"collection_status_auto_minutes\">Points entrés: %1$d (enregistrement chaque %2$d min)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Points entrés: %1$d (enregistrement chaque %2$d sec, précision%3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Points entrés: %1$d (enregistrement chaque %2$d min, précision%3$d m)</string>\n  <string name=\"geopoint_instruction\">Appuyez longuement pour placer un marqueur ou utilisez le bouton d\\'ajout de marqueur</string>\n  <string name=\"geopoint_no_draggable_instruction\">Appuyez sur le bouton \\\"ajouter un marqueur\\\".</string>\n  <string name=\"discard\">Abandoner changements</string>\n  <string name=\"select_item_count\">%s: %d (%d affichés sur la carte)</string>\n  <string name=\"select_item\">Sélectionner</string>\n  <string name=\"new_item\">Nouvel item</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">Éditer le brouillon</string>\n  <string name=\"view_data\">Voir Questionnaire Enregistré </string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">Suivi de la localisation</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">Enregistrement de la localisation…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Montrer ma position</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">Zoomer pour tout voir</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Pause</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">Supprimer le dernier point</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <string name=\"error_fixed\">✅ Erreur corrigée</string>\n  <!--Description for button that explains how to modify the map-->\n  <string name=\"show_how_to_modify_map\">Voir comment modifier la carte</string>\n  <!--Title for instructions on how to modify map features-->\n  <string name=\"how_to_modify_map\">Comment modifier la carte</string>\n  <!--Instruction in a list on how to move a point-->\n  <string name=\"long_press_to_move_point_info_item\">Appuyer longuement pour déplacer le point</string>\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">Supprimer le dernier point</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <string name=\"delete_shape_to_start_over_info_item\">Supprimer la forme pour recommencer</string>\n  <!--Instruction in a list on how to add a new point-->\n  <string name=\"add_point_info_item\">Ajouter un point</string>\n  <!--Instruction in a list on how to tap to add a point-->\n  <string name=\"tap_to_add_a_point_info_item\">Appuyez sur la carte pour ajouter un point</string>\n  <!--Instruction in a list on how to delete the entire shape-->\n  <string name=\"delete_entire_shape_info_item\">Supprimer toute la forme</string>\n  <!--Button text to close the instructions dialog-->\n  <string name=\"done\">Terminé</string>\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <string name=\"how_to_modify\">Comment modifier</string>\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Envoi du formulaire…</string>\n  <string name=\"noselect_error\">Désolé, vous n\\'avez pas sélectionné un formulaire.</string>\n  <string name=\"no_connection\">Pas de connexion réseau disponible</string>\n  <string name=\"sending_items\">Envoi de %1$s sur %2$s formulaire(s)</string>\n  <string name=\"send_selected_data\">Envoyer éléments sélectionnés</string>\n  <string name=\"server_auth_credentials\">Le nom d\\'utilisateur et/ou le mot de passe est incorrect pour le serveur: %s</string>\n  <string name=\"server_requires_auth\">Le serveur requiert une authentification</string>\n  <string name=\"no_forms_uploaded\">Désolé, pas de formulaires téléchargés.</string>\n  <string name=\"upload_results\">Résultats du Téléchargement</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Changer l’affichage</string>\n  <string name=\"show_sent_and_unsent_forms\">Formulaires envoyés et non envoyés</string>\n  <string name=\"show_unsent_forms\">Que des formulaires non envoyés</string>\n  <string name=\"send_in_progress\">L\\'envoi s\\'exécute en tâche de fond, essayer plus tard.</string>\n  <string name=\"forms_upload_succeeded\">Succès de l\\'envoi des formulaires.</string>\n  <string name=\"forms_upload_failed\">Échec de l\\'envoi des formulaires.</string>\n  <string name=\"all_uploads_succeeded\">Succès de tous les envois !</string>\n  <string name=\"some_uploads_failed\">%1$s des %2$s téléversements ont échoué !</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Rafraîchir</string>\n  <string name=\"download\">Télécharger la sélection</string>\n  <string name=\"downloading_data\">Connexion au serveur…</string>\n  <string name=\"fetching_file\">Récupération de %1$s.\\n\\nFormulaire %2$s de %3$s formulaire(s).</string>\n  <string name=\"form_download_progress\">%1$s. Obtention de fichiers multimédia : %2$s de %3$s</string>\n  <string name=\"version\">Version:</string>\n  <string name=\"form_updates_available\">Mise à jour de formulaire disponible</string>\n  <string name=\"newer_version_of_a_form_info\">Ceci est une version mise à jour d\\'un formulaire dont vous disposez déjà !</string>\n  <string name=\"form_update_error\">Échec de mise à jour des formulaires</string>\n  <string name=\"form_update_error_massage\">Ouvrez ODK Collect pour réessayer</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"form_update_notification_title\">Synchronisation des formulaires</string>\n  <string name=\"load_remote_form_error\">Échec de téléchargement de la liste des formulaires</string>\n  <string name=\"forms_download_succeeded\">Succès du téléchargement des formulaires.</string>\n  <string name=\"forms_download_failed\">Échec du téléchargement des formulaires.</string>\n  <string name=\"all_downloads_succeeded\">Succès de tous les téléchargements!</string>\n  <string name=\"some_downloads_failed\">%1$s de %2$s téléchargements ont échoués.</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Version: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Si vous continuez à avoir ce problème, avertissez la personne qui vous a demandé de collecter les données.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect n\\'arrive pas à joindre le serveur à %s. Avez-vous entré l\\'URL correctement ?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect n\\'arrive pas à établir une connexion sécurisée avec le serveur à %s. Avez-vous entré l\\'URL correctement ?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">Le serveur %1$s a retourné le code de réponse %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">Le serveur %s a fourni une réponse invalide.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">Le serveur n\\'a pas fourni d\\'identifiant hash pour le formulaire à télécharger.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_parsing_error\">Ce formulaire ne peut être traité : </string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">Une erreur de disque s\\'est produite en cours du téléchargement de ce formulaire.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">L\\'URL de soumission de ce formulaire n\\'est pas valide.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Formulaires enregistrés</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Formulaires vierges</string>\n  <string name=\"delete_confirm\">Supprimer %s formulaire(s)?</string>\n  <string name=\"delete_file\">Supprimer la sélection</string>\n  <string name=\"delete_no\">Ne pas supprimer</string>\n  <string name=\"delete_yes\">Supprimer ces formulaires</string>\n  <string name=\"form_delete_message\">Supprimer les formulaires sélectionnés</string>\n  <string name=\"file_deleted_ok\">%s formulaire(s) supprimé(s) avec succès.</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Paramètres du projet</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Serveur</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, utilisateur, mot de passe</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Affichage du projet</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Nom, icône, couleur</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Interface utilisateur</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_interface_settings_subtext\">Langue de l’application, taille de la police, navigation</string>\n  <!--Title of a settings section-->\n  <string name=\"maps\">Cartes</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Fond de carte, styles, couches</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Gestion des formulaires</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">Mise à jour, envoi et suppression automatiques</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Identité de l\\'utilisateur et de l\\'appareil</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">Nom d\\'utilisateur, numéro de téléphone et ID de l\\'appareil</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Expérimental </string>\n  <string name=\"protected_settings\">Protégé</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">Déverrouiller les paramètres du projet</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">Changer de mot de passe, définir les contrôles d\\'accès</string>\n  <string name=\"set_admin_password\">Configurer un mot de passe d\\'administration</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Gestion de projet</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">Reconfigurer, réinitialiser, supprimer</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Contrôle d\\'accès</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">Limiter l\\'interface utilisateur</string>\n  <string name=\"server_preferences\">Serveur</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL du serveur</string>\n  <string name=\"url_error\">URL non valable !</string>\n  <string name=\"username\">Nom d\\'utilisateur</string>\n  <string name=\"change_username\">Nom d\\'utilisateur ODK</string>\n  <string name=\"password\">Mot de passe</string>\n  <string name=\"change_password\">Mot de passe ODK</string>\n  <string name=\"password_error_whitespace\">Les espaces ne sont pas permis au début ou à la fin du mot de passe</string>\n  <string name=\"username_error_whitespace\">Les espaces ne sont pas permis au début ou à la fin du nom d\\'utilisateur</string>\n  <string name=\"app_theme\">Thème</string>\n  <string name=\"language\">Langue</string>\n  <string name=\"use_device_language\">Utiliser la langue de l\\'appareil</string>\n  <string name=\"font_size\">Taille de la Police</string>\n  <string name=\"change_font_size\">Taille de la Police</string>\n  <string name=\"font_size_extra_large\">Très grand</string>\n  <string name=\"font_size_large\">Grand</string>\n  <string name=\"font_size_medium\">Normal</string>\n  <string name=\"font_size_small\">Petit</string>\n  <string name=\"font_size_extra_small\">Très petit</string>\n  <string name=\"navigation\">Navigation</string>\n  <string name=\"swipe_navigation\">Utiliser le glissement du doigt à l\\'horizontal</string>\n  <string name=\"buttons_navigation\">Utiliser les boutons suivant/retour</string>\n  <string name=\"swipe_buttons_navigation\">Utiliser le glissement du doigt et les boutons</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Suivant</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Retour</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Mise à jour d\\'un fomulaire</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Mode de mise à jour des formulaires vierges</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Manuel</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Formulaires antérieurement téléchargés uniquement</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Copie exacte du serveur</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Fréquence de Mise à jour automatique</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Toutes les quinze minutes</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Toutes les heures</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Toutes les six heures</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Toutes les 24 heures</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Téléchargement automatique</string>\n  <string name=\"automatic_download_summary\">Télécharger automatiquement les versions mise à jour des formulaires</string>\n  <string name=\"hide_old_form_versions_setting_title\">Cacher les anciennes versions de formulaires</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Seule la version la plus récente apparaîtra dans Remplir un formulaire</string>\n  <string name=\"form_submission_category\">Soumission de formulaire</string>\n  <string name=\"autosend_selector_title\">Envoi automatique</string>\n  <string name=\"autosend\">Envoi automatique</string>\n  <string name=\"off\">Off</string>\n  <string name=\"wifi_autosend\">Seulement Wifi</string>\n  <string name=\"cellular_autosend\">Seulement Cellulaire</string>\n  <string name=\"wifi_cellular_autosend\">Wifi ou Cellulaire</string>\n  <string name=\"delete_after_send\">Supprimer après l\\'envoi</string>\n  <string name=\"delete_after_send_summary\">Suppression des médias et formulaires finalisés après l’envoi au serveur</string>\n  <string name=\"form_filling_category\">Remplissage de formulaire</string>\n  <string name=\"constraint_behavior_title\">Traitement de contraintes</string>\n  <string name=\"constraint_behavior\">Comportement du traitement des contraintes</string>\n  <string name=\"constraint_behavior_on_swipe\">Valider lors du glissement du doigt vers la gauche</string>\n  <string name=\"constraint_behavior_on_finalize\">Reporter la validation tant que le formulaire n\\'est pas finalisé</string>\n  <string name=\"high_resolution_title\">Vidéo Haute Définition</string>\n  <string name=\"high_resolution_summary\">Activer l\\'option vidéos en haute résolution</string>\n  <string name=\"image_size_title\">Taille des images</string>\n  <string name=\"image_size_dialog_title\">Nombre de pixels maximum pour le coté de l\\'image le plus long</string>\n  <string name=\"image_size_large\">Grande (3072px)</string>\n  <string name=\"image_size_medium\">Moyenne (2048px)</string>\n  <string name=\"image_size_small\">Petite (1024px)</string>\n  <string name=\"image_size_very_small\">Très petite (640px)</string>\n  <string name=\"image_size_original\">Taille originale de la caméra (défaut)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Afficher les instructions supplémentaires (guidance hints)</string>\n  <string name=\"guidance_no\">Non</string>\n  <string name=\"guidance_yes\">Oui - toujours visible</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Oui - replié par défaut</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">Enregistrer l\\'audio avec une appli externe</string>\n  <string name=\"form_import_category\">Import de formulaire</string>\n  <string name=\"instance_sync\">Importer les formulaires enregistrés comme terminés</string>\n  <string name=\"instance_sync_summary\">Impacte sur les formulaires ajoutés directement au dossier instances.</string>\n  <string name=\"analytics_preferences\">Données d\\'utilisation</string>\n  <string name=\"analytics\">Collecter des données d\\'utilisation anonymes</string>\n  <string name=\"analytics_summary\">Les données anonymes d\\'utilisation aident l\\'équipe ODK à prioriser les corrections et les nouvelles fonctionnalités</string>\n  <string name=\"form_metadata_title\">Métadonnées du formulaire</string>\n  <string name=\"form_metadata\">Métadonnées du formulaire</string>\n  <string name=\"form_metadata_summary\">Ces valeurs seront ajoutées aux formulaires qui collectent des métadonnées pré-définies (nom d’utilisateur, adresse mail, numéro de téléphone) pour identifier la personne qui soumet des données.</string>\n  <string name=\"user_defined\">Défini par l\\'utilisateur</string>\n  <string name=\"phone_number\">Numéro de téléphone</string>\n  <string name=\"email\">Adresse mail</string>\n  <string name=\"invalid_email_address\">Adresse mail invalide!</string>\n  <string name=\"device_defined\">Défini par l\\'appareil</string>\n  <string name=\"device_id\">ID du périphérique</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Pas disponible</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">Déverrouiller les paramètres</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">Paramètres déverrouillés</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <string name=\"setting_not_available_during_form_entry\">Paramètre non disponible durant la saisie de données dans le formulaire</string>\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Saisissez un nouveau mot de passe</string>\n  <string name=\"admin_password\">Mot de passe de l\\'administrateur</string>\n  <string name=\"admin_password_changed\">Mot de passe administrateur modifié avec succès</string>\n  <string name=\"admin_password_disabled\">Mot de passe administrateur désactivé</string>\n  <string name=\"admin_password_incorrect\">Désolé, le mot de passe n\\'est pas correct !</string>\n  <string name=\"enter_admin_password\">Saisissez le mot de passe administrateur</string>\n  <string name=\"change_admin_password\">Modifier le mot de passe de l\\'administrateur</string>\n  <string name=\"show_password\">Afficher le mot de passe</string>\n  <string name=\"found_in_main\">Décocher pour cacher depuis le menu principal</string>\n  <string name=\"found_in_settings\">Décocher pour cacher depuis les paramètres</string>\n  <string name=\"form_entry\">Décocher pour cacher pendant la saisie de données dans le formulaire</string>\n  <string name=\"form_end\">Décocher pour cacher à la fin du formulaire</string>\n  <string name=\"reset_project_settings_title\">Réinitialiser</string>\n  <string name=\"reset_project_settings_subtext\"> Choisir entre les paramètres, formulaires et données</string>\n  <string name=\"delete_project\">Supprimer</string>\n  <string name=\"select_all\">Tout sélectionner</string>\n  <string name=\"clear_all\">Tout désélectionner </string>\n  <string name=\"reset_settings\">Tous les paramètres (configuration interne, paramètres enregistrés)</string>\n  <string name=\"reset_saved_forms\">Formulaires enregistrés (dossier instances, base de données instances)</string>\n  <string name=\"reset_blank_forms\">Formulaires vierges (dossier de formulaires, base de données de formulaires, base de données de jeux de données)</string>\n  <string name=\"reset_layers\">Couches cartographiques (dossier des couches)</string>\n  <string name=\"reset_cache\">Cache pour chargement de formulaire (dossier .cache)</string>\n  <string name=\"reset_settings_dialog_title\">Sélectionnez ce que vous souhaitez réinitialiser</string>\n  <string name=\"reset_settings_button_reset\">Réinitialiser</string>\n  <string name=\"reset_in_progress\">Réinitialisation en cours…</string>\n  <string name=\"reset_app_state_result\">Résultats de la réinitialisation</string>\n  <string name=\"reset_app_state_warning\">Toutes les données sélectionnées seront supprimées. Cette action ne peut pas être annulée.</string>\n  <string name=\"reset_settings_result\">Tous les paramètres :: %s</string>\n  <string name=\"reset_saved_forms_result\">Formulaires enregistrés :: %s</string>\n  <string name=\"reset_blank_forms_result\">Formulaires vierges :: %s</string>\n  <string name=\"reset_layers_result\">Couches cartographiques :: %s</string>\n  <string name=\"reset_cache_result\">Cache pour chargement de formulaire :: %s</string>\n  <string name=\"main_menu_settings\">Paramètres du Menu principal</string>\n  <string name=\"main_menu_settings_summary\">Montrer ou cacher boutons</string>\n  <string name=\"user_settings\">Paramètres utilisateurs</string>\n  <string name=\"user_setting_summary\">Montrer ou cacher paramètres</string>\n  <string name=\"form_entry_setting\">Paramètres de réponse au formulaire</string>\n  <string name=\"form_entry_settings_summary\">Montrer ou cacher actions</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Retour en arrière</string>\n  <string name=\"moving_backwards_disabled_title\">Retour en arrière désactivé</string>\n  <string name=\"moving_backwards_disabled_message\">Configurer l\\'appareil pour empêcher les utilisateurs de contourner ce paramètre ?\\n\\nLes modifications sont les suivantes :\\n\\u2022 Désactiver la modification du brouillon. \\n\\u2022 Désactiver l\\'enregistrement comme brouillon. \\n\\u2022 Désactiver Aller à la ligne de saisie. \\n\\u2022 Régler le traitement des contraintes de validation lors du passage à l\\'écran suivant.</string>\n  <string name=\"yes\">Oui</string>\n  <string name=\"no\">Non</string>\n  <string name=\"moving_backwards_enabled_title\">Retour en arrière activé</string>\n  <string name=\"moving_backwards_enabled_message\">Vous souhaiterez peut-être revoir les paramètres suivants:\\n\\n\\u2022 Modifier le brouillon\\n\\n\\n- Enregistrer comme brouillon\\n\\u2022Aller à la ligne de saisie\\n\\u2022Traitement des contraintes</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Enregistrer comme brouillon</string>\n  <string name=\"save_mid_summary\">Icône d\\'enregistrement dans la barre supérieure et bouton \\\"Enregistrer comme brouillon\\\" en quittant le formulaire</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">À propos</string>\n  <string name=\"tell_your_friends\">Partager ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Vos collègues continuent-ils à collecter des données sur papier ? Partagez ODK Collect avec eux.</string>\n  <string name=\"leave_a_review\">Laissez un avis sur Play Store</string>\n  <string name=\"leave_a_review_msg\">Votre avis (ce qu\\'on espère est positif) augmente la visibilité de l\\'application dans le Play Store.</string>\n  <string name=\"odk_website\">Visiter le site web de Open Data Kit</string>\n  <string name=\"odk_website_summary\">ODK est utilisé pour collecter des données pour le bien commun dans des environnements contraignantes.</string>\n  <string name=\"odk_forum\">Rejoindre le Forum ODK</string>\n  <string name=\"odk_forum_summary\">Rejoignez le forum pour obtenir de l\\'aide, demander des fonctionnalités, contribuer au code et aux traductions! </string>\n  <string name=\"all_open_source_licenses\">Licences et librairies open source</string>\n  <string name=\"all_open_source_licenses_msg\">Nous sommes debout sur les épaules de géants.</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">Reconfigurer avec un code QR</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">Remplacer tous les paramètres existants</string>\n  <string name=\"qr_code\">Configuration par code QR</string>\n  <string name=\"scan_qr_code_fragment_title\">Lire code</string>\n  <string name=\"view_qr_code_fragment_title\">Code QR</string>\n  <string name=\"import_qrcode_sd\">Importer code QR</string>\n  <string name=\"successfully_imported_settings\">Paramètres importés avec succès</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Limite maximale de caractères atteinte: le code QR ne peut pas être généré pour tous les paramètres</string>\n  <string name=\"share\">Partager</string>\n  <string name=\"turn_on_flashlight\">Allumer la lampe torche</string>\n  <string name=\"turn_off_flashlight\">Eteindre la lampe torche</string>\n  <string name=\"qrcode_with_both_passwords\">Contient des informations sensibles: mots de passe <b>administrateur</b> et <b>serveur</b></string>\n  <string name=\"qrcode_without_passwords\">Ce code QR ne contient pas vos mots de passe administrateur ou serveur. Appuyez pour configurer</string>\n  <string name=\"qrcode_with_admin_password\">Contient de l\\'information sensible: mot de passe <b>administrateur</b></string>\n  <string name=\"qrcode_with_server_password\">Contient de l\\'information sensible: mot de passe <b>serveur</b></string>\n  <string name=\"server_password\">Mot de passe serveur</string>\n  <string name=\"include_password_dialog\">Mots de passe inclus dans le Code</string>\n  <string name=\"generate\">Générer</string>\n  <string name=\"invalid_qrcode\">Le code QR ne contient pas de paramètres valides</string>\n  <string name=\"qr_code_not_found\">Le code QR n\\'a pas été trouvé dans l\\'image sélectionnée. </string>\n  <string name=\"corrupt_imported_preferences_error\">Les paramètres actuels sont corrompus. À partir des paramètres de gestion de projet, réinitialisez les paramètres ou importez-en d\\'autres qui fonctionnent.</string>\n  <string name=\"settings_with_gd_protocol\">Les projets basés sur Google Drive/Sheets ne peuvent plus être créés.</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Annuler</string>\n  <string name=\"cancel_loading_form\">Annuler</string>\n  <string name=\"cancel_location\">Annuler</string>\n  <string name=\"do_not_change\">Annuler</string>\n  <string name=\"canceling\">En train d\\'annuler</string>\n  <string name=\"notification_channel_name\">Notifications générales</string>\n  <string name=\"success\">Succès</string>\n  <string name=\"error_occured\">Une erreur s\\'est produite</string>\n  <string name=\"please_wait\">Veuillez patienter.</string>\n  <string name=\"please_wait_long\">Veuillez patienter. Cette action peut prendre quelques minutes.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Impossible de supprimer \\'%s\\'. Merci de supprimer le fichier manuellement et de re-télécharger le formulaire.</string>\n  <string name=\"fs_create_media_folder_error\">Impossible de créer le dossier multimédia: %s</string>\n  <string name=\"fs_file_copy_error\">Impossible de copier \\'%1$s\\' sur \\'%2$s\\'. Raison: %3$s</string>\n  <string name=\"select_odk_shortcut\">Sélectionnez le raccourci ODK</string>\n  <string name=\"background_audio_help\">Ce formulaire enregistre le son du micro de votre appareil.\\n\\nVous pouvez utiliser l\\'indicateur de volume pour vous assurer que le micro est assez proche de son que vous devez enregistrer.\\n\\nQuittez le formulaire pour arrêter l\\'enregistrement.\\n\\nPour plus d\\'informations contacter la personne responsable de cette collecte de données.</string>\n  <string name=\"loading\">Chargement…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Projets</string>\n  <string name=\"settings\">Paramètres</string>\n  <string name=\"add_project\">Ajouter un projet</string>\n  <string name=\"add\">Ajouter</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Projet existe déjà</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">Vous avez déjà un projet avec ces paramètres de connexion. Voulez vous basculer sur ce projet existant ou en créer un nouveau ?</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Ajouté projet répété</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Basculer sur le projet existant</string>\n  <string name=\"project_name\">Nom du projet</string>\n  <string name=\"project_icon\">Icône du projet</string>\n  <string name=\"project_color\">Couleur du projet </string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Changé à %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">Projet %s en utilisation</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Changer à %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <string name=\"delete_project_dialog_title\">Supprimer %s</string>\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <string name=\"delete_project_message\">En supprimant ce projet, vous supprimerez</string>\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <string name=\"delete_project_instructions\">Tapez %1$s pour confirmer cette action.</string>\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <string name=\"delete_trigger\">supprimer</string>\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <string name=\"form_definitions_count\">Formulaires vierges: %1$d</string>\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <string name=\"sent_count\">Envoyés: %1$d</string>\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <string name=\"unsent_count\">Prêts à envoyer: %1$d</string>\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <string name=\"drafts_count\">Brouillons: %1$d</string>\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"delete_project_confirm_button_text\">Supprimer projet</string>\n  <string name=\"or\">OU</string>\n  <string name=\"hex_color\">Couleur hex</string>\n  <string name=\"invalid_hex_code\">Code de couleur hex n\\'est pas valide</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Collecte de données\\nsans frontières</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Configurer par code QR</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Saisir les détails du projet</string>\n  <string name=\"project_config_tip\">Après avoir ajouté un projet, vous pouvez le configurer dans les Paramètres</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Pas encore de projet?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Essayer une démo</string>\n  <string name=\"scan_configuration_qr_code\">Scanner un code QR de configuration</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Montrer détails</string>\n  <string name=\"errors\">Erreurs</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <string name=\"no_browser_installed\">Aucun navigateur installé!</string>\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">Pour ouvrir ce formulaire, vous devez d\\'abord ouvrir ODK Collect et basculer sur le projet qui le contient.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">ODK Collect n\\'a pas été configuré. Essayez d\\'ouvrir ODK Collect et configure le.\\n\\nSi vous avez cliqué sur un raccourci, vous devrez le recréer après avoir configuré ODK Collect.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">URI non reconnu: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Mauvais URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Ce formulaire rempli ne peut pas être ouvert parce qu\\'il a été supprimé hors de l\\'application.</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">Impossible d\\'éditer ce brouillon car le formulaire vierge correspondant n\\'est pas présent ou a été supprimé. \\n\\\\nFrom ID : %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Vous avez téléchargé deux formulaires différents avec le même numéro d\\'identification et la même version. Peut-être que le même formulaire a été téléchargés deux fois ou à deux serveurs différents. Dans tous les cas, vous devez en supprimer un.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <string name=\"cannot_open_form_because_of_forms_update\">Ce formulaire ne peut pas être ouvert parce qu\\'une mise à jour de formulaires est en cours</string>\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Ouvrir les paramètres</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <string name=\"entities_title\">Listes d\\'entités</string>\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <string name=\"entity_browser_button\">Voir liste d\\'entités</string>\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">Hors ligne</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">Ce formulaire n\\'est pas supporté par cette version d\\'ODK. Merci de mettre à jour Collect. Si vous continuez à avoir ce problème, faites-le savoir à la personne qui vous a demandé de collecter des données.\n\\n\\nVersion de la spécification des Entités de Formulaire : %s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">L\\'application a rencontré un problème durant l\\'usage précédent!</string>\n  <string name=\"cant_start_app\">Impossible de démarrer l\\'application!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">Ce projet était précédemment connecté à un compte Google Drive. Le support de Google Drive a été supprimé. Vous pouvez configurer un serveur ou récupérer les soumissions sur un ordinateur. </string>\n  <string name=\"learn_more_button_text\">En savoir plus</string>\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">Outils pour développeurs</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">Faire planter l\\'application</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Forcer une exception qui fera planter l\\'application </string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <string name=\"debug_filters\">Filtres de débogage</string>\n  <string name=\"debug_filters_summary\">Afficher des informations de débogage pour les filtres pendant la saisie du formulaire</string>\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"use_zxing_for_barcode_scanning\">Retourner à l\\'ancien scanner de codes-barres</string>\n  <string name=\"permission_dialog_title\">A propos des permissions</string>\n  <string name=\"permission_dialog_text\">Il vous sera demandé d\\'autoriser ODK Collect à accéder aux fonctions ci-dessous, sélectionnez \\\"autoriser\\\" si vous souhaitez les utiliser.</string>\n  <string name=\"notifications\">Notifications</string>\n  <string name=\"required_for_notifications\">Requis pour afficher les mises à jours quand les formulaires sont téléchargés, mis à jour ou envoyés.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">Votre formulaire a été enregistré comme brouillon</string>\n  <string name=\"form_saved\">Votre formulaire a été enregistré.</string>\n  <string name=\"form_sending\">Envoi du formulaire en cours…</string>\n  <string name=\"edit_form\">Éditer</string>\n  <string name=\"view_form\">Voir</string>\n  <string name=\"close_snackbar\">Fermer la barre de message</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">Dernier formulaire envoyé : %d seconde auparavant</item>\n    <item quantity=\"many\">Dernier formulaire envoyé : %d secondes auparavant</item>\n    <item quantity=\"other\">Dernier formulaire envoyé : %d secondes auparavant</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"one\">Dernier formulaire envoyé : %d minute auparavant</item>\n    <item quantity=\"many\">Dernier formulaire envoyé : %d minutes auparavant</item>\n    <item quantity=\"other\">Dernier formulaire envoyé : %d minutes auparavant</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"one\">Dernier formulaire envoyé : %d heure auparavant</item>\n    <item quantity=\"many\">Dernier formulaire envoyé : %d heures auparavant</item>\n    <item quantity=\"other\">Dernier formulaire envoyé : %d heures auparavant</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"one\">Dernier formulaire envoyé : %d jour auparavant</item>\n    <item quantity=\"many\">Dernier formulaire envoyé : %d jours auparavant</item>\n    <item quantity=\"other\">Dernier formulaire envoyé : %d jours auparavant</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"one\">%d formulaire prêt à être envoyé</item>\n    <item quantity=\"many\">%d formulaires prêts à être envoyés</item>\n    <item quantity=\"other\">%d formulaires prêts à être envoyés</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">Dans les versions futures, les formulaires finalisés ne seront plus modifiables. Enregistrez les formulaires comme brouillons pour les modifier plus tard.\\n\\nVous pouvez vérifier la présence d\\'erreurs dans un brouillon en tapant les trois points (⋮) puis Vérifier les erreurs</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">Dans les versions futures, les formulaires finalisés ne seront plus modifiables. </string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">Finaliser tous les brouillons</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"one\">Voulez vous finaliser %d brouillon ?</item>\n    <item quantity=\"many\">Voulez vous finaliser %d brouillons ?</item>\n    <item quantity=\"other\">Voulez vous finaliser %d brouillons ?</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">Une fois finalisés, les formulaires seront disponibles du menu \\\"Prêt à envoyer\\\" et vous ne pourrez plus les éditer. Tous brouillons comportant des erreurs ne seront pas finalisés.\\n\\nVous ne pourrez pas annuler cette action.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"one\">Succès ! %d brouillon finalisé.</item>\n    <item quantity=\"many\">Succès ! %d brouillons finalisés.</item>\n    <item quantity=\"other\">Succès ! %d brouillons finalisés.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"one\">%d brouillon contient des erreurs qui doivent être corrigées avant de finalisation.</item>\n    <item quantity=\"many\">%d brouillons contiennent des erreurs qui doivent être corrigées avant de finalisation.</item>\n    <item quantity=\"other\">%d brouillons contiennent des erreurs qui doivent être corrigées avant de finalisation.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d brouillons finalisés. %d brouillons contiennent des erreurs qui doivent être corrigées avant de finaliser.</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d brouillons finalisés. Les brouillons restants doivent être finalisés manuellement.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Erreurs</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">Pas d\\'erreur</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">Décochez pour masquer depuis les brouillons.</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">Rien à afficher</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">Aucun Formulaire Vierge</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">Télécharger un formulaire pour démarrer</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">Aucun brouillon</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">Quand vous enregistrez comme brouillon, les formulaires apparaissent ici</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">Rien de prêt à envoyer</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">Quand vous finalisez vos brouillons, ils apparaissent ici</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">Rien à envoyer</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">Quand vous envoyez des formulaires finalisés, ils apparaissent ici</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">Pas de formulaire à télécharger </string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">Télécharger un formulaire pour démarrer</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">Pas de formulaire à supprimer</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">Quand vous aurez téléchargé des formulaires vierges, ils apparaîtront ici</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">Quand vous aurez enregistré des formulaires, ils apparaîtront ici</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">Récupérer votre travail ?</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"savepoint_recovery_dialog_message\">\\'Collect s\\'\\'est arrété de manière inattendue le \\'EEE dd MMM yyyy\\' à \\'HH\\'h\\'mm\\' et a sauvegardé votre travail.\\n\\nVoulez vous le récupérer ou l\\'\\'ignorer ?\\'</string>\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">Récupérer ?</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Abandoner changements</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">Créer le point</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">Voir ou modifier le point</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">Voir le point</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">Modifier le point</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">Créer la ligne</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">Voir ou modifier la ligne</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">Voir la ligne</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">Créer le polygone</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">Voir ou modifier le polygone</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">Voir le polygone</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <string name=\"settings_json\">JSON de paramètres</string>\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <string name=\"preparing_form_edit\">Préparation du formulaire pour édition…</string>\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <string name=\"user_visible_instance_name\">%s (modification %d)</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_title\">Continuer d\\'éditer le brouillon ?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_draft_edit_found_dialog_message\">\\'Il existe un brouillon de modification en cours sauvegardé le\\' EEE dd MMM yyyy \\'à\\' HH\\'h\\'mm\\'\\n\\nVoulez-vous le reprendre?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_positive_button\">Reprendre l\\'ébauche</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_title\">Modifier la dernière version?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_finalized_edit_found_dialog_message\">\\'Cette soumission a déjà été modifiée.\\n\\nAimeriez-vous modifier la dernière version finalisée le\\' EEE dd MMM yyyy \\'à\\' HH\\'h\\'mm\\'?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_positive_button\">Modifier la dernière version</string>\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n  <string name=\"finalized_form_edit_started\">Vous êtes en mode d\\'édition. Appuyez sur une question pour la modifier.</string>\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-hi/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">संस्करण: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">EEE, MMM dd, yyyy \\'पर जोड़ा\\' HH:mm \\'पर\\' </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">EEE, MMM dd, yyyy \\' पर संरक्षित\\' HH:mm \\'पर\\' </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">EEE, MMM dd, yyyy \\'पर फाइनल हुआ\\'  HH:mm \\'पर\\' </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">EEE, MMM dd, yyyy \\'पर भेजा गया\\' HH:mm \\'पर\\' </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">EEE, MMM dd, yyyy \\'पर भेजना विफल रहा\\' HH:mm \\'पर\\' </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">EEE, MMM dd, yyyy \\'पर मिटाया\\'  HH:mm \\'पर\\' </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">EEE, MMM dd, yyyy \\'पर संशोधित\\' HH:mm \\'पर\\' </string>\n  <string name=\"encrypted_form\">एन्क्रिप्ट किए गए फॉर्म</string>\n  <string name=\"deleted_form\">समाप्त फॉर्म</string>\n  <string name=\"sort_the_list\">सूची को सॉर्ट करें</string>\n  <string name=\"sort_by_name_asc\">नाम, A-Z</string>\n  <string name=\"sort_by_name_desc\">नाम, Z-A</string>\n  <string name=\"sort_by_date_desc\">दिनांक, नयी पहले </string>\n  <string name=\"sort_by_date_asc\">दिनांक, सबसे पुराना पहले</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">सूची को फ़िल्टर करें</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">खोज</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$dफॉर्म जोड दिया.</string>\n  <string name=\"finished_disk_scan\">स्कैनिंग समाप्त. सभी फॉर्म लोड हो गए</string>\n  <string name=\"xform_parse_error\">%1$sके लिए XForm पार्स गलती: %2$sलापता या अमान्य है. </string>\n  <string name=\"loading_form\">फॉर्म लोड हो रहा है </string>\n  <string name=\"survey_loading_reading_form_message\">फॉर्म की परिभाषा पढ़ रहा…</string>\n  <string name=\"survey_loading_reading_data_message\">सर्वे डेटा पढ़ रहा… </string>\n  <string name=\"survey_loading_reading_csv_message\">सीएसवी फाइल पढ़ रहा…</string>\n  <string name=\"parse_error\">क्षमा करें, फॉर्म पार्स करने में असमर्थ</string>\n  <string name=\"cannot_edit_completed_form\">एक बार फाइनल होने के बाद यह फॉर्म संशोधित नहीं किया जा सकता. इसे एन्क्रिप्ट किया जा सकता है.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">सहेजे गए परिवर्तन से बचे हुए परिवर्तनों को लिया गया!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">भाषा बदलें</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Prompt पर जायें</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">शुरु पर जाएं</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">अंत पर जाएं</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">ऊपर जाएं</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"exit\">बाहर आयें</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Prompt को संशोधित करें</string>\n  <string name=\"clearanswer_confirm\">\\\"%s\\\" के लिए प्रतिउत्तर को हटायें क्या?</string>\n  <string name=\"clear_answer\">प्रतिउत्तर हटायें</string>\n  <string name=\"clear_answer_ask\"> यह प्रतिउत्तर हटायें क्या?</string>\n  <string name=\"clear_answer_no\">रद्द करें</string>\n  <string name=\"discard_answer\">प्रतिउत्तर हटायें</string>\n  <string name=\"discard_group\">समूह हटायें</string>\n  <string name=\"delete_repeat\">समूह हटायें</string>\n  <string name=\"delete_repeat_ask\">यह समूह हटायें?</string>\n  <string name=\"delete_repeat_confirm\">समूह  \\\"%s\\\" को हटायें और इसके अन्दर के समूहों को भी?</string>\n  <string name=\"delete_repeat_no\">रद्द करें</string>\n  <string name=\"invalid_answer_error\">क्षमा करें, प्रतिउत्तर अमान्य है </string>\n  <string name=\"required_answer_error\">क्षमा करें , यह प्रतिउत्तर जरुरी है</string>\n  <string name=\"survey_internal_error\">आतंरिक त्रुटि: prompt के स्टेप असफल.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">आप %s के समापन पर हैं</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">क्षमा करें, फॉर्म संरक्षण असफल!</string>\n  <string name=\"data_saved_ok\">फॉर्म सफलतापूर्वक संरक्षित!</string>\n  <string name=\"save_point_error\">रिकवरी बिंदु %sसंरक्षण में त्रुटि है  </string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">बदलाव संरक्षित करें</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">फॉर्म संरक्षित हो रहा है </string>\n  <string name=\"survey_saving_validating_message\">उत्तर मान्य कर रहा है…</string>\n  <string name=\"survey_saving_collecting_message\">डेटा एकत्रित कर रहा है…</string>\n  <string name=\"survey_saving_saving_message\">एसडी कार्ड में सहेज रहा है…</string>\n  <string name=\"survey_saving_finalizing_message\">एसडी कार्ड में फाइनल कर रहा है …</string>\n  <string name=\"survey_saving_encrypting_message\">डेटा एन्क्रिप्ट कर रहा है…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">इस इंस्टैंस के लिए बिल्कुल सही एक रिकॉर्ड नहीं है!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">jr_form_id से एक खाली फॉर्म भी सही मैच नहीं करता है।</string>\n  <string name=\"no_form_id_specified\">कोई FormId निर्दिष्ट नहीं ???</string>\n  <string name=\"phone_does_not_support_rsa\">फोन RSA एन्क्रिप्शन का समर्थन नहीं करता है</string>\n  <string name=\"invalid_rsa_public_key\">अमान्य RSA पब्लिक कुंजी</string>\n  <string name=\"encryption_error_message\">%s फॉर्म फाइनल के जैसे संरक्षित नहीं गया है.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">जारी करें</string>\n  <string name=\"activity_not_found\">%s:के नियंत्रण के लिए कोई गतिविधि नहीं  मिली</string>\n  <string name=\"no_app\">अनुरोधित एप्लिकेशन लापता है कृपया रीडिंग स्वयं दर्ज करें।</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Url खोलें</string>\n  <string name=\"capture_osm\">OpenMapKit लॉन्च करें </string>\n  <string name=\"recapture_osm\">OSM टैगिंग दुबारा करें</string>\n  <string name=\"edited_osm_file\">संपादित OSM XML फाइल:</string>\n  <string name=\"invalid_osm_data\">कुछ गलत हो गया। हमें मान्य OSM डेटा नहीं मिला</string>\n  <string name=\"alert\">चेतावनी</string>\n  <string name=\"install_openmapkit\">कृपया ओपेन मैप किट  इनस्टॉल करें!</string>\n  <string name=\"get_barcode\">बारकोड प्राप्त करें</string>\n  <string name=\"replace_barcode\">बारकोड बदलें</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"invalid_range_widget\">इस विजेट में अमान्य मापदंड हैं!</string>\n  <string name=\"capture_image\">फोटो खींचें</string>\n  <string name=\"choose_image\">इमेज चुनें</string>\n  <string name=\"selected_invalid_image\">चयनित फाइल मान्य इमेज नहीं है</string>\n  <string name=\"take_picture_instruction\">तस्वीर लेने के लिए स्क्रीन को टैप करें</string>\n  <string name=\"annotate_image\">इमेज पर टिप्पणी करें</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">संरक्षित करें और बंद करें</string>\n  <string name=\"sign_button\">हस्ताक्षर  इकट्ठा  करें</string>\n  <string name=\"markup_image\">मार्कअप इमेज</string>\n  <string name=\"draw_image\">स्केच इमेज</string>\n  <string name=\"reset_image\">रीसेट</string>\n  <string name=\"set_color\">रंग सेट करें</string>\n  <string name=\"capture_video\">विडियो रिकार्ड करे</string>\n  <string name=\"choose_video\">वीडियो चुनें</string>\n  <string name=\"play_video\">वीडियो चलाएं</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">आवाज़ रिकार्ड करे </string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">ध्वनि चुनें</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">मीडिया को जोड़ते समय एक गलती हुई%s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">जवाब चुनें</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">चयनित: </string>\n  <string name=\"trigger\">ओके। कृपया जारी रखें।</string>\n  <string name=\"ext_file_no_data_error\">फाइल में कोई डेटा नहीं!</string>\n  <string name=\"ext_conflicting_columns_error\">कॉलम %sमैच!</string>\n  <string name=\"ext_sortBy_numeric_error\">आपके sortby कॉलम में केवल संख्यात्मक मान शामिल होने चाहिए। ली गयी वैल्यू \\'%s\\' थी </string>\n  <string name=\"ext_search_wrong_arguments_error\">खोज () फ़ंक्शन में सिंटैक्स त्रुटि: फ़ंक्शन में 1, 4 या 6 आर्गुमेंट की आवश्यकता है। \\'</string>\n  <string name=\"ext_search_wrong_function_error\">खोज () फ़ंक्शन में सिंटैक्स त्रुटि: %s फ़ंक्शन की की पहचान नहीं\\' </string>\n  <string name=\"ext_search_bad_function_error\">खोज () फ़ंक्शन में सिंटैक्स त्रुटि: %s का मूल्यांकन फ़ंक्शन के रूप में नहीं किया गया</string>\n  <string name=\"ext_search_return_error\">खोज हैंडलर ने \\'%s\\' प्रकार का ऑब्जेक्ट वापस कर दिया .</string>\n  <string name=\"ext_import_generic_error\"> %1$sसे डेटा आयात नहीं किया जा सका . कारण: %2$s</string>\n  <string name=\"ext_import_progress_message\">\\'%1$s\\' से पूर्व लोडेड डेटा, कृपया प्रतीक्षा करें… %2$s</string>\n  <string name=\"ext_import_finalizing_message\">पूर्व लोड किए डेटा को फाइनल कर रहा …</string>\n  <string name=\"ext_import_completed_message\">पूर्व लोड किए गए डेटा को फाइनल कर रहा …</string>\n  <string name=\"ext_not_initialized_error\">एक्सटर्नल डाटा मैनेजर को प्रारंभ नहीं किया गया है।</string>\n  <string name=\"ext_import_csv_missing_error\">%1$sके लिए बाह्य डेटा आयात नहीं किया गया.  शायद आप अपने फॉर्म के साथ%2$s.csv फाइल को शामिल करना भूल गए? </string>\n  <string name=\"ext_search_generic_error\">खोज () फ़ंक्शन: %sमें सिंटैक्स त्रुटि</string>\n  <string name=\"ext_assign_value_error\">\\'%s\\' पर वैल्यू नहीं दे सकता.</string>\n  <string name=\"parser_exception\">XPathParser अपवाद:\\\"%s\\\"</string>\n  <string name=\"get_bearing\">बीअरिंग रिकॉर्ड करें</string>\n  <string name=\"replace_bearing\">असर बदलें</string>\n  <string name=\"getting_bearing\">लोडिंग असर</string>\n  <string name=\"accept_bearing\">रिकॉर्ड असर</string>\n  <string name=\"bearing\">बीअरिंग: %.3f</string>\n  <string name=\"direction\">दिशा: %s</string>\n  <string name=\"select_date\">तारीख़ चुनें</string>\n  <string name=\"select_time\">समय चुनें</string>\n  <string name=\"no_date_selected\">कोई दिनांक चयनित नहीं</string>\n  <string name=\"no_time_selected\">कोई समय चयनित नहीं</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">फाइल: %s अमान्य है</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">फाइल: %sनहीं मिल रही</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB डार्क मैटर</string>\n  <string name=\"basemap\">बेसमैप</string>\n  <string name=\"streets\">स्ट्रीट</string>\n  <string name=\"terrain\"> भूभाग</string>\n  <string name=\"hybrid\">हाइब्रिड </string>\n  <string name=\"satellite\">उपग्रह</string>\n  <string name=\"google_play_services_error_occured\">गूगल मानचित्र तक पहुंचने में असमर्थ क्या गूगल प्ले सर्विस इनस्टॉल है?</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">क्षमा करें, लोकेशन प्रदाता सक्षम नही हैं!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">प. %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">पू. %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">द. %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">उ. %1$s%2$s%3$s</string>\n  <string name=\"start\">शुरु करे</string>\n  <string name=\"enable_gps\">जीपीएस सक्षम करें</string>\n  <string name=\"geo_clear_warning\">फीचर पहले से ही बना है. क्या आप फीचर को क्लियर करना चाहेंगे?</string>\n  <string name=\"geo_exit_warning\">परिवर्तनों को रद्द करें और वापस ODK पर लौटें?</string>\n  <string name=\"polygon_validator\">बहुभुज बनाने के लिए कम से कम 3 पॉइंट होने चाहिए</string>\n  <string name=\"clear\">स्पष्ट</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"geopoint_no_draggable_instruction\">मार्कर बटन जोड़ें पर टैप करें</string>\n  <string name=\"discard\">छोड़ दें </string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">फॉर्म भेजा जा रहा हे</string>\n  <string name=\"noselect_error\">क्षमा करें, आपने कोई फॉर्म नही चुना है!</string>\n  <string name=\"no_connection\">कोई नेटवर्क कनेक्शन उपलब्ध नहीं</string>\n  <string name=\"sending_items\">फॉर्म(s)  %1$s/%2$s भेजे जा रहे हैं</string>\n  <string name=\"send_selected_data\">चुने हुए फॉर्म भेजें</string>\n  <string name=\"server_auth_credentials\">सर्वर के लिए प्रयोगकर्ता नाम या पासवर्ड अमान्य है: %s</string>\n  <string name=\"server_requires_auth\">सर्वर प्रमाणीकरण की आवश्यकता</string>\n  <string name=\"no_forms_uploaded\">क्षमा करें, कोई फॉर्म अपलोड नहीं किया गया.</string>\n  <string name=\"upload_results\">परिणाम अपलोड करें</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">व्यू बदलें</string>\n  <string name=\"show_sent_and_unsent_forms\">भेजे गए और न भेजे गए फॉर्म दिखाएं</string>\n  <string name=\"show_unsent_forms\">नहीं भेजे हुए फॉर्म दिखाएं</string>\n  <string name=\"send_in_progress\">पृष्ठभूमि में भेजना जारी, कृपया शीघ्र पुनः प्रयास करें</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">रिफ्रेश </string>\n  <string name=\"download\">चयनित प्राप्त करें</string>\n  <string name=\"downloading_data\">सर्वर से कनेक्ट हो रहा है</string>\n  <string name=\"fetching_file\">%3$sफॉर्म(s) का%1$s.\\n\\nफॉर्म %2$sमिल रहा है.</string>\n  <string name=\"form_download_progress\">%1$s. %3$sकी %2$sमीडिया फाइलें मिल रही हैं: </string>\n  <string name=\"version\">संस्करण:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">संरक्षित फॉर्म</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">खाली फॉर्म</string>\n  <string name=\"delete_confirm\">फॉर्म(s)%s मिटाएं क्या?</string>\n  <string name=\"delete_file\">चयन मिटाएं</string>\n  <string name=\"delete_no\">न मिटाएं</string>\n  <string name=\"delete_yes\">फॉर्म मिटाएं</string>\n  <string name=\"file_deleted_ok\">%s फॉर्म(s) सफलतापूर्वक मिटाये!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">सर्वर</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">यूजर इंटरफेस</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">फॉर्म मैनेजमेंट</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">प्रयोगकर्ता और डिवाइस पहचान</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">सर्वर सेटिंग</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">सर्वर URL</string>\n  <string name=\"url_error\">क्षमा करें,अमान्य URL!</string>\n  <string name=\"username\">प्रयोगकर्ता नाम </string>\n  <string name=\"change_username\">ODK उपयोगकर्ता नाम</string>\n  <string name=\"password\">पासवर्ड</string>\n  <string name=\"change_password\">odk पासवर्ड</string>\n  <string name=\"password_error_whitespace\">पासवर्ड में अग्रणी या अनुगामी व्हाइटस्पेस की अनुमति नहीं है</string>\n  <string name=\"username_error_whitespace\">उपयोगकर्ता नामों में अग्रणी या अनुगामी श्वेत स्थान की अनुमति नहीं है</string>\n  <string name=\"language\">भाषा</string>\n  <string name=\"use_device_language\">डिवाइस की भाषा प्रयोग करें </string>\n  <string name=\"font_size\">अक्षर फॉन्ट आकार</string>\n  <string name=\"change_font_size\">अक्षर फॉन्ट आकार</string>\n  <string name=\"font_size_extra_large\">ज्यादा बड़ा</string>\n  <string name=\"font_size_large\">बड़ा</string>\n  <string name=\"font_size_medium\">मझला</string>\n  <string name=\"font_size_small\">छोटा</string>\n  <string name=\"font_size_extra_small\">ज्यादा छोटा</string>\n  <string name=\"navigation\">पथ प्रदर्शन</string>\n  <string name=\"swipe_navigation\">क्षैतिज स्वाइप प्रयोग करें</string>\n  <string name=\"buttons_navigation\">आगे / पीछे बटन का प्रयोग करें</string>\n  <string name=\"swipe_buttons_navigation\">स्वाइप और बटन का प्रयोग करें</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">आगामी</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"form_submission_category\">फॉर्म भेजना</string>\n  <string name=\"autosend_selector_title\">स्वतः भेजें</string>\n  <string name=\"autosend\">स्वतः भेजें</string>\n  <string name=\"off\">बंद </string>\n  <string name=\"wifi_autosend\">केवल वाई-फाई </string>\n  <string name=\"cellular_autosend\"> केवल सेलुलर</string>\n  <string name=\"wifi_cellular_autosend\">वाई-फाई या सेलुलर</string>\n  <string name=\"delete_after_send\">भेजने के बाद मिटायें</string>\n  <string name=\"delete_after_send_summary\">सर्वर को भेजने के बाद अंतिम फॉर्म और मीडिया मिटाता है</string>\n  <string name=\"form_filling_category\">फॉर्म भरना</string>\n  <string name=\"constraint_behavior_title\">बाधा प्रसंस्करण</string>\n  <string name=\"constraint_behavior\">प्रसंस्करण व्यवहार बाधा </string>\n  <string name=\"constraint_behavior_on_swipe\">फारवर्ड  स्वाइप पर मान्य करें</string>\n  <string name=\"constraint_behavior_on_finalize\">जब तक फाइनल नही होता मान्यता नहीं दें</string>\n  <string name=\"high_resolution_title\">उच्च रेज़ोल्युशन वीडियो</string>\n  <string name=\"high_resolution_summary\">उच्च-रिज़ॉल्यूशन वीडियो रिकॉर्डिंग सक्षम करें</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_no\">नहीं</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">फॉर्म आयात </string>\n  <string name=\"analytics_preferences\">डेटा का उपयोग</string>\n  <string name=\"analytics\">अनाम उपयोग डेटा एकत्र करें</string>\n  <string name=\"form_metadata_title\">फॉर्म मेटाडेटा</string>\n  <string name=\"form_metadata\">फॉर्म मेटाडेटा</string>\n  <string name=\"form_metadata_summary\">इन मानों को उन फ़ॉर्म में जोड़ा जाएगा जिनमें डेटा जमा करने वाले व्यक्ति की पहचान के लिये प्रयोक्ता नाम, ईमेल और / या टेलीफ़ोन पहले से लोड किए गए फ़ील्ड हैं। </string>\n  <string name=\"user_defined\">उपयोगकर्ता-परिभाषित</string>\n  <string name=\"phone_number\">फ़ोन नंबर</string>\n  <string name=\"email\">इमेल एड्रेस </string>\n  <string name=\"device_defined\">डिवाइस-से-परिभाषित</string>\n  <string name=\"device_id\">डिवाइस आईडी</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\"> नया पासवर्ड दर्ज करें </string>\n  <string name=\"admin_password\">एडमिन पासवर्ड </string>\n  <string name=\"admin_password_changed\">एडमिन पासवर्ड सफलतापूर्वक बदल दिया गया</string>\n  <string name=\"admin_password_disabled\">एडमिन पासवर्ड असक्षम</string>\n  <string name=\"admin_password_incorrect\">क्षमा करें, पासवर्ड  गलत है!</string>\n  <string name=\"enter_admin_password\">एडमिन पासवर्ड  दर्ज करें</string>\n  <string name=\"change_admin_password\">एडमिन पासवर्ड  बदलें</string>\n  <string name=\"show_password\">पासवर्ड दिखायें </string>\n  <string name=\"found_in_main\">मुख्य मेनू से छुपाने के लिए अनचेक करें</string>\n  <string name=\"form_entry\">फॉर्म एंट्री से छुपाने के लिए चेक हटायें</string>\n  <string name=\"reset_project_settings_title\">रीसेट</string>\n  <string name=\"select_all\">सभी का चयन करे</string>\n  <string name=\"clear_all\">सभी साफ करें</string>\n  <string name=\"reset_settings\">सभी सेटिंग (आंतरिक सेटिंग, संरक्षित  सेटिंग)</string>\n  <string name=\"reset_saved_forms\">संरक्षित फॉर्म (इंस्टैंस फोल्डर, इंस्टैंस डेटाबेस)</string>\n  <string name=\"reset_blank_forms\">रिक्त फॉर्म (फॉर्म फ़ोल्डर, फॉर्म डेटाबेस, आइटम सेट डेटाबेस)</string>\n  <string name=\"reset_layers\">मानचित्र लेयरें (लेयर फ़ोल्डर)</string>\n  <string name=\"reset_cache\">फॉर्म लोड कैश (.कैश फोल्डर)</string>\n  <string name=\"reset_settings_dialog_title\">क्या रीसेट करना है, चुनें</string>\n  <string name=\"reset_settings_button_reset\">रीसेट</string>\n  <string name=\"reset_in_progress\">रीसेट किया जा रहा …</string>\n  <string name=\"reset_app_state_result\">परिणाम रीसेट करें</string>\n  <string name=\"reset_app_state_warning\">सभी चयनित डेटा को स्थायी रूप से मिटा दिया जाएगा. वापस नहीं ला सकते</string>\n  <string name=\"reset_settings_result\">सभी सेटिंग्स :: %s</string>\n  <string name=\"reset_saved_forms_result\">संरक्षित फॉर्म :: %s</string>\n  <string name=\"reset_blank_forms_result\">खाली  फॉर्म:: %s</string>\n  <string name=\"reset_layers_result\">मानचित्र लेयरें :: %s</string>\n  <string name=\"reset_cache_result\">फॉर्म लोड कैश :: %s</string>\n  <string name=\"main_menu_settings\">मुख्य मेनू सेटिंग्स</string>\n  <string name=\"user_settings\">प्रयोगकर्ता सेटिंग</string>\n  <string name=\"form_entry_setting\">फॉर्म एंट्री सेटिंग</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">पीछे की ओर बढ़ रहा है</string>\n  <string name=\"yes\">हाँ</string>\n  <string name=\"no\">नहीं</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">बारे में </string>\n  <string name=\"odk_website\">ODK वेबसाइट विजिट करें</string>\n  <string name=\"odk_forum\">ODK फोरम ज्वाइन करें</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"successfully_imported_settings\">सेटिंग सफलतापूर्वक आयातित की गईं</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">अधिकतम वर्ण सीमा आ गयी: सभी सेटिंग्स के लिए क्यूआर कोड नहीं बनाया जा सकता</string>\n  <string name=\"share\">शेयर</string>\n  <string name=\"turn_on_flashlight\">टॉर्च चालू करें</string>\n  <string name=\"turn_off_flashlight\">टॉर्च को बंद करें</string>\n  <string name=\"server_password\">सर्वर पासवर्ड</string>\n  <string name=\"include_password_dialog\">पासवर्ड कोड में शामिल </string>\n  <string name=\"generate\">उत्पन्न</string>\n  <string name=\"invalid_qrcode\">QR कोड में मान्य सेटिंग्स शामिल नहीं हैं I</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">ओके</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">रद्द करें</string>\n  <string name=\"cancel_loading_form\">रद्द करें</string>\n  <string name=\"cancel_location\">रद्द करें</string>\n  <string name=\"do_not_change\">रद्द करें</string>\n  <string name=\"success\">सफल</string>\n  <string name=\"error_occured\">गलती हुई</string>\n  <string name=\"please_wait\">कृपया कुछ क्षण प्रतीक्षा करें</string>\n  <string name=\"please_wait_long\">कृपया प्रतीक्षा करें। इसमें कुछ मिनट लग सकते हैं।</string>\n  <string name=\"fs_delete_media_path_if_file_error\">%sको नहीं मिटाया जा सका. कृपया फाइल को स्वयं हटा दें और फ़ॉर्म को पुनः डाउनलोड करें.</string>\n  <string name=\"fs_create_media_folder_error\">\\'%s\\' मीडिया फ़ोल्डर नहीं बना सका.</string>\n  <string name=\"fs_file_copy_error\">\\'%2$s\\' पर \\'%1$s\\' की नकल नहीं कर सकता. कारण: %3$s</string>\n  <string name=\"select_odk_shortcut\">ODK शॉर्टकट चयन करें</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">अपरिचित URl: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">खराब URl: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">आपने एक ही फॉर्म आईडी और संस्करण के साथ दो अलग-अलग फॉर्म डाउनलोड किए हैं। हो सकता है कि वे अलग-अलग समय या अलग-अलग सर्वरों पर अपलोड किए गए समान फॉर्म थे. किसी भी मामले में, आपको एक को हटाना होगा.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">छोड़ दें </string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ht/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Komanse yon nouvo fòmilè</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Bouyon</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Pare pou ale</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Ale</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Telechaje fòmilè a</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Siprime fòmilè a</string>\n  <string name=\"saved_forms\">Fòm anrejistre</string>\n  <string name=\"version_number\">Vèsyon:%s</string>\n  <string name=\"id_number\">ID%s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Ajoute le\\' EEE, MMM dd, yyyy \\'a\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Modifye le\\' EEE, MMM dd, yyyy \\'a\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Sovgade le\\' EEE, MMM dd, yyyy \\'a\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Finalize le\\' EEE, MMM dd, yyyy \\'a\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Voye ale le\\' EEE, MMM dd, yyyy \\'a\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Siprime le\\' EEE, MMM dd, yyyy \\'a\\' HH:mm</string>\n  <string name=\"submission_deleted\">Soumisyon ki te siprime</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Modifye le\\' EEE, MMM dd, yyyy \\'a\\' HH:mm</string>\n  <string name=\"encrypted_form\">Fòm ki kripte</string>\n  <string name=\"deleted_form\">Fòm ki te siprime</string>\n  <string name=\"sort_the_list\">Aranje lis la</string>\n  <string name=\"sort_by\">Aranje pa</string>\n  <string name=\"sort_by_name_asc\">Non, A-Z</string>\n  <string name=\"sort_by_name_desc\">Non, Z-A</string>\n  <string name=\"sort_by_date_desc\">Dat, resan ansyen</string>\n  <string name=\"sort_by_date_asc\">Dat, ansyen resan</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Dènye sovgad yo anvan</string>\n  <string name=\"filter_the_list\">Filtre lis la</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Rechèch</string>\n  <string name=\"not_granted_permission\">Aktivite w ap eseye kòmanse a mande yon pèmisyon ke w poko bay.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Pèmisyon pou stokaj</string>\n  <string name=\"storage_runtime_permission_denied_desc\">San pèmisyon sa yo, Collect pa p kapab gen aksè ak fom yo oubyen stoke repons yo. Relouvri aplikasyon an lè w pare pou bay pèmisyon yo.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Pèmisyon pou lokalizasyon</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">Pèmisyon pou kamera</string>\n  <string name=\"camera_runtime_permission_denied_desc\">San pèmisyon sa Collect pa p ka aksede a Kamera a. Tanpri eseye ankò lè pare pou w bay li.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Pèmisyon pou anrejistre son</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">San pèmisyon sa Collect pa p ka aksede a mikro a. Tanpri eseye ankò lè pare pou w bay li.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Ouvri kat fòm lan</string>\n  <string name=\"instance_scan_count\"> %1$d Fom ajoute</string>\n  <string name=\"finished_disk_scan\">Eskan lan fini. Tout fòm yo chaje.</string>\n  <string name=\"loading_form\">Fòm lan ap chaje</string>\n  <string name=\"survey_loading_reading_csv_message\">Lekti fichye CSV</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Chanje lang</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Sovgade tankou bouyon</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Ale nan komansman</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Ale nan fen</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Ale anwo</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Gwoup</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Gwoup ki ka repete</string>\n  <string name=\"exit\">Soti</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Ajoute \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Ajoute</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Ajoute yon lot</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Mesaj modifikasyon</string>\n  <string name=\"clearanswer_confirm\">Siprime repons lan de \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Siprime repons</string>\n  <string name=\"clear_answer_ask\">Siprime repons sa?</string>\n  <string name=\"clear_answer_no\">Anile</string>\n  <string name=\"discard_answer\">Siprime repons</string>\n  <string name=\"discard_group\">Siprime gwoup</string>\n  <string name=\"delete_repeat\">Siprime gwoup</string>\n  <string name=\"delete_repeat_ask\">Siprime group sa?</string>\n  <string name=\"delete_repeat_confirm\">Siprime gwoup \\\"%s\\\" ak tout sou gwoup li yo?</string>\n  <string name=\"delete_repeat_no\">Anile</string>\n  <string name=\"invalid_answer_error\">Dezole, repons sa pa valid!</string>\n  <string name=\"required_answer_error\">Dezole repons sa nesesè!</string>\n  <string name=\"success_form_validation\">Siksè! Pa gen erè nan fòmilè a.</string>\n  <!--Title of the end of form screen-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Sovgade tankou bouyon</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Finalize</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Voye ale</string>\n  <string name=\"data_saved_error\">Dezole, sovgad fomilè a echwe</string>\n  <string name=\"data_saved_ok\">Fòmilè sovgade ak siksè!</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Sovgade chanjman yo</string>\n  <string name=\"keep_editing\">Kontinye modifye</string>\n  <string name=\"quit_form_title\">Sovgade fòm lan?</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"quit_form_continue_title\">Kontinye modifye?</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"survey_saving_validating_message\">Validasyon kesyon yo…</string>\n  <string name=\"survey_saving_saving_message\">Sovgad sou kat SD…</string>\n  <string name=\"survey_saving_encrypting_message\">Ankripsyon done yo…</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Mikro a ap itilize deja</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Demaraj</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Ouvri Url</string>\n  <string name=\"alert\">Alèt</string>\n  <string name=\"install_openmapkit\">Souple, enstale OpenMapKit!</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Ouvri fichye</string>\n  <string name=\"svg_file_does_not_exist\">Fichye SVG a pa ekziste!</string>\n  <string name=\"capture_image\">Pran foto</string>\n  <string name=\"choose_image\">Chwazi imaj</string>\n  <string name=\"take_picture_instruction\">Tape ekran an pou w pran yon foto</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Sovgade epi fèmen</string>\n  <string name=\"capture_video\">Anrejistre video</string>\n  <string name=\"choose_video\">Chwazi video</string>\n  <string name=\"play_video\">Jwe video</string>\n  <string name=\"choose_file\">Chwazi fichye</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Siprime</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Anrejistre son</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Chwazi son</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Poz</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Rekòmanse</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Siprime fichye?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Kanpe</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Anrejistreman…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Anrejistreman odyo</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Jwe odyo</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Sovgad fichye</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Chwazi repons</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"ext_file_no_data_error\">Fichye a pa gen done!</string>\n  <string name=\"select_date\">Seleksyone dat</string>\n  <string name=\"select_time\">Seleksyone lè</string>\n  <string name=\"no_date_selected\">Pa gen dat ki seleksyone</string>\n  <string name=\"no_time_selected\">Pa gen lè ki seleksyone</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Fèmen</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Rezon</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Sovgade</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"choices\">Chwa</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Ale nan paramèt</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Poz</string>\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Label for server type setting-->\n  <!--Title of dialog for configuring server URL-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"delete_project\">Siprime</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Sovgade tankou bouyon</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Anile</string>\n  <string name=\"cancel_loading_form\">Anile</string>\n  <string name=\"cancel_location\">Anile</string>\n  <string name=\"do_not_change\">Anile</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">Ajoute</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Montre detay yo</string>\n  <string name=\"errors\">Erè</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Ouvri paramèt yo</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Erè</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-in/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Mulai isi formulir</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Konsep</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Siap kirim</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Terkirim</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Unduh formulir</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Hapus formulir</string>\n  <string name=\"saved_forms\">Formulir tersimpan</string>\n  <string name=\"version_number\">Versi: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Ditambahkan pada\\' EEE, dd MMM yyyy\\', pukul\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Diperbarui pada\\' EEE, dd MMM yyyy\\', pukul\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Disimpan pada\\' EEE, dd MMM yyyy\\', pukul\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Difinalisasi pada\\' EEE, dd MMM yyyy\\', pukul\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Dikirim pada\\' EEE, dd MMM yyyy\\', pukul\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Pengiriman tidak berhasil pada\\' EEE, dd MMM yyyy\\', pukul\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Dihapus pada\\' EEE, dd MMM yyyy\\', pukul\\' HH:mm</string>\n  <string name=\"submission_deleted\">Pengiriman dihapus</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Diubah pada tanggal\\' EEE, dd MMM yyyy\\', pukul\\' HH:mm</string>\n  <string name=\"encrypted_form\">Formulir terenkripsi</string>\n  <string name=\"deleted_form\">Hapus formulir</string>\n  <string name=\"sort_the_list\">Urutkan daftar</string>\n  <string name=\"sort_by\">Urutkan dengan</string>\n  <string name=\"sort_by_name_asc\">Nama, A-Z</string>\n  <string name=\"sort_by_name_desc\">Nama, Z-A</string>\n  <string name=\"sort_by_date_desc\">Tanggal, baru ke lama</string>\n  <string name=\"sort_by_date_asc\">Tanggal, lama ke baru</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Terakhir disimpan dulu</string>\n  <string name=\"filter_the_list\">Pilah daftar</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Cari</string>\n  <string name=\"not_granted_permission\">Aktivitas yang Anda lakukan memerlukan izin akses.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Izin untuk penyimpanan</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Tanpa izin akses, Collect tidak dapat mengakses formulir Anda ataupun menyimpan jawaban. Buka ulang aplikasi jika Anda sudah yakin untuk memberikan izin.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Izin untuk mengakses lokasi</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Tanpa izin <b> Lokasi Akurat </b>, Collect tidak dapat merekam lokasi anda. Silakan coba lagi ketika anda sudah memberikan izin.</string>\n  <string name=\"camera_runtime_permission_denied_title\">Izin untuk mengakses kamera</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Tanpa izin akses, Collect tidak dapat mengakses kamera. Buka ulang aplikasi jika Anda sudah yakin untuk memberikan izin.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Izin untuk merekam suara</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Tanpa izin akses, Collect tidak dapat mengakses mikrofon pada perangkat. Buka ulang aplikasi jika Anda sudah yakin untuk memberikan izin.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Buka peta formulir</string>\n  <string name=\"instance_scan_count\">Formulir %1$d ditambahkan.</string>\n  <string name=\"finished_disk_scan\">Seluruh formulir telah berhasil dimuat.</string>\n  <string name=\"xform_parse_error\">XForm parse bermasalah pada %1$s: %2$s hilang atau tidak valid</string>\n  <string name=\"loading_form\">Memuat Formulir</string>\n  <string name=\"survey_loading_reading_form_message\">Membaca definisi formulir…</string>\n  <string name=\"survey_loading_reading_data_message\">Membaca data survei…</string>\n  <string name=\"survey_loading_reading_csv_message\">Membaca berkas CSV…</string>\n  <string name=\"parse_error\">Maaf, formulir tidak dapat terbaca.</string>\n  <string name=\"loading_form_failed\">Kesalahan terjadi saat membuka formulir. Mohon coba lagi.</string>\n  <string name=\"cannot_edit_completed_form\">Formulir yang sudah final tidak dapat diedit karena sudah terenkripsi.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Perubahan yang belum tersimpan dipulihkan dari savepoint</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Ubah Bahasa</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Daftar Isi</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Simpan sebagai draf</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Awal</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Akhir</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Atas</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <string name=\"edit_finalized_form\">Sunting formulir terfinalisasi</string>\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Grup</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Grup Perulangan</string>\n  <string name=\"exit\">Keluar</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Tambah \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Tambah</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"do_not_add_repeat\">Batal tambah</string>\n  <string name=\"add_another_menu\">Tambah lainnya</string>\n  <string name=\"add_another_question\">Tambah lainnya?</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Ubah Draf</string>\n  <string name=\"clearanswer_confirm\">Hapus isian atas \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Hapus isian</string>\n  <string name=\"clear_answer_ask\">Hapus Isian Ini?</string>\n  <string name=\"clear_answer_no\">Batal</string>\n  <string name=\"discard_answer\">Hapus isian</string>\n  <string name=\"discard_group\">Hapus grup</string>\n  <string name=\"delete_repeat\">Hapus grup</string>\n  <string name=\"delete_repeat_ask\">Hapus grup ini?</string>\n  <string name=\"delete_repeat_confirm\">Hapus grup \\\"%s\\\" dan semua sub-grup?</string>\n  <string name=\"delete_repeat_no\">Batal</string>\n  <string name=\"invalid_answer_error\">Maaf, isian tersebut tidak valid.</string>\n  <string name=\"required_answer_error\">Maaf, bagian ini wajib terisi.</string>\n  <string name=\"success_form_validation\">Sukses! Tidak ada kesalahan yang ditemukan dalam formulir anda.</string>\n  <string name=\"invalid_space_in_answer_singular\">Peringatan: Nilai %s mengandung spasi</string>\n  <string name=\"invalid_space_in_answer_plural\">Peringatan: Nilai %s mengandung spasi</string>\n  <string name=\"too_complex_form\">Formulir ini terlalu rumit untuk perangkat ini. Coba sederhanakan atau minta bantuan di forum.</string>\n  <string name=\"survey_internal_error\">Kesalahan internal: akses daftar isi tidak berhasil.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Anda berada di akhir formulir\\n\\\"%s\\\".</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <string name=\"form_editing_disabled_after_finalizing\">Perubahan tidak dapat dilakukan setelah finalisasi.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <string name=\"form_editing_disabled_after_sending\">Perubahan tidak dapat dilakukan setelah dikirim.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_finalizing\">Perubahan dapat dilakukan setelah finalisasi.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_sending\">Perubahan dapat dilakukan setelah pengiriman.</string>\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Jika Anda perlu mengedit formulir, simpan sebagai draf.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <string name=\"form_editing_enabled_after_finalizing_hint\">Untuk melakukan perubahan setelah menyelesaikan formulir Anda, buka \\\"Siap dikirim\\\", buka formulir, dan ketuk ikon ubah.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <string name=\"form_editing_enabled_after_sending_hint\">Untuk melakukan perubahan setelah mengirim Formulir Anda, buka menu \\\"Terkirim\\\", dan buka Formulir, kemudian ketuk ikon ubah.</string>\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Simpan sebagai draf</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Finalisasi</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Kirim</string>\n  <string name=\"data_saved_error\">Maaf, tidak berhasil menyimpan formulir.</string>\n  <string name=\"data_saved_ok\">Formulir berhasil disimpan.</string>\n  <string name=\"save_point_error\">Terjadi kesalahan saat menyimpan titik pemulihan: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Buang Formulir</string>\n  <string name=\"discard_changes\">Buang Perubahan</string>\n  <string name=\"keep_changes\">Simpan perubahan</string>\n  <string name=\"keep_editing\">Lanjut edit</string>\n  <string name=\"quit_form_title\">Simpan formulir?</string>\n  <string name=\"save_explanation\">Anda dapat menyimpan formulir ini dan mengaksesnya dari draf anda kapan saja.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'Formulir ini terakhir disimpan pada\\' EEE, dd MMM yyyy\\', pukul\\' HH:mm\\'. Simpan sebagai draf untuk menyimpan perubahan.\\'</string>\n  <string name=\"quit_form_continue_title\">Lanjut edit?</string>\n  <string name=\"discard_form_warning\">Jika anda membuang formulir, anda akan kehilangan perubahan yang telah anda buat sejauh ini.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'Formulir ini terakhir disimpan pada\\' EEE, dd MMM yyyy\\', pukul\\' HH:mm\\'. Jika anda membuang perubahan, anda akan kehilangan perubahan yang telah anda buat sejak saat itu.\\'</string>\n  <string name=\"saving_form\">Menyimpan Formulir</string>\n  <string name=\"survey_saving_validating_message\">Memvalidasi jawaban…</string>\n  <string name=\"survey_saving_collecting_message\">Menghimpun data…</string>\n  <string name=\"survey_saving_saving_message\">Menyimpan ke SD card…</string>\n  <string name=\"survey_saving_finalizing_message\">Melakukan finalisasi ke SD card…</string>\n  <string name=\"survey_saving_encrypting_message\">Mengenkripsi data…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Instance ini tidak memuat tepat satu record.</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">jr_form_id ini tidak sesuai dengan tepat satu formulir kosong.</string>\n  <string name=\"no_form_id_specified\">FormId tidak ditentukan?</string>\n  <string name=\"phone_does_not_support_rsa\">Ponsel tidak mendukung enkripsi RSA.</string>\n  <string name=\"invalid_rsa_public_key\">RSA public key tidak valid.</string>\n  <string name=\"encryption_error_message\">%s formulir belum disimpan sebagai final.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">Formulir tidak bisa digunakan.</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Tidak dapat mulai perekaman.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Mikrofon sedang digunakan</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">Media tidak dapat dilampirkan ke formulir tetapi tetap tersedia di %1$s</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Perekaman dinonaktifkan. Aktifkan dalam %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Gagal mencoba memperbarui nilai. Ini biasanya karena penggunaan perhitungan yang salah dalam desain formulir.\\n\\nJika Anda terus mengalami masalah ini, laporkan kepada orang yang meminta Anda untuk mengumpulkan data</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Buka Aplikasi</string>\n  <string name=\"activity_not_found\">Tidak ditemukan aktivitas untuk diselesaikan: %s</string>\n  <string name=\"no_app\">Aplikasi yang dibutuhkan tidak tersedia. Silahkan pasang secara manual.</string>\n  <string name=\"null_intent_value\">Aplikasi eksternal tidak menyediakan informasi yang dibutuhkan.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">Cetak</string>\n  <string name=\"open_url\">Buka URL</string>\n  <string name=\"capture_osm\">Jalankan OpenMapKit</string>\n  <string name=\"recapture_osm\">Tandai Ulang OSM</string>\n  <string name=\"edited_osm_file\">Ubah berkas OSM XML</string>\n  <string name=\"invalid_osm_data\">Terjadi kesalahan. Kami tidak mendapatkan data OSM yang valid.</string>\n  <string name=\"alert\">Peringatan</string>\n  <string name=\"install_openmapkit\">Mohon instal OpenMapKit.</string>\n  <string name=\"get_barcode\">Ambil Barcode</string>\n  <string name=\"replace_barcode\">Ubah Barcode</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Posisikan barcode ke dalam kotak</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"rotate_device\">Rotasikan perangkat</string>\n  <string name=\"open_file\">Buka berkas</string>\n  <string name=\"rank_items\">Peringkat isian</string>\n  <string name=\"number_picker_title\">Pemilihan nomor</string>\n  <string name=\"invalid_range_widget\">Widget ini memiliki parameter yang tidak valid.</string>\n  <string name=\"svg_file_does_not_exist\">Berkas SVG tidak ada.</string>\n  <string name=\"capture_image\">Ambil Foto</string>\n  <string name=\"choose_image\">Pilih Gambar</string>\n  <string name=\"selected_invalid_image\">Berkas terpilih bukan gambar yang valid</string>\n  <string name=\"take_picture_instruction\">Sentuh layar untuk ambil gambar</string>\n  <string name=\"error_front_camera_unavailable\">Kamera depan tidak tersedia dalam perangkat ini.</string>\n  <string name=\"camera_error\">Terjadi kesalahan saat mengambil gambar</string>\n  <string name=\"camera_failed_to_initialize\">Tidak dapat menginisialisasi kamera!</string>\n  <string name=\"annotate_image\">Menganotasi Gambar</string>\n  <string name=\"gif_not_supported\">Berkas gif tidak didukung</string>\n  <string name=\"invalid_file_type\">Aplikasi tidak mengenali tipe file</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_title\">Simpan perubahan</string>\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_explanation\">Apakah Anda ingin Simpan perubahan sebelum keluar?</string>\n  <string name=\"save_and_close\">Simpan dan Tutup</string>\n  <string name=\"signature_capture\">Ambil Tanda Tangan</string>\n  <string name=\"sign_button\">Isi Tanda Tangan</string>\n  <string name=\"markup_image\">Tandai Gambar</string>\n  <string name=\"draw_image\">Gambar Sketsa</string>\n  <string name=\"reset_image\">Set Ulang</string>\n  <string name=\"set_color\">Atur Warna</string>\n  <string name=\"capture_video\">Rekam Video</string>\n  <string name=\"choose_video\">Pilih Video</string>\n  <string name=\"play_video\">Mainkan video</string>\n  <string name=\"choose_file\">Pilih Berkas</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Hapus</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Rekam Suara</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Pilih Suara</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Jeda</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Lanjut</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">Rekam suara: aktif</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">Rekam suara: nonaktif</string>\n  <string name=\"background_audio_permission_explanation\">Formulir ini merekam suara di latar belakang. Anda harus memberikan izin untuk bisa menggunakan mikrofon Anda. Jika tidak, Anda tidak akan dapat membuka formulir ini.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Formulir ini meminta perekaman suara latar belakang. Menonaktifkannya akan menghentikan perekaman dan membuang suara yang terekam. Anda yakin ingin melanjutkan?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Nonaktfikan perekaman</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">Perekaman tidak akan dimulai. Anda harus membuka kembali formulir untuk merekam.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Hapus berkas?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">Anda tidak akan dapat memulihkan berkas ini setelah dihapus. Anda yakin ingin melanjutkan?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Berhenti</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Rekaman…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Rekaman audio</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Putar audio</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Menyimpan file</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Anda harus berhenti merekam sebelum meninggalkan layar ini.</string>\n  <string name=\"error_attaching_binary_file\">Terjadi kesalahan saat memasukkan berkas media: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Pilih nilai</string>\n  <string name=\"select_answer\">Pilih Jawaban</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Ubah nilai</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Tidak ada nilai terpilih</string>\n  <string name=\"selected_title\">Terpilih:</string>\n  <string name=\"trigger\">OK. Lanjut.</string>\n  <string name=\"ext_file_no_data_error\">Berkas tidak memiliki data</string>\n  <string name=\"ext_conflicting_columns_error\">Semua kolom %s telah sesuai</string>\n  <string name=\"ext_sortBy_numeric_error\">Kolom terfilter anda harus hanya berisi nilai numerik. Nilai yang  konflik adalah \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Kesalahan sintaks pada fungsi search(): Fungsi membutuhkan 1, 4 atau 6 argumen.</string>\n  <string name=\"ext_search_wrong_function_error\">Kesalahan sintaks pada fungsi search(): fungsi tidak memiliki kategori \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Kesalahan sintaks pada fungsi search(): \\'%s\\' tidak dapat dievaluasi sebagai fungsi.</string>\n  <string name=\"ext_search_return_error\">Pengendali pencarian mengembalikan tipe obyek \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Tidak dapat mengimpor data dari %1$s. Penyebab: %2$s</string>\n  <string name=\"ext_import_progress_message\">Pra-memuat data dari \\'%1$s\\', mohon tunggu… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Pembacaan data dibatalkan.</string>\n  <string name=\"ext_import_finalizing_message\">Finalisasi pra-memuat data…</string>\n  <string name=\"ext_import_completed_message\">Pembacaan data selesai.</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager belum diinisialisasi.</string>\n  <string name=\"ext_import_csv_missing_error\">Data eksternal untuk %1$s belum diimpor. Mungkin Anda lupa untuk memasukkan berkas %2$s.csv dengan formulir Anda?</string>\n  <string name=\"ext_search_generic_error\">Kesalahan sintaks pada fungsi search(): %s</string>\n  <string name=\"ext_assign_value_error\">Tidak dapat memberikan nilai pada \\'%s\\'.</string>\n  <string name=\"parser_exception\">Pengecualian XPathParser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Rekam Arah Mata Angin</string>\n  <string name=\"replace_bearing\">Ganti Arah Mata Angin</string>\n  <string name=\"getting_bearing\">Memuat Arah Mata Angin</string>\n  <string name=\"accept_bearing\">Rekam Arah Mata Angin</string>\n  <string name=\"bearing\">Arah Mata Angin: %.3f</string>\n  <string name=\"direction\">Arah: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Arah Mata Angin tidak dapat ditemukan: perangkat tidak memiliki akselerometer dan sensor medan magnet atau keduanya.</string>\n  <string name=\"select_date\">Pilih tanggal</string>\n  <string name=\"select_time\">Pilih waktu</string>\n  <string name=\"no_date_selected\">Tanggal tidak dipilih</string>\n  <string name=\"no_time_selected\">Waktu tidak dipilih</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Shafar</string>\n  <string name=\"islamic_month_3\">Rabi\\'ul Awal</string>\n  <string name=\"islamic_month_4\">Rabi\\'ul Akhir</string>\n  <string name=\"islamic_month_5\">Jumadil Awal</string>\n  <string name=\"islamic_month_6\">Jumadil Akhir</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sya\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadhan</string>\n  <string name=\"islamic_month_10\">Syawwal</string>\n  <string name=\"islamic_month_11\">Dzulqa’dah</string>\n  <string name=\"islamic_month_12\">Dzulhijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <string name=\"buddhist_month_1\">Mokkarakhom</string>\n  <!--2nd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_2\">Kumphaphan</string>\n  <!--3rd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_3\">Mīnākhom</string>\n  <!--4th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_4\">Mesayon</string>\n  <!--5th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_5\">Phruetsaphakhom</string>\n  <!--6th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_6\">Mithunayon</string>\n  <!--7th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_7\">Karakadakhom</string>\n  <!--8th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_8\">Singhakhom</string>\n  <!--9th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_9\">Kanyayon</string>\n  <!--10th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_10\">Tulaakhom</string>\n  <!--11th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_11\">Phruetsachikayon</string>\n  <!--12th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_12\">Thanwakhom</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Berkas: %s tidak valid.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Berkas: %s tidak ditemukan.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Masukan identitas</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Identitas</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Anda harus mengidentifikasi diri Anda terlebih dahulu sebelum mengakses formulir ini</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Tutup</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Alasan perubahan</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Alasan</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Anda perlu menjelaskan alasan untuk melakukan perubahan terhadap formulir ini</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Simpan</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">Pilih tempat</string>\n  <string name=\"choices\">Pilihan</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_usgs_sat\">Peta Nasional Gabungan USGS</string>\n  <string name=\"openmap_usgs_img\">Citra Peta Nasional USGS</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Peta dasar</string>\n  <string name=\"basemap_source\">Sumber</string>\n  <string name=\"basemap_source_unavailable\">Maaf, peta dasar %s tidak tersedia pada perangkat ini. Silakan pilih peta dasar lainnya di Pengaturan &gt; Peta.</string>\n  <string name=\"map_style_label\">Gaya peta %s</string>\n  <string name=\"streets\">Jalan</string>\n  <string name=\"terrain\">Lahan</string>\n  <string name=\"hybrid\">Hibrida</string>\n  <string name=\"satellite\">Satelit</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Terang</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Gelap</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Tempat terbuka</string>\n  <string name=\"topographic\">Topografi</string>\n  <string name=\"google_play_services_error_occured\">Tidak dapat mengakses Google Maps. Apakah Google Play Services telah terinstal?</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Dark Matter</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">Lapisan luring</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">Lapisan</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">Pilih lapisan luring</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">Pilih lapisan offline yang akan digunakan untuk semua peta dalam proyek ini. Anda dapat menambahkan opsi ke daftar dengan memilih file MBTiles dari perangkat Anda, serta menghapus lapisan yang sudah ada dari daftar.</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">Pelajari lebih lanjut tentang menambahkan MBTiles.</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">Tambah lapisan</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">Hapus lapisan</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <plurals name=\"non_mbtiles_files_selected_title\">\n    <item quantity=\"other\">%d lapisan tidak bisa ditambahkan</item>\n  </plurals>\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">File yang Anda pilih bukan MBTiles. Anda hanya dapat menambahkan file MBTiles.</string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">Beberapa file yang Anda pilih bukan MBTiles. Anda hanya dapat menambahkan file MBTiles.</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">Lapisan</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">Atur akses lapisan</string>\n  <string name=\"select_layer_access_subtitle\">Apakah Anda ingin lapisan tersedia di semua proyek atau hanya dalam proyek saat ini?</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">Seluruh proyek</string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">Hanya proyek ini</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">Anda yakin ingin menghapus %1$s lapisan luring?</string>\n  <string name=\"record_geopoint\">Rekam titik</string>\n  <string name=\"location_accuracy\">Akurasi: %1$s</string>\n  <string name=\"location_accuracy_unacceptable\">⚠️ Akurasi: %1$s (belum sesuai)</string>\n  <string name=\"provider_disabled_error\">Maaf, penyedia lokasi belum diaktifkan</string>\n  <string name=\"gps_result\">Latitude: %1$s\\nLongitude: %2$s\\nAltitude: %3$s m\\nAkurasi: %4$s m</string>\n  <string name=\"google_play_services_not_available\">Formulir ini hendak merekam pergerakan lokasi Anda, tetapi Google Play Service tidak tersedia.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Formulir ini hendak merekam pergerakan lokasi Anda, tetapi layanan lokasi tidak aktif. Mohon aktifkan terlebih dahulu di pengaturan Android.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Pergi ke pengaturan</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">Lacak lokasi: aktif</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">Lacak lokasi: nonaktif</string>\n  <string name=\"background_location_disabled\">Formulir ini hendak melacak lokasi Anda, tetapi pelacakan lokasi tidak aktif. Mohon aktifkan melalui menu %1$s di atas.</string>\n  <string name=\"background_location_enabled\">Formulir ini melacak lokasi Anda. Anda dapat menonaktifkan pelacakan melalui menu %1$s di atas.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">Periksa kesalahan</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">Waktu berlalu: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">Titik akan tersimpan pada saat %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">Jumlah Satelit: %1$s</string>\n  <string name=\"waiting_for_location\">Mencoba untuk mendapatkan lokasi. Harap tunggu.</string>\n  <string name=\"poor_accuracy\">Akurasi buruk. Harap tunggu.</string>\n  <string name=\"unacceptable_accuracy\">Akurasi yang tidak dapat diterima. Harap tunggu.</string>\n  <string name=\"improving_accuracy\">Meningkatkan akurasi. Harap tunggu.</string>\n  <string name=\"accuracy_status_title\">Mendapatkan Lokasi</string>\n  <!--The accuracy of a location fix in meters-->\n  <string name=\"accuracy_m\">%1$s m</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">B %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">T %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">U %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Metode masukan</string>\n  <string name=\"start\">Mulai</string>\n  <string name=\"enable_gps\">Aktifkan GPS</string>\n  <string name=\"gps_enable_message\">GPS dinonaktifkan pada perangkat Anda. Apakah Anda ingin mengaktifkannya?</string>\n  <string name=\"geo_clear_warning\">Fitur sudah dibuat. Apakah Anda ingin menghapus fitur ini?</string>\n  <string name=\"geo_exit_warning\">Buang perubahan dan kembali ke ODK?</string>\n  <string name=\"polygon_validator\">Setidaknya dibutuhkan 3 titik untuk membuat Polygon.</string>\n  <string name=\"polyline_validator\">Setidaknya dibutuhkan 2 titik untuk membuat Poyline.</string>\n  <string name=\"clear\">Hapus</string>\n  <string name=\"placement_mode\">Penempatan dengan sentuhan</string>\n  <string name=\"manual_mode\">Rekam lokasi secara manual</string>\n  <string name=\"automatic_mode\">Rekam lokasi secara otomatis</string>\n  <string name=\"recording_interval\">Interval rekaman:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"other\">%d detik</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"other\">%d menit</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Persyaratan akurasi:</string>\n  <string name=\"none\">Tidak ada</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"other\">%d meter</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Titik yang dimasukkan: %d</string>\n  <string name=\"collection_status_placement\">Titik yang dimasukkan: %d (sentuh untuk menempatkan poin)</string>\n  <string name=\"collection_status_manual\">Titik yang dimasukkan: %d (rekaman manual)</string>\n  <string name=\"collection_status_auto_seconds\">Titik yang dimasukkan: %1$d (rekaman tiap %2$d detik)</string>\n  <string name=\"collection_status_auto_minutes\">Titik yang dimasukkan: %1$d (rekaman tiap %2$d menit)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Titik yang dimasukkan: %1$d (rekaman tiap %2$d detik, akurasi %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Titik yang dimasukkan: %1$d (rekaman tiap %2$d menit, akurasi %3$d m)</string>\n  <string name=\"geopoint_instruction\">Tekan lama untuk menempatkan tanda atau ketuk tombol tambah penanda</string>\n  <string name=\"geopoint_no_draggable_instruction\">Sentuh tombol tambah penanda.</string>\n  <string name=\"discard\">Buang</string>\n  <string name=\"select_item_count\">%s: %d (%d ditampilkan di peta)</string>\n  <string name=\"select_item\">Pilih</string>\n  <string name=\"new_item\">Isian baru</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">Ubah Draf</string>\n  <string name=\"view_data\">Lihat Formulir Tersimpan</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">Melacak lokasi</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">Lokasi pelacakan</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Tunjukkan lokasi saya</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">Perbesar hingga muat semua</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Jeda</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">Hapus titik terakhir</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">Hapus titik terakhir</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Mengirim Formulir</string>\n  <string name=\"noselect_error\">Maaf, anda belum memilih formulir</string>\n  <string name=\"no_connection\">Koneksi internet tidak tersedia</string>\n  <string name=\"sending_items\">Sedang mengirim %1$s dari %2$s total formulir</string>\n  <string name=\"send_selected_data\">Kirim Terpilih</string>\n  <string name=\"server_auth_credentials\">Nama pengguna atau sandi untuk server tidak valid: %s</string>\n  <string name=\"server_requires_auth\">Server Membutuhkan Autentikasi</string>\n  <string name=\"no_forms_uploaded\">Maaf, tidak ada formulir yang telah diunggah.</string>\n  <string name=\"upload_results\">Unggahan</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Ubah Tampilan</string>\n  <string name=\"show_sent_and_unsent_forms\">Tampilkan Formulir Terkirim dan Belum Terkirim</string>\n  <string name=\"show_unsent_forms\">Tampilkan Formulir Belum Terkirim</string>\n  <string name=\"send_in_progress\">Pengiriman sedang berlangsung, mohon coba beberapa saat lagi.</string>\n  <string name=\"forms_upload_succeeded\">Pengunggahan formulir berhasil</string>\n  <string name=\"forms_upload_failed\">Pengunggahan formulir gagal</string>\n  <string name=\"all_uploads_succeeded\">Semua unggahan berhasil!</string>\n  <string name=\"some_uploads_failed\">%1$s dari %2$s unggahan gagal!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Memuat Ulang</string>\n  <string name=\"download\">Unduh Terpilih</string>\n  <string name=\"downloading_data\">Menghubungkan ke Server</string>\n  <string name=\"fetching_file\">Mengunduh %1$s.\\n\\nFormulir %2$s dari %3$s formulir.</string>\n  <string name=\"form_download_progress\">%1$s. Sedang mengunduh berkas media: %2$s dari %3$s</string>\n  <string name=\"version\">Versi:</string>\n  <string name=\"form_updates_available\">Tersedia pembaruan formulir</string>\n  <string name=\"newer_version_of_a_form_info\">Ini adalah pembaruan dari formulir yang kamu miliki</string>\n  <string name=\"form_update_error\">Pembaruan formulir tidak berhasil</string>\n  <string name=\"form_update_error_massage\">Buka ODK Collect untuk mencoba lagi</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"form_update_notification_title\">Sinkronisasi formulir</string>\n  <string name=\"load_remote_form_error\">Unduhan daftar formulir tidak berhasil</string>\n  <string name=\"forms_download_succeeded\">Pengunduhan formulir berhasil</string>\n  <string name=\"forms_download_failed\">Pengunduhan formulir gagal</string>\n  <string name=\"all_downloads_succeeded\">Seluruh unduhan telah berhasil!</string>\n  <string name=\"some_downloads_failed\">%1$s dari %2$s unduhan tidak berhasil!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Versi: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Jika anda terus mengalami masalah ini, laporkan ke orang yang meminta anda untuk mengumpulkan data.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">ODK Collect tidak dapat menghubung server di %s. Apakah anda sudah memasukkan URL dengan benar?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">ODK Collect tidak dapat menghubung server di %s. Apakah anda sudah memasukkan URL dengan benar?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">Peladen %1$s rmengembalikan kode status %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">Peladen %s mengembalikan isian yang tidak valid.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">Server tidak menyediakan hash untuk formulir ini.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_parsing_error\">Formulir ini tidak dapat diproses:</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">Terjadi kesalahan penyimpanan pada perangkat saat mengunduh formulir ini. </string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">Formulir ini memiliki URL pengiriman yang tidak valid.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Formulir Tersimpan</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Formulir Kosong</string>\n  <string name=\"delete_confirm\">Hapus %s formulir?</string>\n  <string name=\"delete_file\">Hapus Terpilih</string>\n  <string name=\"delete_no\">Batal Hapus</string>\n  <string name=\"delete_yes\">Hapus Formulir</string>\n  <string name=\"form_delete_message\">Menghapus form terpilih</string>\n  <string name=\"file_deleted_ok\">%s formulir telah berhasil dihapus.</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Pengaturan proyek</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Server</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">Alamat website, nama pengguna, kata sandi</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Tampilan proyek</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Nama, simbol, warna</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Antarmuka pengguna</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_interface_settings_subtext\">Bahasa aplikasi, ukuran font, navigasi</string>\n  <!--Title of a settings section-->\n  <string name=\"maps\">Peta</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Peta dasar, gaya, lapisan</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Manajemen Formulir</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">Pembaruan otomatis, kirim otomatis, hapus otomatis</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Identitas perangkat dan pengguna</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">Nama Pengguna, nomor telepon, ID perangkat</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Eksperimental</string>\n  <string name=\"protected_settings\">Terlindung</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">Buka kunci pengaturan yang dilindungi </string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">Ubah kata sandi, atur kontrol akses</string>\n  <string name=\"set_admin_password\">Atur kata sandi admin</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Manajemen proyek</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">Konfigurasi ulang, pengaturan ulang, hapus</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Kontrol akses</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">Batas antarmuka pengguna</string>\n  <string name=\"server_preferences\">Pengaturan Server</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Server URL</string>\n  <string name=\"url_error\">Maaf, URL tidak valid.</string>\n  <string name=\"username\">Nama Pengguna</string>\n  <string name=\"change_username\">Nama Pengguna ODK</string>\n  <string name=\"password\">Kata sandi</string>\n  <string name=\"change_password\">Kata Sandi ODK</string>\n  <string name=\"password_error_whitespace\">Tidak diperkenankan menggunakan spasi pada kata sandi</string>\n  <string name=\"username_error_whitespace\">Tidak diperkenankan menggunakan spasi pada nama pengguna</string>\n  <string name=\"app_theme\">Tema</string>\n  <string name=\"language\">Bahasa</string>\n  <string name=\"use_device_language\">Gunakan bahasa perangkat</string>\n  <string name=\"font_size\">Ukuran teks font</string>\n  <string name=\"change_font_size\">Ukuran teks font</string>\n  <string name=\"font_size_extra_large\">Sangat Besar</string>\n  <string name=\"font_size_large\">Besar</string>\n  <string name=\"font_size_medium\">Sedang</string>\n  <string name=\"font_size_small\">Kecil</string>\n  <string name=\"font_size_extra_small\">Sangat kecil</string>\n  <string name=\"navigation\">Navigasi</string>\n  <string name=\"swipe_navigation\">Gunakan sentuhan ke kanan/kiri</string>\n  <string name=\"buttons_navigation\">Gunakan tombol lanjut/kembali</string>\n  <string name=\"swipe_buttons_navigation\">Gunakan sentuhan dan tombol</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Lanjut</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Kembali</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Pembaruan formulir</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Mode pembaruan formulir kosong</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Manual</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Hanya formulir yang diunduh sebelumnya</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Sama dengan server</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Frekuensi pembaruan otomatis</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Tiap 15 menit</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Tiap 1 jam</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Tiap 6 jam</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Tiap 24 jam</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Unduh otomatis</string>\n  <string name=\"automatic_download_summary\">Otomatis unduh versi formulir terkini</string>\n  <string name=\"hide_old_form_versions_setting_title\">Sembunyikan versi formulir lawas</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Hanya versi terbaru yang akan muncul di menu Isi Formulir Kosong</string>\n  <string name=\"form_submission_category\">Pengiriman Formulir</string>\n  <string name=\"autosend_selector_title\">Kirim otomatis</string>\n  <string name=\"autosend\">Kirim otomatis</string>\n  <string name=\"off\">Mati</string>\n  <string name=\"wifi_autosend\">Hanya Wifi</string>\n  <string name=\"cellular_autosend\">Hanya Selular</string>\n  <string name=\"wifi_cellular_autosend\">Wifi atau selular</string>\n  <string name=\"delete_after_send\">Hapus setelah kirim</string>\n  <string name=\"delete_after_send_summary\">Hapus formulir dan media yang sudah final setelah dikirim ke server</string>\n  <string name=\"form_filling_category\">Mengisi Formulir</string>\n  <string name=\"constraint_behavior_title\">Pengolahan batasan</string>\n  <string name=\"constraint_behavior\">Perilaku pengolahan batasan</string>\n  <string name=\"constraint_behavior_on_swipe\">Validasi pada saat menggeser layar ke kanan</string>\n  <string name=\"constraint_behavior_on_finalize\">Menunda validasi sampai sudah final</string>\n  <string name=\"high_resolution_title\">Video resolusi tinggi</string>\n  <string name=\"high_resolution_summary\">Mengaktifkan perekaman video beresolusi tinggi</string>\n  <string name=\"image_size_title\">Ukuran gambar</string>\n  <string name=\"image_size_dialog_title\">Pixel maksimum pada sisi gambar yang paling panjang</string>\n  <string name=\"image_size_large\">Besar (3072px)</string>\n  <string name=\"image_size_medium\">Sedang (2048px)</string>\n  <string name=\"image_size_small\">Kecil (1024px)</string>\n  <string name=\"image_size_very_small\">Sangat kecil (640px)</string>\n  <string name=\"image_size_original\">Ukuran asli dari kamera (bawaan)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Tampilkan bantuan atas pertanyaan</string>\n  <string name=\"guidance_no\">Tidak</string>\n  <string name=\"guidance_yes\">Ya - selalu tunjukkan</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Ya - sembunyikan</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">Gunakan aplikasi eksternal untuk merekam audio</string>\n  <string name=\"form_import_category\">Impor Formulir</string>\n  <string name=\"instance_sync\">Selesaikan formulir saat impor</string>\n  <string name=\"instance_sync_summary\">Formulir yang rusak akan langsung ditambahkan ke berkas instance</string>\n  <string name=\"analytics_preferences\">Penggunaan data</string>\n  <string name=\"analytics\">Kumpulkan data pengguna anonim</string>\n  <string name=\"analytics_summary\">Data penggunaan anonim membantu tim ODK untuk memprioritaskan perbaikan dan fitur.</string>\n  <string name=\"form_metadata_title\">Metadata Formulir</string>\n  <string name=\"form_metadata\">Metadata formulir</string>\n  <string name=\"form_metadata_summary\">Semua nilai ini akan ditambahkan ke formulir yang menyediakan field nama pengguna, email dan/atau telepon untuk mengidentifikasi orang yang mengirimkan data.</string>\n  <string name=\"user_defined\">Ditentukan-pengguna</string>\n  <string name=\"phone_number\">Nomor Telepon</string>\n  <string name=\"email\">Alamat Email</string>\n  <string name=\"invalid_email_address\">Alamat email tidak valid</string>\n  <string name=\"device_defined\">Ditentukan-perangkat</string>\n  <string name=\"device_id\">ID Perangkat</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Tidak tersedia</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">Buka kunci pengaturan</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">Pengaturan tidak terkunci</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <string name=\"setting_not_available_during_form_entry\">Pengaturan tidak tersedia saat pengisian formulir.</string>\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Masukan Kata Sandi Baru</string>\n  <string name=\"admin_password\">Kata Sandi Admin</string>\n  <string name=\"admin_password_changed\">Kata sandi admin berhasil diubah</string>\n  <string name=\"admin_password_disabled\">Kata sandi admin ditiadakan</string>\n  <string name=\"admin_password_incorrect\">Maaf, kata sandi tidak tepat.</string>\n  <string name=\"enter_admin_password\">Masukan Kata Sandi Admin</string>\n  <string name=\"change_admin_password\">Ubah Kata sandi Admin</string>\n  <string name=\"show_password\">Tunjukan kata sandi</string>\n  <string name=\"found_in_main\">Hapus centang untuk menyembunyikan dari Menu Utama</string>\n  <string name=\"found_in_settings\">Hapus centang untuk menyembunyikan dari Pengaturan yang Tidak Dilindungi</string>\n  <string name=\"form_entry\">Hapus centang untuk menyembunyikan dari Pengisian Formulir</string>\n  <string name=\"form_end\">Hapus centang untuk menyembunyikan dari Akhir Formulir.</string>\n  <string name=\"reset_project_settings_title\">Set Ulang</string>\n  <string name=\"reset_project_settings_subtext\">Pilih dari pengaturan, formulir, data</string>\n  <string name=\"delete_project\">Hapus</string>\n  <string name=\"select_all\">Pilih Semua</string>\n  <string name=\"clear_all\">Batal Pilih</string>\n  <string name=\"reset_settings\">Seluruh pengaturan (pengaturan internal, pengaturan tersimpan)</string>\n  <string name=\"reset_saved_forms\">Formulir tersimpan (folder instances, database instances)</string>\n  <string name=\"reset_blank_forms\">Formulir kosong (folder formulir, database formulir, database itemsets)</string>\n  <string name=\"reset_layers\">Lapisan peta (folder lapisan)</string>\n  <string name=\"reset_cache\">Cache formulir yang dimuat (folder .cache)</string>\n  <string name=\"reset_settings_dialog_title\">Pilih apa yang ingin di set ulang</string>\n  <string name=\"reset_settings_button_reset\">Set Ulang</string>\n  <string name=\"reset_in_progress\">Mengeset ulang…</string>\n  <string name=\"reset_app_state_result\">Set ulang hasil</string>\n  <string name=\"reset_app_state_warning\">Semua data terpilih akan dihapus secara permanen dan tidak dapat dikembalikan.</string>\n  <string name=\"reset_settings_result\">Seluruh pengaturan: %s</string>\n  <string name=\"reset_saved_forms_result\">Simpan formulir :: %s</string>\n  <string name=\"reset_blank_forms_result\">Formulir kosong :: %s</string>\n  <string name=\"reset_layers_result\">Lapisan peta :: %s</string>\n  <string name=\"reset_cache_result\">Cache formulir yang dimuat :: %s</string>\n  <string name=\"main_menu_settings\">Pengaturan Menu Utama</string>\n  <string name=\"main_menu_settings_summary\">Tampilkan atau sembunyikan tombol</string>\n  <string name=\"user_settings\">Pengaturan Pengguna</string>\n  <string name=\"user_setting_summary\">Tampilkan atau sembunyikan pengaturan</string>\n  <string name=\"form_entry_setting\">Pengaturan Pengisian Formulir</string>\n  <string name=\"form_entry_settings_summary\">Tampilkan atau sembunyikan langkah</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Kembali ke halaman sebelumnya</string>\n  <string name=\"moving_backwards_disabled_title\">Kembali ke halaman sebelumnya dinonaktifkan</string>\n  <string name=\"moving_backwards_disabled_message\">Mengonfigurasi perangkat untuk mencegah pengguna melewati pengaturan ini?\\n\\nPerubahan yang dilakukan:\\n\\u2022 Menonaktifkan Ubah Draf\\n\\u2022 Menonaktifkan Simpan sebagai draf\\n\\u2022 Menonaktifkan Daftar Isi\\n\\u2022 Aktifkan pengolahan batasan untuk memvalidasi pada saat menggeser layar ke kanan</string>\n  <string name=\"yes\">Ya</string>\n  <string name=\"no\">Tidak</string>\n  <string name=\"moving_backwards_enabled_title\">Kembali ke halaman sebelumnya diaktifkan</string>\n  <string name=\"moving_backwards_enabled_message\">Anda mungkin ingin meninjau pengaturan berikut:\\n\\n\\u2022 Ubah Draf\\n\\u2022 Simpan sebagai draf\\n\\u2022 Daftar Isi\\n\\u2022 Pengolahan batasan</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Simpan sebagai draf</string>\n  <string name=\"save_mid_summary\">Icon simpan di bilah atas dan tombol \\\"Simpan sebagai draf\\\" saat keluar dari formulir.</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Tentang</string>\n  <string name=\"tell_your_friends\">Bagikan ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Apakah rekan Anda masih menggunakan kertas untuk pengumpulan data? Kabari mereka untuk menggunakan ODK Collect.</string>\n  <string name=\"leave_a_review\">Berikan ulasan di Play Store</string>\n  <string name=\"leave_a_review_msg\">Ulasan Anda (semoga positif) dapat mempromosikan aplikasi ODK di Play Store.</string>\n  <string name=\"odk_website\">Kunjungi situs ODK </string>\n  <string name=\"odk_website_summary\">ODK digunakan sebagai pengumpulan data untuk kepentingan sosial di lingkungan yang menantang.</string>\n  <string name=\"odk_forum\">Gabung forum ODK</string>\n  <string name=\"odk_forum_summary\">Bergabung di Forum untuk mendapatkan bantuan, permintaan fitur, berkontribusi kode program/terjemahan!</string>\n  <string name=\"all_open_source_licenses\">Lisensi/pustaka sumber terbuka</string>\n  <string name=\"all_open_source_licenses_msg\">Kita menjadi besar karena berkolaborasi bersama!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">Konfigurasi ulang menggunakan kode QR</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">Ganti seluruh pengaturan yang ada</string>\n  <string name=\"qr_code\">Konfigurasi kode QR</string>\n  <string name=\"scan_qr_code_fragment_title\">Pemindaian</string>\n  <string name=\"view_qr_code_fragment_title\">Kode QR</string>\n  <string name=\"import_qrcode_sd\">Impor kode QR</string>\n  <string name=\"successfully_imported_settings\">Pengaturan berhasil diimpor</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Batas karakter maksimum tercapai: QR code tidak dapat dibuat untuk seluruh pengaturan</string>\n  <string name=\"share\">Bagikan</string>\n  <string name=\"turn_on_flashlight\">Nyalakan senter</string>\n  <string name=\"turn_off_flashlight\">Matikan senter</string>\n  <string name=\"qrcode_with_both_passwords\">Berisi informasi sensitif: <b>admin</b> dan <b>kata sandi server</b></string>\n  <string name=\"qrcode_without_passwords\">Kode QR tidak berisi <b>admin</b> atau kata sandi <b>server</b>. Sentuh untuk konfigurasi</string>\n  <string name=\"qrcode_with_admin_password\">Berisi informasi sensitif: kata sandi <b>admin</b></string>\n  <string name=\"qrcode_with_server_password\">Berisi informasi sensitif: kata sandi <b>server</b></string>\n  <string name=\"server_password\">Kata Santi Server</string>\n  <string name=\"include_password_dialog\">Kata Sandi Disertakan dalam Kode</string>\n  <string name=\"generate\">Generate</string>\n  <string name=\"invalid_qrcode\">QR code tidak mengandung pengaturan yang benar</string>\n  <string name=\"qr_code_not_found\">Kode QR tidak ditemukan pada gambar terpilih</string>\n  <string name=\"corrupt_imported_preferences_error\">Pengaturan yang sekarang bermasalah. Dari pengaturan manajemen proyek, atur ulang pengaturan atau impor pengaturan.</string>\n  <string name=\"settings_with_gd_protocol\">Proyek Google Drive/Sheets tidak dapat dibuat lagi</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Batal</string>\n  <string name=\"cancel_loading_form\">Batal</string>\n  <string name=\"cancel_location\">Batal</string>\n  <string name=\"do_not_change\">Batal</string>\n  <string name=\"canceling\">Membatalkan</string>\n  <string name=\"notification_channel_name\">Pemberitahuan umum</string>\n  <string name=\"success\">Berhasil</string>\n  <string name=\"error_occured\">Terjadi Kesalahan</string>\n  <string name=\"please_wait\">Mohon tunggu sebentar…</string>\n  <string name=\"please_wait_long\">Mohon tunggu. Proses ini membutuhkan waktu beberapa menit.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Tidak dapat menghapus \\'%s\\'. Mohon hapus berkas secara manual dan unduh ulang formulirnya.</string>\n  <string name=\"fs_create_media_folder_error\">Tidak dapat membuat folder media \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Tidak dapat menyalin \\'%1$s\\' terhadap \\'%2$s\\'. Penyebab: %3$s</string>\n  <string name=\"select_odk_shortcut\">Pilih pintasan ODK</string>\n  <string name=\"background_audio_help\">Formulir ini merekam suara dari perangkat mikrofon Anda. Anda dapat menggunakan indikator volume untuk memastikan bahwa mikrofon sudah cukup dekat dengan suara yang ingin Anda rekam. Untuk menghentikan perekaman mohon keluar dari formulir ini. Untuk informasi lebih lanjut mohon hubungi orang yang meminta anda untuk mengumpulkan data.</string>\n  <string name=\"loading\">Memuat…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Proyek</string>\n  <string name=\"settings\">Pengaturan</string>\n  <string name=\"add_project\">Tambahkan proyek</string>\n  <string name=\"add\">Tambah</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Salin proyek</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">Anda sudah memiliki proyek dengan pengaturan koneksi ini. Apa Anda ingin beralih ke proyek yang ada atau menambah baru?</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Tambahkan salinan proyek</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Beralih ke proyek yang ada</string>\n  <string name=\"project_name\">Nama Proyek</string>\n  <string name=\"project_icon\">Simbol Proyek</string>\n  <string name=\"project_color\">Warna Proyek</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Telah beralih ke %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">Menggunakan %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Beralih ke %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <string name=\"delete_project_dialog_title\">Hapus%s</string>\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <string name=\"delete_project_message\">Dengan menghapus Proyek ini, Anda akan menghapus secara permanen hal-hal berikut ini.</string>\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <string name=\"delete_project_instructions\">Ketik%1$s Ketik untuk, konfirmasi tindakan ini.</string>\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <string name=\"delete_trigger\">menghapus</string>\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <string name=\"form_definitions_count\">Definisi Formulir: %1$d</string>\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <string name=\"sent_count\">Terkirim: %1$d</string>\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <string name=\"unsent_count\">Tidak terkirim: %1$d</string>\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <string name=\"drafts_count\">Konsepsi: %1$d</string>\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"delete_project_confirm_button_text\">Hapus Proyek</string>\n  <string name=\"or\">ATAU</string>\n  <string name=\"hex_color\">Warna heksagonal</string>\n  <string name=\"invalid_hex_code\">Kode heksa tidak valid</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Kumpulkan data di mana saja</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Konfigurasi dengan QR kode</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Masukkan rincian proyek secara manual</string>\n  <string name=\"project_config_tip\">Setelah anda menambahkan proyek anda, anda dapat mengatur konfigurasinya di Pengaturan.</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Belum memliki proyek?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Coba tampilan</string>\n  <string name=\"scan_configuration_qr_code\">Pindai kode QR konfigurasi</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Tampilkan rincian</string>\n  <string name=\"errors\">Kesalahan</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <string name=\"no_browser_installed\">Tidak ada browser yang terpasang!</string>\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">Untuk membuka formulir ini, Anda harus membuka ODK Collect dan beralih ke proyek yang memuatnya.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">ODK Collect belum dikonfigurasi. Cobalah membuka ODK Collect dan mengatur konfigurasinya.\\n\\nJika anda mengetuk pintasan, anda mungkin perlu membuatnya kembali setelah mengkonfigurasi ODK Collect.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">URI tidak dikenal: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">URI salah: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Formulir terisi telah dihapus!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">Draf ini tidak dapat diubah karena formulir kosong terkait tidak ada atau telah dihapus.\\n\\nID Formulir:%1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Anda mengunduh dua formulir berbeda, tetapi memiliki ID dan versi formulir yang sama. Kemungkinan keduanya adalah formulir yang sama dan diunggah pada saat yang berbeda atau pada server yang berbeda. Pada kasus ini, hapus salah satu formulir.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <string name=\"cannot_open_form_because_of_forms_update\">Saat ini, formulir tidak dapat dibuka karena sedang diperbarui. Silakan coba beberapa saat lagi.</string>\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Buka Pengaturan</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <string name=\"entities_title\">Daftar entitas</string>\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <string name=\"entity_browser_button\">Lihat daftar entitas</string>\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">Luring</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">Formulir ini tidak didukung oleh versi ini. Silakan perbarui aplikasi. Jika masalah terus berlanjut, laporkan kepada orang yang meminta Anda mengumpulkan data.\\n\\nVersi spesifikasi Entitas Formulir: %s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">Terjadi kegagalan sistem saat aplikasi berjalan terakhir kali!</string>\n  <string name=\"cant_start_app\">Tidak dapat memulai aplikasi!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">Proyek ini sebelumnya terhubung ke akun Google Drive. Dukungan untuk Google Drive telah dihapus. Anda dapat mengonfigurasi server atau menarik kiriman ke komputer.</string>\n  <string name=\"learn_more_button_text\">Pelajari lebih lanjut</string>\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">Alat pengembang</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">Paksa aplikasi untuk crash</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Memaksa terjadinya pengecualian yang tidak ditangani sehingga aplikasi berhenti secara tiba-tiba</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <string name=\"debug_filters\">Filter debug</string>\n  <string name=\"debug_filters_summary\">Tampilkan informasi debug untuk eksekusi filter selama pengisian formulir.</string>\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"use_zxing_for_barcode_scanning\">Kembali ke pemindai kode batang versi lama</string>\n  <string name=\"permission_dialog_title\">Tentang izin</string>\n  <string name=\"permission_dialog_text\">Anda akan diminta untuk mengizinkan ODK Collect mengakses fitur-fitur di bawah ini, pilih “izinkan” jika anda ingin menggunakannya.</string>\n  <string name=\"notifications\">Notifikasi</string>\n  <string name=\"required_for_notifications\">Diperlukan untuk menampilkan pembaruan saat formulir diunduh, diperbarui, dan dikirimkan.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">Formulir anda telah disimpan sebagai draf.</string>\n  <string name=\"form_saved\">Formulir anda telah disimpan.</string>\n  <string name=\"form_sending\">Kirim formulir…</string>\n  <string name=\"edit_form\">Ubah</string>\n  <string name=\"view_form\">Lihat</string>\n  <string name=\"close_snackbar\">Tutup snackbar</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"other\">Formulir terakhir dikirim: %d beberapa detik yang lalu.</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"other\">Formulir terakhir dikirim: %d beberapa menit yang lalu.</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"other\">Formulir terakhir dikirim: %d beberapa jam yang lalu.</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"other\">Formulir terakhir dikirim: %d beberapa hari yang lalu.</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"other\">%d formulir siap kirim</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">Pada versi terbaru, formulir yang sudah final tidak dapat diedit. Simpan formulir sebagai draf jika ingin mengubahnya.\\n\\nAnda dapat memeriksa kesalahan dalam formulir draf dengan mengetuk tiga titik (⋮), lalu pilih Periksa kesalahan.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">Pada versi terbaru, formulir yang sudah final tidak dapat diedit.</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">Finalkan semua draf</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"other\">Anda ingin finalisasi %d draf?</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">Setelah semua draf sudah difinalkan, formulir akan berada dalam status \\\"Siap kirim\\\" dan tidak dapat diedit lagi. Draf yang memiliki kesalahan tidak bisa difinalkan.\\n\\nTindakan ini tidak dapat dibatalkan.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"other\">Sukses! %d draft sudah final!</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"other\">%d draf memiliki kesalahan yang harus diperbaiki sebelum difinalisasi.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d draf sudah final. %d draf memiliki kesalahan yang harus diperbaiki sebelum final.</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d draf sudah final. Draf yang tersisa perlu difinalisasi secara manual.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Kesalahan</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">Tidak ada kesalahan</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">Hapus centang untuk menyembunyikan dari Draf.</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">Tidak ada formulir</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">Tidak ada formulir kosong</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">Unduh formulir untuk mulai.</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">Tidak ada draf</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">Saat Anda menyimpan sebagai draf, formulir akan muncul di sini.</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">Tidak ada yang siap kirim</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">Saat Anda memfinalisasi draf, formulir akan muncul di sini.</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">Tidak ada yang dikirim</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">Saat Anda mengirim formulir yang telah final, formulir tersebut akan muncul di sini.</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">Tidak ada fomulir yang bisa diunduh</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">Unduh formulir untuk mulai.</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">Tidak ada formulir yang bisa dihapus</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">Saat Anda mengunduh formulir kosong, formulir akan muncul di sini</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">Saat Anda telah menyimpan formulir, formulir akan muncul di sini</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">Pulihkan pekerjaan Anda?</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"savepoint_recovery_dialog_message\">\\'Aplikasi tertutup secara tidak terduga pada \\'EEE, MMM dd, yyyy \\'pukul\\' HH:mm\\' dan telah menyimpan pekerjaan Anda.\\n\\nApakah Anda ingin memulihkan atau menghapusnya?\\'</string>\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">Pulihkan</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Buang</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">Ambil titik</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">Lihat atau ubah titik</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">Lihat titik</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">Ubah titik</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">Ambil garis</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">Lihat atau ubah garis</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">Lihat garis</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">Ambil poligon</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">Lihat atau ubah poligon</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">Lihat poligon</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <string name=\"settings_json\">Pengaturan JSON</string>\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <string name=\"preparing_form_edit\">Mempersiapkan Formulir untuk diedit…</string>\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <string name=\"user_visible_instance_name\">%s (Edit %d)</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_title\">Lanjutkan mengedit draf?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_draft_edit_found_dialog_message\">\\'Draf ubah telah disimpan pada\\' EEE, MMM dd, yyyy \\'pukul\\' HH:mm\\'\\n\\nApakah Anda ingin melanjutkan ubah ini?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_positive_button\">melanjutkan penyuntingan</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_title\">ubah versi terbaru?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_finalized_edit_found_dialog_message\">\\'Pengajuan ini sebelumnya telah diedit.\\n\\nApakah Anda ingin ubah versi terbaru yang diselesaikan pada \\'EEE, MMM dd, yyyy\\' pukul \\'HH:mm\\'?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_positive_button\">ubah Formulir terbaru</string>\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n  <string name=\"finalized_form_edit_started\">Anda sekarang sedang mengedit formulir ini. Ketuk pertanyaan untuk mulai mengedit.</string>\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-it/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Inizia nuovo modulo</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Bozze</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Pronto per l\\'invio</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Inviati</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Scarica modulo</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Elimina modulo</string>\n  <string name=\"saved_forms\">Moduli salvati</string>\n  <string name=\"version_number\">Versione: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Aggiunto il\\' EEE, MMM dd, yyyy \\'alle\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Aggiornato il\\' EEE, dd MMM yyyy \\'alle\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Salvato il\\' EEE, MMM dd, yyyy \\'alle\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Completato il\\' EEE, MMM dd, yyyy \\'alle\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Inviato il\\' EEE, MMM dd, yyyy \\'alle\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Invio fallito il\\' EEE, MMM dd, yyyy \\'alle\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Cancellato il\\' EEE, MMM dd, yyyy \\'alle\\' HH:mm</string>\n  <string name=\"submission_deleted\">Record inviato cancellato</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Modificato il\\' EEE, dd MMM yyyy \\'alle\\' HH:mm</string>\n  <string name=\"encrypted_form\">Modulo crittografato</string>\n  <string name=\"deleted_form\">Modulo cancellato</string>\n  <string name=\"sort_the_list\">Ordina la lista</string>\n  <string name=\"sort_by\">Ordina per</string>\n  <string name=\"sort_by_name_asc\">Nome, A-Z</string>\n  <string name=\"sort_by_name_desc\">Nome, Z-A</string>\n  <string name=\"sort_by_date_desc\">Per data, prima i più recenti</string>\n  <string name=\"sort_by_date_asc\">Per data, prima i meno recenti</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">L\\'ultimo salvato per primo</string>\n  <string name=\"filter_the_list\">Filtra la lista</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Ricerca</string>\n  <string name=\"not_granted_permission\">L\\'attività che stai tentando di avviare richiede un\\'autorizzazione che non viene concessa.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Autorizzazione all\\'uso della memoria</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Senza questi permessi Collect non può accedere ai tuoi questionari o salvare le risposte. Riapri la applicazione quando sei pronto ad autorizzarli.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Autorizzazione all\\'uso dei servizi di localizzazione</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Senza<b>il permesso sulla posizione esatta</b>Collect non può registrare la tua posizione. Riprova quando sei pronto a concederlo. </string>\n  <string name=\"camera_runtime_permission_denied_title\">Autorizzazione uso fotocamera</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Senza questa autorizzazione Collect non può usare la fotocamera. Prova di nuovo quando hai autorizzato l\\'uso della fotocamera</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Autorizzazione uso microfono</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Senza questa autorizzazione Collect non può registrare i suoni. Prova di nuovo quando hai autorizzato l\\'uso del microfono</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Apri formulario di tipo mappa</string>\n  <string name=\"instance_scan_count\">%1$d moduli aggiunti.</string>\n  <string name=\"finished_disk_scan\">Scansione terminata. Tutti i moduli caricati.</string>\n  <string name=\"xform_parse_error\">XForm errore di parsing per %1$s: %2$s mancante o non valido.</string>\n  <string name=\"loading_form\">Caricamento Modulo</string>\n  <string name=\"survey_loading_reading_form_message\">Lettura definizione modulo…</string>\n  <string name=\"survey_loading_reading_data_message\">Lettura dati survey…</string>\n  <string name=\"survey_loading_reading_csv_message\">Lettura file CSV…</string>\n  <string name=\"parse_error\">Spiacenti, non sono in grado di analizzare il modulo.</string>\n  <string name=\"loading_form_failed\">Si è verificato un errore durante il caricamento del modulo. Per favore riprova. </string>\n  <string name=\"cannot_edit_completed_form\">Questo modulo non può essere modificato una volta che viene segnato come completato. Potrebbe venire cifrato.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Cambiamenti non salvati recuperati da punto salvato!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Cambia Lingua</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Apri visione gerarchica</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Salva come bozza</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Vai all\\'inizio</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Vai alla fine</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Vai al livello superiore</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <string name=\"edit_finalized_form\">Modifica Modulo completato</string>\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Gruppo</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Gruppo ripetitivo</string>\n  <string name=\"exit\">Esci</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Aggingere \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Aggiungere</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"do_not_add_repeat\">Non aggiungere</string>\n  <string name=\"add_another_menu\">Aggiungi un altro</string>\n  <string name=\"add_another_question\">Aggiungi un altro?</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Modifica Prompt</string>\n  <string name=\"clearanswer_confirm\">Rimuovere la risposta a \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Rimuovere la risposta</string>\n  <string name=\"clear_answer_ask\">Rimuovere questa risposta?</string>\n  <string name=\"clear_answer_no\">Annulla</string>\n  <string name=\"discard_answer\">Rimuovere la risposta</string>\n  <string name=\"discard_group\">Rimuovere il gruppo</string>\n  <string name=\"delete_repeat\">Rimuovere il gruppo</string>\n  <string name=\"delete_repeat_ask\">Rimuovere questo gruppo?</string>\n  <string name=\"delete_repeat_confirm\">Rimuovere il gruppo \\\"%s\\\" e tutti i suoi sottogruppi?</string>\n  <string name=\"delete_repeat_no\">Annulla</string>\n  <string name=\"invalid_answer_error\">Spiacenti, questa risposta non è valida!</string>\n  <string name=\"required_answer_error\">Spiacenti, questa risposta è richiesta!</string>\n  <string name=\"success_form_validation\">Successo! Nessun errore trovato nel tuo formulario.</string>\n  <string name=\"invalid_space_in_answer_singular\">Attenzione: ci sono degli spazi nel valore sottostante %s </string>\n  <string name=\"invalid_space_in_answer_plural\">Attenzione: ci sono degli spazi nei valori sottostanti %s </string>\n  <string name=\"too_complex_form\">Questo modulo è troppo complesso per questo dispositivo. Prova a semplificare le espressioni o chiedi aiuto sul forum. </string>\n  <string name=\"survey_internal_error\">Errore interno: invio suggerimento fallito.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Ti trovi alla fine di \\\"%s\\\".</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <string name=\"form_editing_disabled_after_finalizing\">Non è possibile apportare modifiche dopo la finalizzazione.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <string name=\"form_editing_disabled_after_sending\">Non è possibile apportare modifiche dopo l\\'invio.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_finalizing\">È possibile apportare modifiche dopo la finalizzazione.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_sending\">È possibile apportare modifiche dopo l\\'invio.</string>\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Se devi apportare modifiche al modulo, \\\"Salva come bozza\\\" finché non sei pronto per l\\'invio.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <string name=\"form_editing_enabled_after_finalizing_hint\">Per apportare modifiche dopo aver completato il formulario, vai su “Pronto per l\\'invio”, apri il formulario e tocca l\\'icona di modifica.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <string name=\"form_editing_enabled_after_sending_hint\">Per apportare modifiche dopo aver inviato il formulario, vai su “Inviati”, apri il formulario e tocca l\\'icona di modifica.</string>\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Salva come bozza</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Finalizzare</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Invia</string>\n  <string name=\"data_saved_error\">Spiacenti, salvataggio modulo fallito!</string>\n  <string name=\"data_saved_ok\">Modulo salvato correttamente!</string>\n  <string name=\"save_point_error\">Errore nel salvataggio del punto di ripristino: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Elimina formulario</string>\n  <string name=\"discard_changes\">Non salvare le modifiche</string>\n  <string name=\"keep_changes\">Salva le modifiche</string>\n  <string name=\"keep_editing\">Continua a modificare</string>\n  <string name=\"quit_form_title\">Salvare il Modulo?</string>\n  <string name=\"save_explanation\">Puoi salvare questo modulo e accedervi dalle tue bozze in qualsiasi momento.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'Questo modulo è stato salvato l\\'\\'ultima volta il\\' EEE, MMM dd, yyyy \\'alle\\' HH:mm\\'. Salva come bozza per conservare le modifiche.\\'</string>\n  <string name=\"quit_form_continue_title\">Continua con le modifiche?</string>\n  <string name=\"discard_form_warning\">Se elimini il modulo, perderai tutte le modifiche apportate fino a quel momento.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'Questo modulo è stato salvato l\\'\\'ultima volta il\\' EEE, MMM dd, yyyy \\'alle\\' HH:mm\\'. Se scarti le modifiche, perderai tutte le modifiche apportate da allora.\\'</string>\n  <string name=\"saving_form\">Salvataggio Modulo</string>\n  <string name=\"survey_saving_validating_message\">Convalida risposte…</string>\n  <string name=\"survey_saving_collecting_message\">Raccolta dati in corso…</string>\n  <string name=\"survey_saving_saving_message\">Salvataggio sulla scheda SD…</string>\n  <string name=\"survey_saving_finalizing_message\">Completamento nella scheda SD…</string>\n  <string name=\"survey_saving_encrypting_message\">Crittografia dati…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Neanche un risultato per questa ricerca!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Nessuno dei moduli vuoti corrisponde a questo jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Nessun FormId specificato???</string>\n  <string name=\"phone_does_not_support_rsa\">Il telefono non supporta la crittografia RSA.</string>\n  <string name=\"invalid_rsa_public_key\">Chiave RSA pubblica non valida.</string>\n  <string name=\"encryption_error_message\">%s Il modulo non è stato ancora salvato come completato.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">Non è possibile utilizzare il formulario</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Impossibile avviare la registrazione. </string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Il microfono è già in uso.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">I media non possono essere allegati al formulario ma sono disponibili all\\'indirizzo %1$s</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Registrazione disabilitata. Abilita in %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Errore durante il tentativo di aggiornare i valori. Ciò è generalmente dovuto a un utilizzo errato dei calcoli nella progettazione del modulo.\\n\\nSe continui ad avere questo problema, segnalalo alla persona che ti ha chiesto di raccogliere i dati.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Invio</string>\n  <string name=\"activity_not_found\">Nessuna attività trovata da gestire: %s</string>\n  <string name=\"no_app\">L\\'applicazione richiesta è mancante. Per piacere inserisci manualmente la lettura.</string>\n  <string name=\"null_intent_value\">L\\'applicativo esterno non ha fornito l\\'informazione attesa.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">Stampa</string>\n  <string name=\"open_url\">Apri URL</string>\n  <string name=\"capture_osm\">Lancia OpenMapKit</string>\n  <string name=\"recapture_osm\">Rifai tag a OSM</string>\n  <string name=\"edited_osm_file\">File XML di OSM modificato:</string>\n  <string name=\"invalid_osm_data\">Qualcosa è andato storto. Non abbiamo ricevuto validi dati OSM.</string>\n  <string name=\"alert\">Attenzione</string>\n  <string name=\"install_openmapkit\">Per favore installare OpenMapKit!</string>\n  <string name=\"get_barcode\">Cattura un codice a barre</string>\n  <string name=\"replace_barcode\">Sostituire il codice a barre</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Inserire il codice a barre dentro il rettangolo</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"rotate_device\">Ruotare dispositivo</string>\n  <string name=\"open_file\">Apri documento</string>\n  <string name=\"rank_items\">Ordina gli elementi</string>\n  <string name=\"number_picker_title\">Selezionatore numerico</string>\n  <string name=\"invalid_range_widget\">Questo widget ha parametri invalidi!</string>\n  <string name=\"svg_file_does_not_exist\">Il file SVG non esiste!</string>\n  <string name=\"capture_image\">Scatta Foto</string>\n  <string name=\"choose_image\">Seleziona Foto</string>\n  <string name=\"selected_invalid_image\">Il file selezionato non è una valida immagine</string>\n  <string name=\"take_picture_instruction\">Tocca lo schermo per scattare una foto</string>\n  <string name=\"error_front_camera_unavailable\">Fotocamera frontale non disponibile in questo dispositivo</string>\n  <string name=\"camera_error\">Si è verificato un errore durante lo scatto della foto</string>\n  <string name=\"camera_failed_to_initialize\">Impossibile inizializzare la fotocamera!</string>\n  <string name=\"annotate_image\">Annota Immagine</string>\n  <string name=\"gif_not_supported\">I file Gif non sono supportati</string>\n  <string name=\"invalid_file_type\">L\\'applicazione ha restituito un tipo di file non valido</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_title\">Salva le modifiche?</string>\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_explanation\">Vuoi salvare le modifiche prima di uscire?</string>\n  <string name=\"save_and_close\">Salva e Chiudi</string>\n  <string name=\"signature_capture\">Firma</string>\n  <string name=\"sign_button\">Inserimento Firma</string>\n  <string name=\"markup_image\">Markup Immagine</string>\n  <string name=\"draw_image\">Bozza Immagine</string>\n  <string name=\"reset_image\">Reset</string>\n  <string name=\"set_color\">Imposta Colore</string>\n  <string name=\"capture_video\">Registra Video</string>\n  <string name=\"choose_video\">Seleziona Video</string>\n  <string name=\"play_video\">Riproduci Video</string>\n  <string name=\"choose_file\">Seleziona File</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Elimina</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Registra Suono</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Seleziona Suono</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Metti in pausa</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Riprendi</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">Registra un audio: attivo</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">Registra un audio: disattivo</string>\n  <string name=\"background_audio_permission_explanation\">Questo formulario registra l\\'audio in sottofondo. Devi concedere l\\'autorizzazione per utilizzare il microfono. In caso contrario, non sarà possibile aprire questo formulario.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Questo formulario richiede la registrazione dell\\'audio in background. Disabilitarlo interromperà la registrazione e scarterà l\\'audio esistente. Sei sicuro di voler procedere?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Disabilita registrazione</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">La registrazione non inizierà immediatamente. Devi riaprire il modulo per registrare.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Eliminare il file?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">Non sarai in grado di recuperare questo file una volta eliminato. Sei sicuro di voler procedere?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Stop</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Registrando…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Registrazione audio</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Riproduci audio</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Salvando il file</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Devi interrompere la registrazione prima di uscire da questa schermata. </string>\n  <string name=\"error_attaching_binary_file\">Si è verificato un errore mentre allegavi: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Selezione valore</string>\n  <string name=\"select_answer\">Selezionare Risposta</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Modifica valore</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Nessun valore selezionato</string>\n  <string name=\"selected_title\">Selezionato:</string>\n  <string name=\"trigger\">OK. Perpiacere continua.</string>\n  <string name=\"ext_file_no_data_error\">Attenzione il file non contiene dati!</string>\n  <string name=\"ext_conflicting_columns_error\">Le colonne %s corrispondono!</string>\n  <string name=\"ext_sortBy_numeric_error\">Il tuo riordino per colonne dovrebbe contenere solo valori numerici. Il valore di conflitto era \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Errore di sintassi nella funzione search(): la funzione necessita di 1,4 o 6 argomenti.</string>\n  <string name=\"ext_search_wrong_function_error\">Errore di sintassi nella funzione search():  funzione non riconosciuta \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Errore di sintassi nella funzione search(): \\'%s\\' non è stata valutata come una funzione.</string>\n  <string name=\"ext_search_return_error\">Il tool di ricerca ha restituito un oggetto di tipo \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Non posso importare dati da %1$s. Motivo: %2$s</string>\n  <string name=\"ext_import_progress_message\">Pre-caricamento dati da \\'%1$s\\', attendere prego… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Dati di lettura cancellati!</string>\n  <string name=\"ext_import_finalizing_message\">Pre-caricamento dei dati in corso…</string>\n  <string name=\"ext_import_completed_message\">Lettura dati completata!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager non è stato inizializzato.</string>\n  <string name=\"ext_import_csv_missing_error\">I dati esterni per %1$s non sono stati importati. Probabilmente hai dimenticato di includere il file %2$s.csv nel tuo modulo?</string>\n  <string name=\"ext_search_generic_error\">Errore di sintassi nella funzione search(): %s.</string>\n  <string name=\"ext_assign_value_error\">Impossibile assegnare un valore a \\'%s\\'.</string>\n  <string name=\"parser_exception\">Eccezione XPathParser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Registra Bussola</string>\n  <string name=\"replace_bearing\">Sostituisci Bussola</string>\n  <string name=\"getting_bearing\">Carica Bussola</string>\n  <string name=\"accept_bearing\">Registra Bussola</string>\n  <string name=\"bearing\">Orientamento: %.3f </string>\n  <string name=\"direction\">Direzione: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Impossibile rilevare il rilevamento: nel dispositivo mancano l\\'accelerometro, il sensore di campo magnetico o entrambi.</string>\n  <string name=\"select_date\">Seleziona una data</string>\n  <string name=\"select_time\">Seleziona un orario</string>\n  <string name=\"no_date_selected\">Nessuna data selezionata</string>\n  <string name=\"no_time_selected\">Nessun orario selezionato</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <string name=\"buddhist_month_1\">Gennaio</string>\n  <!--2nd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_2\">Febbraio</string>\n  <!--3rd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_3\">Marzo</string>\n  <!--4th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_4\">Aprile</string>\n  <!--5th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_5\">Maggio</string>\n  <!--6th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_6\">Giugno</string>\n  <!--7th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_7\">Luglio</string>\n  <!--8th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_8\">Agosto</string>\n  <!--9th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_9\">Settembre</string>\n  <!--10th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_10\">Ottobre</string>\n  <!--11th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_11\">Novembre</string>\n  <!--12th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_12\">Dicembre</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">File: %s non è valido.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">File: %s manca.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Inserisci identità</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Identità</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Questo form necessita che tu ti dentifichi</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Chiudere</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Motivo per la modifica</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">motivo</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Devi spiegare il motivo per la modifica di questo form</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Salvare</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">Selezionare il posto</string>\n  <string name=\"choices\">Scelte</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">Mappa Topologia Nazionale USGS</string>\n  <string name=\"openmap_usgs_sat\">Mappa Ibrida Nazionale USGS</string>\n  <string name=\"openmap_usgs_img\">Immagini di Mappe Nazionali USGS</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Mappa Base</string>\n  <string name=\"basemap_source\">Origine</string>\n  <string name=\"basemap_source_unavailable\">Spiacente, %s le mappe di base non sono disponibili su questo dispositivo. Scegli un\\'altra mappa di base in Impostazioni &gt; Mappe.</string>\n  <string name=\"map_style_label\">%sstile mappa</string>\n  <string name=\"streets\">Streets - Stradale</string>\n  <string name=\"terrain\">Terrain - Territoriale</string>\n  <string name=\"hybrid\">Hybrid - Ibrida</string>\n  <string name=\"satellite\">Satellite - Satellitare</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Chiaro</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Scuro</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">All\\'aperto</string>\n  <string name=\"topographic\">Topografico</string>\n  <string name=\"google_play_services_error_occured\">Impossibile accedere a Google Maps. Il Servizio di Google Play è installato?</string>\n  <string name=\"carto_map_style_positron\">Posizione</string>\n  <string name=\"carto_map_style_dark_matter\">Scuro è importante</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">Livelli non in linea</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">Livello</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">Seleziona Livello non in linea</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">Selezionare il livello offline da utilizzare per tutte le mappe di questo progetto. È possibile aggiungere opzioni all\\'elenco selezionando un file MBTiles dal dispositivo e cancellare i livelli esistenti dall\\'elenco.</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">Per saperne di più sull\\'aggiunta di MBTiles.</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">Aggiungi Livelli</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">Eliminare Livello</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <plurals name=\"non_mbtiles_files_selected_title\">\n    <item quantity=\"one\">%d livello non può essere aggiunto</item>\n    <item quantity=\"many\">%d livelli non possono essere aggiunti</item>\n    <item quantity=\"other\">%d  livelli non possono essere aggiunti</item>\n  </plurals>\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">I file selezionati non sono MBTiles. È possibile aggiungere solo file MBTiles.</string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">Alcuni dei file selezionati non sono MBTiles. È possibile aggiungere solo file MBTiles.</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">Livelli</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">Selezionare livelli di accesso</string>\n  <string name=\"select_layer_access_subtitle\">Si desidera che i livelli siano disponibili in tutti i progetti o solo nel progetto corrente?</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">Tutti i progetti</string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">Solo il progetto attuale</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">Sei sicuro di voler eliminare il %1$s livello non in linea?</string>\n  <string name=\"record_geopoint\">Memorizza un punto</string>\n  <string name=\"location_accuracy\">Precisione: %1$s </string>\n  <string name=\"location_accuracy_unacceptable\">⚠️ Precisione: %1$s (inaccettabile)</string>\n  <string name=\"provider_disabled_error\">Attenzione, i Sistemi di Posizionamento sono disabilitati!</string>\n  <string name=\"gps_result\">Latitudine: %1$s\\nLongitudine: %2$s\\nAltitudine: %3$sm\\nPrecisione: %4$sm</string>\n  <string name=\"google_play_services_not_available\">Questo formulario vuole monitorare la tua posizione ma Google Play Services non è disponibile.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Questo formulario vuole monitorare la tua posizione ma i fornitori di servizi di localizzazione sono disabilitati. Si prega di abilitare nelle impostazioni di Android.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Vai a impostazioni</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">Registra la posizione: attivo</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">Registra la posizione: disattivo</string>\n  <string name=\"background_location_disabled\">Questo modulo vuole tracciare la tua posizione ma il tracciamento è disabilitato. Abilita nel %1$s menu sopra.</string>\n  <string name=\"background_location_enabled\">Questo modulo tiene traccia della tua posizione. Puoi disabilitare il monitoraggio nel menu %1$s in alto.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">Controlla gli errori</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">Tempo trascorso: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">Il punto sarà salvato in %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">Satelliti: %1$s</string>\n  <string name=\"waiting_for_location\">Provando a ottenere la posizione. Prego attendere.</string>\n  <string name=\"poor_accuracy\">Scarsa precisione. Attendere prego.</string>\n  <string name=\"unacceptable_accuracy\">Precisione inaccettabile. Attendere prego.</string>\n  <string name=\"improving_accuracy\">Migliorando la precisione. Attendere prego.</string>\n  <string name=\"accuracy_status_title\">Ottenendo la posizione.</string>\n  <!--The accuracy of a location fix in meters-->\n  <string name=\"accuracy_m\">%1$s m</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">O %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Modalità di inserimento</string>\n  <string name=\"start\">Inizia</string>\n  <string name=\"enable_gps\">Abilita GPS</string>\n  <string name=\"gps_enable_message\">Il GPS è disabilitato sul tuo dispositivo. Vuoi abilitarlo?</string>\n  <string name=\"geo_clear_warning\">Caratteristica già creata. Vuoi cancellarla?</string>\n  <string name=\"geo_exit_warning\">Vuoi scartare le modifiche e ritornare ad ODK?</string>\n  <string name=\"polygon_validator\">Devi avere almeno 3 punti per creare un Poligono</string>\n  <string name=\"polyline_validator\">Devi avere almeno 2 punti per creare una Polilinea</string>\n  <string name=\"clear\">Cancella</string>\n  <string name=\"placement_mode\">Inserimento al tocco</string>\n  <string name=\"manual_mode\">Memorizzazione manuale localizzazione</string>\n  <string name=\"automatic_mode\">Memorizzazione automatica localizzazione</string>\n  <string name=\"recording_interval\">Memorizzazione intervallo:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d</item>\n    <item quantity=\"many\">%dsecondi</item>\n    <item quantity=\"other\">%dsecondi</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%dminuti</item>\n    <item quantity=\"many\">%dminuti</item>\n    <item quantity=\"other\">%dminuti</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Requisiti di precisione:</string>\n  <string name=\"none\">Nessuno</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%dmetri</item>\n    <item quantity=\"many\">%d metri</item>\n    <item quantity=\"other\">%d metri</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Punti inseriti: %d</string>\n  <string name=\"collection_status_placement\">Punti inseriti: %d (tocca per inserire punti)</string>\n  <string name=\"collection_status_manual\">Punti inseriti: %d (salvataggio manuale)</string>\n  <string name=\"collection_status_auto_seconds\">Punti inseriti: %1$d (salva ogni %2$d sec)</string>\n  <string name=\"collection_status_auto_minutes\">Punti inseriti: %1$d (salva ogni %2$d min)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Punti inseriti: %1$d (salva ogni %2$d sec, precisione %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Punti inseriti: %1$d (salva ogni %2$d min, precisione %3$d m)</string>\n  <string name=\"geopoint_instruction\">Long press to place mark or tap add marker button</string>\n  <string name=\"geopoint_no_draggable_instruction\">Tocca Aggiungi Posizione.</string>\n  <string name=\"discard\">Annulla</string>\n  <string name=\"select_item_count\">%s: %d (%d mostrato sulla mappa)</string>\n  <string name=\"select_item\">Selezionare</string>\n  <string name=\"new_item\">Nuovo oggetto</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">Modifica bozza</string>\n  <string name=\"view_data\">Visualizza modulo salvato</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">Tracciamento della posizione</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">Monitoraggio della posizione…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Mostra la mia posizione</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">Ingrandisci per adattare tutto</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Metti in pausa</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">Rimuovi l\\'ultimo punto</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <string name=\"error_fixed\">✅ Errore corretto</string>\n  <!--Description for button that explains how to modify the map-->\n  <string name=\"show_how_to_modify_map\">Mostra come modificare la mappa</string>\n  <!--Title for instructions on how to modify map features-->\n  <string name=\"how_to_modify_map\">Come modificare la mappa</string>\n  <!--Instruction in a list on how to move a point-->\n  <string name=\"long_press_to_move_point_info_item\">Premere a lungo per spostare il punto</string>\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">Rimuovi l\\'ultimo punto</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <string name=\"delete_shape_to_start_over_info_item\">Elimina la forma per ricominciare da capo</string>\n  <!--Instruction in a list on how to add a new point-->\n  <string name=\"add_point_info_item\">Aggiungi punto</string>\n  <!--Instruction in a list on how to tap to add a point-->\n  <string name=\"tap_to_add_a_point_info_item\">Tocca per aggiungere un punto</string>\n  <!--Instruction in a list on how to delete the entire shape-->\n  <string name=\"delete_entire_shape_info_item\">Elimina l\\'intera forma</string>\n  <!--Button text to close the instructions dialog-->\n  <string name=\"done\">Fatto</string>\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <string name=\"how_to_modify\">Come modificare</string>\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Invio del Modulo</string>\n  <string name=\"noselect_error\">Spiacenti, non hai selezionato alcun modulo!</string>\n  <string name=\"no_connection\">Nessuna connessione di rete disponibile</string>\n  <string name=\"sending_items\">Invio di %1$s di %2$s moduli</string>\n  <string name=\"send_selected_data\">Invia selezionato</string>\n  <string name=\"server_auth_credentials\">Nome utente o password del server non validi: %s</string>\n  <string name=\"server_requires_auth\">Server richiede l\\'autenticazione</string>\n  <string name=\"no_forms_uploaded\">Spiacenti, nessun modulo è stato caricato.</string>\n  <string name=\"upload_results\">Risultati del caricamento</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Cambia Vista</string>\n  <string name=\"show_sent_and_unsent_forms\">Mostra Moduli Inviati e Non Inviati</string>\n  <string name=\"show_unsent_forms\">Mostra Moduli Non Inviati</string>\n  <string name=\"send_in_progress\">Invio in background in corso, perpiacere riprova più tardi</string>\n  <string name=\"forms_upload_succeeded\">Caricamento Formulario riuscito</string>\n  <string name=\"forms_upload_failed\">Caricamento Formulario fallito</string>\n  <string name=\"all_uploads_succeeded\">Tutti i caricamenti sono riusciti!</string>\n  <string name=\"some_uploads_failed\">%1$s di %2$s caricamenti falliti!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Aggiorna</string>\n  <string name=\"download\">Scarica Selezionato</string>\n  <string name=\"downloading_data\">Si sta connettendo al server</string>\n  <string name=\"fetching_file\">Caricamento %1$s.\\n\\nModulo %2$s di %3$s modulo(i).</string>\n  <string name=\"form_download_progress\">%1$s. Caricamento file multimediali: %2$s di %3$s</string>\n  <string name=\"version\">Versione:</string>\n  <string name=\"form_updates_available\">Aggiornamenti Modulo disponibili</string>\n  <string name=\"newer_version_of_a_form_info\">Questo è un aggiornamento di un form che hai già</string>\n  <string name=\"form_update_error\">Aggiornamento Formulario fallito</string>\n  <string name=\"form_update_error_massage\">Apri ODK Collect per riprovare</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"form_update_notification_title\">Sincronizzazione dei formulari</string>\n  <string name=\"load_remote_form_error\">Download elenco formulari non riuscito </string>\n  <string name=\"forms_download_succeeded\">Download Formulari riuscito</string>\n  <string name=\"forms_download_failed\">Download formulari non riuscito </string>\n  <string name=\"all_downloads_succeeded\">Tutti i download sono riusciti!</string>\n  <string name=\"some_downloads_failed\">%1$s di %2$s downloads falliti!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Versione: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Se continui ad avere questo problema, segnalalo alla persona che ti ha chiesto di raccogliere i dati.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect non può raggiungere il server %s. Hai inserito correttamente l\\'URL?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect non può connettersi in modo sicuro al server %s. Hai inserito correttamente l\\'URL?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">Il server %1$s ha restituito il codice di stato %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">Il server %s ha fornito una risposta non valida.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">Il server non ha fornito un hash per questo modulo.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_parsing_error\">Questo formulario non può essere elaborato:</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">Si è verificato un errore del disco sul dispositivo durante il download di questo modulo.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">Questo formulario ha un URL di invio non valido.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Moduli Salvati</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Moduli Vuoti</string>\n  <string name=\"delete_confirm\">Eliminare %s modulo(i)?</string>\n  <string name=\"delete_file\">Elimina Selezionato</string>\n  <string name=\"delete_no\">Non Eliminare</string>\n  <string name=\"delete_yes\">Elimina moduli</string>\n  <string name=\"form_delete_message\">Elimina i moduli selezionati</string>\n  <string name=\"file_deleted_ok\">%s modulo(i) cancellato(i) con successo!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Impostazioni del progetto</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Server</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, nome utente, password</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Visualizzazione del progetto</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Nome, icona, colore</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Interfaccia utente</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_interface_settings_subtext\">Lingua dell\\'app, dimensione del carattere, navigazione</string>\n  <!--Title of a settings section-->\n  <string name=\"maps\">Mappe</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Mappa di base, stili, livelli</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Gestione modulo</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">Aggiornamento automatico, invio automatico, eliminazione automatica</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Identità user e dispositivo</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">Nome utente, numero di telefono, ID dispositivo</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Sperimentale</string>\n  <string name=\"protected_settings\">Protetto</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">Sblocca le impostazioni protette</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">Cambia password, imposta i controlli di accesso</string>\n  <string name=\"set_admin_password\">Imposta password amministratore</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Gestione del progetto</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">Riconfigura, ripristina, elimina</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Controllo Accesso</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">Limita l\\'interfaccia utente</string>\n  <string name=\"server_preferences\">Impostazioni Server</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Server URL</string>\n  <string name=\"url_error\">Spiacenti, URL non valida!</string>\n  <string name=\"username\">Nome Utente</string>\n  <string name=\"change_username\">Nome utente ODK</string>\n  <string name=\"password\">Password</string>\n  <string name=\"change_password\">Password ODK</string>\n  <string name=\"password_error_whitespace\">Spazi vuoti all\\'inizio e alla fine delle password non sono ammessi</string>\n  <string name=\"username_error_whitespace\">Spazi vuoti all\\'inizio e alla fine delle username non sono ammessi</string>\n  <string name=\"app_theme\">Tema</string>\n  <string name=\"language\">Lingua</string>\n  <string name=\"use_device_language\">Utilizza lingua del dispositivo</string>\n  <string name=\"font_size\">Dimensione dei caratteri del testo</string>\n  <string name=\"change_font_size\">Dimensione dei caratteri del testo</string>\n  <string name=\"font_size_extra_large\">Molto Grande</string>\n  <string name=\"font_size_large\">Grande</string>\n  <string name=\"font_size_medium\">Media</string>\n  <string name=\"font_size_small\">Piccolo</string>\n  <string name=\"font_size_extra_small\">Molto Piccolo</string>\n  <string name=\"navigation\">Navigazione</string>\n  <string name=\"swipe_navigation\">Scorri il dito orizzontalmente</string>\n  <string name=\"buttons_navigation\">Usa pulsanti avanti/indietro</string>\n  <string name=\"swipe_buttons_navigation\">Scorri il dito e usa pulsanti</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Prossimo</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Indietro</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Aggiornamento Modulo</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Modalità di aggiornamento formulario vuoto </string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Manuale</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Solo formulari scaricati in precedenza</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Abbina esattamente il server</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Frequenza di aggiornamento automatico</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Ogni 15 minuti</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Ogni ora</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Ogni 6 ore</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Ogni 24 ore</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Download automatico</string>\n  <string name=\"automatic_download_summary\">Scarica automaticamente i questionari aggiornati</string>\n  <string name=\"hide_old_form_versions_setting_title\">Nascondi le vecchie versioni dei questionario</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Solo l\\'ultima versione apparirà in Compila Formulario Vuoto</string>\n  <string name=\"form_submission_category\">Sottomissione modulo</string>\n  <string name=\"autosend_selector_title\">Invio automatico</string>\n  <string name=\"autosend\">Invio automatico</string>\n  <string name=\"off\">Spento</string>\n  <string name=\"wifi_autosend\">Solo WiFi</string>\n  <string name=\"cellular_autosend\">Solo cellulare</string>\n  <string name=\"wifi_cellular_autosend\">WiFi o cellulare</string>\n  <string name=\"delete_after_send\">Elimina dopo invio</string>\n  <string name=\"delete_after_send_summary\">Elimina moduli finalizzati e media dopo l\\'\\'invio al server</string>\n  <string name=\"form_filling_category\">Riempimento modulo</string>\n  <string name=\"constraint_behavior_title\">Vincoli di elaborazione</string>\n  <string name=\"constraint_behavior\">Comportamento sui vincoli</string>\n  <string name=\"constraint_behavior_on_swipe\">Convalida scorrendo il dito in avanti</string>\n  <string name=\"constraint_behavior_on_finalize\">Ritarda Convalida fino a quando Completato</string>\n  <string name=\"high_resolution_title\">Video ad alta definizione</string>\n  <string name=\"high_resolution_summary\">Permetti la registrazione di video ad alta risoluzione</string>\n  <string name=\"image_size_title\">Dimensione immagine</string>\n  <string name=\"image_size_dialog_title\">Pixel massimi nel lato lungo dell\\'immagine</string>\n  <string name=\"image_size_large\">Grande (3072px)</string>\n  <string name=\"image_size_medium\">Media (2048px)</string>\n  <string name=\"image_size_small\">Piccola (1024px)</string>\n  <string name=\"image_size_very_small\">Molto piccola (640px)</string>\n  <string name=\"image_size_original\">Dimensione originale della fotocamera (predefinito)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Mostra i chiarimenti per le domande</string>\n  <string name=\"guidance_no\">No</string>\n  <string name=\"guidance_yes\">Sì - mostra sempre</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Sì - compatta</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">Usa un\\'app esterna per la registrazione audio</string>\n  <string name=\"form_import_category\">Importa modulo</string>\n  <string name=\"instance_sync\">Finalizza moduli per importazione</string>\n  <string name=\"instance_sync_summary\">Riguarda un modulo aggiunto direttamente alla cartella esempi</string>\n  <string name=\"analytics_preferences\">Dati sull\\'utilizzo</string>\n  <string name=\"analytics\">Raccolta di dati anonimi sull\\'utilizzo</string>\n  <string name=\"analytics_summary\">La raccolta di dati anonimi sull\\'utilizzo aiuta il team di ODK a dare priorità a certe funzioni e fix</string>\n  <string name=\"form_metadata_title\">Metadati del Modulo</string>\n  <string name=\"form_metadata\">Metadati del modulo</string>\n  <string name=\"form_metadata_summary\">Questi valori vengono precompilati nei moduli provvisti di nome utente, email e/o numero di telefono e servono ad identificare la persona che invia i dati.</string>\n  <string name=\"user_defined\">Definito dall\\'utente</string>\n  <string name=\"phone_number\">Numero di telefono</string>\n  <string name=\"email\">Indirizzo email</string>\n  <string name=\"invalid_email_address\">Indirizzo email invalido!</string>\n  <string name=\"device_defined\">Definito dal dispositivo</string>\n  <string name=\"device_id\">ID Dispositivo</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Non disponibile</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">Sblocca le impostazioni</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">Impostazioni sbloccate</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <string name=\"setting_not_available_during_form_entry\">Impostazione non disponibile durante l\\' immissione nel formulario</string>\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Inserire nuova password</string>\n  <string name=\"admin_password\">Password di amministratore</string>\n  <string name=\"admin_password_changed\">Password dell\\'Amministratore cambiata con successo</string>\n  <string name=\"admin_password_disabled\">Password dell\\'Amministratore disabilitata</string>\n  <string name=\"admin_password_incorrect\">Attenzione, la password è sbagliata!</string>\n  <string name=\"enter_admin_password\">Inserire Password Amministratore</string>\n  <string name=\"change_admin_password\">Cambia la password di amministratore</string>\n  <string name=\"show_password\">Mostra password</string>\n  <string name=\"found_in_main\">Deseleziona per nascondere dal Menu Principale</string>\n  <string name=\"found_in_settings\">Deseleziona per nascondere da Impostazioni non protette</string>\n  <string name=\"form_entry\">Deseleziona per nascondere dal Modulo d\\'Entrata</string>\n  <string name=\"form_end\">Deseleziona per nascondere dal Fine Modulo</string>\n  <string name=\"reset_project_settings_title\">Reimposta</string>\n  <string name=\"reset_project_settings_subtext\">Scegli tra impostazioni, moduli, dati</string>\n  <string name=\"delete_project\">Elimina</string>\n  <string name=\"select_all\">Seleziona tutto</string>\n  <string name=\"clear_all\">Deseleziona tutto</string>\n  <string name=\"reset_settings\">Tutte le impostazioni (impostazioni di base, impostazioni salvate)</string>\n  <string name=\"reset_saved_forms\">Moduli salvati (cartella instances, database instances)</string>\n  <string name=\"reset_blank_forms\">Moduli vuoti (cartella forms, database forms, database itemsets)</string>\n  <string name=\"reset_layers\">Livelli della mappa (cartella layers)</string>\n  <string name=\"reset_cache\">Cache di caricamento dei moduli (cartella .cache)</string>\n  <string name=\"reset_settings_dialog_title\">Seleziona le opzioni da reimpostare</string>\n  <string name=\"reset_settings_button_reset\">Reimposta</string>\n  <string name=\"reset_in_progress\">Reimpostazione…</string>\n  <string name=\"reset_app_state_result\">Reimposta risultati</string>\n  <string name=\"reset_app_state_warning\">I dati selezionati verranno cancellati definitivamente. L\\'operazione non può essere annullata.</string>\n  <string name=\"reset_settings_result\">Tutte le impostazioni :: %s</string>\n  <string name=\"reset_saved_forms_result\">Moduli salvati :: %s</string>\n  <string name=\"reset_blank_forms_result\">Form vuoti :: %s</string>\n  <string name=\"reset_layers_result\">Livelli della mappa :: %s</string>\n  <string name=\"reset_cache_result\">Cache di caricamento dei moduli :: %s</string>\n  <string name=\"main_menu_settings\">Parametri Menu Principale</string>\n  <string name=\"main_menu_settings_summary\">Mostra o nascondi pulsanti</string>\n  <string name=\"user_settings\">Impostazioni utente</string>\n  <string name=\"user_setting_summary\">Mostra o nascondi le impostazioni</string>\n  <string name=\"form_entry_setting\">Settaggi Modulo Ingresso</string>\n  <string name=\"form_entry_settings_summary\">Mostra o nascondi azioni</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Sfoglia indietro</string>\n  <string name=\"moving_backwards_disabled_title\">Sfoglia indietro disabilitato</string>\n  <string name=\"moving_backwards_disabled_message\">Configurare il dispositivo per impedire agli utenti di ignorare questa impostazione?\\n\\nLe modifiche sono:\\n\\u2022 Disabilita salva bozza\\n\\u2022 Disabilita prompt Vai a\\n\\u2022 Imposta elaborazione vincolo su Convalida dopo lo scorrimento in avanti</string>\n  <string name=\"yes\">Sì</string>\n  <string name=\"no\">No</string>\n  <string name=\"moving_backwards_enabled_title\">Sfoglia indietro abilitato</string>\n  <string name=\"moving_backwards_enabled_message\">Potresti voler rivedere le seguenti impostazioni:\\n\\n\\u2022 Modifica bozza\\n\\u2022 Salva bozza\\n\\u2022 Vai al prompt\\n\\u2022 Elaborazione dei vincoli</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Salva come bozza</string>\n  <string name=\"save_mid_summary\">Icona Salva nella barra superiore e pulsante \\\"Salva come bozza\\\" all\\'uscita dal modulo</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Informazioni</string>\n  <string name=\"tell_your_friends\">Condividi ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">I tuoi colleghi stanno ancora raccogliendo dati con carta e penna? Condividi ODK Collect con loro!</string>\n  <string name=\"leave_a_review\">Lascia un commento su Play Store</string>\n  <string name=\"leave_a_review_msg\">Il tuo commento (speriamo positivo) aumenta la visibilità dell\\'applicazione su Play Store</string>\n  <string name=\"odk_website\">Visitare il sito web ODK</string>\n  <string name=\"odk_website_summary\">ODK viene utilizzato per raccogliere dati per il bene sociale in ambienti difficili.</string>\n  <string name=\"odk_forum\">Collegati al forum di ODK</string>\n  <string name=\"odk_forum_summary\">Entra nel forum per avere supporto, richiedere caratteristiche, contribuire a codificare/tradurre!</string>\n  <string name=\"all_open_source_licenses\">Librerie/Licenze open source</string>\n  <string name=\"all_open_source_licenses_msg\">Stiamo sulle spalle dei giganti!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">Riconfigurare attraverso codice QR</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">Sostituisci tutte le impostazioni esistenti</string>\n  <string name=\"qr_code\">Configurazione con codice QR</string>\n  <string name=\"scan_qr_code_fragment_title\">Scansionare</string>\n  <string name=\"view_qr_code_fragment_title\">Codice QR</string>\n  <string name=\"import_qrcode_sd\">Importare codice QR</string>\n  <string name=\"successfully_imported_settings\">Impostazioni importate con successo</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Numero massimo di caratteri raggiunto: codice QR non può essere generato per tutte le impostazioni</string>\n  <string name=\"share\">Condividi</string>\n  <string name=\"turn_on_flashlight\">Accendere il flash</string>\n  <string name=\"turn_off_flashlight\">Spegnere il flash</string>\n  <string name=\"qrcode_with_both_passwords\">Contiene informazioni sensibili: <b>admin</b> and <b>server passwords</b></string>\n  <string name=\"qrcode_without_passwords\">Il codice QR non contiene <b>admin</b> or <b>server</b> passwords. Toccare per configurare</string>\n  <string name=\"qrcode_with_admin_password\">Contiene informazioni sensibili: <b>admin</b> password</string>\n  <string name=\"qrcode_with_server_password\">Contiene informazioni sensibili: <b>server</b> password</string>\n  <string name=\"server_password\">Password del Server</string>\n  <string name=\"include_password_dialog\">Password inclusa nel Codice</string>\n  <string name=\"generate\">Genera</string>\n  <string name=\"invalid_qrcode\">Il codice QR non contiene parametri validi</string>\n  <string name=\"qr_code_not_found\">Il codice QR non è stato rilevato nell\\'immagine selezionata</string>\n  <string name=\"corrupt_imported_preferences_error\">Le impostazioni attuali sono danneggiate. Dalle impostazioni di gestione del progetto, ripristinare le impostazioni o importare quelle funzionanti.</string>\n  <string name=\"settings_with_gd_protocol\">I progetti Google Drive/Fogli non possono più essere creati</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Annulla</string>\n  <string name=\"cancel_loading_form\">Annulla</string>\n  <string name=\"cancel_location\">Annulla</string>\n  <string name=\"do_not_change\">Annulla</string>\n  <string name=\"canceling\">Annullando</string>\n  <string name=\"notification_channel_name\">Impostazioni Generali</string>\n  <string name=\"success\">Successo</string>\n  <string name=\"error_occured\">Si è verificato un errore.</string>\n  <string name=\"please_wait\">Si prega di attendere qualche istante.</string>\n  <string name=\"please_wait_long\">Si prega di attendere. Questo potrebbe richiedere alcuni minuti.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Non puoi cancellare \\'%s\\'. Perpiacere cancella il file manualmente e scarica nuovamente il Modulo.</string>\n  <string name=\"fs_create_media_folder_error\">Non puoi creare la cartella media \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Non puoi copiare \\'%1$s\\' in \\'%2$s\\'. Motivo: %3$s</string>\n  <string name=\"select_odk_shortcut\">Seleziona scorciatoia ODK</string>\n  <string name=\"background_audio_help\">Questo formulario registra l\\'audio dal microfono del tuo dispositivo.\\n\\nPuoi utilizzare l\\'indicatore del volume per assicurarti che il microfono sia abbastanza vicino al suono che devi registrare.\\n\\nPer interrompere la registrazione esci da questo formulario.\\n\\ nPer maggiori informazioni contatta la persona che ti ha chiesto di raccogliere i dati.</string>\n  <string name=\"loading\">Caricando…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Progetti</string>\n  <string name=\"settings\">Impostazioni</string>\n  <string name=\"add_project\">Aggiungere progetto</string>\n  <string name=\"add\">Aggiungere</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Duplicare progetto</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">Hai già un progetto con queste impostazioni di connessione. Vuoi passare al progetto esistente o aggiungerne uno nuovo?</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Aggiungi progetto duplicato</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Passa a progetto esistente</string>\n  <string name=\"project_name\">Nome Progetto</string>\n  <string name=\"project_icon\">Icona del progetto</string>\n  <string name=\"project_color\">Colore Progetto</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Cambiato a %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">Usando %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Cambia a %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <string name=\"delete_project_dialog_title\">Elimina %s</string>\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <string name=\"delete_project_message\">Eliminando questo progetto, eliminerai definitivamente quanto segue</string>\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <string name=\"delete_project_instructions\">Digita %1$sper confermare questa azione.</string>\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <string name=\"delete_trigger\">elimina</string>\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <string name=\"form_definitions_count\">Definizioni dei formulari: %1$d</string>\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <string name=\"sent_count\">Inviati: %1$d</string>\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <string name=\"unsent_count\">Non inviati: %1$d</string>\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <string name=\"drafts_count\">Bozze: %1$d</string>\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"delete_project_confirm_button_text\">Eliminare il Progetto</string>\n  <string name=\"or\">O</string>\n  <string name=\"hex_color\">Colore esadecimale</string>\n  <string name=\"invalid_hex_code\">Invalido colore esadecimale</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Raccogli dati\\novunque</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Configurare attraverso codice QR</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Inserisci manualmente i dettagli del progetto</string>\n  <string name=\"project_config_tip\">Dopo aver aggiunto il progetto, puoi configurarlo in Impostazioni</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Non hai ancora un Progetto?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Prova una demo</string>\n  <string name=\"scan_configuration_qr_code\">Scansiona codice QR</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Mostra dettagli</string>\n  <string name=\"errors\">Errori</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <string name=\"no_browser_installed\">Nessun Browser installato!</string>\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">Per aprire questo modulo, devi prima aprire ODK Collect e passare al progetto che lo contiene.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">ODK Collect non è stato configurato. Prova ad aprire ODK Collect e a configurarlo.\\n\\nSe hai toccato un collegamento, potrebbe essere necessario ricrearlo dopo aver configurato ODK Collect.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">URI non riconosciuto : %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">URI mal strutturato : %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Il modulo compilato è stato cancellato!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">Impossibile modificare questa bozza perché il corrispondente modulo vuoto non esiste o è stato cancellato.\\n\\nID Modulo: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Hai scaricato due diversi Moduli ma con lo stesso nome ID e versione. Potrebbe essere lo stesso Modulo scaricato in momenti diversi o con Server diversi. In ogni caso devi cancellarne uno.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <string name=\"cannot_open_form_because_of_forms_update\">Questo formulario non può essere aperto in questo momento perché i formulari sono in fase di aggiornamento. Si prega di riprovare a breve.</string>\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Apri impostazioni</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <string name=\"entities_title\">Lista Entità</string>\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <string name=\"entity_browser_button\">Visualizzare le liste di entità</string>\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">Non in linea</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">Questo formulario non è supportato da questa versione di ODK Collect. Aggiornare Collect. Se continuate a riscontrare questo problema, segnalatelo alla persona che vi ha chiesto di raccogliere i dati.\\n\\nForm Entities spec version: %s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">Si è verificato un arresto anomalo l\\'ultima volta che l\\'app era in esecuzione!</string>\n  <string name=\"cant_start_app\">Impossibile avviare l\\'app!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">Questo progetto era precedentemente collegato a un account Google Drive. Il supporto di Google Drive è stato rimosso. È possibile configurare un server o prelevare gli invii da un computer.</string>\n  <string name=\"learn_more_button_text\">Saperne di più</string>\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">Strumenti di sviluppo</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">Arresto anomalo dell\\'app</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Forza un\\'eccezione non rilevata che causa l\\'arresto anomalo dell\\'app</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <string name=\"debug_filters\">filtri Debug </string>\n  <string name=\"debug_filters_summary\">Mostra informazioni di debug per le esecuzioni delle espressioni di filtro durante l\\'inserimento dei dati nel formulario</string>\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"use_zxing_for_barcode_scanning\">Torna al vecchio lettore di codici a barre</string>\n  <string name=\"permission_dialog_title\">In merito ai permessi</string>\n  <string name=\"permission_dialog_text\">Ti verrà chiesto di consentire a ODK Collect di accedere alle funzionalità seguenti, seleziona \\\"consenti\\\" se desideri utilizzarle.</string>\n  <string name=\"notifications\">Notifiche</string>\n  <string name=\"required_for_notifications\">Richiesto per mostrare gli aggiornamenti quando i moduli vengono scaricati, aggiornati e inviati.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">Il tuo modulo è stato salvato come bozza.</string>\n  <string name=\"form_saved\">Il tuo modulo è stato salvato</string>\n  <string name=\"form_sending\">Inviando il Modulo…</string>\n  <string name=\"edit_form\">Modifica</string>\n  <string name=\"view_form\">Guarda</string>\n  <string name=\"close_snackbar\">Chiudere la notifica</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">Ultimo formulario inviato: %dsecondi fa</item>\n    <item quantity=\"many\">Ultimi formulari inviati:%dsecondi fa</item>\n    <item quantity=\"other\">Ultimo formulario inviato: %d secondi fa</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"one\">Ultimo formulario inviato: %dminuto fa</item>\n    <item quantity=\"many\">Ultimi formulari inviati:%dminuti fa</item>\n    <item quantity=\"other\">Ultimo formulario inviato: %d minuti fa</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"one\">Ultimo formulario inviato:%d ora fa</item>\n    <item quantity=\"many\">Ultimi formulari inviati: %dore fa</item>\n    <item quantity=\"other\">Ultimo formulario inviato: %d ore fa</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"one\">Ultimo formulario inviato:%d giorno fa</item>\n    <item quantity=\"many\">Ultimi formulari inviati: %dgiorni fa</item>\n    <item quantity=\"other\">Ultimo formulario inviato: %d giorni fa</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"one\">%dformulario pronto per l\\'invio</item>\n    <item quantity=\"many\">%dformulari pronti per l\\'invio</item>\n    <item quantity=\"other\">%dformulari pronti per l\\'invio</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">Nelle versioni successive, i formulari finalizzati non saranno più modificabili. Salva i formulari come bozza per modificarli in seguito.\\n\\nPuoi verificare la presenza di errori in una bozza di formulario toccando i tre punti (⋮) e quindi Controlla errori.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">Nelle versioni successive, i formulari finalizzati non saranno più modificabili.</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">Finalizza tutte le bozze</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"one\">Vuoi finalizzare %d bozza?</item>\n    <item quantity=\"many\">Vuoi finalizzare%dbozze?</item>\n    <item quantity=\"other\">Vuoi finalizzare %d bozze?</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">Una volta finalizzate, tutte le bozze saranno in \\\"Pronte per l\\'invio\\\" e non potrai apportare modifiche. Eventuali bozze contenenti errori non verranno finalizzate.\\n\\nNon potrai annullare questa azione.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"one\">Successo! %d bozza finalizzate.</item>\n    <item quantity=\"many\">Successo! %d bozze finalizzate.</item>\n    <item quantity=\"other\">Successo! %dbozza finalizzata.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"one\">%dbozza contiene errori che devono essere risolti prima di essere finalizzata.</item>\n    <item quantity=\"many\">%d bozze contengono errori che devono essere risolti prima di essere finalizzati.</item>\n    <item quantity=\"other\">%d bozze contengono errori che devono essere risolti prima di essere finalizzati.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d bozze finalizzate. %d le bozze contengono errori che devono essere risolti prima di essere finalizzati.</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%dbozze finalizzate. Le bozze rimanenti devono essere finalizzate manualmente.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Errori</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">Nessun errore</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">Deseleziona per nasconderlo dalle bozze</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">Niente disponibile da visualizzare</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">Nessun formulario vuoto</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">Scarica il formulario per iniziare  </string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">Niente nelle bozze</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">Quando salvi come bozza, i formulari verranno visualizzati qui</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">Niente di pronto per l\\'invio</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">Una volta finalizzate le bozze, queste verranno visualizzate qui</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">Non è stato inviato nulla</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">Quando invii formulari finalizzati, verranno visualizzati qui</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">Nessun formulario da scaricare</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">Scarica il formulario per iniziare</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">Nessun formulario da eliminare</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">Una volta scaricati i formulari vuoti, essi appariranno qui</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">Una volta salvati i formulari, questi appariranno qui</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">Recuperare il tuo lavoro?</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"savepoint_recovery_dialog_message\">\\'Collect si è chiuso inaspettatamente il \\'EEE, MMM dd, yyyy \\'alle\\' HH:mm\\' ed ha salvato il tuo lavoro.\\n\\nVolete recuperarlo o scartarlo?\\'</string>\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">Recupera</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Annulla</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">Ottieni il punto</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">Vedi o Cambia il punto</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">Guarda il punto</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">Cambiare Localizzazione</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">Ottenere la linea</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">Vedi o Cambia la linea</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">Visualizza la linea</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">Ottieni il poligono</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">Vedi o Cambia il poligono</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">Visualizza poligono</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <string name=\"settings_json\">Impostazioni JSON</string>\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <string name=\"preparing_form_edit\">Preparazione del formulario per la modifica…</string>\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <string name=\"user_visible_instance_name\">%s (Modifica%d)</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_title\">Continua a modificare la bozza?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_draft_edit_found_dialog_message\">\\'Questa modifica della bozza è stato salvata l\\'\\'ultima volta il\\' EEE, MMM dd, yyyy \\'alle\\' HH:mm\\'. Vuoi ripristinare questa modifica?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_positive_button\">Ricomincia a modificare</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_title\">Modifica ultima versione?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_finalized_edit_found_dialog_message\">\\'Questo invio è stato precedentemente modificato.\\n\\nDesideri modificare l\\'\\'ultima versione finalizzata il\\' EEE, MMM dd, yyyy \\'at\\' HH:mm\\'?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_positive_button\">Modifica ultimo Modulo</string>\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n  <string name=\"finalized_form_edit_started\">Stai modificando questo formulario. Tocca una domanda per iniziare a modificarla.</string>\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ja/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">バージョン: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">yyyy/MM/dd HH:mm \\'に追加しました\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">yyyy/MM/dd HH:mm \\'に保存しました\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">yyyy/MM/dd HH:mm \\'に確定しました\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">yyyy/MM/dd HH:mm \\'に送信しました\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">yyyy/MM/dd HH:mm \\'に送信失敗しました\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">yyyy/MM/dd HH:mm \\'に削除しました\\'</string>\n  <string name=\"submission_deleted\">送信を削除しました</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">yyyy/MM/dd HH:mm \\'に変更しました\\'</string>\n  <string name=\"encrypted_form\">暗号化フォーム</string>\n  <string name=\"deleted_form\">削除済フォーム</string>\n  <string name=\"sort_the_list\">一覧の並び替え</string>\n  <string name=\"sort_by\">並び順</string>\n  <string name=\"sort_by_name_asc\">名前順、A-Z</string>\n  <string name=\"sort_by_name_desc\">名前順、Z-A</string>\n  <string name=\"sort_by_date_desc\">日付順、新しいものが最初</string>\n  <string name=\"sort_by_date_asc\">日付順、古いものが最初</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">一覧をフィルター</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">検索</string>\n  <string name=\"not_granted_permission\">開始しようとしているアクティビティは、必要なアクセス許可が付与されていません。</string>\n  <string name=\"storage_runtime_permission_denied_title\">ストレージのアクセス許可</string>\n  <string name=\"storage_runtime_permission_denied_desc\">これらのアクセス許可がないと、Collect はフォームにアクセスしたり、回答を保存することができません。 アクセス許可を付与する準備ができたら、アプリをもう一度開いてください。</string>\n  <string name=\"location_runtime_permissions_denied_title\">位置情報アクセス許可</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">カメラアクセス許可</string>\n  <string name=\"camera_runtime_permission_denied_desc\">このアクセス許可がないと、Collect はカメラにアクセスできません。 付与する準備ができたら、もう一度お試しください。</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">オーディオ録音アクセス許可</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">このアクセス許可がないと、Collect はマイクにアクセスできません。 付与する準備ができたら、もう一度お試しください。</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$d フォームを追加しました。</string>\n  <string name=\"finished_disk_scan\">スキャンが終了しました。すべてのフォームがロードされました。</string>\n  <string name=\"xform_parse_error\">%1$s の XForm 解析エラー: %2$s が不足しているか正しくありません。 </string>\n  <string name=\"loading_form\">フォームのロード中</string>\n  <string name=\"survey_loading_reading_form_message\">フォーム定義を読み込み中…</string>\n  <string name=\"survey_loading_reading_data_message\">調査データを読み込み中…</string>\n  <string name=\"survey_loading_reading_csv_message\">CSV ファイルを読み込み中…</string>\n  <string name=\"parse_error\">申し訳ありません、フォームを解析できません。</string>\n  <string name=\"loading_form_failed\">フォームの読み込み時にエラーが発生しました。もう一度やり直してください。</string>\n  <string name=\"cannot_edit_completed_form\">確定済になっているため、このフォームは編集できません。暗号化されているかもしれません。</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">未保存の変更をセーブポイントから回復しました!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">言語を変更</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">プロンプトへ移動</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">最初に移動</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">最後に移動</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">上に移動</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">グループ</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">繰り返し可能グループ</string>\n  <string name=\"exit\">終了</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">\\\"%s\\\" を追加?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">追加</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">別の追加…</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">プロンプトを編集</string>\n  <string name=\"clearanswer_confirm\">\\\"%s\\\" への回答を削除しますか?</string>\n  <string name=\"clear_answer\">回答を削除</string>\n  <string name=\"clear_answer_ask\">回答を削除しますか?</string>\n  <string name=\"clear_answer_no\">キャンセル</string>\n  <string name=\"discard_answer\">回答を削除</string>\n  <string name=\"discard_group\">グループを削除</string>\n  <string name=\"delete_repeat\">グループを削除</string>\n  <string name=\"delete_repeat_ask\">このグループを削除しますか?</string>\n  <string name=\"delete_repeat_confirm\">グループ \\\"%s\\\" およびすべてのサブグループを削除しますか?</string>\n  <string name=\"delete_repeat_no\">キャンセル</string>\n  <string name=\"invalid_answer_error\">申し訳ありません、回答が正しくありません!</string>\n  <string name=\"required_answer_error\">申し訳ありません、この回答は必須です!</string>\n  <string name=\"invalid_space_in_answer_singular\">警告: 基本値 %s に空白があります</string>\n  <string name=\"invalid_space_in_answer_plural\">警告: 基本値 %s に空白があります</string>\n  <string name=\"too_complex_form\">この形式はこの端末には複雑すぎます。 表現を単純化するか、フォーラムで助けを求めてください。</string>\n  <string name=\"survey_internal_error\">内部エラー: プロンプトするステップに失敗しました。</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">%s の最後です。</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">申し訳ありません、フォームが保存できませんでした!</string>\n  <string name=\"data_saved_ok\">フォームを保存しました!</string>\n  <string name=\"save_point_error\">リカバリーポイントの保存中にエラー: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">変更を保存する</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">フォームを保存中</string>\n  <string name=\"survey_saving_validating_message\">回答を検証中…</string>\n  <string name=\"survey_saving_collecting_message\">データ収集中…</string>\n  <string name=\"survey_saving_saving_message\">SD カードに保存中…</string>\n  <string name=\"survey_saving_finalizing_message\">SD カードに確定中…</string>\n  <string name=\"survey_saving_encrypting_message\">データを暗号化中…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">このインスタンスで正確なレコードではありません!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">ブランクフォームはこの jr_form_id に一致しません。</string>\n  <string name=\"no_form_id_specified\">フォームId が指定されていない???</string>\n  <string name=\"phone_does_not_support_rsa\">電話は RSA 暗号化をサポートしていません。</string>\n  <string name=\"invalid_rsa_public_key\">RSA 公開鍵が無効です。</string>\n  <string name=\"encryption_error_message\">%s フォームはファイナライズ済みとして保存されていません。</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">録音を開始できません。</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">マイクはすでに使われています。</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">メディアはフォームに添付できませんでしたが %1$s にあります</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">録音が無効になりました。 %s で有効にしてください</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">起動</string>\n  <string name=\"activity_not_found\">取り扱うアクティビティがありません: %s</string>\n  <string name=\"no_app\">要求したアプリケーションがありません。手動で読みを入力してください。</string>\n  <string name=\"null_intent_value\">外部アプリケーションが、期待した情報を提供しませんでした。</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">URL を開く</string>\n  <string name=\"capture_osm\">OpenMapKit を起動</string>\n  <string name=\"recapture_osm\">OSM タグ付けを繰り返す</string>\n  <string name=\"edited_osm_file\">編集した OSM XML ファイル:</string>\n  <string name=\"invalid_osm_data\">何か問題があります。 有効な OSM データを取得できませんでした。</string>\n  <string name=\"alert\">警告</string>\n  <string name=\"install_openmapkit\">OpenMapKit をインストールしてください!</string>\n  <string name=\"get_barcode\">バーコードを取得</string>\n  <string name=\"replace_barcode\">バーコードを置き換え</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">四角の中にバーコードを入れてください</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">ファイルを開く</string>\n  <string name=\"rank_items\">ランキングアイテム</string>\n  <string name=\"number_picker_title\">数値選択</string>\n  <string name=\"invalid_range_widget\">このウィジェットに無効なパラメータがあります!</string>\n  <string name=\"svg_file_does_not_exist\">SVG ファイルが存在しません!</string>\n  <string name=\"capture_image\">写真を撮影</string>\n  <string name=\"choose_image\">画像を選択</string>\n  <string name=\"selected_invalid_image\">選択したファイルは有効な画像ではありません</string>\n  <string name=\"take_picture_instruction\">画面をタップすると写真を撮ります</string>\n  <string name=\"error_front_camera_unavailable\">このデバイスは前面カメラが利用できません</string>\n  <string name=\"annotate_image\">画像に注釈をつける</string>\n  <string name=\"invalid_file_type\">アプリケーションが無効なファイル形式を返しました</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">保存して閉じる</string>\n  <string name=\"signature_capture\">署名キャプチャ</string>\n  <string name=\"sign_button\">署名を収集</string>\n  <string name=\"markup_image\">画像をマークアップ</string>\n  <string name=\"draw_image\">スケッチ画像</string>\n  <string name=\"reset_image\">リセット</string>\n  <string name=\"set_color\">色を設定</string>\n  <string name=\"capture_video\">ビデオを録画</string>\n  <string name=\"choose_video\">ビデオを選択</string>\n  <string name=\"play_video\">ビデオを再生</string>\n  <string name=\"choose_file\">ファイルを選択</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">削除</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">サウンドを録音</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">サウンドを選択</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">一時停止</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">再開</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"background_audio_permission_explanation\">このフォームはバックグラウンドでオーディオを録音します。 マイクの使用を許可する必要があります。 そうしないと、このフォームを開くことができません。</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">このフォームはバックグラウンドオーディオ録音を要求します。 無効にすると、録音が停止し、既存のオーディオが破棄されます。 続行してもよろしいですか?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">録音を無効にする</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">録音はすぐには開始されません。 録音するには、フォームを再度開く必要があります。</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">ファイルを削除しますか?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">削除すると、このファイルを復元することはできません。 続行してもよろしいですか?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">停止</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">録音中…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">オーディオ録音</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">オーディオを再生</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">ファイルの保存中</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">この画面を終了する前に、録音を停止する必要があります。</string>\n  <string name=\"error_attaching_binary_file\">メディアの添付中にエラーが発生しました: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">値を選択</string>\n  <string name=\"select_answer\">回答を選択してください</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">値を編集</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">値が選択されていません</string>\n  <string name=\"selected_title\">選択済: </string>\n  <string name=\"trigger\">OK、続けます。</string>\n  <string name=\"ext_file_no_data_error\">ファイルにデータが含まれていません!</string>\n  <string name=\"ext_conflicting_columns_error\">列 %s が一致しました!</string>\n  <string name=\"ext_sortBy_numeric_error\">SORTBY 列は、数値しか含めることができません。矛盾する値は \\'%s\\'。</string>\n  <string name=\"ext_search_wrong_arguments_error\">search() 関数で構文エラー: 関数の引数は1、4、または6個が必要です。</string>\n  <string name=\"ext_search_wrong_function_error\">search() 関数で構文エラー: 認識できない関数 \\'%s\\'。</string>\n  <string name=\"ext_search_bad_function_error\">search() 関数で構文エラー: \\'%s\\' は関数として評価されませんでした。</string>\n  <string name=\"ext_search_return_error\">検索ハンドラーはタイプ \\'%s\\' のオブジェクトを返しました。</string>\n  <string name=\"ext_import_generic_error\">%1$s からデータをインポートできませんでした。理由: %2$s</string>\n  <string name=\"ext_import_progress_message\">\\'%1$s\\' からデータをプリロード中。しばらくお待ちください… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">データの読み込みがキャンセルされました!</string>\n  <string name=\"ext_import_finalizing_message\">プリロードされたデータの確定中…</string>\n  <string name=\"ext_import_completed_message\">データの読み込みが完了しました!</string>\n  <string name=\"ext_not_initialized_error\">外部データマネージャーが初期化されていません。</string>\n  <string name=\"ext_import_csv_missing_error\">%1$s の外部データがインポートされていません。フォームに %2$s.csv ファイルを含めるのを忘れていませんか?</string>\n  <string name=\"ext_search_generic_error\">search() 関数で構文エラー: %s</string>\n  <string name=\"ext_assign_value_error\">\\'%s\\' の値を割り当てできません。</string>\n  <string name=\"parser_exception\">XPathParser 例外: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">方位を記録</string>\n  <string name=\"replace_bearing\">方位を置き換え</string>\n  <string name=\"getting_bearing\">方位をロード中</string>\n  <string name=\"accept_bearing\">方位を記録</string>\n  <string name=\"bearing\">方位: %.3f</string>\n  <string name=\"direction\">方角: %s</string>\n  <string name=\"bearing_lack_of_sensors\">挙動を収集できません: デバイスに加速度センサー、磁場センサー、またはその両方がありません。</string>\n  <string name=\"select_date\">日付を選択</string>\n  <string name=\"select_time\">時間を選択</string>\n  <string name=\"no_date_selected\">日付が選択されていません</string>\n  <string name=\"no_time_selected\">時刻が選択されていません</string>\n  <string name=\"ethiopian_month_1\">マスカラム</string>\n  <string name=\"ethiopian_month_2\">テクムト</string>\n  <string name=\"ethiopian_month_3\">フダル</string>\n  <string name=\"ethiopian_month_4\">タフサス</string>\n  <string name=\"ethiopian_month_5\">トルウ</string>\n  <string name=\"ethiopian_month_6\">ヤカテイト</string>\n  <string name=\"ethiopian_month_7\">マガビット</string>\n  <string name=\"ethiopian_month_8\">ミヤズヤ</string>\n  <string name=\"ethiopian_month_9\">グンボット</string>\n  <string name=\"ethiopian_month_10\">サネ</string>\n  <string name=\"ethiopian_month_11\">ハムレ</string>\n  <string name=\"ethiopian_month_12\">ナハセ</string>\n  <string name=\"ethiopian_month_13\">パグメ</string>\n  <string name=\"coptic_month_1\">トウト</string>\n  <string name=\"coptic_month_2\">パオペ</string>\n  <string name=\"coptic_month_3\">ハトール</string>\n  <string name=\"coptic_month_4\">コイアック</string>\n  <string name=\"coptic_month_5\">トーベ</string>\n  <string name=\"coptic_month_6\">メシル</string>\n  <string name=\"coptic_month_7\">パレムハート</string>\n  <string name=\"coptic_month_8\">パレムウデ</string>\n  <string name=\"coptic_month_9\">パションス</string>\n  <string name=\"coptic_month_10\">パオーネ</string>\n  <string name=\"coptic_month_11\">エペープ</string>\n  <string name=\"coptic_month_12\">メソレ</string>\n  <string name=\"coptic_month_13\">エパゴメネ</string>\n  <string name=\"islamic_month_1\">ムハッラム</string>\n  <string name=\"islamic_month_2\">サファル</string>\n  <string name=\"islamic_month_3\">ラビー・ウル・アッワル</string>\n  <string name=\"islamic_month_4\">ラビー・ウッサーニ</string>\n  <string name=\"islamic_month_5\">ジュマダル・ウッラー</string>\n  <string name=\"islamic_month_6\">ジュマダッサーニ</string>\n  <string name=\"islamic_month_7\">ラジャブ</string>\n  <string name=\"islamic_month_8\">シャバーン</string>\n  <string name=\"islamic_month_9\">ラマダーン</string>\n  <string name=\"islamic_month_10\">シャッワール</string>\n  <string name=\"islamic_month_11\">ズルカーダ</string>\n  <string name=\"islamic_month_12\">ズルヒッジャ</string>\n  <string name=\"persian_month_1\">ファルヴァルディーン</string>\n  <string name=\"persian_month_2\">オルディーベヘシュト</string>\n  <string name=\"persian_month_3\">ホルダード</string>\n  <string name=\"persian_month_4\">トルウ</string>\n  <string name=\"persian_month_5\">モルダード</string>\n  <string name=\"persian_month_6\">シャハリーヴァル</string>\n  <string name=\"persian_month_7\">メフル</string>\n  <string name=\"persian_month_8\">アーバーン</string>\n  <string name=\"persian_month_9\">アーザル</string>\n  <string name=\"persian_month_10\">デイ</string>\n  <string name=\"persian_month_11\">バフマン</string>\n  <string name=\"persian_month_12\">エスファンド</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">ファイル: %s が正しくありません。</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">ファイル: %s がありません。</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">IDの入力</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">ID</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">このフォームはあなた自身のIDが必要です</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">閉じる</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">変更の理由</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">理由</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">このフォームを変更する理由を説明する必要があります</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">保存</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_usgs_sat\">USGS ナショナルマップ ハイブリッド</string>\n  <string name=\"openmap_usgs_img\">USGS ナショナルマップ画像</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB ポジトロン</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB ダークマター</string>\n  <string name=\"basemap\">基本地図</string>\n  <string name=\"basemap_source\">ソース</string>\n  <string name=\"map_style_label\">%s 地図スタイル</string>\n  <string name=\"streets\">道路</string>\n  <string name=\"terrain\">地形</string>\n  <string name=\"hybrid\">ハイブリッド</string>\n  <string name=\"satellite\">衛星</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">ライト</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">ダーク</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">アウトドア</string>\n  <string name=\"topographic\">地形</string>\n  <string name=\"google_play_services_error_occured\">Google マップにアクセスできません。 Google Play サービスはインストールされていますか?</string>\n  <string name=\"carto_map_style_positron\">位置</string>\n  <string name=\"carto_map_style_dark_matter\">ダークマター</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">位置を記録</string>\n  <string name=\"provider_disabled_error\">申し訳ありません、ロケーション プロバイダーが無効です!</string>\n  <string name=\"google_play_services_not_available\">このフォームは位置を追跡しようとしますが、Google Play サービスが利用できません。</string>\n  <string name=\"location_providers_disabled_dialog_message\">このフォームは場所を追跡しようとしますが、位置情報プロバイダーが無効になっています。 Android の設定で有効にしてください。</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">設定に移動</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"background_location_disabled\">このフォームは位置を追跡しようとしますが、追跡が無効になっています。 上の %1$s メニューで有効にしてください。</string>\n  <string name=\"background_location_enabled\">このフォームは位置を追跡します。 上の %1$s メニューで追跡を無効にすることができます。</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">W %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">入力方法</string>\n  <string name=\"start\">開始</string>\n  <string name=\"enable_gps\">GPS を有効にする</string>\n  <string name=\"gps_enable_message\">お使いのデバイスで GPS は無効になっています。有効にしますか?</string>\n  <string name=\"geo_clear_warning\">機能は既に作成済です。機能をクリアしますか?</string>\n  <string name=\"geo_exit_warning\">変更を破棄して ODK の戻りますか?</string>\n  <string name=\"polygon_validator\">ポリゴンを作成するには、少なくとも 3 点指定する必要があります</string>\n  <string name=\"polyline_validator\">ポリラインを作成するには、少なくとも2点指定する必要があります</string>\n  <string name=\"clear\">クリア</string>\n  <string name=\"placement_mode\">タップによる配置</string>\n  <string name=\"manual_mode\">手動位置記録</string>\n  <string name=\"automatic_mode\">自動位置記録</string>\n  <string name=\"recording_interval\">記録間隔:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"other\">%d 秒</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"other\">%d 分</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">精度要件:</string>\n  <string name=\"none\">なし</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"other\">%d メートル</item>\n  </plurals>\n  <string name=\"collection_status_paused\">入力された位置: %d</string>\n  <string name=\"collection_status_placement\">入力された位置: %d (タップして位置を配置)</string>\n  <string name=\"collection_status_manual\">入力された位置: %d (手動記録)</string>\n  <string name=\"collection_status_auto_seconds\">入力された位置: %1$d (%2$d 秒ごとに記録)</string>\n  <string name=\"collection_status_auto_minutes\">入力された位置: %1$d (%2$d 分ごとに記録)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">入力された位置: %1$d (%2$d 秒ごとに記録。精度 %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">入力された位置: %1$d (%2$d 分ごとに記録。精度 %3$d m)</string>\n  <string name=\"geopoint_no_draggable_instruction\">マーカーを追加ボタンをタップします。</string>\n  <string name=\"discard\">破棄</string>\n  <!--Name of an action button-->\n  <string name=\"view_data\">保存済のフォームを表示</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">一時停止</string>\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">フォームを送信中</string>\n  <string name=\"noselect_error\">申し訳ありません、フォームが選択されていません!</string>\n  <string name=\"no_connection\">利用可能なネットワークがありません</string>\n  <string name=\"sending_items\">%1$s / %2$s フォームを送信中</string>\n  <string name=\"send_selected_data\">選択済を送信</string>\n  <string name=\"server_auth_credentials\">サーバーのユーザー名とパスワードが正しくありません: %s</string>\n  <string name=\"server_requires_auth\">サーバーは認証が必要です</string>\n  <string name=\"no_forms_uploaded\">申し訳ありません、アップロードしたフォームはありません。</string>\n  <string name=\"upload_results\">アップロード結果</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">ビューを変更</string>\n  <string name=\"show_sent_and_unsent_forms\">送信済および未送信のフォームを表示</string>\n  <string name=\"show_unsent_forms\">未送信のフォームを表示</string>\n  <string name=\"send_in_progress\">バックグラウンド送信が実行中です。しばらくして再度お試しください</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">更新</string>\n  <string name=\"download\">選択を取得</string>\n  <string name=\"downloading_data\">サーバーに接続</string>\n  <string name=\"fetching_file\">%1$s 取得中。\\n\\n%2$s / %3$s フォーム。</string>\n  <string name=\"form_download_progress\">%1$s。 メディア ファイルの取得中: %2$s / %3$s</string>\n  <string name=\"version\">バージョン:</string>\n  <string name=\"form_updates_available\">フォームの更新が利用可能です</string>\n  <string name=\"newer_version_of_a_form_info\">これはお持ちのフォームの更新です</string>\n  <string name=\"form_update_error\">フォームを更新できませんでした</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">フォーム一覧がダウンロードできませんでした</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">この問題が引き続き発生する場合は、データの収集を依頼した人に報告してください。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect が %s のサーバーに到達できません。 URL を正しく入力しましたか?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect が %s のサーバーに安全に接続できません。 URL を正しく入力しましたか?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">サーバー %1$s がステータスコード %2$s を返しました。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">サーバー %s が無効なレスポンスを返しました。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">保存されたフォーム</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">空のフォーム</string>\n  <string name=\"delete_confirm\">%s フォームを削除しますか?</string>\n  <string name=\"delete_file\">選択を削除</string>\n  <string name=\"delete_no\">削除しない</string>\n  <string name=\"delete_yes\">フォームを削除</string>\n  <string name=\"form_delete_message\">選択したフォームを削除中</string>\n  <string name=\"file_deleted_ok\">%s フォームを削除しました!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">サーバー</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">ユーザーインターフェイス</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">地図</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">フォーム管理</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">ユーザーとデバイスの識別</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">実験的</string>\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">サーバー設定</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">サーバー URL</string>\n  <string name=\"url_error\">申し訳ありません、URL が正しくありません!</string>\n  <string name=\"username\">ユーザー名</string>\n  <string name=\"change_username\">ODK ユーザー名</string>\n  <string name=\"password\">パスワード</string>\n  <string name=\"change_password\">ODK パスワード</string>\n  <string name=\"password_error_whitespace\">パスワードの先頭または末尾に空白は使用できません</string>\n  <string name=\"username_error_whitespace\">ユーザー名の先頭または末尾に空白は使用できません</string>\n  <string name=\"app_theme\">テーマ</string>\n  <string name=\"language\">言語</string>\n  <string name=\"use_device_language\">デバイスの言語を使用する</string>\n  <string name=\"font_size\">文字フォントサイズ</string>\n  <string name=\"change_font_size\">文字フォントサイズ</string>\n  <string name=\"font_size_extra_large\">特大</string>\n  <string name=\"font_size_large\">大</string>\n  <string name=\"font_size_medium\">中</string>\n  <string name=\"font_size_small\">小</string>\n  <string name=\"font_size_extra_small\">特小</string>\n  <string name=\"navigation\">ナビゲーション</string>\n  <string name=\"swipe_navigation\">水平方向のスワイプを使用</string>\n  <string name=\"buttons_navigation\">次へ/前へボタンを使用</string>\n  <string name=\"swipe_buttons_navigation\">スワイプとボタンを使用</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">次へ</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">戻る</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">フォームの更新</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">空のフォーム更新モード</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">手動</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">以前ダウンロードしたフォームのみ</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">完全に一致するサーバー</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">自動更新の頻度</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">15 分ごと</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">1 時間ごと</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">6 時間ごと</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">24 時間ごと</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">自動ダウンロード</string>\n  <string name=\"automatic_download_summary\">更新されたバージョンのフォームを自動的にダウンロードします</string>\n  <string name=\"hide_old_form_versions_setting_title\">古いフォームのバージョンを非表示</string>\n  <string name=\"hide_old_form_versions_setting_summary\">ブランクフォームに最新バージョンのみが表示されます</string>\n  <string name=\"form_submission_category\">フォーム送信</string>\n  <string name=\"autosend_selector_title\">自動送信</string>\n  <string name=\"autosend\">自動送信</string>\n  <string name=\"off\">オフ</string>\n  <string name=\"wifi_autosend\">Wifi のみ</string>\n  <string name=\"cellular_autosend\">データ通信のみ</string>\n  <string name=\"wifi_cellular_autosend\">Wifi またはデータ通信</string>\n  <string name=\"delete_after_send\">送信後に削除</string>\n  <string name=\"delete_after_send_summary\">サーバに送信した後、確定されたフォームやメディアを削除します</string>\n  <string name=\"form_filling_category\">フォーム入力</string>\n  <string name=\"constraint_behavior_title\">処理の制約</string>\n  <string name=\"constraint_behavior\">処理動作の制約</string>\n  <string name=\"constraint_behavior_on_swipe\">次へスワイプ時に検証</string>\n  <string name=\"constraint_behavior_on_finalize\">確定済になるまで検証を延期</string>\n  <string name=\"high_resolution_title\">高解像度ビデオ</string>\n  <string name=\"high_resolution_summary\">高解像度のビデオ録画を有効にします</string>\n  <string name=\"image_size_title\">画像サイズ</string>\n  <string name=\"image_size_dialog_title\">画像の長辺の最大ピクセル数</string>\n  <string name=\"image_size_large\">大 (3072px)</string>\n  <string name=\"image_size_medium\">中 (2048px)</string>\n  <string name=\"image_size_small\">小 (1024px)</string>\n  <string name=\"image_size_very_small\">極小 (640px)</string>\n  <string name=\"image_size_original\">カメラの元のサイズ (デフォルト)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">質問のガイダンスを表示する</string>\n  <string name=\"guidance_no\">いいえ</string>\n  <string name=\"guidance_yes\">はい - 常に表示</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">はい - 折りたたみ</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">オーディオ録音の外部アプリを使用する</string>\n  <string name=\"form_import_category\">フォームインポート</string>\n  <string name=\"instance_sync\">インポート時にフォームを確定済にする</string>\n  <string name=\"instance_sync_summary\">インスタンスフォルダーに直接追加されたフォームに影響します。</string>\n  <string name=\"analytics_preferences\">使用状況データ</string>\n  <string name=\"analytics\">匿名の使用状況データを収集</string>\n  <string name=\"form_metadata_title\">フォームメタデータ</string>\n  <string name=\"form_metadata\">フォームメタデータ</string>\n  <string name=\"form_metadata_summary\">これらの値は、プリロードしたフィールドのユーザー名、メール、電話を持つフォームに追加され、データの送信者を識別します。</string>\n  <string name=\"user_defined\">ユーザー定義</string>\n  <string name=\"phone_number\">電話番号</string>\n  <string name=\"email\">メールアドレス</string>\n  <string name=\"invalid_email_address\">メールアドレスが無効です!</string>\n  <string name=\"device_defined\">デバイス定義</string>\n  <string name=\"device_id\">デバイス ID</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">利用できません</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">新しいパスワードを入力してください</string>\n  <string name=\"admin_password\">管理者パスワード</string>\n  <string name=\"admin_password_changed\">管理者パスワードが変更されました</string>\n  <string name=\"admin_password_disabled\">管理者パスワードが無効にされました</string>\n  <string name=\"admin_password_incorrect\">申し訳ありません、パスワードが正しくありません!</string>\n  <string name=\"enter_admin_password\">管理者パスワードを入力してください</string>\n  <string name=\"change_admin_password\">管理者パスワードを変更</string>\n  <string name=\"show_password\">パスワードを表示</string>\n  <string name=\"found_in_main\">チェックを外すとメインメニューから非表示にします</string>\n  <string name=\"form_entry\">チェックを外すとフォーム入力から非表示にします</string>\n  <string name=\"reset_project_settings_title\">リセット</string>\n  <string name=\"delete_project\">削除</string>\n  <string name=\"select_all\">すべて選択</string>\n  <string name=\"clear_all\">すべてクリア</string>\n  <string name=\"reset_settings\">すべての設定 (内部設定、保存された設定)</string>\n  <string name=\"reset_saved_forms\">保存済フォーム (インスタンスフォルダー、インスタンスデータベース)</string>\n  <string name=\"reset_blank_forms\">空のフォーム (フォームフォルダー、フォームデータベース、アイテムセットデータベース)</string>\n  <string name=\"reset_layers\">地図レイヤー (レイヤーフォルダー)</string>\n  <string name=\"reset_cache\">フォーム読み込みキャッシュ (キャッシュフォルダー)</string>\n  <string name=\"reset_settings_dialog_title\">リセットするものを選択</string>\n  <string name=\"reset_settings_button_reset\">リセット</string>\n  <string name=\"reset_in_progress\">リセットしています…</string>\n  <string name=\"reset_app_state_result\">結果のリセット</string>\n  <string name=\"reset_app_state_warning\">選択したデータはすべて完全に削除されます。元に戻すことはできません。</string>\n  <string name=\"reset_settings_result\">すべての設定 :: %s</string>\n  <string name=\"reset_saved_forms_result\">保存されたフォーム :: %s</string>\n  <string name=\"reset_blank_forms_result\">空のフォーム :: %s</string>\n  <string name=\"reset_layers_result\">地図レイヤー :: %s</string>\n  <string name=\"reset_cache_result\">フォーム読み込みキャッシュ :: %s</string>\n  <string name=\"main_menu_settings\">メインメニュー設定</string>\n  <string name=\"user_settings\">ユーザー設定</string>\n  <string name=\"form_entry_setting\">フォーム入力設定</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">後ろへの移動</string>\n  <string name=\"moving_backwards_disabled_title\">後ろへの移動は無効です</string>\n  <string name=\"yes\">はい</string>\n  <string name=\"no\">いいえ</string>\n  <string name=\"moving_backwards_enabled_title\">後ろへの移動は有効です</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">アプリについて</string>\n  <string name=\"tell_your_friends\">ODK Collect を共有</string>\n  <string name=\"tell_your_friends_msg\">あなたの同僚はまだ紙でデータを収集しているのですか? ODK Collect を彼らと共有しましょう。</string>\n  <string name=\"leave_a_review\">Play ストアにレビューを残す</string>\n  <string name=\"leave_a_review_msg\">あなたの (肯定的な) レビューが Play ストアでアプリの認知度を向上させてくれます</string>\n  <string name=\"odk_website\">ODK ウェブサイトを訪問</string>\n  <string name=\"odk_website_summary\">オープンデータキット (ODK) は、課題のある環境での社会的利益のためのデータを収集するために使用されます。</string>\n  <string name=\"odk_forum\">ODK フォーラムに参加</string>\n  <string name=\"odk_forum_summary\">フォーラムに参加して、サポートを受けたり、機能をリクエストしたり、コード/翻訳に貢献してください!</string>\n  <string name=\"all_open_source_licenses\">オープンソースライブラリー/ライセンス</string>\n  <string name=\"all_open_source_licenses_msg\">私たちは巨人の肩の上に立っています!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"qr_code\">QR コード設定</string>\n  <string name=\"scan_qr_code_fragment_title\">スキャン</string>\n  <string name=\"view_qr_code_fragment_title\">QR コード</string>\n  <string name=\"import_qrcode_sd\">QR コードをインポート</string>\n  <string name=\"successfully_imported_settings\">設定を正常にインポートしました</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">最大文字数制限に達しました: すべての設定の QR コードを生成することができません</string>\n  <string name=\"share\">共有</string>\n  <string name=\"turn_on_flashlight\">フラッシュライトをオンにする</string>\n  <string name=\"turn_off_flashlight\">フラッシュライトをオフにする</string>\n  <string name=\"qrcode_with_both_passwords\">機密性の高い情報が含まれます: <b>管理者</b> と <b>サーバーパスワード</b></string>\n  <string name=\"qrcode_without_passwords\">QR コードには <b>管理者</b> または <b>サーバー</b> パスワードは含まれていません。 タップすると設定します</string>\n  <string name=\"qrcode_with_admin_password\">機密性の高い情報が含まれます: <b>管理者</b> パスワード</string>\n  <string name=\"qrcode_with_server_password\">機密性の高い情報が含まれます: <b>サーバー</b> パスワード</string>\n  <string name=\"server_password\">サーバーパスワード</string>\n  <string name=\"include_password_dialog\">パスワードがコードに含まれます</string>\n  <string name=\"generate\">生成</string>\n  <string name=\"invalid_qrcode\">QR コードに有効な設定が含まれていません</string>\n  <string name=\"qr_code_not_found\">選択した画像に QR コードが見つかりませんでした</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">キャンセル</string>\n  <string name=\"cancel_loading_form\">キャンセル</string>\n  <string name=\"cancel_location\">キャンセル</string>\n  <string name=\"do_not_change\">キャンセル</string>\n  <string name=\"canceling\">キャンセル中</string>\n  <string name=\"notification_channel_name\">全般通知</string>\n  <string name=\"success\">成功しました</string>\n  <string name=\"error_occured\">エラーが発生しました</string>\n  <string name=\"please_wait\">しばらくお待ちください。</string>\n  <string name=\"please_wait_long\">しばらくお待ちください。数分間かかることがあります。</string>\n  <string name=\"fs_delete_media_path_if_file_error\">\\'%s\\' を削除できませんでした。ファイルを手動で削除して、フォームを再ダウンロードしてください。</string>\n  <string name=\"fs_create_media_folder_error\">メディアフォルダー \\'%s\\' を作成できませんでした。</string>\n  <string name=\"fs_file_copy_error\">\\'%1$s\\' を \\'%2$s\\' に上書きコピーできませんでした。理由: %3$s</string>\n  <string name=\"select_odk_shortcut\">ODK ショートカットを選択</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">追加</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">認識できない URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">間違った URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">同じフォームIDとバージョンを持つ、2つの異なるフォームをダウンロードしました。異なる時間に、または異なるサーバーにアップロードした、同じフォームかもしれません​​。いずれにせよ、一方を削除する必要があります。</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">破棄</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ka/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">შეავსეთ ცარიელი ფორმა</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">შენახული ფორმების რედაქტირება</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">დასრულებული ფორმების გაგზავნა</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">გაგზავნილი ფორმის ნახვა</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">ცარიელი ფორმის მიღება</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">შენახული ფორმების წაშლა</string>\n  <string name=\"saved_forms\">შევსებული ფორმები</string>\n  <string name=\"version_number\">ვერსია:%s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'დაემატა\\' EEE, MMM dd, yyyy \\'საათსა და წუთზე:\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'განახლდა\\' EEE, MMM dd, yyyy HH:mm \\'-ზე\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'შეინახა\\' EEE, MMM dd, yyyy \\'საათსა და წუთზე:\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'დასრულდა\\' EEE, MMM dd, yyyy \\'საათსა და წუთზე:\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'გაიგზავნა\\' EEE, MMM dd, yyyy \\'საათსა და წუთზე:\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'ვერ გაიგზავნა\\' EEE, MMM dd, yyyy \\'საათსა და წუთზე:\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'წაიშალა\\' EEE, MMM dd, yyyy HH:mm \\'-ზე\\'</string>\n  <string name=\"submission_deleted\">გაგზავნილი ანკეტა წაიშალა</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_form\">წაშლილი ფორმა</string>\n  <string name=\"sort_the_list\">სიის დალაგება</string>\n  <string name=\"sort_by\">დალაგება მიხედვით</string>\n  <string name=\"sort_by_name_asc\">სახელის მიხედვით, A-Z</string>\n  <string name=\"sort_by_name_desc\">სახელის მიხედვით, Z-A</string>\n  <string name=\"sort_by_date_desc\">თარიღის მიხედვით, ჯერ უახლესები</string>\n  <string name=\"sort_by_date_asc\">თარიღის მიხედვით, ჯერ ძველები</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">უპირველეს ყოვლისა შენახულები</string>\n  <string name=\"filter_the_list\">სიის გაფილტვრა</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">ძიება</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">შემოწმება დასრულებულია. ყველა ფორმა ჩამოიტვირთა.</string>\n  <string name=\"loading_form\">ფორმის ჩატვირთვა</string>\n  <string name=\"survey_loading_reading_form_message\">მიმდინარეობს ფორმის წაკითხვა</string>\n  <string name=\"survey_loading_reading_data_message\">მიმდინარეობს გამოკითხვის მონაცემების წაკითხვა</string>\n  <string name=\"survey_loading_reading_csv_message\">მიმდინარეობს CSV ფაილის წაკითხვა</string>\n  <string name=\"parse_error\">უკაცრავად, ფორმის დამუშავება შეუძლებელია.</string>\n  <string name=\"cannot_edit_completed_form\">ფორმის რედაქტირება შეუძლებელია მისი დასრულებულად მონიშვნის შემდეგ. შესაძლოა, ეს ფორმა უკვე დაშიფრული იყოს</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">მოხდა შეუნახავი ცვლილებების აღდგენა!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">ენის შეცვლა</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">მოთხოვნაზე გადასვლა</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">ფორმის შენახვა</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">დასაწყისში გადასვლა</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">ბოლოში გადასვლა</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">ზევით ასვლა</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">ჯგუფი</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">განმეორებადი ჯგუფი</string>\n  <string name=\"exit\">გამოსვლა</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">დავამატოთ \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">დამატება</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">დამატება</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">გამოპასუხების რედაქტირება</string>\n  <string name=\"clearanswer_confirm\">შევცვალოთ ეს გამოპასუხება \\\"%s\\\"-ით?</string>\n  <string name=\"clear_answer\">გამოპასუხების მოცილება</string>\n  <string name=\"clear_answer_ask\">მოვაცილოთ ეს გამოპასუხება?</string>\n  <string name=\"clear_answer_no\">გაუქმება</string>\n  <string name=\"discard_answer\">გამოპასუხების წაშლა</string>\n  <string name=\"discard_group\">ჯგუფის მოცილება</string>\n  <string name=\"delete_repeat\">ჯგუფის მოცილება</string>\n  <string name=\"delete_repeat_ask\">მოვაცილოთ ეს ჯგუფი?</string>\n  <string name=\"delete_repeat_confirm\">მოვაცილოთ \\\"%s\\\" ჯგუფი და მისი ყველა ქვევარიანტი?</string>\n  <string name=\"delete_repeat_no\">გაუქმება</string>\n  <string name=\"invalid_answer_error\">უკაცრავად, აღნიშნული პასუხი არ არის დაშვებული!</string>\n  <string name=\"required_answer_error\">უკაცრავად, ეს გამოპასუხება მოთხოვნილია!</string>\n  <string name=\"invalid_space_in_answer_singular\">გაფრთხილება: მნიშვნელობა %s შეიცავს გამოტოვებულ ადგილებს </string>\n  <string name=\"invalid_space_in_answer_plural\">გაფრთხილება: მნიშვნელობები %s შეიცავს ცარიელ ადგილებს</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">თქვენ ხართ \\\"%s\\\"-ს ბოლოში.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">ფორმის შენახვა</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">დასრულება</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">გაგზავნა</string>\n  <string name=\"data_saved_error\">უკაცრავად, ფორმა ვერ შეინახა!</string>\n  <string name=\"data_saved_ok\">ფორმა წარმატებით შეინახა!</string>\n  <string name=\"save_point_error\">შეცდომა აღდგენის წერტილის შენახვისას: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">ფორმაში ცვლილებების გაუქმება</string>\n  <string name=\"discard_changes\">ცვლილებების გაუქმება</string>\n  <string name=\"keep_changes\">ცვლილებების შენახვა</string>\n  <string name=\"keep_editing\">კითხვარის შევსების გაგრძელება</string>\n  <string name=\"quit_form_title\">გსურთ კითხვარის შენახვა?</string>\n  <string name=\"save_explanation\">თქვენ შეგიძლიათ შეინახოთ ეს კითხვარი და სხვა დროს გახსნათ.  შენახვის მითითების შემთხვევაში აქამდე მონიშნული პასუხები შენარჩუნდება.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"quit_form_continue_title\">გსურთ რედაქტირების გაგრძელება?</string>\n  <string name=\"discard_form_warning\">თუ გააუქმებთ კითხვარს,  აქამდე მონიშნული პასუხები დაიკარგება.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">ფორმის შენახვა…</string>\n  <string name=\"survey_saving_validating_message\">მიმდინარეობს პასუხების შემოწმება</string>\n  <string name=\"survey_saving_collecting_message\">მიმდინარეობს მონაცემთა შეგროვება</string>\n  <string name=\"survey_saving_saving_message\">მიმდინარეობს SD-ბარათზე შენახვა</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">ჩართვა</string>\n  <string name=\"activity_not_found\">არ არსებობს განსახილველი ფუნქცია: %s</string>\n  <string name=\"no_app\">მოთხოვნილი აპლიკაცია არ მოიძებნა, გთხოვთ მიუთითოთ ხელით.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">ბმულის გახსნა</string>\n  <string name=\"capture_osm\">OpenMapKit-ის ჩართვა</string>\n  <string name=\"recapture_osm\">OSM-მარკირების უკან დაბრუნება</string>\n  <string name=\"get_barcode\">ბარკოდის მიღება</string>\n  <string name=\"replace_barcode\">ბარკოდის შეცვლა</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">ფოტოს გადაღება</string>\n  <string name=\"choose_image\">სურათის არჩევა</string>\n  <string name=\"annotate_image\">სურათის ანოტირება</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">შენახვა და დახურვა</string>\n  <string name=\"signature_capture\">ხელმოწერა</string>\n  <string name=\"sign_button\">ხელმოწერის შენახვა</string>\n  <string name=\"markup_image\">ჭდის სურათი</string>\n  <string name=\"draw_image\">მონახაზის სურათი</string>\n  <string name=\"reset_image\">გადატვირთვა</string>\n  <string name=\"set_color\">ფერის დაყენება</string>\n  <string name=\"capture_video\">ვიდეოგადაღება</string>\n  <string name=\"choose_video\">ვიდეოს არჩევა</string>\n  <string name=\"play_video\">ვიდეოჩანაწერის ჩვენება</string>\n  <string name=\"choose_file\">ფაილის არჩევა</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">ხმის ჩაწერა</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">ხმის ჩანაწერის არჩევა</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">შეცდომა მოხდა მედიაფაილის მიმაგრებისას: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">აირჩიე პასუხი</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">არჩეული: </string>\n  <string name=\"trigger\">კარგი. გთხოვთ, გააგრძელოთ.</string>\n  <string name=\"ext_file_no_data_error\">ეს ფაილი მონაცემებს არ შეიცავს!</string>\n  <string name=\"ext_import_completed_message\">მონაცემების წაკითხვა დასრულდა!</string>\n  <string name=\"get_bearing\">ორიენტაციის ჩაწერა</string>\n  <string name=\"replace_bearing\">ორიენტაციის შეცვლა</string>\n  <string name=\"getting_bearing\">ორიენტაცია იტვირთება</string>\n  <string name=\"accept_bearing\">ჩაიწერეთ ორიენტაცია</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">ფაილი: %s არ არის ვალიდური.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">ფაილი: %s არ არის ვალიდური.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"basemap_source\">წყარო</string>\n  <string name=\"streets\">ქუჩები</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">უკაცრავად, მდებარეობის მიმღები არ არის გააქტიურებული!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"start\">დასაწყისი</string>\n  <string name=\"enable_gps\">GPS-ის ჩართვა</string>\n  <string name=\"geo_exit_warning\">გაუქმდეს ცვლილებები და ბრუნდებით ODK-ში?</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <string name=\"none\">არცერთი</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">ფორმის გაგზავნა</string>\n  <string name=\"noselect_error\">უკაცრავად, თქვენ ფორმა არ აგირჩევიათ!</string>\n  <string name=\"no_connection\">ქსელი არ არის ხელმისაწვდომი!</string>\n  <string name=\"sending_items\">%2$s-დან %1$s ფორმის გაგზავნა</string>\n  <string name=\"send_selected_data\">არჩეული ფორმების გაგზავნა</string>\n  <string name=\"server_auth_credentials\">სერვერის არასწორი პაროლი ან მომხმარებლის სახელი: %s</string>\n  <string name=\"server_requires_auth\">სერვერი მოითხოვს ავთენტიკაციას</string>\n  <string name=\"no_forms_uploaded\">უკაცრავად, ფორმები ვერ აიტვირთა.</string>\n  <string name=\"upload_results\">ატვირთვის შედეგი</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">ხედის შეცვლა</string>\n  <string name=\"show_sent_and_unsent_forms\">გაგზავნილი და გაუგზავნელი ფორმები</string>\n  <string name=\"show_unsent_forms\">მხოლოდ გაუგზავნელი ფორმები</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">განახლება</string>\n  <string name=\"download\">არჩეულის ჩამოტვირთვა</string>\n  <string name=\"downloading_data\">სერვერთან დაკავშირება</string>\n  <string name=\"fetching_file\">%1$s.\\n\\nForm %2$sდან of %3$s ფორმ(ებ)ის მიღება</string>\n  <string name=\"form_download_progress\">%1$s. მედია-ფაილების მიღება: %2$s of %3$s</string>\n  <string name=\"version\">ვერსია:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">შევსებული ფორმები</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">ცარიელი ფორმები</string>\n  <string name=\"delete_confirm\">წავშალოთ %s ფორმა?</string>\n  <string name=\"delete_file\">არჩეულის წაშლა</string>\n  <string name=\"delete_no\">არ წაშალოთ</string>\n  <string name=\"delete_yes\">ფორმის წაშლა</string>\n  <string name=\"file_deleted_ok\">%s ფორმა წარმატებით წაიშალა</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">სერვერი</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">მომხმარებლის ინტერფეისი</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">რუკები</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">სერვერი</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">ბმული</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">სერვერის ვებ-მისამართი</string>\n  <string name=\"url_error\">უკავრავად, არასწორი ბმული!</string>\n  <string name=\"username\">მომხმარებლის სახელი</string>\n  <string name=\"change_username\">ODK-ის მომხმარებლის სახელი</string>\n  <string name=\"password\">პაროლი</string>\n  <string name=\"change_password\">პაროლი</string>\n  <string name=\"font_size\">ტექსტის ზომა</string>\n  <string name=\"change_font_size\">ტექსტის ზომა</string>\n  <string name=\"font_size_extra_large\">ძალიან დიდი</string>\n  <string name=\"font_size_large\">დიდი</string>\n  <string name=\"font_size_medium\">საშუალო</string>\n  <string name=\"font_size_small\">პატარა</string>\n  <string name=\"font_size_extra_small\">ძალიან პატარა</string>\n  <string name=\"navigation\">ნავიგაცია</string>\n  <string name=\"swipe_navigation\">ჰორიზონტალური გასრიალების გამოყენება</string>\n  <string name=\"buttons_navigation\">წინა-უკანა ღილაკების გამოყენება</string>\n  <string name=\"swipe_buttons_navigation\">გასრიალების და ღილაკის გამოყენება</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">შემდეგი</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">უკან</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"autosend_selector_title\">ავტომატურად გაგზავნა</string>\n  <string name=\"autosend\">ავტომატურად გაგზავნა</string>\n  <string name=\"delete_after_send\">გაგზავნის შემდგომ წაშლა</string>\n  <string name=\"constraint_behavior\">შეზღუდვის დამუშავების ქცევა</string>\n  <string name=\"constraint_behavior_on_swipe\">სანდოობის შემოწმება წინ გადასვლისას</string>\n  <string name=\"constraint_behavior_on_finalize\">სანდოობის შემოწმების გადადება ფორმის დასრულებამდე</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">შეიყვანეთ ახალი პაროლი</string>\n  <string name=\"admin_password\">ადმინისტრატორის პაროლი</string>\n  <string name=\"admin_password_changed\">ადმინისტრატორის პაროლი წარმატებით შეიცვალა</string>\n  <string name=\"admin_password_disabled\">ადმინისტრატორის პაროლი დაბლოკილია</string>\n  <string name=\"admin_password_incorrect\">უკაცრავად, პაროლი არასწორია!</string>\n  <string name=\"enter_admin_password\">შეიყვანეთ ადმინისტრატორის პაროლი</string>\n  <string name=\"change_admin_password\">ადმინისტრატორის პაროლის შეცვლა</string>\n  <string name=\"show_password\">მაჩვენე პაროლი</string>\n  <string name=\"found_in_main\">გამორთეთ მთავარი მენიუდან დასამალად</string>\n  <string name=\"reset_project_settings_title\">გადატვირთვა</string>\n  <string name=\"reset_settings_button_reset\">გადატვირთვა</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">ფორმის შენახვა</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">შესახებ</string>\n  <string name=\"tell_your_friends\">ODK Collect გაზიარება</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">გაუქმება</string>\n  <string name=\"cancel_loading_form\">გაუქმება</string>\n  <string name=\"cancel_location\">გაუქმება</string>\n  <string name=\"do_not_change\">გაუქმება</string>\n  <string name=\"canceling\">მიმდინარეობს გაუქმება</string>\n  <string name=\"success\">წარმატება</string>\n  <string name=\"error_occured\">მოხდა შეცდომა</string>\n  <string name=\"please_wait\">გთხოვთ, დაიცადოთ.</string>\n  <string name=\"please_wait_long\">გთხოვთ, დაიცადოთ; ოპერაციის შესრულებას შეიძლება რამდენიმე წუთი დაჭირდეს</string>\n  <string name=\"loading\">იტვირთება…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">პროექტები</string>\n  <string name=\"add_project\">პროექტის დამატება</string>\n  <string name=\"add\">დამატება</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">ფორმა შეინახა, როგორც დაუსრულებელი</string>\n  <string name=\"form_saved\">თქვენი ფორმა შეინახა</string>\n  <string name=\"form_sending\">ფორმა იგზავნება</string>\n  <string name=\"edit_form\">რედაქტირება</string>\n  <string name=\"view_form\">ნახვა</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-km/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">ចាប់ផ្តើមទម្រង់ថ្មី</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">សេចក្តីព្រាង</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">រួចរាល់សម្រាប់ការផ្ញើចេញ</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">បានផ្ញើចេញ</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">ទាញយកទម្រង់</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">លុបទម្រង់</string>\n  <string name=\"saved_forms\">រក្សាទុកទម្រង់</string>\n  <string name=\"version_number\">ជំនាន់: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'បន្ថែមនៅ\\'  EEE, MMM dd, yyyy \\'នៅម៉ោង\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'បានធ្វើបច្ចុប្បន្នភាពនៅ\\' EEE, MMM dd, yyyy \\'ម៉ោង\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'រក្សាទុកនៅ\\'  EEE, MMM dd, yyyy \\'នៅម៉ោង\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'បញ្ចប់នៅ\\'  EEE, MMM dd, yyyy \\'នៅម៉ោង\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'ផ្ញើចេញនៅ\\'  EEE, MMM dd, yyyy \\'នៅម៉ោង\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'ការផ្ញើចេញបរាជ័យនៅ\\'  EEE, MMM dd, yyyy \\'នៅម៉ោង\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'បានលុបនៅថ្ងៃទី \\' EEE, MMM dd, yyyy \\'ម៉ោង\\' HH:mm</string>\n  <string name=\"submission_deleted\">ការដាក់ស្នើត្រូវបានលុប</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'កែប្រែនៅថ្ងៃ\\' EEE, MMM dd, yyyy \\'នៅម៉ោង\\' HH:mm</string>\n  <string name=\"encrypted_form\">បម្លែងសំណុំបែបបទ​ជារហស្សលិខ</string>\n  <string name=\"deleted_form\">សំណុំបែបបទដែលបានលុប​</string>\n  <string name=\"sort_the_list\">តម្រៀបបញ្ជី</string>\n  <string name=\"sort_by\">តំរៀបតាម</string>\n  <string name=\"sort_by_name_asc\">ឈ្មោះ, ក-អ</string>\n  <string name=\"sort_by_name_desc\">ឈ្មោះ, អ-ក</string>\n  <string name=\"sort_by_date_desc\">ថ្ងៃខែឆ្នាំ, ថ្មីបំផុតមុន</string>\n  <string name=\"sort_by_date_asc\">ថ្ងៃខែឆ្នាំ, យូរបំផុតមុន</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">តម្រងបញ្ជី</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">ស្វែងរក</string>\n  <string name=\"not_granted_permission\">សកម្មភាពដែលអ្នកកំពុងព្យាយាមធ្វើទាមទារការអនុញ្ញាតដែលមិនត្រូវបានផ្តល់ឱ្យ។</string>\n  <string name=\"storage_runtime_permission_denied_title\">សិទ្ធិក្នុងការផ្ទុក</string>\n  <string name=\"storage_runtime_permission_denied_desc\">បើគ្មានការអនុញ្ញាតទាំងនេះ Collect មិនអាចចូលប្រើទម្រង់របស់អ្នក ឬរក្សាទុកចម្លើយ​បើក​កម្មវិធី​ឡើង​វិញ​នៅ​ពេល​អ្នក​រួចរាល់​ដើម្បី​ផ្តល់​សិទ្ធិ​ទាំងនេះ។</string>\n  <string name=\"location_runtime_permissions_denied_title\">ការអនុញ្ញាតទីតាំង</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$d សំណុំបែបបទបានបន្ថែម។</string>\n  <string name=\"finished_disk_scan\">បញ្ចប់ការស្កេន. សំណុំបែបបទទាំងអស់បានផ្ទុករួចរាល់.</string>\n  <string name=\"xform_parse_error\">XForm មាន​បញ្ហា​ក្នុង​ការ​ញែក​ដោយ​បាត់បង់ ឬ​មិន​ត្រឹម​ត្រូវ​សម្រាប់ %1$s: %2$s</string>\n  <string name=\"loading_form\">កំពុងផ្ទុកសំណុំបែបបទ</string>\n  <string name=\"survey_loading_reading_form_message\">កំពុង​អាន​ទម្រង់​សំណុំបែប​បទ​…</string>\n  <string name=\"survey_loading_reading_data_message\">ការអានទិន្នន័យស្ទង់មតិ …</string>\n  <string name=\"survey_loading_reading_csv_message\">កំពុង​អាន​ឯកសារ CSV</string>\n  <string name=\"parse_error\">សុំទោស, សំណុំ​បែប​បទ​មិន​អាច​បែកចែកបាន</string>\n  <string name=\"cannot_edit_completed_form\">ទម្រង់នេះមិនអាចកែប្រែម្តងទៀតទេ, វាត្រូវបានគេសម្គាល់ថាបានបញ្ចប់ហើយ។ វាប្រហែលជាត្រូវបានអ៊ិនគ្រីប។</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">ការផ្លាស់ប្តូរមិនបានរក្សាទុកត្រូវបានស្តារឡើងវិញពី savepoint!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">ប្ដូរ​ភាសា</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">ទៅ Prompt</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">រក្សាទុកជាសេចក្តីព្រាង</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">ទៅកន្លែងចាប់ផ្តើម</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">ទៅកន្លែងបញ្ចប់</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">ទៅឡើង</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">ក្រុម</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"exit\">ចាកចេញ</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">បន្ថែម \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">បន្ថែម</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">បន្ថែមមួយផ្សេងទៀត</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">កែប្រែ Prompt</string>\n  <string name=\"clearanswer_confirm\">បោះបង់ចម្លើយ​ដើម្បី \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">បោះបង់ចម្លើយ</string>\n  <string name=\"clear_answer_ask\">បោះ​បង់ការឆ្លើយនេះ</string>\n  <string name=\"clear_answer_no\">បោះបង់</string>\n  <string name=\"discard_answer\">បោះបង់ចម្លើយ</string>\n  <string name=\"discard_group\">ដក​ក្រុម​ចេញ</string>\n  <string name=\"delete_repeat\">ដក​ក្រុម​ចេញ</string>\n  <string name=\"delete_repeat_ask\">ដក​ក្រុម​នេះ​ចេញ?</string>\n  <string name=\"delete_repeat_confirm\">ដក​ក្រុម \\\"%s\\\" និង​កូន​ក្រុម​នានា​ចេញ?</string>\n  <string name=\"delete_repeat_no\">បោះបង់</string>\n  <string name=\"invalid_answer_error\">សុំទោស, ការឆ្លើយតបនេះមិនត្រឹមត្រូវ</string>\n  <string name=\"required_answer_error\">សុំទោស, ការឆ្លើយតបនេះទាមទារ</string>\n  <string name=\"invalid_space_in_answer_singular\">ព្រមាន: តម្លៃមូលដ្ឋាន %s មានដកឃ្លា</string>\n  <string name=\"invalid_space_in_answer_plural\">ព្រមាន: តម្លៃមូលដ្ឋាន %s មានដកឃ្លា</string>\n  <string name=\"survey_internal_error\">កំហុសខាងក្នុង: ជំហានដើម្បីជំរុញឱ្យបរាជ័យ។</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">អ្នកដល់ទីបញ្ចប់នៃ %s</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">រក្សាទុកជាពម្រាង</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">បញ្ចប់</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">ផ្ញើ</string>\n  <string name=\"data_saved_error\">សុំទោស, រក្សា​ទុកសំណុំ​បែបបទ​មិន​បាន​សម្រេច!</string>\n  <string name=\"data_saved_ok\">បាន​រក្សាទុកសំណុំ​បែបបទ​ដោយ​ជោគជ័យ!</string>\n  <string name=\"save_point_error\">បញ្ហាក្នុងការរក្សាទុកនូវចំនុចស្តារឡើងវិញ: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">បោះបង់ទម្រង់</string>\n  <string name=\"discard_changes\">បោះបង់ការផ្លាស់ប្តូរ</string>\n  <string name=\"keep_changes\">រក្សាការផ្លាស់ប្តូរ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">កំពុងរក្សាទុកសំណុំ​បែប​បទ​</string>\n  <string name=\"survey_saving_validating_message\">កំពុងផ្ទៀងផ្ទាត់ចម្លើយ</string>\n  <string name=\"survey_saving_collecting_message\">កំពុងប្រមូលទិន្នន័យ</string>\n  <string name=\"survey_saving_saving_message\">កំពុងរក្សាទុកទៅកាន់ថាស SD</string>\n  <string name=\"survey_saving_finalizing_message\">កំពុងបញ្ចប់ទៅកាន់ថាស SD</string>\n  <string name=\"survey_saving_encrypting_message\">កំពុងបម្លែង​ជារហស្សលិខទិន្នន័យ</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">ពុំមានទិន្នន័យណាមួយដែលត្រូវនឹង instance នេះ!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">មិន​មាន​សំណុំ​បែប​បទ​ទទេរពិតប្រាកដមួយដែលត្រូវគ្នានឹង jr_form_id នេះ។</string>\n  <string name=\"no_form_id_specified\">មិនបញ្ជាក់ពី ID របស់ទម្រង់???</string>\n  <string name=\"phone_does_not_support_rsa\">ទូរស័ព្ទមិនបានគាំទ្រការអ៊ីនគ្រីបប្រភេទ RSA</string>\n  <string name=\"invalid_rsa_public_key\">កូនសោរសាធាណះ RSA មិនត្រឹមត្រូវ</string>\n  <string name=\"encryption_error_message\">%s ទម្រង់មិនត្រូវបានរក្សាជាទម្រង់បញ្ចប់។</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">មិនអាចចាប់ផ្តើមការថតបានទេ។</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">មីក្រូហ្វូនត្រូវបានប្រើប្រាស់រួចហើយ។</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">ដំណើរការ</string>\n  <string name=\"activity_not_found\">គ្មានសកម្មភាពដើម្បីដោះស្រាយបាន : %s</string>\n  <string name=\"no_app\">ការ​ស្នើ​សុំ​របស់កម្មវិធី​បានបាត់បង់ ។​ សូមធ្វើការអាន​ដោយ​ដៃ ។</string>\n  <string name=\"null_intent_value\">កម្មវិធីខាងក្រៅមិនបានផ្តល់នូវព័ត៌មានដូចការរំពឹងទុក</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">បើក​អាសយដ្ឋាន</string>\n  <string name=\"capture_osm\">បើក OpenMapKit</string>\n  <string name=\"recapture_osm\">ធ្វើម្តងទៀត OSM tagging</string>\n  <string name=\"edited_osm_file\">បានកែប្រែឯកសារ XML របស់ OSM:</string>\n  <string name=\"invalid_osm_data\">មានកំហុសកើតឡើង, យើងមិនអាចទទួលយកទិន្នន័យ OSM ត្រឹមត្រូវ។</string>\n  <string name=\"alert\">ព្រមាន</string>\n  <string name=\"install_openmapkit\">សូមចុចដើម្បីដំឡើង OpenMapKit!</string>\n  <string name=\"get_barcode\">ទាញបារកូដ</string>\n  <string name=\"replace_barcode\">ជំនួសបារកូដ</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">សូមដាក់បារកូដនៅក្នុងប្រអប់ចតុកោណកែង</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">បើក​ឯកសារ</string>\n  <string name=\"rank_items\">ចំណាត់ថ្នាក់ធាតុ</string>\n  <string name=\"invalid_range_widget\">ធាតុក្រាហ្វិកនេះមានប៉ារ៉ាម៉ែត្រមិនត្រឹមត្រូវ!</string>\n  <string name=\"svg_file_does_not_exist\">ឯកសារ SVG មិនត្រូវបានរកឃើញ</string>\n  <string name=\"capture_image\">ថត​រូបភាព</string>\n  <string name=\"choose_image\">ជ្រើសរើសរូបថត</string>\n  <string name=\"selected_invalid_image\">ឯកសារ​ដែល​បាន​រើស​មិន​មែន​លក្ខណះ​ជា​រូបភាព​ត្រឹម​ត្រូវ</string>\n  <string name=\"take_picture_instruction\">ប៉ះលើផ្ទៃ​អេក្រង់ដើម្បីថត​រូប</string>\n  <string name=\"error_front_camera_unavailable\">កាមេរ៉ាខាងមុខមិនមាននៅលើឧបករណ៍នេះទេ</string>\n  <string name=\"annotate_image\">Annotate Image</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">រក្សាទុក និងបិទ</string>\n  <string name=\"signature_capture\">ថតហត្ថលេខា</string>\n  <string name=\"sign_button\">ប្រមូលហត្ថលេខា</string>\n  <string name=\"markup_image\">រូបភាពសម្គាល់</string>\n  <string name=\"draw_image\">រូបគំនូរវាស</string>\n  <string name=\"reset_image\">ធ្វើសារឡើងវិញ</string>\n  <string name=\"set_color\">កំណត់ពណ៌</string>\n  <string name=\"capture_video\">ថត​វីដេអូ</string>\n  <string name=\"choose_video\">ជ្រើសរើសវីដេអូ</string>\n  <string name=\"play_video\">ចាក់វីដេអូ</string>\n  <string name=\"choose_file\">ជ្រើសរើស​ឯកសារ</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">លុប</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">ថត​សំឡេង</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">ជ្រើសរើសសំឡេង</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">ផ្អាក</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">បន្ត</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">លុបឯកសារ?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">ឈប់</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">កំពុងថត…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">កំពុងថតសម្លេង…</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">ចាក់សម្លេង</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">កំពុងរក្សាទុកឯកសារ</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">អ្នកត្រូវតែបញ្ឈប់ការថតសម្លេងមុននឹងចាក់ចេញពីផ្ទាំងនេះ។</string>\n  <string name=\"error_attaching_binary_file\">មានបញ្ហាមួយបានកើតឡើងខណៈដែលភ្ជាប់ឯកសារមេឌៀ: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">ជ្រើសរើសតម្លៃ</string>\n  <string name=\"select_answer\">ជ្រើសរើសចម្លើយ</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">កែប្រែ​តម្លៃ</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">គ្មាន​តម្លៃត្រូវបានរើស</string>\n  <string name=\"selected_title\">បានជ្រើសរើសៈ</string>\n  <string name=\"trigger\">យល់ព្រម. សូមបន្ត.</string>\n  <string name=\"ext_file_no_data_error\">ឯកសារពុំមានទិន្នន័យ!</string>\n  <string name=\"ext_conflicting_columns_error\">ជួរឈរ %s ដូចគ្នា!</string>\n  <string name=\"ext_sortBy_numeric_error\">ការ​តម្រៀប​តាម​ជួរ​ឈ គួរតែមានតម្លៃជាតួលេខតែប៉ុណ្ណោះ។ តម្លៃដែលប៉ះទង្គិចគ្នា គឺ \\'%s\\'</string>\n  <string name=\"ext_search_wrong_arguments_error\">មានបញ្ហាវាក្យសម្ព័ន្ធក្នុងមុខងារស្វែងរក (search): មុខងារនេះ​ត្រូវ​ការ​អាគុយម៉ង់​​ចំនួន 1, 4 ឬ 6 ។</string>\n  <string name=\"ext_search_wrong_function_error\">មានបញ្ហាវាក្យសម្ព័ន្ធក្នុងមុខងារស្វែងរក (search): មុខងារដែលមិនស្គាល់មាន \\'%s\\' ។</string>\n  <string name=\"ext_search_bad_function_error\">មានបញ្ហាវាក្យសម្ព័ន្ធក្នុងមុខងារស្វែងរក (search): \\'%s\\' មិន​អាច​វាយ​តម្លៃ​ថា​ជា​មុខ​ងារ​ណា​មួយ​បាន​</string>\n  <string name=\"ext_search_return_error\">ការដោះស្រាយស្វែងរកបានត្រឡប់នូវវត្ថុមួយ​ប្រភេទ \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">មិន​អាច​នាំ​ចូល​ទិន្នន័យពី %1$s។ មូលហេតុ: %2$s</string>\n  <string name=\"ext_import_progress_message\">កំពុងផ្ទុកទិន្នន័យជាមុនពី \\'%1$s\\' សូមរងចាំ… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">ការអានទិន្នន័យ​បានបោះបង់</string>\n  <string name=\"ext_import_finalizing_message\">កំពុងបញ្ចប់ការផ្ទុកទិន្នន័យមុន</string>\n  <string name=\"ext_import_completed_message\">ការអានទិន្នន័យ​បានបញ្ចប់!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager មិនត្រូវបានចាប់ផ្តើមទេ។</string>\n  <string name=\"ext_import_csv_missing_error\">ទិន្នន័យ​ខាង​ក្រៅ​សម្រាប់ %1$s មិនត្រូវបាននាំចូល។ ប្រហែល​ជា​អ្នក​ភ្លេច​ភ្ជាប់​ឯកសារ %2$s.csv ជា​មួយ​នឹង​សំណំុ​បែប​បទ​របស់​អ្នក?</string>\n  <string name=\"ext_search_generic_error\">មានបញ្ហាវាក្យសម្ព័ន្ធក្នុងមុខងារស្វែងរក (search): %s</string>\n  <string name=\"ext_assign_value_error\">មិនអាចដាក់តម្លៃនៅ \\'%s\\' ។</string>\n  <string name=\"parser_exception\">XPathParser Exception: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">កត់​ត្រា​​ទ្រង់ទ្រាយ</string>\n  <string name=\"replace_bearing\">ជំនួស​ទ្រង់ទ្រាយ</string>\n  <string name=\"getting_bearing\">កំពុង​ផ្ទុក​ទ្រង់ទ្រាយ</string>\n  <string name=\"accept_bearing\">កត់​ត្រា​​ទ្រង់ទ្រាយ</string>\n  <string name=\"bearing\">ទ្រង់ទ្រាយ៖ %.3f</string>\n  <string name=\"direction\">ទិសដៅ៖ %s</string>\n  <string name=\"bearing_lack_of_sensors\">មិនអាចប្រមូលផលបានឡើយ: ឧបករណ៏ខ្វះល្បឿន, ឧបករណ៏ម៉ាញេទិចឬទាំងពីរ។</string>\n  <string name=\"select_date\">ជ្រើសរើសថ្ងៃខែឆ្នាំ</string>\n  <string name=\"select_time\">ជ្រើសរើសម៉ោង</string>\n  <string name=\"no_date_selected\">មិនបានជ្រើសរើសថ្ងៃខែឆ្នាំ</string>\n  <string name=\"no_time_selected\">មិនបានជ្រើសរើសម៉ោង</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">មេកាប៊ិទ</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">ឯកសារ៖ %s មិនត្រឹមត្រូវ</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">ឯកសារ៖ %s ខ្វះខាត</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">បិទ</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">ហេតុផលនៃការផ្លាស់ប្តូរ</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">ហេតុផល</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">រក្សាទុក</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"choices\">ជម្រើស</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_usgs_sat\">ផែនទីជាតិ Hybrid USGS </string>\n  <string name=\"openmap_usgs_img\">ផែនទីជាតិរូបភាព USGS </string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">ផែនទីមូលដ្ឋាន</string>\n  <string name=\"basemap_source\">ប្រភព</string>\n  <string name=\"map_style_label\">%s រចនាបទផែនទី</string>\n  <string name=\"streets\">ផ្លូវ</string>\n  <string name=\"terrain\">លក្ខណៈភូមិសាស្ត្រ</string>\n  <string name=\"hybrid\">ផែនទីកូនកាត់</string>\n  <string name=\"satellite\">ផ្កាយរណប</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">ភ្លឺ</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">ងងឹត</string>\n  <string name=\"google_play_services_error_occured\">មិនអាចចូលដំណើរការផែនទី Google ។ តើសេវា Google Play បានដំឡើងហើយឬនៅ?</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">សុំទោស, សេវាចាប់យកទីតាំងត្រូវបានបិទ</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">ទៅកាន់ការកំណត់</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">ខាងលិច %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">ខាងកើត %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">ខាងត្បូង %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">ខាងជើង %1$s%2$s%3$s</string>\n  <string name=\"start\">ចាប់ផ្តើម</string>\n  <string name=\"enable_gps\">ដំណើរ​ការ​ប្រព័ន្ធ​ចាប់​ទីតាំង</string>\n  <string name=\"geo_clear_warning\">លក្ខណះ​ពិសេស​ត្រូវ​បាន​បង្កើត។ តើ​អ្នក​ចង់​លុប​លក្ខណះ​នេះ​ដែរ​ឬ​ទេ?</string>\n  <string name=\"geo_exit_warning\">បោះបង់​ការផ្លាស់​ប្តូរ និងត្រឡប់ទៅកាន់ ODK វិញ?</string>\n  <string name=\"polygon_validator\">ត្រូវការចំនុចយ៉ាងតិច 3 ដើម្បី​បង្កើតពហុកោណ</string>\n  <string name=\"clear\">លុ​បចេញ</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"other\">%d វិនាទី</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"other\">%d នាទី</item>\n  </plurals>\n  <string name=\"none\">គ្មាន</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"other\">%d ម៉ែត្រ</item>\n  </plurals>\n  <string name=\"geopoint_no_draggable_instruction\">ប៉ះដើម្បីបន្ថែមប៊ូតុងសម្គាល់</string>\n  <string name=\"discard\">បោះបង់</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">ផ្អាក</string>\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">កំពុងបញ្ជូនសំណុំ​បែប​បទ​</string>\n  <string name=\"noselect_error\">សុំទោស, អ្នកមិនបានជ្រើសរើសសំណុំបែបបទណាមួយទេ!</string>\n  <string name=\"no_connection\">គ្មានការតភ្ជាប់បណ្តាញដែល​អាច​ប្រើបាន</string>\n  <string name=\"sending_items\">កំពុងបញ្ជូនសំណុំ​បែប​បទ​ %1$s នៃ %2$s</string>\n  <string name=\"send_selected_data\">បញ្ជូនការជ្រើសរើស</string>\n  <string name=\"server_auth_credentials\">ឈ្មោះអ្នកប្រើប្រាស់ ឬពាក្យសម្ងាត់មិនត្រឹមត្រូវសម្រាប់ម៉ាស៊ីនមេ៖ %s</string>\n  <string name=\"server_requires_auth\">ម៉ាស៊ីនមេទាមទារឲ្យមានការផ្ទៀងផ្ទាត់</string>\n  <string name=\"no_forms_uploaded\">សុំទោស, គ្មានសំណុំ​បែប​បទសម្រាប់ផ្ទុកឡើង.</string>\n  <string name=\"upload_results\">លទ្ធផលនៃការផ្ទុកឡើង</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">ការផ្លាស់ប្តូរទិដ្ឋភាព</string>\n  <string name=\"show_sent_and_unsent_forms\">បង្ហាញសំណុំ​បែប​បទដែលបានផ្ញើចេញ និងមិនទាន់ផ្ញើចេញ</string>\n  <string name=\"show_unsent_forms\">បង្ហាញសំណុំ​បែប​បទដែលមិនទាន់បានផ្ញើចេញ</string>\n  <string name=\"send_in_progress\">កំពុងផ្ញើផ្នែកខាងក្រោយ, សូមព្យាយាមម្តងទៀតក្នុងរយះពេលខ្លី</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">ធ្វើឲ្យថ្មីឡើងវិញ</string>\n  <string name=\"download\">ទទួលការជ្រើសរើស</string>\n  <string name=\"downloading_data\">កំពុង​ភ្ជាប់​ទៅ​កាន់​ម៉ាស៊ីន​មេ</string>\n  <string name=\"fetching_file\">កំពុង​​ទទួល​យក %1$s ។ \\n\\n ​សំណុំបែបបទ​ទី %2$s នៃ %3$s ។</string>\n  <string name=\"form_download_progress\">%1$s. កំពុងទាញយកឯកសារមេឌៀ: %2$s of %3$s</string>\n  <string name=\"version\">ជំនាន់ ៖</string>\n  <string name=\"form_updates_available\">មានទម្រង់សម្រាប់ធ្វើបច្ចុប្បន្នភាព</string>\n  <string name=\"newer_version_of_a_form_info\">នេះជាការធ្វើ​បច្ចុប្បន្នភាព​លើទម្រង់​ដែល​អ្នក​មាន</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">សំណុំបែបបទដែល​​បាន​រក្សា​ទុក</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">សំណុំបែបបទទទេ</string>\n  <string name=\"delete_confirm\">លុប​សំណុំបែបបទ %s?</string>\n  <string name=\"delete_file\">លុប​អ្វី​ដែល​បាន​ជ្រើស</string>\n  <string name=\"delete_no\">កុំ​លុប</string>\n  <string name=\"delete_yes\">លុប​សំណុំបែបបទ</string>\n  <string name=\"form_delete_message\">កំពុងលុបទម្រង់ដែលបានជ្រើសរើសរួច</string>\n  <string name=\"file_deleted_ok\">%s សំណុំបែបបទបានលុបដោយជោគជ័យ ! </string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">ម៉ាស៊ីនមេ</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">ចំណុច​ប្រទាក់​អ្នក​ប្រើប្រាស់​</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">ផែនទី</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">ការគ្រប់គ្រងទម្រង់</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">អត្តសញ្ញាណ​អ្នក​ប្រើ​ប្រាស់ និងឧបករណ៍</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">ការកំណត់ម៉ាស៊ីនមេ</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">អាសយដ្ឋាន</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL ម៉ាស៊ីន​បម្រើ</string>\n  <string name=\"url_error\">សុំទោស, អាសយដ្ឋានមិនត្រឹមត្រូវ!</string>\n  <string name=\"username\">អ្នក​ប្រើប្រាស់</string>\n  <string name=\"change_username\">ឈ្មោះ​អ្នកប្រើ ODK</string>\n  <string name=\"password\">ពាក្យសម្ងាត់</string>\n  <string name=\"change_password\">ពាក្យ​សម្ងាត់ ODK</string>\n  <string name=\"password_error_whitespace\">ការ​ដក​ឃ្លា​មិន​ត្រូវ​បាន​អនុញ្ញាតិ​នៅ​ក្នុង​ពាក្យ​សម្ងាត់​នៅផ្នែក​ខាង​មុខ ឬ​ខាង​ក្រោយ</string>\n  <string name=\"username_error_whitespace\">ការ​ដក​ឃ្លា​មិន​ត្រូវ​បាន​អនុញ្ញាតិ​នៅ​ក្នុង​ឈ្មោះ​អ្នក​ប្រើ​ប្រាស់​នៅ​ផ្នែក​​ខាង​​មុខ​ ឬ​​ខាង​​ក្រោយ</string>\n  <string name=\"app_theme\">Theme</string>\n  <string name=\"language\">ភាសា</string>\n  <string name=\"use_device_language\">ប្រើ​ប្រាស់​ភាសារបស់​ឧបករណ៍</string>\n  <string name=\"font_size\">ទំហំពុម្ភអក្សរ</string>\n  <string name=\"change_font_size\">ទំហំពុម្ភអក្សរ</string>\n  <string name=\"font_size_extra_large\">ធំបំផុត</string>\n  <string name=\"font_size_large\">ធំ</string>\n  <string name=\"font_size_medium\">មធ្បម</string>\n  <string name=\"font_size_small\">តូច</string>\n  <string name=\"font_size_extra_small\">តូចបំផុត</string>\n  <string name=\"navigation\">ការ​ស្វែង​រកផ្លូវ</string>\n  <string name=\"swipe_navigation\">ប្រើការកកិតដេក</string>\n  <string name=\"buttons_navigation\">ប្រើប៊ូតុង ទៅមុខ/ថយក្រោយ</string>\n  <string name=\"swipe_buttons_navigation\">ប្រើការកកិត និងប៊ូតុង</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">បន្ទាប់</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">ត្រឡប់ក្រោយ</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">ទម្រង់សម្រាប់ធ្វើបច្ចុប្បន្នភាព</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">រៀងរាល់ ១៥នាទី</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">រៀងរាល់ម៉ោង</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">រៀងរាល់ ៦ ម៉ោង</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">រៀងរាល់ ២៤ ម៉ោង</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">ទាញយក​ដោយ​ស្វ័យ​ប្រវត្តិ</string>\n  <string name=\"automatic_download_summary\">ធ្វើបច្ចុប្បន្នភាពជំនាន់ទម្រង់​ដោយទាញយក​​ដោយ​ស្វ័យ​ប្រវត្តិ</string>\n  <string name=\"form_submission_category\">ការដាក់ស្នើរទម្រង់</string>\n  <string name=\"autosend_selector_title\">ផ្ញើ​ចេញ​ដោយ​ស្វ័យ​ប្រវត្តិ</string>\n  <string name=\"autosend\">ផ្ញើចេញ​ដោយ​ស្វ័យ​ប្រវត្តិ</string>\n  <string name=\"off\">បិទ</string>\n  <string name=\"wifi_autosend\">តែ Wifi ប៉ុណ្ណោះ</string>\n  <string name=\"cellular_autosend\">តែ Cellular ប៉ុណ្ណោះ</string>\n  <string name=\"wifi_cellular_autosend\">Wifi ឬ Cellular</string>\n  <string name=\"delete_after_send\">លុបទម្រង់បន្ទាប់ការផ្ញើចេញ</string>\n  <string name=\"delete_after_send_summary\">លុប​សំណុំ​​បែប​​បទ​ដែល​បាន​បញ្ចប់ និង​មីឌៀ ​បន្ទាប់​ពី​ការ​ផ្ញើ​ចេញ​ទៅ​កាន់​ម៉ាស៊ីន​មេ</string>\n  <string name=\"form_filling_category\">ការបំពេញទម្រង់</string>\n  <string name=\"constraint_behavior_title\">ដំណើរការនៃការរឹតបន្តឹង</string>\n  <string name=\"constraint_behavior\">កំពុងដំណើរការឥរិយាបទឧបសគ្គ</string>\n  <string name=\"constraint_behavior_on_swipe\">ផ្តល់​សុពលភាព​នៅ​ពេលដែលអូស​ទៅ​មុខ</string>\n  <string name=\"constraint_behavior_on_finalize\">ពន្យា​សុពលភាព​រហូត​ដល់​បញ្ចប់</string>\n  <string name=\"high_resolution_title\">កំរិតច្បាស់វីដេអូ</string>\n  <string name=\"high_resolution_summary\">បើកការថតវីដេអូគុណភាពច្បាស់</string>\n  <string name=\"image_size_title\">ទំហំរូបថត</string>\n  <string name=\"image_size_dialog_title\">ភីកសែលអតិបរមានៃគែមវែងរបស់រូបភាព</string>\n  <string name=\"image_size_large\">ធំ (3072px)</string>\n  <string name=\"image_size_medium\">មធ្យម (2048px)</string>\n  <string name=\"image_size_small\">តូច (1024px)</string>\n  <string name=\"image_size_very_small\">តូចខ្លាំង (640px)</string>\n  <string name=\"image_size_original\">ទំហំដើមចេញពីកាមេរ៉ា (លំនាំដើម)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">បង្ហាញការណែនាំសម្រាប់សំណួរ</string>\n  <string name=\"guidance_no\">ទេ</string>\n  <string name=\"guidance_yes\">បាទ/ចាស - តែងតែបង្ហាញ</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">បាទ - បញ្ចូលគ្នា</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">នាំចូលទម្រង់</string>\n  <string name=\"instance_sync\">ទម្រង់បញ្ចប់ដែលនាំចូល</string>\n  <string name=\"instance_sync_summary\">ទម្រង់ដែលប៉ះពាល់ត្រូវបានបន្ថែមដោយផ្ទាល់ទៅកាន់ថតទម្រង់</string>\n  <string name=\"analytics_preferences\">ការប្រើប្រាស់ទិន្នន័យ</string>\n  <string name=\"analytics\">ប្រមូលទិន្នន័យប្រើប្រាស់អនាមិក</string>\n  <string name=\"form_metadata_title\">ទម្រង់ទិន្នន័យមេតា</string>\n  <string name=\"form_metadata\">ទម្រង់ទិន្នន័យមេតា</string>\n  <string name=\"form_metadata_summary\">តម្លៃទាំងនេះនឹងត្រូវបានបញ្ចូលបន្ថែមទៅក្នុងទម្រង់ដែលរួមមានឈ្មោះអ្នកប្រើប្រាស់, អ៊ីម៉ែល និង/ឬលេខទូរស័ព្ទដើម្បីកំណត់អ្នកដែលបានបញ្ជូនទិន្នន័យ។</string>\n  <string name=\"user_defined\">អ្នកប្រើប្រាស់កំណត់</string>\n  <string name=\"phone_number\">លេខទូរស័ព្ទ</string>\n  <string name=\"email\">អាសយដ្ឋានអ៊ីម៉ែល</string>\n  <string name=\"invalid_email_address\">អាសយដ្ឋាន​អ៊ីម៉េល​មិនត្រឹមត្រូវ!</string>\n  <string name=\"device_defined\">ឧបករណ៍កំណត់</string>\n  <string name=\"device_id\">ID របស់ឧបករណ៍</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">សូមបំពេញពាក្យសម្ងាត់ថ្មី</string>\n  <string name=\"admin_password\">ពាក្យសម្ងាត់ Admin</string>\n  <string name=\"admin_password_changed\">ពាក្យសម្ងាត់របស់ Admin បានផ្លាស់ប្តូរដោយជោគជ័យ</string>\n  <string name=\"admin_password_disabled\">ពាក្យសម្ងាត់របស់ Admin មិនដំណើការ</string>\n  <string name=\"admin_password_incorrect\">សោកស្តាយផង, ពាក្យសម្ងាត់នេះមិនត្រឹមត្រូវ!</string>\n  <string name=\"enter_admin_password\">បញ្ចូលពាក្យសម្ងាត់របស់ Admin</string>\n  <string name=\"change_admin_password\">ផ្លាស់​ប្តូរ​ពាក្យ​សម្ងាត់​ Admin</string>\n  <string name=\"show_password\">បង្ហាញ​ពាក្យសម្ងាត់</string>\n  <string name=\"found_in_main\">ដោះ​ធីក​ដើម្បី​លាក់ចេញ​ពី​ម៉ឺនុយមេ</string>\n  <string name=\"form_entry\">ដោះ​ធីក​ដើម្បី​លាក់ពី​ទម្រង់​បញ្ចូល</string>\n  <string name=\"reset_project_settings_title\">កំណត់ឡើងវិញ</string>\n  <string name=\"delete_project\">លុប</string>\n  <string name=\"select_all\">ជ្រើសរើសទាំងអស់</string>\n  <string name=\"clear_all\">លុបចេញទាំងអស់</string>\n  <string name=\"reset_settings\">ការកំណត់ទាំងអស់ (ការកំណត់ខាងក្នុង, ការកំណត់ដែលរក្សាទុក)</string>\n  <string name=\"reset_saved_forms\">រក្សាទម្រង់ (ថតករណី, មូលដ្ឋានទិន្នន័យករណី)</string>\n  <string name=\"reset_blank_forms\">ទម្រង់ទទេរ (ថតទម្រង់, មូលដ្ឋានទិន្នន័យទម្រង់, មូលដ្ឋានទិន្នន័យ itemsets)</string>\n  <string name=\"reset_layers\">ស្រទាប់ផែនទី (ថតស្រទាប់)</string>\n  <string name=\"reset_cache\">ទម្រង់ផ្ទុកខាឆឹះ (.ថតខាឆឹះ)</string>\n  <string name=\"reset_settings_dialog_title\">ជ្រើសរើសអ្វីដែលត្រូវកំណត់ឡើងវិញ</string>\n  <string name=\"reset_settings_button_reset\">កំណត់ឡើងវិញ</string>\n  <string name=\"reset_in_progress\">កំពុងកំណត់ឡើងវិញ</string>\n  <string name=\"reset_app_state_result\">កំណត់លទ្ធផលឡើងវិញ</string>\n  <string name=\"reset_app_state_warning\">ទិន្នន័យដែល​បានជ្រើសរើសទាំងអស់នឹងត្រូវលុប។ ទាំងនេះមិនធ្វើវិញបានទេ។</string>\n  <string name=\"reset_settings_result\">ការកំណត់ទាំងអស់ :: %s</string>\n  <string name=\"reset_saved_forms_result\">សំណុំបែបបទដែល​​បាន​រក្សា​ទុក :: %s</string>\n  <string name=\"reset_blank_forms_result\">សំណុំបែបបទទទេ :: %s</string>\n  <string name=\"reset_layers_result\">ស្រទាប់ផែនទី :: %s</string>\n  <string name=\"reset_cache_result\">ទម្រង់ផ្ទុកខាឆឹះ :: %s</string>\n  <string name=\"main_menu_settings\">ការកំណត់ផ្ទាំងមេមីនុយ</string>\n  <string name=\"user_settings\">ការកំណត់របស់អ្នកប្រើប្រាស់</string>\n  <string name=\"form_entry_setting\">ការ​កំណត់​នៃ​ទម្រង់​បញ្ជូល</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">កំពុងរំកិលថយក្រោយ</string>\n  <string name=\"moving_backwards_disabled_title\">ការរំកិលថយក្រោយមិនបានអនុញ្ញាតិ</string>\n  <string name=\"yes\">បាទ/ចាស</string>\n  <string name=\"no\">ទេ</string>\n  <string name=\"moving_backwards_enabled_title\">អាច​ដំណើរការ​ត្រឡប់​ថយ​ក្រោយ​បាន</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">រក្សាទុកជាសេចក្តីព្រាង</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">អំពី</string>\n  <string name=\"tell_your_friends\">ចែក​រំលែក​ ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">តើ​សហការី​របស់​អ្នក​នៅ​តែ​ប្រមូល​ទិន្នន័យ​នៅ​លើ​ក្រដាស​មែន​ទេ? ចែក​រំលែក ODK Collect ជាមួយពួកគេ។</string>\n  <string name=\"leave_a_review\">ទុកមតិនៅក្នុងហាង Play</string>\n  <string name=\"leave_a_review_msg\">ការ​ផ្តល់​មតិ​ (សង្ឃឹមថាជាវិជ្ជមាន) របស់​អ្នក​បង្កើន​ភាព​មើល​ឃើញ​នៃ​កម្មវិធី​នៅ​ក្នុង Play Store ។</string>\n  <string name=\"odk_website\">ចូលទៅកាន់គេហទំព័រ ODK</string>\n  <string name=\"odk_website_summary\">ODK បាន​ប្រើ​ប្រាស់​ដើម្បី​ប្រមូល​ទិន្នន័យសម្រាប់ភាពប្រសើរ​របស់​សង្គមនៅក្នុងបរិយាកាស​ប្រកួត​ប្រជែង​។</string>\n  <string name=\"odk_forum\">ចូល​រួម​ជា​មួយ​វេទិកា ODK</string>\n  <string name=\"odk_forum_summary\">ចូលរួមវេទិកាដើម្បីទទួលបានការគាំទ្រ, លក្ខណៈពិសេសស្នើសុំ, រួមចំណែកកូដ / ការបកប្រែ!</string>\n  <string name=\"all_open_source_licenses\">ប្រភពបណ្ណាល័យបើកចំហ/អាជ្ញាបណ្ណ</string>\n  <string name=\"all_open_source_licenses_msg\">We stand on the shoulders of giants!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"successfully_imported_settings\">បាន​នាំ​ចូល​ការ​កំណត់​ដោយ​ជោគ​ជ័យ</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">តួអក្សរ​ដល់​ដែន​កំណត់​អតិបរមា​: លេខកូដ​ QR ​មិន​អាច​បង្កើត​តាម​ការកំណត់​ទាំងអស់​</string>\n  <string name=\"share\">ចែករំលែក</string>\n  <string name=\"turn_on_flashlight\">បើក​ភ្លើងពិល</string>\n  <string name=\"turn_off_flashlight\">បិទភ្លើងពិល</string>\n  <string name=\"server_password\">ពាក្យសម្ងាត់​ម៉ាស៊ីន​មេ</string>\n  <string name=\"include_password_dialog\">ពាក្យ​សម្ងាត់​មានភ្ជាប់នៅក្នុង​កូដ</string>\n  <string name=\"generate\">បង្កើត</string>\n  <string name=\"invalid_qrcode\">លេខកូដ QR មិនមានការកំណត់ត្រឹមត្រូវ</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">យល់ព្រម</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">បោះបង់</string>\n  <string name=\"cancel_loading_form\">បោះបង់</string>\n  <string name=\"cancel_location\">បោះបង់</string>\n  <string name=\"do_not_change\">បោះបង់</string>\n  <string name=\"canceling\">កំពុង​បោះបង់</string>\n  <string name=\"notification_channel_name\">ការជូនដំណឹង​ជាទូទៅ</string>\n  <string name=\"success\">ជោគជ័យ</string>\n  <string name=\"error_occured\">មានបញ្ហាបានកើតឡើង</string>\n  <string name=\"please_wait\">សូមរង់ចាំមួយភ្លែត</string>\n  <string name=\"please_wait_long\">សូមរងចាំ. វាអាចចំណាយរយះពេលពីរបីនាទី.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">មិនអាចលុប \\'%s\\' ។ សូម​លុប​ឯកសារ​ដោយ​ដៃ​ហើយ​ធ្វើ​ការ​ទាញ​យក​សំណុំ​បែបបទ​ឡើង​វិញ។</string>\n  <string name=\"fs_create_media_folder_error\">មិនអាចបង្កើតថតមេឌៀបាន \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">មិនអាចចម្លងបាន \\'%1$s\\' លើសពី \\'%2$s\\' ។ មូលហេតុ: %3$s</string>\n  <string name=\"select_odk_shortcut\">ជ្រើសរើសផ្លូវកាត់ ODK</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">បន្ថែម</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">មិនស្គាល់ URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">URI អាក្រក់: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">អ្នក​បាន​ទាញ​យក​សំណុំ​បែប​បទ​ពីរខុស​គ្នា​ដែល​មាន​លេខ​សម្គាល់​ទម្រង់​ និង​កំណែ។ ពួក​វា​ប្រហែល​ជា​សំណុំ​បែប​បទ​តែ​មួយ​ដែល​បាន​ផ្ទុក​ឡើង​នៅ​ម៉ោង​ ឬ​ម៉ាស៊ីន​មេ​ខុស​គ្នា។ សូម​អ្នក​លុប​វាមួយ​ក្នុង​​ករណី​ណា​មួយ។</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">បោះបង់</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ln/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">balola lokoto</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"clearanswer_confirm\">%slongola eyano oyo</string>\n  <string name=\"clear_answer_no\">longola</string>\n  <string name=\"delete_repeat_no\">longola</string>\n  <!--Title of the end of form screen-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"capture_osm\">fungola OpenMapKit</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">kanga elili</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"play_video\">beta video</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"delete_no\">kolongola te</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Label for server type setting-->\n  <string name=\"server_url\">Ekangele</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"username\">Kombo ya esalelo</string>\n  <string name=\"change_username\">ODK kombo ya bosaleli</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"show_password\">lakisa kombo ebombami</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">longola</string>\n  <string name=\"cancel_loading_form\">longola</string>\n  <string name=\"cancel_location\">longola</string>\n  <string name=\"do_not_change\">longola</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-lo-rLA/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'ເພີ່ມ​ເມື່ອ​\\' EEE, MMM dd, yyyy \\'ເວ​ລາ\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'​ບັນ​ທຶກ​ເມື່ອ​\\' EEE, MMM dd, yyyy \\'ເວລາ\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'ສຳ​ເລັດ​ເມື່ອ​\\' EEE, MMM dd, yyyy \\'ເວ​ລາ\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'ສົ່ງເມື່ອ​\\' EEE, MMM dd, yyyy \\'ເວລາ\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'ການສົ່ງຂໍ້ມູນລົ້ມເຫຼວເມື່ອ\\' EEE, MMM dd, yyyy \\'ເວລາ\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'ດັດແກ້ວັນທີ\\' EEE, MMM dd, yyyy \\'ເວລາ\\' HH:mm</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">ສີ້ນສຸດການຄົ້ນຫາ. ແບບຟອມທັງໝົດໄດ້​ຖືກຕິດຕັ້ງແລ້ວ</string>\n  <string name=\"loading_form\">ກຳລັງເປີດແບບຟອມ</string>\n  <string name=\"survey_loading_reading_form_message\">​ກຳ​ລັງ​ອ່ານ​​ຄຳ​ນິ​ຍາມແບບ​ຟອມ…</string>\n  <string name=\"survey_loading_reading_data_message\">ກຳ​ລັງ​ອ່ານ​ຂໍ້​ມູນ​ການ​ສຳ​ຫຼວດ…</string>\n  <string name=\"survey_loading_reading_csv_message\">ກຳ​ລັງ​ອ່ານ​ເອ​ກະ​ສານ CSV…</string>\n  <string name=\"parse_error\">ບໍ່ສາມາດເປີດແບບຟອມໄດ້</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">ກູ້ຂໍ້ມູນຄືນຈາກຈຸດທີ່ບັນທຶກໄວ້!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">ເລືອກພາສາ</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">ຂ້າມໄປທີ່</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">ໄປຫາໜ້າທຳອິດຕົ້ນ</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">ໄປຫາໜ້າສຸດທ້າຍ</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">ເລື່ອນຂື້ນ</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">ແກ້ໄຂຈຸດທີ່ເລືອກ</string>\n  <string name=\"clearanswer_confirm\">ຕ້ອງການລຶບຄຳຕອບໄປທີ່ \\\"%s\\\" ບໍ່?</string>\n  <string name=\"clear_answer\">ລຶບຄຳຕອບ</string>\n  <string name=\"clear_answer_ask\">ຕ້ອງການຍ້າຍຄຳຕອບນີ້ບໍ່?</string>\n  <string name=\"clear_answer_no\">ຍົກເລີກ</string>\n  <string name=\"discard_answer\">ລຶບຄຳຕອບ​</string>\n  <string name=\"discard_group\">​ລຶບກຸ່ມ</string>\n  <string name=\"delete_repeat\">ລຶບກຸ່ມ</string>\n  <string name=\"delete_repeat_ask\">ຕ້ອງການລຶບກຸ່ມນີ້ບໍ່?</string>\n  <string name=\"delete_repeat_confirm\">ຕ້ອງການລຶບກຸ່ມ\\\"%s\\\" ແລະ ກຸ່ມຍ່ອຍ​ທັງ​ໝົດ​ຂອງ​ມັນບໍ່?</string>\n  <string name=\"delete_repeat_no\">ຍົກເລີກ</string>\n  <string name=\"invalid_answer_error\">ຄຳຕອບນີ້ບໍ່ຖືກຕ້ອງ</string>\n  <string name=\"required_answer_error\">ກະລຸນາຕື່ມຄຳຕອບໃສ່ຄຳຖາມນີ້ກ່ອນ</string>\n  <string name=\"survey_internal_error\">​ຜິດ​ພາດ​ພາຍ​ໃນ: ຂັ້ນ​ຕອນ​ການ​ເຕືອນ​ລົ້ມ​ເຫຼວ.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">​ທ່ານຢູ່ຈຸດສີ້ນສຸດຂອງ %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">ການບັກທຶກແບບຟອມລົ້ມເຫຼວ</string>\n  <string name=\"data_saved_ok\">ແບບຟອມໄດ້ຖືກບັນທຶກສຳເລັດແລ້ວ</string>\n  <string name=\"save_point_error\">ພໍ້​ບັນ​ຫາ​ໃນ​ລະ​ຫວ່າງບັນ​ທຶກຈຸດ​​ການ​ກູ້​ຄືນ: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">ບັນທຶກການປ່ຽນແປງ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">ກຳລັງບັນທຶກແບບຟອມ</string>\n  <string name=\"survey_saving_validating_message\">ກຳ​ລັງ​ກວດ​ກາ​ຄວາມ​ຖືກ​ຕ້ອງ​ຂອງ​ຄຳ​ຕອບ….</string>\n  <string name=\"survey_saving_collecting_message\">ກຳ​ລັງ​ຮວບ​ຮວມຂໍ້​ມູນ…</string>\n  <string name=\"survey_saving_saving_message\">​ກຳ​ລັງ​ບັນ​ທຶກ​ໃສ່ ກາດ​ຄວມຈຳ​ພາຍນອກ…</string>\n  <string name=\"survey_saving_finalizing_message\">ກຳ​ລັງ​ເຮັດ​ໃຫ້ ກາດ​ຄວາມ​ຈຳ​ພາຍນອກ ສຳ​ເລັດ​ສົມ​ບູນ…</string>\n  <string name=\"survey_saving_encrypting_message\">​ກຳ​ລັງ​ເຂົ້າ​ລະ​ຫັດ​ຂໍ້​ມູນ…</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">ເລີ້ມຕົ້ນການນຳໃຊ້</string>\n  <string name=\"activity_not_found\">ບໍ່ມີກິດຈະກຳໃຫ້ດຳເນີນການ: %s</string>\n  <string name=\"no_app\">ບໍ່ພົບ​ໂປ​ແກມ​ທີ່​ຕ້ອງ​ການ ກະລຸນາປ້ອນຂໍ້ມູນໃໝ່ດ້ວຍຕົນເອງ</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">ເປີດ​ທີ່​ຢູ່​ເວັ​ບ​ໄຊ Url</string>\n  <string name=\"get_barcode\">ສະແກນຫາບາໂຄດ</string>\n  <string name=\"replace_barcode\">ປ່ຽນບາໂຄດໃໝ່</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">ຖ່າຍຮູບ</string>\n  <string name=\"choose_image\">ເລືອກຮູບພາບ</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">ບັນທຶກ ແລະ ປິດການໃຊ້ງານ</string>\n  <string name=\"sign_button\">ບັນທຶກລາຍເຊັນ</string>\n  <string name=\"markup_image\">Markup Image</string>\n  <string name=\"draw_image\">ແຕ້ມຮູບພາບແບບຄ້າວໆ</string>\n  <string name=\"reset_image\">ຕັ້ງຄ່າຄືນໃໝ່</string>\n  <string name=\"set_color\">ເລືອກສີ</string>\n  <string name=\"capture_video\">ບັນທຶກວີດີໂອ</string>\n  <string name=\"choose_video\">ເລືອກວີດີໂອ</string>\n  <string name=\"play_video\">ເປີດວີດີໂອ</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">ບັນທຶກສຽງ</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">ເລືອກຮູບແບບສຽງ</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">​ມີ​ບັນ​ຫາ​ເກີດ​ຂື້ນ​ໃນ​ລະ​ຫວ່າງ​ການ​ຄັດຕິດ​​ເອ​ກະ​ສານ​ມີ​ເດຍ: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">ເລືອກຄຳຕອບ</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">ໄດ້ເລືອກແລ້ວ:</string>\n  <string name=\"trigger\">ຕົກລົງ. ກະລຸນາດຳເນີນຕໍ່</string>\n  <string name=\"ext_file_no_data_error\">​ບໍ່​ມີ​ຂໍ້​ມູນ​ໃນ​ເອ​ກະ​ສານ!</string>\n  <string name=\"ext_conflicting_columns_error\">ຈຳ​ນວນ %s ຖັນ​ທີ່​ກົງ​ກັນ!</string>\n  <string name=\"ext_sortBy_numeric_error\">ຖັນ​ທີ່​ທ່ານ​ຕ້ອງ​ການ​ຈັດ​ລຽງ ຄວ​ນ​ແມີ​ແຕ່​ໂຕ​ເລກ. ຄ່າ​ທີ່​ຂັດ​ແຍ່ງກັນ​ແມ່ນ \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">​ໄວ​ຍະ​ກອນ​ຜິດ​ພາດ​ໃນສູດ​ search(): ສູດ​ຕ້ອງ​ໃຊ້ 1, 4 ຫຼື 6 ຖ້ອຍ​ຄວາມ.</string>\n  <string name=\"ext_search_wrong_function_error\">ໄວ​ຍະ​ກອນ​ຜິດ​ພາດ​ໃນສູດ​ search(): ບໍ່​ຮັບ​ຮູ້​ສູດ​ດັ່ງ​ກ່າວ \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">ໄວ​ຍະ​ກອນ​ຜິດ​ພາດ​ໃນສູດ​ search():  \\'%s\\' ບໍ່​ສາ​ມາດ​ປະ​ເມີນ​ເປັນ​ສູດ​ໄດ້.</string>\n  <string name=\"ext_search_return_error\">ໂຕ​ຄວບ​ຄຸມການ​ຊອກ​ຫາ ສົ່ງ​ປະ​ເພດ​ຂອງ \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">ບໍ່​ສາ​ມາດ​ເອົາ​ຂໍ້​ມູນ​ເຂົ້າ​ຈາກ %1$s. ເຫດ​ຜົນ: %2$s</string>\n  <string name=\"ext_import_progress_message\">ຂໍ້​ມູນ​ກ່ອນ​ໂຫຼດ ຈາກ \\'%1$s\\', ກະ​ລຸ​ນາ​ລໍ​ຖ້າ… %2$s</string>\n  <string name=\"ext_import_finalizing_message\">ກຳ​ລັງ​ເຮັດ​ຂໍ້​ມູນ​ກ່ອນ​ໂຫຼດ ສຳ​ເລັດ​ສົມ​ບູນ…</string>\n  <string name=\"ext_import_completed_message\">​ການ​ອ່ານ​ຂໍ້​ມູນ​ສຳ​ເລັດ!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager ຍັງ​ບໍ່​ທັນ​ເລີ້ມ​ຕົ້ນ.</string>\n  <string name=\"ext_import_csv_missing_error\">ຂໍ້​ມູນ​ພາຍນອກສຳ​ລັບ %1$s ຍັງ​ບໍ່​ທັນ​ຖືກ​ນຳ​ເຂົ້າ. ບັງ​ເທື່ອທ່ານ​ລືມ​ເອົາ​ເອ​ກະ​ສານ  %2$s.csv ປະ​ກອບກັບ​ແບບ​ຟອມ​ຂອງ​ທ່ານ?</string>\n  <string name=\"ext_search_generic_error\">ໄວ​ຍະ​ກອນ​ຜິດ​ພາດ​ໃນສູດ​ search():  \\'%s\\' </string>\n  <string name=\"ext_assign_value_error\">ບໍ່​ສາ​ມາດກຳ​ນົດ​ຄ່າ​ທີ່ \\'%s\\'.</string>\n  <string name=\"get_bearing\">ບັກທືກທິດທາງ</string>\n  <string name=\"replace_bearing\">ປ່ຽນທິດທາງ</string>\n  <string name=\"getting_bearing\">ກຳລັງສະແດງທິດທາງ</string>\n  <string name=\"accept_bearing\">ບັກທືກທິດທາງ</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">ຟາຍເອ​ກະ​ສານ: %s ບໍ່ຖືກຕ້ອງ</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">ບໍ່​ພົບຟາຍ​ເອ​ກະ​ສານ %s </string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">ຄຳສັ່ງບັນທຶກຈຸດທີ່ຕັ້ງ GPS ຖືກປິດໃຫ້ໃຊ້ງານ</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">ກຳລັງສົ່ງແບບຟອມ</string>\n  <string name=\"noselect_error\">ທ່ານບໍ່ທັນ​ໄດ້ເລືອກແບບຟອມ!</string>\n  <string name=\"no_connection\">ບໍ່ມີເຄືອຂ່າຍໃຫ້ເຊື່ອມຕໍ່</string>\n  <string name=\"sending_items\">ກຳລັງສົ່ງ %1$s ຈາກແບບຟອມທັງໝົດ %2$s </string>\n  <string name=\"send_selected_data\">ສົ່ງແບບຟອມທີ່ເລືອກ</string>\n  <string name=\"server_requires_auth\">ເຊີເວີຕ້ອງການກວດ​ສອບ​ຊື່ຜູ້ໃຊ້ງານ ແລະ ລະ​ຫັດ​ຜ່ານ</string>\n  <string name=\"no_forms_uploaded\">ບໍ່ສາມາດ ອັບໂລດ ແບບຟອມໄດ້</string>\n  <string name=\"upload_results\">ຜົນໄດ້ຮັບ​ຂອງ​ການ​ ອັບໂລດ</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">ປ່ຽນຮູບ​ແບບ​ການ​​ເບິ່ງ</string>\n  <string name=\"show_sent_and_unsent_forms\">ສະແດງແບບຟອມທີ່ສົ່ງແລ້ວ ແລະ ຍັງບໍ່ໄດ້ສົ່ງ\n</string>\n  <string name=\"show_unsent_forms\">ສະແດງແບບຟອມທີ່ຍັງບໍ່ທັນໄດ້ສົ່ງ</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">ຣີເຟຼສ (Refresh)</string>\n  <string name=\"download\">​ດາວໂລດເອົາຟາຍທີ່ເລືອກ</string>\n  <string name=\"downloading_data\">ກຳລັງເຊື່ອມຕໍ່ກັບເຊີເວີ</string>\n  <string name=\"fetching_file\">ໄປຮັບ %1$s.\\n\\nຟອມ %2$s ຂອງ %3$s ຟອມ(s).</string>\n  <string name=\"form_download_progress\">%1$s. Getting media files: %2$s of %3$s</string>\n  <string name=\"version\">​ລຸ້ນ​ທີ:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">ແບບຟອມທີ່ບັນທຶກໄວ້</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">ແບບຟອມປ້ອນຂໍ້ມູນໃໝ່</string>\n  <string name=\"delete_confirm\">ຕ້ອງການລຶບແບບຟອມ %s ບໍ່?</string>\n  <string name=\"delete_file\">ລຶບສະເພາະແບບຟອມທີ່ໄດ້ເລືອກ</string>\n  <string name=\"delete_no\">ບໍ່ຕ້ອງການລຶບ</string>\n  <string name=\"delete_yes\">ລຶບແບບຟອມ</string>\n  <string name=\"file_deleted_ok\">ລຶບແບບຟອມ%s ສຳເລັດແລ້ວ!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">​ການ​ຕັ້ງ​ຄ່າ​ເຊີ​ເວີ</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL ທີ່​ຢູ່​ເວັບ​ໄຊ</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL ຂອງເສີບເວີ</string>\n  <string name=\"url_error\">ທີ່​ຢູ່​ເວັບ​ໄຊ ຫຼື URL ບໍ່ຖືກຕ້ອງ</string>\n  <string name=\"username\">ບັນຊີຜູ້ໃຊ້ງານ</string>\n  <string name=\"change_username\">ບັນຊີຜູ້ໃຊ້ ODK</string>\n  <string name=\"password\">ລະຫັດຜ່ານ</string>\n  <string name=\"change_password\">ລະຫັດຜ່ານ ODK</string>\n  <string name=\"font_size\">ຂະໜາດຕົວອັກສອນ</string>\n  <string name=\"change_font_size\">ຂະໜາດຕົວອັກສອນ</string>\n  <string name=\"font_size_extra_large\">ຂະໜາດໃຫຍ່ທີ່ສຸດ</string>\n  <string name=\"font_size_large\">ຂະໜາດໃຫຍ່</string>\n  <string name=\"font_size_medium\">ຂະໜາດປານກາງ</string>\n  <string name=\"font_size_small\">ຂະໜາດນ້ອຍ</string>\n  <string name=\"font_size_extra_small\">ຂະໜາດນ້ອຍທີ່ສຸດ</string>\n  <string name=\"navigation\">ຄົ້ນຫາ</string>\n  <string name=\"swipe_navigation\">ໃຊ້ການເລື່ອນໜ້າຈໍໄປທາງຂ້າງ</string>\n  <string name=\"buttons_navigation\">ກົດປູ່ມເລື່ອນໄປທາງໜ້າ/ກັບຫຼັງ</string>\n  <string name=\"swipe_buttons_navigation\">​ໃຊ້ເລື່ອນໜ້າຈໍ ແລະ ປຸ່ມ</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">ເລື່ອນຕໍ່ໄປ</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"delete_after_send\">ລຶບພາຍຫຼັງສົ່ງແລ້ວ</string>\n  <string name=\"delete_after_send_summary\">ລຶບຟອມທີ່ສຳເລັດແລ້ວ ແລະ ສື່ ຫຼັງຈາກສົ່ງຫາເສີບເວີ</string>\n  <string name=\"constraint_behavior\">ລັກສະນະການປະເມີນຜົນແບບມີເງື່ອນໄຂ</string>\n  <string name=\"constraint_behavior_on_swipe\">ການກວດ​ສອບ​ເມື່ອປັດເລື່ອນໄປທາງໜ້າ</string>\n  <string name=\"constraint_behavior_on_finalize\">ເລື່ອນການກວດສອບໄປຈົນກວ່າການສຳຫຼວດສຳເລັດ</string>\n  <string name=\"high_resolution_summary\">ເປີດ​ການ​ນຳ​ໃຊ້ ການ​ບັນ​ທຶກວີ​ດີ​ໂອ​ແບບ​ຄວາມ​ລະ​ອຽດ​ສູງ</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">​ປ້ອນລະຫັດຜ່ານໃໝ່</string>\n  <string name=\"admin_password_changed\">ລະຫັດຜ່ານຂອງຜູ້ຄວບຄຸມລະບົບ​ໄດ້​ປ່ຽນສຳເລັດແລ້ວ</string>\n  <string name=\"admin_password_disabled\">ລະ​ຫັດ​ຜ່ານ​ຂອງ​ຜູ້​ຄວບ​ຄຸມ​ລະ​ບົບ​ບໍ່​ໄດ້​ເປີດ​ນຳ​ໃຊ້</string>\n  <string name=\"admin_password_incorrect\">ລະຫັດຜ່ານບໍ່ຖືກຕ້ອງ!</string>\n  <string name=\"enter_admin_password\">ປ້ອນລະຫັດຜ່ານຜູ້ຄວບຄຸມລະບົບ</string>\n  <string name=\"found_in_main\">ຍົກເລີກການ​ເລືອກ​ ເພື່ອເຊື່ອງຈາກເມນູຫລັກ</string>\n  <string name=\"reset_project_settings_title\">ຕັ້ງຄ່າຄືນໃໝ່</string>\n  <string name=\"reset_settings_button_reset\">ຕັ້ງຄ່າຄືນໃໝ່</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">ຕົກລົງ</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">ຍົກເລິກ</string>\n  <string name=\"cancel_loading_form\">ຍົກເລີກ</string>\n  <string name=\"cancel_location\">ຍົກເລີກ</string>\n  <string name=\"do_not_change\">ຍົກເລີກ</string>\n  <string name=\"success\">ສຳເລັດແລ້ວ</string>\n  <string name=\"error_occured\">ມີຄວາມຜິດພາດເກີດຂຶ້ນ</string>\n  <string name=\"please_wait\">ກະລຸນາລໍຖ້າຈັກໜ້ອຍ</string>\n  <string name=\"please_wait_long\">ກະລຸນາລໍຖ້າ. ມັນອາດຕ້ອງໃຊ້ເວລາໜ້ອຍໜື່ງ</string>\n  <string name=\"fs_delete_media_path_if_file_error\">ບໍ່​ສາ​ມາດ​ລຶບ \\'%s\\'. ກະ​ລຸ​ນາ​ລຶບ​ເອ​ກະ​ສານດ້ວຍ​ມື ແລະ ດາວ​ໂຫຼດ​ຟອມ​ມາ​ໃໝ່.</string>\n  <string name=\"fs_create_media_folder_error\">ບໍ່​ສາ​ມາດ​ສ້າງ​ໂຟນ​ເດີ \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">ບໍ່​ສາ​ມາດ​ກ໋ອບ​ປີ້ \\'%1$s\\' ຈາກ​ທັງ​ໝົດ \\'%2$s\\'. ​ເຫດຜົນ: %3$s</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">ທ່ານ​ໄດ້​ດາວ​ໂຫຼດ​ສອງ​ແບບ​ຟອມ ທີ່​ມີ ID ແລະ ເວີ​ຊັນ ຄື​ກັນ. ບາງ​ເທື່ອ​ຟອ​ມ​ດັ່ງ​ກ່າວ​ເປັນ​ຟອມ​ດຽວ​ກັນ ແຕ່​ອັບ​ໂຫຼດ​ເວ​ລາ​ຕ່າງ​ກັນ ຫຼື ຫາເສີບ​ເວີ​ຕ່າງ​ກັນ. ຢ່າງ​ໃດ​ກໍ່​ຕາມ ທ່ານ​ຄວນ​ລຶບ​ຟອມ​ໜຶ່ງ​ຖີ້ມ.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-lt/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Sukurta\\' EEE, MMM dd, yyyy HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Išsaugota\\' EEE, MMM dd, yyyy HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Užbaigta\\' EEE, MMM dd, yyyy HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Išsiųsta\\' EEE, MMM dd, yyyy HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Išsiųsti nepavyko\\' EEE, MMM dd, yyyy HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Skenavimas baigtas. Visos formos įkeltos.</string>\n  <string name=\"loading_form\">Įkeliama forma</string>\n  <string name=\"parse_error\">Deja, nepavyko apdoroti formos.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Neišsaugoti pakeitimai atkurti pagal išsaugojimo tašką!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Pakeisti kalbą</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Atsakymų sąrašas</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Eiti į pradžią</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Eiti į pabaigą</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Eiti į viršų</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Keisti atsakymą</string>\n  <string name=\"clearanswer_confirm\">Pašalinti atsakymą į \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Pašalinti atsakymą</string>\n  <string name=\"clear_answer_ask\">Pašalinti šį atsakymą?</string>\n  <string name=\"clear_answer_no\">Atšaukti</string>\n  <string name=\"discard_answer\">Pašalinti atsakymą</string>\n  <string name=\"discard_group\">Pašalinti grupę</string>\n  <string name=\"delete_repeat\">Pašalinti grupę</string>\n  <string name=\"delete_repeat_ask\">Pašalinti šią grupę?</string>\n  <string name=\"delete_repeat_confirm\">Pašalinti grupę \\\"%s\\\" ir visus jos pogrupius?</string>\n  <string name=\"delete_repeat_no\">Atšaukti</string>\n  <string name=\"invalid_answer_error\">Deja, šis atsakymas yra netinkamas!</string>\n  <string name=\"required_answer_error\">Atsakymas į šį klausimą yra privalomas!</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Jūs esate \\\"%s\\\" pabaigoje.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Deja, formos išsaugoti nepavyko!</string>\n  <string name=\"data_saved_ok\">Forma sėkmingai išsaugota!</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Išsaugoti pakeitimus</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Forma išsaugoma</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Paleisti</string>\n  <string name=\"activity_not_found\">Nerasta, kas galėtų valdyti: %s</string>\n  <string name=\"no_app\">Nerasta nurodyta programa. Atskaitą įveskite rankiniu būdu.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"get_barcode\">Nuskaityti brūkšninį kodą</string>\n  <string name=\"replace_barcode\">Pakeisti brūkšninį kodą</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Užfiksuoti kadrą</string>\n  <string name=\"choose_image\">Pasirinkite paveikslėlį</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Išsaugoti ir uždaryti</string>\n  <string name=\"sign_button\">Paimti parašą</string>\n  <string name=\"markup_image\">Anotuoti paveikslėlį</string>\n  <string name=\"draw_image\">Braižyti</string>\n  <string name=\"reset_image\">Atstatyti</string>\n  <string name=\"set_color\">Nustatyti spalvą</string>\n  <string name=\"capture_video\">Įrašyti video</string>\n  <string name=\"choose_video\">Pasirinkite video</string>\n  <string name=\"play_video\">Groti video</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Įrašyti garsą</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Pasirinkite garsą</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Pažymėkite atsakymą</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Parinkta:</string>\n  <string name=\"trigger\">Gerai. Prašome tęsti.</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Failas: %s yra netinkamas.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Failas: %s nėra.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Deja, padėties valdikliai yra išjungti!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Siunčiama forma</string>\n  <string name=\"noselect_error\">Deja, Jūs nepažymėjote jokių formų!</string>\n  <string name=\"no_connection\">Nėra interneto ryšio</string>\n  <string name=\"sending_items\">Siunčiama %1$s iš %2$s formų</string>\n  <string name=\"send_selected_data\">Išsiųsti pažymėtas</string>\n  <string name=\"server_requires_auth\">Serveris reikalauja autentifikavimo</string>\n  <string name=\"no_forms_uploaded\">Deja, formos nepavyko išsiųsti.</string>\n  <string name=\"upload_results\">Išsiuntimo rezultatai</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Keisti vaizdą</string>\n  <string name=\"show_sent_and_unsent_forms\">Išsiųstos ir neišsiųstos formos</string>\n  <string name=\"show_unsent_forms\">Tik neišsiųstos formos</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Atnaujinti</string>\n  <string name=\"download\">Atsiųsti parinktas</string>\n  <string name=\"downloading_data\">Jungiamasi prie serverio</string>\n  <string name=\"form_download_progress\">%1$s. Siunčiami medijos failai: %2$s iš %3$s</string>\n  <string name=\"version\">Versija:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Išsaugotos formos</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Tuščios formos</string>\n  <string name=\"delete_confirm\">Ištrinti %s formą(-as)?</string>\n  <string name=\"delete_file\">Ištrinti pažymėtas</string>\n  <string name=\"delete_no\">Neištrinti</string>\n  <string name=\"delete_yes\">Ištrinti formas</string>\n  <string name=\"file_deleted_ok\">%s forma(-os) sėkmingai ištrintos!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Serveris</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Serveris</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Serverio URL</string>\n  <string name=\"url_error\">Netinkamas URL!</string>\n  <string name=\"username\">Naudotojo vardas</string>\n  <string name=\"change_username\">ODK naudotojo vardas</string>\n  <string name=\"password\">Slaptažodis</string>\n  <string name=\"change_password\">Slaptažodis</string>\n  <string name=\"font_size\">Teksto šrifto dydis</string>\n  <string name=\"change_font_size\">Teksto šrifto dydis</string>\n  <string name=\"font_size_extra_large\">Labai didelis</string>\n  <string name=\"font_size_large\">Didelis</string>\n  <string name=\"font_size_medium\">Vidutinis</string>\n  <string name=\"font_size_small\">Mažas</string>\n  <string name=\"font_size_extra_small\">Labai mažas</string>\n  <string name=\"navigation\">Navigation</string>\n  <string name=\"swipe_navigation\">Use horizontal swipes</string>\n  <string name=\"buttons_navigation\">Use forward/backward buttons</string>\n  <string name=\"swipe_buttons_navigation\">Use swipes and buttons</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Next</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Įveskite naują slaptažodį</string>\n  <string name=\"admin_password_changed\">Administratoriaus slaptažodis sėkmingai pakeistas</string>\n  <string name=\"admin_password_disabled\">Administratoriaus slaptažodis išjungtas</string>\n  <string name=\"admin_password_incorrect\">Neteisingas slaptažodis!</string>\n  <string name=\"enter_admin_password\">Įveskite administratoriaus slaptažodį</string>\n  <string name=\"found_in_main\">Uncheck to hide from Main Menu</string>\n  <string name=\"reset_project_settings_title\">Atstatyti</string>\n  <string name=\"reset_settings_button_reset\">Atstatyti</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">Gerai</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Atšaukti</string>\n  <string name=\"cancel_loading_form\">Atšaukti</string>\n  <string name=\"cancel_location\">Atšaukti</string>\n  <string name=\"do_not_change\">Atšaukti</string>\n  <string name=\"success\">Sėkmingai</string>\n  <string name=\"error_occured\">Įvyko klaida</string>\n  <string name=\"please_wait\">Prašome šiek tiek palaukti.</string>\n  <string name=\"please_wait_long\">Prašome palaukti. Tai gali užtrukti kelias minutes.</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-mg/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <!--Title of the end of form screen-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Maka sary</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Label for server type setting-->\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_password\">Teny miafina ODK</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ml/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_form\">ഫോം ഡിലീറ്റ് ചെയ്യുക </string>\n  <string name=\"sort_the_list\">പട്ടിക ക്രമീകരിക്കുക</string>\n  <string name=\"sort_by_name_asc\">പേര്, A - Z </string>\n  <string name=\"sort_by_date_desc\">തിയതി, പുതിയത് ആദ്യം </string>\n  <string name=\"sort_by_date_asc\">തിയതി, പഴയത് ആദ്യം</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">തിരയുക</string>\n  <string name=\"not_granted_permission\">നിങ്ങൾ തുടങ്ങാൻ ഉദ്ദേശിക്കുന്ന പ്രവൃത്തിക്ക് ലഭിച്ചിട്ടില്ലാത്ത അനുമതികൾ വേണം </string>\n  <string name=\"storage_runtime_permission_denied_title\">സ്റ്റോറേജ് അനുമതികൾ </string>\n  <string name=\"storage_runtime_permission_denied_desc\">ഈ അനുമതികൾ ഇല്ലാതെ കളക്ടിന് നിങ്ങളുടെ ഫോമിലേക്ക് പ്രവേശിക്കുകയോ ഉത്തരങ്ങൾ സേവ് ചെയ്യുകയോ ചെയ്യാൻ ആകുന്നതല്ല. അനുമതി നല്കാൻ തയ്യാറാകുമ്പോൾ ആപ്പ് വീണ്ടും തുറക്കുക.</string>\n  <string name=\"location_runtime_permissions_denied_title\">സ്ഥാന നിർണയത്തിനുള്ള അനുമതി.</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">ക്യാമറ അനുമതി</string>\n  <string name=\"camera_runtime_permission_denied_desc\">ഈ അനുമതി ഇല്ലാതെ കളക്ടിന് ക്യാമറ നിയന്ത്രിക്കാൻ കഴിയുകയില്ല. ദയവു ചെയ്ത് അനുമതി നല്കാൻ തയ്യാറാകുമ്പോൾ വീണ്ടും ശ്രമിക്കുക.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">തിരച്ചിൽ അവസാനിച്ചിരിക്കുന്നു. എല്ലാ ഫോമുകളും ലോഡ് ചെയ്‌തിരിക്കുന്നു.</string>\n  <string name=\"loading_form\">ഫോം ലോഡ് ചെയ്തുകൊണ്ടിരിക്കുന്നു</string>\n  <string name=\"survey_loading_reading_data_message\">സർവ്വേ വിവരങ്ങൾ വായിച്ചുകൊണ്ടിരിക്കുന്നു…</string>\n  <string name=\"loading_form_failed\">ഫോം ലോഡ് ചെയ്തുകൊണ്ടിരിക്കുമ്പോൾ ഒരു തെറ്റ് പറ്റിയിരിക്കുന്നു. ദയവു ചെയ്ത് വീണ്ടും ശ്രമിക്കുക.</string>\n  <string name=\"cannot_edit_completed_form\">അന്തിമമായി അടയാളപ്പെടുത്തിക്കഴിഞ്ഞാൽ ഈ ഫോർമ് എഡിറ്റ് ചെയ്യാൻ കഴിയില്ല. ഇത് എൻക്രിപ്റ്റ് ചെയ്തതായിരിക്കാം.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">ഭാഷ മാറ്റുക</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">തുടക്കത്തിലേക്ക് പോകുക </string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">അവസാനത്തിലേക്കു പോകുക</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">മുകളിലേക്ക് പോകുക</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"clearanswer_confirm\"> 1 %s  എന്നയാൾക്ക് പ്രതികരണം നീക്കംചെയ്യണോ?</string>\n  <string name=\"clear_answer\">പ്രതികരണം നീക്കം ചെയ്യുക</string>\n  <string name=\"clear_answer_ask\">ഈ പ്രതികരണം നീക്കംചെയ്യണോ?</string>\n  <string name=\"clear_answer_no\">റദ്ദാക്കുക</string>\n  <string name=\"discard_answer\">പ്രതികരണം നീക്കം ചെയ്യുക</string>\n  <string name=\"discard_group\">ഗ്രൂപ്പ് നീക്കംചെയ്യുക</string>\n  <string name=\"delete_repeat\">ഗ്രൂപ്പ് നീക്കംചെയ്യുക</string>\n  <string name=\"delete_repeat_ask\">ഈ ഗ്രൂപ്പ് നീക്കംചെയ്യണോ?</string>\n  <string name=\"delete_repeat_confirm\">ഗ്രൂപ്പ് \\\" %s\\\" ഉം അതിന്റെ എല്ലാ ഉപ-ഗ്രൂപ്പുകളും നീക്കംചെയ്യണോ?</string>\n  <string name=\"delete_repeat_no\">റദ്ദാക്കുക</string>\n  <string name=\"required_answer_error\">ക്ഷമിക്കണം, ഈ ഉത്തരം അത്യാവശ്യമാണ്</string>\n  <!--Title of the end of form screen-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">ക്ഷമിക്കണം, ഫോർമ് സംരക്ഷിക്കുന്നത് പരാജയപ്പെട്ടു!</string>\n  <string name=\"data_saved_ok\">ഫോർമ് വിജയകരമായി സംരക്ഷിച്ചു!</string>\n  <string name=\"save_point_error\">വീണ്ടെടുക്കൽ പോയിന്റ് സംരക്ഷിക്കുന്നതിൽ പിശക്/പ്രമാദം: 1 %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"activity_not_found\"> കൈകാര്യം ചെയ്യാൻ പ്രവർത്തനങ്ങളൊന്നും കണ്ടെത്തിയില്ല: %s</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Url തുറക്കുക </string>\n  <string name=\"capture_osm\">OpenMapKit സമാരംഭിക്കുക</string>\n  <string name=\"recapture_osm\">OSM ടാഗ്ഗിംഗ് വീണ്ടും ചെയ്യുക</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">ഫയൽ തുറക്കുക</string>\n  <string name=\"svg_file_does_not_exist\">SVG ഫയൽ നിലവിലില്ല!</string>\n  <string name=\"capture_image\">ചിത്രം എടുക്കുക</string>\n  <string name=\"choose_image\">ചിത്രം തിരഞ്ഞെടുക്കുക </string>\n  <string name=\"take_picture_instruction\">ചിത്രം എടുക്കാനായി സ്‌ക്രീനിൽ തൊടുക</string>\n  <string name=\"error_front_camera_unavailable\">ഈ ഉപകരണത്തിൽ മുന്നിലെ ക്യാമറ ലഭ്യമല്ല</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"capture_video\"> വീഡിയോ റെക്കോർഡ് ചെയ്യുക</string>\n  <string name=\"choose_video\">വീഡിയോ തിരഞ്ഞെടുക്കുക</string>\n  <string name=\"choose_file\">ഫയൽ തിരഞ്ഞെടുക്കുക</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">ഡിലീറ്റ് </string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">ശബ്ദംറെക്കോർഡ് ചെയ്യുക</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">ഈ ഫയൽ ഡിലീറ്റ് ചെയ്യണമോ?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">ഈ ഫയൽ ഡിലീറ്റ് ചെയ്താൽ തിരിച്ചെടുക്കാവുന്നതല്ല. മുന്നോട്ടു പോകണം എന്ന് ഉറപ്പാണോ?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">നിർത്തുക</string>\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">ഫയൽ സേവ് ചെയ്തുകൊണ്ടിരിക്കുന്നു</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">മൂല്യം തിരഞ്ഞെടുക്കുക</string>\n  <string name=\"select_answer\">ഉത്തരം തിരഞ്ഞെടുക്കുക</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">മൂല്യത്തിൽ മാറ്റം വരുത്തുക</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">മൂല്യം ഒന്നും തന്നെ തിരഞ്ഞെടുത്തട്ടില്ല</string>\n  <string name=\"selected_title\">തിരഞ്ഞെടുക്കപ്പെട്ടത്:</string>\n  <string name=\"trigger\">ശരി. ദയവു ചെയ്തു തുടരുക</string>\n  <string name=\"ext_file_no_data_error\">ഈ ഫയലിൽ വിവരങ്ങൾ ഒന്നും തന്നെയില്ല!</string>\n  <string name=\"select_date\">തിയതി തിരഞ്ഞെടുക്കുക</string>\n  <string name=\"select_time\">സമയം തിരഞ്ഞെടുക്കുക</string>\n  <string name=\"no_date_selected\">തിയതി തിരഞ്ഞെടുക്കപ്പെട്ടട്ടില്ല</string>\n  <string name=\"no_time_selected\">സമയം തിരഞ്ഞെടുക്കപെട്ടട്ടില്ല </string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">അടയ്ക്കുക</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">മാറ്റങ്ങൾക്കു കാരണം</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">കാരണം</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">സംരക്ഷിച്ച ഫോർമുകൾ</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"delete_confirm\">1 %s ഫോർമ് (കൾ) ഇല്ലാതാക്കണോ?</string>\n  <string name=\"delete_file\">തിരഞ്ഞെടുത്തത് ഇല്ലാതാക്കുക</string>\n  <string name=\"delete_no\">ഇല്ലാതാക്കരുത്</string>\n  <string name=\"delete_yes\">ഫോർമുകൾ ഇല്ലാതാക്കുക</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Label for server type setting-->\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_username\">ODK ഉപയോക്തൃനാമം</string>\n  <string name=\"change_password\">ODK സങ്കേതപദം</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"show_password\">പാസ്സ്‌വേർഡ് കാണിക്കുക</string>\n  <string name=\"delete_project\">ഡിലീറ്റ് </string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">റദ്ദാക്കുക</string>\n  <string name=\"cancel_loading_form\">റദ്ദാക്കുക</string>\n  <string name=\"cancel_location\">റദ്ദാക്കുക</string>\n  <string name=\"do_not_change\">റദ്ദാക്കുക</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-mr/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">आवृत्ती: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'अॅड ईईई, एमएमएम डीडी, यॅय\\' एचएच: मि.ह.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'सेव्हड् ओव्हर\\' ईईई, एमएमएम डीडी, य्वाय \\'एट\\' एचएच: एमएम</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'इ.ई.ई., एमएमएम डीडी, य्यय\\' एचएच: एमएम वर अंतिम रूप दिले</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'सेंड ऑन\\' EEE, MMM dd, yyyy HH: mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'ईईई, एमएमएम डीडी, येय एचएच: एमएम \\'वर पाठविणे अयशस्वी झाले</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'EEE वर हटविले\\', \\'एमएमएम डीडी, याय\\' \\'एचएच: एमएम\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'सुधारित\\' वर EEE, d MMM, yyyy \\'at\\' HH: mm</string>\n  <string name=\"encrypted_form\">कूटबद्ध फॉर्म</string>\n  <string name=\"deleted_form\">फॉर्म हटविला</string>\n  <string name=\"sort_the_list\">सूची क्रमवारी लावा</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">सूची फिल्टर करा</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$d  फॉर्म जोडले.</string>\n  <string name=\"finished_disk_scan\">स्कॅनिंग पूर्ण. सर्व फॉर्म लोड केले</string>\n  <string name=\"xform_parse_error\">%1$s साठी XForm parse त्रुटी: %2$s गहाळ आहे किंवा अवैध आहे</string>\n  <string name=\"loading_form\">फॉर्म लोड होत आहे</string>\n  <string name=\"survey_loading_reading_form_message\">फॉर्म व्याख्या वाचत आहे …</string>\n  <string name=\"survey_loading_reading_data_message\">सर्वेक्षण डेटा वाचत आहे …</string>\n  <string name=\"survey_loading_reading_csv_message\">CSV फायली वाचत आहे …</string>\n  <string name=\"parse_error\">क्षमस्व, फॉर्म विश्लेषित करण्यास अक्षम</string>\n  <string name=\"cannot_edit_completed_form\">अंतिम रूपाने चिन्हांकित केल्यावर हे फॉर्म संपादित करणे शक्य नाही. हे कूटबद्ध केले जाऊ शकते.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">जतन न केलेले बदल पुनर्प्राप्त!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">भाषा बदला</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">प्रॉम्प्ट वर जा</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">सुरू करण्यासाठी जा</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">संम्पतही कडे जा </string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">वर जा</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"exit\">निर्गमन करा</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">सूचना संपादित करा</string>\n  <string name=\"clearanswer_confirm\">\\\"%s\\\" ला प्रतिसाद काढायचा का ?</string>\n  <string name=\"clear_answer\">प्रतिसाद काढा</string>\n  <string name=\"clear_answer_ask\">हा प्रतिसाद काढायचा का ?</string>\n  <string name=\"clear_answer_no\">रद्द करा</string>\n  <string name=\"discard_answer\">प्रतिसाद काढा.</string>\n  <string name=\"discard_group\">गट काढा</string>\n  <string name=\"delete_repeat\">गट काढा</string>\n  <string name=\"delete_repeat_ask\">हा गट काढायचा?</string>\n  <string name=\"delete_repeat_confirm\">गट  1%s आणि त्याचे सर्व उप गट काढायचे?</string>\n  <string name=\"delete_repeat_no\">रद्द करा</string>\n  <string name=\"invalid_answer_error\">क्षमस्व, हा प्रतिसाद अवैध आहे!</string>\n  <string name=\"required_answer_error\">क्षमस्व, हे प्रतिसाद आवश्यक आहे!</string>\n  <string name=\"survey_internal_error\">अंतर्गत त्रुटी: प्रॉम्प्टसाठी चरण अयशस्वी.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">आपण 1%s च्या समाप्तीस आहात</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">क्षमस्व, एक फॉर्म हटविणे क्रिया आधीच प्रगतीपथावर आहे!</string>\n  <string name=\"data_saved_ok\">फॉर्म यशस्वीरित्या जतन केला!</string>\n  <string name=\"save_point_error\">पुनर्प्राप्ती बिंदू जतन करताना त्रुटी: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">बदल जतन करा</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">फॉर्म जतन केला जात आहे</string>\n  <string name=\"survey_saving_validating_message\">उत्तरे सत्यापित करीत आहे …</string>\n  <string name=\"survey_saving_collecting_message\">डेटा संकलित करत आहे …</string>\n  <string name=\"survey_saving_saving_message\">SD कार्डवर जतन करीत आहे …</string>\n  <string name=\"survey_saving_finalizing_message\">SD कार्डमध्ये अंतिम रूप देत आहे …</string>\n  <string name=\"survey_saving_encrypting_message\">डेटा एन्क्रिप्ट करत आहे …</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">या घटनेसाठी नक्की एक रेकॉर्ड नाही!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">या jr_form_id शी न जुळणारे एक रिक्त स्वरूप</string>\n  <string name=\"no_form_id_specified\">फॉर्मID निर्दिष्ट नाही ???</string>\n  <string name=\"phone_does_not_support_rsa\">फोन RSA एन्क्रिप्शनचे समर्थन करत नाही.</string>\n  <string name=\"invalid_rsa_public_key\">Invalid RSA public key.</string>\n  <string name=\"encryption_error_message\">%s फॉर्म अंतिम रूपाने जतन केले गेले नाही.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">लाँच करा</string>\n  <string name=\"activity_not_found\">हाताळण्यासाठी कोणतीही गतिविधी आढळली नाही: %s</string>\n  <string name=\"no_app\">विनंती केलेले अनुप्रयोग गहाळ आहे. कृपया स्वतः वाचन प्रविष्ट करा.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">युरएल उघडा</string>\n  <string name=\"capture_osm\">OpenMapKit लाँच करा</string>\n  <string name=\"recapture_osm\">OSM टॅगिंग पुन्हा करा</string>\n  <string name=\"edited_osm_file\">संपादित OSM XML फाइल:</string>\n  <string name=\"invalid_osm_data\">काहीतरी चूक झाली. आम्हाला वैध ओपन स्ट्रीट नकाशा डेटा प्राप्त झाला नाही.</string>\n  <string name=\"alert\">अॅलर्ट</string>\n  <string name=\"install_openmapkit\">कृपया ओपन मॅप किट स्थापित करा!</string>\n  <string name=\"get_barcode\">बारकोड मिळवा</string>\n  <string name=\"replace_barcode\">बारकोड पुनर्स्थित करा</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"invalid_range_widget\">या विजेटमध्ये अवैध मापदंड आहेत!</string>\n  <string name=\"capture_image\">छायाचित्र काढा</string>\n  <string name=\"choose_image\">प्रतिमा निवडा</string>\n  <string name=\"selected_invalid_image\">निवडलेली फाईल वैध प्रतिमा नाही</string>\n  <string name=\"take_picture_instruction\">एक चित्र घेण्यासाठी स्क्रीन टॅप करा</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">जतन करा आणि बंद करा</string>\n  <string name=\"sign_button\">स्वाक्षरी एकत्रीकरण</string>\n  <string name=\"markup_image\">मार्कअप प्रतिमा</string>\n  <string name=\"draw_image\">स्केच प्रतिमा</string>\n  <string name=\"reset_image\">रीसेट करा</string>\n  <string name=\"set_color\">रंग सेट करा</string>\n  <string name=\"capture_video\">व्हिडिओ रेकॉर्ड करा</string>\n  <string name=\"choose_video\">व्हिडिओ निवडा</string>\n  <string name=\"play_video\">व्हिडिओ प्ले करा</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">ध्वनी रेकॉर्ड करा</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">ध्वनी निवडा</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">मिडिया संलग्न करताना त्रुटी आली: 1%s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">उत्तर निवडा</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">निवडलेले:</string>\n  <string name=\"trigger\">ठीक आहे. कृपया चालू ठेवा.</string>\n  <string name=\"ext_file_no_data_error\">फाइलमध्ये कोणताही डेटा नाही!</string>\n  <string name=\"ext_conflicting_columns_error\">स्तंभ%s जुळत आहे!</string>\n  <string name=\"ext_sortBy_numeric_error\">आपल्या क्रमवारीत स्तंभमध्ये केवळ अंकीय मूल्ये असावीत. विवादित मूल्य \\' 1%s \\'. होता.</string>\n  <string name=\"ext_search_wrong_arguments_error\">शोधात वाक्यरचना त्रुटी () कार्य: कार्य आवश्यक 1, 4 किंवा 6 वितर्क.</string>\n  <string name=\"ext_search_wrong_function_error\">शोधात वाक्यरचना त्रुटी () कार्य: अपरिचीत कार्य \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">शोध () फंक्शनमध्ये वाक्यरचना त्रुटी: \\' 1%s\\' चे फंक्शन म्हणून मूल्यांकन झाले नाही.</string>\n  <string name=\"ext_search_return_error\">शोध हँडलरने \\'1%s\\' प्रकारचे ऑब्जेक्ट परत केले.</string>\n  <string name=\"ext_import_generic_error\"> %1$sवरून डेटा आयात करू शकले नाही . कारण: %2$s</string>\n  <string name=\"ext_import_progress_message\"> \\'%1$s\\' वरून डेटा पूर्व लोड होत आहे, कृपया प्रतीक्षा करा, %2$s</string>\n  <string name=\"ext_import_finalizing_message\">पूर्व लोड केलेल्या डेटाला अंतिम रूप देत आहे …</string>\n  <string name=\"ext_import_completed_message\">डेटा वाचन पूर्ण!</string>\n  <string name=\"ext_not_initialized_error\">बाह्यडेटा व्यवस्थापक प्रारंभ झाला नाही.</string>\n  <string name=\"ext_import_csv_missing_error\">%1$s साठी बाह्य डेटा आयात केला गेला नाही. कदाचित आपण आपल्या फॉर्मसह  %2$s.csv फाइल समाविष्ट करणे विसरलात?</string>\n  <string name=\"ext_search_generic_error\">शोधात वाक्यरचना त्रुटी () कार्य: %s</string>\n  <string name=\"ext_assign_value_error\">\\'%s\\' वर मूल्य नियुक्त करू शकत नाही.</string>\n  <string name=\"parser_exception\">एक्स पाथ पार्सर अपवादः  \\\"%s\\\"</string>\n  <string name=\"get_bearing\">रेकॉर्ड बेअरिंग</string>\n  <string name=\"replace_bearing\">बेअरिंग पुनर्स्थित करा</string>\n  <string name=\"getting_bearing\">लोडिंग बेअरिंग</string>\n  <string name=\"accept_bearing\">रेकॉर्ड बेअरिंग</string>\n  <string name=\"bearing\">असणारा:%.3f</string>\n  <string name=\"direction\">दिशा: %s</string>\n  <string name=\"select_date\">तारीख निवडा</string>\n  <string name=\"select_time\">वेळेची निवड करा</string>\n  <string name=\"no_date_selected\">तारीख निवडली नाही</string>\n  <string name=\"no_time_selected\">वेळ निवडला नाही</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">फाइल: %s अवैध आहे.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">फाइल: %s गहाळआहे.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">यूएसजीएस राष्ट्रीय नकाशा टोपो</string>\n  <string name=\"openmap_cartodb_positron\">कार्टो डीबी पॉझिट्रॉन</string>\n  <string name=\"openmap_cartodb_darkmatter\">कार्टो डीबी गडद बाब</string>\n  <string name=\"basemap\">बेसमॅप</string>\n  <string name=\"streets\">रस्ते</string>\n  <string name=\"terrain\">भूप्रदेश</string>\n  <string name=\"hybrid\">संकरित</string>\n  <string name=\"satellite\">उपग्रह</string>\n  <string name=\"google_play_services_error_occured\">गूगल नकाशे मध्ये प्रवेश करण्यात अक्षम. गूगल प्ले सेवा स्थापित केली आहे का?</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">क्षमस्व, स्थान प्रदाता अक्षम केले आहेत!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">पश्चिम %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">पूर्व %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">दक्षिण %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">उत्तर %1$s%2$s%3$s</string>\n  <string name=\"start\">प्रारंभ करा</string>\n  <string name=\"enable_gps\">जीपीएस सक्षम करा</string>\n  <string name=\"geo_clear_warning\">वैशिष्ट्य आधीच तयार केले आपण वैशिष्ट्य साफ करू इच्छिता?</string>\n  <string name=\"geo_exit_warning\">बदल टाकून ओडीके कडे परत जा?</string>\n  <string name=\"polygon_validator\">बहुभुज तयार करण्यासाठी किमान 3 गुण असणे आवश्यक आहे</string>\n  <string name=\"clear\">साफ करा</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"geopoint_no_draggable_instruction\">जोडा मार्कर बटण टॅप करा</string>\n  <string name=\"discard\">टाकून द्या</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">फॉर्म पाठवत आहे</string>\n  <string name=\"noselect_error\">क्षमस्व, आपण कोणतेही फॉर्म निवडलेले नाहीत!</string>\n  <string name=\"no_connection\">कोणतेही नेटवर्क कनेक्शन उपलब्ध नाही</string>\n  <string name=\"sending_items\"> फॉर्म(स)%1$s of %2$s पाठवत आहे.</string>\n  <string name=\"send_selected_data\">निवडलेले पाठवा</string>\n  <string name=\"server_auth_credentials\">सर्व्हरसाठी अवैध वापरकर्तानाव किंवा संकेतशब्द: %s</string>\n  <string name=\"server_requires_auth\">सर्व्हरला प्रमाणीकरणाची आवश्यकता आहे</string>\n  <string name=\"no_forms_uploaded\">क्षमस्व, कोणताही फॉर्म अपलोड करण्यात आला नाही</string>\n  <string name=\"upload_results\">परिणाम अपलोड करा</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">दृश्य बदला</string>\n  <string name=\"show_sent_and_unsent_forms\">पाठवलेले आणि रद्द केलेले फॉर्म दर्शवा</string>\n  <string name=\"show_unsent_forms\">पाठवलेले आणि रद्द केलेले फॉर्म दर्शवा</string>\n  <string name=\"send_in_progress\">पार्श्वभूमी पाठविणे चालू आहे, कृपया लवकरच पुन्हा प्रयत्न करा</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">रिफ्रेश करा</string>\n  <string name=\"download\">निवडलेले आणा</string>\n  <string name=\"downloading_data\">सर्व्हरशी कनेक्ट करीत आहे</string>\n  <string name=\"fetching_file\"> %1$s  मिळवत आहे. \\n\\nफॉर्म %2$s च्या%3$s फॉर्म(स) </string>\n  <string name=\"form_download_progress\">%1$s मीडिया फायली मिळवत आहे: %2$s पैकी %3$s</string>\n  <string name=\"version\">आवृत्ती:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">जतन केलेले फॉर्म</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">रिक्त फॉर्म</string>\n  <string name=\"delete_confirm\">फॉर्म(स)  %s हटवायचे का?</string>\n  <string name=\"delete_file\">निवडलेले हटवा ?</string>\n  <string name=\"delete_no\">हटवू नका</string>\n  <string name=\"delete_yes\">फॉर्म हटवा</string>\n  <string name=\"file_deleted_ok\">%s फॉर्म(स) यशस्वीरित्या हटविले!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">सर्व्हर</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">वापरकर्ता इंटरफेस</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">फॉर्म व्यवस्थापन</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">वापरकर्ता आणि डिव्हाइस ओळख</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">सर्व्हर सेटिंग्ज</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">यु र एल </string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">सर्व्हर युरएल</string>\n  <string name=\"url_error\">क्षमस्व, अवैध यु र एल </string>\n  <string name=\"username\">वापरकर्तानाव</string>\n  <string name=\"change_username\">ODK वापरकर्तानाव</string>\n  <string name=\"password\">संकेतशब्द</string>\n  <string name=\"change_password\">ODK संकेतशब्द</string>\n  <string name=\"password_error_whitespace\">पासवर्डमध्ये अग्रस्थानी किंवा अनुसरणी व्हायरसपासची परवानगी नाही</string>\n  <string name=\"username_error_whitespace\">वापरकर्तानावांमध्ये अग्रगण्य किंवा अनुसरणी व्हायरसपेस अनुमत नाहीत</string>\n  <string name=\"language\">भाषा</string>\n  <string name=\"use_device_language\">डिव्हाइस भाषा वापरा</string>\n  <string name=\"font_size\">मजकूर फॉन्ट आकार</string>\n  <string name=\"change_font_size\">मजकूर फॉन्ट आकार</string>\n  <string name=\"font_size_extra_large\">जास्त मोठं</string>\n  <string name=\"font_size_large\">मोठा</string>\n  <string name=\"font_size_medium\">मध्यम</string>\n  <string name=\"font_size_small\">लहान</string>\n  <string name=\"font_size_extra_small\">अतिरिक्त लहान</string>\n  <string name=\"navigation\">नेव्हिगेशन</string>\n  <string name=\"swipe_navigation\">क्षैतिज स्वाइप वापरा</string>\n  <string name=\"buttons_navigation\">फॉरवर्ड / बॅकवर्ड बटणे वापरा</string>\n  <string name=\"swipe_buttons_navigation\">स्वाइप आणि बटणे वापरा</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">पुढे</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"form_submission_category\">फॉर्म सबमिशन</string>\n  <string name=\"autosend_selector_title\">स्वयं पाठवा</string>\n  <string name=\"autosend\">स्वयं पाठवा</string>\n  <string name=\"off\">बंद</string>\n  <string name=\"wifi_autosend\">केवळ Wifi</string>\n  <string name=\"cellular_autosend\">केवळ सेल्यूलर</string>\n  <string name=\"wifi_cellular_autosend\">वायफाय किंवा सेल्यूलर</string>\n  <string name=\"delete_after_send\">पाठवानंतर हटवा</string>\n  <string name=\"delete_after_send_summary\">सर्व्हरवर पाठविल्यानंतर अंतिम स्वरूप आणि मीडिया हटविते</string>\n  <string name=\"form_filling_category\">फॉर्म भरणे</string>\n  <string name=\"constraint_behavior_title\">प्रतिबंध प्रोसेसिंग</string>\n  <string name=\"constraint_behavior\">प्रतिबंध प्रोसेसिंग वर्तन</string>\n  <string name=\"constraint_behavior_on_swipe\">पुढील स्वाइप वर प्रमाणित करा</string>\n  <string name=\"constraint_behavior_on_finalize\">अंतिम रूपाने होईपर्यंत प्रमाणीकरण स्थगित करा</string>\n  <string name=\"high_resolution_title\">उच्च रिझोल्यूशन व्हिडिओ</string>\n  <string name=\"high_resolution_summary\">उच्च-रिजोल्यूशन व्हिडिओ रेकॉर्डिंग सक्षम करा</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">फॉर्म भरणे</string>\n  <string name=\"analytics_preferences\">वापर डेटा</string>\n  <string name=\"analytics\">अनामित वापर डेटा संकलित करा</string>\n  <string name=\"form_metadata_title\">फॉर्म मेटाडेटा</string>\n  <string name=\"form_metadata\">फॉर्म मेटाडेटा</string>\n  <string name=\"form_metadata_summary\">ही मूल्ये डेटा सबमिट करणाऱ्या व्यक्तीस ओळखण्यासाठी वापरकर्तानाव, ईमेल आणि / किंवा टेलिफोन प्रील्डेड फील्ड्स असलेल्या फॉर्ममध्ये जोडली जातील.</string>\n  <string name=\"user_defined\">वापरकर्ता परिभाषित</string>\n  <string name=\"phone_number\">फोन नंबर</string>\n  <string name=\"email\">ईमेल पत्ता</string>\n  <string name=\"device_defined\">डिव्हाइस-परिभाषित</string>\n  <string name=\"device_id\">डिव्हाइस ID</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">नवीन संकेतशब्द प्रविष्ट करा</string>\n  <string name=\"admin_password\">प्रशासन संकेतशब्द</string>\n  <string name=\"admin_password_changed\">प्रशासक संकेतशब्द यशस्वीरित्या बदलला</string>\n  <string name=\"admin_password_disabled\">प्रशासन संकेतशब्द अक्षम केला</string>\n  <string name=\"admin_password_incorrect\">क्षमस्व, संकेतशब्द चुकीचा आहे!</string>\n  <string name=\"enter_admin_password\">प्रशासन पासवर्ड प्रविष्ट करा</string>\n  <string name=\"change_admin_password\">प्रशासन पासवर्ड बदला</string>\n  <string name=\"show_password\">संकेतशब्द दर्शवा</string>\n  <string name=\"found_in_main\">मुख्य मेनूमधून लपविण्यासाठी अनचेक करा</string>\n  <string name=\"form_entry\">फॉर्म एन्ट्रीमधून लपविण्यासाठी अनचेक करा</string>\n  <string name=\"reset_project_settings_title\">रीसेट करा</string>\n  <string name=\"select_all\">सर्व निवडा</string>\n  <string name=\"clear_all\">सर्व साफ करा</string>\n  <string name=\"reset_settings\">सर्व सेटिंग्ज (अंतर्गत सेटिंग्ज, सेव्ह सेटिंग्ज)</string>\n  <string name=\"reset_saved_forms\">जतन फॉर्म (उदाहरण फोल्डर, उदाहरणे डेटाबेस)</string>\n  <string name=\"reset_blank_forms\">रिक्त फॉर्म (फॉर्म फोल्डर, फॉर्म डेटाबेस, आयटसेट्स डेटाबेस)</string>\n  <string name=\"reset_layers\">नकाशा स्तर (स्तर फलक)</string>\n  <string name=\"reset_cache\">फॉर्म लोड कॅशे (.cache फोल्डर)</string>\n  <string name=\"reset_settings_dialog_title\">काय रीसेट करावे ते निवडा</string>\n  <string name=\"reset_settings_button_reset\">रीसेट करा</string>\n  <string name=\"reset_in_progress\">रीसेट करत आहे …</string>\n  <string name=\"reset_app_state_result\">परिणाम रीसेट करा</string>\n  <string name=\"reset_app_state_warning\">सर्व निवडलेला डेटा कायमचा हटविला जाईल. तेथे पूर्ववत नाही</string>\n  <string name=\"reset_settings_result\">सर्व सेटिंग्ज :: %s</string>\n  <string name=\"reset_saved_forms_result\">जतन फॉर्म :: %s</string>\n  <string name=\"reset_blank_forms_result\">रिक्त फॉर्म :: %s</string>\n  <string name=\"reset_layers_result\">नकाशा स्तर :: %s</string>\n  <string name=\"reset_cache_result\">फॉर्म लोड कॅशे :: %s</string>\n  <string name=\"main_menu_settings\">मुख्य मेनू सेटिंग्ज</string>\n  <string name=\"user_settings\">वापरकर्ता सेटिंग्ज</string>\n  <string name=\"form_entry_setting\">फॉर्म प्रवेश सेटिंग्ज</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">विषयी</string>\n  <string name=\"odk_website\">ओडीके वेबसाइट ला भेट द्या</string>\n  <string name=\"odk_website_summary\">ओडीकेे चा उपयोग आव्हानात्मक पर्यावरणात सामाजिक चांगल्यासाठी डेटा गोळा करण्यासाठी केला जातो.</string>\n  <string name=\"odk_forum\">ओडीके मंचात सहभागी व्हा</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"successfully_imported_settings\">सेटिंग्ज यशस्वीपणे आयात केली</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">कमाल वर्ण मर्यादेपर्यंत पोहोचले: सर्व सेटिंग्जसाठी क्यूआर कोड व्युत्पन्न करणे शक्य नाही</string>\n  <string name=\"share\">सामायिक करा</string>\n  <string name=\"turn_on_flashlight\">फ्लॅशलाइट चालू करा</string>\n  <string name=\"turn_off_flashlight\">फ्लॅशलाइट बंद करा</string>\n  <string name=\"server_password\">सर्व्हर पासवर्ड</string>\n  <string name=\"include_password_dialog\">कोडमध्ये समाविष्ट संकेतशब्द</string>\n  <string name=\"generate\">व्युत्पन्न करा</string>\n  <string name=\"invalid_qrcode\">क्यूआर कोडमध्ये वैध सेटिंग्ज नाहीत</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">ठीक आहे</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">रद्द करा</string>\n  <string name=\"cancel_loading_form\">रद्द करा</string>\n  <string name=\"cancel_location\">रद्द करा</string>\n  <string name=\"do_not_change\">रद्द करा</string>\n  <string name=\"success\">यश</string>\n  <string name=\"error_occured\">त्रुटी झाले</string>\n  <string name=\"please_wait\">कृपया काही क्षण प्रतीक्षा करा</string>\n  <string name=\"please_wait_long\">कृपया थांबा. यास काही मिनिटे लागू शकेल.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">%s\\'. हटविणे शक्य नाही. कृपया फाईल व्यक्तिचलितपणे हटवा आणि फॉर्म पुन्हा डाउनलोड करा.</string>\n  <string name=\"fs_create_media_folder_error\">मीडिया फोल्डर \\'%s\\' तयार करू शकले नाही.</string>\n  <string name=\"fs_file_copy_error\"> \\'%1$s\\' वर \\'%2$s\\' कॉपी करू शकलो नाही. कारण :%3$s</string>\n  <string name=\"select_odk_shortcut\">ओडीके शॉर्टकट निवडा</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">अपरिचित युरएल: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">खराब युरएल %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">आपण एकाच फॉर्म आयडी आणि आवृत्तीसह दोन भिन्न फॉर्म डाउनलोड केले आहेत. कदाचित ते वेगळ्या वेळी किंवा भिन्न सर्व्हरवर अपलोड केलेले समान फॉर्म होते. यापैकी एक फाईल हटवा.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">टाकून द्या</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ms/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Tukar Bahasa</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"clear_answer_no\">Batal</string>\n  <string name=\"delete_repeat_no\">Batal</string>\n  <!--Title of the end of form screen-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"choose_image\">Pilih Gambar</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Borang Kosong</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Label for server type setting-->\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"password\">Kata Laluan</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"show_password\">Tunjuk Kata Laluan</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Batal</string>\n  <string name=\"cancel_loading_form\">Batal</string>\n  <string name=\"cancel_location\">Batal</string>\n  <string name=\"do_not_change\">Batal</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-my/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">ဖောင်အသစ်ဖြည့်မည်</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">ဖောင်ကြမ်းများ</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">တင်ပို့ရန်အသင့်</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">တင်ပို့ပြီးဖောင်</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">ဒေါင်းလုပ်ရယူပြီးဖောင်</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">ဖျက်ထားသောဖောင်</string>\n  <string name=\"saved_forms\">သိမ်းထားပြီးဖောင်များ</string>\n  <string name=\"version_number\">ဗားရှင်း ၁%s</string>\n  <string name=\"id_number\">ID 1%s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">EEE, MMM dd, yyyy \\'at\\' HH:mm ကထပ်မံဖြည့်ထည့်ခဲ့သည်</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">EEE, MMM dd, yyyy \\'at\\' HH:mm ကနောက်ဆုံးအခြေအနေကိုမွမ်းမံခဲ့သည်</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">EEE, MMM dd, yyyy \\'at\\' HH:mm ကသိမ်းဆည်းခဲ့သည်</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">EEE, MMM dd, yyyy \\'at\\' HH:mm ကအပြီးသတ်ပြင်ဆင်ခဲ့သည်</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">EEE, MMM dd, yyyy \\'at\\' HH:mm ကပေးပို့ခဲ့သည်</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">EEE, MMM dd, yyyy \\'at\\' HH:mm ပေးပို့စဉ်မအောင်မြင်ခဲ့ပါ </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">EEE, MMM dd, yyyy \\'at\\' HH:mm ကဖျက်ထားသည်</string>\n  <string name=\"submission_deleted\">ပေးပို့ပြီးဖောင်ကိုဖျက်လိုက်ပြီ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">EEE, MMM dd, yyyy \\'at\\' HH:mm ကထပ်မံမွမ်းမံခဲ့သည်</string>\n  <string name=\"encrypted_form\">လုံခြုံအောင်ကုတ်ဖြင့်သိမ်းထားသည့်ဖောင်</string>\n  <string name=\"deleted_form\">ဖျက်သိမ်းပြီးဖောင်</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">ပံုစံမ်ားစစ္ေဆးၿပီး ရရိွပါၿပီ</string>\n  <string name=\"loading_form\">ပံုစံရယူေနပါသည္</string>\n  <string name=\"parse_error\">ပံုစံကို မခြဲျခားႏိုင္ပါ</string>\n  <string name=\"cannot_edit_completed_form\">ဤပုံစံကို အပြီးသတ်ပြီးသည့်အချိန်မှစ၍ ပြုပြင်၍မရတော့ပါ။ ဤပုံစံသည် encrypted တင်ထားပြီးဖြစ်နိုင်သည်။</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">သိမ္းဆည္းေနရာမွ မသိမ္းဆည္းရေသးသည္မ်ား ျပန္လည္ရယူရန္</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">ဘာသာစကားပြောင်းပါ</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">ခ်က္ျခင္းသြားရန္</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">ျပန္လည္စတင္ရန္</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">အဆံုးသတ္ရန္</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">အေပၚသို႔တက္ရန္</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">နောက်တစ်ခုထည့်ရန်</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">ခ်က္ျခင္းျပင္ဆင္ရန္</string>\n  <string name=\"clearanswer_confirm\">\\\"%s\\\" ၏အဖြေကို ဖယ်မည်လား?</string>\n  <string name=\"clear_answer\">ဖြေကြားမှုကို ဖယ်ထုတ်ရန်</string>\n  <string name=\"clear_answer_ask\">ဤဖြေကြားမှုကို ဖယ်ထုတ်လိုပါသလား</string>\n  <string name=\"clear_answer_no\">မဖယ်ထုတ်ပါ</string>\n  <string name=\"discard_answer\">အဖြေကို ဖယ်ရှားရန်</string>\n  <string name=\"discard_group\">အုပ်စုကို ဖယ်ရှားရန်</string>\n  <string name=\"delete_repeat\">အုပ်စုကို ဖယ်ရှားရန်</string>\n  <string name=\"delete_repeat_ask\">ဒီအုပ်စုကို ဖယ်ရှားမည်လား</string>\n  <string name=\"delete_repeat_confirm\"> \\\"%s\\\" အုပ်စုနှင့် သူ၏ အုပ်စုခွဲများကို ဖယ်ရှားမည်လား</string>\n  <string name=\"delete_repeat_no\">မလုပ်တော့ပါ</string>\n  <string name=\"invalid_answer_error\">ဖမ္းယူမွု မေကာင္းပါ</string>\n  <string name=\"required_answer_error\">အေျဖမ်ား မျပည့္စံုေသးပါ</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">သင္သည္ \\\"%s\\\"၏ ေနာက္ဆံုးအပိုင္းသို႔ ေရာက္ရိွေနပါသည္။</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">ပုံစံအား မသိမ်းဆည်းနိုင်ပါ</string>\n  <string name=\"data_saved_ok\">ပုံစံအားအောင်မြင်စွာ သိမ်းဆည်းနိုင်ပါသည်</string>\n  <string name=\"save_point_error\">ပြန်လည်ရယူသည့် နေရာ အမှားဖြစ်ပေါ်%s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">ေျပာင္းလဲမွုမ်ားကို သိမ္းဆည္းရန္</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">ပံုစံသိမ္းဆည္းေနပါသည္</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">စတင္ေဆာင္ရြက္ရန္</string>\n  <string name=\"activity_not_found\">လုပ်ဆောင်ရန် အလုပ်မတွေ့ပါ%s</string>\n  <string name=\"no_app\">လုပ္ေဆာင္မွုလြဲေခ်ာ္ေနပါသည္၊ ေသခ်ာစြာဖတ္ရွု၍ ၿဖည့္သြင္းပါ</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"capture_osm\">OpenMapKit ကိုဖွင့်ပါ</string>\n  <string name=\"recapture_osm\">OSM စာကပ်ခြင်း ပြန်လုပ်ပါ</string>\n  <string name=\"get_barcode\">ဘာကုတ္(ဒ္) ရယူပါ</string>\n  <string name=\"replace_barcode\">ဘားကုတ္(ဒ္) အစားထိုးရန္</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">ပုံရိုက်ပါ</string>\n  <string name=\"choose_image\">ပံုေရြးခ်ယ္ရန္</string>\n  <string name=\"annotate_image\">ပုံတွင်မှတ်ချက်ရေးပါ</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">သိမ္းဆည္း ၿပီးပိတ္ရန္</string>\n  <string name=\"signature_capture\">လက်မှတ်ရယူခြင်း</string>\n  <string name=\"sign_button\">အမွန္အသားအား ရွာရန္</string>\n  <string name=\"markup_image\">ပံုမွတ္ရန္</string>\n  <string name=\"draw_image\">ပံုၾကမ္းဆြဲရန္</string>\n  <string name=\"reset_image\">မူလအတုိင္းျပန္ထားရန္</string>\n  <string name=\"set_color\">အေရာင္ေရြးရန္</string>\n  <string name=\"capture_video\">ဗီဒီယိုရိုက်ပါ</string>\n  <string name=\"choose_video\">ဗြီဒီယို ေရြးခ်ယ္ရန္</string>\n  <string name=\"play_video\">ဗြီဒီယိုဖြင့္ရန္</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">အသံသွင်းပါ</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">အသံေရြးခ်ယ္ရန္</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">အေျဖေရြးခ်ယ္ရန္</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">ေရြးခ်ယ္ၿပီး:</string>\n  <string name=\"trigger\">ဆက္လက္လုပ္ေဆာင္ရန္</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">ဖိုင္: %s မေကာင္းမြန္ပါ</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">ဖိုင္: %s ေပ်ာက္ဆံုးေနပါသည္</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">တည္ေနရာဖမ္းယူမွု ခ်ိဳ႔ယြင္းေနပါသည္</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">ပံုစံေပးပို႔ျခင္း</string>\n  <string name=\"noselect_error\">ပံုစံေရြးခ်ယ္ထားျခင္းမရိွပါ</string>\n  <string name=\"no_connection\">ကြန္ယက္ခ်ိတ္ဆက္၍ မရပါ</string>\n  <string name=\"sending_items\"> %1$s ၊ %2$s ပံုစံမ်ား ေပးပို႔ေနပါသည္</string>\n  <string name=\"send_selected_data\">ေရြးခ်ယ္ထားေသာပံုစံမ်ားေပးပို႔ရန္</string>\n  <string name=\"server_requires_auth\">ဆာဗာမွ မွန္ကန္ေၾကာင္း ေထာက္ခံရန္လိုအပ္ပါသည္</string>\n  <string name=\"no_forms_uploaded\">ပံုစံမရယူႏိုင္ေသးပါ</string>\n  <string name=\"upload_results\">ရယူမွုရလဒ္မ်ား</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">ျမင္ကြင္းေျပာင္းရန္</string>\n  <string name=\"show_sent_and_unsent_forms\">ေပးပို့ၿပီးႏွင့္ မေပးပို႔ရေသးေသာ ပံုစံမ်ား</string>\n  <string name=\"show_unsent_forms\">မေပးပို႔ရေသးေသာ ပံုစံမ်ား</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">ပိုမီုေကာင္းမြန္လာရန္ လုပ္ေဆာင္ျခင္း</string>\n  <string name=\"download\">ရွေးချယ်ရန်</string>\n  <string name=\"downloading_data\">ဆာဗာကို ချိတ်ဆက်နေစဉ်</string>\n  <string name=\"form_download_progress\">%1$s. မီဒီယာဖိုင္ရယူေနပါသည္: %2$s of %3$s</string>\n  <string name=\"version\">လုပ္ေဆာင္မွုအဆင့္:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">ပုံစံသိမ်းရန်</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">ပံုစံအလြတ္မ်ား</string>\n  <string name=\"delete_confirm\">ဖ်က္ရန္ %s ပံုစံ</string>\n  <string name=\"delete_file\">ေရြးခ်ယ္ထားသည္မ်ားကို ဖ်က္ရန္</string>\n  <string name=\"delete_no\">မဖ်က္ရန္</string>\n  <string name=\"delete_yes\">ပုံစံကို ဖယ်ရှားရန်</string>\n  <string name=\"file_deleted_ok\">%s ပံုစံမ်ားဖ်က္ၿပီးျဖစ္ပါသည္</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">ဆာဗာ</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">ရင္းျမစ္ေနရာ</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"url_error\">ရင္းျမစ္ေနရာမွားယြင္းေနပါသည္</string>\n  <string name=\"username\">သံုးစြဲသူအမည္</string>\n  <string name=\"change_username\">ODK အသုံးပြုသူအမည်</string>\n  <string name=\"password\">လ်ိဳ႔၀ွက္နံပါတ္</string>\n  <string name=\"change_password\">ODK စကားဝှက်</string>\n  <string name=\"font_size\">စာလံုးအရြယ္အစား</string>\n  <string name=\"change_font_size\">စာလံုးအရြယ္အစား</string>\n  <string name=\"font_size_extra_large\">အလြန္ႀကီး</string>\n  <string name=\"font_size_large\">အႀကီး</string>\n  <string name=\"font_size_medium\">အလယ္အလတ္</string>\n  <string name=\"font_size_small\">ေသး</string>\n  <string name=\"font_size_extra_small\">အလြန္ေသး</string>\n  <string name=\"navigation\">Navigation</string>\n  <string name=\"swipe_navigation\">Use horizontal swipes</string>\n  <string name=\"buttons_navigation\">Use forward/backward buttons</string>\n  <string name=\"swipe_buttons_navigation\">Use swipes and buttons</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Next</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">လ်ိဳ့၀ွက္နံပါတ္ အသစ္ထည့္သြင္းရန္</string>\n  <string name=\"admin_password_changed\">စီမံခန့္ခြဲမွုလ်ိဳ့၀ွက္နံပါတ္(Admin Password) အား ေျပာင္းလဲၿပီးပါၿပီ</string>\n  <string name=\"admin_password_disabled\">စီမံခန့္ခြဲမွုလ်ိဳ့၀ွက္နံပါတ္(Admin Password) မေဆာင္ရြက္ႏိုင္ပါ</string>\n  <string name=\"admin_password_incorrect\">လ်ိဳ့၀ွက္နံပါတ္ မမွန္ကန္ပါ!</string>\n  <string name=\"enter_admin_password\">စီမံခန့္ခြဲမွုလ်ိဳ့၀ွက္နံပါတ္အား ထည့္သြင္းရန္</string>\n  <string name=\"show_password\">စကားဝှက်ပြပါ</string>\n  <string name=\"found_in_main\">Uncheck to hide from Main Menu</string>\n  <string name=\"reset_project_settings_title\">မူလအတုိင္းျပန္ထားရန္</string>\n  <string name=\"reset_settings_button_reset\">မူလအတုိင္းျပန္ထားရန္</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">ပယ်ဖျက်ပါ</string>\n  <string name=\"cancel_loading_form\">ပယ်ဖျက်ပါ</string>\n  <string name=\"cancel_location\">ပယ်ဖျက်ပါ</string>\n  <string name=\"do_not_change\">မလုပ်တော့ပါ</string>\n  <string name=\"canceling\">ပယ်ဖျက်နေသည်</string>\n  <string name=\"success\">ေအာင္ျမင္ပါၿပီ</string>\n  <string name=\"error_occured\">မွားယြင္းေနပါသည္</string>\n  <string name=\"please_wait\">ေခတၱေစာင့္ပါ</string>\n  <string name=\"please_wait_long\">ေခတၱေစာင့္ပါ၊ အခ်ိန္အနည္းငယ္ယူပါသည္</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ne-rNP/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">EEE, MMM dd, yyyy \\'को\\' HH:mm \\'मा थपिएको\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">EEE, MMM dd, yyyy \\'को\\' HH:mm \\'मा संचित गरिएको\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">EEE, MMM dd, yyyy \\'को\\' HH:mm \\'मा अन्तिम रुप दिइएको\\' </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">EEE, MMM dd, yyyy \\'को\\' HH:mm \\'मा पठाईएको\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">EEE, MMM dd, yyyy \\'को\\' HH:mm \\'मा पठाउन असफल भएको\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">EEE, MMM dd, yyyy \\'को\\' HH:mm \\'मा हटाइएको\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">EEE, MMM dd, yyyy \\'को\\' HH:mm \\'मा परिवर्तन गरिएको\\'</string>\n  <string name=\"encrypted_form\">गुप्तिकृत फारम।</string>\n  <string name=\"deleted_form\">मेटाइएको फारम।</string>\n  <string name=\"sort_the_list\">सूची क्रमबद्ध गर्नुहोस्।</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">स्क्यानिङ समाप्त । सबै फारमहरू लोड भए ।</string>\n  <string name=\"xform_parse_error\">X फारमको शब्द-सम्मतीमा गल्ति भयो | %1$s मा %2$s तत्व छुटेको छ |</string>\n  <string name=\"loading_form\">फारम लोड हुदैछ |</string>\n  <string name=\"survey_loading_reading_form_message\">फारमको परिभाषा पढ़िदैछ…</string>\n  <string name=\"survey_loading_reading_data_message\">सर्भे डाटा रिड हुँदै…</string>\n  <string name=\"survey_loading_reading_csv_message\">CSV फाइल्स रिड हुँदै…</string>\n  <string name=\"parse_error\">माफ गर्नुहोला, फारमको शब्द-सम्मती गर्न सकिएन |</string>\n  <string name=\"cannot_edit_completed_form\">एक पटक पूरा भनी अंकित गरिएको फारमलाई सम्पादन गर्न मिल्दैन । सो फारम गुप्तिकृत (encrypted) गरिएको पनि हुन सक्छ । </string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">संचित नगरिएका परिवर्तनहरु संचितबिन्दुवाट पुन: निकाल्न सकियो !</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">भाषा परिवर्तन गर्नुहोस् |</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">प्रम्टमा जानुहोस् </string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">सुरुमा जानुहोस् </string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">अन्त्यमा जानुहोस् </string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">माथिल्लोमा जानुहोस् </string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">समूह</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">दोहोरिने समूह</string>\n  <string name=\"exit\">बाहिरिनुस्।</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">अर्को थप्नुहोस्</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">सम्पादन गर्न प्रेरित गर्नुहोस् </string>\n  <string name=\"clearanswer_confirm\">\\\"%s\\\"को जवाफ हटाउनु हुन्छ ?</string>\n  <string name=\"clear_answer\">जवाफ हटाउनुहोस् </string>\n  <string name=\"clear_answer_ask\">यो जवाफ हटाउनु हुन्छ?</string>\n  <string name=\"clear_answer_no\">फिर्ता जानुहोस्</string>\n  <string name=\"discard_answer\">जवाफ हटाउनुहोस् </string>\n  <string name=\"discard_group\">ग्रुप हटाउनुहोस्</string>\n  <string name=\"delete_repeat\">ग्रुप हटाउनुहोस् </string>\n  <string name=\"delete_repeat_ask\">यो ग्रुप हटाउनुहुन्छ ?</string>\n  <string name=\"delete_repeat_confirm\">\\\"%s\\\" समूह र त्यस भित्रका सबै उप-समूहलाई हटाउनुहुन्छ  ?</string>\n  <string name=\"delete_repeat_no\">फिर्ता जानुहोस्</string>\n  <string name=\"invalid_answer_error\">अमान्य प्रतिक्रिया </string>\n  <string name=\"required_answer_error\">माफ गर्नुहोला, यो उत्तर अनिवार्य चाहिन्छ !</string>\n  <string name=\"survey_internal_error\">भित्री गल्ती: प्रम्ट सम्म को चरण असफल भयो |</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">तपाई %sको अन्त्यमा हुनुहुन्छ |</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">माफ गर्नुहोला , फारम संचित हुन सकेन !</string>\n  <string name=\"data_saved_ok\">फारम सफलतापूर्वक संचित भयो !</string>\n  <string name=\"save_point_error\">रिकभरी बिन्दु संचित गर्दा गल्ति भयो: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">परिवर्तनहरु संचित गर्नुहोस् </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">फारम संचित गरिदैछ |</string>\n  <string name=\"survey_saving_validating_message\">जवाफहरू प्रमाणीकरण गरिदै…</string>\n  <string name=\"survey_saving_collecting_message\">डाटा संकलन हुँदै…</string>\n  <string name=\"survey_saving_saving_message\">मेमोरी कार्डमा सेभ हुँदै…</string>\n  <string name=\"survey_saving_finalizing_message\">मेमोरी कार्डमा अन्तिम रुप दिइदैछ…</string>\n  <string name=\"survey_saving_encrypting_message\">तथ्यांक गुप्तिकृत गरिदैछ…</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">प्रारम्भ गर्नुहोस् </string>\n  <string name=\"activity_not_found\">प्रयोग गर्न कुनै पनि क्रियाकलाप भेटिएन: %s </string>\n  <string name=\"no_app\">माग गर्नुभएको एप्लिकेशन छैन | नाप स्वयम् दाखिला गर्नुहोला |</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">URL खोल्नुहोस्</string>\n  <string name=\"capture_osm\">ओपन म्याप किट खोल्नुहोस्</string>\n  <string name=\"recapture_osm\">फेरी OSM ट्याग गर्नुहोस्</string>\n  <string name=\"alert\">सावधान</string>\n  <string name=\"install_openmapkit\">कृपया ओपन म्याप स्ट्रीट इन्सटल गर्नुहोस् !</string>\n  <string name=\"get_barcode\">बारकोड उपलब्ध गराउनुहोस्</string>\n  <string name=\"replace_barcode\">बारकोड बदल्नुहोस् </string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">बार कोडलाई समकोण (चार पाटे बाक्सा) भित्र राख्नुहोस्</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">फोटो लिनुहोस् </string>\n  <string name=\"choose_image\">तस्विर छान्नुहोस् </string>\n  <string name=\"take_picture_instruction\">फोटो लिन स्क्रीन ट्याप गर्नुहोस्</string>\n  <string name=\"error_front_camera_unavailable\">यस यन्त्रमा फ्रन्ट क्यामेरा छैन</string>\n  <string name=\"annotate_image\">तस्विर एनोटेट गर्नुहोस्।</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">संचित गरी बन्द गर्नुहोस् </string>\n  <string name=\"signature_capture\">हस्ताक्षर गर्नुहोस्</string>\n  <string name=\"sign_button\">सहि जम्मा गर्नुहोस् </string>\n  <string name=\"markup_image\">मार्कअप तस्विर</string>\n  <string name=\"draw_image\">तस्बिर कोर्नुहोस् </string>\n  <string name=\"reset_image\">पहिलाकै ठाउँमा ल्याउनुहोस् </string>\n  <string name=\"set_color\">रंग छान्नुहोस् </string>\n  <string name=\"capture_video\">भिडियो रेकर्ड गर्नुहोस् </string>\n  <string name=\"choose_video\">भिडियो छान्नुहोस् </string>\n  <string name=\"play_video\">भिडियो हेर्नुहोस्</string>\n  <string name=\"choose_file\">फाइलहरू छान्नुहोस्</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">आवाज रेकर्ड गर्नुहोस् </string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">आवाज छान्नुहोस् </string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">मिडिया अट्याच गर्दा  त्रुटि भयो: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">उत्तर छान्नुहोस्</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">छानिएका:</string>\n  <string name=\"trigger\">हुन्छ | कृपया जारी राख्नुहोस् |</string>\n  <string name=\"ext_file_no_data_error\">फाइलमा कुनै तथ्यांक छैन!</string>\n  <string name=\"ext_conflicting_columns_error\">%s महलहरु मिले |</string>\n  <string name=\"ext_sortBy_numeric_error\">तपाईले क्रमबद्ध गर्न खोज्नु भएको महलमा संख्यात्मक मानहरु मात्र हुन पर्छ | नमिलेको मान \\'%s\\' थियो |</string>\n  <string name=\"ext_search_wrong_arguments_error\">search() function मा शब्द-सम्मती को गल्ति भयो: यो function लाई तर्क 1, 4 र 6 चाहिन्छ |</string>\n  <string name=\"ext_search_wrong_function_error\">search() function मा शब्द-सम्मती को गल्ति भयो: अपरिचित function \\'%s\\'</string>\n  <string name=\"ext_search_bad_function_error\">search() function मा शब्द-सम्मती को गल्ति भयो: \\'%s\\\" लाई function जसरी मूल्यांकन गरिएन |</string>\n  <string name=\"ext_search_return_error\">खोज ह्याण्डलरले \\'%s\\' खालको वस्तु फिर्ता पठायो |</string>\n  <string name=\"ext_import_generic_error\">%1$s वाट तथ्यांक आयात गर्न सकिएन | कारण: %2$s</string>\n  <string name=\"ext_import_progress_message\">\\'%1$s\\' वाट तथ्यांक पूर्व-लोड गरिदैछ, कृपया पर्खिनुहोला…%2$s</string>\n  <string name=\"ext_import_cancelled_message\">डेटा पढ्ने रद्द गरियो!</string>\n  <string name=\"ext_import_finalizing_message\">पूर्व-लोड गरिएका तथ्यांकहरुलाई अन्तिम रुप दिइदैछ…</string>\n  <string name=\"ext_import_completed_message\">तथ्यांक पढेर सकियो !</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager लाई चालू गरिएको छैन |</string>\n  <string name=\"ext_import_csv_missing_error\">%1$s को बाह्य तथ्यांक आयात गरिएको छैन | सायद तपाईले फारमसंग %2$s.csv फाइल हाल्न बिर्सनु भयो कि ?</string>\n  <string name=\"ext_search_generic_error\">search() function मा शब्द-सम्मती को गल्ति भयो: %s</string>\n  <string name=\"ext_assign_value_error\">\\'%s\\' मा कुनै मान दिन सकिएन |</string>\n  <string name=\"get_bearing\">दिशानिर्देश रेकर्ड गर्नुहोस् </string>\n  <string name=\"replace_bearing\">दिशानिर्देश बदल्नुहोस् </string>\n  <string name=\"getting_bearing\">दिशानिर्देश लोड हुदैछ </string>\n  <string name=\"accept_bearing\">दिशानिर्देश रेकर्ड गर्नुहोस् </string>\n  <string name=\"select_date\">मिति छान्नुहोस्</string>\n  <string name=\"select_time\">समय छान्नुहोस्</string>\n  <string name=\"no_date_selected\">मिति छानिएको छैन</string>\n  <string name=\"no_time_selected\">समय छानिएको छैन</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">फाइल: %s अमान्य छ |</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">फाइल: %s छुटेको छ |</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">अमेरिकी भूगर्भीय सर्वेक्षण राष्ट्रिय नक्शा Topo</string>\n  <string name=\"openmap_usgs_sat\">अमेरिकी भूगर्भीय सर्वेक्षण राष्ट्रिय नक्शा Hybrid</string>\n  <string name=\"openmap_usgs_img\">अमेरिकी भूगर्भीय सर्वेक्षण राष्ट्रिय नक्शा Imagery</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"streets\">टोलहरु</string>\n  <string name=\"terrain\">भूभाग</string>\n  <string name=\"hybrid\">हाइव्रिड</string>\n  <string name=\"satellite\">उपग्रह</string>\n  <string name=\"google_play_services_error_occured\">गुगल  म्याप्स् मा जान सकिएन । के गुगल प्ले सर्भिसेस इन्स्टल गरिएको छ ?</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">माफ गर्नुहोस्, स्थान प्रदायकहरु निष्क्रिय गरिएका छन् !</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">पश्चिम%1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">पूर्व %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">दक्षिण %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">उत्तर %1$s%2$s%3$s</string>\n  <string name=\"start\">शुरू</string>\n  <string name=\"enable_gps\">GPS शुरु गर्नुहोस्</string>\n  <string name=\"clear\">मेटाउनुहोस्</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"discard\">त्याग्नुहोस्</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">फारम पठाईदैछ |</string>\n  <string name=\"noselect_error\">कुनै एक फारम छान्नुहोस् </string>\n  <string name=\"no_connection\">नेटवर्क कनेक्सन उपलब्ध छैन</string>\n  <string name=\"sending_items\">%2$s मध्येको %1$s फारम(हरु) पठाईदैछ |</string>\n  <string name=\"send_selected_data\">छानिएका पठाउनुहोस् </string>\n  <string name=\"server_auth_credentials\">सर्भरको प्रयोगकर्ता नाम वा पासवर्ड गलत छ %s</string>\n  <string name=\"server_requires_auth\">सर्भरलाई प्रमाणीकरण चाहिन्छ </string>\n  <string name=\"no_forms_uploaded\">माफ गर्नुहोस्, कुनै फारम पनि अपलोड गरिएन |</string>\n  <string name=\"upload_results\">अपलोडको परिणामहरु </string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">देखाइने तरिका परिवर्तन गर्नुहोस् </string>\n  <string name=\"show_sent_and_unsent_forms\">पठाईएका र नपठाईएका फारमहरू देखाउनुहोस् </string>\n  <string name=\"show_unsent_forms\">नपठाईएका फारमहरू देखाउनुहोस् </string>\n  <string name=\"send_in_progress\">पृष्‍ठभूमीमा send सन्चालित हुँदै , कृपया फेरी प्रयास गर्नुहोस् ।</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">रिफ्रेश/n/गर्नुहोस् </string>\n  <string name=\"download\">छानिएका/nपाउनुहोस्</string>\n  <string name=\"downloading_data\">सर्भरसँग कनेक्ट हुँदैछ</string>\n  <string name=\"fetching_file\">%1$s.\\n\\nफारम प्राप्त गरिदै | %3$s फारम(हरू) मध्येको %2$s </string>\n  <string name=\"form_download_progress\">%1$s. मिडिया फाइलहरु प्राप्त गरिदैछ: %3$s मध्येको %2$s</string>\n  <string name=\"version\">संस्करण:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">संचित फारमहरू </string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">खाली फारमहरू</string>\n  <string name=\"delete_confirm\">%s फारम(हरु) डिलिट गर्नु हुन्छ ?</string>\n  <string name=\"delete_file\">छानिएका डिलिट गर्नुहोस</string>\n  <string name=\"delete_no\">डिलिट नगर्नुहोस</string>\n  <string name=\"delete_yes\">फारमहरू डिलिट गर्नुहोस्</string>\n  <string name=\"file_deleted_ok\">%s फारम (हरु) डिलिट भयो !</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">प्रयोगकर्ता अन्तरक्रिया पृष्‍ठ</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">फारम व्यवस्थापन</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">प्रयोगकर्ता तथा यन्त्र पहिचान</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">सर्भर सेटिङ्स</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">सर्भर URL</string>\n  <string name=\"url_error\">गलत URL</string>\n  <string name=\"username\">प्रयोगकर्ताको नाम </string>\n  <string name=\"change_username\">ODK प्रयोगकर्ता नाम </string>\n  <string name=\"password\">पासवर्ड </string>\n  <string name=\"change_password\">ODK पासवर्ड </string>\n  <string name=\"password_error_whitespace\">पासवर्डमा खाली स्पेस हुनु हुँदैन ।</string>\n  <string name=\"username_error_whitespace\">प्रयोगकर्ता नाममा खाली स्पेस हुनु हुँदैन ।</string>\n  <string name=\"font_size\">अक्षरको आकार</string>\n  <string name=\"change_font_size\">अक्षरको आकार</string>\n  <string name=\"font_size_extra_large\">अझ ठूलो </string>\n  <string name=\"font_size_large\">ठूलो </string>\n  <string name=\"font_size_medium\">मध्यम </string>\n  <string name=\"font_size_small\">सानो </string>\n  <string name=\"font_size_extra_small\">अझ सानो </string>\n  <string name=\"navigation\">संचालन</string>\n  <string name=\"swipe_navigation\">स्वाइप को प्रयोग गर्नुहोस् </string>\n  <string name=\"buttons_navigation\">अगाडि / पछाडी बटनहरुको प्रयोग गर्नुहोस् |</string>\n  <string name=\"swipe_buttons_navigation\"> स्वाइप र बटन हरुको प्रयोग गर्नुहोस् </string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">अघि</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">पछाडि</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"form_submission_category\">फारम बुझाउँदा</string>\n  <string name=\"autosend_selector_title\">स्वत: प्रेषण</string>\n  <string name=\"autosend\">स्वत: प्रेषण</string>\n  <string name=\"wifi_autosend\">Wifi मात्र</string>\n  <string name=\"cellular_autosend\">मोबाईल ईन्टरनेट मात्र</string>\n  <string name=\"wifi_cellular_autosend\">Wifi वा मोबाईल ईन्टरनेट</string>\n  <string name=\"delete_after_send\">पठाए पछि डिलिट गर्नुहोस् </string>\n  <string name=\"delete_after_send_summary\">अन्तिम रुप दिइएको फारम र मिडिया सर्भरमा पठाए पछि डिलिट गर्छ </string>\n  <string name=\"form_filling_category\">फारम भर्दा</string>\n  <string name=\"constraint_behavior\">अभाव प्रशोधन व्यवहार </string>\n  <string name=\"constraint_behavior_on_swipe\">तेर्सो स्वाइप गरे पछी मान्य छ कि छैन हेर्नुहोला </string>\n  <string name=\"constraint_behavior_on_finalize\">अन्तिम रुप नदिए सम्म मान्यता दिन स्थगित गर्नुहोस् </string>\n  <string name=\"high_resolution_title\">ठुलो साइजको भिडियो</string>\n  <string name=\"high_resolution_summary\">उच्च स्तरीय भिडियो रेकर्ड गर्ने क्षमता सक्रिय गर्नुस् </string>\n  <string name=\"image_size_title\">फोटो साइज</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_no\">होइन</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">फारम ल्याउदा</string>\n  <string name=\"form_metadata_title\">फारम मेटा डाटा</string>\n  <string name=\"form_metadata\">फारम मेटा डाटा</string>\n  <string name=\"form_metadata_summary\">यी सूचनाहरु प्रयोगकर्ता नाम , ईमेल र / वा फोन नम्बर भरिएको फारमहरुमा डाटा भरेर बुझाउने व्यक्तीको पहिचान गर्न थप गरिनेछन् ।</string>\n  <string name=\"phone_number\">फोन नम्बर</string>\n  <string name=\"email\">ईमेल</string>\n  <string name=\"device_id\">यन्त्र पहिचान नम्बर</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">नयाँ पासवर्ड हाल्नुहोस्</string>\n  <string name=\"admin_password\">प्रशासक पासवर्ड</string>\n  <string name=\"admin_password_changed\">प्रशासक पासवर्ड सफलतापूर्वक परिवर्तन भयो</string>\n  <string name=\"admin_password_disabled\">प्रशासक पासवर्ड निष्क्रिय गरिएको छ </string>\n  <string name=\"admin_password_incorrect\">माफ गर्नुहोला, पासवर्ड गलत छ !</string>\n  <string name=\"enter_admin_password\">प्रशासक पासवर्ड हाल्नुहोस् </string>\n  <string name=\"change_admin_password\">प्रशासक पासवर्ड परिवर्तन गर्न</string>\n  <string name=\"show_password\">पासवर्ड देखाउन</string>\n  <string name=\"found_in_main\">मूल मेनु मा नदेखाउनको लागि ठिक चिन्ह हटाउनुहोस् </string>\n  <string name=\"reset_project_settings_title\">पहिलाकै ठाउँमा ल्याउनुहोस् </string>\n  <string name=\"select_all\">सबै छान्नुहोस्</string>\n  <string name=\"clear_all\">सबै मेटाउनु होस्</string>\n  <string name=\"reset_settings_button_reset\">पहिलाकै ठाउँमा ल्याउनुहोस् </string>\n  <string name=\"reset_settings_result\">सबै  सेटिङहरू::%s </string>\n  <string name=\"reset_saved_forms_result\">बचत गरिएको फारमहरू:: %s</string>\n  <string name=\"reset_blank_forms_result\">खाली  फारमहरू :: %s </string>\n  <string name=\"reset_layers_result\">नक्शाका तहहरू::%s </string>\n  <string name=\"main_menu_settings\">मूल मेनु सेटिङ्स</string>\n  <string name=\"user_settings\">प्रयोगकर्ता सेटिङ्स</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"yes\">हो</string>\n  <string name=\"no\">होइन</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">जानकारी</string>\n  <string name=\"tell_your_friends\">ODK Collect शेयर गर्नुहोस् ।</string>\n  <string name=\"tell_your_friends_msg\">के तपाईँका सहकर्मीहरू अझै कागजमा डाटा संकलन गर्दै हुनुहुन्छ? तिनीहरुसँग ODK Collect शेयर गर्नुहोस् ।</string>\n  <string name=\"leave_a_review\">हामीलाई  प्ले स्टोर आफ्नो मा समीक्षा छोड्नुहोस् ।</string>\n  <string name=\"odk_forum\">ODK मंचमा आवद्ध हुनुहोस्</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"share\">शेयर</string>\n  <string name=\"turn_on_flashlight\">flashlight अन गर्नुहोस्</string>\n  <string name=\"turn_off_flashlight\">flashlight बन्द गर्नुहोस् </string>\n  <string name=\"server_password\">सर्भर पासवर्ड</string>\n  <string name=\"include_password_dialog\">पासवर्डहरु कोड मै छन्</string>\n  <string name=\"generate\">निकाल्नुस्</string>\n  <string name=\"invalid_qrcode\">QR कोडमा सही सेटिङ्स छैन</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">ठीक</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">फिर्ता जानुहोस्</string>\n  <string name=\"cancel_loading_form\">फिर्ता जानुहोस्</string>\n  <string name=\"cancel_location\">फिर्ता जानुहोस्</string>\n  <string name=\"do_not_change\">फिर्ता जानुहोस्</string>\n  <string name=\"canceling\">फिर्ता हुँदै</string>\n  <string name=\"success\">सफल</string>\n  <string name=\"error_occured\">गल्ती भयो </string>\n  <string name=\"please_wait\">कृपया केही समय पर्खनुहोस् </string>\n  <string name=\"please_wait_long\">कृपया पर्खनुहोस् । केही समय लाग्न सक्छ । </string>\n  <string name=\"fs_delete_media_path_if_file_error\">\\'%s\\' डिलिट गर्न सकिएन | कृपया सो फाइल आफैं डिलिट गर्नुहोस् र फारम फेरी डाउनलोड गर्नुहोस् |</string>\n  <string name=\"fs_create_media_folder_error\">मिडिया फोल्डर बनाउन असमर्थ \\'%s\\'।</string>\n  <string name=\"fs_file_copy_error\">\\'%2$s\\' माथि \\'%1$s\\' कपी गर्न सकिएन | कारण: %3$s</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">तपाईंले एकै फारम आइ डी र भर्सनको दुई भिन्न फारम डाउनलोड गर्नुभयो । ती फारमहरू भिन्न समय वा भिन्न सर्भरमा अपलोड गरिएको हुन सक्छ । कुनै एक फारम डिलिट गर्नुहोस् ।</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">त्याग्नुहोस्</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-nl/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Begin met nieuw formulier</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Concepten</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Klaar voor verzending</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Verzonden</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Download formulier</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Verwijder formulier</string>\n  <string name=\"saved_forms\">Opgeslagen formulieren</string>\n  <string name=\"version_number\">Versie: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Toegevoegd op\\' EEE dd MMM yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Gewijzigd op\\' EEE dd MMM yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Opgeslagen op\\' EEE dd MMM yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Afgerond op\\' EEE dd MMM yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Verzonden op\\' EEE dd MMM yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Verzenden mislukt op\\' EEE dd MMM yyyy \\'om\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Verwijderd op\\' EEE dd MMM yyyy \\'om\\' HH:mm</string>\n  <string name=\"submission_deleted\">Ingediend formulier verwijderd</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Gewijzigd op\\' EEE dd MMM yyyy \\'om\\' HH:mm</string>\n  <string name=\"encrypted_form\">Versleuteld formulier</string>\n  <string name=\"deleted_form\">Verwijderd formulier</string>\n  <string name=\"sort_the_list\">Sorteer de lijst</string>\n  <string name=\"sort_by\">Sorteer op</string>\n  <string name=\"sort_by_name_asc\">Naam, A-Z</string>\n  <string name=\"sort_by_name_desc\">Naam, Z-A</string>\n  <string name=\"sort_by_date_desc\">Datum, nieuwste eerst</string>\n  <string name=\"sort_by_date_asc\">Datum, oudste eerst</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Laatst bewaarde eerst</string>\n  <string name=\"filter_the_list\">Lijst filteren</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Zoek</string>\n  <string name=\"storage_runtime_permission_denied_title\">Opslagrechten</string>\n  <string name=\"location_runtime_permissions_denied_title\">Locatie rechten</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">Camera rechten</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Geluidsopname rechten</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$d formulieren toegevoegd.</string>\n  <string name=\"finished_disk_scan\">Klaar met scannen, alle formulieren geladen.</string>\n  <string name=\"xform_parse_error\">Fout tijdens XForm parse voor %1$s: %2$s ontbreekt of is ongeldig</string>\n  <string name=\"loading_form\">Formulier laden</string>\n  <string name=\"survey_loading_reading_form_message\">Inlezen formulier definitie..</string>\n  <string name=\"survey_loading_reading_data_message\">Formulier gegevens inlezen…</string>\n  <string name=\"survey_loading_reading_csv_message\">Inlezen CSV bestanden…</string>\n  <string name=\"parse_error\">Formulier inlezen mislukt.</string>\n  <string name=\"cannot_edit_completed_form\">Dit formulier kan niet worden aangepast nadat het als afgewerkt is aangeduid. Mogelijk is het versleuteld.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Unsaved changes recovered from savepoint!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Taal wijzigen</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Ga naar vraag</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Bewaar als concept</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Naar begin</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Naar einde</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Ga terug</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Groep</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Herhalende groep</string>\n  <string name=\"exit\">Verlaten</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">%s toevoegen?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Toevoegen</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Voeg toe</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Edit Prompt</string>\n  <string name=\"clearanswer_confirm\">Verwijder het antwoord voor \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Verwijder antwoord</string>\n  <string name=\"clear_answer_ask\">Verwijder dit antwoord?</string>\n  <string name=\"clear_answer_no\">Terug</string>\n  <string name=\"discard_answer\">Verwijder antwoord</string>\n  <string name=\"discard_group\">Verwijder groep</string>\n  <string name=\"delete_repeat\">Groep verwijderen\n</string>\n  <string name=\"delete_repeat_ask\">Deze groep verwijderen?</string>\n  <string name=\"delete_repeat_confirm\">Groep \\\"%s\\\" en alle subgroepen verwijderen?</string>\n  <string name=\"delete_repeat_no\">Annuleer</string>\n  <string name=\"invalid_answer_error\">Sorry, dit antwoord is ongeldig!</string>\n  <string name=\"required_answer_error\">Sorry, deze vraag is verplicht!</string>\n  <string name=\"invalid_space_in_answer_singular\">Waarschuwing: de onderliggende waarde, %s, heeft spaties</string>\n  <string name=\"invalid_space_in_answer_plural\">Waarschuwing: de onderliggende waardes, %s, hebben spaties</string>\n  <string name=\"too_complex_form\">Dit formulier is te complex voor dit apparaat. Probeer de expressies te vereenvoudigen of vraag om hulp in het online forum.</string>\n  <string name=\"survey_internal_error\">Interne fout: doorgaan naar invoer mislukt.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">U bent aan het einde van \\\"%s\\\".</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Bewaar als concept</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Afronden</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Verzend</string>\n  <string name=\"data_saved_error\">Sorry, formulier opslaan mislukt!</string>\n  <string name=\"data_saved_ok\">Formulier is opgeslagen!</string>\n  <string name=\"save_point_error\">Fout bij maken herstelpunt: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Wijzigingen opslaan</string>\n  <string name=\"keep_editing\">Doorgaan met invullen</string>\n  <string name=\"quit_form_title\">Bewaar formulier?</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'Dit formulier was voor het laatst bewaard op \\' EEE dd MMM yyyy \\'om\\' HH:mm\\'. Bewaar als concept om de wijzigingen te behouden.\\'</string>\n  <string name=\"quit_form_continue_title\">Doorgaan met wijzigen?</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Formulier opslaan</string>\n  <string name=\"survey_saving_validating_message\">Valideren antwoorden…</string>\n  <string name=\"survey_saving_collecting_message\">Data verzamelen…</string>\n  <string name=\"survey_saving_saving_message\">Opslaan naar SD-kaart…</string>\n  <string name=\"survey_saving_finalizing_message\">Afronden naar SD-kaart…</string>\n  <string name=\"survey_saving_encrypting_message\">Data versleutelen…</string>\n  <string name=\"invalid_rsa_public_key\">Ongeldige publieke RSA sleutel</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Kon opname niet starten.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Microfoon wordt reeds gebruikt.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Starten</string>\n  <string name=\"activity_not_found\">Geen programma gevonden voor: %s</string>\n  <string name=\"no_app\">Benodigde applicatie ontbreekt. Voer de waarde handmatig in.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Open Url</string>\n  <string name=\"capture_osm\">OpenMapKit starten</string>\n  <string name=\"recapture_osm\">OSM Tagging opnieuw uitvoeren</string>\n  <string name=\"invalid_osm_data\">Er is iets fout gegaan. We hebben geen correcte OSM data ontvangen.</string>\n  <string name=\"install_openmapkit\">Installeer OpenMapKit alsjeblieft.</string>\n  <string name=\"get_barcode\">Scan barcode</string>\n  <string name=\"replace_barcode\">Vervang barcode</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Positioneer de barcode binnen het rechthoek</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Open bestand</string>\n  <string name=\"svg_file_does_not_exist\">SVG bestand bestaat niet!</string>\n  <string name=\"capture_image\">Foto nemen</string>\n  <string name=\"choose_image\">Selecteer afbeelding</string>\n  <string name=\"selected_invalid_image\">Geselecteerd bestand is niet een valide afbeelding</string>\n  <string name=\"camera_failed_to_initialize\">Kan de camera niet activeren!</string>\n  <string name=\"annotate_image\">Aantekening maken bij afbeelding</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Opslaan en sluiten</string>\n  <string name=\"signature_capture\">Handtekening vastleggen</string>\n  <string name=\"sign_button\">Zet handtekening</string>\n  <string name=\"markup_image\">Markup Image</string>\n  <string name=\"draw_image\">Sketch Image</string>\n  <string name=\"reset_image\">Reset</string>\n  <string name=\"set_color\">Set Color</string>\n  <string name=\"capture_video\">Video vastleggen</string>\n  <string name=\"choose_video\">Selecteer video</string>\n  <string name=\"play_video\">Bekijk video</string>\n  <string name=\"choose_file\">Bestand Kiezen</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Verwijder</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Audio vastleggen</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Selecteer audio</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Pauzeer</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Doorgaan</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Verwijder bestand?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Stop</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Opnemen…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Geluidsopname</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Speel geluid</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Opslaan bestand</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Je moet de opname stoppen voordat je dit scherm verlaat.</string>\n  <string name=\"error_attaching_binary_file\">Er is een fout opgetreden bij het koppelen van media: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Selecteer waarde</string>\n  <string name=\"select_answer\">Kies een antwoord</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Wijzig waarde</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Geen waarde geselecteerd</string>\n  <string name=\"selected_title\">Gekozen:</string>\n  <string name=\"trigger\">OK. Ga door.</string>\n  <string name=\"ext_file_no_data_error\">Het bestand is leeg!</string>\n  <string name=\"ext_conflicting_columns_error\">Kolommen %s komen overeen!</string>\n  <string name=\"ext_sortBy_numeric_error\">Gesorteerde kolom mag alleen numerieke waarden bevatten. Foutieve waarde was \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Fout in search() functie: De functie moet 1, 4 of 6 parameters hebben.</string>\n  <string name=\"ext_search_wrong_function_error\">Fout in search() functie: Onbekende functie \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Fout in search() functie: \\'%s\\' werd niet als functie geavalueerd.</string>\n  <string name=\"ext_import_generic_error\">Kon geen data importeren uit %1$s. Oorzaak: %2$s</string>\n  <string name=\"ext_import_progress_message\">Vooraf inlezen data \\'%1$s\\', een ogenblik geduld… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Data lezen geannuleerd</string>\n  <string name=\"ext_import_finalizing_message\">Afronden vooraf ingevulde data…</string>\n  <string name=\"ext_import_completed_message\">Data inlezen voltooid!</string>\n  <string name=\"ext_not_initialized_error\">De ExternalDataManager is niet opstart.</string>\n  <string name=\"ext_import_csv_missing_error\">Externe data voor %1$s kon niet ingelezen worden. Misschien was het bestand %2$s.csv niet beschikbaar bij het formulier?</string>\n  <string name=\"ext_search_generic_error\">Fout in search() functie: %s</string>\n  <string name=\"ext_assign_value_error\">Kon de waarde niet toewijzen bij \\'%s\\'.</string>\n  <string name=\"get_bearing\">Richting vastleggen</string>\n  <string name=\"replace_bearing\">Vervang richting</string>\n  <string name=\"getting_bearing\">Richting bepalen</string>\n  <string name=\"accept_bearing\">Richting vastleggen</string>\n  <string name=\"select_date\">Selecteer datum</string>\n  <string name=\"select_time\">Selecteer tijd</string>\n  <string name=\"no_date_selected\">Geen datum geselecteerd</string>\n  <string name=\"no_time_selected\">Geen tijd geselecteerd</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Bestand: %s is ongeldig.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Bestand: %s niet gevonden.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Sluit</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Reden voor wijzigingen</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Reden</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Je moet de redenen voor de wijzigingen in dit formulier uitleggen.</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Bewaar</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"choices\">Keuzes</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"basemap_source\">Bron</string>\n  <string name=\"map_style_label\">%s kaartstijl</string>\n  <string name=\"streets\">Straten</string>\n  <string name=\"terrain\">Terrein</string>\n  <string name=\"hybrid\">Hybride</string>\n  <string name=\"satellite\">Satelliet</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Licht</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Donker</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Buiten</string>\n  <string name=\"topographic\">Topografisch</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Sorry, GPS is uitgeschakeld!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Ga naar instellingen</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"start\">Start</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <string name=\"none\">Geen</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"select_item\">Selecteer</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Toon mijn locatie</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Pauzeer</string>\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Formulier verzenden</string>\n  <string name=\"noselect_error\">Sorry, u heeft geen formulier gekozen!</string>\n  <string name=\"no_connection\">Geen internet verbinding beschikbaar</string>\n  <string name=\"sending_items\">Verzenden: %1$s van %2$s formulier(en)</string>\n  <string name=\"send_selected_data\">Verstuur selectie</string>\n  <string name=\"server_auth_credentials\">Ongeldige gebruikersnaam of wachtwoord voor server: %s</string>\n  <string name=\"server_requires_auth\">Server vereist authenticatie</string>\n  <string name=\"no_forms_uploaded\">Sorry, no form was uploaded.</string>\n  <string name=\"upload_results\">Upload Results</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Wijzig weergave</string>\n  <string name=\"show_sent_and_unsent_forms\">Verzonden en niet-verzonden formulieren</string>\n  <string name=\"show_unsent_forms\">Alleen niet-verzonden formulieren</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Ververs</string>\n  <string name=\"download\">Download selectie</string>\n  <string name=\"downloading_data\">Verbinden met server</string>\n  <string name=\"form_download_progress\">%1$s. Getting media files: %2$s of %3$s</string>\n  <string name=\"version\">Versie:</string>\n  <string name=\"form_updates_available\">Formulier updates beschikbaar</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"forms_download_failed\">Downloaden formulieren mislukt</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Opgeslagen formulieren</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Lege Formulieren</string>\n  <string name=\"delete_confirm\">%s formulier(en) verwijderen?</string>\n  <string name=\"delete_file\">Verwijder selectie</string>\n  <string name=\"delete_no\">Niet verwijderen</string>\n  <string name=\"delete_yes\">Verwijderen</string>\n  <string name=\"form_delete_message\">Verwijderen van geselecteerde formulieren</string>\n  <string name=\"file_deleted_ok\">%s formulier(en) verwijderd!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Project instellingen</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Server</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, gebruikersnaam, wachtwoord</string>\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Naam, icoon, kleur</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Gebruikersinterface</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">Kaarten</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Formulierbeheer</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"protected_settings\">Beveiligd</string>\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Projectbeheer</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Toegangsbeheer</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Server</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Server URL</string>\n  <string name=\"url_error\">Sorry, ongeldige URL!</string>\n  <string name=\"username\">Gebruikersnaam</string>\n  <string name=\"change_username\">ODK Username</string>\n  <string name=\"password\">Wachtwoord</string>\n  <string name=\"change_password\">Wachtwoord</string>\n  <string name=\"app_theme\">Thema</string>\n  <string name=\"language\">Taal</string>\n  <string name=\"font_size\">Tekstgrootte</string>\n  <string name=\"change_font_size\">Tekstgrootte</string>\n  <string name=\"font_size_extra_large\">Extra groot</string>\n  <string name=\"font_size_large\">Groot</string>\n  <string name=\"font_size_medium\">Nomaal</string>\n  <string name=\"font_size_small\">Klein</string>\n  <string name=\"font_size_extra_small\">Extra klein</string>\n  <string name=\"navigation\">Navigatie</string>\n  <string name=\"swipe_navigation\">Horizontaal vegen</string>\n  <string name=\"buttons_navigation\">Navigatie knoppen</string>\n  <string name=\"swipe_buttons_navigation\">Gebruik knoppen en vegen</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Verder</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Terug</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Formulier update</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Elke vijftien minuten</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Elk uur</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Elke zes uur</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Elke 24 uur</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Automatische download</string>\n  <string name=\"automatic_download_summary\">Automatische download van geüpdatet versies van formulieren. </string>\n  <string name=\"hide_old_form_versions_setting_title\">Verberg oude formulier versies. </string>\n  <string name=\"hide_old_form_versions_setting_summary\">Alleen de nieuwste versie zal verschijnen in Vul Leeg Formulier In. </string>\n  <string name=\"autosend_selector_title\">Automatisch versturen</string>\n  <string name=\"autosend\">Automatisch versturen</string>\n  <string name=\"off\">Uit</string>\n  <string name=\"delete_after_send\">Verwijderen na versturen</string>\n  <string name=\"delete_after_send_summary\">Verwijderd afgeronde formulieren en media na verzending</string>\n  <string name=\"constraint_behavior\">Verwerking van beperkingen</string>\n  <string name=\"constraint_behavior_on_swipe\">Controleer bij doorgaan</string>\n  <string name=\"constraint_behavior_on_finalize\">Controleer bij afronden</string>\n  <string name=\"high_resolution_summary\">Hoge resolutie video opnames toestaan</string>\n  <string name=\"image_size_large\">Groot (3072px)</string>\n  <string name=\"image_size_medium\">Gemiddeld (2048px)</string>\n  <string name=\"image_size_small\">Klein (1024px)</string>\n  <string name=\"image_size_very_small\">Erg klein (640px)</string>\n  <string name=\"image_size_original\">Orginele grote van de camera (standaard)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_no\">Nee</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_metadata_title\">Formulier Metadata</string>\n  <string name=\"form_metadata\">Formulier metadata</string>\n  <string name=\"user_defined\">gebruiker gedefineerd</string>\n  <string name=\"phone_number\">Telefoonnummer</string>\n  <string name=\"email\">emailadres</string>\n  <string name=\"device_defined\">Apparaat gedefinieerd</string>\n  <string name=\"device_id\">Apparaat ID</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Nieuw wachtwoord</string>\n  <string name=\"admin_password\">Admin wachtwoord</string>\n  <string name=\"admin_password_changed\">Admin wachtwoord gewijgd</string>\n  <string name=\"admin_password_disabled\">Admin wachtwoord uitgeschakeld</string>\n  <string name=\"admin_password_incorrect\">Wachtwoord onjuist!</string>\n  <string name=\"enter_admin_password\">Geef admin wachtwoord</string>\n  <string name=\"change_admin_password\">Wijzig Admin wachtwoord</string>\n  <string name=\"show_password\">Wachtwoord tonen</string>\n  <string name=\"found_in_main\">Uitvinken om te verbergen in hoofdmenu</string>\n  <string name=\"reset_project_settings_title\">Reset</string>\n  <string name=\"delete_project\">Verwijder</string>\n  <string name=\"select_all\">Selecteer Alles</string>\n  <string name=\"reset_settings_button_reset\">Reset</string>\n  <string name=\"reset_settings_result\">Alle instellingen ::%s</string>\n  <string name=\"reset_saved_forms_result\">Opgeslagen formulieren :: 1%s</string>\n  <string name=\"reset_blank_forms_result\">Lege formulieren ::%s </string>\n  <string name=\"reset_layers_result\">Kaartlagen:: %s</string>\n  <string name=\"main_menu_settings\">Hoofdmenu instellingen</string>\n  <string name=\"user_settings\">Gebruikersinstellingen</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"no\">Nee</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Bewaar als concept</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Over</string>\n  <string name=\"tell_your_friends\">Deel ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Verzamelen jouw collega\\'s nog steeds data op papier? Deel ODK Collect met hen. </string>\n  <string name=\"leave_a_review\">Schrijf een review op de Google Play Store.</string>\n  <string name=\"leave_a_review_msg\">Jouw (hopelijk positieve) review vergroot de zichtbaarheid van de Applicatie in de Play Store</string>\n  <string name=\"odk_website\">Bezoek de ODK website</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"share\">Deel</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Terug</string>\n  <string name=\"cancel_loading_form\">Terug</string>\n  <string name=\"cancel_location\">Terug</string>\n  <string name=\"do_not_change\">Annuleer</string>\n  <string name=\"canceling\">Wordt geannuleerd</string>\n  <string name=\"notification_channel_name\">Algemene notificaties</string>\n  <string name=\"success\">Voltooid</string>\n  <string name=\"error_occured\">Fout opgetreden</string>\n  <string name=\"please_wait\">Een moment geduld alstublieft.</string>\n  <string name=\"please_wait_long\">Geduld, dit kan enkele minuten duren.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Kon bestand \\'%s\\' niet verwijderen. Verwijd het bestand handmatig en download het formulier opnieuw.</string>\n  <string name=\"fs_create_media_folder_error\">Kon media directory \\'%s\\' niet aanmaken.</string>\n  <string name=\"fs_file_copy_error\">Kon \\'%1$s\\' niet over \\'%2$s\\' kopieren. Oorzaak: %3$s</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">Toevoegen</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Ingevuld formulier is verwijderd!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Je hebt twee formulieren met een identieke form ID en versie. Misschien zijn ze op verschillende tijden of verschillende servers geupload. Je moet er één verwijderen.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-no/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Lagt til\\' EEE, MMM dd, yyyy \\'klokken\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Lagret\\' EEE, MMM dd, yyyy \\'klokken\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Sluttført den\\' EEE, MMM dd, yyyy \\'klokken\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Sendt\\' EEE, MMM dd, yyyy \\'klokken\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Sending mislyktes den\\' EEE, MMM dd, yyyy \\'klokken\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Endret den\\' EEE, MMM dd, yyyy \\'kl\\' HH:mm</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Henting fullført.</string>\n  <string name=\"loading_form\">Henter skjema</string>\n  <string name=\"survey_loading_reading_form_message\">Leser inn skjemadefinisjoner…</string>\n  <string name=\"survey_loading_reading_data_message\">Leser skjemadata…</string>\n  <string name=\"survey_loading_reading_csv_message\">Leser CSV filer…</string>\n  <string name=\"parse_error\">Beklager, klarte ikke å analysere skjema.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Ikke lagret endringer hentet fra lagringspunkt</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Endre språk</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Gå til prompt</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Gå til start</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Gå til slutten</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Gå opp</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Rediger prompt</string>\n  <string name=\"clearanswer_confirm\">Slette svar til \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Slett svar</string>\n  <string name=\"clear_answer_ask\">Slette svaret?</string>\n  <string name=\"clear_answer_no\">Avbryt</string>\n  <string name=\"discard_answer\">Slett svar</string>\n  <string name=\"discard_group\">Slett gruppe</string>\n  <string name=\"delete_repeat\">Slett gruppe</string>\n  <string name=\"delete_repeat_ask\">Slette gruppen?</string>\n  <string name=\"delete_repeat_confirm\">Slette gruppen \\\"%s\\\" og dens subgrupper?</string>\n  <string name=\"delete_repeat_no\">Avbryt</string>\n  <string name=\"invalid_answer_error\">Beklager, ugyldig svar!</string>\n  <string name=\"required_answer_error\">Beklager, svaret er påkrevd!</string>\n  <string name=\"survey_internal_error\">Intern feil: step to prompt feilet.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Du er på slutten av \\\"%s\\\".</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Beklager, skjemalagring mislyktes!</string>\n  <string name=\"data_saved_ok\">Skjema lagret!</string>\n  <string name=\"save_point_error\">Feil under lagring av gjenopprettingspunkt: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Lagre endringer</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Lagrer skjema</string>\n  <string name=\"survey_saving_validating_message\">Validerer svar…</string>\n  <string name=\"survey_saving_collecting_message\">Henter data…</string>\n  <string name=\"survey_saving_saving_message\">Lagrer til SD-kort…</string>\n  <string name=\"survey_saving_finalizing_message\">Fullfører til SD-kort…</string>\n  <string name=\"survey_saving_encrypting_message\">Krypterer data…</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Start</string>\n  <string name=\"activity_not_found\">Fant ingen handling: %s</string>\n  <string name=\"no_app\">Applikasjon mangler. Vennligst tast inn teksten manuelt</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Åpne URL</string>\n  <string name=\"capture_osm\">Start OpenMapKit</string>\n  <string name=\"recapture_osm\">Gjør om OSM tagging</string>\n  <string name=\"get_barcode\">Registrer strekkode</string>\n  <string name=\"replace_barcode\">Erstatt strekkode</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Ta bilde</string>\n  <string name=\"choose_image\">Velg bilde</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Save and Close</string>\n  <string name=\"sign_button\">Gather Signature</string>\n  <string name=\"markup_image\">Markup Image</string>\n  <string name=\"draw_image\">Sketch Image</string>\n  <string name=\"reset_image\">Reset</string>\n  <string name=\"set_color\">Set Color</string>\n  <string name=\"capture_video\">Ta opp video</string>\n  <string name=\"choose_video\">Velg video</string>\n  <string name=\"play_video\">Spill video</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Ta opp lyd</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Velg lyd</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">En feil oppstod under lagring av media: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Velg svar</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Valgt: </string>\n  <string name=\"trigger\">OK. Vennligst fortsett.</string>\n  <string name=\"ext_file_no_data_error\">Filen inneholder ingen data!</string>\n  <string name=\"ext_conflicting_columns_error\">Kolonnene %s er like!</string>\n  <string name=\"ext_sortBy_numeric_error\">Sorteringskolonnen må bare inneholde numeriske verdier. Konflikt oppstår ved verdi \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Syntaksfeil i funksjonen search(): Denne funksjonen trenger 1, 4 eller 6 argumenter.</string>\n  <string name=\"ext_search_wrong_function_error\">Syntaksfeil i funksjonen search(): Ukjent funksjon \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Syntaksfeil i funksjonen search(): \\'%s\\' er ikke en gyldig funksjon.</string>\n  <string name=\"ext_search_return_error\">Søket returnerte et objekt av typen  \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Kan ikke importere data data fra %1$s. Årsak: %2$s</string>\n  <string name=\"ext_import_progress_message\">Forhåndslater data fra \\'%1$s\\', vennligst vent… %2$s</string>\n  <string name=\"ext_import_finalizing_message\">Fullfører forhåndslastede data</string>\n  <string name=\"ext_import_completed_message\">Innlesing av data fullført!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager er ikke startet opp.</string>\n  <string name=\"ext_import_csv_missing_error\">Eksterne data for  %1$s ble ikke importert. Kanskje du har glemt å legge ved filen  %2$s.csv med skjemamalen?</string>\n  <string name=\"ext_search_generic_error\">Syntaksfeil i fuksjonen search(): %s</string>\n  <string name=\"ext_assign_value_error\">Kan ikke tildele verdien  \\'%s\\'.</string>\n  <string name=\"get_bearing\">Registrer posisjon</string>\n  <string name=\"replace_bearing\">Legg inn ny posisjon</string>\n  <string name=\"getting_bearing\">Hent posisjon</string>\n  <string name=\"accept_bearing\">Registrer posisjon</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Filen: %s er ugyldig.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Filen: %s mangler.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"google_play_services_error_occured\">Ikke mulig å åpne Google Maps. Er Google Play Services installert?</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Beklager, stedstjenesten er deaktivert!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Sender skjema</string>\n  <string name=\"noselect_error\">Beklager, du har ikke valgt skjema!</string>\n  <string name=\"no_connection\">Nettverket er ikke tilgjengelig</string>\n  <string name=\"sending_items\">Sender %1$s av %2$s skjema(er)</string>\n  <string name=\"send_selected_data\">Send utvalgte</string>\n  <string name=\"server_requires_auth\">Server krever godkjenning</string>\n  <string name=\"no_forms_uploaded\">Beklager, opplasting feilet.</string>\n  <string name=\"upload_results\">Vis oversikt</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Endre visning</string>\n  <string name=\"show_sent_and_unsent_forms\">Sendte og usendte skjemaer</string>\n  <string name=\"show_unsent_forms\">Usendte skjemaer</string>\n  <string name=\"send_in_progress\">Bakgrunnssending kjører, vennligst prøv igjen senere</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Oppdater</string>\n  <string name=\"download\">Hent utvalgte</string>\n  <string name=\"downloading_data\">Kobler til server</string>\n  <string name=\"fetching_file\">Henter %1$s.\\n\\nSkjema %2$s av %3$s skjema(er).</string>\n  <string name=\"form_download_progress\">%1$s. Henter mediafiler: %2$s av %3$s</string>\n  <string name=\"version\">Versjon:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Lagrede skjemaer</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Blanke skjemaer</string>\n  <string name=\"delete_confirm\">Slette %s skjema(er)?</string>\n  <string name=\"delete_file\">Slett utvalgte</string>\n  <string name=\"delete_no\">Ikke slett</string>\n  <string name=\"delete_yes\">Slett skjemaer</string>\n  <string name=\"file_deleted_ok\">%s skjema(er) er slettet!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Server</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Server</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Server URL</string>\n  <string name=\"url_error\">Beklager, ugyldig URL!</string>\n  <string name=\"username\">Brukernavn</string>\n  <string name=\"change_username\">ODK brukernavn</string>\n  <string name=\"password\">Passord</string>\n  <string name=\"change_password\">Passord</string>\n  <string name=\"password_error_whitespace\">Mellomrom er ikke tillatt før eller etter passord</string>\n  <string name=\"username_error_whitespace\">Mellomrom er ikke tillatt før eller etter brukernavn</string>\n  <string name=\"font_size\">Fontstørrelse</string>\n  <string name=\"change_font_size\">Fontstørrelse</string>\n  <string name=\"font_size_extra_large\">Ekstra stor</string>\n  <string name=\"font_size_large\">Stor</string>\n  <string name=\"font_size_medium\">Medium</string>\n  <string name=\"font_size_small\">Liten</string>\n  <string name=\"font_size_extra_small\">Ekstra liten</string>\n  <string name=\"navigation\">Navigation</string>\n  <string name=\"swipe_navigation\">Use horizontal swipes</string>\n  <string name=\"buttons_navigation\">Use forward/backward buttons</string>\n  <string name=\"swipe_buttons_navigation\">Use swipes and buttons</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Next</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"delete_after_send\">Slett etter innsendt.</string>\n  <string name=\"delete_after_send_summary\">Sletter fullførte skjemaer og media etter at de er sendt til serveren</string>\n  <string name=\"constraint_behavior\">Begrens prosesseringsmetoden</string>\n  <string name=\"constraint_behavior_on_swipe\">Validere ved sveip forover</string>\n  <string name=\"constraint_behavior_on_finalize\">Avvent validering til skjemaet er fullført</string>\n  <string name=\"high_resolution_summary\">Slå på høyoppløst videoopptak.</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Enter new password</string>\n  <string name=\"admin_password_changed\">Admin password successfully changed</string>\n  <string name=\"admin_password_disabled\">Admin password disabled</string>\n  <string name=\"admin_password_incorrect\">Sorry, password is incorrect!</string>\n  <string name=\"enter_admin_password\">Enter Admin Password</string>\n  <string name=\"found_in_main\">Uncheck to hide from Main Menu</string>\n  <string name=\"reset_project_settings_title\">Reset</string>\n  <string name=\"reset_settings_button_reset\">Reset</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Avbryt</string>\n  <string name=\"cancel_loading_form\">Avbryt</string>\n  <string name=\"cancel_location\">Avbryt</string>\n  <string name=\"do_not_change\">Avbryt</string>\n  <string name=\"success\">Suksess</string>\n  <string name=\"error_occured\">Det er oppstatt en feil</string>\n  <string name=\"please_wait\">Vennligst vent.</string>\n  <string name=\"please_wait_long\">Vennligst vent. Det kan ta litt tid.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Kan ikke slette \\'%s\\'. Slett filen manuelt, og last ned skjemamal på nytt. </string>\n  <string name=\"fs_create_media_folder_error\">Kan ikke opprette mediamappe \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Kan ikke kopiere \\'%1$s\\' over \\'%2$s\\'. Årsak: %3$s</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Du har lastet ned to ulike skjemamaler med samme skjema-ID og versjon. Kanskje er samme skjemamal lastet opp til ulike tidspunkt eller til ulike servere. Uansett bør du slette en av dem. </string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-pl/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">Wersja: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Dodany\\' EEE, MMM dd, yyyy \\'o\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Zapisany\\' EEE, MMM dd, yyyy \\'o\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Zakończony\\' EEE, MMM dd, yyyy \\'o\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Wysłany\\' EEE, MMM dd, yyyy \\'o\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Wysyłanie nie powiodło się\\' EEE, MMM dd, yyyy \\'o\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Skasowany \\' EEE, MMM dd, yyyy \\'o\\' HH:mm</string>\n  <string name=\"submission_deleted\">Usunięto zapis</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Zmieniony\\' EEE, MMM dd, yyyy \\'o\\' HH:mm</string>\n  <string name=\"encrypted_form\">Formularz zaszyfrowany</string>\n  <string name=\"deleted_form\">Formularz usunięty</string>\n  <string name=\"sort_the_list\">Sortuj listę</string>\n  <string name=\"sort_by\">Sortuj według</string>\n  <string name=\"sort_by_name_asc\">Nazwa, A-Z</string>\n  <string name=\"sort_by_name_desc\">Nazwa, Z-A</string>\n  <string name=\"sort_by_date_desc\">Data, najpierw najnowsze</string>\n  <string name=\"sort_by_date_asc\">Data, najpierw najstarsze</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">Filtruj listę</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Szukaj</string>\n  <string name=\"not_granted_permission\">Aktywność, którą próbujesz wykonać, wymaga autoryzacji, której nie posiadasz.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Uprawnienia do przechowywania</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Bez tych uprawnieć Collect nie ma dostępu do formularzy i nie może zapisywać odpowiedzi. Spróbuj ponownie, kiedy uprawnienia zostaną nadane.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Uprawnienia dostępu do lokalizacji</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">Uprawnienia dostępu do kamery</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Bez tych uprawnieć Collect nie ma dostępu do kamery. Spróbuj ponownie, kiedy uprawnienia zostaną nadane.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Uprawnienia do nagrywania dźwięku</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Bez tych uprawnieć Collect nie ma dostępu do mikrofonu. Spróbuj ponownie, kiedy uprawnienia zostaną nadane.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">Dodano %1$d formularzy.</string>\n  <string name=\"finished_disk_scan\">Zakończone skanowanie. Wszystkie formularze są załadowane.</string>\n  <string name=\"xform_parse_error\">Xform błąd przetwarzania dla %1$s: %2$s nie istnieje lub jest błędem.</string>\n  <string name=\"loading_form\">Wczytywanie Formularza</string>\n  <string name=\"survey_loading_reading_form_message\">Wczytuję definicję formularza…</string>\n  <string name=\"survey_loading_reading_data_message\">Wczytuję zebrane dane…</string>\n  <string name=\"survey_loading_reading_csv_message\">Wczytuję pliki CSV…</string>\n  <string name=\"parse_error\">Przykro mi, nie mogę zanalizować formularza.</string>\n  <string name=\"loading_form_failed\">Wystąpił błąd w trakcie ładowania formularza. Spróbuj ponownie,</string>\n  <string name=\"cannot_edit_completed_form\">Po zaznaczeniu jako skończony ten formularz nie może być edytowany. Może być zaszyfrowany.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Niezapisane zmiany odzyskane z punktu zapisu!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Zmień Język</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Idź Do Pytania</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Idź Do Początku</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Idź Na Koniec</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Idź Do Góry</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Grupa</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Powtarzająca się Grupa</string>\n  <string name=\"exit\">Wyjdź</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Dodać \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Dodaj</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Dodaj kolejne</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Edytuj Pytanie</string>\n  <string name=\"clearanswer_confirm\">Czy usunąć odpowiedź na \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Usuń odpowiedź</string>\n  <string name=\"clear_answer_ask\">Czy Usunąć Tę Odpowiedź?</string>\n  <string name=\"clear_answer_no\">Anuluj</string>\n  <string name=\"discard_answer\">Usuń odpowiedź</string>\n  <string name=\"discard_group\">Usuń grupę</string>\n  <string name=\"delete_repeat\">Usuń grupę</string>\n  <string name=\"delete_repeat_ask\">Czy Usunąć Tę Grupę?</string>\n  <string name=\"delete_repeat_confirm\">Czy usunąć grupę \\\"%s\\\" i wszystkie jej podgrupy?</string>\n  <string name=\"delete_repeat_no\">Anuluj</string>\n  <string name=\"invalid_answer_error\">Przykro mi, ta odpowiedź jest niepoprawna!</string>\n  <string name=\"required_answer_error\">Przykro mi, ta odpowiedź jest wymagana!</string>\n  <string name=\"invalid_space_in_answer_singular\">Ostrzeżenie: wartość podstawowa %s zawiera spacje</string>\n  <string name=\"invalid_space_in_answer_plural\">Ostrzeżenie: wartości podstawowe %s zawierają spacje</string>\n  <string name=\"too_complex_form\">Formularz jest zbyt skomplikowany dla tego urządzenia. Spróbuj uprościć wyrażenia, lub poproś o pomoc na forum.</string>\n  <string name=\"survey_internal_error\">Błąd wewnętrzny: przejście do pytania nie powiodło się.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Jesteś przy końcu %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Przykro mi, zapis formularza się nie powiódł!</string>\n  <string name=\"data_saved_ok\">Formularz został zapisany pomyślnie!</string>\n  <string name=\"save_point_error\">Błąd przy zapisywaniu punktu odtwarzania: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Zapisz Zmiany</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Zapisuję Formularz</string>\n  <string name=\"survey_saving_validating_message\">Sprawdzam poprawność odpowiedzi…</string>\n  <string name=\"survey_saving_collecting_message\">Zbieram danych…</string>\n  <string name=\"survey_saving_saving_message\">Zapisuję na kartę SD…</string>\n  <string name=\"survey_saving_finalizing_message\">Zakańczam na kartę SD…</string>\n  <string name=\"survey_saving_encrypting_message\">Szyfruję dane…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Więcej niż jeden zapis dla tego wystąpienia!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Więcej niż jeden pusty formularz odpowiada temu jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Nie wybrano FormID???</string>\n  <string name=\"phone_does_not_support_rsa\">Telefon nie wspiera szyfrowania RSA.</string>\n  <string name=\"invalid_rsa_public_key\">Niepoprawny klucz publiczny RSA.</string>\n  <string name=\"encryption_error_message\">Formularz %s nie został zapisany jako zakończony.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Uruchom</string>\n  <string name=\"activity_not_found\">Nie znaleziono aktywności do obsługi: %s</string>\n  <string name=\"no_app\">Brakuje żądanej aplikacji. Proszę, wprowadź ręcznie odczytane wartości.</string>\n  <string name=\"null_intent_value\">Zewnętrzna aplikacja nie dostarczyła oczekiwanych informacji.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Otwórz URL</string>\n  <string name=\"capture_osm\">Uruchom OpenMapKit</string>\n  <string name=\"recapture_osm\">Ponów oznaczanie OSM </string>\n  <string name=\"edited_osm_file\">Edytowany plik OSM XML:</string>\n  <string name=\"invalid_osm_data\">Coś się nie udało. Nie dostaliśmy poprawnych danych OSM.</string>\n  <string name=\"alert\">Alarm</string>\n  <string name=\"install_openmapkit\">Proszę zainstalować OpenMapKit!</string>\n  <string name=\"get_barcode\">Pobierz Kod Kreskowy</string>\n  <string name=\"replace_barcode\">Zastąp Kod Kreskowy</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Umieść kod kreskowy wewnątrz prostokąta</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Otwórz plik</string>\n  <string name=\"rank_items\">Uszereguj pozycje</string>\n  <string name=\"number_picker_title\">Wybór Wartości</string>\n  <string name=\"invalid_range_widget\">Ten widget ma nieprawidłowe parametry!</string>\n  <string name=\"svg_file_does_not_exist\">Plik SVG nie istnieje!</string>\n  <string name=\"capture_image\">Zrób Zdjęcie</string>\n  <string name=\"choose_image\">Wybierz Obraz</string>\n  <string name=\"selected_invalid_image\">Wybrany plik nie jest poprawnym obrazem</string>\n  <string name=\"take_picture_instruction\">Dotknij ekran, by zrobić zdjęcie</string>\n  <string name=\"error_front_camera_unavailable\">Przednia kamera nie jest dostępna w tym urządzeniu</string>\n  <string name=\"annotate_image\">Dodaj opis Obrazu</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Zapisz i Zamknij</string>\n  <string name=\"signature_capture\">Przechwytywanie Podpisu</string>\n  <string name=\"sign_button\">Pobierz Podpis</string>\n  <string name=\"markup_image\">Zaznacz na Obrazie</string>\n  <string name=\"draw_image\">Naszkicuj Obraz</string>\n  <string name=\"reset_image\">Resetuj</string>\n  <string name=\"set_color\">Ustaw Kolor</string>\n  <string name=\"capture_video\">Nagraj Wideo</string>\n  <string name=\"choose_video\">Wybierz Wideo</string>\n  <string name=\"play_video\">Odtwórz Wideo</string>\n  <string name=\"choose_file\">Wybierz plik</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Nagraj Dźwięk</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Wybierz Dźwięk</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">Wystąpił błąd w momencie dołączania mediów: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Wybierz wartość</string>\n  <string name=\"select_answer\">Wybierz Odpowiedź</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Edytuj wartość</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Nie wybrano wartości</string>\n  <string name=\"selected_title\">Wybrany:</string>\n  <string name=\"trigger\">OK. Kontynuuj, proszę.</string>\n  <string name=\"ext_file_no_data_error\">Plik nie zawiera danych!</string>\n  <string name=\"ext_conflicting_columns_error\">Kolumny %s pasują1</string>\n  <string name=\"ext_sortBy_numeric_error\">Kolumna, wg której sortujesz, powinna zawierać wyłącznie wartości liczbowe. Problem sprawiła wartość \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Błąd składni w funkcji search(): Funkcja potrzebuje 1, 4 lub 6 argumentów.</string>\n  <string name=\"ext_search_wrong_function_error\">Błąd składni w funkcji search(): Nierozpoznana funkcja \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Błąd składni w funkcji search(): \\'%s\\' nie została obliczona jako funkcja.</string>\n  <string name=\"ext_search_return_error\">Obsługa wyszukiwania zwróciła obiekt typu \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Nie mogę zaimportować danych z %1$s. Powód: %2$s</string>\n  <string name=\"ext_import_progress_message\">Wstępne ładowanie danych z \\'%1$s\\'. Proszę czekać… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Odczyt danych anulowany!</string>\n  <string name=\"ext_import_finalizing_message\">Kończę wstępne ładowanie danych…</string>\n  <string name=\"ext_import_completed_message\">Zakońyłem wczytywanie danych!</string>\n  <string name=\"ext_not_initialized_error\">Manager Danych Zewnętrznych nie został inicjalizowany.</string>\n  <string name=\"ext_import_csv_missing_error\">Dane zewnętrzne dla %1$s nie zostały zaimportowane. Czy zapomniałeś załączyć %2$s.csv plik do swojego formularza?</string>\n  <string name=\"ext_search_generic_error\">Błąd składni w funkcji search(): %s</string>\n  <string name=\"ext_assign_value_error\">Nie mogę przypisać wartości do \\'%s\\'.</string>\n  <string name=\"parser_exception\">Wyjątek XPathParser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Zapisz Namiar</string>\n  <string name=\"replace_bearing\">Zamień Namiar</string>\n  <string name=\"getting_bearing\">Ładuję Namiar</string>\n  <string name=\"accept_bearing\">Zapisz Namiar</string>\n  <string name=\"bearing\">Namiar: %.3f</string>\n  <string name=\"direction\">Kierunek: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Nie można ustalić lokalizacji: urządzenie nie posiada akcelerometru, sensora pola magnetycznego, lub obu.</string>\n  <string name=\"select_date\">Wybierz datę</string>\n  <string name=\"select_time\">Wybierz godzinę</string>\n  <string name=\"no_date_selected\">Nie wybrano daty</string>\n  <string name=\"no_time_selected\">Nie wybrano godziny</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Plik: %s jest niepoprawny.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Plik: %s nie został odnaleziony.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Wprowadź swoje dane</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Tożsamość</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Ten formularz wymaga podania twoich danych.</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Zamknij</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Powód zmian</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Powód</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Musisz uzasadnić powód zmian w tym formularzu</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Zapisz</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_usgs_sat\">USGS National Map Hybrid</string>\n  <string name=\"openmap_usgs_img\">USGS National Map Imagery</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Mapa bazowa</string>\n  <string name=\"basemap_source\">Źródło</string>\n  <string name=\"map_style_label\">%s styl mapy</string>\n  <string name=\"streets\">Ulice</string>\n  <string name=\"terrain\">Teren</string>\n  <string name=\"hybrid\">Hybryda</string>\n  <string name=\"satellite\">Satelita</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Jasny</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Ciemny</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Na zewnątrz</string>\n  <string name=\"topographic\">Topograficzny</string>\n  <string name=\"google_play_services_error_occured\">Nie można uzyskać dostępu do Google Maps. Czy Google Play Services są zainstalowane?</string>\n  <string name=\"carto_map_style_positron\">Pozyton</string>\n  <string name=\"carto_map_style_dark_matter\">Ciemna Materia</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">Zapisz punkt</string>\n  <string name=\"provider_disabled_error\">Przykro mi, dostawcy usługi Lokalizacji są niedostępni.</string>\n  <string name=\"google_play_services_not_available\">Ten formularz chce śledzić twoją lokalizację, ale usługi Google Play nie są dostępne.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Ten formularz chce śledzić twoją lokalizację, ale usługi dostawców systemu lokalizacji są wyłączone. Włącz je w ustawieniach Androida.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Przejdź do ustawień</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"background_location_disabled\">Ten formularz chce śledzić twoją lokalizację, jednak funkcja śledzenia jest wyłączona. Włącz ją wybierając %1$s w menu powyżej.</string>\n  <string name=\"background_location_enabled\">Ten formularz śledzi twoją lokalizację. Możesz wyłączyć funkcję śledzenia poprzez %1$s w menu powyżej.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">Z %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">W %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">Pd %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">Pn %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Metoda wprowadzania</string>\n  <string name=\"start\">Zacznij</string>\n  <string name=\"enable_gps\">Włącz GPS</string>\n  <string name=\"gps_enable_message\">GPS jest nieaktywne na twoim urządzeniu. Czy chcesz je włączyc?</string>\n  <string name=\"geo_clear_warning\">Właściwość już stworzona. Chcesz usunąć właściwość?</string>\n  <string name=\"geo_exit_warning\">Usunąć zmiany i wrócić do ODK?</string>\n  <string name=\"polygon_validator\">Wymagane są przynajmniej 3 punkty aby utworzyć wielokąt</string>\n  <string name=\"polyline_validator\">Wymagane co najmniej 2 punkty, by utworzyć polilinię</string>\n  <string name=\"clear\">Wyczyść</string>\n  <string name=\"placement_mode\">Wybór lokalizacji przez dotknięcie</string>\n  <string name=\"manual_mode\">Manualny zapis lokalizacji</string>\n  <string name=\"automatic_mode\">Automatyczny zapis lokalizacji</string>\n  <string name=\"recording_interval\">Interwały zapisu:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d sekunda</item>\n    <item quantity=\"few\">%d sekund</item>\n    <item quantity=\"many\">%d sekund</item>\n    <item quantity=\"other\">%d sekundy</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d minuta</item>\n    <item quantity=\"few\">%d minut</item>\n    <item quantity=\"many\">%d minut</item>\n    <item quantity=\"other\">%d minuty</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Wymagana dokładność:</string>\n  <string name=\"none\">Żaden</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d metr</item>\n    <item quantity=\"few\">%d metrów</item>\n    <item quantity=\"many\">%d metrów</item>\n    <item quantity=\"other\">%d metry</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Wprowadzone punkty: %d</string>\n  <string name=\"collection_status_placement\">Wprowadzone punkty: %d (dotknij, by dodać)</string>\n  <string name=\"collection_status_manual\">Wprowadzone punkty: %d (wprowadzanie manualne)</string>\n  <string name=\"collection_status_auto_seconds\">Wprowadzone punkty: %1$d (zapis co %2$d sek)</string>\n  <string name=\"collection_status_auto_minutes\">Wprowadzone punkty: %1$d (zapis co %2$d min)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Wprowadzone punkty: %1$d (zapis co %2$d sek, dokładność%3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Wprowadzone punkty: %1$d (zapis co %2$d min, dokładność%3$d m)</string>\n  <string name=\"geopoint_no_draggable_instruction\">Dotknij przycisk Dodaj znacznik.</string>\n  <string name=\"discard\">Usuń</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Wysyłanie Formularza</string>\n  <string name=\"noselect_error\">Przykro mi, nie zaznaczyłeś żadnego formularza!</string>\n  <string name=\"no_connection\">Brak połączenia do sieci</string>\n  <string name=\"sending_items\">Wysyłanie %1$s z %2$s formularza/y</string>\n  <string name=\"send_selected_data\">Wyślij Wybrane</string>\n  <string name=\"server_auth_credentials\">Nieprawidłowa nazwa użytkownika lub hasło do serwera: %s</string>\n  <string name=\"server_requires_auth\">Serwer Wymaga Uwierzytelnienia</string>\n  <string name=\"no_forms_uploaded\">Przykro mi, nie wysłano żadnego formularza.</string>\n  <string name=\"upload_results\">Wyślij Wyniki</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Zmień Widok</string>\n  <string name=\"show_sent_and_unsent_forms\">Pokaż Wysłane i Nie Wysłane Formularze</string>\n  <string name=\"show_unsent_forms\">Pokaż Nie Wysłane Formularze</string>\n  <string name=\"send_in_progress\">Trwa wysyłka w tle, spróbuj wkrótce ponownie</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Odśwież</string>\n  <string name=\"download\">Pobierz Wybrane</string>\n  <string name=\"downloading_data\">Łączenie z Serwerem</string>\n  <string name=\"fetching_file\">Pobieram %1$s.\\n\\nFomularz %2$s z %3$s formularzy.</string>\n  <string name=\"form_download_progress\">%1$s. Pobieram pliki medialne: %2$s z %3$s</string>\n  <string name=\"version\">Wersja:</string>\n  <string name=\"form_updates_available\">Dostępne nowe wersje formularzy</string>\n  <string name=\"newer_version_of_a_form_info\">To aktualizacja do twojego formularza</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Zapisane Formularze</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Czyste Formularze</string>\n  <string name=\"delete_confirm\">Czy usunąć %s formularz(y)?</string>\n  <string name=\"delete_file\">Skasuj Wybrane</string>\n  <string name=\"delete_no\">Nie Kasuj</string>\n  <string name=\"delete_yes\">Skasuj Formularze</string>\n  <string name=\"form_delete_message\">Kasowanie wybranych formularzy</string>\n  <string name=\"file_deleted_ok\">%s formularz/y zostało skasowanych!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Serwer</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">Interfejs użytkownika</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">Mapy</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Zarządzanie formularzem</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Tożsamość użytkownika i urządzenia</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Ustawienia Serwera</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Serwer URL</string>\n  <string name=\"url_error\">Przykro mi, niepoprawny URL!</string>\n  <string name=\"username\">Identyfikator użytkownika</string>\n  <string name=\"change_username\">Nazwa użytkownika ODK</string>\n  <string name=\"password\">Hasło</string>\n  <string name=\"change_password\">Hasło ODK</string>\n  <string name=\"password_error_whitespace\">Spacje na początku lub na końcu hasła są niedozwolone</string>\n  <string name=\"username_error_whitespace\">Spacje na początku lub na końcu nazwy użytkownika są niedozwolone</string>\n  <string name=\"app_theme\">Motyw</string>\n  <string name=\"language\">Język</string>\n  <string name=\"use_device_language\">Użyj języka urządzenia</string>\n  <string name=\"font_size\">Wielkość czcionki</string>\n  <string name=\"change_font_size\">Wielkość czcionki</string>\n  <string name=\"font_size_extra_large\">Extra Duży</string>\n  <string name=\"font_size_large\">Duży</string>\n  <string name=\"font_size_medium\">Średni</string>\n  <string name=\"font_size_small\">Mały</string>\n  <string name=\"font_size_extra_small\">Extra Mały</string>\n  <string name=\"navigation\">Nawigacja</string>\n  <string name=\"swipe_navigation\">Użyj horyzontalnych przeciągnięć</string>\n  <string name=\"buttons_navigation\">Użyj przycisków następny/poprzedni</string>\n  <string name=\"swipe_buttons_navigation\">Użyj przeciągnięć i przycisków</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Następny</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Powrót</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Aktualizacja formularza</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Co piętnaście minut</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Co godzinę</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Co sześć godzin</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Co 24 godziny</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Automatyczne pobieranie</string>\n  <string name=\"automatic_download_summary\">Automatycznie pobierz zaktualizowane wersje formularzy</string>\n  <string name=\"hide_old_form_versions_setting_title\">Ukryj stare wersje formularzy</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Tylko najnowsza wersja pojawi się w Wypełnij Pusty Formularz</string>\n  <string name=\"form_submission_category\">Przesyłanie formularzu</string>\n  <string name=\"autosend_selector_title\">Automatyczne wysyłanie</string>\n  <string name=\"autosend\">Automatyczne wysyłanie</string>\n  <string name=\"off\">Wyłączone</string>\n  <string name=\"wifi_autosend\">Tylko wifi</string>\n  <string name=\"cellular_autosend\">Tylko sieć</string>\n  <string name=\"wifi_cellular_autosend\">Wifi oraz sieć</string>\n  <string name=\"delete_after_send\">Skasuj po wysłaniu</string>\n  <string name=\"delete_after_send_summary\">Usuń zakończone formularze i pliki medialne po wysłaniu na serwer.</string>\n  <string name=\"form_filling_category\">Wypełnianie formularzu</string>\n  <string name=\"constraint_behavior_title\">Przetwarzanie ograniczeń</string>\n  <string name=\"constraint_behavior\">Przetwarzam zachowanie ograniczeń</string>\n  <string name=\"constraint_behavior_on_swipe\">Sprawdź poprawność po przeciągnięciu do przodu</string>\n  <string name=\"constraint_behavior_on_finalize\">Odłóż sprawdzanie poprawności do chwili zakończenia</string>\n  <string name=\"high_resolution_title\">Wideo w wysokiej rozdzielczości</string>\n  <string name=\"high_resolution_summary\">Włącz nagrywanie wideo wysokiej rozdzielczości</string>\n  <string name=\"image_size_title\">Rozmiar Obrazu</string>\n  <string name=\"image_size_dialog_title\">Maksymalna liczba pikseli wzdłuż długiej krawędzi obrazu</string>\n  <string name=\"image_size_large\">Duży (3072px)</string>\n  <string name=\"image_size_medium\">Średni (2048px)</string>\n  <string name=\"image_size_small\">Mały (1024px)</string>\n  <string name=\"image_size_very_small\">Bardzo mały (640px)</string>\n  <string name=\"image_size_original\">Rozmiar oryginalny (domyślne)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Pokaż wskazówki do pytań</string>\n  <string name=\"guidance_no\">Nie</string>\n  <string name=\"guidance_yes\">Tak - zawsze pokazuj</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Tak - zawijaj</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">Import formularzu</string>\n  <string name=\"instance_sync\">Finalizuj formularze podczas importu</string>\n  <string name=\"instance_sync_summary\">Wpływa na formularze dodawane bezpośrednio do folderu formularzy.</string>\n  <string name=\"analytics_preferences\">Dane użytkowania</string>\n  <string name=\"analytics\">Zbieraj anonimowe dane dotyczące użytkowania</string>\n  <string name=\"form_metadata_title\">Metadane formularza</string>\n  <string name=\"form_metadata\">Metadane formularza</string>\n  <string name=\"form_metadata_summary\">Wartości te zostaną dodane do formularzy z uprzednio zdefiniowanymi polami z nazwą użytkownika, adresem email i / lub telefonem w celu identyfikacji osoby przesyłającej dane.</string>\n  <string name=\"user_defined\">Zdefiniowane przez użytkownika</string>\n  <string name=\"phone_number\">Numer telefonu</string>\n  <string name=\"email\">Adres email</string>\n  <string name=\"invalid_email_address\">Nieprawidłowy adres email!</string>\n  <string name=\"device_defined\">Zdefiniowane przez urządzenie</string>\n  <string name=\"device_id\">ID urządzenia</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Wprowadź nowe hasło</string>\n  <string name=\"admin_password\">Hasło Administratora</string>\n  <string name=\"admin_password_changed\">Hasło administratora zostało zmienione</string>\n  <string name=\"admin_password_disabled\">Hasło administratora jest wyłączone</string>\n  <string name=\"admin_password_incorrect\">Przykro mi, hasło jest niepoprawne!</string>\n  <string name=\"enter_admin_password\">Wprowadź Hasło Administratora</string>\n  <string name=\"change_admin_password\">Zmień Hasło Administratora</string>\n  <string name=\"show_password\">Pokaż hasło</string>\n  <string name=\"found_in_main\">Odznacz aby ukryć w Głównym Menu</string>\n  <string name=\"form_entry\">Odznacz aby ukryć z Wypełniania Formularza</string>\n  <string name=\"reset_project_settings_title\">Reset</string>\n  <string name=\"select_all\">Zaznacz Wszystkie</string>\n  <string name=\"clear_all\">Odznacz Wszystkie</string>\n  <string name=\"reset_settings\">Wszystkie ustawienia (ustawienia wewnętrzne, ustawienia zapisane)</string>\n  <string name=\"reset_saved_forms\">Zapisane formularze (folder formularzy, baza danych formularzy)</string>\n  <string name=\"reset_blank_forms\">Czyste formularze (folder formularzy, baza danych formularzy, baza danych zbiorów pozycji) </string>\n  <string name=\"reset_layers\">Warstwy mapy (folder warstw)</string>\n  <string name=\"reset_cache\">Pamięć podręczna załadowanego formularza (folder cache)</string>\n  <string name=\"reset_settings_dialog_title\">Wybierz co zresetować</string>\n  <string name=\"reset_settings_button_reset\">Reset</string>\n  <string name=\"reset_in_progress\">Resetowanie…</string>\n  <string name=\"reset_app_state_result\">Wyniki resetu</string>\n  <string name=\"reset_app_state_warning\">Wszystkie wybrane dane zostaną trwale usunięte. Nie ma możliwości cofnięcia operacji.</string>\n  <string name=\"reset_settings_result\">Wszystkie ustawienia :: %s</string>\n  <string name=\"reset_saved_forms_result\">Zapisane formularze :: %s</string>\n  <string name=\"reset_blank_forms_result\">Puste formularze :: %s</string>\n  <string name=\"reset_layers_result\">Warstwy mapy :: %s</string>\n  <string name=\"reset_cache_result\">Pamięć podręczna załadowanego formularza :: %s</string>\n  <string name=\"main_menu_settings\">Ustawienia Menu Głównego</string>\n  <string name=\"user_settings\">Ustawienia użytkownika</string>\n  <string name=\"form_entry_setting\">Ustawienia Wprowadzania Formularza</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Cofanie się</string>\n  <string name=\"moving_backwards_disabled_title\">Cofanie się nie jest możliwe</string>\n  <string name=\"yes\">Tak</string>\n  <string name=\"no\">Nie</string>\n  <string name=\"moving_backwards_enabled_title\">Cofanie się jest możliwe</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">O Aplikacji</string>\n  <string name=\"tell_your_friends\">Udostępnij ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Czy Twoi koledzy wciąż używają do zbierania danych arkuszy papieru? Podziel się z nimi ODK Collect.</string>\n  <string name=\"leave_a_review\">Zostaw swoją opinię na Play Store</string>\n  <string name=\"leave_a_review_msg\">Twoja (mamy nadzieję pozytywna) recenzja przyczyni się do zwiększenia widoczności Aplikacji w Play Store.</string>\n  <string name=\"odk_website\">Odwiedź stronę ODK</string>\n  <string name=\"odk_website_summary\">ODK służy do zbierania danych w trudnych warunkach dla społeczego dobra.</string>\n  <string name=\"odk_forum\">Dołącz do forum ODK</string>\n  <string name=\"odk_forum_summary\">Dołącz do forum by uzyskać wsparcie, zgłaszać potrzeby nowych opcji, wnieść swój wkład w tłumaczenie i kodowanie.</string>\n  <string name=\"all_open_source_licenses\">Biblioteki/licencje open source</string>\n  <string name=\"all_open_source_licenses_msg\">Stoimy na ramionach gigantów!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"successfully_imported_settings\">Ustawienia zostały pomyślnie zaimportowane</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Osiągnięto maksymalną liczbę znaków: kod QR nie może zostać wygenerowany dla wszystkich ustawień</string>\n  <string name=\"share\">Udostępnij</string>\n  <string name=\"turn_on_flashlight\">Włącz latarkę</string>\n  <string name=\"turn_off_flashlight\">Wyłącz latarkę</string>\n  <string name=\"server_password\">Hasło Serwera</string>\n  <string name=\"include_password_dialog\">Hasło Zawarte w Kodzie</string>\n  <string name=\"generate\">Wygeneruj</string>\n  <string name=\"invalid_qrcode\">Kod QR nie zawiera poprawnych ustawień</string>\n  <string name=\"qr_code_not_found\">Na wybranym zdjęciu nie odnaleziono kodu QR</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Anuluj</string>\n  <string name=\"cancel_loading_form\">Anuluj</string>\n  <string name=\"cancel_location\">Anuluj</string>\n  <string name=\"do_not_change\">Anuluj</string>\n  <string name=\"canceling\">Anulowanie</string>\n  <string name=\"notification_channel_name\">Ogólne powiadomienia</string>\n  <string name=\"success\">Sukces</string>\n  <string name=\"error_occured\">Wystąpił Błąd</string>\n  <string name=\"please_wait\">Poczekaj kilka chwil.</string>\n  <string name=\"please_wait_long\">Poczekaj. To może zająć kilka minut.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Nie mogę skasować \\'%s\\'. Skasuj, proszę, plik manualnie i ponownie pobierz formularz.</string>\n  <string name=\"fs_create_media_folder_error\">Nie mogę utworzyć folderu mediów \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Nie mogę skopiować \\'%1$s\\' na \\'%2$s\\'. Powód: %3$s</string>\n  <string name=\"select_odk_shortcut\">Wybierz skrót ODK</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">Dodaj</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Nierozpoznany URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Zły URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Pobrałeś dwa różne formularze z tym samym ID formularza oraz wersją. Może był to ten sam formularz wysłany w różnym czasie lub na różne serwery. Powinieneś usunąć jeden z tych formularzy.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Usuń</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ps/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"cannot_edit_completed_form\">دا فورمه کله چې بشپړ شوي په توګه نښه شوې وي نه شي کولی چی تصحیح یی کړی. دا کیدی شي کوډ شوی وي.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">ژبه تغیر کړی</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">اولي برخي ته لاړ شی</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">اخرني برخي ته لاړ شی</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">پورته لاړ شی</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"clearanswer_confirm\">ځواب %sته لري کړي</string>\n  <string name=\"clear_answer\">ځواب لري کړی</string>\n  <string name=\"clear_answer_ask\">ځواب لري کړی؟</string>\n  <string name=\"clear_answer_no\">فسخ کول</string>\n  <string name=\"discard_answer\">ځواب لري کړی</string>\n  <string name=\"discard_group\">ډله لیري کړي</string>\n  <string name=\"delete_repeat\">ډله لیري کړي</string>\n  <string name=\"delete_repeat_ask\">دغه ډله لری کړي؟</string>\n  <string name=\"delete_repeat_confirm\">ډله او ټول فرعی ډلی حذف کړی%s</string>\n  <string name=\"delete_repeat_no\">فسخ کول</string>\n  <string name=\"invalid_answer_error\">اوبخښی! دا ځواب سم ندي!</string>\n  <string name=\"required_answer_error\">اوبخښی، ځواب ورکول اړین دي!</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">تاسو د %sپای کښی یاست.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">اوبخښی، د فورمي ثبتول ناکامه شو!</string>\n  <string name=\"data_saved_ok\">فورمه په بریالی توکه ثبت شو!</string>\n  <string name=\"save_point_error\">د خطا ثبت بیا موندولو نښه: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">فورمه د ثبت کیدو په حال کښی</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"activity_not_found\">هیڅ ډول فعالیت نه دی ترسره شوی%s</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"capture_osm\">OpenMapKit خلاص کړی</string>\n  <string name=\"recapture_osm\">OSM ټک کول بیا ترسره کړی</string>\n  <string name=\"get_barcode\">بار کود ثبت کړی</string>\n  <string name=\"replace_barcode\">بارکوډ تغیر کړی</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">عکس واخلی</string>\n  <string name=\"annotate_image\">تصویر شرحه کړی</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"signature_capture\">د امضا ثبت کول</string>\n  <string name=\"capture_video\">ویدیو ثبت کړی</string>\n  <string name=\"play_video\">ویډیو روښانه کړی</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">غږ ثبت کړی</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"trigger\">هوکی، لطفا دوام ورکړی</string>\n  <string name=\"islamic_month_1\">محرم</string>\n  <string name=\"islamic_month_2\">صفر</string>\n  <string name=\"islamic_month_3\">ربیع الاول</string>\n  <string name=\"islamic_month_4\">ربیع الثانی</string>\n  <string name=\"islamic_month_5\">جماد الاول</string>\n  <string name=\"islamic_month_6\">جماد الثانی</string>\n  <string name=\"islamic_month_7\">رجب</string>\n  <string name=\"islamic_month_8\">شعبان</string>\n  <string name=\"islamic_month_9\">رمضان</string>\n  <string name=\"islamic_month_10\">شوال</string>\n  <string name=\"islamic_month_11\">ذیقعده</string>\n  <string name=\"islamic_month_12\">ذولحجة</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s ( %2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">اوبخښه ، د موقعیت وړاندي کوونکي غیر فعاله دی!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">فورمه لیږل</string>\n  <string name=\"noselect_error\">بخښنه غواړو، تاسو کومه فورمه نه ده ټاکلې</string>\n  <string name=\"no_connection\">د شبکې پیوستون شتون نلري</string>\n  <string name=\"sending_items\">د %1$sفورمه(نو) %2$sلیږل</string>\n  <string name=\"send_selected_data\">ټاکل شوي فورمی ولیږی</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">تازه یی کړی</string>\n  <string name=\"download\">انتخاب شوي راښکته کړي</string>\n  <string name=\"downloading_data\">سرور ته د نښلیدو په حال کښی</string>\n  <string name=\"form_updates_available\">نوي تصحیح شوی فورمي شتون لري</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">ثبت شوی فورمی</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">سپین فورمي</string>\n  <string name=\"delete_confirm\">فورم(ونه) حذف کړی %s؟</string>\n  <string name=\"delete_file\">انتخاب شوی حذف کړی</string>\n  <string name=\"delete_no\">مه یی ړنګوی</string>\n  <string name=\"delete_yes\">فورمي حذف کړي</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"username\">کار نوم</string>\n  <string name=\"change_username\">ODK کارنوم</string>\n  <string name=\"password\">پټنوم</string>\n  <string name=\"change_password\">ODK پټنوم</string>\n  <string name=\"app_theme\">مدار</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">هر پنځلس دقیقه </string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">هر ساعت</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">هر ښپږ ساعتونو کښی</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">هر 24ساعتونو کښی</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">په اتوماتیک ډول راښکته کول</string>\n  <string name=\"automatic_download_summary\">په اتوماتیک ډول تصحیح شوی فورمي ښکته کړي</string>\n  <string name=\"hide_old_form_versions_setting_title\">زاړه بڼه نسخه پټ کړئ</string>\n  <string name=\"hide_old_form_versions_setting_summary\">یواځې نوې نسخه به په خالي فارم کې راښکاره شي</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">د پوښتنو لپاره لارښوونه ښکاره کړئ</string>\n  <string name=\"guidance_no\">نه</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"show_password\">پټنوم ښودل</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">بیرته شاته تک کول</string>\n  <string name=\"moving_backwards_disabled_title\">د بيرته راګرځولو توانول غیر فعاله دي</string>\n  <string name=\"yes\">هو</string>\n  <string name=\"no\">نه</string>\n  <string name=\"moving_backwards_enabled_title\">د بيرته راګرځولو توانول فعال دي</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">هوکی</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">فسخ کول</string>\n  <string name=\"cancel_loading_form\">فسخ کول</string>\n  <string name=\"cancel_location\">فسخ کول</string>\n  <string name=\"do_not_change\">فسخ کول</string>\n  <string name=\"canceling\">فسخ کولو کی قرار لری</string>\n  <string name=\"error_occured\">خطا وشوله</string>\n  <string name=\"please_wait\">مهرباني وکړئ څو ځلې انتظار وکړئ.</string>\n  <string name=\"please_wait_long\">هیله کوم لږ صبر وکړه. دا کیدای شي څو دقیقو وخت ونیسي.</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-pt/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Preencher novo formulário</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Rascunhos</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Pronto para enviar</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Enviado</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Baixar formulário</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Apagar formulário</string>\n  <string name=\"saved_forms\">Formulários salvos</string>\n  <string name=\"version_number\">Versão: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Adicionada em\\' EEE, dd \\'de\\' MMM \\'de\\' yyyy \\'às\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Atualizado em\\' EEE, dd \\'de\\' MMM \\'de\\' yyyy \\'às\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Salvo em\\' EEE, dd \\'de\\' MMM \\'de\\' yyyy \\'às\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Finalizado em\\' EEE, dd \\'de\\' MMM \\'de\\' yyyy \\'às\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Enviado em\\' EEE, dd \\'de\\' MMM \\'de\\' yyyy \\'às\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'O envio falhou em\\' EEE, dd \\'de\\' MMM \\'de\\' yyyy \\'às\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Apagado em\\' EEE, dd \\'de\\' MMM \\'de\\' yyyy \\'às\\' HH:mm</string>\n  <string name=\"submission_deleted\">Apagado formulário enviado</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Modificado em\\' EEE, dd \\'de\\' MMM \\'de\\' yyyy \\'às\\' HH:mm</string>\n  <string name=\"encrypted_form\">Formulário encriptado</string>\n  <string name=\"deleted_form\">Formulário apagado</string>\n  <string name=\"sort_the_list\">Ordenar a lista</string>\n  <string name=\"sort_by\">Ordenar por</string>\n  <string name=\"sort_by_name_asc\">Nome, A-Z</string>\n  <string name=\"sort_by_name_desc\">Nome, Z-A</string>\n  <string name=\"sort_by_date_desc\">Data, mais nova primeiro</string>\n  <string name=\"sort_by_date_asc\">Data, mais antiga primeiro</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Mais recentemente salva primeiro</string>\n  <string name=\"filter_the_list\">Filtrar a lista</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Procurar</string>\n  <string name=\"not_granted_permission\">A atividade que você está tentando iniciar exige uma permissão que você não concedeu.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Permissões de armazenamento</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Sem estas permissões, o ODK Collect não pode acessar os seus formulários ou salvar formulários enviados. Abra o aplicativo novamente quando estiver pronto para conceder estas permissões.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Permissões de localização</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Sem a permissão de <b> Precise Location </b>, o ODK Collect não consegue registrar a sua localização. Por favor, tente novamente quando estiver pronto para conceder a permissão.</string>\n  <string name=\"camera_runtime_permission_denied_title\">Permissão de acesso à câmera</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Sem esta permissão, o ODK Collect não consegue acessar sua câmera. Por favor, tente novamente quando estiver pronto a concedê-la.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Permissão de gravar áudio</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Sem esta permissão, o ODK Collect não consegue acessar seu microfone. Por favor, tente novamente quando estiver pronto a concedê-la.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Abrir o mapa do formulário</string>\n  <string name=\"instance_scan_count\">%1$dformulários adicionados.</string>\n  <string name=\"finished_disk_scan\">Varredura finalizada. Todos os formulários foram carregados.</string>\n  <string name=\"xform_parse_error\">Erro de leitura do XForm %1$s: %2$s está faltando ou é inválido.</string>\n  <string name=\"loading_form\">Carregando Formulário</string>\n  <string name=\"survey_loading_reading_form_message\">Lendo definição do formulário…</string>\n  <string name=\"survey_loading_reading_data_message\">Lendo dados da pesquisa…</string>\n  <string name=\"survey_loading_reading_csv_message\">Lendo arquivos CSV…</string>\n  <string name=\"parse_error\">Desculpe, não é possível analisar formulário.</string>\n  <string name=\"loading_form_failed\">Um erro ocorreu enquanto o formulário era carregado. Por favor, tente novamente.</string>\n  <string name=\"cannot_edit_completed_form\">Este formulário não pode ser editado, pois ele foi marcado como finalizado. Ele pode estar criptografado.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Alterações não salvas recuperadas</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Alterar Idioma</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Ir para atual</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Salvar como rascunho</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Ir para o Início</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Ir para o Fim</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Subir</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <string name=\"edit_finalized_form\">Editar formulário finalizado</string>\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Grupo</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Grupo de repetições</string>\n  <string name=\"exit\">Sair</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Adicionar \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Adicionar</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"do_not_add_repeat\">Não adicionar</string>\n  <string name=\"add_another_menu\">Adicionar outra</string>\n  <string name=\"add_another_question\">Adicionar outro?</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Editar Prompt</string>\n  <string name=\"clearanswer_confirm\">Remover a resposta para \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Remover a resposta</string>\n  <string name=\"clear_answer_ask\">Remover esta Resposta?</string>\n  <string name=\"clear_answer_no\">Cancelar</string>\n  <string name=\"discard_answer\">Remover a resposta</string>\n  <string name=\"discard_group\">Remover grupo</string>\n  <string name=\"delete_repeat\">Remover grupo</string>\n  <string name=\"delete_repeat_ask\">Remover este Grupo?</string>\n  <string name=\"delete_repeat_confirm\">Remover grupo \\\"%s\\\" e todos os seus sub-grupos?</string>\n  <string name=\"delete_repeat_no\">Cancelar</string>\n  <string name=\"invalid_answer_error\">Desculpe, esta resposta é inválida!</string>\n  <string name=\"required_answer_error\">Desculpe, esta resposta é obrigatória!</string>\n  <string name=\"success_form_validation\">Sucesso! Não foram encontrados erros no seu formulário.</string>\n  <string name=\"invalid_space_in_answer_singular\">Atenção: a resposta %s contém espaços</string>\n  <string name=\"invalid_space_in_answer_plural\">Atenção: as respostas %s contêm espaços</string>\n  <string name=\"too_complex_form\">Este formulário é complexo demais para este dispositivo. Tente simplificar expressões ou peça ajuda no fórum.</string>\n  <string name=\"survey_internal_error\">Erro interno: falha ao abrir a questão.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Você está no fim de \\\"%s\\\".</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <string name=\"form_editing_disabled_after_finalizing\">Não é possível editar após finalizar.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <string name=\"form_editing_disabled_after_sending\">Não é possível editar após o envio.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_finalizing\">É possível editar após finalizar.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_sending\">É possível editar após o envio.\n</string>\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Se você precisa fazer alterações em seu formulário, utilize \\\"Salvar como rascunho\\\" até que você esteja pronto para enviar.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <string name=\"form_editing_enabled_after_finalizing_hint\">Para fazer alterações após finalizar o seu formulário, vá para \\\"Pronto para Enviar\\\", abra o formulário e toque no ícone de edição.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <string name=\"form_editing_enabled_after_sending_hint\">Para fazer alterações após enviar o seu formulário, vá para Enviados, abra o formulário e toque no ícone de edição.</string>\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Salvar como rascunho</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Finalizar</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Enviar</string>\n  <string name=\"data_saved_error\">Desculpe, falha ao salvar formulário!</string>\n  <string name=\"data_saved_ok\">Formulário salvo com sucesso!</string>\n  <string name=\"save_point_error\">Erro ao salvar ponto de recuperação: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Descartar Formulário</string>\n  <string name=\"discard_changes\">Descartar Mudanças</string>\n  <string name=\"keep_changes\">Salvar Alterações</string>\n  <string name=\"keep_editing\">Continuar editando</string>\n  <string name=\"quit_form_title\">Salvar o formulário?</string>\n  <string name=\"save_explanation\">Você pode salvar esse formulário e acessá-lo em seus rascunhos a qualquer momento.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'Esse formulário foi salvo em\\' EEE, dd \\'de\\' MMM \\'de\\' yyyy \\'às\\' HH:mm\\'. Salve como rascunho para manter as alterações.\\'</string>\n  <string name=\"quit_form_continue_title\">Continuar editando?</string>\n  <string name=\"discard_form_warning\">Se descartar o formulário, você perderá todas as alterações que fez até agora.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'Esse formulário foi salvo em\\' EEE, dd \\'de\\' MMM \\'de\\' yyyy \\'às\\' HH:mm\\'. Se você descartar as alterações, você pederá qualquer mudança que tiver feito desde então.\\'</string>\n  <string name=\"saving_form\">Salvando Formulário</string>\n  <string name=\"survey_saving_validating_message\">Validando respostas…</string>\n  <string name=\"survey_saving_collecting_message\">Coletando dados…</string>\n  <string name=\"survey_saving_saving_message\">Salvando no cartão de memória…</string>\n  <string name=\"survey_saving_finalizing_message\">Desconectando o cartão de memória…</string>\n  <string name=\"survey_saving_encrypting_message\">Criptografando dados…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Não há um registro exato para esta instância!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Um formulário em branco não se ajusta exatamente ao jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Nenhum FormId foi especificado???</string>\n  <string name=\"phone_does_not_support_rsa\">O telefone não suporta encriptação RSA.</string>\n  <string name=\"invalid_rsa_public_key\">Chave pública RSA inválida.</string>\n  <string name=\"encryption_error_message\">%s Formulário não foi salvo como finalizado.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">O Formulário não pode ser utilizado</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Não foi possível iniciar a gravação</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">O microfone está ocupado</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">O arquivo de mídia não pode ser anexado ao formulário, mas está disponível em%1$s</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Gravação interrompida. Reiniciar em %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Falha ao tentar atualizar os valores. Isso ocorre geralmente por erros no campos calculados na definição do formulário.\\n\\nSe você continuar a ter esse problema, entre em contato com a pessoa que solicitou que você coletasse dados.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Executar App</string>\n  <string name=\"activity_not_found\">Não foi encontrada nenhuma atividade por ser tratada: %s</string>\n  <string name=\"no_app\">Aplicativo faltando. Por favor, inserir manualmente a informação</string>\n  <string name=\"null_intent_value\">A aplicação externa não enviou as informações esperadas.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">Imprimir</string>\n  <string name=\"open_url\">Abrir URL</string>\n  <string name=\"capture_osm\">Executar o OpenMapKit</string>\n  <string name=\"recapture_osm\">Refazer o tageamento OSM</string>\n  <string name=\"edited_osm_file\">Arquivo OSM XML Editado:</string>\n  <string name=\"invalid_osm_data\">Algo deu errado. Não conseguimos obter dados válidos do OSM.</string>\n  <string name=\"alert\">Alerta</string>\n  <string name=\"install_openmapkit\">Por favor, instala o OpenMapKit!</string>\n  <string name=\"get_barcode\">Capturar Código de Barra</string>\n  <string name=\"replace_barcode\">Trocar Código de Barras</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Coloque o código de barras dentro do retângulo</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"rotate_device\">Girar</string>\n  <string name=\"open_file\">Abrir arquivo</string>\n  <string name=\"rank_items\">Ordenar itens</string>\n  <string name=\"number_picker_title\">Teclado para escolher números</string>\n  <string name=\"invalid_range_widget\">Esta ferramenta possui parâmetros inválidos!</string>\n  <string name=\"svg_file_does_not_exist\">O arquivo SVG não existe!</string>\n  <string name=\"capture_image\">Tirar Foto</string>\n  <string name=\"choose_image\">Escolher Imagem</string>\n  <string name=\"selected_invalid_image\">O arquivo selecionado não é uma imagem válida</string>\n  <string name=\"take_picture_instruction\">Toque na tela para tirar uma fotografia</string>\n  <string name=\"error_front_camera_unavailable\">A câmera da frente não está disponível neste dispositivo</string>\n  <string name=\"camera_error\">Houve um erro ao tirar a fotografia</string>\n  <string name=\"camera_failed_to_initialize\">Não foi possível inicializar a câmera!</string>\n  <string name=\"annotate_image\">Imagem com anotações</string>\n  <string name=\"gif_not_supported\">Arquivos GIF não são suportados</string>\n  <string name=\"invalid_file_type\">A aplicação retornou um tipo de arquivo inválido</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_title\">Salvar alterações?</string>\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_explanation\">Você quer salvar as alterações antes de sair?</string>\n  <string name=\"save_and_close\">Salvar e Fechar</string>\n  <string name=\"signature_capture\">Coletar assinatura</string>\n  <string name=\"sign_button\">Coletar Assinatura</string>\n  <string name=\"markup_image\">Marcar Imagem</string>\n  <string name=\"draw_image\">Desenhar Imagem</string>\n  <string name=\"reset_image\">Restaurar</string>\n  <string name=\"set_color\">Definir Cor</string>\n  <string name=\"capture_video\">Gravar Vídeo</string>\n  <string name=\"choose_video\">Escolher Vídeo</string>\n  <string name=\"play_video\">Assistir o Vídeo</string>\n  <string name=\"choose_file\">Escolher Arquivo</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Apagar</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Gravar Áudio</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Escolher Áudio</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Pausar</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Continuar</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">Gravação de áudio: ligado</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">Gravação de áudio: desligado</string>\n  <string name=\"background_audio_permission_explanation\">Esse formulário grava áudio em segundo plano. Você precisa dar permissão de acesso ao microfone para o ODK Collect. Se não permitir isso, você não poderá abrir esse formulário.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Esse formulário solicita gravação de áudio em segundo plano. Desabilitar isso irá interromper a gravação e descartar o áudio já gravado. Você tem certeza que deseja continuar?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Desabilitar gravação</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">A gravação não será iniciada imediatamente. Você precisa abrir novamente o formulário para iniciar a gravação.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Apagar arquivo?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">Você não será capaz de recuperar esse arquivo caso ele seja apagado. Você tem certeza de que deseja continuar?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Parar</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Gravando…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Gravação de áudio</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Tocar áudio</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Salvando arquivo</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Você deve parar a gravação antes de sair dessa tela</string>\n  <string name=\"error_attaching_binary_file\">Um erro ocorreu ao anexar o arquivo: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Selecione valor</string>\n  <string name=\"select_answer\">Selecionar Resposta</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Edite valor</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Nenhum valor selecionado</string>\n  <string name=\"selected_title\">Selecionado:</string>\n  <string name=\"trigger\">OK. Por favor, continuar.</string>\n  <string name=\"ext_file_no_data_error\">O arquivo não contém dados!</string>\n  <string name=\"ext_conflicting_columns_error\">As colunas %s correspondem!</string>\n  <string name=\"ext_sortBy_numeric_error\">As colunas selecionadas devem conter apenas valores numéricos. Valores com conflito foram \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Erro de sintaxe na função search(): a função necessita de 1, 4 ou 6 argumentos.</string>\n  <string name=\"ext_search_wrong_function_error\">Erro de sintaxe na função search(): Função desconhecida \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Erro de sintaxe na função search():  \\'%s\\' não foi avaliada como sendo uma função.</string>\n  <string name=\"ext_search_return_error\">O gestor de procura retornou um objeto do tipo %s\\'.</string>\n  <string name=\"ext_import_generic_error\">Não foi possível importar os dados de%1$s. Motivo: %2$s</string>\n  <string name=\"ext_import_progress_message\">Pré-carregando dados de \\'%1$s\\', por favor, aguarde… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Leitura de dados cancelada!</string>\n  <string name=\"ext_import_finalizing_message\">Finalizando dados pré-carregados…</string>\n  <string name=\"ext_import_completed_message\">Leitura de dados finalizada!</string>\n  <string name=\"ext_not_initialized_error\">O ExternalDataManager não foi inicializado.</string>\n  <string name=\"ext_import_csv_missing_error\">Dados externos de %1$s não foram importados. Será que falta incluir o arquivo %2$s.csv no seu formulário?</string>\n  <string name=\"ext_search_generic_error\">Erro de sintaxe na função search(): %s</string>\n  <string name=\"ext_assign_value_error\">Não foi possível atribuir o valor a \\'%s\\'.</string>\n  <string name=\"parser_exception\">Exceção XPathParser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Gravar rumo</string>\n  <string name=\"replace_bearing\">Substituir rumo</string>\n  <string name=\"getting_bearing\">Carregando rumo</string>\n  <string name=\"accept_bearing\">Gravar rumo</string>\n  <string name=\"bearing\">Rumo: %.3f</string>\n  <string name=\"direction\">Direção: %s</string>\n  <string name=\"bearing_lack_of_sensors\">O rumo não foi coletado: em seu dispositivo falta o acelerômetro, sensor de campo magnético ou ambos.</string>\n  <string name=\"select_date\">Selecionar data</string>\n  <string name=\"select_time\">Selecionar hora</string>\n  <string name=\"no_date_selected\">Nenhuma data selecionada</string>\n  <string name=\"no_time_selected\">Nenhuma hora selecionada</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <string name=\"buddhist_month_1\">makara</string>\n  <!--2nd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_2\">kumbha</string>\n  <!--3rd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_3\">mīna</string>\n  <!--4th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_4\">meṣa</string>\n  <!--5th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_5\">vṛṣabha</string>\n  <!--6th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_6\">mithuna</string>\n  <!--7th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_7\">karkaṭa</string>\n  <!--8th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_8\">sinha</string>\n  <!--9th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_9\">kanyā</string>\n  <!--10th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_10\">tulā</string>\n  <!--11th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_11\">vṛścika</string>\n  <!--12th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_12\">dhanu</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Arquivo: %s é inválido.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Arquivo: %s não encontrado.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Insira identidade</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Identidade</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Este formulário exige que você se identifique</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Fechar</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Razão das alterações</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Razão</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Você necessita explicar a razão para as alterações neste formulário</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Salvar</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">Selecione o lugar</string>\n  <string name=\"choices\">Opções</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">Mapa Topográfico Nacional USGS</string>\n  <string name=\"openmap_usgs_sat\">Mapa Híbrido Nacional USGS</string>\n  <string name=\"openmap_usgs_img\">Mapa de Imagem Nacional USGS</string>\n  <string name=\"openmap_cartodb_positron\">Positron CartoDB</string>\n  <string name=\"openmap_cartodb_darkmatter\">Matéria Escura CartoDB</string>\n  <string name=\"basemap\">Mapa base</string>\n  <string name=\"basemap_source\">Fonte</string>\n  <string name=\"basemap_source_unavailable\">Desculpe, %s mapas base não estão disponíveis nesse dispositivo. Por favor, escolha outro mapa base em Configurações &gt; Mapas.</string>\n  <string name=\"map_style_label\">estilo do mapa %s</string>\n  <string name=\"streets\">Ruas</string>\n  <string name=\"terrain\">Terreno</string>\n  <string name=\"hybrid\">Hibrido</string>\n  <string name=\"satellite\">Satélite</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Claro</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Escuro</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Ambiente externo</string>\n  <string name=\"topographic\">Topográfico</string>\n  <string name=\"google_play_services_error_occured\">Não foi capaz de acessar o Google Maps. O serviço do Google Play está instalado?</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Matéria escura</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">Camadas offline</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">Camada</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">Selecionar camada offline</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">Selecione a camada offline para uso em todos os mapas deste projeto. Você pode adicionar opções à lista selecionando um arquivo MBTiles do seu dispositivo e também excluir camadas existentes da lista.</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">Saber mais sobre adicionar MBTiles.</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">Adicionar camadas</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">Apagar camada</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <plurals name=\"non_mbtiles_files_selected_title\">\n    <item quantity=\"one\">%d camada não pode ser adicionada</item>\n    <item quantity=\"many\">%d camadas não puderam ser adicionadas</item>\n    <item quantity=\"other\">%d camadas não puderam ser adicionadas</item>\n  </plurals>\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">Os arquivos selecionados não são do formato MTTiles. Você só pode adicionar arquivos do formato MBTiles.</string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">Alguns dos arquivos que você selecionou não são do formato MTTiles. Você só pode adicionar arquivos do formato MBTiles.</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">Camadas</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">Selecionar acesso às camadas</string>\n  <string name=\"select_layer_access_subtitle\">Você deseja que as camadas esteja disponíveis em todos os projetos ou somente no projeto atual?</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">Todos os projetos</string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">Apenas o projeto atual</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">Você tem certeza que deseja excluir a camada offline %1$s?</string>\n  <string name=\"record_geopoint\">Registre um ponto</string>\n  <string name=\"location_accuracy\">Precisão: %1$s</string>\n  <string name=\"location_accuracy_unacceptable\">⚠️ Precisão: %1$s (inaceitável)</string>\n  <string name=\"provider_disabled_error\">Desculpe, provedor de localização desabilitado!</string>\n  <string name=\"gps_result\">Latitude: %1$s\\nLongitude: %2$s\\nAltitude: %3$s m\\nPrecisão: %4$s m</string>\n  <string name=\"google_play_services_not_available\">Este formulário necessita rastrear sua localização, mas o Google Play Services não está disponível.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Este formulário necessita rastrear sua localização, mas a localização está desabilitada no seu dispositivo. Por favor, habilite nas configurações do celular.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Ir para as configurações</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">Rastrear localização: ligado</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">Rastrear localização: desligado</string>\n  <string name=\"background_location_disabled\">Este formulário necessita rastrear sua localização, mas a localização está desabilitada no seu celular. Por favor, habilite no menu %1$s acima.</string>\n  <string name=\"background_location_enabled\">Este formulário rastreia a sua localização. Você pode desabilitar o rastreamento de localização no menu %1$s acima.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">Verificar erros</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">Tempo decorrido: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">O ponto será salvo em: %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">Satélites: %1$s</string>\n  <string name=\"waiting_for_location\">Tentando obter localização. Por favor, aguarde.</string>\n  <string name=\"poor_accuracy\">Acurácia ruim. Por favor, aguarde.</string>\n  <string name=\"unacceptable_accuracy\">Acurácia inaceitável. Por favor, aguarde.</string>\n  <string name=\"improving_accuracy\">Melhorando a acurácia. Por favor, aguarde.</string>\n  <string name=\"accuracy_status_title\">Obtendo localização</string>\n  <!--The accuracy of a location fix in meters-->\n  <string name=\"accuracy_m\">%1$s m</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">O %1$s %2$s %3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">L %1$s %2$s %3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s %2$s %3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s %2$s %3$s</string>\n  <string name=\"input_method\">Método de entrada</string>\n  <string name=\"start\">Iniciar</string>\n  <string name=\"enable_gps\">Habilitar GPS</string>\n  <string name=\"gps_enable_message\">O GPS está desabilitado no seu dispositivo. Quer habilitá-lo?</string>\n  <string name=\"geo_clear_warning\">A feature já foi criada. Gostaria de limpar a feature?</string>\n  <string name=\"geo_exit_warning\">Descartar mudanças e retornar ao ODK?</string>\n  <string name=\"polygon_validator\">Precisa ter pelo menos 3 pontos para criar Polígono</string>\n  <string name=\"polyline_validator\">Precisa ter pelo menos 2 pontos para criar uma Cadeia de Linhas</string>\n  <string name=\"clear\">Limpar</string>\n  <string name=\"placement_mode\">Indicar a localização tocando na tela</string>\n  <string name=\"manual_mode\">Registro manual da localização</string>\n  <string name=\"automatic_mode\">Registro automático da localização</string>\n  <string name=\"recording_interval\">Intervalo de registro:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d segundo</item>\n    <item quantity=\"many\">%d segundos</item>\n    <item quantity=\"other\">%d segundos</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d minuto</item>\n    <item quantity=\"many\">%d minutos</item>\n    <item quantity=\"other\">%d minutos</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Exigência de precisão:</string>\n  <string name=\"none\">Nenhuma</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d metro</item>\n    <item quantity=\"many\">%d metros</item>\n    <item quantity=\"other\">%d metros</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Pontos inseridos: %d</string>\n  <string name=\"collection_status_placement\">Pontos inseridos: %d (toque na tela para inserir pontos)</string>\n  <string name=\"collection_status_manual\">Pontos inseridos: %d (registro manual)</string>\n  <string name=\"collection_status_auto_seconds\">Pontos inseridos: %1$d (registrados a cada %2$d s)</string>\n  <string name=\"collection_status_auto_minutes\">Pontos inseridos: %1$d (registrados a cada %2$d min)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Pontos inseridos: %1$d (registro a cada %2$d s, com precisão de %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Pontos inseridos: %1$d (registro a cada %2$d m, com precisão de %3$d m)</string>\n  <string name=\"geopoint_instruction\">Pressione por mais tempo para colocar marcador ou selecione o botão Adicionar Marcador.</string>\n  <string name=\"geopoint_no_draggable_instruction\">Selecione o botão Adicionar Marcador.</string>\n  <string name=\"discard\">Descartar</string>\n  <string name=\"select_item_count\">1 %s: 2 %d (2 %d mostrado no mapa)</string>\n  <string name=\"select_item\">Selecionar</string>\n  <string name=\"new_item\">Novo item</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">Editar Rascunho</string>\n  <string name=\"view_data\">Ver formulário salvo</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">Registro de localização</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">Obtendo localização…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Mostrar minha localização</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">Zoom para enquadrar tudo</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Pausar</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">Remover o último ponto</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <string name=\"error_fixed\">✅ Erro corrigido</string>\n  <!--Description for button that explains how to modify the map-->\n  <string name=\"show_how_to_modify_map\">Mostrar como modificar o mapa</string>\n  <!--Title for instructions on how to modify map features-->\n  <string name=\"how_to_modify_map\">Como modificar o mapa</string>\n  <!--Instruction in a list on how to move a point-->\n  <string name=\"long_press_to_move_point_info_item\">Mantenha pressionado para mover o ponto</string>\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">Remover o último ponto</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <string name=\"delete_shape_to_start_over_info_item\">Apagar o polígono e começar novamente</string>\n  <!--Instruction in a list on how to add a new point-->\n  <string name=\"add_point_info_item\">Adicionar ponto</string>\n  <!--Instruction in a list on how to tap to add a point-->\n  <string name=\"tap_to_add_a_point_info_item\">Toque para adicionar um ponto</string>\n  <!--Instruction in a list on how to delete the entire shape-->\n  <string name=\"delete_entire_shape_info_item\">Apagar todo o polígono</string>\n  <!--Button text to close the instructions dialog-->\n  <string name=\"done\">Feito</string>\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <string name=\"how_to_modify\">Como corrigir</string>\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Enviando Formulário</string>\n  <string name=\"noselect_error\">Desculpe, você não selecionou nenhum formulário!</string>\n  <string name=\"no_connection\">Sem conexão disponível</string>\n  <string name=\"sending_items\">Enviando %1$s de %2$s formulário(s)</string>\n  <string name=\"send_selected_data\">Enviar selecionado</string>\n  <string name=\"server_auth_credentials\">Nome de usuário ou senha inválidos para o servidor: %s</string>\n  <string name=\"server_requires_auth\">Servidor Requer Autenticação</string>\n  <string name=\"no_forms_uploaded\">Desculpe, nenhum formulário foi enviado.</string>\n  <string name=\"upload_results\">Enviar Resultados</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Mudar visualização</string>\n  <string name=\"show_sent_and_unsent_forms\">Formulários enviados e não enviados</string>\n  <string name=\"show_unsent_forms\">Apenas formulários não enviados</string>\n  <string name=\"send_in_progress\">Envio automático em segundo plano sendo executado, por favor, tente novamente em alguns instantes</string>\n  <string name=\"forms_upload_succeeded\">Formulários enviados com sucesso</string>\n  <string name=\"forms_upload_failed\">Falha no envio dos formulários</string>\n  <string name=\"all_uploads_succeeded\">Sucesso nos uploads!</string>\n  <string name=\"some_uploads_failed\">1 %1$s de 2 %2$s uploads falharam!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Atualizar</string>\n  <string name=\"download\">Pegar Selecionados</string>\n  <string name=\"downloading_data\">Conectando com o Servidor</string>\n  <string name=\"fetching_file\">Baixando %1$s.\\n\\nFormulário %2$s de %3$s.</string>\n  <string name=\"form_download_progress\">%1$s. Acessando arquivos de mídia: %2$s de %3$s</string>\n  <string name=\"version\">Versão:</string>\n  <string name=\"form_updates_available\">Atualizações de formulários disponíveis</string>\n  <string name=\"newer_version_of_a_form_info\">Esta é uma atualização de um formulário que você possui</string>\n  <string name=\"form_update_error\">Falha na atualização do formulário</string>\n  <string name=\"form_update_error_massage\">Abrir o ODK Collect para tentar novamente</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"form_update_notification_title\">Sincronizando formulários</string>\n  <string name=\"load_remote_form_error\">Falha ao baixar a lista de formulários</string>\n  <string name=\"forms_download_succeeded\">Formulários baixados com sucesso</string>\n  <string name=\"forms_download_failed\">Falha no download dos formulários</string>\n  <string name=\"all_downloads_succeeded\">Todos os downloads foram concluídos com sucesso!</string>\n  <string name=\"some_downloads_failed\">%1$sde%2$sdownloads falharam!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Versão: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Se o problema persistir, informe a pessoa que lhe pediu para coletar os dados.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">O Collect não consegue se conectar ao servidor em %s . Você digitou a URL corretamente?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">O Collect não consegue se conectar de modo seguro ao servidor em %s. Você digitou a URL corretamente?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">O servidor %1$sretornou o código de status %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">O servidor %s forneceu uma resposta inválida.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">O servidor não forneceu uma chave para esse formulário.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_parsing_error\">Esse formulário não pode ser processado:</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">Um erro de acesso a disco ocorreu ao baixar esse formulário.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">Esse formulário tem uma URL de envio de respostas inválida.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Formulários Salvos</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Formulários em Branco</string>\n  <string name=\"delete_confirm\">Apagar %s formulário(s)?</string>\n  <string name=\"delete_file\">Apagar Selecionados</string>\n  <string name=\"delete_no\">Não Apagar</string>\n  <string name=\"delete_yes\">Apagar Formulários</string>\n  <string name=\"form_delete_message\">Apagando formulários selecionados</string>\n  <string name=\"file_deleted_ok\">%s formulário(s) apagados com sucesso!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Configurações de projeto</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Servidor</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, usuário, senha</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Exibição de projeto</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Nome, ícone, cor</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Interface de usuário</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_interface_settings_subtext\">Idioma do aplicativo, tamanho de fonte, navegação</string>\n  <!--Title of a settings section-->\n  <string name=\"maps\">Mapas</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Mapa base, estilos, camadas</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Gestão de formulários</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">Atualização automática, envio automático, exclusão automática</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Identificação de usuário e dispositivo</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">Nome de usuário, número de telefone, ID do dispositivo</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Experimental</string>\n  <string name=\"protected_settings\">Protegido</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">Desbloquear configurações protegidas</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">Mudar a senha, definir controles de acesso</string>\n  <string name=\"set_admin_password\">Definir senha de administrador</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Gerenciamento de projeto</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">Reconfigurar, redefinir, excluir</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Controle de acesso</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">Limitar interface de usuário</string>\n  <string name=\"server_preferences\">Servidor</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Servidor URL</string>\n  <string name=\"url_error\">Desculpe, URL inválida!</string>\n  <string name=\"username\">Nome do usuário</string>\n  <string name=\"change_username\">Usuário ODK</string>\n  <string name=\"password\">Senha</string>\n  <string name=\"change_password\">Senha</string>\n  <string name=\"password_error_whitespace\">Espaços em branco antes e depois de senhas não são permitidos</string>\n  <string name=\"username_error_whitespace\">Espaços em branco antes e depois de nomes de usuários não são permitidos</string>\n  <string name=\"app_theme\">Tema</string>\n  <string name=\"language\">Idioma</string>\n  <string name=\"use_device_language\">Utilizar idioma do dispositivo</string>\n  <string name=\"font_size\">Tamanho da Fonte do Texto</string>\n  <string name=\"change_font_size\">Tamanho da Fonte do Texto</string>\n  <string name=\"font_size_extra_large\">Extra Grande</string>\n  <string name=\"font_size_large\">Grande</string>\n  <string name=\"font_size_medium\">Média</string>\n  <string name=\"font_size_small\">Pequena</string>\n  <string name=\"font_size_extra_small\">Extra pequena</string>\n  <string name=\"navigation\">Navegação</string>\n  <string name=\"swipe_navigation\">Deslizar tela horizontalmente</string>\n  <string name=\"buttons_navigation\">Use os botões avançar/retroceder</string>\n  <string name=\"swipe_buttons_navigation\">Use deslizar tela e botões</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Próximo</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Voltar</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Atualização de formulários</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Modo de atualização dos formulários</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Atualização manual</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Formulários presentes no dispositivo</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Formulários exatamente iguais ao servidor</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Intervalo entre as atualizações automáticas</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">A cada 15 minutos</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">A cada hora</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">A cada 6 horas</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">A cada 24 horas</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Download automático</string>\n  <string name=\"automatic_download_summary\">Baixar automaticamente versões atualizadas dos formulários</string>\n  <string name=\"hide_old_form_versions_setting_title\">Esconder versões antigas de formulários</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Somente a versão mais recente aparecerá em Formulário em Branco</string>\n  <string name=\"form_submission_category\">Envio de formulários</string>\n  <string name=\"autosend_selector_title\">Enviar automaticamente</string>\n  <string name=\"autosend\">Enviar automaticamente</string>\n  <string name=\"off\">Desligado</string>\n  <string name=\"wifi_autosend\">Somente wifi</string>\n  <string name=\"cellular_autosend\">Somente celular</string>\n  <string name=\"wifi_cellular_autosend\">Wifi ou celular</string>\n  <string name=\"delete_after_send\">Apagar depois de enviado</string>\n  <string name=\"delete_after_send_summary\">Apaga formulários completos e arquivos de mídia depois de enviados ao servidor.</string>\n  <string name=\"form_filling_category\">Preenchimento de formulários</string>\n  <string name=\"constraint_behavior_title\">Processamento de regras</string>\n  <string name=\"constraint_behavior\">Comportamento de processamento da regra</string>\n  <string name=\"constraint_behavior_on_swipe\">Validar ao avançar</string>\n  <string name=\"constraint_behavior_on_finalize\">Adiar validação até finalizar</string>\n  <string name=\"high_resolution_title\">Vídeo de alta resolução</string>\n  <string name=\"high_resolution_summary\">Ativar gravação de vídeos em alta resolução</string>\n  <string name=\"image_size_title\">Tamanho de imagem</string>\n  <string name=\"image_size_dialog_title\">Valor máximo de pixels do maior lado da imagem</string>\n  <string name=\"image_size_large\">Grande (3072px)</string>\n  <string name=\"image_size_medium\">Médio (2048px)</string>\n  <string name=\"image_size_small\">Pequeno (1024px)</string>\n  <string name=\"image_size_very_small\">Muito pequeno (640px)</string>\n  <string name=\"image_size_original\">Tamanho original da câmera (padrão)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Mostrar orientação para perguntas</string>\n  <string name=\"guidance_no\">Não</string>\n  <string name=\"guidance_yes\">Sim - sempre mostrar</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Sim - recolhido</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">Usar outro app para gravação de áudio</string>\n  <string name=\"form_import_category\">Importação de formulários</string>\n  <string name=\"instance_sync\">Finalizar formulários na importação</string>\n  <string name=\"instance_sync_summary\">Afeta formulários adicionados diretamente na pasta instances.</string>\n  <string name=\"analytics_preferences\">Uso de dados</string>\n  <string name=\"analytics\">Coletar dados anonimamente</string>\n  <string name=\"analytics_summary\">Dados anônimos de uso ajudam a equipe do ODK na priorização de correções de erros e funcionalidades.</string>\n  <string name=\"form_metadata_title\">Metadados do Formulário</string>\n  <string name=\"form_metadata\">Metadados do Formulário</string>\n  <string name=\"form_metadata_summary\">Estes valores serão adicionados aos formulários que têm os campos usuário, e-mail e/ou telefone pré-determinados para identificar a pessoa que está enviando os dados.</string>\n  <string name=\"user_defined\">Definidos pelo usuário</string>\n  <string name=\"phone_number\">Número do telefone</string>\n  <string name=\"email\">Endereço de e-mail</string>\n  <string name=\"invalid_email_address\">Endereço de e-mail inválido!</string>\n  <string name=\"device_defined\">Definidos pelo dispositivo</string>\n  <string name=\"device_id\">ID do dispositivo</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Não está disponível</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">Desbloquear configurações</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">Configurações desbloqueadas</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <string name=\"setting_not_available_during_form_entry\">Parâmetro não disponível durante o preenchimento do formulário</string>\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Digite a nova senha</string>\n  <string name=\"admin_password\">Senha Administrativa</string>\n  <string name=\"admin_password_changed\">Senha administrativa alterada com sucesso</string>\n  <string name=\"admin_password_disabled\">Senha administrativa desabilitada</string>\n  <string name=\"admin_password_incorrect\">Lamento, senha incorreta!</string>\n  <string name=\"enter_admin_password\">Digite a senha administrativa</string>\n  <string name=\"change_admin_password\">Mudar Senha Administrativa</string>\n  <string name=\"show_password\">Mostra a senha</string>\n  <string name=\"found_in_main\">Desmarque para esconder no Menu Principal</string>\n  <string name=\"found_in_settings\">Desmarque para não exibir nas Configurações Não Protegidas</string>\n  <string name=\"form_entry\">Desmarque para ocultar da entrada de formulário</string>\n  <string name=\"form_end\">Desmarque para ocultar do Fim do Formulário</string>\n  <string name=\"reset_project_settings_title\">Restaurar</string>\n  <string name=\"reset_project_settings_subtext\">Escolha entre configurações, formulários, dados</string>\n  <string name=\"delete_project\">Apagar</string>\n  <string name=\"select_all\">Selecionar tudo</string>\n  <string name=\"clear_all\">Limpar tudo</string>\n  <string name=\"reset_settings\">Todas as configurações (configurações internas, configurações salvas)</string>\n  <string name=\"reset_saved_forms\">Formulários salvos (pasta com as instâncias, bancos de dados das instâncias)</string>\n  <string name=\"reset_blank_forms\">Formulários em branco (pasta de formulários, banco de dados de formulários, banco de dados dos conjuntos de itens)</string>\n  <string name=\"reset_layers\">Camadas do mapa (pasta de camadas)</string>\n  <string name=\"reset_cache\">Cache de carga dos formulários (pasta .cache)</string>\n  <string name=\"reset_settings_dialog_title\">Selecione o que restaurar</string>\n  <string name=\"reset_settings_button_reset\">Restaurar</string>\n  <string name=\"reset_in_progress\">Restaurando…</string>\n  <string name=\"reset_app_state_result\">Resultados da restauração</string>\n  <string name=\"reset_app_state_warning\">Todos dados selecionados serão apagados permanentemente. Não é possível desfazer esta ação.</string>\n  <string name=\"reset_settings_result\">Todas configurações :: %s</string>\n  <string name=\"reset_saved_forms_result\">Formulários salvos :: %s</string>\n  <string name=\"reset_blank_forms_result\">Formulários em branco :: %s</string>\n  <string name=\"reset_layers_result\">Layers do mapa :: %s</string>\n  <string name=\"reset_cache_result\">Carga do cache do formulário :: %s</string>\n  <string name=\"main_menu_settings\">Configurações de Menu Principal</string>\n  <string name=\"main_menu_settings_summary\">Exibir ou ocultar botões</string>\n  <string name=\"user_settings\">Configurações de Usuário</string>\n  <string name=\"user_setting_summary\">Exibir ou ocultar configurações</string>\n  <string name=\"form_entry_setting\">Configurações de entrada do formulário</string>\n  <string name=\"form_entry_settings_summary\">Exibir ou ocultar ações</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Movendo-se para trás</string>\n  <string name=\"moving_backwards_disabled_title\">Movimento para trás desabilitado</string>\n  <string name=\"moving_backwards_disabled_message\">Configure o dispositivo para evitar que usuarios evitem essa opção de configuração?\\n\\nAs mudanças são:\\n\\u2022 Desabilitar Editar Rascunho\\n\\u2022 Desabilitar Salvar como Rascunho\\n\\u2022 Desabilitar Vir para Questão\\n\\u2022 Validação ao Avançar</string>\n  <string name=\"yes\">Sim</string>\n  <string name=\"no\">Não</string>\n  <string name=\"moving_backwards_enabled_title\">Movimento para trás habilitado</string>\n  <string name=\"moving_backwards_enabled_message\">Você pode querer revisar as seguintes configurações:\\n\\n Editar Rascunho\\n\\u2022 Salvar como Rascunho\\n\\u2022 Ir para o Prompt de Comando\\n\\u2022 Processamento de Restrições</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Salvar como rascunho</string>\n  <string name=\"save_mid_summary\">Ícone Salvar na barra superior e botão \\\"Salvar como rascunho\\\" ao sair do formulário</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Sobre</string>\n  <string name=\"tell_your_friends\">Compartilhar ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Seus colegas ainda estão coletando dados em papel? Compartilhe o ODK Collect com eles.</string>\n  <string name=\"leave_a_review\">Deixe um opinião sua na Play Store</string>\n  <string name=\"leave_a_review_msg\">A sua opinião (possivelmente positiva) aumenta a visibilidade da App na Play Store.</string>\n  <string name=\"odk_website\">Visite o site ODK</string>\n  <string name=\"odk_website_summary\">ODK é usado para coleta de dados pelo bem comum em ambientes desafiadores.</string>\n  <string name=\"odk_forum\">Junte-se ao fórum ODK</string>\n  <string name=\"odk_forum_summary\">Junte-se ao fórum para obter suporte, requisitar funcionalidades, contribuir com o código/traduções!</string>\n  <string name=\"all_open_source_licenses\">Bibliotecas/licenças open source</string>\n  <string name=\"all_open_source_licenses_msg\">Nós estamos nos ombros dos gigantes!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">Reconfigurar com QR Code</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">Substituir todas as configurações existentes</string>\n  <string name=\"qr_code\">Configuração via QR Code</string>\n  <string name=\"scan_qr_code_fragment_title\">Escanear QR Code</string>\n  <string name=\"view_qr_code_fragment_title\">QR Code</string>\n  <string name=\"import_qrcode_sd\">Importar QR Code</string>\n  <string name=\"successfully_imported_settings\">Configurações importadas com sucesso</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Número máximo de caracteres alcançado: QR code não pode ser gerado para todas as configurações</string>\n  <string name=\"share\">Compartilhar</string>\n  <string name=\"turn_on_flashlight\">Ligue a luz do flash</string>\n  <string name=\"turn_off_flashlight\">Desligue a luz do flash</string>\n  <string name=\"qrcode_with_both_passwords\">Contém informação sensível: senhas do <b>admin</b> e do <b>servidor</b></string>\n  <string name=\"qrcode_without_passwords\">Este QR code não contém senhas do <b>admin</b> nem do <b>servidor</b>. Toque para configurar</string>\n  <string name=\"qrcode_with_admin_password\">Contém informação sensível: senha do <b>admin</b> </string>\n  <string name=\"qrcode_with_server_password\">Contém informação sensível: senha do <b>servidor</b> </string>\n  <string name=\"server_password\">Senha do servidor</string>\n  <string name=\"include_password_dialog\">Senha Adicionada no Código</string>\n  <string name=\"generate\">Gerar</string>\n  <string name=\"invalid_qrcode\">QR Code não contém configurações válidas</string>\n  <string name=\"qr_code_not_found\">Um QR Code não foi encontrado na imagem selecionada</string>\n  <string name=\"corrupt_imported_preferences_error\">As configurações atuais estão corrompidas. Nas configurações de gerenciamento de projeto, restaure as configurações padrão ou importe configurações que estejam ok.</string>\n  <string name=\"settings_with_gd_protocol\">Não podem mais ser criados projetos no Google Drive/Sheets</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Cancelar</string>\n  <string name=\"cancel_loading_form\">Cancelar</string>\n  <string name=\"cancel_location\">Cancelar</string>\n  <string name=\"do_not_change\">Cancelar</string>\n  <string name=\"canceling\">Cancelando</string>\n  <string name=\"notification_channel_name\">Notificações gerais</string>\n  <string name=\"success\">Sucesso</string>\n  <string name=\"error_occured\">Ocorreu um erro</string>\n  <string name=\"please_wait\">Por favor, aguarde um pouco.</string>\n  <string name=\"please_wait_long\">Por favor, aguarde. Isto levará alguns minutos.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Não foi possível apagar \\'%s\\'. Por favor apague o arquivo manualmente e descarregue o formulário novamente.</string>\n  <string name=\"fs_create_media_folder_error\">Não foi possível criar a pasta de mídia \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Não foi possível copiar os dados de \\'%1$s\\' para \\'%2$s\\'.. Motivo: %3$s</string>\n  <string name=\"select_odk_shortcut\">Selecionar o Atalho ODK</string>\n  <string name=\"background_audio_help\">Esse formulário grava áudio do microfone do seu dispositivo.\\n\\nVocê pode usar o indicador de volume para garantir que o microfone esteja próximo o suficiente do som que você precisa gravar.\\n\\nPara parar a gravação, por favor, saia desse formulário.\\n\\nPara mais informações, por favor entre em contato com a pessoa que solicitou que você coletasse dados.</string>\n  <string name=\"loading\">Carregando…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Projetos</string>\n  <string name=\"settings\">Configurações</string>\n  <string name=\"add_project\">Adicionar projeto</string>\n  <string name=\"add\">Adicionar</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Duplicar projeto</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">Você já tem um projeto com essas configurações de conexão. Você deseja mudar para o projeto existente ou adicionar um novo?</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Adicionar projeto duplicado</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Alternar para projeto existente</string>\n  <string name=\"project_name\">Nome do projeto</string>\n  <string name=\"project_icon\">Ícone do projeto</string>\n  <string name=\"project_color\">Cor do projeto</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Alternado para %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">Usando %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Alternar para %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <string name=\"delete_project_dialog_title\">Apagar %s</string>\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <string name=\"delete_project_message\">Ao excluir esse projeto, você excluirá permanentemente o seguinte</string>\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <string name=\"delete_project_instructions\">Digite %1$s para confirmar essa ação.</string>\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <string name=\"delete_trigger\">apagar</string>\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <string name=\"form_definitions_count\">Definições do formulário: %1$d</string>\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <string name=\"sent_count\">Enviado: %1$d</string>\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <string name=\"unsent_count\">Não enviados: %1$d</string>\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <string name=\"drafts_count\">Rascunhos: %1$d</string>\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"delete_project_confirm_button_text\">Apagar o projeto</string>\n  <string name=\"or\">OR</string>\n  <string name=\"hex_color\">Código de cor hexadecimal</string>\n  <string name=\"invalid_hex_code\">Código de cor hexadecimal inválido</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Colete dados \\nem qualquer lugar</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Configurar com código QR </string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Entrar com os detalhes do projeto manualmente</string>\n  <string name=\"project_config_tip\">Após adicionar o seu projeto, você pode configurá-lo em Configurações</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Não tem ainda um projeto?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Tente a demonstração</string>\n  <string name=\"scan_configuration_qr_code\">Ler o QR Code de configuração</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Exibir detalhes</string>\n  <string name=\"errors\">Erros</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <string name=\"no_browser_installed\">Sem navegador instalado!</string>\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">Para abrir esse formulário, você deve primeiro abrir o ODK Collect e alternar para o projeto que o contém.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">O ODK Collect não foi configurado. Tente abrir o ODK Collect e configurá-lo;\\n\\nSe você clicou em um atalho, você pode precisar recriá-lo depois de configurar o ODK Collect.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">URL não reconhecida: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">URI ruim: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">O formulário preenchido foi excluído!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">Não é possível alterar esse rascunho por que o formulário em branco correspondente não foi encontrado ou foi excluído.\\n\\nForm ID: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Dois formulários foram descarregados com a mesma identificação e versão. Talvez eles sejam os mesmos formulários carregados várias vezes ou junto a servidores diferentes. De todo modo, um deles deve ser apagado.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <string name=\"cannot_open_form_because_of_forms_update\">Esse formulário não pode ser aberto agora porque os formulários estão sendo atualizados. Por favor, tente novamente em breve.</string>\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Abrir configurações</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <string name=\"entities_title\">Listas de Entidades</string>\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <string name=\"entity_browser_button\">Ver listas de entidades</string>\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">Offline</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">Este formulário não é compatível com esta versão do ODK Collect. Por favor, atualize o ODK Collect. Se o problema persistir, informe a pessoa responsável pela coleta de dados.\\n\\nVersão de especificação das Entidades do Formulário: %s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">Houve um \\\"crash\\\" na última vez que o app foi executado!</string>\n  <string name=\"cant_start_app\">Não foi possível executar o app!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">Este projeto estava conectado anteriormente a uma conta do Google Drive. O suporte ao Google Drive foi removido. Você pode configurar um servidor ou enviar as respostas para um computador.</string>\n  <string name=\"learn_more_button_text\">Saiba mais</string>\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">Ferramentas de desenvolvedor</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">Forçar a quebra do aplicativo</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Forçar uma exceção não tratada causando a quebra do aplicativo</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <string name=\"debug_filters\">Filtros de debug</string>\n  <string name=\"debug_filters_summary\">Mostrar informações de diagnóstico de erros para execuções de filtragens durante o preenchimento do formulário</string>\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"use_zxing_for_barcode_scanning\">Reverter para o scanner de código de barras antigo</string>\n  <string name=\"permission_dialog_title\">Sobre permissões</string>\n  <string name=\"permission_dialog_text\">Você deverá permitir que o ODK Collect tenha acesso às configurações abaixo, selecione \\\"permitir\\\" se você deseja usá-las.</string>\n  <string name=\"notifications\">Notificações</string>\n  <string name=\"required_for_notifications\">Requerido para mostrar atualizações quando o formulário é baixado, atualizado e enviado.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">Seu formulário foi salvo como rascunho.</string>\n  <string name=\"form_saved\">Seu formulário foi salvo.</string>\n  <string name=\"form_sending\">Enviando o formulário…</string>\n  <string name=\"edit_form\">Editar</string>\n  <string name=\"view_form\">Visualizar</string>\n  <string name=\"close_snackbar\">Fechar snackbar</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">Último formulário enviado: há %d segundos. </item>\n    <item quantity=\"many\">Últimos formulários enviados: há %d segundos. </item>\n    <item quantity=\"other\">Últimos formulários enviados: há %d segundos. </item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"one\">Último formulário enviado: há %d minutos. </item>\n    <item quantity=\"many\">Últimos formulários enviados: há %d minutos. </item>\n    <item quantity=\"other\">Últimos formulários enviados: há %d minutos. </item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"one\">Último formulário enviado: há %d horas. </item>\n    <item quantity=\"many\">Últimos formulários enviados: há %d horas. </item>\n    <item quantity=\"other\">Últimos formulários enviados: há %d horas. </item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"one\">Último formulário enviado: há %d dia.</item>\n    <item quantity=\"many\">Último formulário enviado: há %d dias.</item>\n    <item quantity=\"other\">Último formulário enviado: há %d dias.</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"one\">%d formulário pronto para ser enviado</item>\n    <item quantity=\"many\">%d formulários prontos para serem enviados</item>\n    <item quantity=\"other\">%d  formulários prontos para serem enviados</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">Nas próximas versões formulários finalizados não poderão ser modificados. Salve os formulários como rascunho para fazer modificações depois. \\n\\nVocê pode verificar erros em um formulário rascunho pressionando três pontos (⋮) e depois Verificar erros.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">Nas próximas versões formulários finalizados não poderão ser modificados.</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">Marcar todos os rascunhos como finalizados</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"one\">Você quer finalizar %d rascunho?</item>\n    <item quantity=\"many\">Você quer finalizar %d rascunhos?</item>\n    <item quantity=\"other\">Você quer finalizar %d rascunhos?</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">Ao finalizar todos os rascunhos eles serão movidos para \\\"Pronto para enviar\\\" e você não poderá mais fazer modificações. Qualquer rascunho que contiver erros não será finalizado.\\n\\nVocê não poderá desfazer essa ação.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"one\">Sucesso! %d rascunho finalizado.</item>\n    <item quantity=\"many\">Sucesso! %d rascunhos finalizados.</item>\n    <item quantity=\"other\">Sucesso! %d rascunhos finalizados.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"one\">%d rascunho contém erros que precisam ser corrigidos antes de finalizar.</item>\n    <item quantity=\"many\">%d rascunhos contém erros que precisam ser corrigidos antes de finalizar.</item>\n    <item quantity=\"other\">%d rascunhos contém erros que precisam ser corrigidos antes de finalizar.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d rascunhos finalizados. %d rascunhos possuem erros que precisam ser corrigidos antes de finalizar.</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d rascunhos finalizados. Os demais rascunhos precisam ser finalizados manualmente.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Erros</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">Não há erros</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">Desmarque para não aparecer em Rascunhos</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">Não há nada para exibir</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">Não há formulários em branco</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">Baixe o formulário para começar</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">Não há nada em rascunhos</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">Quando você salvar como rascunho os formulários aparecerão aqui</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">Não há nada pronto para enviar</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">Quando você finalizar os rascunhos eles aparecerão aqui</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">Não há nada em enviados</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">Quando você enviar formulários finalizados eles aparecerão aqui</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">Não há formulários para baixar</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">Baixe o formulário para começar</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">Não há formulários para apagar</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">Quando você tiver baixado formulários em branco, eles aparecerão aqui</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">Quando você tiver formulários salvos, eles aparecerão aqui</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">Recuperar o seu trabalho?</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"savepoint_recovery_dialog_message\">\\'O Collect foi encerrado de forma inesperada em \\'EEE, dd MMM yyyy \\'às\\' HH:mm\\' e salvou o seu trabalho.\\n\\nVocê deseja recuperar ou descartar esses dados?\\'</string>\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">Recuperar</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Descartar</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">Obter ponto</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">Ver ou modificar o ponto</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">Visualizar ponto</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">Alterar ponto</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">Obter linha</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">Ver ou modificar linha</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">Visualizar linha</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">Obter polígono</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">Ver ou modificar polígono</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">Visualizar polígono</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <string name=\"settings_json\">Parâmetros JSON</string>\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <string name=\"preparing_form_edit\">Preparando o formulário para edição…</string>\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <string name=\"user_visible_instance_name\">%s (Alteração %d )</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_title\">Continuar editando o rascunho?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_draft_edit_found_dialog_message\">\\'Uma alteração rascunho foi salva em \\'EEE, dd MMM yyyy \\'às\\' HH:mm\\'\\n\\nVocê deseja retomar esta alteração?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_positive_button\">Continuar editando</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_title\">Editar a versão mais recente?</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_finalized_edit_found_dialog_message\">\\'Essa resposta foi alterada anteriormente.\\n\\nVocê gostaria de alterar a última versão finalizada em\\' EEE, dd MMM yyyy \\'às\\' HH:mm\\'?\\'</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_positive_button\">Editar o formulário mais recente</string>\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n  <string name=\"finalized_form_edit_started\">Você está em modo de edição do formulário agora. Toque em uma questão para começar a editar.</string>\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ro/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">Versiunea %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Adăugat\\' EEE, dd MMM yyyy \\'la\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Salvat\\' EEE, dd MMM yyyy \\'la\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Finalizat\\' EEE, dd MMM yyyy \\'la\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Trimis\\' EEE, dd MMM yyyy \\'la\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Trimitere eșuată\\' EEE, dd MMM yyyy \\'la\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Modificat în\\' EEE, dd MMM yyyy \\'la\\' HH:mm</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Căutare</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Scanare finalizată. Toate formularele sunt încărcate.</string>\n  <string name=\"loading_form\">Încărcare formular</string>\n  <string name=\"survey_loading_reading_form_message\">Citesc definiția formularului…</string>\n  <string name=\"survey_loading_reading_data_message\">Citesc datele chestionarului…</string>\n  <string name=\"survey_loading_reading_csv_message\">Citesc fișierele CSV…</string>\n  <string name=\"parse_error\">Ne pare rău, nu am putut examina formularul.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Modificările nesalvate au fost restaurate din punctul de salvare!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Schimbă limba</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Mergeți către promt</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Mergeți la început</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Mergeți la sfârșit</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Mergeți sus</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Editare câmp</string>\n  <string name=\"clearanswer_confirm\">Ștergeți răspunsul de la \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Ștergeți răspunsul</string>\n  <string name=\"clear_answer_ask\">Ștergeți acest răspuns?</string>\n  <string name=\"clear_answer_no\">Anulează</string>\n  <string name=\"discard_answer\">Eliminați răspunsul</string>\n  <string name=\"discard_group\">Eliminați grupul</string>\n  <string name=\"delete_repeat\">Eliminați grupul</string>\n  <string name=\"delete_repeat_ask\">Eliminați acest grup?</string>\n  <string name=\"delete_repeat_confirm\">Eliminați grupul \\\"%s\\\" și toate sub-grupurile lui?</string>\n  <string name=\"delete_repeat_no\">Anulați</string>\n  <string name=\"invalid_answer_error\">Ne pare rău, răspunsul este invalid!</string>\n  <string name=\"required_answer_error\">Ne pare rău, răspunsul este obligatoriu!</string>\n  <string name=\"survey_internal_error\">Eroare internă: trecerea către prompt eșuată.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Sunteți la sfârșitul \\\"%s\\\".</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Ne pare rău, salvarea formularului nu a reușit!</string>\n  <string name=\"data_saved_ok\">Salvarea formularului a reușit!</string>\n  <string name=\"save_point_error\">Eroare la salvarea punctului de recuperare: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Salvați modificările</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Salvare formular</string>\n  <string name=\"survey_saving_validating_message\">Validez răspunsurile…</string>\n  <string name=\"survey_saving_collecting_message\">Colectez date…</string>\n  <string name=\"survey_saving_saving_message\">Salvez pe SD card…</string>\n  <string name=\"survey_saving_finalizing_message\">Fizalizez pe SD card…</string>\n  <string name=\"survey_saving_encrypting_message\">Criptez datele…</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Lansare</string>\n  <string name=\"activity_not_found\">Nu s-a găsit nicio activitate care să se ocupe de : %s</string>\n  <string name=\"no_app\">Aplicația solicitată lipsește. Vă rugăm să introduceți manual textul.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Deschideți Url</string>\n  <string name=\"capture_osm\">Lansează OpenMapKit</string>\n  <string name=\"recapture_osm\">Reface etichetare (tagging) OSM</string>\n  <string name=\"alert\">Alertă</string>\n  <string name=\"install_openmapkit\">Vă rugăm instalați OpenMapKit!</string>\n  <string name=\"get_barcode\">Preluați cod de bare</string>\n  <string name=\"replace_barcode\">Înlocuiți codul de bare</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Fă o poză</string>\n  <string name=\"choose_image\">Alegeți imagine</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Salvați și închideți</string>\n  <string name=\"sign_button\">Colectați semnătura</string>\n  <string name=\"markup_image\">Marcați imaginea</string>\n  <string name=\"draw_image\">Desenați imaginea</string>\n  <string name=\"reset_image\">Resetați</string>\n  <string name=\"set_color\">Setați culoarea</string>\n  <string name=\"capture_video\">Înregistrează videoclipul</string>\n  <string name=\"choose_video\">Alegeți video</string>\n  <string name=\"play_video\">Redați video</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Înregistrează sunet</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Alegeți sunet</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">A intervenit o eroare în timpul atașării fișierului media: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Selectați răspunsul</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Selectat: </string>\n  <string name=\"trigger\">OK. Vă rugăm să continuați.</string>\n  <string name=\"ext_file_no_data_error\">Fișierul nu conține date!</string>\n  <string name=\"ext_conflicting_columns_error\">%s coloane se potrivesc!</string>\n  <string name=\"ext_sortBy_numeric_error\">Coloana de sortare trebuie să conțină doar valori numerice. Valoarea care a generat conflictul a fost \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Eroare de sintaxă în funcția search(): Funcția necesită 1, 4 sau 6 argumente.</string>\n  <string name=\"ext_search_wrong_function_error\">Eroare de sintaxă în funcția search(): Funcția \\'%s\\' nerecunoscută.</string>\n  <string name=\"ext_search_bad_function_error\">Eroare de sintaxă în funcția search(): \\'%s\\' nu a fost evaluată ca funcție.</string>\n  <string name=\"ext_search_return_error\">Search handler a returnat un obiect de tip \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Nu am putut importa date din %1$s. Motiv: %2$s</string>\n  <string name=\"ext_import_progress_message\">Preîncarc date din \\'%1$s\\', vă rugăm să așteptați… %2$s</string>\n  <string name=\"ext_import_finalizing_message\">Finalizez preîncărcarea datelor…</string>\n  <string name=\"ext_import_completed_message\">Citirea datelor a fost finalizată!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager nu a fost inițializat.</string>\n  <string name=\"ext_import_csv_missing_error\">Datele externe pentru %1$s nu au fost importate. Poate ai uitat să includeți fișierul %2$s.csv în formularul dumneavoastră?</string>\n  <string name=\"ext_search_generic_error\">Eroare de sintaxă în funcția search(): %s.</string>\n  <string name=\"ext_assign_value_error\">Nu pot atribui valoarea la \\'%s\\'.</string>\n  <string name=\"get_bearing\">Înregistrare poziție</string>\n  <string name=\"replace_bearing\">Înlocuire poziție</string>\n  <string name=\"getting_bearing\">Încărcare poziție</string>\n  <string name=\"accept_bearing\">Înregistrare poziție</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Fișierul %s este invalid.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Fișierul %s lipsește.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"streets\">Străzi</string>\n  <string name=\"terrain\">Teren</string>\n  <string name=\"hybrid\">Hibrid</string>\n  <string name=\"satellite\">Satelit</string>\n  <string name=\"google_play_services_error_occured\">Nu am putut accesa Google Maps. Aveți instalat Google Play Services?</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Ne pare rău, localizarea este dezactivată!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">Vest %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">Est %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">Sud %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">Nord%1$s%2$s%3$s</string>\n  <string name=\"start\">Start</string>\n  <string name=\"enable_gps\">Activați GPS</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Trimitere formular</string>\n  <string name=\"noselect_error\">Ne pare rău, nu ați selectat niciun formular!</string>\n  <string name=\"no_connection\">Nu sunt disponibile legături la rețea</string>\n  <string name=\"sending_items\">Trimitere %1$s din %2$s formular(e)</string>\n  <string name=\"send_selected_data\">Trimiteți selectatele</string>\n  <string name=\"server_requires_auth\">Serverul necesită autentificare</string>\n  <string name=\"no_forms_uploaded\">Ne pare rău, niciun formular nu a fost încărcat.</string>\n  <string name=\"upload_results\">Încărcați rezultate</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Schimbați vederea</string>\n  <string name=\"show_sent_and_unsent_forms\">Afișați formularele trimise și netrimise</string>\n  <string name=\"show_unsent_forms\">Doar formularele netrimise</string>\n  <string name=\"send_in_progress\">Trimiterea în fundal rulează, vă rugăm să încercați din nou mai târziu.</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Actualizați</string>\n  <string name=\"download\">Luați cele selectate</string>\n  <string name=\"downloading_data\">Conectare la server</string>\n  <string name=\"fetching_file\">Preluare %1$s.\\n\\nFormular %2$s din %3$s formular(e).</string>\n  <string name=\"form_download_progress\">%1$s. Preluare fișiere media: %2$s din %3$s</string>\n  <string name=\"version\">Versiune:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Formulare salvate</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Formulare necompletate</string>\n  <string name=\"delete_confirm\">Ștergeți %s formular(e)?</string>\n  <string name=\"delete_file\">Ștergeți cele selectate</string>\n  <string name=\"delete_no\">Nu ștergeți</string>\n  <string name=\"delete_yes\">Ștergeți formulare</string>\n  <string name=\"file_deleted_ok\">%s formular(e) șters(e) cu succes!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Setări server</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL server</string>\n  <string name=\"url_error\">Ne pare rău, URL invalid!</string>\n  <string name=\"username\">Nume de utilizator</string>\n  <string name=\"change_username\">Nume utilizator ODK</string>\n  <string name=\"password\">Parola</string>\n  <string name=\"change_password\">Parola ODK</string>\n  <string name=\"password_error_whitespace\">Nu sunt permise spații în parole</string>\n  <string name=\"username_error_whitespace\">Nu sunt permise spații în numele de utilizator</string>\n  <string name=\"font_size\">Mărimea fontului textului</string>\n  <string name=\"change_font_size\">Mărimea fontului textului</string>\n  <string name=\"font_size_extra_large\">Extra mare</string>\n  <string name=\"font_size_large\">Mare</string>\n  <string name=\"font_size_medium\">Medie</string>\n  <string name=\"font_size_small\">Mică</string>\n  <string name=\"font_size_extra_small\">Extra mică</string>\n  <string name=\"navigation\">Navigare</string>\n  <string name=\"swipe_navigation\">Utilizați gesturi orizontale</string>\n  <string name=\"buttons_navigation\">Utilizați butoanele înainte/înapoi</string>\n  <string name=\"swipe_buttons_navigation\">Utilizați gesturi și butoane</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Următor</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"delete_after_send\">Ștergeți după trimitere</string>\n  <string name=\"delete_after_send_summary\">Ștergeți formulare finalizate și fișierele media după trimiterea către server</string>\n  <string name=\"constraint_behavior\">Comportament de procesare constrâns</string>\n  <string name=\"constraint_behavior_on_swipe\">Validare în timpul inaintării</string>\n  <string name=\"constraint_behavior_on_finalize\">Amânați validarea până la finalizare</string>\n  <string name=\"high_resolution_summary\">Activați înregistrările video high-resolution</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_no\">Nu</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Introduceți noua parolă</string>\n  <string name=\"admin_password\">Parola administratorului</string>\n  <string name=\"admin_password_changed\">Parola de administrator a fost schimbată cu succes</string>\n  <string name=\"admin_password_disabled\">Parola de administrator dezactivată</string>\n  <string name=\"admin_password_incorrect\">Ne pare rău, parola este incorectă!</string>\n  <string name=\"enter_admin_password\">Introduceți parola de administrator</string>\n  <string name=\"change_admin_password\">Schimbați parola administratorului</string>\n  <string name=\"found_in_main\">Deselectați pentru a ascunde din Meniul principal</string>\n  <string name=\"reset_project_settings_title\">Resetați</string>\n  <string name=\"reset_settings_button_reset\">Resetați</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"yes\">Da</string>\n  <string name=\"no\">Nu</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Descriere</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Anulează</string>\n  <string name=\"cancel_loading_form\">Anulează</string>\n  <string name=\"cancel_location\">Anulează</string>\n  <string name=\"do_not_change\">Anulați</string>\n  <string name=\"success\">Succes</string>\n  <string name=\"error_occured\">Eroare</string>\n  <string name=\"please_wait\">Vă rugăm să așteptați câteva momente.</string>\n  <string name=\"please_wait_long\">Vă rugăm să așteptați. Ar putea dura câteva minute.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Nu am putut șterge \\'%s\\'. Vă rugăm să ștergeți fișierul maunal și să re-descărcați formularul.</string>\n  <string name=\"fs_create_media_folder_error\">Nu pot crea directorul media \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Nu pot copia \\'%1$s\\' peste \\'%2$s\\'. Motiv: %3$s</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Ați descărcat două formulare diferite cu același form ID și versiune. Poate a fost același formular încărcat la momente diferite sau către servere diferite. Va trebui să ștergeți unul.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ru/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Начать новую форму</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Черновики</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Готово к отправке</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Выгружено</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Загрузить форму</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Удалить форму</string>\n  <string name=\"saved_forms\">Сохраненные формы</string>\n  <string name=\"version_number\">Версия: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Добавлено\\' EEE, dd MMM yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Обновлено\\' EEE, dd MMM yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Сохранено\\' EEE, dd MMM yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">Финализировано EEE, dd MMM yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Выгружено\\' EEE, dd MMM yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Не удалось выгрузить\\' EEE, dd MMM yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Удалено\\' EEE, dd MMM yyyy \\'в\\' HH:mm</string>\n  <string name=\"submission_deleted\">Выгрузка удалена</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Изменено\\' EEE, dd MMM yyyy \\'в\\' HH:mm</string>\n  <string name=\"encrypted_form\">Зашифрованный опрос</string>\n  <string name=\"deleted_form\">Удалённая форма</string>\n  <string name=\"sort_the_list\">Сортировать список</string>\n  <string name=\"sort_by\">Сортировать по</string>\n  <string name=\"sort_by_name_asc\">А-Я</string>\n  <string name=\"sort_by_name_desc\">Я-А</string>\n  <string name=\"sort_by_date_desc\">Дате изменения</string>\n  <string name=\"sort_by_date_asc\">Дате добавления</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Сначала последние сохраненные</string>\n  <string name=\"filter_the_list\">Фильтровать список</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Поиск</string>\n  <string name=\"not_granted_permission\">Для действия, которое вы пытаетесь сделать, требуется разрешение, которое не предоставлено.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Доступ к накопителю</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Без этих разрешений Collect не сможет получить доступ к опросам или сохранять ответы. Откройте приложение заново когда вы будете готовы предоставить доступ.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Доступ к геолокации</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Без разрешения <b>Точное местоположение</b> Collect не может записать ваше местоположение. Повторите попытку, когда будете готовы предоставить его.</string>\n  <string name=\"camera_runtime_permission_denied_title\">Доступ к камере</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Без данного разрешения Collect не сможет использовать видео-камеру. Попробуйте снова когда будете готовы предоставить доступ.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Доступ к записи аудио</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Без данного разрешения Collect не сможет использовать микрофон. Попробуйте снова когда будете готовы предоставить доступ.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Открыть карту формы</string>\n  <string name=\"instance_scan_count\">Добавлено опросников: %1$d шт.</string>\n  <string name=\"finished_disk_scan\">Сканирование завершено. Все бланки опросников загружены.</string>\n  <string name=\"xform_parse_error\">Ошибка интерпретации XForm. В %1$s отсутствует элемент %2$s</string>\n  <string name=\"loading_form\">Загрузка бланка опросника…</string>\n  <string name=\"survey_loading_reading_form_message\">Чтение описания опросника…</string>\n  <string name=\"survey_loading_reading_data_message\">Чтение данных опроса…</string>\n  <string name=\"survey_loading_reading_csv_message\">Чтение csv-файлов…</string>\n  <string name=\"parse_error\">Не удалось прочесть структуру бланка</string>\n  <string name=\"loading_form_failed\">Ошибка загрузки. Попробуйте ещё раз.</string>\n  <string name=\"cannot_edit_completed_form\">Ответы нельзя редактировать (они могут быть зашифрованы) после того как опрос помечен как финализированный.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Несохранённые изменения загружены из точки восстановления!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Смена языка</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Быстрый переход</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Сохранить как черновик</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">В начало</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">В конец</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Назад</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Группа</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Повторяющаяся группа</string>\n  <string name=\"exit\">Выйти</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Добавить \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Добавить</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Добавить другое</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Изменить вопрос</string>\n  <string name=\"clearanswer_confirm\">Удалить ответ к \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Удалить ответ</string>\n  <string name=\"clear_answer_ask\">Удалить этот ответ?</string>\n  <string name=\"clear_answer_no\">Отмена</string>\n  <string name=\"discard_answer\">Удалить ответ</string>\n  <string name=\"discard_group\">Удалить группу</string>\n  <string name=\"delete_repeat\">Удалить группу</string>\n  <string name=\"delete_repeat_ask\">Удалить эту группу?</string>\n  <string name=\"delete_repeat_confirm\">Удалить группу \\\"%s\\\" и все её подгруппы?</string>\n  <string name=\"delete_repeat_no\">Отмена</string>\n  <string name=\"invalid_answer_error\">Этот ответ неверный</string>\n  <string name=\"required_answer_error\">Ответ на этот вопрос обязателен!</string>\n  <string name=\"success_form_validation\">Успешно! Ошибки в вашей форме не найдены.</string>\n  <string name=\"invalid_space_in_answer_singular\">Внимание: нижеследующее значение %s содержит пробелы</string>\n  <string name=\"invalid_space_in_answer_plural\">Внимание: нижеследующие значения %s содержат пробелы</string>\n  <string name=\"too_complex_form\">Эта форма слишком сложна для этого устройства. Попробуйте упростить структуру или обратитесь за помощью на форум.</string>\n  <string name=\"survey_internal_error\">Внутренняя ошибка: step to prompt failed.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Вы в конце опросника \\\"%s\\\"</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Если вам необходимо внести изменения в форму, сохраните ее как черновик, пока не будете готовы к отправке</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Сохранить как черновик</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Завершить</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Отправить</string>\n  <string name=\"data_saved_error\">Не удалось сохранить ответы</string>\n  <string name=\"data_saved_ok\">Ответы успешно сохранены</string>\n  <string name=\"save_point_error\">Ошибка сохранения точки восстановления: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Отменить форму</string>\n  <string name=\"discard_changes\">Отменить изменения</string>\n  <string name=\"keep_changes\">Сохранить ответы</string>\n  <string name=\"keep_editing\">Продолжить редактирование</string>\n  <string name=\"quit_form_title\">Сохранить форму?</string>\n  <string name=\"save_explanation\">Вы можете сохранить эту форму и получить к ней доступ из своих черновиков в любое время.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'В последний раз эта форма была сохранена\\' EEE, MMM dd, yyyy \\'в\\' HH:mm\\'. Сохраните как черновик, чтобы сохранить изменения\\'.</string>\n  <string name=\"quit_form_continue_title\">Продолжить редактирование?</string>\n  <string name=\"discard_form_warning\">Если вы отмените форму, вы потеряете все изменения, которые вы сделали до сих пор.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'В последний раз эта форма была сохранена\\' EEE, MMM dd, yyyy \\'в\\' HH:mm\\'. Если вы отмените изменения, вы потеряете все изменения, сделанные с тех пор\\'.</string>\n  <string name=\"saving_form\">Выполняется сохранение …</string>\n  <string name=\"survey_saving_validating_message\">Проверка ответов…</string>\n  <string name=\"survey_saving_collecting_message\">Сбор данных…</string>\n  <string name=\"survey_saving_saving_message\">Сохранение на SD карту…</string>\n  <string name=\"survey_saving_finalizing_message\">Финализация на SD-карту…</string>\n  <string name=\"survey_saving_encrypting_message\">Шифрование данных…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Должна быть одна запись для этого варианта!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Более одной пустой формы соответствует этому jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Не указан FormId???</string>\n  <string name=\"phone_does_not_support_rsa\">Телефон не поддерживает шифрование RSA.</string>\n  <string name=\"invalid_rsa_public_key\"> Неверный публичный ключ RSA.</string>\n  <string name=\"encryption_error_message\">%s Опрос НЕ был сохранён как финализированный.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">Форма не может быть использована</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Не удалось начать запись.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Микрофон уже используется.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">Медиафайл не может быть прикреплен к форме, но доступен по адресу %1$s</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Запись отключена. Включить в %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Ошибка при попытке обновить значения. Как правило, это происходит из-за неправильного  дизайна формы.\\n\\nЕсли проблема не уйдет, сообщите о ней человеку, который попросил вас собрать данные.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Запуск</string>\n  <string name=\"activity_not_found\">Случилось что-то страшное: %s</string>\n  <string name=\"no_app\">Запрошенное приложение отсутствует. Пожалуйста, выполните ручной ввод.</string>\n  <string name=\"null_intent_value\">Внешнее приложение не предоставило ожидаемую информацию</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">Печать</string>\n  <string name=\"open_url\">Открыть адрес</string>\n  <string name=\"capture_osm\">Запустить OpenMapKit</string>\n  <string name=\"recapture_osm\">Повторить теги OSM</string>\n  <string name=\"edited_osm_file\">Редактируемый файл OSM XML:</string>\n  <string name=\"invalid_osm_data\">Что-то пошло не так. Не удалось получить правильные данные OSM.</string>\n  <string name=\"alert\">Тревога</string>\n  <string name=\"install_openmapkit\">Пожалуйста, установите OpenMapKit!</string>\n  <string name=\"get_barcode\">Получить штрих-код</string>\n  <string name=\"replace_barcode\">Заменить штрих-код</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Вместите штрихкод внутрь прямоугольника</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Открыть файл</string>\n  <string name=\"rank_items\">Сортировать</string>\n  <string name=\"number_picker_title\">Выбор номера</string>\n  <string name=\"invalid_range_widget\">У этого виджета неправильные параметры!</string>\n  <string name=\"svg_file_does_not_exist\">SVG файл не найден!</string>\n  <string name=\"capture_image\">Сделать снимок</string>\n  <string name=\"choose_image\">Выберите изображение</string>\n  <string name=\"selected_invalid_image\">Выбрайнный файл не является изображением</string>\n  <string name=\"take_picture_instruction\">Нажмите на экран, чтобы сделать фото</string>\n  <string name=\"error_front_camera_unavailable\">Передняя камера недоступна на данном устройстве</string>\n  <string name=\"camera_error\">Произошла ошибка при съемке</string>\n  <string name=\"camera_failed_to_initialize\">Не удалось запустить камеру!</string>\n  <string name=\"annotate_image\">Добавить примечание</string>\n  <string name=\"gif_not_supported\">GIF-файлы не поддерживаются</string>\n  <string name=\"invalid_file_type\">Приложение вернуло недопустимый тип файла</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Сохранить и закрыть</string>\n  <string name=\"signature_capture\">Подпись</string>\n  <string name=\"sign_button\">Взять подпись</string>\n  <string name=\"markup_image\">Разметить изображение</string>\n  <string name=\"draw_image\">Нарисовать изображение</string>\n  <string name=\"reset_image\">Сброс</string>\n  <string name=\"set_color\">Выбрать цвет</string>\n  <string name=\"capture_video\">Записать видео</string>\n  <string name=\"choose_video\">Выберите видео</string>\n  <string name=\"play_video\">Проиграть видео</string>\n  <string name=\"choose_file\">Выберите файл</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Удалить</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Записать аудио</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Выберите аудио</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Пауза</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Продолжить</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">Запись звука: вкл.</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">Запись звука: выкл.</string>\n  <string name=\"background_audio_permission_explanation\">Эта форма записывает звук в фоновом режиме. Вы должны предоставить разрешение на использование микрофона. В противном случае вы не сможете открыть эту форму.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Эта форма запрашивает фоновую аудиозапись. Отключение остановит запись и удалит существующее аудио. Вы уверены, что хотите продолжить?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Отключить запись</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">Запись начнется не сразу. Вы должны повторно открыть форму для записи.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Удалить файл?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">Вы не сможете восстановить этот файл после его удаления. Вы уверены, что хотите продолжить?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Остановить</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Запись…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Аудио запись</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Воспроизвести аудио</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Сохранение файла</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Вы должны остановить запись, прежде чем покинуть этот экран.</string>\n  <string name=\"error_attaching_binary_file\">Произошла ошибка при добавлении файла: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Выбрать значение</string>\n  <string name=\"select_answer\">Выбрать ответ</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Изменить значение</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Значение не выбрано</string>\n  <string name=\"selected_title\">Выбрано:</string>\n  <string name=\"trigger\">OK. Продолжайте.</string>\n  <string name=\"ext_file_no_data_error\">Файл не содержит данных!</string>\n  <string name=\"ext_conflicting_columns_error\">Столбцы %s совпадают!</string>\n  <string name=\"ext_sortBy_numeric_error\">Столбец для сортировки может содержать только численные значения. Проблемное значение \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Синтаксическая ошибка в функции search(): функция принимает 1, 4 или 6 аргументов.</string>\n  <string name=\"ext_search_wrong_function_error\">Синтаксическая ошибка в функции search(): неопознанная функция \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Синтаксическая ошибка в функции search(): \\'%s\\' не является функцией.</string>\n  <string name=\"ext_search_return_error\">Обработчик поиска вернул объект типа \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Не удалось импортировать данные из %1$s. Причина: %2$s</string>\n  <string name=\"ext_import_progress_message\">Загружаются данные из \\'%1$s\\', пожалуйста, ждите… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Чтение данных отменено!</string>\n  <string name=\"ext_import_finalizing_message\">Загруженные данные финализируются…</string>\n  <string name=\"ext_import_completed_message\">Чтение данных завершено</string>\n  <string name=\"ext_not_initialized_error\">Обработчик внешних данных ExternalDataManager не инициализирован.</string>\n  <string name=\"ext_import_csv_missing_error\">Внешние данные для %1$s не были загружены. Возможно, вы забыли включить файл %2$s.csv в опросник?</string>\n  <string name=\"ext_search_generic_error\">Синтаксическая ошибка в функции search(): %s</string>\n  <string name=\"ext_assign_value_error\">Не удалось присовить значение к \\'%s\\'.</string>\n  <string name=\"parser_exception\">Исключение XPathParser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Определить азимут</string>\n  <string name=\"replace_bearing\">Изменить азимут</string>\n  <string name=\"getting_bearing\">Определяю азимут</string>\n  <string name=\"accept_bearing\">Принять азимут</string>\n  <string name=\"bearing\">Азимут: %.3f</string>\n  <string name=\"direction\">Направление: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Не удалось определить азимут: в устройстве отсутствует акселерометр или сенсор магнитного поля, или то и другое.</string>\n  <string name=\"select_date\">Выберите дату</string>\n  <string name=\"select_time\">Выберите время</string>\n  <string name=\"no_date_selected\">Дата не выбрана</string>\n  <string name=\"no_time_selected\">Время не выбрано</string>\n  <string name=\"ethiopian_month_1\">Мескерем</string>\n  <string name=\"ethiopian_month_2\">Тикимт</string>\n  <string name=\"ethiopian_month_3\">Хидар</string>\n  <string name=\"ethiopian_month_4\">Тахсас</string>\n  <string name=\"ethiopian_month_5\">Тир</string>\n  <string name=\"ethiopian_month_6\">Йекатит</string>\n  <string name=\"ethiopian_month_7\">Мегабит</string>\n  <string name=\"ethiopian_month_8\">Миязья</string>\n  <string name=\"ethiopian_month_9\">Гинбот</string>\n  <string name=\"ethiopian_month_10\">Сене</string>\n  <string name=\"ethiopian_month_11\">Хамле</string>\n  <string name=\"ethiopian_month_12\">Нахасе</string>\n  <string name=\"ethiopian_month_13\">Пагумен</string>\n  <string name=\"coptic_month_1\">Тот</string>\n  <string name=\"coptic_month_2\">Фаофи</string>\n  <string name=\"coptic_month_3\">Атир</string>\n  <string name=\"coptic_month_4\">Хойяк</string>\n  <string name=\"coptic_month_5\">Тиби</string>\n  <string name=\"coptic_month_6\">Мехир</string>\n  <string name=\"coptic_month_7\">Фаменот</string>\n  <string name=\"coptic_month_8\">Фармути</string>\n  <string name=\"coptic_month_9\">Пахон</string>\n  <string name=\"coptic_month_10\">Пайни</string>\n  <string name=\"coptic_month_11\">Эпифи</string>\n  <string name=\"coptic_month_12\">Мисори</string>\n  <string name=\"coptic_month_13\">Пи Коги Энавот</string>\n  <string name=\"islamic_month_1\">Мухаррам</string>\n  <string name=\"islamic_month_2\">Сафар</string>\n  <string name=\"islamic_month_3\">Раби аль-авваль</string>\n  <string name=\"islamic_month_4\">Раби ас-сани</string>\n  <string name=\"islamic_month_5\">Джумада аль-уля</string>\n  <string name=\"islamic_month_6\">Джумада ас-сани</string>\n  <string name=\"islamic_month_7\">Раджаб</string>\n  <string name=\"islamic_month_8\">Шабан</string>\n  <string name=\"islamic_month_9\">Рамадан</string>\n  <string name=\"islamic_month_10\">Шавваль</string>\n  <string name=\"islamic_month_11\">Зу-ль-када</string>\n  <string name=\"islamic_month_12\">Зу-ль-хиджа</string>\n  <string name=\"persian_month_4\">Тир</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Файл: %s некорректный.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Файл: %s не найден.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Введите имя</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Опознание</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Эта форма требует, чтобы вы идентифицировали себя</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Закрыть</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Причина изменений</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Причина</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Вам необходимо объяснить причину внесения изменений в эту форму</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Сохранить</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">Выберите место</string>\n  <string name=\"choices\">Варианты</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">Национальная карта USGS: топографическая</string>\n  <string name=\"openmap_usgs_sat\">Национальная карта USGS: спутниковая</string>\n  <string name=\"openmap_usgs_img\">Национальная карта USGS: съёмка с воздуха</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Тёмная материя</string>\n  <string name=\"basemap\">Подложка</string>\n  <string name=\"basemap_source\">Источник</string>\n  <string name=\"basemap_source_unavailable\">К сожалению, базовые карты %s недоступны на этом устройстве. Выберите другую базовую карту в разделе Настройки &gt; Карты.</string>\n  <string name=\"map_style_label\">%s стиль карты</string>\n  <string name=\"streets\">Улицы</string>\n  <string name=\"terrain\">Ландшафт</string>\n  <string name=\"hybrid\">Гибридная</string>\n  <string name=\"satellite\">Спутник</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Светлая</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Тёмная</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Снаружи</string>\n  <string name=\"topographic\">Топографический</string>\n  <string name=\"google_play_services_error_occured\">Не удалось получить доступ к Google картам. Установлен ли Google Play Services?</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">Оффлайн слои</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">Слой</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">Выбрать офлайн-слой</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">Выберите офлайн-слой для использования во всех картах этого проекта. Вы можете добавить параметры в список, выбрав файл MBTiles на своем устройстве, а также удалить существующие слои из списка.</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">Подробнее о добавлении MBTiles.</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">Добавить слои</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">Удалить слой</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">Выбранные вами файлы не являются MBTiles. Вы можете добавлять только файлы MBTiles.</string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">Некоторые из выбранных вами файлов не являются MBTiles. Вы можете добавлять только файлы MBTiles.</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">Слои</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">Выбрать доступ к слоям</string>\n  <string name=\"select_layer_access_subtitle\">Хотите ли вы, чтобы слои были доступны во всех проектах или только в текущем проекте?</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">Все проекты</string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">Только текущий проект</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">Вы уверены, что хотите удалить оффлайн слой %1$s?</string>\n  <string name=\"record_geopoint\">Зафиксировать точку</string>\n  <string name=\"provider_disabled_error\">Включите функцию определения местоположения на вашем устройстве!</string>\n  <string name=\"google_play_services_not_available\">Эта форма хочет отслеживать ваше местоположение, но сервисы Google Play недоступны.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Эта форма хочет отслеживать ваше местоположение, но определение местоположения отключено. Включите в настройках Android.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Перейдите в настройки</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">Местоположение: вкл.</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">Местоположение: выкл.</string>\n  <string name=\"background_location_disabled\">Эта форма хочет отслеживать ваше местоположение, но отслеживание отключено. Пожалуйста, включите в меню %1$s выше.</string>\n  <string name=\"background_location_enabled\">Эта форма отслеживает ваше местоположение. Вы можете отключить отслеживание в меню %1$s выше.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">Проверить на ошибки</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">Истекшее время: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">Точка будет сохранена в %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">Спутники: %1$s</string>\n  <string name=\"waiting_for_location\">Попытка получить местоположение. Пожалуйста, подождите.</string>\n  <string name=\"poor_accuracy\">Плохая точность. Пожалуйста, подождите.</string>\n  <string name=\"unacceptable_accuracy\">Недопустимая точность. Пожалуйста, подождите.</string>\n  <string name=\"improving_accuracy\">Повышение точности. Пожалуйста, подождите.</string>\n  <string name=\"accuracy_status_title\">Получение местоположения</string>\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">З %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">В %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">Ю %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">С %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Метод ввода</string>\n  <string name=\"start\">Начать</string>\n  <string name=\"enable_gps\">Включить GPS</string>\n  <string name=\"gps_enable_message\">GPS отключен на вашем устройстве. Хотите включить его?</string>\n  <string name=\"geo_clear_warning\">Опция уже создана. Вы хотите удалить опцию?</string>\n  <string name=\"geo_exit_warning\">Отказаться от изменений и вернуться к ODK?</string>\n  <string name=\"polygon_validator\">В многоугольнике должно быть как минимум 3 точки</string>\n  <string name=\"polyline_validator\">Должно быть как минимум 2 точки, что бы создать ломаную линию</string>\n  <string name=\"clear\">Очистить</string>\n  <string name=\"placement_mode\">Размещение нажатием</string>\n  <string name=\"manual_mode\">Ручная запись местоположения</string>\n  <string name=\"automatic_mode\">Автоматическая запись местоположения</string>\n  <string name=\"recording_interval\">Интервал записи:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <string name=\"accuracy_requirement\">Требование к точности:</string>\n  <string name=\"none\">Нет</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"collection_status_paused\">Введено точек: %d</string>\n  <string name=\"collection_status_placement\">Введено точек: %d (нажмите для добавления точек)</string>\n  <string name=\"collection_status_manual\">Введено точек: %d (ручная запись)</string>\n  <string name=\"collection_status_auto_seconds\">Введено точек: %1$d (запись каждые %2$d секунды)</string>\n  <string name=\"collection_status_auto_minutes\">Введено точек: %1$d (запись каждые %2$d минуты)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Введено точек: %1$d (запись каждые %2$d сек, точность %3$d м)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Введено точек: %1$d (запись каждые %2$d мин, точность %3$d м)</string>\n  <string name=\"geopoint_no_draggable_instruction\">Нажмите кнопку \\\"добавить отметку\\\".</string>\n  <string name=\"discard\">Отменить</string>\n  <string name=\"select_item_count\">%s: %d (%d показано на карте)</string>\n  <string name=\"select_item\">Выбрать</string>\n  <string name=\"new_item\">Новый элемент</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">Редактировать Черновик</string>\n  <string name=\"view_data\">Посмотреть Сохраненную Форму</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">Отслеживание местоположения</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">Отслеживание местоположения…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Показать мое местоположение</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">Увеличить, чтобы уместить все</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Пауза</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">Удалить последнюю точку</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">Удалить последнюю точку</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Выгрузка опроса</string>\n  <string name=\"noselect_error\">Не было выбрано ни одного бланка</string>\n  <string name=\"no_connection\">Отсутствует подключение к сети</string>\n  <string name=\"sending_items\">Выгрузка %1$s опроса из %2$s</string>\n  <string name=\"send_selected_data\">Выгрузить выбранные</string>\n  <string name=\"server_auth_credentials\">Неправильное имя пользователя или пароль для сервера: %s</string>\n  <string name=\"server_requires_auth\">Необходима аутентификация</string>\n  <string name=\"no_forms_uploaded\">Опрос НЕ выгружен</string>\n  <string name=\"upload_results\">Результаты выгрузки</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Изменить внешний вид</string>\n  <string name=\"show_sent_and_unsent_forms\">Показывать выгруженные и невыгруженные опросы</string>\n  <string name=\"show_unsent_forms\">Показать невыгруженные опросы</string>\n  <string name=\"send_in_progress\">Выполняется отправка в фоне, попробуйте снова через некоторое время.</string>\n  <string name=\"forms_upload_succeeded\">Загрузка форм выполнена успешно</string>\n  <string name=\"forms_upload_failed\">Не удалось загрузить формы</string>\n  <string name=\"all_uploads_succeeded\">Все загрузки выполнены успешно!</string>\n  <string name=\"some_uploads_failed\">%1$s из %2$s не удалось загрузить!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Обновить</string>\n  <string name=\"download\">Загрузить выбранные</string>\n  <string name=\"downloading_data\">Подключение к серверу…</string>\n  <string name=\"fetching_file\">Загружаю %1$s.\\n\\nБланк опросника %2$s из %3$s.</string>\n  <string name=\"form_download_progress\">%1$s. Загрузка медиа-файлов: %2$s из %3$s</string>\n  <string name=\"version\">Версия:</string>\n  <string name=\"form_updates_available\">Доступен обновленный опросник</string>\n  <string name=\"newer_version_of_a_form_info\">Это обновление для уже имеющегося у вас опросника</string>\n  <string name=\"form_update_error\">Не удалось обновить форму</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">Не удалось загрузить список форм</string>\n  <string name=\"forms_download_succeeded\">Загрузка форм выполнена успешно</string>\n  <string name=\"forms_download_failed\">Не удалось загрузить формы</string>\n  <string name=\"all_downloads_succeeded\">Все загрузки прошли успешно!</string>\n  <string name=\"some_downloads_failed\">%1$s из %2$s не удалось загрузить!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Версия: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Если вы продолжаете сталкиваться с этой проблемой, сообщите об этом человеку, который попросил вас собрать данные.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect не может связаться с сервером по адресу %s. Вы правильно ввели URL?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect не может безопасно подключиться к серверу по адресу %s. Вы правильно ввели URL?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">Сервер %1$s вернул статусный код %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">Сервер %s предоставил неверный ответ.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">Сервер не предоставил хеш для этой формы.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">При загрузке этой формы на устройстве произошла ошибка диска.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">Эта форма имеет недопустимый URL отправки.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Сохраненные формы</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Бланки</string>\n  <string name=\"delete_confirm\">Удалить бланк: %s шт.?</string>\n  <string name=\"delete_file\">Удалить выбранные</string>\n  <string name=\"delete_no\">Не удалять</string>\n  <string name=\"delete_yes\">Удалить бланки</string>\n  <string name=\"form_delete_message\">Удаляю выбранные опросники</string>\n  <string name=\"file_deleted_ok\">Бланк опросника успешно удалён: %s шт.</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Настройки проекта</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Сервер</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, логин, пароль</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Отображение проекта</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Название, иконка, цвет</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Интерфейс</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">Карты</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Базовая карта, стили, слои</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Управление опросниками</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">Автообновление, автоотправка, автоудаление</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">ID пользователя и устройства</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">Имя пользователя, номер телефона, ID устройства</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Экспериментальный</string>\n  <string name=\"protected_settings\">Защищенные</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">Разблокировать защищенные настройки</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">Сменить пароль, установить контроль доступа</string>\n  <string name=\"set_admin_password\">Установить пароль администратора</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Управление проектом</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">Перенастроить, сбросить, удалить</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Контроль доступа</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">Ограничить пользовательский интерфейс</string>\n  <string name=\"server_preferences\">Настройки сервера</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">Сервер</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Адрес сервера</string>\n  <string name=\"url_error\">Неверный адрес!</string>\n  <string name=\"username\">Имя пользователя</string>\n  <string name=\"change_username\">Имя пользователя ODK</string>\n  <string name=\"password\">Пароль</string>\n  <string name=\"change_password\">Пароль ODK</string>\n  <string name=\"password_error_whitespace\">Не допускается использовать пробелы в начале и конце пароля</string>\n  <string name=\"username_error_whitespace\">Не допускается использовать пробелы в начале и конце имени пользователя</string>\n  <string name=\"app_theme\">Тема</string>\n  <string name=\"language\">Язык</string>\n  <string name=\"use_device_language\">Использовать язык устройства</string>\n  <string name=\"font_size\">Размер шрифта</string>\n  <string name=\"change_font_size\">Размер шрифта</string>\n  <string name=\"font_size_extra_large\">Очень большой</string>\n  <string name=\"font_size_large\">Большой</string>\n  <string name=\"font_size_medium\">Средний</string>\n  <string name=\"font_size_small\">Маленький</string>\n  <string name=\"font_size_extra_small\">Очень маленький</string>\n  <string name=\"navigation\">Навигация</string>\n  <string name=\"swipe_navigation\">Горизонтальные свайпы</string>\n  <string name=\"buttons_navigation\">Кнопки вперёд/назад</string>\n  <string name=\"swipe_buttons_navigation\">Свайпы и кнопки</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Далее</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Назад</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">ОБНОВЛЕНИЕ БЛАНКОВ</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Режим обновления пустой формы</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Ручной</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Только ранее загруженные формы</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Точное соответствие серверу</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Частота автоматического обновления</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Каждые 15 минут</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Каждый 1 час</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Каждые 6 часов</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Каждые 24 часа</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Автозагрузка</string>\n  <string name=\"automatic_download_summary\">Автоматически загружать новые версии бланков</string>\n  <string name=\"hide_old_form_versions_setting_title\">Скрывать прошлые версии бланков</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Только новые версии будут показаны в списке заполнения бланков</string>\n  <string name=\"form_submission_category\">ВЫГРУЗКА ОПРОСОВ</string>\n  <string name=\"autosend_selector_title\">Авто выгрузка</string>\n  <string name=\"autosend\">Авто-выгрузка</string>\n  <string name=\"off\">Выключено</string>\n  <string name=\"wifi_autosend\">Только Wi-Fi</string>\n  <string name=\"cellular_autosend\">Только сотовая связь</string>\n  <string name=\"wifi_cellular_autosend\">Wi-Fi или сотовая связь</string>\n  <string name=\"delete_after_send\">Удалить после отправки</string>\n  <string name=\"delete_after_send_summary\">Удаляет финализированные опросники и их медиа после отправки данных на сервер</string>\n  <string name=\"form_filling_category\">Заполнение опросника</string>\n  <string name=\"constraint_behavior_title\">Обработка ограничений</string>\n  <string name=\"constraint_behavior\">Способ проверки ограничений</string>\n  <string name=\"constraint_behavior_on_swipe\">Проверять после каждого вопроса</string>\n  <string name=\"constraint_behavior_on_finalize\">Проводить валидацию при финализации опросника</string>\n  <string name=\"high_resolution_title\">HD-видео</string>\n  <string name=\"high_resolution_summary\">Разрешить запись видео высокого разрешения</string>\n  <string name=\"image_size_title\">Размер изображения</string>\n  <string name=\"image_size_dialog_title\">Наибольший размер изображения по длинной стороне</string>\n  <string name=\"image_size_large\">Большой (3072 пикс)</string>\n  <string name=\"image_size_medium\">Средний (2048 пикс)</string>\n  <string name=\"image_size_small\">Малый (1024 пикс)</string>\n  <string name=\"image_size_very_small\">Крошечный (640 пикс)</string>\n  <string name=\"image_size_original\">Оригинальный размер с камеры (по умолчанию)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Показать указания к вопросам</string>\n  <string name=\"guidance_no\">Нет</string>\n  <string name=\"guidance_yes\">Да - всегда показывать</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Да - показывать свёрнутыми</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">Использовать внешнее приложение для аудиозаписи</string>\n  <string name=\"form_import_category\">Импорт опросника</string>\n  <string name=\"instance_sync\">Финализировать опросы при импорте</string>\n  <string name=\"instance_sync_summary\">Влияет на формы, добавленные в папку экземпляров.</string>\n  <string name=\"analytics_preferences\">Данные по использованию</string>\n  <string name=\"analytics\">Сбор анонимной статистики</string>\n  <string name=\"analytics_summary\">Анонимные данные об использовании помогают команде ODK определять приоритеты исправлений и функций.</string>\n  <string name=\"form_metadata_title\">Метаданные</string>\n  <string name=\"form_metadata\">Метаданные</string>\n  <string name=\"form_metadata_summary\">будут добавлены к ответам для идентификации интервьюера проводящего опросы</string>\n  <string name=\"user_defined\">Пользовательские данные</string>\n  <string name=\"phone_number\">Номер телефона</string>\n  <string name=\"email\">Адрес эл. почты</string>\n  <string name=\"invalid_email_address\">Неверный адрес почты!</string>\n  <string name=\"device_defined\">Автоматически определено</string>\n  <string name=\"device_id\">Идентификатор устройства</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Недоступно</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">Разблокировать настройки</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">Настройки разблокированы</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <string name=\"setting_not_available_during_form_entry\">Настройка недоступна при заполнении формы</string>\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Укажите новый пароль</string>\n  <string name=\"admin_password\">Пароль администратора</string>\n  <string name=\"admin_password_changed\">Пароль администратора успешно изменён</string>\n  <string name=\"admin_password_disabled\">Пароль администратора отключен</string>\n  <string name=\"admin_password_incorrect\">Пароль неверный!</string>\n  <string name=\"enter_admin_password\">Введите пароль администратора</string>\n  <string name=\"change_admin_password\">Изменить пароль администратора</string>\n  <string name=\"show_password\">Показать пароль</string>\n  <string name=\"found_in_main\">Снимите галочку, чтобы скрыть пункт в главном меню</string>\n  <string name=\"found_in_settings\">Снимите галочку, чтобы скрыть незащищенные настройки</string>\n  <string name=\"form_entry\">Снимите галочку, чтобы скрыть пункт при заполнении опросника</string>\n  <string name=\"form_end\">Снимите галочку, чтобы скрыть пункт в конце формы</string>\n  <string name=\"reset_project_settings_title\">Сброс</string>\n  <string name=\"reset_project_settings_subtext\">Выберите из настроек, форм, данных</string>\n  <string name=\"delete_project\">Удалить</string>\n  <string name=\"select_all\">Выбрать всё</string>\n  <string name=\"clear_all\">Очистить всё</string>\n  <string name=\"reset_settings\">Все настройки (внутренние настройки, сохранённые настройки)</string>\n  <string name=\"reset_saved_forms\">Сохранённые опросы (instances folder, instances database)</string>\n  <string name=\"reset_blank_forms\">Бланки опросников (forms folder, forms database, itemsets database)</string>\n  <string name=\"reset_layers\">Слои карты (папка layers)</string>\n  <string name=\"reset_cache\">Кэш (папка .cache)</string>\n  <string name=\"reset_settings_dialog_title\">Выберите, что хотите сбросить</string>\n  <string name=\"reset_settings_button_reset\">Сброс</string>\n  <string name=\"reset_in_progress\">Сбрасывание…</string>\n  <string name=\"reset_app_state_result\">Результаты сброса</string>\n  <string name=\"reset_app_state_warning\">Все выбранные данные будут удалены безвозвратно. Отмена не предусмотрена.</string>\n  <string name=\"reset_settings_result\">Все настройки: %s</string>\n  <string name=\"reset_saved_forms_result\">Сохранённые опросы: %s</string>\n  <string name=\"reset_blank_forms_result\">Бланки опросников: %s</string>\n  <string name=\"reset_layers_result\">Слои карт: %s</string>\n  <string name=\"reset_cache_result\">Кэш загрузки формы :: %s</string>\n  <string name=\"main_menu_settings\">Кнопки главного меню…</string>\n  <string name=\"main_menu_settings_summary\">Показать или скрыть кнопки</string>\n  <string name=\"user_settings\">Видимость настроек</string>\n  <string name=\"user_setting_summary\">Показать или скрыть настройки</string>\n  <string name=\"form_entry_setting\">Настройки ввода опросников</string>\n  <string name=\"form_entry_settings_summary\">Показать или скрыть действия</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Возвращение назад</string>\n  <string name=\"moving_backwards_disabled_title\">Возвращение назад запрещено</string>\n  <string name=\"moving_backwards_disabled_message\">Настроить устройство, чтобы пользователи не могли обойти этот параметр?\\n\\nИзменения:\\n\\u2022 Отключить редактирование черновика\\n\\u2022 Отключить сохранить как черновик\\n\\u2022 Отключить запрос перехода\\n\\u2022 Установить Обработка ограничений на Проверка при свайпе вперед</string>\n  <string name=\"yes\">Да</string>\n  <string name=\"no\">Нет</string>\n  <string name=\"moving_backwards_enabled_title\">Возвращение назад разрешено</string>\n  <string name=\"moving_backwards_enabled_message\">Вы можете просмотреть следующие настройки:\\n\\n\\u2022 Редактировать черновик\\n\\u2022 Сохранить как черновик\\n\\u2022 Перейти к подсказке\\n\\u2022 Обработка ограничений</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Сохранить как черновик</string>\n  <string name=\"save_mid_summary\">Иконка сохранения в верхней панели и кнопка \\\"Сохранить как черновик\\\" при выходе из формы</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">О программе</string>\n  <string name=\"tell_your_friends\">Рекомендовать ODK</string>\n  <string name=\"tell_your_friends_msg\">Коллеги ведут сбор данных на бумаге? Поделитесь с ними этим решением</string>\n  <string name=\"leave_a_review\">Оставить отзыв в Play Store</string>\n  <string name=\"leave_a_review_msg\">Ваш отзыв (надеемся, положительный) делает приложение более заметным в Play Store</string>\n  <string name=\"odk_website\">Вебсайт ODK</string>\n  <string name=\"odk_website_summary\">ODK — приложение для сбора данных работающее без интернета</string>\n  <string name=\"odk_forum\">Форум</string>\n  <string name=\"odk_forum_summary\">Вопросы и предложения</string>\n  <string name=\"all_open_source_licenses\">Используемые компоненты</string>\n  <string name=\"all_open_source_licenses_msg\">Мы стоим на плечах гигантов!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">Перенастроить с помощью QR-кода</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">Заменить все существующие настройки</string>\n  <string name=\"qr_code\">Настройка QR-кода</string>\n  <string name=\"scan_qr_code_fragment_title\">Сканировать</string>\n  <string name=\"view_qr_code_fragment_title\">QR-код</string>\n  <string name=\"import_qrcode_sd\">Импортировать QR-код</string>\n  <string name=\"successfully_imported_settings\">Настройки успешно импортированы</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Превышен лимит количества символов: QR-код не удалось сгенерировать для всех настроек.</string>\n  <string name=\"share\">Поделиться</string>\n  <string name=\"turn_on_flashlight\">Включить подсветку</string>\n  <string name=\"turn_off_flashlight\">Выключить подсветку</string>\n  <string name=\"server_password\">Пароль сервера</string>\n  <string name=\"include_password_dialog\">Пароли включены в код</string>\n  <string name=\"generate\">Сгенерировать</string>\n  <string name=\"invalid_qrcode\">В QR-коде не содержатся правильные настройки</string>\n  <string name=\"qr_code_not_found\">QR-код не найден на выбранном изображении</string>\n  <string name=\"corrupt_imported_preferences_error\">Текущие настройки повреждены. Из настроек управления проектом сбросить настройки или импортировать рабочие.</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Отмена</string>\n  <string name=\"cancel_loading_form\">Отмена</string>\n  <string name=\"cancel_location\">Отмена</string>\n  <string name=\"do_not_change\">Отмена</string>\n  <string name=\"canceling\">Отменяю</string>\n  <string name=\"notification_channel_name\">Общие уведомления</string>\n  <string name=\"success\">Успешно</string>\n  <string name=\"error_occured\">Произошла ошибка</string>\n  <string name=\"please_wait\">Подождите несколько секунд.</string>\n  <string name=\"please_wait_long\">Пожалуйста, подождите. Это может занять несколько минут.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Не удалось удалить \\'%s\\'. Пожалуйста, удалите файл вручную и загрузите опросник заново.</string>\n  <string name=\"fs_create_media_folder_error\">Не удалось создать директорию media \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Не удалось скопировать \\'%1$s\\' в \\'%2$s\\'. Причина: %3$s</string>\n  <string name=\"select_odk_shortcut\">Выбрать ярлык ODK</string>\n  <string name=\"background_audio_help\">Эта форма записывает звук с микрофона вашего устройства.\\n\\nВы можете использовать индикатор громкости, чтобы убедиться, что микрофон находится достаточно близко к звуку, который вам нужно записать.\\n\\nЧтобы остановить запись, закройте эту форму.\\n\\ nДля получения дополнительной информации обратитесь к человеку, который попросил вас собрать данные.</string>\n  <string name=\"loading\">Загрузка…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Проекты</string>\n  <string name=\"settings\">Настройки</string>\n  <string name=\"add_project\">Добавить проект</string>\n  <string name=\"add\">Добавить</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Дублированный проект</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">У вас уже есть проект с такими настройками подключения. Вы хотите переключиться на существующий проект или добавить новый?</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Добавить дублированный проект</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Перейти к существующему проекту</string>\n  <string name=\"project_name\">Имя проекта</string>\n  <string name=\"project_icon\">Значок проекта</string>\n  <string name=\"project_color\">Цвет проекта</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Переключено на %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">Используется %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Переключить на %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"or\">ИЛИ</string>\n  <string name=\"hex_color\">HEX цвет</string>\n  <string name=\"invalid_hex_code\">Неверный HEX код</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Собирайте данные\\nгде угодно</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Настроить с помощью QR-кода</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Вручную введите данные проекта</string>\n  <string name=\"project_config_tip\">После того, как вы добавите свой проект, вы можете изменить его в настройках</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Еще нет проекта?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Попробуйте демо</string>\n  <string name=\"scan_configuration_qr_code\">Сканировать QR-код конфигурации</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Показать детали</string>\n  <string name=\"errors\">Ошибки</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">Чтобы открыть эту форму, необходимо сначала открыть ODK Collect и переключиться на проект, который ее содержит.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">ODK Collect не настроен. Попробуйте открыть ODK Collect и настроить его.\\n\\nЕсли вы нажали на ярлык, вам может потребоваться создать его заново после настройки ODK Collect.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Не удалось распознать URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Неправильный URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Заполненная форма удалена!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">Невозможно изменить этот черновик, так как соответствующая пустая форма отсутствует или была удалена.\\n\\nИдентификатор формы:%1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Вы загрузили два разных опросника с одинаковым ID и версией. Возможно, это один и тот же опросник загруженный в разное время или на разные сервера. В любом случае, вам следует удалить один из них.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <string name=\"cannot_open_form_because_of_forms_update\">Эта форма не может быть открыта прямо сейчас, потому что формы обновляются. Попробуйте еще раз позднее.</string>\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Открыть настройки</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">Оффлайн</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">Эта форма не поддерживается этой версией ODK Collect. Пожалуйста, обновите программу. Если у вас продолжает возникать эта проблема, сообщите о ней тому, кто попросил вас собрать данные.\\n\\nВерсия Entities: %s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">При последнем запуске приложения произошел сбой!</string>\n  <string name=\"cant_start_app\">Не удается запустить приложение!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"learn_more_button_text\">Узнать больше</string>\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">Инструменты разработчика</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">Сбой приложения</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Вызвать исключение, вызывающее сбой приложения</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"permission_dialog_title\">О доступе</string>\n  <string name=\"notifications\">Уведомления</string>\n  <string name=\"required_for_notifications\">Требуется для отображения обновлений при загрузке, обновлении и отправке форм.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">Ваша форма сохранена как черновик.</string>\n  <string name=\"form_saved\">Ваша форма сохранена.</string>\n  <string name=\"form_sending\">Отправка формы…</string>\n  <string name=\"edit_form\">Редактировать</string>\n  <string name=\"view_form\">Просмотр</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">В следующих версиях финализированные формы больше не будут доступны для редактирования. Сохраните формы как черновики, чтобы редактировать их позже.\\n\\nВы можете проверить наличие ошибок в черновике формы, нажав на три точки (⋮), а затем Проверить на ошибки.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">В следующих версиях завершенные формы больше нельзя будет редактировать.</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">Завершить все черновики</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">После того, как вы завершите все черновики, они будут в состоянии «Готово к отправке», и вы не сможете вносить изменения. Любые черновики, содержащие ошибки, не будут завершены.\\n\\nВы не сможете отменить это действие.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d черновиков завершено. Оставшиеся черновики необходимо завершить вручную.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Ошибки</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">Нет ошибок</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">Снимите галочку, чтобы скрыть из черновиков</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">Ничего недоступно для отображения</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">Нет пустых форм</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">Загрузите форму, чтобы начать</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">В черновиках ничего нет</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">Когда вы сохраните черновики, они появятся здесь</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">Нет готовых к отправке</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">Когда вы завершите черновики, они появятся здесь</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">Нет выгруженных</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">Когда вы выгрузите завершенные формы, они появятся здесь</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">Нет форм для загрузки</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">Загрузите форму, чтобы начать</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">Нет форм для удаления</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">Когда вы загрузите пустые формы, они появятся здесь</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">Если вы сохраните формы, они появятся здесь</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">Восстановить работу?</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">Восстановить</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Отменить</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">Найти координаты</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">Посмотреть или изменить точку</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">Посмотреть координаты</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">Изменить координаты</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">Найти линию</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">Посмотреть или изменить линию</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">Посмотреть линию</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">Найти полигон</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">Посмотреть или изменить полигон</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">Посмотреть полигон</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-rw/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">Version: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Added on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Saved on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Finalized on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Sent on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Sending failed on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Deleted on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <string name=\"submission_deleted\">Ibyo mwohereje byasibwe </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Byahinduwe kuri\\' EEE, MMM dd, yyyy \\'kuri\\' HH: mm</string>\n  <string name=\"encrypted_form\">Inyandiko shingiro cg ngenderwaho itagaragara cg ihishe.</string>\n  <string name=\"deleted_form\">Inyandiko shingiro cg ngenderwaho yasibwe</string>\n  <string name=\"sort_the_list\">Shyira urutonde ku murongo</string>\n  <string name=\"sort_by\">Shyira ku rutonde</string>\n  <string name=\"sort_by_name_asc\">Urutonde rwamazina, uva kuri A ujya kuri Z</string>\n  <string name=\"sort_by_name_desc\">Urutonde rwamazina, uva kuri Z ujya kuri A </string>\n  <string name=\"sort_by_date_desc\">Itariki yanyuma iheruka </string>\n  <string name=\"sort_by_date_asc\">Itariki, yabanjirije iyanyuma iheruka.</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">Hisha urutonde</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Shakisha</string>\n  <string name=\"not_granted_permission\">Igikorwa mugerageza gutangira kirasaba uruhushya, kandi ntirwatanzwe</string>\n  <string name=\"storage_runtime_permission_denied_title\">Uburenganzira ku bubiko</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Udafite ubwo burenganzira Collect ntabwo ishobora kwemera form cg ibike amakuru. Ongera ufungure iyo application mugihe ufite ubwo burenganzira</string>\n  <string name=\"location_runtime_permissions_denied_title\">Uburenganzira bwokwerekana location </string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">Ha uruhushya Camera</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Utatanze uruhushya porogaramu ntibasha gukoresha Camera bityo ongera kugerageza nyuma yuko uhaye uruhushya porogaramu</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Rikodinga uburenganzira bwa odiyo </string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Udafite uburenganzira Collect ntishobora kwemera konte. Mwakongera igihe mwaba mubonye ubwo burenganzira. </string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$d Form zashyizwemo</string>\n  <string name=\"finished_disk_scan\">Kugenzura byarangiye. Ibyo mwatanze byose byabitswe.</string>\n  <string name=\"xform_parse_error\">Ingorane z\\'ikosa kuri fomu %1$s : %2$s ni ibitabonetse cyangwa bituzuye</string>\n  <string name=\"loading_form\">Kuzuza form </string>\n  <string name=\"survey_loading_reading_form_message\">Porogaramu irasoma ubusobanuro bwa fomu</string>\n  <string name=\"survey_loading_reading_data_message\">Porogaramu irasoma amakuru y\\'igenzura.</string>\n  <string name=\"survey_loading_reading_csv_message\">Usome ububiko bwa CSV </string>\n  <string name=\"parse_error\">Ihangane, ntibikunze kugenzura amakuru ari mu ifishi</string>\n  <string name=\"loading_form_failed\">Habayeho ikibazo ubwo wuzuzaga form. Mwongere mugerageze. </string>\n  <string name=\"cannot_edit_completed_form\">Iyi form ntabwo ushobora kugira icyo uhindura mugihe byerekanyeko yarangiye. Ishobora kuba yerekana amakuru mu buryo butagaragara. </string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Ibyo utasevinze byabashije kwisevinga!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Hindura ururimi </string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Jya kubyo ugomba gukora</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Jya ahatangira </string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Jya ahahera </string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Jya hejuru </string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Itsinda </string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Itsinda ryisubiramo </string>\n  <string name=\"exit\">Sohokamo</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Ongera \\'1%s\\' </string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Shyiramo</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Shyiraho ibindi </string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Gukosora ibyo wanditse </string>\n  <string name=\"clearanswer_confirm\">Siba igisubizo \\'%s\\'</string>\n  <string name=\"clear_answer\">Siba ibyo wanditse</string>\n  <string name=\"clear_answer_ask\">Dukuremo ibyo wanditse? </string>\n  <string name=\"clear_answer_no\">Bikuremo</string>\n  <string name=\"discard_answer\">Siba ibyo wanditse</string>\n  <string name=\"discard_group\">Kuramo itsinda </string>\n  <string name=\"delete_repeat\">Kuramo itsinda </string>\n  <string name=\"delete_repeat_ask\">Dukuremo iri tsinda? </string>\n  <string name=\"delete_repeat_confirm\">Siba itsinda ryose ndetse n\\'udutsinda duto turibarizwamo \\'%s\\'</string>\n  <string name=\"delete_repeat_no\">Bikuremo</string>\n  <string name=\"invalid_answer_error\">Mwihangane, ibyo mwanditse sibyo! </string>\n  <string name=\"required_answer_error\">Ugomba gutanga igisubizo</string>\n  <string name=\"invalid_space_in_answer_singular\">Ikitonderwa: wasize umwanya mubyo wanditse 1%s</string>\n  <string name=\"invalid_space_in_answer_plural\">Ikitonderwa: wasize umwanya mubyo wanditse 1%s</string>\n  <string name=\"too_complex_form\">Iyi  fishi iremereye tabuleti yawe, gerageza kugira ibyo ugabanyamo.</string>\n  <string name=\"survey_internal_error\">Porogaramu yahuye n\\'ikibazo cyo kutagaragaza amakuru.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">ugeze ku iherezo rya 1%s</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Mwihangane, kubika amakuru byanze! </string>\n  <string name=\"data_saved_ok\">Amakuru yabitswe! </string>\n  <string name=\"save_point_error\">Ikosa mukwisevinga biragaruka: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Bika ibyo wahinduye</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Ibika ry\\'amakuru yinyandiko shingiro.</string>\n  <string name=\"survey_saving_validating_message\">Porogaramu iragenzura amakuru yatanzwe</string>\n  <string name=\"survey_saving_collecting_message\">Gukusanya amakuru</string>\n  <string name=\"survey_saving_saving_message\">Kubika kuri SD card </string>\n  <string name=\"survey_saving_finalizing_message\">Birikurangira bijya mububiko</string>\n  <string name=\"survey_saving_encrypting_message\">Guhisha amakuru</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Kuri ubu ntabwo ari icy\\'ibitsemo kimwe!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Nta fomu itujujwe ihura n\\'iyi jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Nta ndanga fomu yerekanwe??</string>\n  <string name=\"phone_does_not_support_rsa\">Tabuleti ntishoboye kurangiza iki gikorwa</string>\n  <string name=\"invalid_rsa_public_key\">Imibare yatanzwe siyo</string>\n  <string name=\"encryption_error_message\">Ifishi ntiyigeze yemezwa yuko yarangiye 1%s</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Kurikodinga byanze</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Mikoro isanzwe iri gukoreshwa</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">Aya makuru ntashoboye gushyirwa mu ifishi ariko ushobora kuyasanga 1%1$s </string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Gufata amajwi byahagaritse. biraza gukunda muri 1%s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Habaye ikosa mw\\'ivugurura ry\\'indangagaciro. Ibi ubusanzwe biterwa no kuba habayeimibarire idakwiye mu kubaka i fishi.\\n\\n\nN\\'ukomea guhura n\\'iki kibazo, wakigeza kuwa gusabye gukusanya  amakuru.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Bitangize</string>\n  <string name=\"activity_not_found\">Nta gikorwa kigaragaraga gishakirwa igisubizo 1%s</string>\n  <string name=\"no_app\">Ibyo mwasabye ntibiboneka. Nimubyandikemo bisanzwe</string>\n  <string name=\"null_intent_value\">Ntabwo habashije kuboneka amakuru mwifuzaga </string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Fungura Url</string>\n  <string name=\"capture_osm\">Tangiza OpenMapKit </string>\n  <string name=\"recapture_osm\">Subiramo ibyagaragajwe kuri OSM.</string>\n  <string name=\"edited_osm_file\">Hindura ububiko bwa OSM XML</string>\n  <string name=\"invalid_osm_data\">Mugihe hari ibitagenze neza. Ibyagaragajwe kuri OSM ntibihabwa agaciro.</string>\n  <string name=\"alert\">ba maso</string>\n  <string name=\"install_openmapkit\">Nimushyire OpenMapKit muri tablet yanyu!</string>\n  <string name=\"get_barcode\">Fata Barcode</string>\n  <string name=\"replace_barcode\">Hindura Barcode</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Shyira Barcode muri urwo rukiramende</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Fungura ubwo bubiko</string>\n  <string name=\"rank_items\">Shyira ku murongo/Tondeka</string>\n  <string name=\"number_picker_title\">Agafata mibare</string>\n  <string name=\"invalid_range_widget\">Ibyo wagaragaje muri ubu buryo ntagaciro byahawe! </string>\n  <string name=\"svg_file_does_not_exist\">Ububiko bwa SVG ntabwo buboneka! </string>\n  <string name=\"capture_image\">Fata ifoto </string>\n  <string name=\"choose_image\">Hitamo Ishusho</string>\n  <string name=\"selected_invalid_image\">Ifoto wahisemo mububiko si ifoto yukuri</string>\n  <string name=\"take_picture_instruction\">Kora kuri screen ufate ifoto </string>\n  <string name=\"error_front_camera_unavailable\">Ifoto y\\'imbere ntabwo iboneka kuri iki gikoresho</string>\n  <string name=\"annotate_image\">Andika ku ifoto </string>\n  <string name=\"invalid_file_type\">Saba kugarura ubwoko bw\\'ububiko butaribwo </string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Bika, hanyuma ufunge</string>\n  <string name=\"signature_capture\">Gufata sinya</string>\n  <string name=\"sign_button\">kusanya sinya</string>\n  <string name=\"markup_image\">Shyira akamenyetso ku ifoto </string>\n  <string name=\"draw_image\">Erekana ifoto </string>\n  <string name=\"reset_image\">Hindura</string>\n  <string name=\"set_color\">Hitamo ibara</string>\n  <string name=\"capture_video\">Fata Video </string>\n  <string name=\"choose_video\">Hitamo Video</string>\n  <string name=\"play_video\">Kina Video </string>\n  <string name=\"choose_file\">Hitamo dokima </string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Siba</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Fata ijwi </string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Hitamo Ijwi </string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Hagarika </string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Komeza</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"background_audio_permission_explanation\">Ubu buryo bubika amajwi kuri iyi gahunda. Ugomba gufungura insakazamajwi yawe, iyo utabikoze iyi gahunda ntifunguka.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Ubu buryo busaba kubika amajwi kuri iyi gahunda. Kutayikoresha bihagarika kubika amajwi kandi bigahanagura n\\'ayarariho. Urashaka gukomeza?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Hagarika kurikodinga </string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">Iyandikwa ntiriratangira ako kanya. Banza wongere ufungure aho kwandika.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Siba dokima </string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">Ntabwo ushobora kwongera kubona iyi dokima mugihe waba uyisibye. Wizeye nezako wifuzaga kuyisiba? </string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Rekekeraho/hagarika</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Kurikodinga </string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Kurikodinga audio </string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Gukina audio </string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Kubika dokima </string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Banza uhagarike gufata amajwi mbere yo kuva kuri ino paji</string>\n  <string name=\"error_attaching_binary_file\">Hagaragaye ikibazo mu gihe hageragezwaga guhuza amakuru 1%s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Hitamo</string>\n  <string name=\"select_answer\">Hitamo Igisubizo</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Hindura ibyo wanditse </string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Ntakintu wahisemo </string>\n  <string name=\"selected_title\">Ibyatoranijwe</string>\n  <string name=\"trigger\">Ok. Ngaho komeza</string>\n  <string name=\"ext_file_no_data_error\">Dokima nta makuru arimo</string>\n  <string name=\"ext_conflicting_columns_error\">Amakuru agaragara mu buryo buhagaze arahura neza 1%s</string>\n  <string name=\"ext_sortBy_numeric_error\">Akadirishya uhitamo gakoreshwamo imibare gusa. Iyo wibeshye aba ari \\'%s\\'</string>\n  <string name=\"ext_search_wrong_arguments_error\">Ikosa ryagaragaye mumikorere yishakisha(): Imikorere isaba ubusobanuro kuri 1, 4 cyangwa 6.</string>\n  <string name=\"ext_search_wrong_function_error\">Ikosa ryagaragaye mumikorere yishakisha(): Imikorere ntizwi \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Ikosa ryagaragaye mumikorere yishakisha(): \\'%s\\' Ntabwo yahawe agaciro nk\\'imikorere.</string>\n  <string name=\"ext_search_return_error\">Ishakisha riratanga uburyo bwa \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Kuzana amakuru ntibyakunze 1%1$s. Impamvu: 2%2$s</string>\n  <string name=\"ext_import_progress_message\">Amakuru ari gutunganywa \\'1%1$s\\', ba wihanganye gato \\'2%2$s\\'</string>\n  <string name=\"ext_import_cancelled_message\">Gusoma amakuru mwarimwakuyemo!</string>\n  <string name=\"ext_import_finalizing_message\">Gusoza amakuru mwarimwashyizemo </string>\n  <string name=\"ext_import_completed_message\">Gusoma amakuru byarangiye! </string>\n  <string name=\"ext_not_initialized_error\">Ibika ry\\'ikusanyamakuru ntabwo ryavuguruwe.</string>\n  <string name=\"ext_import_csv_missing_error\">Amakuru mwashakaga kuri 1%1$s ntabwo yabashije kuboneka. Birashoboka ko mwibagiwe gushyiramo 2%2$s.csv file muri form yawe? </string>\n  <string name=\"ext_search_generic_error\">Ikosa ryagaragaye mu rutonde  rwimikorere mwishakisha():%s</string>\n  <string name=\"ext_assign_value_error\">Ntibyakunze gushyiraho aya makuru \\\"%s\\\"</string>\n  <string name=\"parser_exception\">Uretse gusa ubusesenguzi bugaragazwa n\\'iyi nzira ya X: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Andika ibyo uhawe.</string>\n  <string name=\"replace_bearing\">Simbura ibyo uhawe.</string>\n  <string name=\"getting_bearing\">Kanda hazunguruke ibyanditswe.</string>\n  <string name=\"accept_bearing\">Andika ibyo uhawe.</string>\n  <string name=\"bearing\">Ingano cg intera ya:%.3f</string>\n  <string name=\"direction\">Icyerekezo: \\\"1%s\\\"</string>\n  <string name=\"bearing_lack_of_sensors\">Ingano ntabwo yakiriwe: haraburamo igipimo cy\\'ubwiyongere n\\'igipimo cya rukuruzi cyangwa byombi.</string>\n  <string name=\"select_date\">Hitamo itariki </string>\n  <string name=\"select_time\">Hitamo igihe </string>\n  <string name=\"no_date_selected\">Nta tariki yatoranijwe </string>\n  <string name=\"no_time_selected\">Nta gihe cyatoranijwe</string>\n  <string name=\"ethiopian_month_1\">Isozwa cg Impera ry\\'igihe cy\\'imvura.</string>\n  <string name=\"ethiopian_month_2\">Tikimt </string>\n  <string name=\"ethiopian_month_3\">Hidar </string>\n  <string name=\"ethiopian_month_4\">Tahsas </string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit </string>\n  <string name=\"ethiopian_month_7\">Megabit </string>\n  <string name=\"ethiopian_month_8\">Miazia </string>\n  <string name=\"ethiopian_month_9\">Ginbot </string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-awwal</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">\\\"1%1$s\\\" (\\\"2%2$s\\\")</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Ntibyemewe 1%s</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Irabura 1%s</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Andika amazina yawe</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Amazina</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Ningombwa kuzuza amazina yawe</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Funga/vamo</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Impamvu zatumye uhindura amakuru wari winjije</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Impamvu</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Ugomba kwandi impamvu uhinduye amakuru wari watanze </string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Bika amakuru wanditse</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_usgs_sat\">USGS National Map Hybrid</string>\n  <string name=\"openmap_usgs_img\">USGS National Map Imagery</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Position</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Indangamimerere igaragaza amakuru.</string>\n  <string name=\"basemap_source\">Aho byatangiriye</string>\n  <string name=\"map_style_label\">1%s imiterere ya ikarita</string>\n  <string name=\"streets\">Imihanda</string>\n  <string name=\"terrain\">Ubutaka</string>\n  <string name=\"hybrid\">Ibituburano</string>\n  <string name=\"satellite\">Icyogajuru.</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Itara</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Ubwijima</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">ikirere</string>\n  <string name=\"topographic\">Amakuru y\\'aho uhagaze</string>\n  <string name=\"google_play_services_error_occured\">Ntibishoka ko ukoresha ikarita iranga ahantu.Reba ko waba ufite google play muri telefone yawe</string>\n  <string name=\"carto_map_style_positron\">Utunyangingo.</string>\n  <string name=\"carto_map_style_dark_matter\">Ibibazo bidasobanutse cg byijimye.</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">Irabika amakuru</string>\n  <string name=\"provider_disabled_error\">Ihangane, ibituma amakuru y\\'ahantu afatwa birafunze</string>\n  <string name=\"google_play_services_not_available\">Iyi form irashaka kugenzura aho uherereye, ariko Google Play Services ntabwo iboneka</string>\n  <string name=\"location_providers_disabled_dialog_message\">Iyi nyandiko shingiro irashaka kugenzura aho uherereye, ariko banyirigikorwa baho ntabwo bemejwe. Usabwa kwemeza igenamiterere ya Android.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Jya mu igenamiterere</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"background_location_disabled\">Iyi form ishaka kwerekana aho uherereye ariko ntiwabifunguye. Yishyireho 1%1$s murutonde ruri hejuru.</string>\n  <string name=\"background_location_enabled\">Iyi form iri kwerekana aho uherereye.Ushobobora kubihagarika muri 1%1$s unyuze ku rutonde ruri hejuru aha</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">W %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Uburyo bwo kwinjiza amakuru</string>\n  <string name=\"start\">Tangira</string>\n  <string name=\"enable_gps\">Emeza GPS</string>\n  <string name=\"gps_enable_message\">GPS irafunze kuri tabuleti yawe. urifuza kuyifungura?</string>\n  <string name=\"geo_clear_warning\">Byamaze gutungana. Murifuza kubisiba? </string>\n  <string name=\"geo_exit_warning\">Siba ibyari byamaze guhindurwa ubundi usubire kuri porogaramu ya ODK</string>\n  <string name=\"polygon_validator\">Bigomba kuba bifite byibuze ibintu 3 kugirango bikore Polygon </string>\n  <string name=\"polyline_validator\">Bigomba kugira ibintu byibuze 2 kugirango bikore Polyline </string>\n  <string name=\"clear\">Siba </string>\n  <string name=\"placement_mode\">Ishyirwa mu bikorwa n\\'ihitamo.</string>\n  <string name=\"manual_mode\">Ubryo bw\\'umwihariko mu kubika neza ibyafashwe.</string>\n  <string name=\"automatic_mode\">Uburyo bwoguhita birikodinga aho uherereye</string>\n  <string name=\"recording_interval\">Rikodinga intera</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d second</item>\n    <item quantity=\"other\">%d seconds</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d minute</item>\n    <item quantity=\"other\">%d minutes</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Icyerekana ko aribyo</string>\n  <string name=\"none\">Ntacyo</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d meter</item>\n    <item quantity=\"other\">%d meters</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Ikimenyetso ndangahantu kinjiye:%d .</string>\n  <string name=\"collection_status_placement\">Ikimenyetso ndangahantu kinjiye: %d (kanda mumwanya w\\'ikimenyetso ndangahantu)</string>\n  <string name=\"collection_status_manual\">Ikimenyetso ndangahantu kinjiye: %d (mu buryo busanzwe)</string>\n  <string name=\"collection_status_auto_seconds\">Ikimenyetso ndangahantu kinjiye: %1$d (andika buri segonda ibi %2$d)</string>\n  <string name=\"collection_status_auto_minutes\">Ikimenyetso ndangahantu kinjiye: %1$d (andika buri munota ibi %2$d)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Ikimenyetso ndangahantu kinjiye: %1$d (andika buri segonda %2$d, kundengo ya %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Ikimenyetso ndangahantu kinjiye: %1$d (andika buri munota %2$d, kundengo ya %3$d m)</string>\n  <string name=\"geopoint_no_draggable_instruction\">Kanda hano wemeze.</string>\n  <string name=\"discard\">Kuraho cg Jugunya.</string>\n  <!--Name of an action button-->\n  <string name=\"view_data\">Reba Form Wabitse </string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Hagarika </string>\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Kohereza Form </string>\n  <string name=\"noselect_error\">Mwihangane, nta form mwigeze muhitamo! </string>\n  <string name=\"no_connection\">Nta internet ihari </string>\n  <string name=\"sending_items\">Porogaramu iri kohereza 1%1$s muri 2%2$s y\\'ifishi</string>\n  <string name=\"send_selected_data\">Ohereza ibyo wahisemo</string>\n  <string name=\"server_auth_credentials\">Izina cyangwa se ijambo banga sibyo 1%s</string>\n  <string name=\"server_requires_auth\">Server irasaba Authentication </string>\n  <string name=\"no_forms_uploaded\">Nta fishi yigeze yoherezwa.</string>\n  <string name=\"upload_results\">Ohereza ibyakozwe.</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Hindura uko bigaragara </string>\n  <string name=\"show_sent_and_unsent_forms\">Erekana Form zagiye na form zitagiye</string>\n  <string name=\"show_unsent_forms\">Erekana form zitagiye</string>\n  <string name=\"send_in_progress\">Ohereza ibyagaragajwe bitaremezwa, hita wongera ugerageze.</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Rifureshinga </string>\n  <string name=\"download\">Byatoranijwe</string>\n  <string name=\"downloading_data\">Kwihuza na Server </string>\n  <string name=\"fetching_file\">Akira %1$s.\\n\\n y\\'inyandiko shingiro %2$s ya %3$s inyandiko shingiro (zose).</string>\n  <string name=\"form_download_progress\">%1$s. Akira ububiko bw\\'amajwi: %2$s ya %3$s</string>\n  <string name=\"version\">Urutonde rw\\'ibibazo/form(ariko biteganywa kugenda z\\'ihabwa nimero bitewe n\\' impinduka ikozwe ho nyuma y\\'uko yahawe aba atanga amakuru)</string>\n  <string name=\"form_updates_available\">Ubu wabona ifishi igezweho</string>\n  <string name=\"newer_version_of_a_form_info\">Aya ni amakuru agezweho ku ifishi ufite</string>\n  <string name=\"form_update_error\">Gushyira ifishi ku gihe ntibyakunze</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">Kubika amakuru ari mu ifishi kuri tabuleti ntibyakunze</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Mu gihe ukomeje guhura niki kibazo, baza uwagusabye gutanga aya makuru</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect ntibasha kugera kuri seriveri kuri %s. Ese winjijemo linki neza</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect ntibasha guhura na seriveri kuri %s. Ese winjijemo linki neza</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">Seriveri %1$s irerekana kode %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">Server 1%s yatanze amakuru atariyo </string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Ifishi zibitse</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Ifishi zitarimo ikintu</string>\n  <string name=\"delete_confirm\">Siba %s ifishi</string>\n  <string name=\"delete_file\">Siba icyo utoranije</string>\n  <string name=\"delete_no\">Ntubisibe</string>\n  <string name=\"delete_yes\">Siba ifishi</string>\n  <string name=\"form_delete_message\">Ifishi wahisemo ziri gusibwa</string>\n  <string name=\"file_deleted_ok\">Ifishi %s zamaze gusibwa</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Settings za project</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Ububiko</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">Link, username, password</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Uko project igaragara</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Izina, ikirango, ibara</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Aho umuntu akorera </string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">Ikarita</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Gucunga neza inyandiko shingiro</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Ukoresha n\\'ikiranga igikoresho</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Ubunararibonye</string>\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Igenamiterere ya musakaza</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Server URL</string>\n  <string name=\"url_error\">Mwihangane URL ntabwo ariyo</string>\n  <string name=\"username\">Izina ry\\'uyikoresha.</string>\n  <string name=\"change_username\">Izina ry\\'ukoresha ODK</string>\n  <string name=\"password\">Umubarebanga cyangwa se Ijambobanga</string>\n  <string name=\"change_password\">Ijambo banga rya ODK</string>\n  <string name=\"password_error_whitespace\">Mu ijambobanga ahantu hariho ubusa ntibyemewe </string>\n  <string name=\"username_error_whitespace\">Kuzuza ahantu hariho ubusa ntabwo byemewe mu izina ugomba gukoresha </string>\n  <string name=\"app_theme\">Isanganyamatsiko</string>\n  <string name=\"language\">Ururimi</string>\n  <string name=\"use_device_language\">Kurikiza imikorere y\\'igikoresho</string>\n  <string name=\"font_size\">Ingano y\\'amagambo</string>\n  <string name=\"change_font_size\">Ingano y\\'amagambo</string>\n  <string name=\"font_size_extra_large\">Nini cyane</string>\n  <string name=\"font_size_large\">Nini</string>\n  <string name=\"font_size_medium\">Iri mu rugero</string>\n  <string name=\"font_size_small\">Ntoya</string>\n  <string name=\"font_size_extra_small\">Nto cyane</string>\n  <string name=\"navigation\">Kogoga</string>\n  <string name=\"swipe_navigation\">Koresha uburyo bwo kunyereza ujyana hirya</string>\n  <string name=\"buttons_navigation\">Koresha buto ijyana imbere/isubira inyuma</string>\n  <string name=\"swipe_buttons_navigation\">Koresha kunyereza na za buto</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Ibikurikira</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Subira inyuma </string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Amakuru kuri form</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Uburyo bwa Form iriho ubusa</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Imfashanyigisho</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Inyandiko shingiro yakuwe mububiko nyuma gusa.</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Ibihuye neza na server </string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Inshuro z\\'ivugurura ryikora</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Buri minota 15</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Buri saha</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Buri nyuma y\\'amasaha atandatu </string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Buri nyuma y\\'amasaha 24</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Kwidawunilodinga </string>\n  <string name=\"automatic_download_summary\">Guhita bidawunilodinga versions nshya ya forms </string>\n  <string name=\"hide_old_form_versions_setting_title\">Hisha amakuru ya form imaze igihe kinini </string>\n  <string name=\"hide_old_form_versions_setting_summary\">Hazagaragazwa gusa inyandiko shingiro yanyuma itujujwe.</string>\n  <string name=\"form_submission_category\">Kohereza Form </string>\n  <string name=\"autosend_selector_title\">Kwiyohereza</string>\n  <string name=\"autosend\">Kwiyohereza</string>\n  <string name=\"off\">Irafunze</string>\n  <string name=\"wifi_autosend\">Wifi yonyine</string>\n  <string name=\"cellular_autosend\">Interineti ya Tabuleti yonyine</string>\n  <string name=\"wifi_cellular_autosend\">Wifi ndetse na Interineti ya tabuleti</string>\n  <string name=\"delete_after_send\">Gusiba nyuma yokohereza </string>\n  <string name=\"delete_after_send_summary\">Nyuma yo kohereza form na media kukicaro Ushobora guhita ubisiba.</string>\n  <string name=\"form_filling_category\">Kuzuza Form </string>\n  <string name=\"constraint_behavior_title\">Gukemura uburyo bwintegeko.</string>\n  <string name=\"constraint_behavior\">Imyitwarire yo gukemura uburyo bwintegeko</string>\n  <string name=\"constraint_behavior_on_swipe\">Kwemeza uburyo bw\\'ikubura ryihuse.</string>\n  <string name=\"constraint_behavior_on_finalize\">Komeza ugaragaze iyemezwa kugeza usoje.</string>\n  <string name=\"high_resolution_title\">Amashusho nyayo murwego rwohejuru.</string>\n  <string name=\"high_resolution_summary\">Emeza ibikamashusho kuburyo buhanitse.</string>\n  <string name=\"image_size_title\">Ingano y\\'ifoto</string>\n  <string name=\"image_size_dialog_title\">Kumuzenguruko w\\'ishusho hagaragare udushusho duto.</string>\n  <string name=\"image_size_large\">Nini (3072px)</string>\n  <string name=\"image_size_medium\">Iri mu rugero (2048 px)</string>\n  <string name=\"image_size_small\">Ntoya (1024 px)</string>\n  <string name=\"image_size_very_small\">Ntoya cyane (640 px)</string>\n  <string name=\"image_size_original\">Ingano yumwimerere kuva kuri kamera (isanzwe)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Erekana ubuyobozi kubibazo</string>\n  <string name=\"guidance_no\">Oya</string>\n  <string name=\"guidance_yes\">Yego-burigihe bigarara</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Yego - yaguye</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">Kwifashisha APP y\\'indi mugufata amajwi</string>\n  <string name=\"form_import_category\">Gurura cg shaka inyandiko shingiro.</string>\n  <string name=\"instance_sync\">Rangiza kuzuza neza inyandiko shingiro yakuruwe cg yashatswe.</string>\n  <string name=\"instance_sync_summary\">Shyiraho akamenyetso bwangu kunyandiko shingiro kuri urwo rukurikirane mbumbe rw\\'amakuru.</string>\n  <string name=\"analytics_preferences\">Ingano ya interineti yakoreshejwe</string>\n  <string name=\"analytics\">Kusanya amakuru yo gukoresha atazwi</string>\n  <string name=\"analytics_summary\">Amakuru yo gukoresha atazwi afasha itsinda rya ODK gushyira imbere gukosora </string>\n  <string name=\"form_metadata_title\">Inyandiko shingiro mbumbe y\\'urukurikirane rw\\'amakuru yatanzwe.</string>\n  <string name=\"form_metadata\">Inyandiko shingiro mbumbe y\\'urukurikirane rw\\'amakuru yatanzwe.</string>\n  <string name=\"form_metadata_summary\">Ako gaciro kose kongerwa ku nyandiko shingiro gafite izina rikoresha, email na/ cg telefone yinjijwe kare igaragaza umuntu wohereje amakuru.</string>\n  <string name=\"user_defined\">Umukoresha-asobanuwe</string>\n  <string name=\"phone_number\">numero ya telefoni</string>\n  <string name=\"email\">umwirondoro wa Imeyili</string>\n  <string name=\"invalid_email_address\">Imeyili siyo</string>\n  <string name=\"device_defined\">Igikoresho cyasobanuwe</string>\n  <string name=\"device_id\">Ibiranga igikoresho </string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Ntabwo bihari </string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Shyiramo ijambobanga rishya </string>\n  <string name=\"admin_password\">Ijambo ry\\'ibanga ry\\'umuyobozi</string>\n  <string name=\"admin_password_changed\">Ijambo ryibanga rya admin ryahinduwe neza</string>\n  <string name=\"admin_password_disabled\">Ijambo ryibanga ryahagaritswe</string>\n  <string name=\"admin_password_incorrect\">Mwihangane, ijambo banga siryo! </string>\n  <string name=\"enter_admin_password\">Andika ijambo ryibanga rya Admin </string>\n  <string name=\"change_admin_password\">Hindura ijambo ryibanga  rya admin</string>\n  <string name=\"show_password\">garagaza ijambobanga</string>\n  <string name=\"found_in_main\">Kutemeza bihisha uhereye k\\'urutonde rw\\'ibanze cg fatizo.</string>\n  <string name=\"form_entry\">Ntuhitemo inyandiko shingiro ihishwe ahubwo uyinjize.</string>\n  <string name=\"reset_project_settings_title\">Hindura</string>\n  <string name=\"delete_project\">Siba</string>\n  <string name=\"select_all\">Hitamo byose</string>\n  <string name=\"clear_all\">Siba byose</string>\n  <string name=\"reset_settings\">Igenamiterere zose ( izimbere, izabitswe)</string>\n  <string name=\"reset_saved_forms\">Inyandiko shingiro zabitswe (Urukurikirane rw\\'imbumbe z\\'inyankiko z\\'amakuru, Urukurikirane rw\\'amakuru fatizo yibanze)</string>\n  <string name=\"reset_blank_forms\">Inyandiko shingiro zitujujwe (Imbumbe z\\'inyankiko shingiro, amakuru fatizo yinyandiko shingiro yibanze kumbumbe zinzweho)</string>\n  <string name=\"reset_layers\">Shushanya ikarita (Ukore ububiko bwayo)</string>\n  <string name=\"reset_cache\">Hisha aho wohereje inyandiko shingiro (Hisha ububiko bw\\'izo nyandiko)</string>\n  <string name=\"reset_settings_dialog_title\">Hitamo ibyo ushaka kongera gutangiza.</string>\n  <string name=\"reset_settings_button_reset\">Hindura</string>\n  <string name=\"reset_in_progress\">Kongera gukora igenamiterere.</string>\n  <string name=\"reset_app_state_result\">Ongera utangize ibyakonzwe.</string>\n  <string name=\"reset_app_state_warning\">Ibyo wahisemo byose biravamo burundu. Nturi bubashe gusubira inyuma</string>\n  <string name=\"reset_settings_result\">Igenamiterere zose::%s</string>\n  <string name=\"reset_saved_forms_result\">Ifishi zibitse 1%s</string>\n  <string name=\"reset_blank_forms_result\">Ifishi zitarimo ikintu 1%s</string>\n  <string name=\"reset_layers_result\">Shushanya ikarita:: %s</string>\n  <string name=\"reset_cache_result\">Hisha ipakirwa ry\\'inyandiko shingiro::%s</string>\n  <string name=\"main_menu_settings\">Igenamiterere ry\\'urutonde fatizo</string>\n  <string name=\"user_settings\">Ikoreshwa ry\\'igenamiterere.</string>\n  <string name=\"form_entry_setting\">Igenamiterere ku kwinjiza inyandiko shingiro.</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Gusubira inyuma</string>\n  <string name=\"moving_backwards_disabled_title\">Gusubira inyuma ntibikunda</string>\n  <string name=\"yes\">Yego</string>\n  <string name=\"no\">Oya</string>\n  <string name=\"moving_backwards_enabled_title\">Gusubira inyuma byemejwe</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Kuri</string>\n  <string name=\"tell_your_friends\">Gusangiza ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Abo mukorana baracyakusanyiriza amakuru mu mpapuro? Bahe ODK collect</string>\n  <string name=\"leave_a_review\">Tanga igitekerezo cyawe muri play store</string>\n  <string name=\"leave_a_review_msg\">Igitekerezo cyawe(twizera cyiza) kiratuma application irushaho kugaragara cyane muri Play Store</string>\n  <string name=\"odk_website\">Sura urubuga rwa ODK</string>\n  <string name=\"odk_website_summary\">ODK ikoreshwa mugukusanya amakuru kubwimibereho myiza mubidukikije bifite ibibazo.</string>\n  <string name=\"odk_forum\">Itabire ihuriro ry\\'abakoresha ODK</string>\n  <string name=\"odk_forum_summary\">Injira ihuriro kugirango ubone ubufasha, gusaba ibiranga, gutanga umusanzu / ibisobanuro!</string>\n  <string name=\"all_open_source_licenses\">Gufungura amasomero yisoko / impushya</string>\n  <string name=\"all_open_source_licenses_msg\">Duhagaze ku bitugu by\\'ibihangange!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"qr_code\">Kwemeza umubarendangabanga cg kode ya QR</string>\n  <string name=\"scan_qr_code_fragment_title\">Sikana</string>\n  <string name=\"view_qr_code_fragment_title\">QR Code</string>\n  <string name=\"import_qrcode_sd\">Kurura umubarendangabanga cg kode ya QR</string>\n  <string name=\"successfully_imported_settings\">Igenamiterere ryatumijwe neza</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Umubare wanyuma ntarengwa w’inyuguti wageze: Umubarendangabanga cg kode ya QR ntishobora kubyara igenamiterere ryose.</string>\n  <string name=\"share\">Gusangiza</string>\n  <string name=\"turn_on_flashlight\">Cana itara</string>\n  <string name=\"turn_off_flashlight\">Zimya itara</string>\n  <string name=\"qrcode_with_both_passwords\">Ibigizwe n\\'amakuru agaragara:  Jambobanga rya <b>muyoboyi</b> na<b>musakaza</b></string>\n  <string name=\"qrcode_without_passwords\">Kanda wemeze Umubarendangabanga cg kode ya QR iginzwe na jambobanga  rya<b>muyobozi</b> cg <b>musakaza</b></string>\n  <string name=\"qrcode_with_admin_password\">Ibigizwe n\\'amakuru agaragara ya: Jambobanga <b>muyobozi</b> </string>\n  <string name=\"qrcode_with_server_password\">Ibigizwe n\\'amakuru agaragara ya: Jambobanga <b>musakaza</b></string>\n  <string name=\"server_password\">Ijambo banga rya seriveri</string>\n  <string name=\"include_password_dialog\">Ijambo ryibanga ririmo kode</string>\n  <string name=\"generate\">Kubyaza </string>\n  <string name=\"invalid_qrcode\">Kode za QR ntabwo zirimo settings zuzuye</string>\n  <string name=\"qr_code_not_found\">QR code y\\'ishusho wakanzeho ntiyabonetse </string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">Emeza</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Bikuremo</string>\n  <string name=\"cancel_loading_form\">Bikuremo</string>\n  <string name=\"cancel_location\">Bikuremo</string>\n  <string name=\"do_not_change\">Bikuremo</string>\n  <string name=\"canceling\">Birikuvamo</string>\n  <string name=\"notification_channel_name\">Ubutumwa bukwibutsa</string>\n  <string name=\"success\">Byakunze</string>\n  <string name=\"error_occured\">Hari ikosa ribaye</string>\n  <string name=\"please_wait\">Rindira igihe gito</string>\n  <string name=\"please_wait_long\">Rindira. Bishobora gufata igihe gito.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Ntabwo bibasha gusibika \\' %s \\'. Siba iyi form n\\'intoki wongere uyi downloadinge.</string>\n  <string name=\"fs_create_media_folder_error\">Ntabubiko mbumbe bubasha gukorwa \\' %s \\'.</string>\n  <string name=\"fs_file_copy_error\">Wikora copy ya %1$s kuri %2$s .\nImpamvu: %3$s</string>\n  <string name=\"select_odk_shortcut\">Hitamo inzira z\\'ubusamo za ODK</string>\n  <string name=\"background_audio_help\">Iyi fomu yandika amajwi ya mikoro yigikoresho cyawe. \\ n\\n Ushobora gukoresha igipimo cyerekana amajwi kugirango umenye neza ko mikoro yegeranye bihagije nijwi ukeneye gufata amajwi. n\\n\\ Guhagarika gufata amajwi nyamuneka sohoka muri iyi fomu.n\\n\\ Kubindi bisobanuro nyamuneka hamagara umuntu wagusabye gukusanya amakuru.</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Project zitandukanye</string>\n  <string name=\"add_project\">Shyiramo project</string>\n  <string name=\"add\">Shyiramo</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <string name=\"project_name\">Izina rya project</string>\n  <string name=\"project_icon\">Akarango ka Project</string>\n  <string name=\"project_color\">Ibara rya Project</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Byihinduye kuri %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">Gukoresha %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Imukira kuri %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"delete_project_confirm_button_text\">Siba Project</string>\n  <string name=\"or\">Cyangwa </string>\n  <string name=\"hex_color\">Amabara ya Hex</string>\n  <string name=\"invalid_hex_code\">hex code ntiyemewe</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Kusanya amakuru/aho ariho hose</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Byihuze hifashishijwe kode ya QR</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Ukoresheje amaboko injiza amakuru kuri project</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Nta poroje ufitemo</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Gerageza kubyerekana</string>\n  <string name=\"scan_configuration_qr_code\">Umubarendangabanga wemejwe kuri QR</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Link uriho ntibashije kuboneka %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Link uriho siyo: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Inyandiko shingiro yujujwe yarasibwe.</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Wa downloadinze form 2 zitandukanye ariko zihuje amakuru.\nBirashoboka ko ari zimwe zashyizwemo mubihe bitandukanye cgw ziturutse ahatandukanye. Nimba ariko bimeze, siba imwe.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Kuraho cg Jugunya.</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-si/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <!--Title of the end of form screen-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">තත්පර %d</item>\n    <item quantity=\"other\">තත්පර %d</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">විනාඩි %d</item>\n    <item quantity=\"other\">විනාඩි %d</item>\n  </plurals>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">මීටර %d</item>\n    <item quantity=\"other\">මීටර %d</item>\n  </plurals>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Label for server type setting-->\n  <!--Title of dialog for configuring server URL-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">පැය 6 කට වරක්</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">දිනකට පැය24 කට වරක්</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"show_password\">මුර පදය පෙන්වන්න</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-sl/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Izpolni prazen obrazec</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Osnutki</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Pripravljeni za oddajo</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Oddani</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Prenesi obrazec</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Izbriši obrazec</string>\n  <string name=\"saved_forms\">Shranjeni obrazci</string>\n  <string name=\"version_number\">Verzija: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Dodan\\' EEE,MMM dd, yyyy \\'ob\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'Posodobljen\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Shranjen \\' EEE,MMM dd, yyyy \\'ob\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Zaključen\\' EEE,MMM dd, yyyy \\'ob\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Poslan \\' EEE,MMM dd, yyyy \\'ob\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Pošiljanje ni uspelo \\' EEE,MMM dd, yyyy \\'ob\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Odstranjen na\\' EEE,MMM dd, yyyy \\'ob\\' HH:mm</string>\n  <string name=\"submission_deleted\">Poslan obrazec izbrisan</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Spremenjen\\' EEE, MMM dd,  yyyy \\'ob\\' HH:mm</string>\n  <string name=\"encrypted_form\">Šifriran obrazec</string>\n  <string name=\"deleted_form\">Izbrisan obrazec</string>\n  <string name=\"sort_the_list\">Sortiraj seznam</string>\n  <string name=\"sort_by\">Sortiraj po</string>\n  <string name=\"sort_by_name_asc\">Ime, A-Z</string>\n  <string name=\"sort_by_name_desc\">Ime, Z-A</string>\n  <string name=\"sort_by_date_desc\">Datum, najprej najnovejši</string>\n  <string name=\"sort_by_date_asc\">Datum, najprej najstarejši</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Po datumu shranjevanja</string>\n  <string name=\"filter_the_list\">Filtiraj seznam</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Poišči</string>\n  <string name=\"not_granted_permission\">Željena aktivnost nima potrebnih dovoljenj za dostop.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Dovoljenja za shranjevanje</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Brez teh dovoljenj, Collect ne more dostopati do vaših obrazcev ali shraniti odgovorov. Aplikacijo znova odprite, ko ste jih pripravljeni odobriti.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Dovoljenja za dostop do lokacije</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">Brez dovoljenja za <b> lokacijo</b> Collect ne more zabeležiti vaše lokacije. Prosimo, poskusite znova, ko boste pripravljeni, da ga odobrite.</string>\n  <string name=\"camera_runtime_permission_denied_title\">Dovoljenje za fotoaparat</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Brez tega dovoljenja Collect ne more dostopati do fotoaparata. Prosimo, poskusite znova, ko ste pripravljeni odobriti.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Dovoljenje za snemanje zvoka</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Brez tega dovoljenja Collect ne more dostopati do mikrofona. Prosimo, poskusite znova, ko ste pripravljeni odobriti.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Odpri zemljevid z obrazci</string>\n  <string name=\"instance_scan_count\">%1$d obrazcev dodanih.</string>\n  <string name=\"finished_disk_scan\">Končano skeniranje. Vsi obrazci so naloženi.</string>\n  <string name=\"xform_parse_error\">Xform napaka obravnave za %1$s: %2$s manjka ali je napačen.</string>\n  <string name=\"loading_form\">Nalagam obrazec</string>\n  <string name=\"survey_loading_reading_form_message\">Berem definicijo obrazca …</string>\n  <string name=\"survey_loading_reading_data_message\">Berem podatke obrazcev…</string>\n  <string name=\"survey_loading_reading_csv_message\">Berem CSV datoteke…</string>\n  <string name=\"parse_error\">Napaka, ne morem opredeliti obrazca.</string>\n  <string name=\"loading_form_failed\">Pri nalaganju obrazca je prišlo do napake. Prosimo, poizkusite ponovno.</string>\n  <string name=\"cannot_edit_completed_form\">Obrazca ni moč popravljati ko je bil zaključen. Lahko pa ga šifrirate.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Neshranjene spremembe vrnjene na shranjeno točko.</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Zamenjaj jezik</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Pojdi na vprašanje</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Shrani kot osnutek</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Pojdi na začetek</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Pojdi na konec</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Pojdi gor</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Skupina</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Ponavljajoča skupina</string>\n  <string name=\"exit\">Izhod</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Dodaj \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Dodaj</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Dodaj ponovno</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Uredi</string>\n  <string name=\"clearanswer_confirm\">Odstranim odgovor za \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Odstrani odgovor</string>\n  <string name=\"clear_answer_ask\">Odstranim odgovor?</string>\n  <string name=\"clear_answer_no\">Prekini</string>\n  <string name=\"discard_answer\">Odstrani odgovor</string>\n  <string name=\"discard_group\">Odstrani skupino</string>\n  <string name=\"delete_repeat\">Odstrani skupino</string>\n  <string name=\"delete_repeat_ask\">Odstranim to skupino?</string>\n  <string name=\"delete_repeat_confirm\">Odstranim skupino %s in vse njene podskupine?</string>\n  <string name=\"delete_repeat_no\">Prekini</string>\n  <string name=\"invalid_answer_error\">Ta odgovor je napačen!</string>\n  <string name=\"required_answer_error\">Ta odgovor je obvezen!</string>\n  <string name=\"success_form_validation\">Brez najdenih napak v obrazcu!</string>\n  <string name=\"invalid_space_in_answer_singular\">Opozorilo: vrednost %s vsebuje presledke</string>\n  <string name=\"invalid_space_in_answer_plural\">Opozorila: vrednosti %s vsebujejo presledke</string>\n  <string name=\"too_complex_form\">Obrazec je preveč kompleksen za vašo napravo. Poskusite poenostaviti vprašanja ali vprašati za pomoč na forumu.</string>\n  <string name=\"survey_internal_error\">Interna napaka: prehod ni uspel.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Ste pri koncu %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">Če želite spreminjati shranjene odgovore, shranite kot osnutek dokler niste pripravljeni za oddajo.</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Shrani kot osnutek</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Zaključi</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Pošlji</string>\n  <string name=\"data_saved_error\">Shranjevanje obrazca ni uspelo!</string>\n  <string name=\"data_saved_ok\">Obrazec uspešno shranjen!</string>\n  <string name=\"save_point_error\">Napaka pri shranjevanju obnovitvene točke %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Zavrzi obrazec</string>\n  <string name=\"discard_changes\">Zavrzi spremembe</string>\n  <string name=\"keep_changes\">Shrani spremembe</string>\n  <string name=\"keep_editing\">Urejaj še naprej</string>\n  <string name=\"quit_form_title\">Shrani obrazec?</string>\n  <string name=\"save_explanation\">Obrazec lahko shranite kot osnutek in ga nato urejate iz osnutkov.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'Obrazec zadnjič shranjen \\' EEE, MMM dd, yyyy \\'at\\' HH:mm\\'. Shranite kot osnutek, če želite shraniti spremembe.\\'</string>\n  <string name=\"quit_form_continue_title\">Nadaljuj z urejanjem?</string>\n  <string name=\"discard_form_warning\">Če boste zavrgli obrazec, boste izgubili vse vnešene podatke do zdaj.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'Obrazec zadnjič shranjen\\' EEE, MMM dd, yyyy \\'at\\' HH:mm\\'. Če boste zavrgli spremembe, jih boste izgubili.\\'</string>\n  <string name=\"saving_form\">Shrani obrazec</string>\n  <string name=\"survey_saving_validating_message\">Vrednotim odgovore…</string>\n  <string name=\"survey_saving_collecting_message\">Zbiram podatke…</string>\n  <string name=\"survey_saving_saving_message\">Shranjujem na SD kartico…</string>\n  <string name=\"survey_saving_finalizing_message\">Zaključujem na SD kartico…</string>\n  <string name=\"survey_saving_encrypting_message\">Šifriram podatke…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Ni samo enega zapisa za to instanco!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Ni samo enega praznega obrazca s tem jr_form_id.</string>\n  <string name=\"no_form_id_specified\">ID obrazca ni bil specificiran???</string>\n  <string name=\"phone_does_not_support_rsa\">Telefon ne podpira RSA šifriranja.</string>\n  <string name=\"invalid_rsa_public_key\">Neveljaven RSA javni ključ.</string>\n  <string name=\"encryption_error_message\">Obrazec %s ni bil shranjen kot dokončan.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Začetek snemanja zvoka neuspešen.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Mikrofon že v uporabi.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">Medijske datoteke ni mogoče priložiti obrazcu, vendar je na voljo na %1$s.</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Snemanje zvoka onemogočeno. Omogoči v %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">Napaka pri posodabljanju vrednosti. To je običajno posledica napačne uporabe izračunov v oblikovanju obrazca.\\n\\nČe se ta težava še naprej pojavlja, jo sporočite osebi, ki vas je prosila, da zbirate podatke.</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Zaženi</string>\n  <string name=\"activity_not_found\">Ne najdem aktivnosti za obravnavo: %s</string>\n  <string name=\"no_app\">Zahtevane aplikacije ne najdem ali ni na voljo. Ročno vnesi vrednost.</string>\n  <string name=\"null_intent_value\">Zunanja aplikacija ni zagotovila pričakovanih informacij.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">Tisk</string>\n  <string name=\"open_url\">Odpri URL</string>\n  <string name=\"capture_osm\">Zaženi OpenMapKit</string>\n  <string name=\"recapture_osm\">Ponovi OSM označevanje</string>\n  <string name=\"edited_osm_file\">Urejena OSM XML datoteka:</string>\n  <string name=\"invalid_osm_data\">Nekaj je šlo narobe. Nismo prejeli veljavnih OSM podatkov.</string>\n  <string name=\"alert\">Opozorilo</string>\n  <string name=\"install_openmapkit\">Prosim, namestite OpenMapKit!</string>\n  <string name=\"get_barcode\">Pridobi črtno kodo</string>\n  <string name=\"replace_barcode\">Zamenjaj črtno kodo</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Postavite črtno kodo znotraj pravokotnika.</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Odpri datoteko</string>\n  <string name=\"rank_items\">Razvrsti </string>\n  <string name=\"number_picker_title\">Izbirnik številk</string>\n  <string name=\"invalid_range_widget\">Ta widget ima neveljavne parametre!</string>\n  <string name=\"svg_file_does_not_exist\">SVG datoteka ne obstaja!</string>\n  <string name=\"capture_image\">Fotografiraj</string>\n  <string name=\"choose_image\">Izberi sliko</string>\n  <string name=\"selected_invalid_image\">Izbrana datoteka ni veljavna slika.</string>\n  <string name=\"take_picture_instruction\">Klikni na ekran za fotografiranje</string>\n  <string name=\"error_front_camera_unavailable\">Prednja kamera ni na voljo v tej napravi</string>\n  <string name=\"camera_error\">Prišlo je do napake pri fotografiranju.</string>\n  <string name=\"camera_failed_to_initialize\">Kamera ni na voljo!</string>\n  <string name=\"annotate_image\">Označi na sliki</string>\n  <string name=\"gif_not_supported\">Gif datoteke niso podprte</string>\n  <string name=\"invalid_file_type\">Aplikacija je vrnila neveljavno vrsto datoteke</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Shrani in zapri</string>\n  <string name=\"signature_capture\">Zajem podpisa</string>\n  <string name=\"sign_button\">Podpiši</string>\n  <string name=\"markup_image\">Označi sliko</string>\n  <string name=\"draw_image\">Skiciraj sliko</string>\n  <string name=\"reset_image\">Resetiraj</string>\n  <string name=\"set_color\">Nastavi barvo</string>\n  <string name=\"capture_video\">Posnemi video</string>\n  <string name=\"choose_video\">Izberi video</string>\n  <string name=\"play_video\">Predvajaj video</string>\n  <string name=\"choose_file\">Izberi datoteko</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Izbriši</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Posnemi zvok</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Izberi zvočno datoteko</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Premor</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Nadaljuj</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">Snemanje zvoka: vklopljeno</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">Snemanje zvoka: izklopljeno</string>\n  <string name=\"background_audio_permission_explanation\">Ta obrazec snema zvok v ozadju. Za uporabo morate dovoliti uporabo mikrofona. Če tega ne storite, ne boste mogli odpreti tega obrazca.</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">Ta obrazec želi snemati zvok v ozadju. Onemogočanje le-tega bo ustavilo snemanje zvoka in zavrglo obstoječi posnetek. Ali želite nadaljevati?</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">Onemogoči snemanje.</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">Snemanje zvoka se ne bo začelo takoj. Obrazec morate ponovno odpreti, da pričnete s snemanjem.</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Izbriši datoteko?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">Ne boste mogli obnoviti te datoteke, ko jo izbrišete. Ste prepričani, da želite nadaljevati?</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">Ustavi</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">Snemanje..</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">Zvočni posnetek</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">Predvajaj posnetek</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Shranjevanje datoteke</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">Preden nadaljujte morate ustaviti snemanje zvoka.</string>\n  <string name=\"error_attaching_binary_file\">Napaka pri dodajanju medijske vsebine: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Izberi vrednost</string>\n  <string name=\"select_answer\">Izberi odgovor</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Uredi vrednost</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Ni izbrana nobena vrednost.</string>\n  <string name=\"selected_title\">Izbrano:</string>\n  <string name=\"trigger\">OK. Nadaljuj</string>\n  <string name=\"ext_file_no_data_error\">Datoteka ne vsebuje podatkov!</string>\n  <string name=\"ext_conflicting_columns_error\">Stolpci %s se ujemajo!</string>\n  <string name=\"ext_sortBy_numeric_error\">Stolpec za razvrstitev naj bi vseboval le številčne vrednosti. Težava je pri vrednosti  \\' %s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Sintaktična napaka v search() funkciji. Funkcija potrebuje 1,4 ali 6 argumentov.</string>\n  <string name=\"ext_search_wrong_function_error\">Sintaktična napaka v search() funkciji: neprepoznana funkcija \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Sintaktična napaka v search() funkciji. \\'%s\\' ne morem ovrednotiti kot funkcijo.</string>\n  <string name=\"ext_search_return_error\">Iskalnik je vrnil objekt tipa \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Ne morem uvoziti podatkov iz %1$s. Razlog: %2$s</string>\n  <string name=\"ext_import_progress_message\">Nalagam podatke iz \\'%1$s\\', prosimo počakajte… %2$s.</string>\n  <string name=\"ext_import_cancelled_message\">Branje podatkov prekinjeno!</string>\n  <string name=\"ext_import_finalizing_message\">Zaključujem prednaložene podatke …</string>\n  <string name=\"ext_import_completed_message\">Branje podatkov končano!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager ni bil inicializiran.</string>\n  <string name=\"ext_import_csv_missing_error\">Zunanji podatki za %1$s niso bili uvoženi. Morda si pozabil vključiti %2$s csv datoteko v obrazec?</string>\n  <string name=\"ext_search_generic_error\">Sintaktična napaka v search() funkciji: %s</string>\n  <string name=\"ext_assign_value_error\">Ne morem pripisati vrednosti k \\'%s\\'.</string>\n  <string name=\"parser_exception\">XPathParser Izjema: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Zabeleži azimut</string>\n  <string name=\"replace_bearing\">Zamenjaj azimut</string>\n  <string name=\"getting_bearing\">Nalagam azimut</string>\n  <string name=\"accept_bearing\">Zabeleži azimut</string>\n  <string name=\"bearing\">Azimut: %.3f</string>\n  <string name=\"direction\">Smer: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Napaka: azimuta ni mogoče zabeležiti, ker naprava nima pospeškomera, senzorja magnetnega polja ali obojega.</string>\n  <string name=\"select_date\">Izberite datum</string>\n  <string name=\"select_time\">Izberite čas</string>\n  <string name=\"no_date_selected\">Ni izbran noben datum</string>\n  <string name=\"no_time_selected\">Ni izbranega časa</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Datoteka %s je neveljavna.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Ne najdem datoteke %s.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">Vnesite identiteto</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Identiteta</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">Ta obrazec zahteva, da se identificirate.</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Zapri</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Razlog za spremembe</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Razlog</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">Morate pojasniti razlog za spremembe tega obrazca.</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Shrani</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">Izberite lokacijo</string>\n  <string name=\"choices\">Možnosti</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_usgs_sat\">USGS National Map Hybrid</string>\n  <string name=\"openmap_usgs_img\">USGS National Map Imagery</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Basemap</string>\n  <string name=\"basemap_source\">Vir</string>\n  <string name=\"basemap_source_unavailable\">Oprostite, %s temeljne karte niso na voljo na tem napravi. Prosimo, izberite drugo temeljno karto v Nastavitve &gt; Zemljevidi.</string>\n  <string name=\"map_style_label\">%s slog zemljevida</string>\n  <string name=\"streets\">Ulice</string>\n  <string name=\"terrain\">Teren</string>\n  <string name=\"hybrid\">Hibrid</string>\n  <string name=\"satellite\">Satelit</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Svetlost</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Temnost</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Na prostem</string>\n  <string name=\"topographic\">Topografsko</string>\n  <string name=\"google_play_services_error_occured\">Ne morem dostopati do Google Maps. Je nameščen Google Play Services?</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Dark Matter</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">Posnemi točko</string>\n  <string name=\"provider_disabled_error\">Ponudniki lociranja so onemogočeni!</string>\n  <string name=\"google_play_services_not_available\">Ta obrazec želi uporabiti vašo lokacijo, vendar storitve Google Play niso na voljo.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Ta obrazec želi uporabiti vašo lokacijo, vendar so ponudniki lokacije onemogočeni. Prosimo, omogočite jih v nastavitvah Androida.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Pojdite v nastavitve</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">Sledenje lokaciji: vklopljeno</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">Sledenje lokaciji: izklopljeno</string>\n  <string name=\"background_location_disabled\">Ta obrazec želi slediti vaši lokaciji, vendar je sledenje onemogočeno. Prosimo, omogočite ga v meniju %1$s zgoraj.</string>\n  <string name=\"background_location_enabled\">Ta obrazec sledi vaši lokaciji. Sledenje lahko onemogočite v meniju %1$s zgoraj.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">Preveri napake</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">Pretečen čas: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">Točka bo shranjena pri %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">Sateliti: %1$s</string>\n  <string name=\"waiting_for_location\">Pridobivam lokacijo. Prosim, počakajte.</string>\n  <string name=\"poor_accuracy\">Slaba natančnost. Prosim, počakajte.</string>\n  <string name=\"unacceptable_accuracy\">Nesprejemljiva natančnost. Prosim, počakajte.</string>\n  <string name=\"improving_accuracy\">Izboljšujem natančnost. Počakajte, prosim.</string>\n  <string name=\"accuracy_status_title\">Pridobivam lokacijo</string>\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">Z %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">V %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">J %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">S %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Vhodna metoda</string>\n  <string name=\"start\">Začni</string>\n  <string name=\"enable_gps\">Omogoči GPS</string>\n  <string name=\"gps_enable_message\">GPS v napravi je onemogočen. Želite omogočiti?</string>\n  <string name=\"geo_clear_warning\">Funkcija je že narejena. Želite izbrisati funkcijo?</string>\n  <string name=\"geo_exit_warning\">Želite zavreči spremembe in se vrniti v ODK?</string>\n  <string name=\"polygon_validator\">Potrebne so vsaj 3 točke za ustvarjenje poligona</string>\n  <string name=\"polyline_validator\">Potrebne so vsaj 2 točki za polilinijo</string>\n  <string name=\"clear\">Počisti</string>\n  <string name=\"placement_mode\">Določitev z dotikom</string>\n  <string name=\"manual_mode\">Ročno beleženje lokacije</string>\n  <string name=\"automatic_mode\">Samodejno beleženje lokacije</string>\n  <string name=\"recording_interval\">Interval beleženja:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d sekunda</item>\n    <item quantity=\"two\">%d sekundi</item>\n    <item quantity=\"few\">%d sekund</item>\n    <item quantity=\"other\">%d sekund</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d minuta</item>\n    <item quantity=\"two\">%d minuti</item>\n    <item quantity=\"few\">%d minut</item>\n    <item quantity=\"other\">%d minut</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Zahtevana natančnost:</string>\n  <string name=\"none\">Brez</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d meter</item>\n    <item quantity=\"two\">%d metra</item>\n    <item quantity=\"few\">%d metrov</item>\n    <item quantity=\"other\">%d metrov</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Vnesene točke: %d</string>\n  <string name=\"collection_status_placement\">Vnesene točke: %d (tapnite za postavitev točk)</string>\n  <string name=\"collection_status_manual\">Vnesene točke: %d (ročno beleženje)</string>\n  <string name=\"collection_status_auto_seconds\">Vnesene točke: %1$d (snemanje vsakih %2$d sekund)</string>\n  <string name=\"collection_status_auto_minutes\">Vnesene točke: %1$d (snemanje vsakih %2$d min)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Vnesene točke: %1$d (snemanje vsakih %2$d sekund, natančnost %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Vnesene točke: %1$d (snemanje vsakih %2$d min, natančnost %3$d m)</string>\n  <string name=\"geopoint_no_draggable_instruction\">Pritisni gumb za označevanje.</string>\n  <string name=\"discard\">Zavrzi</string>\n  <string name=\"select_item_count\">%s: %d (%d prikazano na zemljevidu)</string>\n  <string name=\"select_item\">Izberi</string>\n  <string name=\"new_item\">Nov element</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">Uredi osnutek</string>\n  <string name=\"view_data\">Ogled shranjenega obrazca</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">Sledenje lokaciji</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">Beležim lokacijo…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">Pokaži mojo lokacijo</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">Prikaži vse</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Premor</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">Odstrani zadnjo točko</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">Odstrani zadnjo točko</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Pošiljam obrazec</string>\n  <string name=\"noselect_error\">Niste izbrali obrazca!</string>\n  <string name=\"no_connection\">Mrežna povezava ni na voljo</string>\n  <string name=\"sending_items\">Pošiljam %1$s od %2$s obrazcev</string>\n  <string name=\"send_selected_data\">Pošlji izbrane</string>\n  <string name=\"server_auth_credentials\">Napačno uporabniško ime ali geslo za strežnik: %s</string>\n  <string name=\"server_requires_auth\">Strežnik zahteva prijavo</string>\n  <string name=\"no_forms_uploaded\">Napaka. Obrazec ni naložen.</string>\n  <string name=\"upload_results\">Rezultati nalaganja</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Spremeni pogled</string>\n  <string name=\"show_sent_and_unsent_forms\">Pokaži poslane in neposlane obrazce</string>\n  <string name=\"show_unsent_forms\">Pokaži neposlane obrazce</string>\n  <string name=\"send_in_progress\">Pošiljanja se izvaja v ozadju, prosim poskusite znova cez malo časa</string>\n  <string name=\"forms_upload_succeeded\">Obrazci so bili uspešno poslani.</string>\n  <string name=\"forms_upload_failed\">Obrazci niso bili poslani.</string>\n  <string name=\"all_uploads_succeeded\">Vsi prenosi so uspeli!</string>\n  <string name=\"some_uploads_failed\">%1$s od %2$s pošiljanj ni uspelo!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Obnovi</string>\n  <string name=\"download\">Prevzami izbrane</string>\n  <string name=\"downloading_data\">Povezujem se s strežnikom</string>\n  <string name=\"fetching_file\">Pridobivam %1$s.\\n\\nObrazec %2$s od %3$sobrazcev.</string>\n  <string name=\"form_download_progress\">%1$s. Pridobivam medijske datoteke: %2$s od %3$s</string>\n  <string name=\"version\">Verzija:</string>\n  <string name=\"form_updates_available\">Na voljo so posodobitve obrazca</string>\n  <string name=\"newer_version_of_a_form_info\">To je novejša verzija že prenešenega obrazca</string>\n  <string name=\"form_update_error\">Posodobitev obrazca ni uspela.</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">Seznam obrazcev ni bilo mogoče prenesti.</string>\n  <string name=\"forms_download_succeeded\">Obrazci so bili uspešno preneseni.</string>\n  <string name=\"forms_download_failed\">Obrazci niso bili preneseni.</string>\n  <string name=\"all_downloads_succeeded\">Vsi prenosi so uspeli!</string>\n  <string name=\"some_downloads_failed\">%1$s od %2$s prenosov ni uspelo!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Različica: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">Če se vam težave večkrat ponavljajo, jih sporočite osebi, ki vas je prosila, da zbirate podatke.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect ne more doseči strežnika na %s. Ste pravilno vnesli URL?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect se ne more varno povezati s strežnikom na %s. Ste pravilno vnesli URL?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">Strežnik %1$s je vrnil kodo stanja %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">Strežnik %s je posredoval neveljaven odgovor.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">Strežnik ni zagotovil hasha za ta obrazec.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">Pri prenosu tega obrazca je prišlo do napake na napravi.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">Ta obrazec ima neveljaven URL za oddajo.</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Shranjeni obrazci</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Prazni obrazci</string>\n  <string name=\"delete_confirm\">Izbrišem %sobrazec(obrazce)?</string>\n  <string name=\"delete_file\">Izbriši izbrano</string>\n  <string name=\"delete_no\">Ne briši</string>\n  <string name=\"delete_yes\">Izbriši obrazce</string>\n  <string name=\"form_delete_message\">Izbriši izbrane obrazce</string>\n  <string name=\"file_deleted_ok\">%s obrazcev uspešno zbrisanih!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">Nastavitve projekta</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Strežnik</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, uporabniško ime, geslo</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">Prikaz projektov</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">Ime, ikone, barve</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">Uporabniški vmesnik</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">Zemljevidi</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Temeljne karte, stili, plasti</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Upravljanje obrazcev</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">Samodejno posodabljanje, pošiljanje in brisanje</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Identiteta uporabnika in naprave</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">Uporabniško ime, telefonska številka, ID naprave</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">Eksperimentalno</string>\n  <string name=\"protected_settings\">Zaščiteno</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">Odkleni zaščitene nastavitve</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">Spremenite geslo, nastavite nadzor dostopa</string>\n  <string name=\"set_admin_password\">Nastavi skrbniško geslo</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Upravljanje projektov</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">Ponovno nastavi, ponastavi, izbriši</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">Nadzor dostopa</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">Omejitve uporabniškega vmesnika</string>\n  <string name=\"server_preferences\">Nastavitve strežnika</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Strežnik URL</string>\n  <string name=\"url_error\">Napaka, neveljaven URL!</string>\n  <string name=\"username\">Uporabniško ime</string>\n  <string name=\"change_username\">ODK uporabniško ime</string>\n  <string name=\"password\">Geslo</string>\n  <string name=\"change_password\">ODk geslo</string>\n  <string name=\"password_error_whitespace\">Presledki na začetku ali na koncu gesla niso dovoljeni</string>\n  <string name=\"username_error_whitespace\">Presledki na začetku ali na koncu uporabniškega imena niso dovoljeni</string>\n  <string name=\"app_theme\">Tema</string>\n  <string name=\"language\">Jezik</string>\n  <string name=\"use_device_language\">Uporabi jezik naprave</string>\n  <string name=\"font_size\">Velikost črk</string>\n  <string name=\"change_font_size\">Velikost črk</string>\n  <string name=\"font_size_extra_large\">Extra veliko</string>\n  <string name=\"font_size_large\">Veliko</string>\n  <string name=\"font_size_medium\">Srednje</string>\n  <string name=\"font_size_small\">Malo</string>\n  <string name=\"font_size_extra_small\">Extra malo</string>\n  <string name=\"navigation\">Navigacija</string>\n  <string name=\"swipe_navigation\">Uporabi horizontalne potege</string>\n  <string name=\"buttons_navigation\">Uporabi naprej/nazaj tipke</string>\n  <string name=\"swipe_buttons_navigation\">Uporabi potege in tipke</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Naprej</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Nazaj</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Posodobitev obrazcev</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">Posodobitve praznih obrazcev</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">Ročno</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">Samo obstoječi obrazci</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">Strogo usklajevanje obrazcev s stanjem na strežniku</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">Frekvenca samodejnih posodobitev</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Vsakih petnajst minut</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Vsako uro</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Vsakih šest ur</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Vsakih 24 ur</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Samodejni prenos</string>\n  <string name=\"automatic_download_summary\">Samodejno prenesi posodobljene različice obrazcev</string>\n  <string name=\"hide_old_form_versions_setting_title\">Skrij stare različice obrazca</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Prikazana bo samo najnovejša različica v seznamu praznih obrazcev.</string>\n  <string name=\"form_submission_category\">Predložitev obrazca</string>\n  <string name=\"autosend_selector_title\">Samodejno pošlji</string>\n  <string name=\"autosend\">Samodejno pošlji</string>\n  <string name=\"off\">Izključeno</string>\n  <string name=\"wifi_autosend\">Samo WiFi</string>\n  <string name=\"cellular_autosend\">Samo mobilno omrežje</string>\n  <string name=\"wifi_cellular_autosend\">WiFi ali mobilno omrežje</string>\n  <string name=\"delete_after_send\">Izbriši po pošiljanju</string>\n  <string name=\"delete_after_send_summary\">Izbriše zaključene obrazce in medijske datoteke iz naprave po pošiljanju na strežnik.</string>\n  <string name=\"form_filling_category\">Izpolnjevanje obrazca</string>\n  <string name=\"constraint_behavior_title\">Omejitve procesiranja</string>\n  <string name=\"constraint_behavior\">Način obravnave omejitev</string>\n  <string name=\"constraint_behavior_on_swipe\">Ovrednoti ob potegu naprej</string>\n  <string name=\"constraint_behavior_on_finalize\">Zadrži vrednotenje do zaključka</string>\n  <string name=\"high_resolution_title\">Visoko-resolucijski video</string>\n  <string name=\"high_resolution_summary\">Omogoči video zapis visoke resolucije</string>\n  <string name=\"image_size_title\">Velikost slike</string>\n  <string name=\"image_size_dialog_title\">Največje število pikslov na daljši stranici slike</string>\n  <string name=\"image_size_large\">Velika (3072px)</string>\n  <string name=\"image_size_medium\">Srednja (2048px)</string>\n  <string name=\"image_size_small\">Majhna (1024px)</string>\n  <string name=\"image_size_very_small\">Zelo majhna (640px)</string>\n  <string name=\"image_size_original\">Izvirna velikost iz kamere (privzeto)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Pokaži navodila za vprašanja</string>\n  <string name=\"guidance_no\">Ne</string>\n  <string name=\"guidance_yes\">Da - vedno prikazano</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Da - skrito</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">Uporabite zunanjo aplikacijo za snemanje zvoka</string>\n  <string name=\"form_import_category\">Uvoz obrazcev</string>\n  <string name=\"instance_sync\">Zaključi obrazec ob uvozu</string>\n  <string name=\"instance_sync_summary\">Vpliva na obrazce, dodane neposredno v mapo.</string>\n  <string name=\"analytics_preferences\">Podatki o uporabi</string>\n  <string name=\"analytics\">Zberi anonimne podatke o uporabi</string>\n  <string name=\"analytics_summary\">Anonimni podatki o uporabi pomagajo ODK ekipi določiti prednost popravkov in funkcij.</string>\n  <string name=\"form_metadata_title\">Metapodatki obrazca</string>\n  <string name=\"form_metadata\">Metapodatki obrazca</string>\n  <string name=\"form_metadata_summary\">Vrednosti bodo dodane obrazcem, ki imajo uporabnisko ime, email ali telefonsko stevilko …</string>\n  <string name=\"user_defined\">Določeno s strani uporabnika</string>\n  <string name=\"phone_number\">Telefonska številka</string>\n  <string name=\"email\">E-poštni naslov</string>\n  <string name=\"invalid_email_address\">Neveljaven e-poštni naslov!</string>\n  <string name=\"device_defined\">Določeno z napravo</string>\n  <string name=\"device_id\">ID naprave</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Ni na voljo</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">Odkleni nastavitve</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">Nastavitve odklenjene</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Vnesi novo geslo</string>\n  <string name=\"admin_password\">Admin geslo</string>\n  <string name=\"admin_password_changed\">Admin geslo uspešno spremenjeno</string>\n  <string name=\"admin_password_disabled\">Admin geslo onemogočeno</string>\n  <string name=\"admin_password_incorrect\">Geslo je napačno</string>\n  <string name=\"enter_admin_password\">Vnesi Admin geslo</string>\n  <string name=\"change_admin_password\">Spremeni Admin geslo</string>\n  <string name=\"show_password\">Pokaži geslo</string>\n  <string name=\"found_in_main\">Ne izberi da ne prikaže v Glavnem meniju</string>\n  <string name=\"found_in_settings\">Ne odkljukaj, da ne prikažeš pri Splošnih nastavitvah</string>\n  <string name=\"form_entry\">Ne odkljukaj, da ne prikažeš pri izpopolnjevanju obrazca</string>\n  <string name=\"form_end\">Ne odkljukaj, da ne prikažeš pri zaključevanju obrazca</string>\n  <string name=\"reset_project_settings_title\">Resetiraj</string>\n  <string name=\"reset_project_settings_subtext\">Izberite med nastavitvami, obrazci, podatki.</string>\n  <string name=\"delete_project\">Izbriši</string>\n  <string name=\"select_all\">Izberi vse</string>\n  <string name=\"clear_all\">Počisti vse.</string>\n  <string name=\"reset_settings\">Vse nastavitve (notranje in določene s strani uporabnika)</string>\n  <string name=\"reset_saved_forms\">Shranjeni obrazci</string>\n  <string name=\"reset_blank_forms\">Prazni obrazci (mapa obrazcev, baza podatkov, baza vprašanj)</string>\n  <string name=\"reset_layers\">Sloji zemljevida (mapa slojev)</string>\n  <string name=\"reset_cache\">Predpomnilnik obrazcev (mapa .predpomnilnika)</string>\n  <string name=\"reset_settings_dialog_title\">Izberite, kaj želite ponastaviti.</string>\n  <string name=\"reset_settings_button_reset\">Resetiraj</string>\n  <string name=\"reset_in_progress\">Ponastavljanje …</string>\n  <string name=\"reset_app_state_result\">Ponastavi rezultate</string>\n  <string name=\"reset_app_state_warning\">Vsi izbrani podatki bodo trajno izbrisani. Razveljavitev ni mogoča.</string>\n  <string name=\"reset_settings_result\">Vse nastavitve :: %s</string>\n  <string name=\"reset_saved_forms_result\">Shranjeni obrazci :: %s</string>\n  <string name=\"reset_blank_forms_result\">Prazni obrazci :: %s</string>\n  <string name=\"reset_layers_result\">Sloji zemljevida :: %s</string>\n  <string name=\"reset_cache_result\">Predpomnilnik obrazcev :: %s</string>\n  <string name=\"main_menu_settings\">Glavni meni nastavitev</string>\n  <string name=\"main_menu_settings_summary\">Pokaži ali skrij gumbe</string>\n  <string name=\"user_settings\">Uporabniške nastavitve</string>\n  <string name=\"user_setting_summary\">Pokaži ali skrij nastavitve</string>\n  <string name=\"form_entry_setting\">Nastavitve za vnos obrazca</string>\n  <string name=\"form_entry_settings_summary\">Pokaži ali skrij dejanja</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Gibanje nazaj</string>\n  <string name=\"moving_backwards_disabled_title\">Gibanje nazaj onemogočeno</string>\n  <string name=\"moving_backwards_disabled_message\">Želite preprečiti uporabnikom, da bi zaobšli to nastavitev?\\n\\nSpremembe so: \\n\\u2022\\u2022 Onemogoči urejanje osnutka \\n\\u2022 Onemogoči shranjevanje kot osnutek \\n\\u2022 Onemogoči prehod na poziv \\n\\u2022 Ovrednosti vprašanja ob premiku naprej</string>\n  <string name=\"yes\">Da</string>\n  <string name=\"no\">Ne</string>\n  <string name=\"moving_backwards_enabled_title\">Gibanje nazaj omogočeno</string>\n  <string name=\"moving_backwards_enabled_message\">Morda želite pregledati naslednje nastavitve:\\n\\n\\u2022 Uredi osnutek\\n\\u2022 Shrani kot osnutek\\n\\u2022 Pojdi na poziv\\n\\u2022 Vrednotenje vprašanj</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Shrani kot osnutek</string>\n  <string name=\"save_mid_summary\">Shrani ikona v zgornji vrstici in gumb \\\"Shrani kot osnutek\\\" ob izhodu iz obrazca</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">O nas</string>\n  <string name=\"tell_your_friends\">Delite ODK Collect.</string>\n  <string name=\"tell_your_friends_msg\">Ali vaši kolegi še vedno zbirajo podatke na papirju? Delite ODK Collect z njimi.</string>\n  <string name=\"leave_a_review\">Oceni aplikacijo v Play Store.</string>\n  <string name=\"leave_a_review_msg\">Vaša (upajmo pozitivna) ocena poveča vidnost aplikacije v Play Store.</string>\n  <string name=\"odk_website\">Obiščite spletno stran ODK.</string>\n  <string name=\"odk_website_summary\">ODK se uporablja za zbiranje podatkov za družbeno dobro v zahtevnih okoljih.</string>\n  <string name=\"odk_forum\">Pridružite se ODK forumu</string>\n  <string name=\"odk_forum_summary\">Pridružite se forumu, da dobite podporo, izrazite podporo novim funkcijam in sodelujete pri programiranju ali prevodih!</string>\n  <string name=\"all_open_source_licenses\">Odprtokodne knjižnice/licence</string>\n  <string name=\"all_open_source_licenses_msg\">Stojimo na ramenih velikanov!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">Ponovno konfigurirajte s QR kodo</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">Zamenjajte vse obstoječe nastavitve.</string>\n  <string name=\"qr_code\">QR koda konfiguracija</string>\n  <string name=\"scan_qr_code_fragment_title\">Skeniraj</string>\n  <string name=\"view_qr_code_fragment_title\">QR koda</string>\n  <string name=\"import_qrcode_sd\">Uvozi QR kodo</string>\n  <string name=\"successfully_imported_settings\">Nastavitve uspešno uvožene</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Doseženo največje število znakov: QR koda ne more biti ustvarjena za vse nastavitve</string>\n  <string name=\"share\">Deli</string>\n  <string name=\"turn_on_flashlight\">Vklopi svetilko</string>\n  <string name=\"turn_off_flashlight\">Izklopi svetilko</string>\n  <string name=\"qrcode_with_both_passwords\">Vsebuje občutljive informacije: <b>admin</b> in <b>gesla za strežnik</b></string>\n  <string name=\"qrcode_without_passwords\">QR koda ne vsebuje gesel za <b>admin</b> ali <b>server</b>. Tapnite za konfiguracijo.</string>\n  <string name=\"qrcode_with_admin_password\">Vsebuje občutljive informacije: <b>admin</b> geslo</string>\n  <string name=\"qrcode_with_server_password\">Vsebuje občutljive informacije: <b>server</b> geslo</string>\n  <string name=\"server_password\">Geslo strežnika</string>\n  <string name=\"include_password_dialog\">Gesla vključena v kodo</string>\n  <string name=\"generate\">Generiraj</string>\n  <string name=\"invalid_qrcode\">QR koda ne vsebuje veljavnih nastavitev</string>\n  <string name=\"qr_code_not_found\">QR koda ni bila najdena na izbrani sliki.</string>\n  <string name=\"corrupt_imported_preferences_error\">Trenutne nastavitve so poškodovane. Iz nastavitev upravljanja projekta ponastavite nastavitve ali uvozite delujoče.</string>\n  <string name=\"settings_with_gd_protocol\">Google Drive/Sheets projekti niso več na voljo</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Prekini</string>\n  <string name=\"cancel_loading_form\">Prekini</string>\n  <string name=\"cancel_location\">Prekini</string>\n  <string name=\"do_not_change\">Prekini</string>\n  <string name=\"canceling\">Prekinjam</string>\n  <string name=\"notification_channel_name\">Splošna obvestila</string>\n  <string name=\"success\">Uspešno</string>\n  <string name=\"error_occured\">Nastala je napaka</string>\n  <string name=\"please_wait\">Malo počakaj, prosim.</string>\n  <string name=\"please_wait_long\">Počakaj. To lahko traja nekaj minut.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Ne morem izbrisati\\'%s\\'. Izbriši datoteko ročno in ponovno naloži obrazec.</string>\n  <string name=\"fs_create_media_folder_error\">Ne morem ustvariti medijsko mapo \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Ne morem kopirat \\'%1$s\\' preko \\'%2$s\\'. Razlog: %3$s.</string>\n  <string name=\"select_odk_shortcut\">Izberite bližnjico za dostop do ODK</string>\n  <string name=\"background_audio_help\">Ta obrazec snema zvok preko mikrofona vaše naprave.\\n\\nZa zagotovitev, da je mikrofon dovolj blizu zvoku, ki ga želite posneti, lahko uporabite kazalnik glasnosti.\\n\\nZa ustavitev snemanja zapustite ta obrazec.\\n\\nZa več informacij se obrnite na osebo, ki vas je prosila za zbiranje podatkov.</string>\n  <string name=\"loading\">Nalaganje …</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Projekti</string>\n  <string name=\"settings\">Nastavitve</string>\n  <string name=\"add_project\">Dodaj projekt</string>\n  <string name=\"add\">Dodaj</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Podvoji projekt</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">Projekt s temi nastavitvami povezave že obstaja? Ali želite preklopiti na obstoječi projekt ali dodati novega?</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Dodaj podvojen projekt</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Preklopi na obstoječi projekt</string>\n  <string name=\"project_name\">Ime projekta</string>\n  <string name=\"project_icon\">Ikona projekta</string>\n  <string name=\"project_color\">Barva projekta</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">Preklopil na %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">Uporablja se %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">Preklopi na %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"or\">ALI</string>\n  <string name=\"hex_color\">Hex barva</string>\n  <string name=\"invalid_hex_code\">Neveljaven hex koda</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">Zbiraj podatke\\nkjer koli</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">Nastavi z QR kodo</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Ročno vnesi projekt</string>\n  <string name=\"project_config_tip\">Po dodajanju projekta ga lahko konfigurirate v Nastavitvah.</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Še nimate projekta?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Poskusite demo.</string>\n  <string name=\"scan_configuration_qr_code\">Skenirajte konfiguracijsko QR kodo</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Pokaži podrobnosti</string>\n  <string name=\"errors\">Napake</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">Za odprtje tega obrazca morate najprej odpreti ODK Collect in preklopiti na projekt, ki ga vsebuje.</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">ODK Collect ni bil pravilno konfiguriran. Poskusite odpreti ODK Collect in ga konfigurirati.\\n\\nČe ste tapnili na bližnjico, jo boste morda morali ponovno ustvariti po konfiguriranju ODK Collect.</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Neprepoznan URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Nedelujoč URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Izpolnjen obrazec je bil izbrisan!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">Osnutek ni na voljo za urejanje, saj ni prisotnega osnovnega praznega obrazca ali pa je bil izbrisan.\\n\\nForm ID: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Naložili ste dva različna obrazca z istim ID obrazca in verzijo. Izbrišite enega od obeh.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Odpri nastavitve</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">Ob zadnjem delovanju aplikacije je prišlo do kritične napake in zaustavitve aplikacije!</string>\n  <string name=\"cant_start_app\">Ne morem zagnati aplikacije!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">Ta projekt je bil v preteklosti povezan z računom Google Drive. Podpora za Google Drive je bila odstranjena. Lahko ponastavite strežnik ali prenesete izpolnjene obrazce na računalnik.</string>\n  <string name=\"learn_more_button_text\">Preberi več</string>\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">Razvijalska orodja</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">Prisilno zaustavi aplikacijo</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Neujeta napaka zaustavila aplikacijo</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"permission_dialog_title\">O dovoljenjih</string>\n  <string name=\"permission_dialog_text\">Če želite dovoliti dostop ODK Collect do spodaj navedenih funkcij, izberite \\\"dovoli\\\", če jih želite uporabljati.</string>\n  <string name=\"notifications\">Obvestila</string>\n  <string name=\"required_for_notifications\">Potrebno za prikaz posodobitev, ko se obrazci prenesejo, posodobijo in pošljejo.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">Vaš obrazec je bil shranjen kot osnutek.</string>\n  <string name=\"form_saved\">Vaš obrazec je bil shranjen.</string>\n  <string name=\"form_sending\">Pošiljam obrazec…</string>\n  <string name=\"edit_form\">Uredi</string>\n  <string name=\"view_form\">Poglej</string>\n  <string name=\"close_snackbar\">Zapri snackbar</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">Zadnji obrazec poslan: %d sekundo nazaj</item>\n    <item quantity=\"two\">Zadnji obrazec poslan: %d sekundi nazaj</item>\n    <item quantity=\"few\">Zadnji obrazec poslan: %d sekund nazaj</item>\n    <item quantity=\"other\">Zadnji obrazec poslan: %d sekund nazaj</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"one\">Zadnji obrazec poslan: %d minuto nazaj</item>\n    <item quantity=\"two\">Zadnji obrazec poslan: %d minuti nazaj</item>\n    <item quantity=\"few\">Zadnji obrazec poslan: %d minut nazaj</item>\n    <item quantity=\"other\">Zadnji obrazec poslan: %d minut nazaj</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"one\">Zadnji obrazec poslan: %d uro nazaj</item>\n    <item quantity=\"two\">Zadnji obrazec poslan: %d uri nazaj</item>\n    <item quantity=\"few\">Zadnji obrazec poslan: %d ur nazaj</item>\n    <item quantity=\"other\">Zadnji obrazec poslan: %d ur nazaj</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"one\">Zadnji obrazec poslan: %d dan nazaj</item>\n    <item quantity=\"two\">Zadnji obrazec poslan: %d dni nazaj</item>\n    <item quantity=\"few\">Zadnji obrazec poslan: %d dni nazaj</item>\n    <item quantity=\"other\">Zadnji obrazec poslan: %d dni nazaj</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"one\">%d obrazec pripravljen za pošiljanje</item>\n    <item quantity=\"two\">%d obrazca pripravljena za pošiljanje</item>\n    <item quantity=\"few\">%d obrazcev pripravljenih za pošiljanje</item>\n    <item quantity=\"other\">%d obrazcev pripravljenih za pošiljanje</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">V kasnejših različicah zaključenih obrazcev ne bo več mogoče urejati. Obrazce shranite kot osnutke, če jih želite kasneje še spreminjati. \\n\\nLahko ovrednotiti morebitne napake pri izpolnjevanju obrazca tako, da tapnete tri pike (⋮) in nato Preveri napake.</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">V kasnejših različicah zaključenih obrazcev ne bo več mogoče urejati.</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">Zaključite vse osnutke</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"one\">Ali želite dokončati %d osnutek?</item>\n    <item quantity=\"two\">Želite dokončati %d osnutka?</item>\n    <item quantity=\"few\">Želite dokončati %d osnutkov?</item>\n    <item quantity=\"other\">Želite dokončati %d osnutkov?</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">Ko zaključite osnutke, bodo v stanju \\\"Pripravljeni za pošiljanje\\\" in jih ne boste mogli več urejati. Vsi osnutki z napakami ne bodo zaključeni.\\n\\nTega dejanja ne boste mogli razveljaviti.</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"one\">Uspeh! %d osnutek je dokončan.</item>\n    <item quantity=\"two\">Uspeh! %d osnutka sta dokončana.</item>\n    <item quantity=\"few\">Uspeh! %d osnutkov je dokončanih.</item>\n    <item quantity=\"other\">Uspeh! %d osnutkov je dokončanih.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"one\">%d osnutek ima napake, ki jih je treba odpraviti pred zaključitvijo.</item>\n    <item quantity=\"two\">%d osnutka imata napake, ki jih je treba odpraviti pred zaključitvijo.</item>\n    <item quantity=\"few\">%d osnutkov ima napake, ki jih je treba odpraviti pred zaključitvijo.</item>\n    <item quantity=\"other\">%d osnutkov ima napake, ki jih je treba odpraviti pred zaključitvijo.</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d osnutkov je dokončanih. %d osnutkov ima napake, ki jih je treba odpraviti pred zaključitivijo.</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d osnutkov zaključenih. Preostale osnutke bo potrebno zaključiti ročno.</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Napake</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">Brez napak</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">Odkljukajte, da ne prikazujete v osnutkih</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">Ničesar ni na voljo za prikaz</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">Brez praznih obrazcev</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">Prenesite obrazec, da začnete.</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">Ničesar ni v osnutkih</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">Ko shranite obrazec kot osnutek, se bodo pojavili tukaj</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">Nič ni pripravljenega za pošiljanje</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">Ko zaključite osnutke, se bodo pojavili tukaj</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">Nič ni bilo poslano</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">Ko pošljete zaključene obrazce, se bodo pojavili tukaj</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">Brez obrazcev za prenos.</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">Prenesite obrazec, da začnete</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">Ni obrazcev za izbris</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">Ko boste prenesli prazne obrazce, se bodo pojavili tukaj</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">Ko boste imeli shranjene obrazce, se bodo pojavili tukaj</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Zavrzi</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-so/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"loading_form\">La soo deg foomka</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Luuqada beddel</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Tag bilowga </string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Ku laabo dhamaadka</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Kor u soco</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"clear_answer\">Jawaabta ka saar</string>\n  <string name=\"clear_answer_no\">Jooji</string>\n  <string name=\"discard_answer\">Ka saar Jawaabta</string>\n  <string name=\"discard_group\">Ka saar kooxda</string>\n  <string name=\"delete_repeat\">Ka saar kooxda</string>\n  <string name=\"delete_repeat_ask\">Ka saar Kooxdan?</string>\n  <string name=\"delete_repeat_no\">Jooji</string>\n  <!--Title of the end of form screen-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Foomamka Banaan</string>\n  <string name=\"delete_yes\">Tir tir  Foomamka</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Label for server type setting-->\n  <!--Title of dialog for configuring server URL-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"show_password\">Muuji erayga Sirta</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Jooji</string>\n  <string name=\"cancel_loading_form\">Jooji</string>\n  <string name=\"cancel_location\">Jooji</string>\n  <string name=\"do_not_change\">Jooji</string>\n  <string name=\"error_occured\">Cilad ku timi</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-sq/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">Versioni: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Shtuar më\\' EEE, MMM dd, yyyy \\'në orën\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Ruajtur më\\' EEE, MMM dd, yyyy \\'në orën\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Finalizuar më\\' EEE, MMM dd, yyyy \\'në orën\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Dërguar më\\' EEE, MMM dd, yyyy \\'në orën\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Dërgimi dështoi më\\' EEE, MMM dd, yyyy \\'në orën\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Fshirë më\\' EEE, MMM dd, yyyy \\'në orën\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Modifikuar më\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <string name=\"encrypted_form\">Formular i koduar</string>\n  <string name=\"deleted_form\">Formular i fshirë</string>\n  <string name=\"sort_the_list\">Rendit listën</string>\n  <string name=\"sort_by\">Rendit sipas</string>\n  <string name=\"sort_by_name_asc\">Emër, A-Z</string>\n  <string name=\"sort_by_name_desc\">Emër, Z-A</string>\n  <string name=\"sort_by_date_desc\">Datë, më të rejat në fillim</string>\n  <string name=\"sort_by_date_asc\">Datë, më të vjetrat në fillim</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">Filtro listën</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Kërko</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Pa këto leje Collect nuk mund të aksesojë format tuaja apo të ruajë përgjigjet. Rihapeni applikacionin kur të të jeni gati të lejoni ato.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Lejo aksesin në vendndodhje</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">Lejo aksesin në kamera</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Lejo aksesin në regjistrimin audio</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$d formë/a u shtuan.</string>\n  <string name=\"finished_disk_scan\">Skanimi përfundoi. Të gjitha format u ngarkuan.</string>\n  <string name=\"xform_parse_error\">Gabim në dekodimin e XForm për %1$s: %2$s mungon ose i pavlefshëm.</string>\n  <string name=\"loading_form\">Duke ngarkuar formularin</string>\n  <string name=\"survey_loading_reading_form_message\">Duke lexuar përkufzimin e formularit…</string>\n  <string name=\"survey_loading_reading_data_message\">Duke lexuar të dhënat e sondazhit…</string>\n  <string name=\"survey_loading_reading_csv_message\">Duke lexuar file-t CSV…</string>\n  <string name=\"parse_error\">Formulari nuk mund të dekodohet.</string>\n  <string name=\"cannot_edit_completed_form\">Ky formular nuk mund të modifikohet pasi është shënuar \\'i plotësuar\\'. Mund të jetë i enkriptuar.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Ndryshimet e paruajtura u rikthyen nga pika ku ishin ruajtur!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Ndrysho gjuhën</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Shko në fillim</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Shko në fund</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Shko sipër</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Grup</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Grup që përsëritet</string>\n  <string name=\"exit\">Dil</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Shto një tjetër</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"clearanswer_confirm\">Hiq përgjigjen nga \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Hiq përgjigjen</string>\n  <string name=\"clear_answer_ask\">Të hiqet përgjigja?</string>\n  <string name=\"clear_answer_no\">Anullo</string>\n  <string name=\"discard_answer\">Hiq përgjigjen</string>\n  <string name=\"discard_group\">Hiq grupin</string>\n  <string name=\"delete_repeat\">Hiq grupin</string>\n  <string name=\"delete_repeat_ask\">Të hiqet grupi?</string>\n  <string name=\"delete_repeat_confirm\">Hiq grupin \\\"%s\\\" dhe të gjitha nëngrupet e tij?</string>\n  <string name=\"delete_repeat_no\">Anullo</string>\n  <string name=\"invalid_answer_error\">Ndjesë, kjo përgjigje është e pavlefshme!</string>\n  <string name=\"required_answer_error\">Kjo përgjigje duhet të plotësohet patjetër!</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Jeni në fund të %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Ndjesë, ruajtja e formualrit dështoi!</string>\n  <string name=\"data_saved_ok\">Formulari u ruajt me sukses!</string>\n  <string name=\"save_point_error\">Gabim në ruajtjen e pikës së shpëtimit: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Ruaj ndryshimet</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Duke ruajtur formularin</string>\n  <string name=\"survey_saving_validating_message\">Duke validuar përgjigjet…</string>\n  <string name=\"survey_saving_collecting_message\">Duke mbledhur të dhëna…</string>\n  <string name=\"survey_saving_saving_message\">Duke ruajtur në kartën SD…</string>\n  <string name=\"survey_saving_finalizing_message\">Duke finalizuar në kartën SD…</string>\n  <string name=\"survey_saving_encrypting_message\">Duke enkriptuar të dhënat…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Nuk ka vetëm një të dhënë regjistruar për këtë kërkesë!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Kjo jr_form_id nuk përputhet vetëm me një formular bosh.</string>\n  <string name=\"no_form_id_specified\">Nuk është specifikuar numër identifikimi për formën/at???</string>\n  <string name=\"phone_does_not_support_rsa\">Telefoni nuk mund të suportojë enkriptim RSA</string>\n  <string name=\"invalid_rsa_public_key\">Çelësi publik RSA i pavlefshëm.</string>\n  <string name=\"encryption_error_message\">Formulari %s nuk është ruajtur si i finalizuar</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Nis</string>\n  <string name=\"activity_not_found\">Nuk gjendet aktivitet për t\\'u procesuar: %s</string>\n  <string name=\"no_app\">Aplikacioni i kërkuar mungon. Lutemi, futni të dhënat manualisht.</string>\n  <string name=\"null_intent_value\">Aplikacioni i jashtëm nuk dha informacionin e pritshëm.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Hap URL</string>\n  <string name=\"capture_osm\">Hap OpenMapKit</string>\n  <string name=\"recapture_osm\">Etiketo sërish OSM</string>\n  <string name=\"edited_osm_file\">File OSM XML i modifikuar:</string>\n  <string name=\"invalid_osm_data\">Diçka nuk shkoi si duhet. Nuk morrëm të dhëna OSM të vlefshme.</string>\n  <string name=\"alert\">Kujdes</string>\n  <string name=\"install_openmapkit\">Lutemi, instaloni OpenMapKit!</string>\n  <string name=\"get_barcode\">Ngarko barcode</string>\n  <string name=\"replace_barcode\">Zëvendëso barcode-in</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Vendos barcode-in brenda drejtkëndëshit</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Hap file</string>\n  <string name=\"rank_items\">Rendit pohimet</string>\n  <string name=\"invalid_range_widget\">Ky widget ka parametra të paplotë!</string>\n  <string name=\"svg_file_does_not_exist\">File-i SVG nuk ekziston!</string>\n  <string name=\"capture_image\">Bëj foto</string>\n  <string name=\"choose_image\">Zgjidh imazh</string>\n  <string name=\"selected_invalid_image\">File i përzgjedhur nuk është imazh i vlefshëm</string>\n  <string name=\"take_picture_instruction\">Prek ekranin për të bërë fotografi</string>\n  <string name=\"error_front_camera_unavailable\">Ky aparat nuk ka maera të përparme</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Ruaj dhe mbyll</string>\n  <string name=\"signature_capture\">Firmos</string>\n  <string name=\"sign_button\">Merr firmën</string>\n  <string name=\"markup_image\">Shëno imazhin</string>\n  <string name=\"draw_image\">Vizato imazh</string>\n  <string name=\"reset_image\">Rivendos</string>\n  <string name=\"set_color\">Cakto ngjyrën</string>\n  <string name=\"capture_video\">Regjistro video</string>\n  <string name=\"choose_video\">Zgjidh video</string>\n  <string name=\"play_video\">Shih videon</string>\n  <string name=\"choose_file\">Përzgjidh file</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Regjistro audio</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Zgjidh tingull</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">Ndodhi një gabim gjatë lidhjes së medias: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Përzgjidh vlerën</string>\n  <string name=\"select_answer\">Zgjidh përgjigjen</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Ndrysho vlerën</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Nuk keni përzgjedhur vlerën</string>\n  <string name=\"selected_title\">Përzgjedhur:</string>\n  <string name=\"trigger\">OK. Lutemi, vazhdoni.</string>\n  <string name=\"ext_file_no_data_error\">Ky file nuk ka të dhëna!</string>\n  <string name=\"ext_conflicting_columns_error\">Kolonat %s përputhen!</string>\n  <string name=\"ext_sortBy_numeric_error\">Kolona e renditjes duhet të përmbajë vetëm vlera numerike. Vlera konfliktuese ishte \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Gabim sintaksor në funksionin e kërkimit (): Funksioni kërkon 1, 4 ose 6 argumente.</string>\n  <string name=\"ext_search_wrong_function_error\">Gabim sintaksor në funksionin e kërkimit (): Funksion i panjohur \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Gabim sintaksor në funksionin e kërkimit (): \\'%s\\' nuk vlerësohet si funksion.</string>\n  <string name=\"ext_search_return_error\">Kërkimi gjen nj objekt të tipit \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Nuk mund të importohen të dhëna nga %1$s. Arseyja 2%2$s</string>\n  <string name=\"ext_import_progress_message\">Duke ngarkuar të dhëna paraprake nga \\'%1$s\\', lutemi prisni… %2$s</string>\n  <string name=\"ext_import_finalizing_message\">Duke finalizuar të dhënat e ngarkuara paraprakisht…</string>\n  <string name=\"ext_import_completed_message\">Leximi i të dhënave përfundoi!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager nuk është iniciuar.</string>\n  <string name=\"ext_import_csv_missing_error\">Të dhënat e jashtme për %1$s nuk janë importuar. Ndoshta keni harruar të përfshini file %2$s.csv me formularin tuaj?</string>\n  <string name=\"ext_search_generic_error\">Gabim sintaksor në funksionin e kërkimit (): %s</string>\n  <string name=\"ext_assign_value_error\">Num mund t\\'i jepet vlerë \\'%s\\'.</string>\n  <string name=\"parser_exception\">Përjashtim XPathParser \\\"%s\\\"</string>\n  <string name=\"direction\">Drejtim: %s</string>\n  <string name=\"select_date\">Zgjidh datën</string>\n  <string name=\"select_time\">Zgjidh orën</string>\n  <string name=\"no_date_selected\">Data nuk është përzgjedhur</string>\n  <string name=\"no_time_selected\">Ora nuk është përzgjedhur</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">File: 1%s është i pavlefshëm</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">File: 1%s mungon.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"basemap\">Harta bazë</string>\n  <string name=\"streets\">Rrugë</string>\n  <string name=\"terrain\">Terren</string>\n  <string name=\"hybrid\">Hibride</string>\n  <string name=\"satellite\">Satelit</string>\n  <string name=\"google_play_services_error_occured\">E pamundur të aksesohen Google Maps. A është i instaluar Google Play Services?</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Ofruesit e vendndodhjes janë të ç\\'aktivizuar!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Shko tek parametrat</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">P %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">L %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">J %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">V %1$s%2$s%3$s</string>\n  <string name=\"start\">Fillo</string>\n  <string name=\"enable_gps\">Aktivizo GPS-in</string>\n  <string name=\"geo_clear_warning\">Tipari është krijuar tanimë. A doni ta pastroni këtë tipar?</string>\n  <string name=\"geo_exit_warning\">Elimino ndryshimet dhe kthehu në ODK?</string>\n  <string name=\"polygon_validator\">Duhen së paku 3 pika për të krijuar Poligonin</string>\n  <string name=\"clear\">Pastro</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"discard\">Elimino</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Formulari po dërgohet</string>\n  <string name=\"noselect_error\">Ndjesë, ju nuk keni përzgjedhur asnjë formular</string>\n  <string name=\"no_connection\">Nuk jeni i lidhur me internetin</string>\n  <string name=\"sending_items\">Duke dërguar %1$s nga %2$s formular(ë)</string>\n  <string name=\"send_selected_data\">Dërgo të përzgjedhurat</string>\n  <string name=\"server_auth_credentials\">Emri i përdouresit ose fjalëkalimi janë të pavlefshëm për serverin: 1%s</string>\n  <string name=\"server_requires_auth\">Ky server kërkon autentikim</string>\n  <string name=\"no_forms_uploaded\">Nuk është ngarkuar asnjë formular.</string>\n  <string name=\"upload_results\">Ngarko rezultatet</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Ndrysho pamjen</string>\n  <string name=\"show_sent_and_unsent_forms\">Trego formularët e dërguar dhe të padërguar</string>\n  <string name=\"show_unsent_forms\">Trego formularët e padërguar</string>\n  <string name=\"send_in_progress\">Një dërgim po ekzekutohet në background, lutemi provoni sërish pas pak.</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Rifresko</string>\n  <string name=\"download\">Përzgjidhu</string>\n  <string name=\"downloading_data\">Duke u lidhur me server-in</string>\n  <string name=\"form_download_progress\">%1$s. Duke ngarkuar media: %2$s of %3$s</string>\n  <string name=\"version\">Versioni</string>\n  <string name=\"form_updates_available\">Forma të rifreskuara janë gati</string>\n  <string name=\"newer_version_of_a_form_info\">Ky është një përditësim i një formulari që e keni tanimë</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Formularët e ruajtur</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Forma bosh</string>\n  <string name=\"delete_confirm\">Fshi %s formë(a)?</string>\n  <string name=\"delete_file\">Fshi të përzgjedhurat</string>\n  <string name=\"delete_no\">Mos fshi</string>\n  <string name=\"delete_yes\">Fshi format</string>\n  <string name=\"form_delete_message\">Duke fshirë formularët e përzgjedhur</string>\n  <string name=\"file_deleted_ok\">%s formë(a) u fshinë me sukses!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Server</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">Fasada e përdoruesit</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Administrimi i formularëve</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Identiteti i përdoruesit dhe aparatit</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Parametrat e serverit</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL e serverit</string>\n  <string name=\"url_error\">URL-ja nuk është e saktë!</string>\n  <string name=\"username\">Emri i përdoruesit</string>\n  <string name=\"change_username\">Emri i Përdoruesit</string>\n  <string name=\"password\">Fjalëkalimi</string>\n  <string name=\"change_password\">Fjalëkalimi</string>\n  <string name=\"app_theme\">Tema</string>\n  <string name=\"language\">Gjuha</string>\n  <string name=\"use_device_language\">Përdor gjuhën e aparatit</string>\n  <string name=\"font_size\">Madhësia tekstit</string>\n  <string name=\"change_font_size\">Madhësia e tekstit</string>\n  <string name=\"font_size_extra_large\">Shumë e madhe</string>\n  <string name=\"font_size_large\">E madhe</string>\n  <string name=\"font_size_medium\">Mesatare</string>\n  <string name=\"font_size_small\">E vogël</string>\n  <string name=\"font_size_extra_small\">Shumë e vogël</string>\n  <string name=\"navigation\">Navigim</string>\n  <string name=\"swipe_navigation\">Përdor prekjen horizontale në ekran</string>\n  <string name=\"buttons_navigation\">Përdor butonat përpara/prapa</string>\n  <string name=\"swipe_buttons_navigation\">Përdor prekjen në ekran dhe butonat</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Tjetra</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Çdo pesëmbëdhjetë minuta</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Çdo një orë</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Çdo gjashtë orë</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Çdo 24 orë</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Shkarkim automatik</string>\n  <string name=\"automatic_download_summary\">Shkarko automatikisht variantet e vjetra të formularit</string>\n  <string name=\"hide_old_form_versions_setting_title\">Fshih variantet e vjetra të formularit</string>\n  <string name=\"form_submission_category\">Dërgimi i formularëve</string>\n  <string name=\"autosend_selector_title\">Dërgo automatikisht</string>\n  <string name=\"autosend\">Dërgo automatikisht</string>\n  <string name=\"off\">Fikur</string>\n  <string name=\"wifi_autosend\">Vetëm wifi</string>\n  <string name=\"cellular_autosend\">Vetëm celular</string>\n  <string name=\"wifi_cellular_autosend\">Wifi ose celular</string>\n  <string name=\"delete_after_send\">Fshi formularët pasi të nisen</string>\n  <string name=\"delete_after_send_summary\">Fshi formularët e finalizuar dhe mediat pasi të jenë ngarkuar në server</string>\n  <string name=\"form_filling_category\">Plotësimi i formularëve</string>\n  <string name=\"constraint_behavior_title\">Kufizo procesimin</string>\n  <string name=\"constraint_behavior\">Limito sjelljet procesuese</string>\n  <string name=\"constraint_behavior_on_swipe\">Valido pasi të ecet përpara</string>\n  <string name=\"constraint_behavior_on_finalize\">Mos valido deri sa të finalizohet</string>\n  <string name=\"high_resolution_title\">Video me rezolucion të lartë</string>\n  <string name=\"high_resolution_summary\">Mundëso regjistrimin video me rezolucion të lartë</string>\n  <string name=\"image_size_title\">Madhësia e imazhit</string>\n  <string name=\"image_size_dialog_title\">Numri maksimal i pixel-ave në buzën e gjatë të imazhit</string>\n  <string name=\"image_size_large\">E madhe (3072px)</string>\n  <string name=\"image_size_medium\">Mesatare (2048px)</string>\n  <string name=\"image_size_small\">E vogël (1024px)</string>\n  <string name=\"image_size_very_small\">Shumë e vogël (640px)</string>\n  <string name=\"image_size_original\">Madhësia origjinale nga kamera (e parazgjedhur)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Trego udhëzuesin për pyetjet</string>\n  <string name=\"guidance_no\">Jo</string>\n  <string name=\"guidance_yes\">Po - gjithnjë e shfaqur</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">Importimi i formularëve</string>\n  <string name=\"instance_sync\">Finalizo format në importim</string>\n  <string name=\"analytics_preferences\">Përdorimi i të dhënave</string>\n  <string name=\"analytics\">Mblidh të dhëna përdorimi anonime</string>\n  <string name=\"form_metadata_title\">Metadata të formularit</string>\n  <string name=\"form_metadata\">Metadata të formularit</string>\n  <string name=\"form_metadata_summary\">Këto të dhëna do të shtohen në formularët që kanë një fushë të plotësuar paraprakisht me emër përdoruesi, email dhe/ose telefon në mënyrë që të identifikohet personi që plotësoi formularin.</string>\n  <string name=\"user_defined\">Përdoruesi u përcaktua.</string>\n  <string name=\"phone_number\">Numri i telefonit</string>\n  <string name=\"email\">Adresa email</string>\n  <string name=\"invalid_email_address\">Adresa email nuk është e vlefshme</string>\n  <string name=\"device_defined\">Aparati u përckatua</string>\n  <string name=\"device_id\">Identifikimi i aparatit</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Vendos fjalëkalim të ri</string>\n  <string name=\"admin_password\">Fjalëkalimi i Administratorit</string>\n  <string name=\"admin_password_changed\">Fjalëkalimi i Administratorit u ndryshua me sukses</string>\n  <string name=\"admin_password_disabled\">Fjalëkalimi i Administratorit është ç\\'aktivizuar</string>\n  <string name=\"admin_password_incorrect\">Fjalëkalimi është i pasaktë!</string>\n  <string name=\"enter_admin_password\">Vendos fjalëkalimin e Administratorit</string>\n  <string name=\"change_admin_password\">Ndrysho fjalëkalimin e Administratorit</string>\n  <string name=\"show_password\">Shfaq fjalëkalimin</string>\n  <string name=\"found_in_main\">Hiq përzgjedhjen për ta fshehur nga Menuja Kryesore</string>\n  <string name=\"form_entry\">Hiq përzgjedhjen për ta fshehur nga Form Entry</string>\n  <string name=\"reset_project_settings_title\">Rivendos</string>\n  <string name=\"select_all\">Zgjidh të gjitha</string>\n  <string name=\"clear_all\">Pastro të gjitha</string>\n  <string name=\"reset_settings\">Të gjithë parametrat (paramterat e brendshëm, parametrat e ruajtur)</string>\n  <string name=\"reset_saved_forms\">Formularët e ruajtur (direktoritë, baza e të dhënave)</string>\n  <string name=\"reset_blank_forms\">Formularë bosh (dosja e formularëve, baza e të dhënave të formave, baza e të dhënave itemsets)</string>\n  <string name=\"reset_layers\">Shtrasat e hartës (dosja e shtresave)</string>\n  <string name=\"reset_cache\">Cache e ngarkimit të formularëve (.dosja e cache)</string>\n  <string name=\"reset_settings_dialog_title\">Zgjidh çfarë do të rivendoset</string>\n  <string name=\"reset_settings_button_reset\">Rivendos</string>\n  <string name=\"reset_in_progress\">Duke rivendosur…</string>\n  <string name=\"reset_app_state_result\">Rivendos rezultatet</string>\n  <string name=\"reset_app_state_warning\">Të gjitha të dhënat do të fshihen përgjithmonë. Ky veprim nuk mund të kthehet mbrapsht. </string>\n  <string name=\"reset_settings_result\">Të gjithë paramentrat :: %s</string>\n  <string name=\"reset_saved_forms_result\">Formularë të ruajtur :: %s</string>\n  <string name=\"reset_blank_forms_result\">Formularë bosh :: %s</string>\n  <string name=\"reset_layers_result\">Shtresat e hartës :: %s</string>\n  <string name=\"reset_cache_result\">Cache ngarkimi i formularëve :: %s</string>\n  <string name=\"main_menu_settings\">Parametrat e Menusë Kryesore</string>\n  <string name=\"user_settings\">Parametrat e përdoruesit</string>\n  <string name=\"form_entry_setting\">Parametrat e plotësimit të formularëve</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Shko mbrapsht</string>\n  <string name=\"moving_backwards_disabled_title\">Të shkuarit mbrapsht është ç\\'aktivizuar</string>\n  <string name=\"yes\">Po</string>\n  <string name=\"no\">Jo</string>\n  <string name=\"moving_backwards_enabled_title\">Të shkuarit mbrapsht është aktivizuar</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Rreth</string>\n  <string name=\"tell_your_friends\">Ndaj ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Kolegët tuaj i mbledhin të dhënat me letër akoma? Ndaj ODK Collect me ta.</string>\n  <string name=\"leave_a_review\">Vlerëso në Play Store</string>\n  <string name=\"odk_website\">Vizitoni faqen e ODK në internet</string>\n  <string name=\"odk_forum\">Bëhu pjesë e forumit ODK</string>\n  <string name=\"odk_forum_summary\">Regjistrohu në forum për suport, kërkuar elementë të rinj apo kontribuar me kodim apo përkthim.</string>\n  <string name=\"all_open_source_licenses_msg\">Ne qëndrojmë mbi shpatullat e gjigantëve!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"successfully_imported_settings\">Parametrat u importuan me sukses</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">U arrit limiti maksimal i karaktereve: Kodi QR nuk mund të gjenerohet për të gjithë parametrat. </string>\n  <string name=\"share\">Ndaj</string>\n  <string name=\"turn_on_flashlight\">Ndiz dritën e kameras</string>\n  <string name=\"turn_off_flashlight\">Fik dritën e kameras</string>\n  <string name=\"server_password\">Fjalëkalimi i serverit</string>\n  <string name=\"include_password_dialog\">Fjalëkalimët të përfshirë në kod</string>\n  <string name=\"generate\">Gjenero</string>\n  <string name=\"invalid_qrcode\">Kodi QR nuk ka parametra të vlefshëm</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Anullo</string>\n  <string name=\"cancel_loading_form\">Anullo</string>\n  <string name=\"cancel_location\">Anullo</string>\n  <string name=\"do_not_change\">Anullo</string>\n  <string name=\"canceling\">Duke anuluar</string>\n  <string name=\"success\">Sukses</string>\n  <string name=\"error_occured\">Ndodhi një gabim</string>\n  <string name=\"please_wait\">Lutemi, prisni pak sekonda.</string>\n  <string name=\"please_wait_long\">Lutemi, prisni. Mund të duhen disa minuta.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">\\'%s\\' nuk mund të fshihet. Lutemi, fshini fil manualisht dhe shkarkoni formularin sërish.</string>\n  <string name=\"fs_create_media_folder_error\">Dosja media \\'%s\\' nuk mund të krijohet.</string>\n  <string name=\"fs_file_copy_error\">\\'%1$s\\' nuk mund të kopjohet mbi \\'%2$s\\'. Arsyeja: %3$s</string>\n  <string name=\"select_odk_shortcut\">Përzgjidh ODK Shortcut</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">URL e panjohur: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">URL e gabuar: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Keni ngarkuar dy formularë të ndryshëm me të njëjtin numër identifikues dhe version. Ndoshta është i njëjti formular, i ngarkuar në kohë të ndryshme ose servera të ndryshëm. Në çdo rast, duhet të fshini një prej tyre. </string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Elimino</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-sr/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">Verzija: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Dodato\\' EEE, MMM dd, yyyy \\'u\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Sačuvano\\' EEE, MMM dd, yyyy \\'u\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Završeno\\' EEE, MMM dd, yyyy \\'u\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Poslato\\' EEE, MMM dd, yyyy \\'u\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Otpremanje na server nije uspjelo\\' EEE, MMM dd, yyyy \\'u\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Obrisano\\' EEE, MMM dd, yyyy \\'u\\' HH:mm</string>\n  <string name=\"submission_deleted\">Poslata anketa obrisana</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Modifikovano na\\' EEE, MMM dd, yyyy \\'u\\' HH:mm</string>\n  <string name=\"encrypted_form\">Formular pod enkripcijom</string>\n  <string name=\"deleted_form\">Obrisan formular</string>\n  <string name=\"sort_the_list\">Sortiraj listu</string>\n  <string name=\"sort_by\">Sortiraj na osnovu</string>\n  <string name=\"sort_by_name_asc\">Ime, A-Z</string>\n  <string name=\"sort_by_name_desc\">Ime, Z-A</string>\n  <string name=\"sort_by_date_desc\">Datum, prvo najskoriji</string>\n  <string name=\"sort_by_date_asc\">Datum, prvo najstariji</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">Filtriraj listu</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Pretraži</string>\n  <string name=\"not_granted_permission\">Aktivnost koju pokušavate da pokrenete zahtijeva dozvolu koja nije odobrena.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Dozvola za pristup memoriji</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Bez ovih dozvola Collent ne može pristupiti formularima ili sačuvati odgovore. Ponovo otvorite aplikaciju kada budete spremni da dozvolite pristup.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Pristup lokaciji</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">Pristup kameri</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Bez ovih dozvola Collect ne može pristupiti kameri. Molim ponovo pokušajte kada budete spremni da dozvolite pristup.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Dozvola za snimanje audija</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Bez ovih dozvola Collect ne može pristupit mikrofonu. Molim pokušajte ponovo kada budete spremni da odoborite dozvole.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$d dodatih formulara.</string>\n  <string name=\"finished_disk_scan\">Skeniranje završeno. Svi formulari su učitani.</string>\n  <string name=\"xform_parse_error\">XForm greška sintakse za %1$s:%2$s nije validan ili nedostaje.</string>\n  <string name=\"loading_form\">Učitavanje formulara</string>\n  <string name=\"survey_loading_reading_form_message\">Učitavam definiciju formulara…</string>\n  <string name=\"survey_loading_reading_data_message\">Učitavam rezultate ankete…</string>\n  <string name=\"survey_loading_reading_csv_message\">Učitavam CSV fajlove…</string>\n  <string name=\"parse_error\">Nisam u stanju da analiziram formular!</string>\n  <string name=\"loading_form_failed\">Došlo je do greške prilikom učitavanja formulara. Molim pokušajte ponovo.</string>\n  <string name=\"cannot_edit_completed_form\">Ovaj formular ne može biti mijenjan nakon što je označen kao završen. Može biti pod enkripcijom.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Nesačuvane izmjene su preuzete sa memorijske tačke!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Promijeni jezik</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Idi na Prompt</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Idi na početak</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Idi na kraj</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Idi gore</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Grupa</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Grupa za ponavljanje</string>\n  <string name=\"exit\">Izađi</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Dodaj još</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Uredi Prompt</string>\n  <string name=\"clearanswer_confirm\">Ukloni odgovor na \\\"%s\\\"</string>\n  <string name=\"clear_answer\">Ukloni odgovor</string>\n  <string name=\"clear_answer_ask\">Ukloni ovaj odgovor?</string>\n  <string name=\"clear_answer_no\">Odustani</string>\n  <string name=\"discard_answer\">Ukloni odgovor</string>\n  <string name=\"discard_group\">Obriši grupu</string>\n  <string name=\"delete_repeat\">Obriši grupu</string>\n  <string name=\"delete_repeat_ask\">Obriši ovu grupu?</string>\n  <string name=\"delete_repeat_confirm\">Obriši grupu \\\"%s\\\" i sve njene podgrupe?</string>\n  <string name=\"delete_repeat_no\">Odustani</string>\n  <string name=\"invalid_answer_error\">Odgovor nije validan!</string>\n  <string name=\"required_answer_error\">Odgovor je obavezan!</string>\n  <string name=\"invalid_space_in_answer_singular\">Upozorenje: vrijednost %s sadrži razmake</string>\n  <string name=\"invalid_space_in_answer_plural\">Upozorenje: vrijednosti %s sadrže razmake</string>\n  <string name=\"too_complex_form\">Formular je previše kompleksan za vaoj uređaj. Pokušaj da uprostiš izraze ili zatraži pomoć na forumu.</string>\n  <string name=\"survey_internal_error\">Interna greška: korak ka promt-u nije moguć.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Nalazite se na kraju %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Izvini, čuvanje formulara nije uspjelo!</string>\n  <string name=\"data_saved_ok\">Formular je uspješno sačuvan!</string>\n  <string name=\"save_point_error\">Greška u čuvanju tačke povratka: %s </string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Sačuvaj izmjene</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Čuvam formular</string>\n  <string name=\"survey_saving_validating_message\">Validiram odgovore…</string>\n  <string name=\"survey_saving_collecting_message\">Prikupljam podatke…</string>\n  <string name=\"survey_saving_saving_message\">Čuvam na SD karticu…</string>\n  <string name=\"survey_saving_finalizing_message\">Finaliziram na SD karticu…</string>\n  <string name=\"survey_saving_encrypting_message\">Enkripcija podataka je u toku…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Ne baš samo jedna zabilješka za ovu instancu!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Ne samo jedan prazan formular je povezan sa ovim jr_form_id.</string>\n  <string name=\"no_form_id_specified\">FormID nije označen???</string>\n  <string name=\"phone_does_not_support_rsa\">Telefon ne podržava RSA enkripciju.</string>\n  <string name=\"invalid_rsa_public_key\">Pogrešan RSA pristupna šifra.</string>\n  <string name=\"encryption_error_message\">%s Formular nije sačuvan kao finalizovan!</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Pokreni</string>\n  <string name=\"activity_not_found\">Aktivnost nije pronađena: %s</string>\n  <string name=\"no_app\">Tražena aplikacija nedostaje. Molim ručno unesite podatke.</string>\n  <string name=\"null_intent_value\">Spoljašna aplikacija nije dostavila očekivane podatke.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Otvori web adresu URL</string>\n  <string name=\"capture_osm\">Pokreni OpenMapKit</string>\n  <string name=\"recapture_osm\">Ponovo pokreni OSM Tagging</string>\n  <string name=\"edited_osm_file\">Izmijenjen OSM XML fajl:</string>\n  <string name=\"invalid_osm_data\">Nešto je pošlo po zlu. Nismo dobili validne OSM podatke.</string>\n  <string name=\"alert\">Uzbuna</string>\n  <string name=\"install_openmapkit\">Molim instalirajte OpenMapKit!</string>\n  <string name=\"get_barcode\">Preuzmi barkod</string>\n  <string name=\"replace_barcode\">Zamijeni barkod</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Postavi barkod unutar kocke</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Otvori fajl</string>\n  <string name=\"rank_items\">Rangiraj stavke</string>\n  <string name=\"number_picker_title\">Birač brojeva</string>\n  <string name=\"invalid_range_widget\">Ovaj widget ima nepravilne parametre!</string>\n  <string name=\"svg_file_does_not_exist\">SVG fajl ne postoji!</string>\n  <string name=\"capture_image\">Fotografiši</string>\n  <string name=\"choose_image\">Izaberi sliku</string>\n  <string name=\"selected_invalid_image\">Odabrani fajl nije validna slika</string>\n  <string name=\"take_picture_instruction\">Dodirni ekran kako bi fotografisao</string>\n  <string name=\"error_front_camera_unavailable\">Prednja kamera nije dostupna na ovom uređaju</string>\n  <string name=\"annotate_image\">Dodaj detalje o slici</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Sačuvaj i izađi</string>\n  <string name=\"signature_capture\">Elektronski potpis</string>\n  <string name=\"sign_button\">Prikupi elektronski potpis</string>\n  <string name=\"markup_image\">Markup slika</string>\n  <string name=\"draw_image\">Skica</string>\n  <string name=\"reset_image\">Resetuj</string>\n  <string name=\"set_color\">Postavi boje</string>\n  <string name=\"capture_video\">Snimi video sadržaj</string>\n  <string name=\"choose_video\">Izaberi video</string>\n  <string name=\"play_video\">Pusti video</string>\n  <string name=\"choose_file\">Izaberi fajl</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Snimi zvuk</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Izaberi zvuk</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">Došlo je do greške prilikom učitavanja sadržaja: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Odaberi vrijednost</string>\n  <string name=\"select_answer\">Odaberi odgovor</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Izmijeni vrijednost</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Nijedna vrijednost nije odabrana</string>\n  <string name=\"selected_title\">Odabrano:</string>\n  <string name=\"trigger\">Ok. Molim nastavite dalje.</string>\n  <string name=\"ext_file_no_data_error\">Fajl ne sadrži podatke!</string>\n  <string name=\"ext_conflicting_columns_error\">Kolone %s se podudaraju!</string>\n  <string name=\"ext_sortBy_numeric_error\">Vaša kolona za razvrstavanje treba da sadrži samo brojčane vrijednosti. Problematičan unos je \\'%s\\'</string>\n  <string name=\"ext_search_wrong_arguments_error\">Greška u sintaksi prilikom search()/pretraži funkcije. Funkciji je potreban 1, 4 ili 6 argumenata.</string>\n  <string name=\"ext_search_wrong_function_error\">Greška u sintaksi prilikom search()/pretraži funkcije. Neprepoznata funkcija \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Greška u sintaksi prilikom search()/pretraži funkcije: \\'%s\\' nije učitana kao funkcija.</string>\n  <string name=\"ext_search_return_error\">Pretraga je vratila objekat tipa \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Ne mogu da učitam podatke iz %1$s. Razlog: %2$s</string>\n  <string name=\"ext_import_progress_message\">Učitavam podatke iz \\'%1$s\\', molim sačekajte…%2$s</string>\n  <string name=\"ext_import_cancelled_message\">Učitavanje podataka obustavljeno.</string>\n  <string name=\"ext_import_finalizing_message\">Finaliziram učitavanje podataka…</string>\n  <string name=\"ext_import_completed_message\">Učitavanje podataka je završeno!</string>\n  <string name=\"ext_not_initialized_error\">Vanjski upravljač podacima nije pokrenut.</string>\n  <string name=\"ext_import_csv_missing_error\">Vanjski podaci za %1$s nisu učitani. Možda ste zaboravili da uključiti %2$s.csv fajl sa Vašim formularom?</string>\n  <string name=\"ext_search_generic_error\">Greška u sintaksi prilikom search()/pretraži funkcije: %s</string>\n  <string name=\"ext_assign_value_error\">Ne mogu da dodijelim vrijednost pri \\'%s\\'.</string>\n  <string name=\"parser_exception\">XPathParser izuzetak :\\\"%s\\\"</string>\n  <string name=\"get_bearing\">Snimi položaj</string>\n  <string name=\"replace_bearing\">Zamijeni položaj</string>\n  <string name=\"getting_bearing\">Učitaj položaj</string>\n  <string name=\"accept_bearing\">Snimi položaj</string>\n  <string name=\"bearing\">Pozicioniranje: %.3f</string>\n  <string name=\"direction\">Pravac: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Pravac ne može biti određen: uređaju nedostaje mjerač brzine, magnetnog polja ili oboje.</string>\n  <string name=\"select_date\">Odaberi datum</string>\n  <string name=\"select_time\">Odaberi vrijeme</string>\n  <string name=\"no_date_selected\">Datum nije odabran</string>\n  <string name=\"no_time_selected\">Vrijeme nije odabrano</string>\n  <string name=\"ethiopian_month_1\">Januar</string>\n  <string name=\"ethiopian_month_2\">Februar</string>\n  <string name=\"ethiopian_month_3\">Mart</string>\n  <string name=\"ethiopian_month_4\">April</string>\n  <string name=\"ethiopian_month_5\">Maj</string>\n  <string name=\"ethiopian_month_6\">Jun</string>\n  <string name=\"ethiopian_month_7\">Jul</string>\n  <string name=\"ethiopian_month_8\">Avgust</string>\n  <string name=\"ethiopian_month_9\">Septembar</string>\n  <string name=\"ethiopian_month_10\">Oktobar</string>\n  <string name=\"ethiopian_month_11\">Novembar</string>\n  <string name=\"ethiopian_month_12\">Decembar</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Januar</string>\n  <string name=\"coptic_month_2\">Februar</string>\n  <string name=\"coptic_month_3\">Mart</string>\n  <string name=\"coptic_month_4\">April</string>\n  <string name=\"coptic_month_5\">Maj</string>\n  <string name=\"coptic_month_6\">Jun</string>\n  <string name=\"coptic_month_7\">Jul</string>\n  <string name=\"coptic_month_8\">Avgust</string>\n  <string name=\"coptic_month_9\">Septembar</string>\n  <string name=\"coptic_month_10\">Oktobar</string>\n  <string name=\"coptic_month_11\">Novembar</string>\n  <string name=\"coptic_month_12\">Decembar</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Januar</string>\n  <string name=\"islamic_month_2\">Februar</string>\n  <string name=\"islamic_month_3\">Mart</string>\n  <string name=\"islamic_month_4\">April</string>\n  <string name=\"islamic_month_5\">Maj</string>\n  <string name=\"islamic_month_6\">Jun</string>\n  <string name=\"islamic_month_7\">Jul</string>\n  <string name=\"islamic_month_8\">Avgust</string>\n  <string name=\"islamic_month_9\">Septembar</string>\n  <string name=\"islamic_month_10\">Oktobar</string>\n  <string name=\"islamic_month_11\">Novembar</string>\n  <string name=\"islamic_month_12\">Decembar</string>\n  <string name=\"persian_month_4\">Maj</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Fajl: %s je nevalidno.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Fajl: %s nedostaje.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_usgs_sat\">USCG National Map Hybrid</string>\n  <string name=\"openmap_usgs_img\">USGC National Map Imagery</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Osnovna mapa</string>\n  <string name=\"streets\">Ulice</string>\n  <string name=\"terrain\">Teren</string>\n  <string name=\"hybrid\">Hibrid</string>\n  <string name=\"satellite\">Satelit</string>\n  <string name=\"google_play_services_error_occured\">Ne mogu da pristupim Google Mapama. Je li Google Play Service instaliran?</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">Zabilježi lokaciju</string>\n  <string name=\"provider_disabled_error\">Pronalaženje lokacije je isključeno!</string>\n  <string name=\"google_play_services_not_available\">Ovaj formular želi da prati vašu poziciju ali Google Play Services nisu dostupne.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Ovaj formular želi da prati vašu lokaciju ali provajderi lokacije su isključeni. Molim uključite ih u podešavanjima Androida.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Idi na podešavanja.</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"background_location_disabled\">Ovaj formular želi da prati vašu lokaciju ali je ona isključena. Molim uključite lokaciju u %1$s meniju iznad.</string>\n  <string name=\"background_location_enabled\">Ovaj formular bilježi tvoju lokaciju. Možeš onemogućiti praćenje u %1$s meniju iznad.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">W%1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Metod unosa</string>\n  <string name=\"start\">Početak</string>\n  <string name=\"enable_gps\">Omogući GPS</string>\n  <string name=\"geo_clear_warning\">Karakteristika je već kreirana. Da li biste željeli da obrišete karakteristiku?</string>\n  <string name=\"geo_exit_warning\">Odbaci izmjene i vrati se na ODK?</string>\n  <string name=\"polygon_validator\">Moram imati makar 3 tačke kako bih kreirao Polygon</string>\n  <string name=\"polyline_validator\">Moraju postojati najmanje 2 tačke kako bi se kreirao Polyline</string>\n  <string name=\"clear\">Očisti</string>\n  <string name=\"placement_mode\">Postavi dodirom</string>\n  <string name=\"manual_mode\">Ručno bilježenje lokacije</string>\n  <string name=\"automatic_mode\">Automatsko bilježenje lokacije</string>\n  <string name=\"recording_interval\">Interval bilježenja:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d sekundi</item>\n    <item quantity=\"few\">%d sekundi</item>\n    <item quantity=\"other\">%dsekundi</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d minuta</item>\n    <item quantity=\"few\">%d minuta</item>\n    <item quantity=\"other\">%d minuta</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Uslovi preciznosti:</string>\n  <string name=\"none\">Ništa</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d metar</item>\n    <item quantity=\"few\">%d metara</item>\n    <item quantity=\"other\">%d metara</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Unijeti podaci: %d</string>\n  <string name=\"collection_status_placement\">Unijeti podaci: %d (dodirni kako bi unio podatke)</string>\n  <string name=\"collection_status_manual\">Unijeti podaci: %d (ručni unos)</string>\n  <string name=\"collection_status_auto_seconds\">Unijeti podaci: %1$d (bilježim svake %2$d sekunde)</string>\n  <string name=\"collection_status_auto_minutes\">Unesene tačke: %1$d (bilježim svaka %2$d minuta)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Unesene tačke: %1$d (bilježim svakih %2$d sekundi, preciznost %3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Unesene tačke: %1$d (bilježim svakih %2$d minuta, preciznost %3$d m)</string>\n  <string name=\"geopoint_no_draggable_instruction\">Dodirni da dodaš dugme za marker</string>\n  <string name=\"discard\">Odbaci</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Saljem formular</string>\n  <string name=\"noselect_error\">Niste odabrali nijedan formular!</string>\n  <string name=\"no_connection\">Povezivanje na mrežu trenutno nije dostupno</string>\n  <string name=\"sending_items\">Šaljem %1$s od %2$s formulara().</string>\n  <string name=\"send_selected_data\">Pošalji označeno</string>\n  <string name=\"server_auth_credentials\">Pogrešno korisničko ime ili lozinka za pristup serveru: %s</string>\n  <string name=\"server_requires_auth\">Server zahtijeva kredencijale</string>\n  <string name=\"no_forms_uploaded\">Nijedan formular nije otpremljen.</string>\n  <string name=\"upload_results\">Rezultati otpremljivanja</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Promijeni izgled</string>\n  <string name=\"show_sent_and_unsent_forms\">Prikaži poslate i neposlate formulare</string>\n  <string name=\"show_unsent_forms\">Prikaži neposlate formulare</string>\n  <string name=\"send_in_progress\">Otpremanje u pozadini je u toku, molim pokušajte za koji trenutak</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Osvježi</string>\n  <string name=\"download\">Preuzmi odabrano</string>\n  <string name=\"downloading_data\">Povezivanje sa serverom</string>\n  <string name=\"fetching_file\">Dobijam %1$s.\\n\\nForm %2$s od %3$s formulara().</string>\n  <string name=\"form_download_progress\">%1$s. Preuzimam media fajlove: %2$s od %3$s</string>\n  <string name=\"version\">Verzija:</string>\n  <string name=\"form_updates_available\">Ažuriranja formulara su dostupna</string>\n  <string name=\"newer_version_of_a_form_info\">Ovo je novija verzija ranije preuzetog formulara</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Sačuvani formulari</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Prazni formulari</string>\n  <string name=\"delete_confirm\">Obriši %s formular(e)?</string>\n  <string name=\"delete_file\">Obriši označeno</string>\n  <string name=\"delete_no\">Ne briši!</string>\n  <string name=\"delete_yes\">Obriši formulare</string>\n  <string name=\"form_delete_message\">Brišem odabrane formulare</string>\n  <string name=\"file_deleted_ok\">%s formular(i) su uspješno obrisani!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Server</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">Korisnički interfejs </string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Upravljanje formularima</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Identitet uređaja i korisnika</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Podešavanje servera</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Adresa - URL - servera</string>\n  <string name=\"url_error\">Nepostojeći URL</string>\n  <string name=\"username\">Korisničko ime</string>\n  <string name=\"change_username\">ODK Korisničko ime</string>\n  <string name=\"password\">Lozinka</string>\n  <string name=\"change_password\">ODK Lozinka</string>\n  <string name=\"password_error_whitespace\">Prazan prostor na početku ili kraju nije dozvoljen kod lozinki</string>\n  <string name=\"username_error_whitespace\">Prazan prostor na početku ili kraju nije dozvoljen kod korisničkih imena</string>\n  <string name=\"app_theme\">Tema</string>\n  <string name=\"language\">Jezik</string>\n  <string name=\"use_device_language\">Koristi jezik uređaja</string>\n  <string name=\"font_size\">Veličina teksta</string>\n  <string name=\"change_font_size\">Veličina teksta</string>\n  <string name=\"font_size_extra_large\">Veoma veliko</string>\n  <string name=\"font_size_large\">Veliko</string>\n  <string name=\"font_size_medium\">Srednje</string>\n  <string name=\"font_size_small\">Malo</string>\n  <string name=\"font_size_extra_small\">Veoma malo</string>\n  <string name=\"navigation\">Navigacija</string>\n  <string name=\"swipe_navigation\">Koristi horizontalna prevlačenja preko ekrana</string>\n  <string name=\"buttons_navigation\">Koristi naprijed/nazad dugmad</string>\n  <string name=\"swipe_buttons_navigation\">Koristi prevlačenje i dugmad</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Sledeće</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Nazad</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Ažuriranje formulara</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Svakih 15 minuta</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Svakog sata</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Svakih 6 sati</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Svakih 24 sata</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Automatsko preuzimanje</string>\n  <string name=\"automatic_download_summary\">Automatski preuzmi ažuriranu verziju formulara</string>\n  <string name=\"hide_old_form_versions_setting_title\">Sakrij stare verzije formulara</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Samo nova verzija će se pojaviti u Popuni prazan formular</string>\n  <string name=\"form_submission_category\">Otpremanje formulara</string>\n  <string name=\"autosend_selector_title\">Automatski otpremi</string>\n  <string name=\"autosend\">Automatski otpremi</string>\n  <string name=\"off\">Isključeno</string>\n  <string name=\"wifi_autosend\">Samo Wifi</string>\n  <string name=\"cellular_autosend\">Samo mrežni podaci</string>\n  <string name=\"wifi_cellular_autosend\">Wifi ili mrežni podaci</string>\n  <string name=\"delete_after_send\">Obriši nakon otpreme</string>\n  <string name=\"delete_after_send_summary\">Briše završene ankete i medije nakon otpremanja na server</string>\n  <string name=\"form_filling_category\">Ispunjavanje formulara</string>\n  <string name=\"constraint_behavior_title\">Ograničenje procesuiranja</string>\n  <string name=\"constraint_behavior\">Ograniči procesuiranje</string>\n  <string name=\"constraint_behavior_on_swipe\">Validiraj nakon prevlačenja za naprijed</string>\n  <string name=\"constraint_behavior_on_finalize\">Odloži validaciju dok anketa nije završena</string>\n  <string name=\"high_resolution_title\">Video visoke rezolucije</string>\n  <string name=\"high_resolution_summary\">Omogući snimanje video sadržaja visoke rezolucije</string>\n  <string name=\"image_size_title\">Veličina slike</string>\n  <string name=\"image_size_dialog_title\">Maksimalan broj piksela na osnovu dugačke ivice slike</string>\n  <string name=\"image_size_large\">Veliko (3072px)</string>\n  <string name=\"image_size_medium\">Srednje (2048px)</string>\n  <string name=\"image_size_small\">Malo (1024px)</string>\n  <string name=\"image_size_very_small\">Veoma malo (640px)</string>\n  <string name=\"image_size_original\">Originalna veličina iz kamere (fabrička podešavanja)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Prikaži uputstva za pitanja</string>\n  <string name=\"guidance_no\">Ne</string>\n  <string name=\"guidance_yes\">Da - uvijek pokaži</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Da - minimizirano</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">Unošenje formulara</string>\n  <string name=\"instance_sync\">Finaliziraj formular pri unosu</string>\n  <string name=\"instance_sync_summary\">Odnosi se na fomrulare direktno dodate folderu instanci.</string>\n  <string name=\"analytics_preferences\">Podaci o korišćenju</string>\n  <string name=\"analytics\">Prikupi anonimne podatke o korišćenju</string>\n  <string name=\"form_metadata_title\">Metapodaci formulara</string>\n  <string name=\"form_metadata\">Metapodaci formulara</string>\n  <string name=\"form_metadata_summary\">Ove vrijednosti će biti dodate formularima koji imaju korisnično ime, email ili telefon u početnim poljima kako bi se identifikovala osoba koja otprema podatke.</string>\n  <string name=\"user_defined\">Definiše korisnik</string>\n  <string name=\"phone_number\">Broj telefona</string>\n  <string name=\"email\">Email adresa</string>\n  <string name=\"invalid_email_address\">Pogrešna email adresa!</string>\n  <string name=\"device_defined\">Definiše uređaj</string>\n  <string name=\"device_id\">ID uređaja</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Ukucaj novu lozinku</string>\n  <string name=\"admin_password\">Administratorska lozinka</string>\n  <string name=\"admin_password_changed\">Administratorska lozinka je uspješno promijenjena</string>\n  <string name=\"admin_password_disabled\">Administratorska lozinka je onemogućena</string>\n  <string name=\"admin_password_incorrect\">Netačna lozinka</string>\n  <string name=\"enter_admin_password\">Ukucaj administratorsku lozinku</string>\n  <string name=\"change_admin_password\">Promijeni administratorsku lozinku</string>\n  <string name=\"show_password\">Prikaži lozinku</string>\n  <string name=\"found_in_main\">Uklonite oznaku da biste isključili iz glavnog menija</string>\n  <string name=\"form_entry\">Ukloni oznaku kako bi sakrio stavku iz Form Entry</string>\n  <string name=\"reset_project_settings_title\">Resetuj</string>\n  <string name=\"select_all\">Obilježi sve</string>\n  <string name=\"clear_all\">Očisti sve</string>\n  <string name=\"reset_settings\">Sva podeševanja (interna podešavanja, sačuvana podešavanja)</string>\n  <string name=\"reset_saved_forms\">Sačuvani formulari (folderi instanci, baze podataka instanci)</string>\n  <string name=\"reset_blank_forms\">Prazni formulari (folder formulara, baza podataka formulara, baza podataka ajtema)</string>\n  <string name=\"reset_layers\">Slojevi mape (folder slojeva)</string>\n  <string name=\"reset_cache\">Keš za učitavanje formulara (keš folder)</string>\n  <string name=\"reset_settings_dialog_title\">Obilježi šta želiš da resetuješ</string>\n  <string name=\"reset_settings_button_reset\">Resetuj</string>\n  <string name=\"reset_in_progress\">Resetujem…</string>\n  <string name=\"reset_app_state_result\">Rezultati resetovanja</string>\n  <string name=\"reset_app_state_warning\">Svi obilježeni podaci biće trajno obrisani. Nema opcije da se brisanje poništi.</string>\n  <string name=\"reset_settings_result\">Sva podešavanja :: %s</string>\n  <string name=\"reset_saved_forms_result\">Sačuvani formulari :: %s</string>\n  <string name=\"reset_blank_forms_result\">Prazni formulari :: %s</string>\n  <string name=\"reset_layers_result\">Slojevi mape :: %s</string>\n  <string name=\"reset_cache_result\">Keš za ulčitavanje formulara :: %s</string>\n  <string name=\"main_menu_settings\">Podešavanja glavnog menija</string>\n  <string name=\"user_settings\">Korisnička podešavanja</string>\n  <string name=\"form_entry_setting\">Podešavanje ispunjavanja formulara</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Idi nazad</string>\n  <string name=\"moving_backwards_disabled_title\">Povratak nazad onemogućen</string>\n  <string name=\"yes\">Da</string>\n  <string name=\"no\">Ne</string>\n  <string name=\"moving_backwards_enabled_title\">Kretanje unazad je omogućeno</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Informacije </string>\n  <string name=\"tell_your_friends\">Podijeli ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Da li tvoje kolege i dalje prikupljaju podatke na papiru? Podijeli ODK sa njima</string>\n  <string name=\"leave_a_review\">Napiši Play Store recenziju</string>\n  <string name=\"leave_a_review_msg\">Vaša (nadamo se pozitivna) recenzija povećava vidljivost aplikacije u Play Storu.</string>\n  <string name=\"odk_website\">Posjeti ODK vebsajt</string>\n  <string name=\"odk_website_summary\">ODK se koristi za prikupljanje podataka o društvenim stvarima u izazovnim situacijama.</string>\n  <string name=\"odk_forum\">Pridruži se ODK forumu</string>\n  <string name=\"odk_forum_summary\">Pridruži se forumu za podršku, traži opcije, ili doprinesi kodu/prevodu!</string>\n  <string name=\"all_open_source_licenses\">Otvori biblioteke/ licence koda</string>\n  <string name=\"all_open_source_licenses_msg\">Stojimo na ramenima velikana!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"successfully_imported_settings\">Podešavanja su uspješno preuzeta</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Dostignut je maksimalan broj karaktera: QR kod ne može biti generisan za sva podešavanja</string>\n  <string name=\"share\">Podijeli</string>\n  <string name=\"turn_on_flashlight\">Upali lampu</string>\n  <string name=\"turn_off_flashlight\">Ugasi lampu</string>\n  <string name=\"server_password\">Server lozinka</string>\n  <string name=\"include_password_dialog\">Lozinka je dio Koda</string>\n  <string name=\"generate\">Generiši</string>\n  <string name=\"invalid_qrcode\">QR kod ne sadrži validna podešavanja</string>\n  <string name=\"qr_code_not_found\">QR kod nije pronađen na odabranoj slici</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">Ok.</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Odustani</string>\n  <string name=\"cancel_loading_form\">Odustani</string>\n  <string name=\"cancel_location\">Odustani</string>\n  <string name=\"do_not_change\">Odustani</string>\n  <string name=\"canceling\">Odustajem</string>\n  <string name=\"notification_channel_name\">Opšta obavještenja</string>\n  <string name=\"success\">Uspjeh</string>\n  <string name=\"error_occured\">Greška</string>\n  <string name=\"please_wait\">Molim sačekajte par trenutaka</string>\n  <string name=\"please_wait_long\">Molim sačekajte. Ovo može potrajati nekoliko minuta.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Nisam mogao da obrišem \\'%s\\'. Molim obrišite fajl ručno ili ponovo preuzmite formular.</string>\n  <string name=\"fs_create_media_folder_error\">Ne mogu da kreiram medija folder \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Nisam mogao da prekopiram \\'%1$s\\' preko \\'%2$s\\'. Razlog: %3$s</string>\n  <string name=\"select_odk_shortcut\">Odaberi ODK prečicu</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Nepoznat URL: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Loš URL: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Preuzeli ste dva drugačija formulara sa istim ID-jem i verzijom. Možda su to isti formulari učitani u različito vrijeme ili sa različitih servera. U svakom slučaju, treba da obrišete jedan.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Odbaci</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-sv-rSE/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Börja nytt formulär</string>\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">Version: %s</string>\n  <string name=\"id_number\">ID: 1%s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Tillagd\\' yyyy-MM-dd (EEE) \\'kl\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Sparad\\' yyyy-MM-dd (EEE) \\'kl\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Avslutad\\' yyyy-MM-dd (EEE) \\'kl\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Skickad\\' yyyy-MM-dd (EEE) \\'kl\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Sändning misslyckades\\' yyyy-MM-dd (EEE) \\'kl\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Raderad\\' yyyy-MM-dd (EEE) \\'kl\\' HH:mm</string>\n  <string name=\"submission_deleted\">Skickat formulär har tagits bort</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Ändrad\\' yyyy-MM-dd (EEE) \\'kl\\' HH:mm</string>\n  <string name=\"encrypted_form\">Krypterat formulär</string>\n  <string name=\"deleted_form\">Raderat formulär</string>\n  <string name=\"sort_the_list\">Sortera listan</string>\n  <string name=\"sort_by\">Sorteringsordning</string>\n  <string name=\"sort_by_name_asc\">Namn, A-Ö</string>\n  <string name=\"sort_by_name_desc\">Namn, Ö-A</string>\n  <string name=\"sort_by_date_desc\">Datum, nyaste först</string>\n  <string name=\"sort_by_date_asc\">Datum, äldsta först</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">Filtrera listan</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Sök</string>\n  <string name=\"storage_runtime_permission_denied_title\">Lagringsbehörigheter</string>\n  <string name=\"location_runtime_permissions_denied_title\">Platsbehörigheter</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">Kamerabehörigheter</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Behörigheter för att spela in ljud</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$d formulär tillagda.</string>\n  <string name=\"finished_disk_scan\">Inläsning avslutad. Alla formulär har laddats in.</string>\n  <string name=\"xform_parse_error\">XForm fel vid tolkning av %1$s: %2$s saknas eller är ogiltig</string>\n  <string name=\"loading_form\">Laddar formulär.</string>\n  <string name=\"survey_loading_reading_form_message\">Läser in formulärdefinition…</string>\n  <string name=\"survey_loading_reading_data_message\">Läser in undersökningsdata…</string>\n  <string name=\"survey_loading_reading_csv_message\">Läser in CSV-filer…</string>\n  <string name=\"parse_error\">Tyvärr, ej möjligt att tolka formulär.</string>\n  <string name=\"loading_form_failed\">Ett fel uppstod vid laddning av formuläret. Var vänlig och försök igen.</string>\n  <string name=\"cannot_edit_completed_form\">Det här formuläret kan inte redigeras efter det har avslutats. Det kan vara krypterat.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Osparade ändringar har återskapats från återställningspunkt!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Ändra språk</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Gå till inmatningsrad</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Gå till början</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Gå till slutet</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Flytta upp</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Gruppera</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Repeterande grupp</string>\n  <string name=\"exit\">Avsluta</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Vill du lägga till \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Lägg till</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Lägg till ytterligare en</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Inmatningstext</string>\n  <string name=\"clearanswer_confirm\">Ta bort svar för \\\"%s\\\"</string>\n  <string name=\"clear_answer\">Ta bort svar</string>\n  <string name=\"clear_answer_ask\">Ska detta svar tas bort?</string>\n  <string name=\"clear_answer_no\">Avbryt</string>\n  <string name=\"discard_answer\">Ta bort svar</string>\n  <string name=\"discard_group\">Ta bort grupp</string>\n  <string name=\"delete_repeat\">Ta bort grupp</string>\n  <string name=\"delete_repeat_ask\">Ta bort denna grupp</string>\n  <string name=\"delete_repeat_confirm\">Ta bort gruppen \\\"%s\\\" och alla dess undergrupper?</string>\n  <string name=\"delete_repeat_no\">Avbryt</string>\n  <string name=\"invalid_answer_error\">Tyvärr, detta svar är ogiltigt!</string>\n  <string name=\"required_answer_error\">Tyvärr, detta svar är obligatoriskt!</string>\n  <string name=\"invalid_space_in_answer_singular\">Varning: bakomliggande värde %s innehåller mellanslag</string>\n  <string name=\"invalid_space_in_answer_plural\">Varning: bakomliggande värden %s innehåller mellanslag</string>\n  <string name=\"survey_internal_error\">Internt fel: gå till inmatningsrad misslyckades.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Du är vid slutet av %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Tyvärr, formuläret kunde inte sparas!</string>\n  <string name=\"data_saved_ok\">Formuläret har sparats!</string>\n  <string name=\"save_point_error\">Fel vid sparande av återställningspunkt: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Spara ändringar</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Sparar formulär</string>\n  <string name=\"survey_saving_validating_message\">Validerar svar…</string>\n  <string name=\"survey_saving_collecting_message\">Samlar in data…</string>\n  <string name=\"survey_saving_saving_message\">Sparar till minneskort…</string>\n  <string name=\"survey_saving_finalizing_message\">Avslutar till minneskort…</string>\n  <string name=\"survey_saving_encrypting_message\">Krypterar data…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Inte exakt en post för denna instans!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Inte exakt ett tomt formulär överensstämmer med detta jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Har formulär-ID angivits???</string>\n  <string name=\"phone_does_not_support_rsa\">Telefonen stöder inte RSA-kryptering.</string>\n  <string name=\"invalid_rsa_public_key\">Ogiltig publik RSA-nyckel.</string>\n  <string name=\"encryption_error_message\">%s formulär har inte sparats som avslutat.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Starta</string>\n  <string name=\"activity_not_found\">Ingen aktivitet finns för att hantera: %s</string>\n  <string name=\"no_app\">Det begärda programmet saknas. Vänligen mata in värdet manuellt.</string>\n  <string name=\"null_intent_value\">Den externa applikationen gav inte förväntad information.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Öppna URL</string>\n  <string name=\"capture_osm\">Starta OpenMapKit</string>\n  <string name=\"recapture_osm\">Upprepa OSM-taggning</string>\n  <string name=\"edited_osm_file\">Redigerad OSM XML fil:</string>\n  <string name=\"invalid_osm_data\">Någonting gick fel. Giltig OSM-data kunde inte erhållas.</string>\n  <string name=\"alert\">Varning</string>\n  <string name=\"install_openmapkit\">Vänligen installera OpenMapKit!</string>\n  <string name=\"get_barcode\">Läs streckkod</string>\n  <string name=\"replace_barcode\">Ersätt streckkod</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Placera streckkoden innanför rektangeln</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Öppna fil</string>\n  <string name=\"invalid_range_widget\">Ogiltiga parametrar för widget!</string>\n  <string name=\"svg_file_does_not_exist\">SVG-fil finns inte!</string>\n  <string name=\"capture_image\">Ta bild</string>\n  <string name=\"choose_image\">Välj bild</string>\n  <string name=\"selected_invalid_image\">Vald fil är inte en giltig bild</string>\n  <string name=\"take_picture_instruction\">Knacka på skärmen för att ta ett kort</string>\n  <string name=\"error_front_camera_unavailable\">Frontkamera inte tillgänglig på denna enhet</string>\n  <string name=\"annotate_image\">Lägg till text på bild</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Spara och stäng</string>\n  <string name=\"sign_button\">Läs in signatur</string>\n  <string name=\"markup_image\">Rita i bild</string>\n  <string name=\"draw_image\">Skissa i bild</string>\n  <string name=\"reset_image\">Återställ</string>\n  <string name=\"set_color\">Välj färg</string>\n  <string name=\"capture_video\">Spela in film</string>\n  <string name=\"choose_video\">Välj film</string>\n  <string name=\"play_video\">Spela upp film</string>\n  <string name=\"choose_file\">Välj fil</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Spela in ljud</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Välj ljud</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">Ett fel inträffade vid infogning av mediefil: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Välj ett värde</string>\n  <string name=\"select_answer\">Välj svar</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Redigera värde</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Inget värde valt</string>\n  <string name=\"selected_title\">Valda:</string>\n  <string name=\"trigger\">OK. Vänligen fortsätt.</string>\n  <string name=\"ext_file_no_data_error\">Filen innehåller ingen data!</string>\n  <string name=\"ext_conflicting_columns_error\">Kolumnerna %s överensstämmer!</string>\n  <string name=\"ext_sortBy_numeric_error\">Din sorteringskolumn bör endast innehålla numeriska värden. Motstridiga värden var \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Syntaxfel i search()-funktionen: funktionen behöver 1, 4 eller 6 argument.</string>\n  <string name=\"ext_search_wrong_function_error\">Syntaxfel i search()-funktionen: okänd funktion \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Syntaxfel i search()-funktionen: \\'%s\\' känns inte igen som en funktion.</string>\n  <string name=\"ext_search_return_error\">Sök-handler returnerade ett objekt av typen \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Kan inte importera data från %1$s. Orsak: %2$s</string>\n  <string name=\"ext_import_progress_message\">Förladdar data från \\'%1$s\\', vänligen vänta… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Datainläsning avbruten!</string>\n  <string name=\"ext_import_finalizing_message\">Avslutar förladdade data…</string>\n  <string name=\"ext_import_completed_message\">Inläsning av data slutförd!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager har inte initialiserats.</string>\n  <string name=\"ext_import_csv_missing_error\">Extern data för %1$s har inte importerats. Kanske har du glömt att inkludera filen %2$s.csv i ditt formulär?</string>\n  <string name=\"ext_search_generic_error\">Syntaxfel i search()-funktionen: %s</string>\n  <string name=\"ext_assign_value_error\">Kan inte tilldela värdet vid \\'%s\\'.</string>\n  <string name=\"parser_exception\">XPathParser undantag: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Lagra riktning</string>\n  <string name=\"replace_bearing\">Ersätt riktning</string>\n  <string name=\"getting_bearing\">Laddar riktning</string>\n  <string name=\"accept_bearing\">Lagra riktning</string>\n  <string name=\"bearing\">Riktning: %.3f</string>\n  <string name=\"direction\">Riktning: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Bäring kan inte fastställas: enheten saknar accelerometer och/eller magnetisk sensor.</string>\n  <string name=\"select_date\">Välj datum</string>\n  <string name=\"select_time\">Välj tid</string>\n  <string name=\"no_date_selected\">Inget datum valt</string>\n  <string name=\"no_time_selected\">Ingen tidpunkt vald</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimit</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Filen: %s är ogiltig!</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Filen: %s saknas.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Identitet</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">Stäng</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">Anledning till ändringarna</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Anledning</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Spara</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_usgs_sat\">USGS nationell hybridkarta</string>\n  <string name=\"openmap_usgs_img\">USGS nationell satellitkarta</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Grundkarta</string>\n  <string name=\"basemap_source\">Källa</string>\n  <string name=\"map_style_label\">%s kartstil</string>\n  <string name=\"streets\">Streets</string>\n  <string name=\"terrain\">Terrain</string>\n  <string name=\"hybrid\">Hybrid</string>\n  <string name=\"satellite\">Satellite</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Ljus</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Mörk</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Utomhus</string>\n  <string name=\"topographic\">Topografisk</string>\n  <string name=\"google_play_services_error_occured\">Åtkomst till Google Maps ej möjlig. Är Google Play Services installerad?</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Mörk materia</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Tyvärr, platstjänster är inaktiverade!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">V %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">O %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Inmatningsmetod</string>\n  <string name=\"start\">Starta</string>\n  <string name=\"enable_gps\">Aktivera GPS</string>\n  <string name=\"gps_enable_message\">GPS är inte aktiverad på din enhet. Vill du aktivera den?</string>\n  <string name=\"geo_clear_warning\">Objekt redan skapat. Vill du rensa objektet?</string>\n  <string name=\"geo_exit_warning\">Ignorera ändringar och återgå till ODK?</string>\n  <string name=\"polygon_validator\">Minst tre punkter krävs för att skapa en polygon</string>\n  <string name=\"polyline_validator\">Det behövs minst 2 punkter för att skapa en polyline</string>\n  <string name=\"clear\">Rensa</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <string name=\"accuracy_requirement\">Krav på noggrannhet:</string>\n  <string name=\"none\">Ingen</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"geopoint_no_draggable_instruction\">Knacka på knappen \\'lägg till markör\\'.</string>\n  <string name=\"discard\">Ignorera</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Skickar formulär</string>\n  <string name=\"noselect_error\">Tyvärr, du har inte valt några formulär!</string>\n  <string name=\"no_connection\">Ingen nätverksförbindelse är tillgänglig</string>\n  <string name=\"sending_items\">Skickar %1$s av %2$s formulär</string>\n  <string name=\"send_selected_data\">Skicka valt</string>\n  <string name=\"server_auth_credentials\">Ogiltigt användarnamn eller lösenord för servern: %s</string>\n  <string name=\"server_requires_auth\">Servern kräver autentisering</string>\n  <string name=\"no_forms_uploaded\">Tyvärr, inga formulär laddades upp</string>\n  <string name=\"upload_results\">Uppladdningsresultat</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Ändra vy</string>\n  <string name=\"show_sent_and_unsent_forms\">Visa skickade och oskickade formulär</string>\n  <string name=\"show_unsent_forms\">Visa oskickade formulär</string>\n  <string name=\"send_in_progress\">Sändning pågår i bakgrunden, vänligen försök igen senare</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Uppdatera</string>\n  <string name=\"download\">Hämta valda</string>\n  <string name=\"downloading_data\">Ansluter till server</string>\n  <string name=\"fetching_file\">Hämtar %1$s.\\n\\nFormulär %2$s av %3$s formulär.</string>\n  <string name=\"form_download_progress\">%1$s. Hämtar mediafil: %2$s av %3$s</string>\n  <string name=\"version\">Version:</string>\n  <string name=\"form_updates_available\">Uppdaterade formulär är tillgängliga</string>\n  <string name=\"newer_version_of_a_form_info\">Detta är en uppdatering av ett befintligt formulär</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Sparade formulär</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Tomma formulär</string>\n  <string name=\"delete_confirm\">Radera %s formulär?</string>\n  <string name=\"delete_file\">Radera vald</string>\n  <string name=\"delete_no\">Radera inte</string>\n  <string name=\"delete_yes\">Radera formulär</string>\n  <string name=\"form_delete_message\">Raderar valda formulär</string>\n  <string name=\"file_deleted_ok\">%s formulär har raderats!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Server</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">Användargränssnitt</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">Kartor</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Formulärhantering</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Användar- och enhetsidentitet</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Serverinställningar</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Server-URL</string>\n  <string name=\"url_error\">Tyvärr, ogiltig URL!</string>\n  <string name=\"username\">Användarnamn</string>\n  <string name=\"change_username\">ODK användarnamn</string>\n  <string name=\"password\">Lösenord</string>\n  <string name=\"change_password\">ODK lösenord</string>\n  <string name=\"password_error_whitespace\">Inledande eller avslutande blanksteg är inte tillåtet i lösenord</string>\n  <string name=\"username_error_whitespace\">Inledande eller avslutande blanksteg är inte tillåtet i användarnamn</string>\n  <string name=\"app_theme\">Tema</string>\n  <string name=\"language\">Språk</string>\n  <string name=\"use_device_language\">Använd enhetens språk</string>\n  <string name=\"font_size\">Textstorlek</string>\n  <string name=\"change_font_size\">Textstorlek</string>\n  <string name=\"font_size_extra_large\">Extra stor</string>\n  <string name=\"font_size_large\">Stor</string>\n  <string name=\"font_size_medium\">Mellan</string>\n  <string name=\"font_size_small\">Liten</string>\n  <string name=\"font_size_extra_small\">Extra liten</string>\n  <string name=\"navigation\">Navigering</string>\n  <string name=\"swipe_navigation\">Använd horisontella svepningar</string>\n  <string name=\"buttons_navigation\">Använd framåt/bakåt knappar</string>\n  <string name=\"swipe_buttons_navigation\">Använd svepningar och knappar</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Nästa</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Tillbaka</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Uppdatera formulär</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Varje femtonde minut</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Varje timme</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Varje sjätte timme</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Varje dygn</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Automatisk nedladdning</string>\n  <string name=\"automatic_download_summary\">Ladda ner uppdaterade formulär automatiskt</string>\n  <string name=\"hide_old_form_versions_setting_title\">Göm gamla versioner av formulären</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Endast den senaste versionen kommer att synas vid ifyllning av tomma formulär</string>\n  <string name=\"form_submission_category\">Skicka in formulär</string>\n  <string name=\"autosend_selector_title\">Skicka automatiskt</string>\n  <string name=\"autosend\">Skicka automatiskt</string>\n  <string name=\"off\">Av</string>\n  <string name=\"wifi_autosend\">Endast WiFi</string>\n  <string name=\"cellular_autosend\">Endast mobilt nätverk</string>\n  <string name=\"wifi_cellular_autosend\">WiFi eller mobilt nätverk</string>\n  <string name=\"delete_after_send\">Radera efter sändning</string>\n  <string name=\"delete_after_send_summary\">Raderar avslutade formulär och media efter dessa sänts till servern</string>\n  <string name=\"form_filling_category\">Formulärifyllnad</string>\n  <string name=\"constraint_behavior_title\">Hantering av restriktioner</string>\n  <string name=\"constraint_behavior\">Hantering av restriktioner</string>\n  <string name=\"constraint_behavior_on_swipe\">Validera vid svepning framåt</string>\n  <string name=\"constraint_behavior_on_finalize\">Validera vid avslutning</string>\n  <string name=\"high_resolution_title\">Högupplöst video</string>\n  <string name=\"high_resolution_summary\">Tillåt högupplösta filminspelningar</string>\n  <string name=\"image_size_title\">Bildstorlek</string>\n  <string name=\"image_size_dialog_title\">Maximalt antal pixlar på långsidan av bilden</string>\n  <string name=\"image_size_large\">Stor (3 072 px)</string>\n  <string name=\"image_size_medium\">Medium (2 048 px)</string>\n  <string name=\"image_size_small\">Liten (1 024 px)</string>\n  <string name=\"image_size_very_small\">Mycket liten (640 px)</string>\n  <string name=\"image_size_original\">Ursprunglig storlek i kamera (standard)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_no\">Nej</string>\n  <string name=\"guidance_yes\">Ja - visa alltid</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Ja - komprimerad</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">Importera formulär</string>\n  <string name=\"instance_sync\">Avsluta formulär vid import</string>\n  <string name=\"instance_sync_summary\">Påverkar formulär som lagts till direkt i mappen \\'instances\\'.</string>\n  <string name=\"analytics_preferences\">Användningsdata</string>\n  <string name=\"analytics\">Samla in anonym användningsdata</string>\n  <string name=\"form_metadata_title\">Formulärmetadata</string>\n  <string name=\"form_metadata\">Formulärmetadata</string>\n  <string name=\"form_metadata_summary\">Dessa värden kommer att läggas till i formulär som har förifyllda fält för användarnamn, e-post och/eller telefonnummer för att identifiera användaren.</string>\n  <string name=\"user_defined\">Användardefinierad</string>\n  <string name=\"phone_number\">Telefonnummer</string>\n  <string name=\"email\">E-postadress</string>\n  <string name=\"invalid_email_address\">Ogiltig e-postadress!</string>\n  <string name=\"device_defined\">Enhetsdefinierad</string>\n  <string name=\"device_id\">ID enhet</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Ange nytt lösenord</string>\n  <string name=\"admin_password\">Administratörslösenord</string>\n  <string name=\"admin_password_changed\">Administratörslösenordet har ändrats</string>\n  <string name=\"admin_password_disabled\">Administratörslösenord är inaktiverat</string>\n  <string name=\"admin_password_incorrect\">Tyvärr, lösenordet är felaktigt!</string>\n  <string name=\"enter_admin_password\">Ange administratörslösenordet</string>\n  <string name=\"change_admin_password\">Ändra administratörslösenordet</string>\n  <string name=\"show_password\">Visa lösenordet</string>\n  <string name=\"found_in_main\">Avmarkera för att dölja i huvudmenyn</string>\n  <string name=\"form_entry\">Avmarkera för att dölja i formulärinmatning</string>\n  <string name=\"reset_project_settings_title\">Återställ</string>\n  <string name=\"select_all\">Välj alla</string>\n  <string name=\"clear_all\">Rensa alla</string>\n  <string name=\"reset_settings\">Alla inställningar (interna inställningar, sparade inställningar)</string>\n  <string name=\"reset_saved_forms\">Sparade formulär (instances-mappen, instances databasen)</string>\n  <string name=\"reset_blank_forms\">Tomma formulär (formulärmapp, formulärdatabasen, \\'itemsets\\'-databasen)</string>\n  <string name=\"reset_layers\">Kartlager (lagermapp)</string>\n  <string name=\"reset_cache\">Formulär cache (.cache mapp)</string>\n  <string name=\"reset_settings_dialog_title\">Välj vad som skall återställas</string>\n  <string name=\"reset_settings_button_reset\">Återställ</string>\n  <string name=\"reset_in_progress\">Återställer…</string>\n  <string name=\"reset_app_state_result\">Återställningsresultat</string>\n  <string name=\"reset_app_state_warning\">All vald data kommer att raderas permanent och kan inte ångras.</string>\n  <string name=\"reset_settings_result\">Alla inställningar :: %s</string>\n  <string name=\"reset_saved_forms_result\">Sparade formulär :: %s</string>\n  <string name=\"reset_blank_forms_result\">Tomma formulär :: %s</string>\n  <string name=\"reset_layers_result\">Kartlager :: %s</string>\n  <string name=\"reset_cache_result\">Formulär cache :: %s</string>\n  <string name=\"main_menu_settings\">Inställningar för huvudmenyn</string>\n  <string name=\"user_settings\">Användarinställningar</string>\n  <string name=\"form_entry_setting\">Formulärs inmatningsinställningar</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Navigera bakåt</string>\n  <string name=\"moving_backwards_disabled_title\">Bakåtnavigering inaktiverad</string>\n  <string name=\"yes\">Ja</string>\n  <string name=\"no\">Nej</string>\n  <string name=\"moving_backwards_enabled_title\">Bakåtnavigering aktiverad</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Om</string>\n  <string name=\"tell_your_friends\">Dela ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Samlar dina kollegor fortfarande data på papper?\nDela ODK Collect med dem.</string>\n  <string name=\"leave_a_review\">Lämna ett omdöme på Play Store</string>\n  <string name=\"leave_a_review_msg\">Din (förhoppningsvis positiva) recension ökar synligheten av Appen i Play Store.</string>\n  <string name=\"odk_website\">Besök ODKs hemsida</string>\n  <string name=\"odk_website_summary\">ODK används för att samla in data för socialt arbete i utmanande miljöer</string>\n  <string name=\"odk_forum\">Gå med i ODK forumet</string>\n  <string name=\"odk_forum_summary\">Gå med i forumet för få hjälp, föreslå nya funktioner eller bidra med kod och översättningar!</string>\n  <string name=\"all_open_source_licenses\">Bibliotek/licenser för öppen källkod</string>\n  <string name=\"all_open_source_licenses_msg\">Vi står på jättarnas axlar!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"successfully_imported_settings\">Import av inställningarna lyckades</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Maximalt antal tecken uppnått: QR-kod kan inte genereras för alla inställningar</string>\n  <string name=\"share\">Dela</string>\n  <string name=\"turn_on_flashlight\">Slå på ficklampa</string>\n  <string name=\"turn_off_flashlight\">Stäng av ficklampa</string>\n  <string name=\"server_password\">Serverlösenord</string>\n  <string name=\"include_password_dialog\">Koden innehåller lösenorden</string>\n  <string name=\"generate\">Generera</string>\n  <string name=\"invalid_qrcode\">QR-koden innehåller inga giltiga inställningar</string>\n  <string name=\"qr_code_not_found\">Ingen QR-kod kunde hittas i vald bild</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Avbryt</string>\n  <string name=\"cancel_loading_form\">Avbryt</string>\n  <string name=\"cancel_location\">Avbryt</string>\n  <string name=\"do_not_change\">Avbryt</string>\n  <string name=\"canceling\">Avbryter</string>\n  <string name=\"notification_channel_name\">Allmäna notifieringar</string>\n  <string name=\"success\">Lyckades</string>\n  <string name=\"error_occured\">Ett fel inträffade</string>\n  <string name=\"please_wait\">Var vänlig och vänta ett tag.</string>\n  <string name=\"please_wait_long\">Var vänlig och vänta. Detta kan ta en stund.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Kan inte radera \\'%s\\'. Vänligen radera filen manuellt och ladda hem formuläret igen.</string>\n  <string name=\"fs_create_media_folder_error\">Kan inte skapa mediamappen \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Kan inte kopiera \\'%1$s\\' över \\'%2$s\\'. Orsak: %3$s</string>\n  <string name=\"select_odk_shortcut\">Välj ODK genväg</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">Lägg till</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Okänd URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Ogiltig URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Du har laddat ner två olika formulär med samma ID och version. Kanske har dem laddats upp vid olika tidpunkter eller till olika servrar. I vilket fall som helst bör du radera ett av dem.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Ignorera</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-sw/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Anza fomu mpya</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Rasimu</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Tayari kutuma</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Imetumwa</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Pakua fomu</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Futa fomu</string>\n  <string name=\"saved_forms\">Fomu zilizohifadhiwa</string>\n  <string name=\"version_number\">Toleo: %s</string>\n  <string name=\"id_number\">Utambulisho: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Imeongezwa kwenye\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Imehifadhiwa kwenye\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Imekamilishwa kwenye\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Imetumwa\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Kutuma ilikataa\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Futa kwenye\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <string name=\"submission_deleted\">Wasilisho limefutwa</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Imerekebishwa\\' EEE, MMM dd, yyyy \\'katika\\' HH:mm</string>\n  <string name=\"encrypted_form\">Fomu yenye usimbaji fiche</string>\n  <string name=\"deleted_form\">Fomu iliyofutwa</string>\n  <string name=\"sort_the_list\">Panga orodha</string>\n  <string name=\"sort_by\">Panga kwa</string>\n  <string name=\"sort_by_name_asc\">Jina, A-Z</string>\n  <string name=\"sort_by_name_desc\">Jina, Z-A</string>\n  <string name=\"sort_by_date_desc\">Tarehe, ya sasa kwanza</string>\n  <string name=\"sort_by_date_asc\">Tarehe, ya zamani kwanza</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Tarehe, Iliyohifadhiwa mwisho iwe mwanzo</string>\n  <string name=\"filter_the_list\">Chuja orodha</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Tafuta</string>\n  <string name=\"not_granted_permission\">Kitendo unachijaribu kufanya kinahitaji ruhusa ambayo bado haijatolewa.</string>\n  <string name=\"storage_runtime_permission_denied_title\">Ruhusa ya kuhifadhi</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Bila hizi ruhusa Collect haiwezi kufikia fomu au kuhifadhi majibu. Fungua tena program ukiwa tayari kuziruhusu.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Ruhusa ya kuonesha eneo </string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">Ruhusa ya camera</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Bila hii ruhusa Collect haiwezi kutumia kamera. Tafadhali jaribu tena ukiwa tayari kutoa ruhusa.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Ruhusa ya kurekodi sauti</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Bila hii ruhusa Collect haiwezi kutumia kinakili sauti. Tafadhali jaribu tena ukiwa tayari kutoa ruhusa.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Fungua ramani ya fomu</string>\n  <string name=\"instance_scan_count\">%1$dfomu zimeongezwa.</string>\n  <string name=\"finished_disk_scan\">Imemaliza kuskani. Fomu zote zimewekwa.</string>\n  <string name=\"xform_parse_error\">XFomu imesoma kimakosa kwa%1$s: %2$simepotea au batili.</string>\n  <string name=\"loading_form\">Fomu inakuja</string>\n  <string name=\"survey_loading_reading_form_message\">Inasoma maana ya fomu…</string>\n  <string name=\"survey_loading_reading_data_message\">Inasoma taarifa za utafit…</string>\n  <string name=\"survey_loading_reading_csv_message\">Inasoma faili za CSV…</string>\n  <string name=\"parse_error\">Pole, haiwezi kuchanganua fomu.</string>\n  <string name=\"loading_form_failed\">Tatizo limetokea wakati wa kupakua fomu. Tafadhali jaribu tena.</string>\n  <string name=\"cannot_edit_completed_form\">Hii fomu haiwezi kurekebishwa wakati imeshamalizika. Inaweza kuwa na usimbaji fiche.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Mabadiliko ambayo hayakuwa yamehifadhiwa yamerudishwa!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Badili lugha </string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Nenda kwenye chanzo</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Nenda mwanzo</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Nenda mwisho</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Nenda juu</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Kundi</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Kundi linalojirudia</string>\n  <string name=\"exit\">Toka</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Ongeza \\\"1%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Ongeza</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Ongeza nyingine</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Hariri prompt</string>\n  <string name=\"clearanswer_confirm\">Ondoa jibu la \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Ondoa jibu</string>\n  <string name=\"clear_answer_ask\">Ondoa jibu hili?</string>\n  <string name=\"clear_answer_no\">Sitisha</string>\n  <string name=\"discard_answer\">Ondoa jibu</string>\n  <string name=\"discard_group\">Ondoa kikundi</string>\n  <string name=\"delete_repeat\">Ondoa kikundi</string>\n  <string name=\"delete_repeat_ask\">Ondoa kikundi hiki?</string>\n  <string name=\"delete_repeat_confirm\">Ondoa kikundi \\\"%s\\\" na vikundi vyake vidogo?</string>\n  <string name=\"delete_repeat_no\">Sitisha</string>\n  <string name=\"invalid_answer_error\">Samahani,hili  jibu ni  batili!</string>\n  <string name=\"required_answer_error\">Samahani, jibu hili linahitajika!</string>\n  <string name=\"invalid_space_in_answer_singular\">Onyo: thamani ya chini%sina nafasi</string>\n  <string name=\"invalid_space_in_answer_plural\">Onyo: thamani za chini %szina nafasi</string>\n  <string name=\"too_complex_form\">Fomu hii ni ngumu sana kwa uwezo wa kifaa hiki. Jaribu kuirahisisha au omba msaada katika jukwaa.</string>\n  <string name=\"survey_internal_error\">Kosa la ndani: hatua ya kutoa taarifa imeshindikana.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Upo mwishoni mwa %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Malizia</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Tuma</string>\n  <string name=\"data_saved_error\">Samahani, uhifadhi wa fomu umeshindikana!</string>\n  <string name=\"data_saved_ok\">Umefanikiwa kuhifadhi fomu!</string>\n  <string name=\"save_point_error\">Hitilafu katika kuhifadhi alama ya urejeshi \\\"recovery point\\\" :%s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Futa fomu</string>\n  <string name=\"discard_changes\">Kataa mabadiliko</string>\n  <string name=\"keep_changes\">Hifadhi mabadiliko</string>\n  <string name=\"keep_editing\">Endelea kuhariri</string>\n  <string name=\"quit_form_title\">Hifadhi fomu ?</string>\n  <string name=\"save_explanation\">Unaweza kutanza hii fomu na kuipata kwenye rasimu ya fomu wakati wowote</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"quit_form_continue_title\">Endelea kuhariri</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Inahifadhi fomu</string>\n  <string name=\"survey_saving_validating_message\">Inathibitisha majibu…</string>\n  <string name=\"survey_saving_collecting_message\">Inakusanya taarifa…</string>\n  <string name=\"survey_saving_saving_message\">Inahifadhi kwenye memorikadi…</string>\n  <string name=\"survey_saving_finalizing_message\">Kamilisha/hifadhi katika memorikadi…</string>\n  <string name=\"survey_saving_encrypting_message\">Kuongeza usalama katika taarifa…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Sio sahihi iwe rekodi moja kwa huu mfano!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Sio lazima fomu moja tupu ilingane na hii jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Hakuna utambulisho wa fomu uliyoainishwa???</string>\n  <string name=\"phone_does_not_support_rsa\">Simu hairuhusu usimbaji fiche wa RSA</string>\n  <string name=\"invalid_rsa_public_key\">Ruhusa ya Umma ya RSA ni batili.</string>\n  <string name=\"encryption_error_message\">         %s Fomu haija hifadhiwa kama ilivyo hitimishwa</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Haikuwezekana kuanza kurekodi</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Kinasa sauti tayari kinatumika</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">Kurekodi kumezimwa. Washa hapa %s</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Anza</string>\n  <string name=\"activity_not_found\">Hakuna shughuli ya kushughulikiwa: %s</string>\n  <string name=\"no_app\">Ombi lilioombwa halipo. Tafadhali ingiza kwa Kuandika.</string>\n  <string name=\"null_intent_value\">Programu ya nje haijatoa taarifa iliyotarajiwa.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Fungua URL</string>\n  <string name=\"capture_osm\">Fungua OpenMapKit</string>\n  <string name=\"recapture_osm\">Rudia kuweka taarifa za OSM</string>\n  <string name=\"edited_osm_file\">Faili la OSM XML lililohaririwa:</string>\n  <string name=\"invalid_osm_data\">Kuna kitu kimeenda vibaya. Hatujapata takwimu sahihi ya OSM.</string>\n  <string name=\"alert\">Tahadhari</string>\n  <string name=\"install_openmapkit\">Tafadhali sakinisha OpenMapKit!</string>\n  <string name=\"get_barcode\">Pata barcode</string>\n  <string name=\"replace_barcode\">Badili barcode</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Panga msimbo pau ndani ya mstatili</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Fungua faili</string>\n  <string name=\"rank_items\">Pangilia vitu</string>\n  <string name=\"number_picker_title\">Kichaguzi cha nambari</string>\n  <string name=\"invalid_range_widget\">Widget hii  ina vigezo batili!</string>\n  <string name=\"svg_file_does_not_exist\">Kabrasha la SVG halipo</string>\n  <string name=\"capture_image\">Chukua picha</string>\n  <string name=\"choose_image\">Chagua picha</string>\n  <string name=\"selected_invalid_image\">Faili lillilochaguliwa sio picha sahihi</string>\n  <string name=\"take_picture_instruction\">Bofya kioo kuchukua picha</string>\n  <string name=\"error_front_camera_unavailable\">Kamera ya mbele haipo katika kifaa hiki</string>\n  <string name=\"camera_error\">Kuna shida/tatizo katika kuchukua picha</string>\n  <string name=\"camera_failed_to_initialize\">Haikuweza kufungua kamera</string>\n  <string name=\"annotate_image\">Elezea picha</string>\n  <string name=\"gif_not_supported\">faili za Gif hazikubaliwi</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Hifadhi na funga</string>\n  <string name=\"signature_capture\">Chukua sahihi</string>\n  <string name=\"sign_button\">Pata sahihi</string>\n  <string name=\"markup_image\">Teua picha</string>\n  <string name=\"draw_image\">Chora picha</string>\n  <string name=\"reset_image\">Rekebisha upya</string>\n  <string name=\"set_color\">Weka rangi</string>\n  <string name=\"capture_video\">Rekodi video</string>\n  <string name=\"choose_video\">Chagua video</string>\n  <string name=\"play_video\">Cheza video</string>\n  <string name=\"choose_file\">Chagua faili</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">Futa</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Rekodi sauti</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Chagua sauti</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">Simamisha</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">Endelea</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">Futa faili ?</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">Inahifadhi file</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">Hitilafu ilitokea wakati wa kuunganisha midia:%s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Chagua thamani</string>\n  <string name=\"select_answer\">Chagua jibu</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Hariri thamani</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Hakuna thamani iliyochaguliwa</string>\n  <string name=\"selected_title\">Chaguliwa:</string>\n  <string name=\"trigger\">Sawa. tafadhali endelea.</string>\n  <string name=\"ext_file_no_data_error\">Faili halina taarifa!</string>\n  <string name=\"ext_conflicting_columns_error\">Safu%simemechi!</string>\n  <string name=\"ext_sortBy_numeric_error\">Mpangilio wako katika safu lazima ujumuishe nambari tu. Nambari zilizojichanganya zilikuwa \\'%s\\'. </string>\n  <string name=\"ext_search_wrong_arguments_error\">Makosa ya kisintaksia kwenye kutafuta (): Inahitaji hoja 1,4 au 6.</string>\n  <string name=\"ext_search_wrong_function_error\">Makosa ya kisintaksia kwenye kutafuta (): Haijafahamika \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Makosa ya kisintaksia kwenye kutafuta(): \\' %s\\' haikutathiminiwa kama ni kazi.</string>\n  <string name=\"ext_search_return_error\">Mwongozo wa kutafuta umerejesha kitu cha aina \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Imeshindwa kuingiza  taarifa kutoka %1$s. Sababu: %2$s</string>\n  <string name=\"ext_import_progress_message\">Uwekaji wa takwimu wa awali kutoka \\'%1$s\\', tafadhali subiri…%2$s</string>\n  <string name=\"ext_import_cancelled_message\">Usomaji data umekatishwa!</string>\n  <string name=\"ext_import_finalizing_message\">Umaliziaji wa kuweka takwimu za awali…</string>\n  <string name=\"ext_import_completed_message\">Usomaji wa taarifa umekamilika!</string>\n  <string name=\"ext_not_initialized_error\">Taarifa milikishi za nje hazijaanzishwa.</string>\n  <string name=\"ext_import_csv_missing_error\">Taarifa za nje za%1$shazijaingizwa Inawezekana umesahau kujumuisha %2$s.csv file katika fomu yako</string>\n  <string name=\"ext_search_generic_error\">Makosa ya kimpangilio katika kazi ya utafutaji() :%s</string>\n  <string name=\"ext_assign_value_error\">Haiwezi tambua thamani katika \\'%s\\'.</string>\n  <string name=\"parser_exception\">Isipokuwa ya XPathparser: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Rekodi muelekeo</string>\n  <string name=\"replace_bearing\">Badilisha muelekeo</string>\n  <string name=\"getting_bearing\">Inatafuta muelekeo</string>\n  <string name=\"accept_bearing\">Rekodi muelekeo</string>\n  <string name=\"bearing\">Uelekeo: \\\"Bearing\\\"%.3f</string>\n  <string name=\"direction\">Uelekeo: \\\"Direction\\\"%s</string>\n  <string name=\"bearing_lack_of_sensors\">Uelekeo hauwezi kukusanywa: kifaa hakina accelerometer, magnetic field sensor au vyote.</string>\n  <string name=\"select_date\">Chagua tarehe</string>\n  <string name=\"select_time\">Chagua muda</string>\n  <string name=\"no_date_selected\">Hakuna tarehe iliyochaguliwa</string>\n  <string name=\"no_time_selected\">Hakuna muda uliochaguliwa</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safari</string>\n  <string name=\"islamic_month_3\">Rabi\\'al -awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\'al -athani</string>\n  <string name=\"islamic_month_5\">Jumada al -awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-tani</string>\n  <string name=\"islamic_month_7\">Rajabu</string>\n  <string name=\"islamic_month_8\">Shabani</string>\n  <string name=\"islamic_month_9\">Ramadhani</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhul al-ahmad</string>\n  <string name=\"islamic_month_12\">Dhul al hijja</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s ( %2$s )</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Faili: %s ni batili.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Faili: %s halionekani.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">Utambulisho</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">Sababu</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">Hifadhi</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS ramani za kitaifa za topo</string>\n  <string name=\"openmap_usgs_sat\">USGS National Map Hybrid</string>\n  <string name=\"openmap_usgs_img\">USGS National Map Imagery</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Ramani msingi</string>\n  <string name=\"basemap_source\">Chanzo</string>\n  <string name=\"map_style_label\">%s aina ya ramani</string>\n  <string name=\"streets\">Mitaa</string>\n  <string name=\"terrain\">Ardhi ya eneo</string>\n  <string name=\"hybrid\">Mseto</string>\n  <string name=\"satellite\">Satelaiti</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Mwanga</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Giza</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">Nje</string>\n  <string name=\"topographic\">Picha mpya</string>\n  <string name=\"google_play_services_error_occured\">Upatikanaji wa ramani za google umeshindikana. Je, huduma ya Google Play imesakinishwa kwenye simu?</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Jambo la Giza</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">Rekodi pointi</string>\n  <string name=\"provider_disabled_error\">Samahani, kionesha mahali hakijawezeshwa!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">Magh %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">Mash %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">Kus %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">Kas %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Njia ya pembejeo</string>\n  <string name=\"start\">Anza</string>\n  <string name=\"enable_gps\">Wezesha GPS</string>\n  <string name=\"gps_enable_message\">GPS imezimwa kwenye kifaa chako. Ungependa kutumia?</string>\n  <string name=\"geo_clear_warning\">Umbo lishatengenezwa. Ungependa kufuta Umbo?</string>\n  <string name=\"geo_exit_warning\">Sitisha mabadiliko na rudi kwenye ODK?</string>\n  <string name=\"polygon_validator\">Lazima uwe na angalau  nukta 3 ili kutengeneza poligoni</string>\n  <string name=\"polyline_validator\">Ni lazima uwe angalau na pointi 2 kutengeneza Mistari </string>\n  <string name=\"clear\">Futa</string>\n  <string name=\"placement_mode\">Kuwekwa kwa kugonga</string>\n  <string name=\"manual_mode\">Rekodi sauti kawaida</string>\n  <string name=\"automatic_mode\">Rekodi sauti automatiki</string>\n  <string name=\"recording_interval\">Muda wa kurekodi</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <string name=\"accuracy_requirement\">Sharti sahihi</string>\n  <string name=\"none\">Hakuna</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"collection_status_paused\">Vidokezo vilivyoingizwa: %d</string>\n  <string name=\"geopoint_no_draggable_instruction\">Bofya kitufe cha kuongeza alama.</string>\n  <string name=\"discard\">Sitisha</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">Simamisha</string>\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Inatuma fomu</string>\n  <string name=\"noselect_error\">Samahani, hujachagua fomu zozote!</string>\n  <string name=\"no_connection\">Hakuna mtandao unaopatikana</string>\n  <string name=\"sending_items\">Inatuma %1$s kati ya %2$s fomu(s)</string>\n  <string name=\"send_selected_data\">Tuma zilizochaguliwa</string>\n  <string name=\"server_auth_credentials\">Jina la mtumiaji au nenosiri kwenye seva si sahihi:%s </string>\n  <string name=\"server_requires_auth\">Seva inahitaji uthibitishaji</string>\n  <string name=\"no_forms_uploaded\">Pole, hakuna fomu Iliyopakiwa.</string>\n  <string name=\"upload_results\">Matokeo ya kupakiwa</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Badili muonekano</string>\n  <string name=\"show_sent_and_unsent_forms\">Onyesha fomu zilizotumwa na zisizotumwa </string>\n  <string name=\"show_unsent_forms\">Onyesha fomu zisizotumwa</string>\n  <string name=\"send_in_progress\">Utumaji unaendelea kwa nyuma, jaribu tena baada ya muda mfupi</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Onyesha upya</string>\n  <string name=\"download\">Chukuwa kilicho chaguliwa</string>\n  <string name=\"downloading_data\">Inaunganisha kwenye seva</string>\n  <string name=\"fetching_file\">Kupata%1$s. \\ fomu %2$s\\ kati ya%3$s fomu (s).</string>\n  <string name=\"form_download_progress\">%1$s. inapata kabrasha za media: %2$s kati ya %3$s</string>\n  <string name=\"version\">Toleo:</string>\n  <string name=\"form_updates_available\">Matolea mapya ya fomu yanapatikana</string>\n  <string name=\"newer_version_of_a_form_info\">Hili ni toleo jipya la fomu</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Fomu zilizohifadhiwa</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Fomu tupu</string>\n  <string name=\"delete_confirm\">Futa %s fomu(s)?</string>\n  <string name=\"delete_file\">Futa iliyochaguliwa</string>\n  <string name=\"delete_no\">Usifute</string>\n  <string name=\"delete_yes\">Futa fomu</string>\n  <string name=\"form_delete_message\">Inafuta fomu zilizochaguliwa</string>\n  <string name=\"file_deleted_ok\">%s fomu(s) zimefanikiwa kufutwa!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Seva</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">Kiolesura \\\"interface\\\" cha mtumiaji</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">Ramani</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Usimamizi wa fomu</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Utambulisho wa mtumiaji na kifaa </string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">Usimamizi wa mradi</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Mipangilio ya seva</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL ya seva</string>\n  <string name=\"url_error\">Samahani, URL ni batili!</string>\n  <string name=\"username\">Jina la mtumiaji </string>\n  <string name=\"change_username\">Jina la mtumiaji wa ODK</string>\n  <string name=\"password\">Nenosiri</string>\n  <string name=\"change_password\">Nenosiri la ODK</string>\n  <string name=\"password_error_whitespace\">Nafasi za wazi kabla au baada ya nywila \\\"neno la siri\\\" hairuhusiwi</string>\n  <string name=\"username_error_whitespace\">Nafasi za wazi kabla au baada ya jina la mtumiaji haziruhusiwi</string>\n  <string name=\"app_theme\">Muonekano</string>\n  <string name=\"language\">Lugha</string>\n  <string name=\"use_device_language\">Tumia lugha ya kifaa</string>\n  <string name=\"font_size\">Ukubwa wa maandishi</string>\n  <string name=\"change_font_size\">Ukubwa wa maandishi</string>\n  <string name=\"font_size_extra_large\">Kubwa sana</string>\n  <string name=\"font_size_large\">Kubwa</string>\n  <string name=\"font_size_medium\">Kati</string>\n  <string name=\"font_size_small\">Ndogo</string>\n  <string name=\"font_size_extra_small\">Ndogo sana</string>\n  <string name=\"navigation\">Tafuta</string>\n  <string name=\"swipe_navigation\">Tumia upitishaji wa mlalo\\\"horizontal swipes\\\"</string>\n  <string name=\"buttons_navigation\">Tumia vitufe vya mbele/yuma</string>\n  <string name=\"swipe_buttons_navigation\">Tumia upitishaji na vitufe</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Inayofuata</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Rudi</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Tolea jipya la fomu</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Kila dakika kumi na tano</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Kila saa</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Kila masaa sita</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Kila masaa 24</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Upakuaji otomatiki</string>\n  <string name=\"automatic_download_summary\">Pakua otomatiki matoleao mapya ya fomu</string>\n  <string name=\"hide_old_form_versions_setting_title\">Ficha matoleo ya fomu za zamani</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Toleo jipya zaidi pekee itaonekana katika kujaza fomu tupu</string>\n  <string name=\"form_submission_category\">Uwasilishaji wa fomu</string>\n  <string name=\"autosend_selector_title\">Inajituma yenyewe</string>\n  <string name=\"autosend\">Inajituma yenyewe</string>\n  <string name=\"off\">Zima</string>\n  <string name=\"wifi_autosend\">Wifi tu</string>\n  <string name=\"cellular_autosend\">Simu tu</string>\n  <string name=\"wifi_cellular_autosend\">Wifi au simu</string>\n  <string name=\"delete_after_send\">Futa baada ya kutuma</string>\n  <string name=\"delete_after_send_summary\">Inafuta fomu zilizokamilika na midia baada ya kuzituma kwenye seva</string>\n  <string name=\"form_filling_category\">Ujazaji wa fomu</string>\n  <string name=\"constraint_behavior_title\">Kuchakata kikwazo </string>\n  <string name=\"constraint_behavior\">Kikwazo uchakataji tabia</string>\n  <string name=\"constraint_behavior_on_swipe\">Thibitisha pindi unapitisha mbele</string>\n  <string name=\"constraint_behavior_on_finalize\">Hairisha uthibitisho hadi ukamilishe</string>\n  <string name=\"high_resolution_title\">Video yenye azimio la juu/kiwango kikubwa</string>\n  <string name=\"high_resolution_summary\">Ruhusu kurekodi video zenye kiwango cha juu</string>\n  <string name=\"image_size_title\">Ukubwa wa picha </string>\n  <string name=\"image_size_dialog_title\">Saizi kubwa ya upande mrefu wa picha</string>\n  <string name=\"image_size_large\">Kubwa (3072px)</string>\n  <string name=\"image_size_medium\">Kati (2048px)</string>\n  <string name=\"image_size_small\">Ndogo (1024px)</string>\n  <string name=\"image_size_very_small\">Ndogo sana (640px)</string>\n  <string name=\"image_size_original\">Ukubwa asili kutoka katika kamera (chaguo-msingi)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Onyesha muongozo wa maswali</string>\n  <string name=\"guidance_no\">hapana</string>\n  <string name=\"guidance_yes\">Ndio - ioneshwe kila mara</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Ndio - iliharibika</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">Ingiza fomu</string>\n  <string name=\"instance_sync\">Kukamilisha fomu katika kuingiza</string>\n  <string name=\"instance_sync_summary\">Inaathiri fomu zilizowekwa moja kwa moja kwenye makabrasha ya mifano.</string>\n  <string name=\"analytics_preferences\">Matumizi ya takwimu</string>\n  <string name=\"analytics\">Kusanya takwimu za matumizi bila majina</string>\n  <string name=\"form_metadata_title\">Unda taarifa za data</string>\n  <string name=\"form_metadata\">Unda taarifa za data</string>\n  <string name=\"form_metadata_summary\">Thamani hizi zitaongezwa katika fomu ambazo zina jina, barua pepe na au namba ya simu kupakuliwa kabla nafasi kumtambulisha mtu aliye wasilisha data.</string>\n  <string name=\"user_defined\">Mtumiaji ametambulika</string>\n  <string name=\"phone_number\">Namba ya simu</string>\n  <string name=\"email\">Anuwani ya barua pepe</string>\n  <string name=\"invalid_email_address\">Anwani ya barua pepe ni batili!</string>\n  <string name=\"device_defined\">Kifaa kimetambulika</string>\n  <string name=\"device_id\">Utambulisho wa kifaa</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">Haipatikani</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Ingiza nenosiri jipya</string>\n  <string name=\"admin_password\">Nenosiri la msimamizi</string>\n  <string name=\"admin_password_changed\">Nenosiri la msimamizi limebadilishwa kwa ufanisi</string>\n  <string name=\"admin_password_disabled\">Nenosiri la msimamizi halijawezeshwa</string>\n  <string name=\"admin_password_incorrect\">Samahani nenosiri sio sahihi</string>\n  <string name=\"enter_admin_password\">Ingiza nenosiri la msimamizi</string>\n  <string name=\"change_admin_password\">Badili nenosiri la msimamizi</string>\n  <string name=\"show_password\">Onyesha nenosiri</string>\n  <string name=\"found_in_main\">Ondoa alama ili kuficha kutoka kwenye orodha kuu</string>\n  <string name=\"form_entry\">Ondoa alama kuficha  kujaza fomu</string>\n  <string name=\"reset_project_settings_title\">Rekebisha upya</string>\n  <string name=\"delete_project\">Futa</string>\n  <string name=\"select_all\">Chagua zote</string>\n  <string name=\"clear_all\">Futa zote</string>\n  <string name=\"reset_settings\">Mipangilio yote (mipangilio ya ndani, mipangilio iliyohifadhiwa)</string>\n  <string name=\"reset_saved_forms\">Fomu zilizohifadhiwa (kabrasha la mfano, hazina data ya mfano)</string>\n  <string name=\"reset_blank_forms\">Fomu tupu(kabrasha la fomu,hifadhidata ya fomu,seti_vifaa vya hifadhidata)</string>\n  <string name=\"reset_layers\">Safu za ramani (Kabrasha la safu)</string>\n  <string name=\"reset_cache\">Fomu ya kuweka hifahi ya muda mfupi (.kabrasha la hifadhi la muda mfupi)</string>\n  <string name=\"reset_settings_dialog_title\">Chagua kipi cha kurekebisha</string>\n  <string name=\"reset_settings_button_reset\">Rudisha mwanzo</string>\n  <string name=\"reset_in_progress\">Kurudisha mwanzo…</string>\n  <string name=\"reset_app_state_result\">Matokeo ya kurudisha ya mwanzo</string>\n  <string name=\"reset_app_state_warning\">Taarifa zote ulizochagua zinaenda kufutika. Hutaweza kuzirudisha tena.</string>\n  <string name=\"reset_settings_result\">Mipangilio yote :: %s</string>\n  <string name=\"reset_saved_forms_result\">Fomu zilizohifadhiwa :: %s</string>\n  <string name=\"reset_blank_forms_result\">Fomu tupu :: %s</string>\n  <string name=\"reset_layers_result\">Safu za ramani :: %s</string>\n  <string name=\"reset_cache_result\">Fomu ya takwimu za muda mfupi :: %s</string>\n  <string name=\"main_menu_settings\">Mipangilio ya menyu kuu</string>\n  <string name=\"user_settings\">Mipangilio ya mtumiaji</string>\n  <string name=\"form_entry_setting\">Unda mipangilio ya kuingia</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Kusonga nyuma</string>\n  <string name=\"moving_backwards_disabled_title\">Kusonga nyuma kumesitishwa</string>\n  <string name=\"yes\">Ndiyo</string>\n  <string name=\"no\">Hapana</string>\n  <string name=\"moving_backwards_enabled_title\">Kusonga nyuma kumewezeshwa</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Kuhusu</string>\n  <string name=\"tell_your_friends\">Shirikisha wengine programu ya ODK collect</string>\n  <string name=\"tell_your_friends_msg\">Je ndugu na jamaa zako bado wanakusanya taarifa kupitia karatasi? Washirikishe programu ya ODK.</string>\n  <string name=\"leave_a_review\">Acha tathmini katika Play Store</string>\n  <string name=\"leave_a_review_msg\">Tathmini yako (tunatumaini itakuwa chanya) inaongeza nafasi ya programu hii kuonekana Pay Store</string>\n  <string name=\"odk_website\">Tembelea tovuti ya ODK</string>\n  <string name=\"odk_website_summary\">ODK inatumika kwenye ukusanyaji wa taarifa za kijamii katika mazingira yenye changamoto.</string>\n  <string name=\"odk_forum\">Jiunge na jukwaa la ODK</string>\n  <string name=\"odk_forum_summary\">Jiunge na jukwaa kupata msaada, kuomba makala,  kuchangia kanuni au kutafsiri!</string>\n  <string name=\"all_open_source_licenses\">Maktaba/leseni ya chanzo wazi</string>\n  <string name=\"all_open_source_licenses_msg\">Tumesimama kwenye mabega ya mashujaa!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"successfully_imported_settings\">Mipangilio imeingizwa kwa mafanikio</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Kiwango cha herufi kimefikia ukomo: Misimbo ya QR haziwezi kutengenezwa kwa mipangilio yote</string>\n  <string name=\"share\">Shirikisha</string>\n  <string name=\"turn_on_flashlight\">Washa mwanga</string>\n  <string name=\"turn_off_flashlight\">Zima mwanga</string>\n  <string name=\"server_password\">Nenosiri la seva</string>\n  <string name=\"include_password_dialog\">Nenosiri limejumuishwa ndani ya kanuni</string>\n  <string name=\"generate\">Zalisha</string>\n  <string name=\"invalid_qrcode\">Kanuni za QR hazijajumuisha mipangilio sahihi</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">Sawa</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Sitisha</string>\n  <string name=\"cancel_loading_form\">Sitisha</string>\n  <string name=\"cancel_location\">Sitisha</string>\n  <string name=\"do_not_change\">Sitisha</string>\n  <string name=\"canceling\">Inasitishwa</string>\n  <string name=\"success\">Mafanikio</string>\n  <string name=\"error_occured\">Kosa limetokea</string>\n  <string name=\"please_wait\">Tafadhali subiri kidogo.</string>\n  <string name=\"please_wait_long\">Tafadhali subiri. Hii itachukua dakika chache.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Imeshindwa kufuta\\'%s\\'. Tafadhali futa kabrasha na rudia kupakua fomu.</string>\n  <string name=\"fs_create_media_folder_error\">Imeshindwa tengeneza kabrasha la midia\\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Haiwezi nakili  \\'%1$s\\' zidi ya \\'%2$s\\'. Sababu:%3$s</string>\n  <string name=\"select_odk_shortcut\">Chagua njia fupi ya ODK </string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Miradi</string>\n  <string name=\"add_project\">Ongeza mradi</string>\n  <string name=\"add\">Ongeza</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <string name=\"project_name\">Jina la mradi</string>\n  <string name=\"project_color\">Rangi ya mradi</string>\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">URI  Isiyotambulika: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">URI mbaya:%s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Fomu iliyojazwa ilishafutwa</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Unepakua fomu mbili tofauti  zenye  fomu ya utambulisho sawa na toleo sawa. Labda ilikuwa ni fomu moja iliyoekwa muda tofauti au seva tofauti. Kama ni hivyo futa mojawapo. </string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Sitisha</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-sw-rKE/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Imeongezwa \\'EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Imehifadhiwa \\'EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Imekamilishwa \\'EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Imetumwa \\'EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'ilishindwa kutumwa \\'EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sort_by_name_asc\">Jina, A-Z</string>\n  <string name=\"sort_by_name_desc\">Jina, Z-A</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Scanning imemalizika. Fomu zote tayari</string>\n  <string name=\"loading_form\">Fomu inawekwa</string>\n  <string name=\"survey_loading_reading_form_message\">Fomu inasomwa taarifa</string>\n  <string name=\"survey_loading_reading_data_message\">Takwimu ya utafiti inasomwa…</string>\n  <string name=\"survey_loading_reading_csv_message\">Faili za CSV zinasomwa…</string>\n  <string name=\"parse_error\">Samahani, haiwezi kuchanganua fomu</string>\n  <string name=\"cannot_edit_completed_form\">Fomu hii haiwezi kuhaririwa ikiwa imewekwa alama kuwa imekamilika Inaweza kusimbwa kwa njia fiche.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Madiliko ambayo hayakuhifadhiwa, yameokolewa kutoka awali</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Badilisha Lugha</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Enda</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Enda Mwanzo</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Enda Mwisho</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Enda Juu</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Kikundi kinachoweza kurudiwa</string>\n  <string name=\"exit\">Toka</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Ongeza nyingine</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Hariri ilani</string>\n  <string name=\"clearanswer_confirm\">Ondoa jibu la \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Ondoa jibu</string>\n  <string name=\"clear_answer_ask\">Ondoa jibu hili?</string>\n  <string name=\"clear_answer_no\">Tamatisha</string>\n  <string name=\"discard_answer\">Futa majibu</string>\n  <string name=\"discard_group\">Ondoa kikundi</string>\n  <string name=\"delete_repeat\">Ondoa kikundi</string>\n  <string name=\"delete_repeat_ask\">Je, Ondoa kikundi hiki?</string>\n  <string name=\"delete_repeat_confirm\">Je, ondoa kikundi \\\"%s\\\" na vikundi vilivyo ndani yake?</string>\n  <string name=\"delete_repeat_no\">Futa</string>\n  <string name=\"invalid_answer_error\">Samahani, jibu hili si halali!</string>\n  <string name=\"required_answer_error\">Samahani, jibu hili linahitajika!</string>\n  <string name=\"survey_internal_error\">Hitilafu ya ndani: hatua ya amri ina kasoro.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Umefika mwisho wa %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Samahani, fomu haikuhifadhiwa!</string>\n  <string name=\"data_saved_ok\">Fomu imehifadhiwa!</string>\n  <string name=\"save_point_error\">Hitilafu imetokea kwa ncha ya urejesho: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Hifadhi mabadiliko</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Hifadhi ya fomu</string>\n  <string name=\"survey_saving_validating_message\">Majibu yanadhibitishwa…</string>\n  <string name=\"survey_saving_collecting_message\">Takwimu inakusanywa…</string>\n  <string name=\"survey_saving_saving_message\">Takwimu inahifadhiwa kwa kadi ya SD…</string>\n  <string name=\"survey_saving_finalizing_message\">Tamatisho la takwimu kwa kadi ya SD…</string>\n  <string name=\"survey_saving_encrypting_message\">usimbaji fiche wa takwimu…</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">zindua</string>\n  <string name=\"activity_not_found\">Hakuna shughuli iliyopatikana kushughulikia: %s</string>\n  <string name=\"no_app\">Application unayotaka haipo. Tafadhali wasilisha matokeo</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Url wazi</string>\n  <string name=\"capture_osm\">Anzisha OpenMapkit</string>\n  <string name=\"get_barcode\">Weka Barcode</string>\n  <string name=\"replace_barcode\">Badilisha Barcode</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Piga Picha</string>\n  <string name=\"choose_image\">Chagua picha</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Hifadhi na ufunge</string>\n  <string name=\"sign_button\">Kusanya saini</string>\n  <string name=\"markup_image\">ghafi picha</string>\n  <string name=\"draw_image\">chora picha</string>\n  <string name=\"reset_image\">fanya upya</string>\n  <string name=\"set_color\">weka rangi</string>\n  <string name=\"capture_video\">Rekodi kanda</string>\n  <string name=\"choose_video\">Chagua video</string>\n  <string name=\"play_video\">Cheza video</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Rekodi Sauti</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Chagua sauti</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Chagua jibu</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Iliyochaguliwa</string>\n  <string name=\"trigger\">Sawa. Tafadhali Endelea.</string>\n  <string name=\"ext_file_no_data_error\">Faili haina ujumbe</string>\n  <string name=\"ext_conflicting_columns_error\">Safuwima %s zinalingana!</string>\n  <string name=\"ext_import_generic_error\">Data haingeagizwa kutoka %1$s. Sababu: %2$s</string>\n  <string name=\"ext_import_progress_message\">Data inapakia kutoka \\'%1$s\\', tafadhali ngoja… %2$s</string>\n  <string name=\"ext_import_finalizing_message\">Takwimu ya pakio inatamatishwa…</string>\n  <string name=\"ext_import_completed_message\">Usomaji wa takwimu umemalizika</string>\n  <string name=\"ext_assign_value_error\">Safuwima %s zinalingana!</string>\n  <string name=\"get_bearing\">Rekodi Eneo/Mahali</string>\n  <string name=\"replace_bearing\">Badilisha Eneo/Mahali</string>\n  <string name=\"getting_bearing\">Inapakia eneo</string>\n  <string name=\"accept_bearing\">Rekodi Eneo/Mahali</string>\n  <string name=\"select_date\">Changua tarehe</string>\n  <string name=\"select_time\">Chagua wakati</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Faili: %s si sawa.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Faili: %s haipo.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"streets\">Barabara</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Samahani, viwekaji mahali/eneo vimezimwa!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"start\">Anza</string>\n  <string name=\"clear\">Futa</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Kutuma Fomu</string>\n  <string name=\"noselect_error\">Samahani, hujachagua fomu yoyote!</string>\n  <string name=\"no_connection\">Hakuna mtandao unapatikana!</string>\n  <string name=\"sending_items\">kutuma %1$s kati ya fomu() %2$s</string>\n  <string name=\"send_selected_data\">Tuma iliyochaguliwa</string>\n  <string name=\"server_requires_auth\">Server inahitaji uthibitishaji</string>\n  <string name=\"no_forms_uploaded\">Samahani, hakuna fomu ilipakiwa.</string>\n  <string name=\"upload_results\">Pakia matokeo</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Badilisha mtazamo</string>\n  <string name=\"show_sent_and_unsent_forms\">Onyesha Fomu zilizotumwa na ambazo hazijatumwa</string>\n  <string name=\"show_unsent_forms\">Onyesha fomu ambazo hazijatumwa</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Upya</string>\n  <string name=\"download\">Pata iliyochaguliwa</string>\n  <string name=\"downloading_data\">inawasiliana na server</string>\n  <string name=\"form_download_progress\">%1$s. Kupata faili za vyombo vya habari: %2$s kati ya %3$s</string>\n  <string name=\"version\">Toleo:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Fomu zilizo hifadhiwa</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Fomu tupu</string>\n  <string name=\"delete_confirm\">Futa/Ondoa %s Fomu (nyingi)?</string>\n  <string name=\"delete_file\">Futa/Ondoa zilizochaguliwa</string>\n  <string name=\"delete_no\">USIFUTE</string>\n  <string name=\"delete_yes\">Futa/Ondoa Fomu</string>\n  <string name=\"file_deleted_ok\">Fomu() %s zimefutwa vilivyo!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Mazingira ya Server</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">anwani ya wavuti</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"url_error\">Samahani, URL si halali!</string>\n  <string name=\"username\">Jina la mtumiaji</string>\n  <string name=\"change_username\">Jina la mtumiaji</string>\n  <string name=\"password\">Thibitisho</string>\n  <string name=\"change_password\">Nywila ya ODK</string>\n  <string name=\"language\">Lugha</string>\n  <string name=\"font_size\">Ukubwa wa fonti za nakala</string>\n  <string name=\"change_font_size\">Ukubwa wa fonti za nakala</string>\n  <string name=\"font_size_extra_large\">Kubwa zaidi</string>\n  <string name=\"font_size_large\">Kubwa</string>\n  <string name=\"font_size_medium\">Katikati</string>\n  <string name=\"font_size_small\">Ndogo</string>\n  <string name=\"font_size_extra_small\">Ndogo zaidi</string>\n  <string name=\"navigation\">uelekezaji</string>\n  <string name=\"swipe_navigation\">Teleza kwa upande</string>\n  <string name=\"buttons_navigation\">Tumia vibonyezo vya mbele na nyuma</string>\n  <string name=\"swipe_buttons_navigation\">Telezesha ama tumia vibonyezo</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Ijayo</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"constraint_behavior\">Uchakatishi wa dhibitisho</string>\n  <string name=\"constraint_behavior_on_swipe\">Dhibitisha ukisonga mbele</string>\n  <string name=\"constraint_behavior_on_finalize\">Chelewesha fomu hadi itamatishwe</string>\n  <string name=\"high_resolution_summary\">Ezesha kurekodi video za mwonekano wa hali ya juu</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"phone_number\">Nambari ya simu</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Weka password mpya</string>\n  <string name=\"admin_password_changed\">Password ya msimamizi imebadilishwa vyema</string>\n  <string name=\"admin_password_disabled\">Password ya msimamizi imefungwa</string>\n  <string name=\"admin_password_incorrect\">Samahani, password si sawa</string>\n  <string name=\"enter_admin_password\">Weka password ya msimamizi</string>\n  <string name=\"reset_project_settings_title\">fanya upya</string>\n  <string name=\"reset_settings_button_reset\">fanya upya</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">Sawa</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Tamatisha</string>\n  <string name=\"cancel_loading_form\">Tamatisha</string>\n  <string name=\"cancel_location\">Tamatisha</string>\n  <string name=\"do_not_change\">Futa</string>\n  <string name=\"success\">Mafanikio</string>\n  <string name=\"error_occured\">Hitilifau ilitokea</string>\n  <string name=\"please_wait\">Tafadhali subiri dakika chache.</string>\n  <string name=\"please_wait_long\">Tafadhali subiri. Hii inaweza kuchukua dakika chache.</string>\n  <string name=\"fs_create_media_folder_error\">Faili \\'%s\\' la midia halitengenezeki.</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Umepakua fomu mbili tofauti zinayo ID na version moja. Labda ilikuwa fomu moja ikipakiwa</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-te/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">వెర్షన్: %s</string>\n  <string name=\"id_number\">ID:%s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'చేర్చిన తేదీ\\' EEE, MMM dd, yyyy \\'సమయం\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'సేవ్ చేసిన తేదీ\\' EEE, MMM dd, yyyy \\'సమయం\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'ఖరారు చేసిన తేదీ\\' EEE, MMM dd, yyyy \\'సమయం\\' HH:mm </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'పంపిన తేదీ\\' EEE, MMM dd, yyyy \\'సమయం\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'పంపడం విఫలమైన తేదీ\\' EEE, MMM dd, yyyy \\'సమయం\\' HH:mm </string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'తొలగించిన తేదీ\\' EEE, MMM dd, yyyy \\'సమయం\\' HH:mm</string>\n  <string name=\"submission_deleted\">సబ్మిషన్  తొలగించబడింది</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'సవరించిన తేదీ\\' EEE, MMM dd, yyyy \\'సమయం\\' HH:mm </string>\n  <string name=\"encrypted_form\">గుప్తీకరించిన రూపం</string>\n  <string name=\"deleted_form\">తొలగించిన ఫారమ్</string>\n  <string name=\"sort_the_list\">జాబితాను క్రమబద్ధీకరించండి</string>\n  <string name=\"sort_by\">క్రమబద్ధీకరణ </string>\n  <string name=\"sort_by_name_asc\">పేరు, A-Z</string>\n  <string name=\"sort_by_name_desc\">పేరు, Z-A</string>\n  <string name=\"sort_by_date_desc\">తేదీ, కొత్తది ముందు</string>\n  <string name=\"sort_by_date_asc\">తేదీ, పాతది ముందు</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">జాబితాను ఫిల్టర్ చేయండి</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">వెతకండి</string>\n  <string name=\"not_granted_permission\">మీరు మొదులు పెట్టడానికి ప్రయత్నిస్తున్న పనికి అనుమతి అవసరం. అది మీకు ఇవ్వబడలేదు.</string>\n  <string name=\"storage_runtime_permission_denied_title\">నిల్వ ఉంచటానికి అనుమతులు</string>\n  <string name=\"storage_runtime_permission_denied_desc\">ఈ అనుమతులు లేకుండా మీ ఫారమ్‌లను యాక్సెస్ చేయలేరు లేదా సమాధానాలను సేవ్ చేయలేరు. మీరు వాటిని మంజూరు చేయడానికి సిద్ధంగా ఉన్నప్పుడు ఆప్ ని తిరిగి తెరవండి.</string>\n  <string name=\"location_runtime_permissions_denied_title\">ప్రదేశ అనుమతులు</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">కెమెరా అనుమతి</string>\n  <string name=\"camera_runtime_permission_denied_desc\">ఈ అనుమతి లేకుండా కెమెరాను యాక్సెస్ చేయలేరు. మీరు మంజూరు చేయడానికి సిద్ధంగా ఉన్నప్పుడు దయచేసి మళ్ళీ ప్రయత్నించండి.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">ఆడియో అనుమతి రికార్డ్ చేయండి</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">ఈ అనుమతి లేకుండా మైక్రోఫోన్‌ను యాక్సెస్ చేయలేరు. మీరు మంజూరు చేయడానికి సిద్ధంగా ఉన్నప్పుడు దయచేసి మళ్ళీ ప్రయత్నించండి.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$dఫారంలు జోడించబడ్డాయి.</string>\n  <string name=\"finished_disk_scan\">స్కానింగ్ పూర్తయింది. అన్ని ఫారంలు లోడ్ చేయబడ్డాయి.</string>\n  <string name=\"xform_parse_error\">XForm పార్స్ లోపం%1$s:%2$sమిస్సింగ్ లేదా చెల్లదు.</string>\n  <string name=\"loading_form\">ఫారం లోడ్ అవుతోంది </string>\n  <string name=\"survey_loading_reading_form_message\">ఫారమ్ నిర్వచనం చదువుతోంది </string>\n  <string name=\"survey_loading_reading_data_message\">సర్వే డేటాను చదువుతోంది …</string>\n  <string name=\"survey_loading_reading_csv_message\">CSV ఫైళ్ళను చదువుతోంది… </string>\n  <string name=\"parse_error\">క్షమించండి, ఫారంను అన్వయించలేకపోయాము.</string>\n  <string name=\"loading_form_failed\">ఫారమ్‌ను లోడ్ చేస్తున్నప్పుడు లోపం సంభవించింది. దయచేసి మళ్ళీ ప్రయత్నించండి.</string>\n  <string name=\"cannot_edit_completed_form\">ఈ ఫారం గుర్తించబడిన తర్వాత దాన్ని సవరించడం సాధ్యం కాదు. ఇది ఎన్క్రిప్ట్ అయి ఉండవచ్చు.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">సేవ్ చేసినప్పటి  నుండి సేవ్ చేయని మార్పులు రికవర్ అయ్యాయి!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">భాషని మార్చు</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">ప్రాంప్ట్ కు వెళ్ళండి</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">మొదటికి వెళ్లు </string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">చివరికి వెళ్లు </string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">పైకి వెళ్లు </string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">గ్రూపు</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">పునరావృత గ్రూపు</string>\n  <string name=\"exit\">నిష్క్రమించు</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">%s జోడించు</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">జోడించు</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">మరొకదాన్ని జోడించండి</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">ప్రాంప్ట్ సవరించండి</string>\n  <string name=\"clearanswer_confirm\">\\\"%s\\\" కు ప్రతిస్పందనను తొలగించాలా?</string>\n  <string name=\"clear_answer\">ప్రతిస్పందనను  తొలగించు</string>\n  <string name=\"clear_answer_ask\">ఈ ప్రతిస్పందనను తొలగించాలా?</string>\n  <string name=\"clear_answer_no\">రద్దు చేయి</string>\n  <string name=\"discard_answer\">ప్రతిస్పందనను  తొలగించు</string>\n  <string name=\"discard_group\">గ్రూపును తీసెయ్యి </string>\n  <string name=\"delete_repeat\">గ్రూపును తీసెయ్యి </string>\n  <string name=\"delete_repeat_ask\">ఈ గ్రూపు ని తీసివేయాలా?</string>\n  <string name=\"delete_repeat_confirm\">%s గ్రూపు, దీనికి సంబందించిన మిగిలిన గ్రూపులన్నిటిని తీసివేయాలా?</string>\n  <string name=\"delete_repeat_no\">రద్దు చేయి</string>\n  <string name=\"invalid_answer_error\">క్షమించండి, ఈ ప్రతిస్పందన వాలీడ్ కాదు </string>\n  <string name=\"required_answer_error\">క్షమించండి, ఈ ప్రతిస్పందన తప్పనిసరి నింపాలి</string>\n  <string name=\"invalid_space_in_answer_singular\">హెచ్చరిక: సూచించిన విలువ%s లో స్పేసెస్ ఉన్నాయి</string>\n  <string name=\"invalid_space_in_answer_plural\">హెచ్చరిక: సూచించిన %s విలువలలో స్పేసెస్ ఉన్నాయి</string>\n  <string name=\"too_complex_form\">ఈ పరికరం కోసం ఈ ఫారం చాలా క్లిష్టంగా ఉంటుంది. వ్యక్తీకరణలను సరళీకృతం చేయడానికి ప్రయత్నించండి లేదా ఫోరమ్‌లో సహాయం కోసం అడగండి.</string>\n  <string name=\"survey_internal_error\">అంతర్గత లోపం: ప్రాంప్ట్ ప్రారంభించే దశ విఫలమైంది.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">మీరు%sచివరిలో ఉన్నారు.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">క్షమించండి, ఫారం సేవ్ అవ్వలేదు</string>\n  <string name=\"data_saved_ok\">ఫారం విజయవంతంగా సేవ్ చేయబడింది!</string>\n  <string name=\"save_point_error\">రికవరీ పాయింట్‌ను సేవ్ చేయడంలో లోపం ఉంది: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">మార్పులను సేవ్ చెయ్యి</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">ఫారం సేవ్ చేయబడుతుంది</string>\n  <string name=\"survey_saving_validating_message\">సమాధానాలను ధృవీకరిస్తోంది…</string>\n  <string name=\"survey_saving_collecting_message\">డేటాను సేకరిస్తోంది…</string>\n  <string name=\"survey_saving_saving_message\">SD కార్డుకు సేవ్ చేస్తోంది…</string>\n  <string name=\"survey_saving_finalizing_message\">SD కార్డుకు ఖరారు చేస్తోంది…</string>\n  <string name=\"survey_saving_encrypting_message\">డేటాను గుప్తీకరిస్తోంది…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">ఈ ఉదాహరణకి సరిగ్గా ఒక రికార్డ్ కాదు!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">ఈ jr_form_id కి ఒక ఖాళీ ఫారం సరిగ్గా సరిపోలలేదు.</string>\n  <string name=\"no_form_id_specified\">ఎటువంటి ఫారం గుర్తింపు సంఖ్య పేర్కొనబడలేదు???</string>\n  <string name=\"phone_does_not_support_rsa\">మీ ఫోన్ ఎటువంటి RSA గుప్తీకరణకు మద్దతు ఇవ్వదు.</string>\n  <string name=\"invalid_rsa_public_key\">RSA పబ్లిక్ కీ చెల్లదు.</string>\n  <string name=\"encryption_error_message\">%sఫారం ఖరారైనట్లు సేవ్ చేయబడలేదు.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">ప్రారంభం</string>\n  <string name=\"activity_not_found\">చెయ్యడానికి ఏమి యాక్టివిటీ  లేదు : %s</string>\n  <string name=\"no_app\">అభ్యర్థించిన అప్లికేషన్ లేదు. దయచేసి రీడింగ్ మాన్యువల్‌గా నమోదు చేయండి.</string>\n  <string name=\"null_intent_value\">ఎక్స్టర్నల్ అప్లికేషన్ ఆశించిన సమాచారాన్ని అందించలేదు.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Url  ఓపెన్ చెయ్యి  </string>\n  <string name=\"capture_osm\">ఓపెన్‌మ్యాప్‌కిట్‌ను ప్రారంభించండి</string>\n  <string name=\"recapture_osm\">ఓఎస్ఎం టాగింగ్‌ను మరలా  చేయండి</string>\n  <string name=\"edited_osm_file\">సవరించబడిన OSM XML ఫైల్:</string>\n  <string name=\"invalid_osm_data\">ఏదో తప్పు జరిగింది. సరైన OSM డేటా రాలేదు.</string>\n  <string name=\"alert\">హెచ్చరిక</string>\n  <string name=\"install_openmapkit\">దయచేసి OpenMapKit ని ఇన్‌స్టాల్ చేయండి!</string>\n  <string name=\"get_barcode\">బార్‌కోడ్ పొందండి</string>\n  <string name=\"replace_barcode\">బార్‌కోడ్‌ను మార్చండి</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">బార్‌కోడ్‌ను దీర్ఘచతురస్రం లోపల ఉంచండి</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">ఫైలును తెరవండి</string>\n  <string name=\"rank_items\">ర్యాంక్ అంశాలు</string>\n  <string name=\"number_picker_title\">నెంబర్ ను ఎంచుకునేది </string>\n  <string name=\"invalid_range_widget\">ఈ విడ్జెట్ లో చెల్లని ప్రాతిపదికలు కలిగి ఉంది!</string>\n  <string name=\"svg_file_does_not_exist\">SVG ఫైల్ ఉనికిలో లేదు!</string>\n  <string name=\"capture_image\">ఫోటో తియ్యి</string>\n  <string name=\"choose_image\">ఫోటో ని ఎంచుకోండి</string>\n  <string name=\"selected_invalid_image\">ఎంచుకున్న ఫైల్ చెల్లుబాటు అయ్యే చిత్రం కాదు</string>\n  <string name=\"take_picture_instruction\">ఫోటో తీయడానికి స్క్రీన్ పైన నొక్కండి</string>\n  <string name=\"error_front_camera_unavailable\">ముందు వైపు కెమెరా ఈ పరికరంలో లేదు</string>\n  <string name=\"annotate_image\">ఫోటో మీద వ్యాఖ్యానం వ్రాయండి </string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">సేవ్ చేసి క్లోజ్ చెయ్యండి </string>\n  <string name=\"signature_capture\">సంతకం పెట్టండి</string>\n  <string name=\"sign_button\">సంతకాన్ని సేకరించండి</string>\n  <string name=\"markup_image\">మార్కప్ చిత్రం</string>\n  <string name=\"draw_image\">స్కెచ్ చిత్రం </string>\n  <string name=\"reset_image\">రీసెట్</string>\n  <string name=\"set_color\">రంగును సెట్ చేయండి</string>\n  <string name=\"capture_video\">వీడియో రికార్డు చెయ్యి </string>\n  <string name=\"choose_video\">వీడియోను ఎంచుకోండి</string>\n  <string name=\"play_video\">వీడియో ప్లే చెయ్యి</string>\n  <string name=\"choose_file\">ఫైల్‌ను ఎంచుకోండి</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">మాటలు రికార్డు చెయ్యి </string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">సౌండ్ ఎంచుకోండి</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">మీడియాను అటాచ్ చేసేటప్పుడు ఎర్రర్ వచ్చింది:%s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">ఎదేని విలువ ఎంచుకోండీ</string>\n  <string name=\"select_answer\">సమాధానం ఎంచుకోండి</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">విలువ మార్చండి</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">విలువ ఎంచుకోలేదు</string>\n  <string name=\"selected_title\">ఎంచుకోబడింది:</string>\n  <string name=\"trigger\">ఓకే. దయచేసి కొనసాగించండి.</string>\n  <string name=\"ext_file_no_data_error\">ఫైల్‌లో డేటా లేదు!</string>\n  <string name=\"ext_conflicting_columns_error\">నిలువు వరుసలు %s మ్యాచ్!</string>\n  <string name=\"ext_sortBy_numeric_error\">మీ సార్ట్‌బై కాలమ్‌లో సంఖ్యలు మాత్రమే ఉండాలి. వైరుధ్య విలువ \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">సెర్చ్() ఫంక్షన్‌లో సింటాక్స్ లోపం: ఫంక్షన్‌కు 1, 4 లేదా 6 ఆర్గ్యుమెంట్‌లు అవసరం.</string>\n  <string name=\"ext_search_wrong_function_error\">సెర్చ్ () ఫంక్షన్‌లో సింటాక్స్ లోపం: గుర్తించబడని ఫంక్షన్ \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">సెర్చ్ () ఫంక్షన్‌లో సింటాక్స్ లోపం: \\'%s\\' ఫంక్షన్‌గా అంచనా వేయబడలేదు.</string>\n  <string name=\"ext_search_return_error\">సెర్చ్ హాండ్లర్ రిటర్న్డ్ ఆబ్జెక్ట్ అఫ్ టైపు %s</string>\n  <string name=\"ext_import_generic_error\">%1$sనుండి డేటాను ఇంపోర్ట్ చేయలేకపోయింది. కారణము:%2$s</string>\n  <string name=\"ext_import_progress_message\">\\'%1$s\\' నుండి డేటాను ముందే లోడ్ చేస్తోంది, దయచేసి వేచి ఉండండి…%2$s</string>\n  <string name=\"ext_import_cancelled_message\">డేటాను చదవడం రద్దు చేయబడింది!</string>\n  <string name=\"ext_import_finalizing_message\">ముందుగా లోడ్ చేసిన డేటాను ఖరారు చేస్తోంది…</string>\n  <string name=\"ext_import_completed_message\">డేటా చదవడం పూర్తయింది!</string>\n  <string name=\"ext_not_initialized_error\">ఎక్స్టర్నల్ డేటా మేనేజర్ ప్రారంభించబడలేదు.</string>\n  <string name=\"ext_import_csv_missing_error\">%1$sకోసం బాహ్య డేటా దిగుమతి చేయబడలేదు. బహుశా %2$s.CSV ఫైల్‌ను మీ ఫారమ్‌తో చేర్చడం మర్చిపోయారా?</string>\n  <string name=\"ext_search_generic_error\">సెర్చ్() ఫంక్షన్‌లో సింటాక్స్ లోపం:%s</string>\n  <string name=\"ext_assign_value_error\">\\'%s\\' వద్ద విలువను కేటాయించలేరు.</string>\n  <string name=\"parser_exception\">XPathParser మినహాయింపు: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">దిశ ని రికార్డు చేయండి </string>\n  <string name=\"replace_bearing\">రీప్లేస్ బేరింగ్ </string>\n  <string name=\"getting_bearing\">లోడింగ్ బేరింగ్ </string>\n  <string name=\"accept_bearing\">దిశ ని రికార్డు చేయండి </string>\n  <string name=\"bearing\">బేరింగ్:%.3f</string>\n  <string name=\"direction\">దిశ:%s</string>\n  <string name=\"bearing_lack_of_sensors\">బేరింగ్ సేకరించడం సాధ్యం కాదు: పరికరం యాక్సిలెరోమీటర్, మాగ్నెటిక్ ఫీల్డ్ సెన్సార్ లేదా రెండూ లేవు </string>\n  <string name=\"select_date\">తేదిని ఎంపిక చేసుకొండి</string>\n  <string name=\"select_time\">సమయాన్ని ఎంపిక చేసుకొండి</string>\n  <string name=\"no_date_selected\">తేదిని ఎంపిక చేసుకొలేదు</string>\n  <string name=\"no_time_selected\">సమయాన్ని ఎంపిక చేసుకొలేదు</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">హైదర్ </string>\n  <string name=\"ethiopian_month_4\">తహసాస్ </string>\n  <string name=\"ethiopian_month_5\">టిర్</string>\n  <string name=\"ethiopian_month_6\">యేకటిట్</string>\n  <string name=\"ethiopian_month_7\">మగాబిత్</string>\n  <string name=\"ethiopian_month_8\">మీయాజి</string>\n  <string name=\"ethiopian_month_9\">గింబోట్</string>\n  <string name=\"ethiopian_month_10\">సేనియ</string>\n  <string name=\"ethiopian_month_11\">హాంలై </string>\n  <string name=\"ethiopian_month_12\">నేహసీ</string>\n  <string name=\"ethiopian_month_13\">పెగుమెయి</string>\n  <string name=\"coptic_month_1\">తౌట్</string>\n  <string name=\"coptic_month_2\">పాపై </string>\n  <string name=\"coptic_month_3\">హతోర్ </string>\n  <string name=\"coptic_month_4\">కోయక్ </string>\n  <string name=\"coptic_month_5\">టోబి </string>\n  <string name=\"coptic_month_6\">మెషిర్ </string>\n  <string name=\"coptic_month_7\">పరిమహత్ </string>\n  <string name=\"coptic_month_8\">పార్మౌటి </string>\n  <string name=\"coptic_month_9\">పషన్స్ </string>\n  <string name=\"coptic_month_10\">పవోని </string>\n  <string name=\"coptic_month_11\">ఎపిప్ </string>\n  <string name=\"coptic_month_12\">మెసోరి </string>\n  <string name=\"coptic_month_13\">ఫై కోగి ఎనావోట్ </string>\n  <string name=\"islamic_month_1\">మొహర్రం</string>\n  <string name=\"islamic_month_2\">సఫర్</string>\n  <string name=\"islamic_month_3\">రబి అల్ అవ్వల్ </string>\n  <string name=\"islamic_month_4\">రబి అల్ థాని </string>\n  <string name=\"islamic_month_5\">జుమాద అల్ అవ్వల్ </string>\n  <string name=\"islamic_month_6\">జుమాద అల్ థాని </string>\n  <string name=\"islamic_month_7\">రజబ్</string>\n  <string name=\"islamic_month_8\">శబాన్</string>\n  <string name=\"islamic_month_9\">రంజాన్</string>\n  <string name=\"islamic_month_10\">షవ్వాల్ </string>\n  <string name=\"islamic_month_11\">ధుః అల్ కీదాహ్ </string>\n  <string name=\"islamic_month_12\">ధుః అల్ హిజ్జాహ్ </string>\n  <string name=\"persian_month_1\">ఫార్వర్దిన్ </string>\n  <string name=\"persian_month_2\">ఆర్డీబెహెశ్ట్ </string>\n  <string name=\"persian_month_3\">కోర్దడ్</string>\n  <string name=\"persian_month_4\">టిర్</string>\n  <string name=\"persian_month_5\">మోర్దాద్</string>\n  <string name=\"persian_month_6\">షహ్రివర్</string>\n  <string name=\"persian_month_7\">మెహర్</string>\n  <string name=\"persian_month_8\">ఆబన్</string>\n  <string name=\"persian_month_9\">ఆజర్</string>\n  <string name=\"persian_month_10\">డే</string>\n  <string name=\"persian_month_11\">బహమన్</string>\n  <string name=\"persian_month_12\">ఇస్ఫాన్డ్ </string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">ఫైల్: %sచెల్లదు.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">ఫైల్: %sలేదు.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">గుర్తింపును నమోదు చేయండి</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">గుర్తింపు</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">ఈ ఫారమ్ మిమ్మల్ని మీరు గుర్తించాల్సిన అవసరం ఉంది</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">మూసివేయండి</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">మార్పులకు కారణం</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">కారణం</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">ఈ ఫారమ్‌లో మార్పులకు కారణాన్ని మీరు వివరించాలి</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">సేవ్ చేయండి</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS నేషనల్ మ్యాప్ టోపో</string>\n  <string name=\"openmap_usgs_sat\">USGS నేషనల్ మ్యాప్ హైబ్రిడ్</string>\n  <string name=\"openmap_usgs_img\">USGS నేషనల్ మ్యాప్ ఇమేజరీ</string>\n  <string name=\"openmap_cartodb_positron\">కార్టోడిబి పోసిట్రాన్</string>\n  <string name=\"openmap_cartodb_darkmatter\">కార్టోడిబి డార్క్ మేటర్</string>\n  <string name=\"basemap\">బేస్ మ్యాప్</string>\n  <string name=\"basemap_source\">ఆధారం</string>\n  <string name=\"map_style_label\">%s మ్యాప్ శైలి</string>\n  <string name=\"streets\">స్ట్రీట్స్</string>\n  <string name=\"terrain\">భుభాగము</string>\n  <string name=\"hybrid\">హైబ్రిడ్</string>\n  <string name=\"satellite\">ఉపగ్రహం</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">Light</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">Dark</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">బయటి ప్రదేశాలు</string>\n  <string name=\"topographic\">Topographic</string>\n  <string name=\"google_play_services_error_occured\">Google మ్యాప్స్‌ను యాక్సెస్ చేయడం సాధ్యం కాట్లేదు. Google Play సేవలు ఇన్‌స్టాల్ చేయబడిందా?</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Dark Matter</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">ఒక పాయింట్ రికార్డ్ చేయండి</string>\n  <string name=\"provider_disabled_error\">క్షమించండి, లొకేషన్ ప్రొవైడర్లు డిసేబుల్ చెయ్యబడ్డాయి!</string>\n  <string name=\"google_play_services_not_available\">ఈ ఫారం మీ స్థానాన్ని ట్రాక్ చేయాలనుకుంటుంది, కానీ గూగుల్ ప్లే సేవలు అందుబాటులో లేవు.</string>\n  <string name=\"location_providers_disabled_dialog_message\">ఈ ఫారం మీ స్థానాన్ని ట్రాక్ చేయాలనుకుంటుంది కాని స్థాన ప్రొవైడర్లు నిలిపివేయబడ్డారు. దయచేసి Android సెట్టింగ్‌లలో ప్రారంభించండి.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">సెట్టింగులకు వెళ్లండి</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"background_location_disabled\">ఈ ఫారం మీ స్థానాన్ని ట్రాక్ చేయాలనుకుంటుంది కాని ట్రాకింగ్ నిలిపివేయబడింది. దయచేసి పై మెను %1$sలో ప్రారంభించండి.</string>\n  <string name=\"background_location_enabled\">ఈ ఫారం మీ స్థానాన్ని ట్రాక్ చేస్తుంది. పై మెను%1$sలో మీరు ట్రాకింగ్‌ను నిలిపివేయవచ్చు.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">పశ్చిమం %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">తూర్పు %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">దక్షిణం %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">ఉత్తరం %1$s%2$s%3$s</string>\n  <string name=\"input_method\">ఇన్‌పుట్ పద్ధతి</string>\n  <string name=\"start\">మొదలు</string>\n  <string name=\"enable_gps\">GPS ని ప్రారంభించండి</string>\n  <string name=\"gps_enable_message\">మీ పరికరంలో GPS డిజేబుల్ చేయబడి ఉంది. మీరు ఎనేబుల్ చేయదలుచుకుంటున్నారా?</string>\n  <string name=\"geo_clear_warning\">ఫీచర్ ఇప్పటికే క్రియేట్ చేయబడింది. మీరు ఫీచర్ తీసెయ్యాలనుకుంటున్నారా?</string>\n  <string name=\"geo_exit_warning\">మార్పులను విస్మరించి, ODK కి తిరిగి వెళ్లాలా?</string>\n  <string name=\"polygon_validator\">బహుభుజిని సృష్టించడానికి కనీసం 3 పాయింట్లు ఉండాలి</string>\n  <string name=\"polyline_validator\">పాలిలైన్ సృష్టించడానికి కనీసం 2 పాయింట్లు ఉండాలి</string>\n  <string name=\"clear\">తొలగించు</string>\n  <string name=\"placement_mode\">ఇక్కడ  నొక్కడం ద్వారా కర్సర్ ప్లేస్‌మెంట్</string>\n  <string name=\"manual_mode\">మాన్యువల్ లొకేషన్ రికార్డింగ్</string>\n  <string name=\"automatic_mode\">ఆటోమేటిక్ లొకేషన్ రికార్డింగ్</string>\n  <string name=\"recording_interval\">రికార్డింగ్ చెయ్యాల్సిన వ్యవధి:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d రెండవలు </item>\n    <item quantity=\"other\">%dసెకన్లు   </item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%dనిమిషాలు</item>\n    <item quantity=\"other\">%d నిమిషాలు </item>\n  </plurals>\n  <string name=\"accuracy_requirement\">ఖచ్చితత్వ అవసరం:</string>\n  <string name=\"none\">ఏమీ కాదు</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d మీటర్ల</item>\n    <item quantity=\"other\">%d మీటర్ల</item>\n  </plurals>\n  <string name=\"collection_status_paused\">నమోదు చేసిన పాయింట్లు:%d</string>\n  <string name=\"collection_status_placement\">నమోదు చేసిన పాయింట్లు:%d (పాయింట్లను ఉంచడానికి నొక్కండి)</string>\n  <string name=\"collection_status_manual\">నమోదు చేసిన పాయింట్లు: %d(మాన్యువల్ రికార్డింగ్)</string>\n  <string name=\"collection_status_auto_seconds\">నమోదు చేసిన పాయింట్లు:%1$d(ప్రతి %2$dసెకను రికార్డ్ చేయడం)</string>\n  <string name=\"collection_status_auto_minutes\">నమోదు చేసిన పాయింట్లు: %1$d(ప్రతి %2$dనిమిషం రికార్డింగ్)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">నమోదు చేసిన పాయింట్లు: %1$d(ప్రతి %2$dసెకను రికార్డింగ్, ఖచ్చితత్వం%3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">నమోదు చేసిన పాయింట్లు: %1$d(ప్రతి %2$dనిమిషం రికార్డింగ్, ఖచ్చితత్వం %3$dm)</string>\n  <string name=\"geopoint_no_draggable_instruction\">మార్కర్ బటన్ యాడ్ చెయ్యడానికి ఇక్కడ నొక్కండి.</string>\n  <string name=\"discard\">విస్మరించండి</string>\n  <!--Name of an action button-->\n  <string name=\"view_data\">సేవ్ చెయ్యబడిన ఫారంలు చూడండి</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">ఫారం పంపుతోంది</string>\n  <string name=\"noselect_error\">క్షమించండి, మీరు ఏ ఫారంలను ఎంచుకోలేదు!</string>\n  <string name=\"no_connection\">ఎటువంటి నెట్‌వర్క్ కనెక్షన్ అందుబాటులో లేదు</string>\n  <string name=\"sending_items\">%2$s ఫారం(లు) నుండి %1$sపంపుతోంది </string>\n  <string name=\"send_selected_data\">ఎంచుకున్నది పంపు</string>\n  <string name=\"server_auth_credentials\">సర్వర్ కోసం ఉపయోగించిన యూజర్ పేరు , పాస్ వర్డు సరైనవి కావు:%s</string>\n  <string name=\"server_requires_auth\">సర్వర్‌కు ధృవీకరణ తప్పనిసరి</string>\n  <string name=\"no_forms_uploaded\">క్షమించండి, ఏ ఫారం కూడా అప్‌లోడ్ చేయబడలేదు.</string>\n  <string name=\"upload_results\">ఫలితాలను అప్‌లోడ్ చేయండి</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">వీక్షణను మార్చండి</string>\n  <string name=\"show_sent_and_unsent_forms\">పంపిన మరియు పంపని ఫారంలను చూపించు</string>\n  <string name=\"show_unsent_forms\">పంపని ఫారంలను చూపించు</string>\n  <string name=\"send_in_progress\">బ్యాక్ గ్రౌండ్ లో సెండ్ జరుగుతోంది , దయచేసి కాసేపట్లో మళ్ళీ ప్రయత్నించండి</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">రిఫ్రెష్</string>\n  <string name=\"download\">ఎంపిక చేసుకున్నవి పొందండి </string>\n  <string name=\"downloading_data\">సర్వర్‌కు కనెక్ట్ అవుతోంది</string>\n  <string name=\"fetching_file\">ఫారం%3$s(ల )నుండి .\\n\\nఫారం%1$s ,%2$s పొందుతున్నాం </string>\n  <string name=\"form_download_progress\">%1$s. మీడియా ఫైళ్ళను పొందుతున్నాము:  %3$s లో %2$s</string>\n  <string name=\"version\">వెర్షన్:</string>\n  <string name=\"form_updates_available\">ఫారంలో జరిగిన మార్పులు అందుబాటులో ఉన్నాయి</string>\n  <string name=\"newer_version_of_a_form_info\">ఇది మీ వద్ద ఉన్న ఫారమ్‌కు నవీకరణ</string>\n  <string name=\"form_update_error\">ఫారం నవీకరణ విఫలమైంది</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">ఫారం జాబితా విఫలమైంది</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">సేవ్ చెయ్యబడిన ఫారంలు </string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">ఖాళీ ఫారంలు</string>\n  <string name=\"delete_confirm\">%s ఫారం(ల) ను తొలగించాలా? </string>\n  <string name=\"delete_file\">ఎంచుకున్నదాన్ని తొలగించండి</string>\n  <string name=\"delete_no\">తొలగించవద్దు </string>\n  <string name=\"delete_yes\">ఫారంలను తొలగించు </string>\n  <string name=\"form_delete_message\">ఎంచుకున్న ఫారమ్‌లను తొలగిస్తోంది</string>\n  <string name=\"file_deleted_ok\">%sఫారం(లు) విజయవంతంగా తొలగించబడ్డాయి!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">సర్వర్</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">యూజర్ ఇంటర్ఫేస్</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">మ్యాప్స్</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">ఫారం నిర్వహణ</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">వినియోగదారు, పరికరం గుర్తింపు</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">ప్రయోగాత్మక</string>\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">సర్వర్ సెట్టింగులు</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">సర్వర్ URL</string>\n  <string name=\"url_error\">క్షమించండి, URL తప్పు!</string>\n  <string name=\"username\">యూజర్ పేరు</string>\n  <string name=\"change_username\">ODK యూజర్ నేమ్ </string>\n  <string name=\"password\">పాస్ వర్డ్ </string>\n  <string name=\"change_password\">ODK పాస్ వర్డ్ </string>\n  <string name=\"password_error_whitespace\">పాస్ వర్డ్ ముందు లేదా తరువాత స్పేస్ ఉండకూడదు </string>\n  <string name=\"username_error_whitespace\">యూసర్ నేమ్ లో ముందు లేదా తరువాత స్పేస్ ఉండకూడదు </string>\n  <string name=\"app_theme\">థీము</string>\n  <string name=\"language\">భాష</string>\n  <string name=\"use_device_language\">పరికర భాషను ఉపయోగించండి</string>\n  <string name=\"font_size\">అక్షరాల సైజు </string>\n  <string name=\"change_font_size\">అక్షరాల సైజు  </string>\n  <string name=\"font_size_extra_large\">చాలా పెద్దది</string>\n  <string name=\"font_size_large\">పెద్ద</string>\n  <string name=\"font_size_medium\">మధ్యస్థం </string>\n  <string name=\"font_size_small\">చిన్న</string>\n  <string name=\"font_size_extra_small\">చాలా చిన్న</string>\n  <string name=\"navigation\">నావిగేషన్</string>\n  <string name=\"swipe_navigation\">అడ్డంగా ఉన్న స్వైప్‌లను ఉపయోగించండి</string>\n  <string name=\"buttons_navigation\">ముందుకు / వెనుక బటన్లను ఉపయోగించండి</string>\n  <string name=\"swipe_buttons_navigation\">స్వైప్‌లు మరియు బటన్లను ఉపయోగించండి</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">తరువాత</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">వెనుకకు </string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">ఫారం నవీకరణ</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">ఖాళీ ఫారంలు నవీకరణ మోడ్</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">గతంలో డౌన్‌లోడ్ చేసిన ఫారమ్‌లు మాత్రమే</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">సర్వర్‌తో సరిగ్గా సరిపోలండి</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">ప్రతి పదిహేను నిమిషాలకు</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">ప్రతి గంట</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">ప్రతి ఆరు గంటలకు</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">ప్రతి 24 గంటలకు</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">ఆటోమాటిక్ డౌన్‌లోడ్</string>\n  <string name=\"automatic_download_summary\">మార్పులు జరిగిన ఫారంలను ఆటోమేటిక్ గా డౌన్‌లోడ్ చేయండి</string>\n  <string name=\"hide_old_form_versions_setting_title\">పాత ఫారంలు కనిపించకుండా పెట్టు</string>\n  <string name=\"hide_old_form_versions_setting_summary\">ఫారం నింపడాంలో కొత్త వెర్షన్ మాత్రమే కనిపిస్తుంది</string>\n  <string name=\"form_submission_category\">ఫారం సమర్పణ</string>\n  <string name=\"autosend_selector_title\">ఆటోమేటిక్ గా పంపు </string>\n  <string name=\"autosend\">ఆటోమేటిక్ గ పంపు </string>\n  <string name=\"off\">ఆఫ్</string>\n  <string name=\"wifi_autosend\">వైఫై మాత్రమే</string>\n  <string name=\"cellular_autosend\">సెల్యులార్ మాత్రమే</string>\n  <string name=\"wifi_cellular_autosend\">వైఫై లేదా సెల్యులార్</string>\n  <string name=\"delete_after_send\">పంపిన తర్వాత తొలగించండి</string>\n  <string name=\"delete_after_send_summary\">సర్వర్‌కు పంపిన తర్వాత ఫైనల్ చేసిన ఫారమ్‌లను మరియు మీడియాను తొలగిస్తుంది</string>\n  <string name=\"form_filling_category\">ఫారం నింపడం</string>\n  <string name=\"constraint_behavior_title\">పరిమితి ప్రాసెసింగ్</string>\n  <string name=\"constraint_behavior\">కంస్ట్రయిన్ట్ ప్రాసెసింగ్ బిహేవియర్ </string>\n  <string name=\"constraint_behavior_on_swipe\">ఫార్వర్డ్ స్వైప్ మీద ధృవీకరించండి</string>\n  <string name=\"constraint_behavior_on_finalize\">ఫైనల్ అయ్యే వరకు వాలిడేషన్ వాయిదా వేయండి</string>\n  <string name=\"high_resolution_title\">హై రెసొల్యూషన్ వీడియో </string>\n  <string name=\"high_resolution_summary\">అధిక రిజల్యూషన్ ఉన్న వీడియో రికార్డింగ్‌ చేయడానికి వీలు కల్పించండి </string>\n  <string name=\"image_size_title\">ఇమేజ్ పరిమాణం</string>\n  <string name=\"image_size_dialog_title\">చిత్రం యొక్క పొడవైన అంచు యొక్క గరిష్ట పిక్సెల్‌లు</string>\n  <string name=\"image_size_large\">పెద్దది (3072px)</string>\n  <string name=\"image_size_medium\">మీడియం సైజు (2048 px)</string>\n  <string name=\"image_size_small\">చిన్నది (1024px)</string>\n  <string name=\"image_size_very_small\">చాలా చిన్నది (640 px)</string>\n  <string name=\"image_size_original\">కెమెరా నుండి అసలు సైజు(డిఫాల్ట్)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">ప్రశ్నలకు సూచనలు చూపండి</string>\n  <string name=\"guidance_no\">కాదు </string>\n  <string name=\"guidance_yes\">అవును - ఎల్లప్పుడూ చూపబడుతుంది</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">అవును - Collapsed </string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">ఫారం దిగుమతి</string>\n  <string name=\"instance_sync\">దిగుమతులపై ఫారమ్‌లను ముగించండి</string>\n  <string name=\"instance_sync_summary\">ఇన్‌స్టాన్స్ ఫోల్డర్‌కు నేరుగా జోడించిన ఫారమ్‌లను ప్రభావితం చేస్తుంది.</string>\n  <string name=\"analytics_preferences\">వినియోగించబడిన డేటా</string>\n  <string name=\"analytics\">అనామక వినియోగ డేటాను సేకరించండి</string>\n  <string name=\"form_metadata_title\">ఫారం మెటాడేటా</string>\n  <string name=\"form_metadata\">ఫారం మెటాడేటా</string>\n  <string name=\"form_metadata_summary\">డేటాను సమర్పించే వ్యక్తిని గుర్తించడానికి వినియోగదారు పేరు, ఇమెయిల్ మరియు / లేదా టెలిఫోన్ ప్రీలోడ్ చేసిన ఫీల్డ్‌లను కలిగి ఉన్న ఫారమ్‌లకు ఈ విలువలు జోడించబడతాయి.</string>\n  <string name=\"user_defined\">వినియోగదారుడు నిర్వచించింది </string>\n  <string name=\"phone_number\">ఫోన్ నెంబర్</string>\n  <string name=\"email\">ఈ-మెయిల్ అడ్రెస్</string>\n  <string name=\"invalid_email_address\">చెల్లని ఇమెయిల్ చిరునామా!</string>\n  <string name=\"device_defined\">పరికరం నిర్వచించింది </string>\n  <string name=\"device_id\">పరికరం యొక్క ID</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">అందుబాటులో లేదు</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">క్రొత్త పాస్‌వర్డ్‌ను నమోదు చేయండి</string>\n  <string name=\"admin_password\">అడ్మిన్ పాస్‌వర్డ్‌</string>\n  <string name=\"admin_password_changed\">అడ్మిన్ పాస్‌వర్డ్ విజయవంతంగా మార్చబడింది</string>\n  <string name=\"admin_password_disabled\">అడ్మిన్ పాస్‌వర్డ్ నిలిపివేయబడింది</string>\n  <string name=\"admin_password_incorrect\">క్షమించండి, పాస్‌వర్డ్ తప్పు!</string>\n  <string name=\"enter_admin_password\">అడ్మిన్ పాస్‌వర్డ్‌ను నమోదు చేయండి</string>\n  <string name=\"change_admin_password\">అడ్మిన్ పాస్‌వర్డ్‌ను మార్చండి</string>\n  <string name=\"show_password\">పాస్ వర్డ్ చూపించు </string>\n  <string name=\"found_in_main\">ప్రధాన మెనూ నుండి దాచడానికి ఎంపిక చేయకండి</string>\n  <string name=\"form_entry\">ఫారం ఎంట్రీ నుండి దాచడానికి ఎంపిక చేయవద్దు</string>\n  <string name=\"reset_project_settings_title\">రీసెట్</string>\n  <string name=\"select_all\">అన్నీ ఎంపిక చేసుకొండి</string>\n  <string name=\"clear_all\">అన్నీ క్లియర్ చేయండి</string>\n  <string name=\"reset_settings\">అన్ని సెట్టింగ్‌లు (అంతర్గత సెట్టింగ్‌లు, సేవ్ చేసిన సెట్టింగ్‌లు)</string>\n  <string name=\"reset_saved_forms\">సేవ్ చేసిన ఫారమ్‌లు (ఇన్‌స్టాన్స్ ఫోల్డర్, ఇన్‌స్టాన్స్ డేటాబేస్)</string>\n  <string name=\"reset_blank_forms\">ఖాళీ ఫారం లు (ఫారం ఫోల్డర్, ఫారమ్ డేటాబేస్, ఐటమ్‌సెట్స్ డేటాబేస్)</string>\n  <string name=\"reset_layers\">మ్యాప్ లేయర్స్ (లేయర్స్ ఫోల్డర్)</string>\n  <string name=\"reset_cache\">ఫారం లోడ్ కాష్ (.కాష్ ఫోల్డర్)</string>\n  <string name=\"reset_settings_dialog_title\">ఏమి రీసెట్ చేయాలో ఎంచుకోండి</string>\n  <string name=\"reset_settings_button_reset\">రీసెట్</string>\n  <string name=\"reset_in_progress\">రీసెట్ చేస్తోంది …</string>\n  <string name=\"reset_app_state_result\">ఫలితాలను రీసెట్ చేయండి</string>\n  <string name=\"reset_app_state_warning\">ఎంచుకున్న డేటా మొత్తం శాశ్వతంగా తొలగించబడుతుంది. డేటాను మళ్ళీ పొందలేరు </string>\n  <string name=\"reset_settings_result\">అన్ని సెట్టింగులు ::%s</string>\n  <string name=\"reset_saved_forms_result\">సేవ్ చేసిన ఫారంలు ::%s</string>\n  <string name=\"reset_blank_forms_result\">ఖాళీ ఫారంలు ::%s</string>\n  <string name=\"reset_layers_result\">మ్యాప్ లేయర్స్ ::%s</string>\n  <string name=\"reset_cache_result\">ఫారం లోడ్ cache ::%s</string>\n  <string name=\"main_menu_settings\">ప్రధాన మెనూ సెట్టింగులు</string>\n  <string name=\"user_settings\">వినియోగదారుడి సెట్టింగులు</string>\n  <string name=\"form_entry_setting\">ఫారం ఎంట్రీ సెట్టింగులు</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">వెనుకకు వెలుతోంది</string>\n  <string name=\"moving_backwards_disabled_title\">వెనుకకు వెళ్లడం నిలిపివేయబడింది</string>\n  <string name=\"yes\">అవును</string>\n  <string name=\"no\">లేదు</string>\n  <string name=\"moving_backwards_enabled_title\">వెనుకకు వెళ్ళటం ఆప్షన్ ప్రారంభించబడింది</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">ODK గురించి</string>\n  <string name=\"tell_your_friends\">ODK కలెక్ట్ ని షేర్ చెయ్యండి</string>\n  <string name=\"tell_your_friends_msg\">మీ సహచరులు ఇప్పటికీ కాగితంపై డేటాను సేకరిస్తున్నారా? వారితో ODK Collect పంచుకోండి </string>\n  <string name=\"leave_a_review\">ప్లే స్టోర్ విమర్షని ఇక్కడ రాయండి</string>\n  <string name=\"leave_a_review_msg\">మీ ( సానుకూల) సమీక్ష Play Store లో ఈ App గురించి ఎక్కువ మందికి తెలిసేలా చేస్తుంది </string>\n  <string name=\"odk_website\">ODK వెబ్‌సైట్‌ను సందర్శించండి</string>\n  <string name=\"odk_website_summary\">సవాలు చేసే వాతావరణాలలో సామాజిక మంచి కోసం డేటాను సేకరించడానికి ODK ఉపయోగించబడుతుంది.</string>\n  <string name=\"odk_forum\">ODK ఫోరమ్‌లో చేరండి</string>\n  <string name=\"odk_forum_summary\">మద్దతు పొందడానికి ఫోరమ్‌లో చేరండి, లక్షణాలను అభ్యర్థించండి, కోడ్ / అనువాదాలను అందించండి!</string>\n  <string name=\"all_open_source_licenses\">ఓపెన్ సోర్స్ లైబ్రరీలు/లైసెన్సులు</string>\n  <string name=\"all_open_source_licenses_msg\">మేము రాక్షసుల భుజాలపై నిలబడతాం!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"scan_qr_code_fragment_title\">స్కాన్</string>\n  <string name=\"view_qr_code_fragment_title\">QR కోడ్</string>\n  <string name=\"import_qrcode_sd\">QR కోడ్‌ను దిగుమతి చేయండి</string>\n  <string name=\"successfully_imported_settings\">సెట్టింగులు విజయవంతంగా దిగుమతి చేయబడ్డాయి</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">గరిష్ట అక్షరాల పరిమితి చేరుకుంది: అన్ని సెట్టింగ్‌ల కోసం QR కోడ్ ఉత్పత్తి చేయబడదు</string>\n  <string name=\"share\">షేర్</string>\n  <string name=\"turn_on_flashlight\">ఫ్లాష్ లైట్ ని ఆన్ చెయ్యండి</string>\n  <string name=\"turn_off_flashlight\">ఫ్లాష్ లైట్ ని ఆఫ్ చెయ్యండి</string>\n  <string name=\"server_password\">సర్వర్ పాస్వర్డ్</string>\n  <string name=\"include_password_dialog\">పాస్వర్డ్ కోడ్ లో జోడించి ఉంది. </string>\n  <string name=\"generate\">రూపొందించండి</string>\n  <string name=\"invalid_qrcode\">QR కోడ్‌లో చెల్లుబాటు అయ్యే సెట్టింగ్‌లు లేవు</string>\n  <string name=\"qr_code_not_found\">ఎంచుకున్న ఫోటో లో QR కోడ్ కనపడటం లేదు</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">రద్దు చేయి</string>\n  <string name=\"cancel_loading_form\">రద్దు చేయి</string>\n  <string name=\"cancel_location\">రద్దు చేయి</string>\n  <string name=\"do_not_change\">రద్దు చేయి</string>\n  <string name=\"canceling\">రద్దు చేయబడుతుంది </string>\n  <string name=\"notification_channel_name\">సాధారణ నోటిఫికేషన్‌లు</string>\n  <string name=\"success\">సక్సెస్ </string>\n  <string name=\"error_occured\">ఎర్రర్ వచ్చింది</string>\n  <string name=\"please_wait\">దయచేసి కొన్ని క్షణాలు వేచి ఉండండి.</string>\n  <string name=\"please_wait_long\">దయచేసి వేచి ఉండండి. దీనికి కొన్ని నిమిషాలు పట్టవచ్చు.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">\\'%s\\' తొలగించడం సాధ్యం కాలేదు. దయచేసి ఫైల్‌ను మాన్యువల్‌గా తొలగించి, ఫారమ్‌ను తిరిగి డౌన్‌లోడ్ చేయండి.</string>\n  <string name=\"fs_create_media_folder_error\">మీడియా ఫోల్డర్‌%sను తయారు చేయడం సాధ్యం కాలేదు.</string>\n  <string name=\"fs_file_copy_error\">%2$sమీద %1$sను కాపీ చేయలేకపోయింది . కారణం :%3$s</string>\n  <string name=\"select_odk_shortcut\">ODK సత్వరమార్గాన్ని ఎంచుకోండి</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">జోడించు</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">గుర్తించని URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">తప్పు URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">మీరు ఒకే ఫారమ్ ఐడి, వెర్షన్‌తో రెండు వేర్వేరు ఫారమ్‌లను డౌన్‌లోడ్ చేసారు. అది వేర్వేరు సమయాల్లో లేదా వేర్వేరు సర్వర్‌లకు అప్‌లోడ్ చేసిన ఒకే ఫారం కావచ్చు. మీరు ఒకదాన్ని తొలగించవలసిందే. </string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">విస్మరించండి</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-th-rTH/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">สร้างแบบสำรวจใหม่</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">แบบสำรวจฉบับร่าง</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">แบบฟอร์มพร้อมส่ง</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">แบบสำรวจที่ส่งแล้ว</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">ดาวน์โหลดแบบสำรวจ</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">แบบฟอร์มที่ถูกลบ</string>\n  <string name=\"saved_forms\">บันทึกแบบสำรวจ</string>\n  <string name=\"version_number\">เวอร์ชั่น: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'เพิ่มเมื่อ\\' EEE, MMM dd, yyyy \\'เวลา\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'บันทึกเมื่อ\\' EEE, MMM dd, yyyy \\'เวลา\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">เสร็จสมบูรณ์เมื่อ EEE, MMM dd, yyyy เวลา HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'ส่งแล้วเมื่อ\\' EEE, MMM dd, yyyy \\'เวลา\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'การส่งผิดพลาดเมื่อ\\' EEE, MMM dd, yyyy \\'เวลา\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'ลบเมื่อ\\' EEE, MMM dd, yyyy \\'เวลา\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'แก้ไขเมื่อ\\' EEE, MMM dd, yyy \\'เวลา\\' HH:mm</string>\n  <string name=\"encrypted_form\">แบบสำรวจเข้ารหัส</string>\n  <string name=\"deleted_form\">แบบฟอร์มที่ถูกลบ</string>\n  <string name=\"sort_the_list\">จัดเรียงรายการ</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">ค้นหา</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">สิ้นสุดการค้นหา แบบสำรวจทั้งหมดถูกติดตั้งแล้ว</string>\n  <string name=\"loading_form\">กำลังเปิดแบบสำรวจ</string>\n  <string name=\"survey_loading_reading_form_message\">กำลังอ่านคำจำกัดความของแบบสอบถาม…</string>\n  <string name=\"survey_loading_reading_data_message\">กำลังอ่านข้อมูลการสำรวจ</string>\n  <string name=\"survey_loading_reading_csv_message\">กำลังอ่านไฟล์ CSV</string>\n  <string name=\"parse_error\">ขออภัย ไม่สามารถเปิดแบบสำรวจได้</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">กู้คืนข้อมูลจากจุดที่บันทึกไว้</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">เปลี่ยนภาษา</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">ข้ามไปที่</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">บันทึกเป็นฉบับร่าง</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">ไปที่จุดเริ่มต้น</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">ไปที่จุดสิ้นสุด</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">เลื่อนขึ้น</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"exit\">ออก</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">แก้ไขจุดที่เลือก</string>\n  <string name=\"clearanswer_confirm\">ย้ายคำตอบไปที่ \\\"%s\\\" หรือไม่?</string>\n  <string name=\"clear_answer\">ปิดคำตอบ</string>\n  <string name=\"clear_answer_ask\">ปิดคำตอบนี้หรือไม่?</string>\n  <string name=\"clear_answer_no\">ยกเลิก</string>\n  <string name=\"discard_answer\">ปิดคำตอบ</string>\n  <string name=\"discard_group\">ลบกลุ่ม</string>\n  <string name=\"delete_repeat\">ลบกลุ่ม</string>\n  <string name=\"delete_repeat_ask\">ต้องการลบกลุ่มนี้หรือไม่</string>\n  <string name=\"delete_repeat_confirm\">ลบกลุ่ม \\\"%s\\\" และกลุ่มย่อยทั้งหมด</string>\n  <string name=\"delete_repeat_no\">ยกเลิก</string>\n  <string name=\"invalid_answer_error\">ขออภัย คำตอบไม่ถูกต้อง</string>\n  <string name=\"required_answer_error\">ขออภัย โปรดตอบคำถามนี้</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">ท่านอยู่ที่จุดสิ้นสุดของ %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">หากท่านต้องการแก้ไขแบบสำรวจ ให้ \\\"บันทึกเป็นฉบับร่าง\\\" จนกว่าท่านจะพร้อมส่ง</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">บันทึกเป็นฉบับร่าง</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">ส่งแบบสำรวจ</string>\n  <string name=\"data_saved_error\">ขออภัย ไม่สามารถบันทึกแบบสำรวจได้</string>\n  <string name=\"data_saved_ok\">บันทึกแบบสำรวจเรียบร้อยแล้ว</string>\n  <string name=\"save_point_error\">มีข้อผิดพลาดในการบันทึกจุดย้อนคืน : %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">ละทิ้งแบบสำรวจ</string>\n  <string name=\"keep_changes\">บันทึกการเปลี่ยนแปลง</string>\n  <string name=\"keep_editing\">แก้ไขต่อไป</string>\n  <string name=\"quit_form_title\">บันทึกแบบสำรวจ?</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">กำลังบันทึกแบบสำรวจ</string>\n  <string name=\"survey_saving_validating_message\">กำลังตรวจความถูกต้องของคำตอบ</string>\n  <string name=\"survey_saving_collecting_message\">กำลังรวบรวมข้อมูล</string>\n  <string name=\"survey_saving_saving_message\">กำลังบันทึกลงใน SD card</string>\n  <string name=\"survey_saving_encrypting_message\">กำลังเข้ารหัสข้อมูล</string>\n  <string name=\"no_form_id_specified\">ไม่ได้ระบุ FormId</string>\n  <string name=\"phone_does_not_support_rsa\">โทรศัพท์ไม่รองรับการเข้ารหัส RSA</string>\n  <string name=\"invalid_rsa_public_key\">RSA public key ไม่ถูกต้อง</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">เริ่มการทำงาน</string>\n  <string name=\"activity_not_found\">ไม่มีกิจกรรมให้ดำเนินการ: %s</string>\n  <string name=\"no_app\">ไม่พบโปรแกรมที่เรียกใช้ กรุณากรอกข้อมูลด้วยตนเอง</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">ไปที่ Url</string>\n  <string name=\"capture_osm\">เปิด OpenMapKit</string>\n  <string name=\"edited_osm_file\">แก้ไขไฟล์ OSM XML:</string>\n  <string name=\"invalid_osm_data\">มีข้อผิดพลาด ไม่สามารถเข้าถึงข้อมูล OSM ที่ใช้ได้</string>\n  <string name=\"alert\">แจ้งเตือน</string>\n  <string name=\"install_openmapkit\">กรุณาติดตั้ง OpenMapKit</string>\n  <string name=\"get_barcode\">สแกนบาร์โคด</string>\n  <string name=\"replace_barcode\">เปลี่ยนบาร์โคดใหม่</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">ถ่ายภาพ</string>\n  <string name=\"choose_image\">เลือกรูปภาพ</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">บันทึกและปิดการทำงาน</string>\n  <string name=\"signature_capture\">ลงลายมือชื่อ</string>\n  <string name=\"sign_button\">บันทึกลายมือชื่อ</string>\n  <string name=\"markup_image\">Markup Image</string>\n  <string name=\"draw_image\">ภาพวาด</string>\n  <string name=\"reset_image\">ตั้งค่าใหม่</string>\n  <string name=\"set_color\">เลือกสี</string>\n  <string name=\"capture_video\">บันทึกภาพเคลื่อนไหว</string>\n  <string name=\"choose_video\">เลือกภาพเคลื่อนไหว</string>\n  <string name=\"play_video\">เปิดภาพเคลื่อนไหว</string>\n  <string name=\"choose_file\">เลือกไฟล์</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">บันทึกเสียง</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">เลือกไฟล์เสียง</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">เกิดข้อผิดพลาดระหว่างแนบไฟล์มีเดีย: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">เลือกคำตอบ</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Selected: </string>\n  <string name=\"trigger\">ตกลง โปรดดำเนินการต่อ</string>\n  <string name=\"ext_file_no_data_error\">ไม่มีข้อมูลในไฟล์</string>\n  <string name=\"ext_conflicting_columns_error\">คอลัมน์ %s ตรงกัน</string>\n  <string name=\"ext_sortBy_numeric_error\">ข้อมูลในคอลัมน์ที่จัดเรียงต้องมีค่าเป็นตัวเลข ค่าที่ผิดพลาดคือ \\'%s\\'</string>\n  <string name=\"ext_import_generic_error\">ไม่สามารถนำเข้าข้อมูลจาก %1$s. เนื่องจาก: %2$s</string>\n  <string name=\"ext_import_progress_message\">กำลังโหลดข้อมูลล่วงหน้าจาก \\'%1$s\\', กรุณารอสักครู่… %2$s</string>\n  <string name=\"ext_import_finalizing_message\">กำลังตรวจสอบข้อมูลตั้งต้น…</string>\n  <string name=\"ext_import_completed_message\">การอ่านข้อมูลเสร็จสมบูรณ์</string>\n  <string name=\"ext_not_initialized_error\">ระบบจัดการข้อมูลภายนอกยังไม่ถูกเปิดใช้งาน</string>\n  <string name=\"ext_import_csv_missing_error\">ข้อมูลภายนอกของ %1$s ยังไม่ถูกนำเข้า เป็นไปได้ว่าคุณลืมแนบไฟล์ %2$s.csv ไว้กับแบบสำรวจ</string>\n  <string name=\"ext_assign_value_error\">ไม่สามารถกำหนดค่า \\'%s\\'</string>\n  <string name=\"get_bearing\">บันทึกทิศทาง</string>\n  <string name=\"replace_bearing\">แทนที่ทิศทาง</string>\n  <string name=\"getting_bearing\">กำลังแสดงทิศทาง</string>\n  <string name=\"accept_bearing\">บันทึกทิศทาง</string>\n  <string name=\"select_date\">เลือกวันที่</string>\n  <string name=\"select_time\">เลือกเวลา</string>\n  <string name=\"no_date_selected\">ยังไม่ได้เลือกวันที่</string>\n  <string name=\"no_time_selected\">ยังไม่ได้เลือกเวลา</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">ไฟล์: %s ไม่ถูกต้อง</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">ไม่พบไฟล์ %s</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS National Map Topo</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"streets\">Streets</string>\n  <string name=\"terrain\">Terrain</string>\n  <string name=\"hybrid\">Hybrid</string>\n  <string name=\"satellite\">Satellite</string>\n  <string name=\"google_play_services_error_occured\">เปิด Google Maps ไม่ได้ ตรวจสอบว่ามี Google Play Services ติดตั้งหรือไม่\n</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">ขออภัย GPS ถูกปิดการใช้งานไว้</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">ตะวันตก %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">ตะวันออก %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">ใต้ %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">เหนือ %1$s%2$s%3$s</string>\n  <string name=\"start\">เริ่มต้น</string>\n  <string name=\"enable_gps\">เปิด GPS</string>\n  <string name=\"geo_exit_warning\">ยกเลิกการเปลี่ยนแปลงและกลับไปที่ ODK?</string>\n  <string name=\"polygon_validator\">ต้องมีจุดพิกัด 3 จุดขึ้นไปเพื่อสร้าง Polygon</string>\n  <string name=\"clear\">ลบ</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <string name=\"geopoint_no_draggable_instruction\">แตะที่ปุ่มเพิ่มเครื่องหมาย</string>\n  <string name=\"discard\">ยกเลิก</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">กำลังส่งแบบสำรวจ</string>\n  <string name=\"noselect_error\">ขออภัย ท่านยังไม่ได้เลือกแบบสำรวจ</string>\n  <string name=\"no_connection\">ไม่มีเครือข่ายให้เชื่อมต่อ</string>\n  <string name=\"sending_items\">กำลังส่ง %1$s จากแบบสำรวจทั้งหมด %2$s</string>\n  <string name=\"send_selected_data\">ส่งแบบสำรวจที่เลือกไว้</string>\n  <string name=\"server_auth_credentials\">ชื่อผู้ใช้และรหัสผ่านไม่ถูกต้องสำหรับเซิร์ฟเวอร์: %s</string>\n  <string name=\"server_requires_auth\">Server Requires Authentication</string>\n  <string name=\"no_forms_uploaded\">ขออภัย ไม่สามารถส่งแบบสำรวจได้</string>\n  <string name=\"upload_results\">ส่งผลสำรวจ</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">เปลี่ยนมุมมอง</string>\n  <string name=\"show_sent_and_unsent_forms\">แสดงแบบสำรวจที่ส่งแล้วและยังไม่ได้ส่ง</string>\n  <string name=\"show_unsent_forms\">แสดงแบบสำรวจที่ยังไม่ได้ส่ง</string>\n  <string name=\"send_in_progress\">กำลังส่งข้อมูล โปรดลองใหม่อีกครั้ง</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">รีเฟรช</string>\n  <string name=\"download\">ดาวน์โหลดที่เลือกไว้</string>\n  <string name=\"downloading_data\">กำลังเชื่อมต่อกับเซิร์ฟเวอร์</string>\n  <string name=\"form_download_progress\">%1$s. Getting media files: %2$s of %3$s</string>\n  <string name=\"version\">เวอร์ชั่น:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">บันทึกแบบสำรวจ</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">แบบสำรวจเปล่า</string>\n  <string name=\"delete_confirm\">ต้องการลบแบบสำรวจ %s หรือไม่</string>\n  <string name=\"delete_file\">ลบแบบสำรวจที่เลือกไว้</string>\n  <string name=\"delete_no\">ไม่ลบ</string>\n  <string name=\"delete_yes\">ลบแบบสำรวจ</string>\n  <string name=\"file_deleted_ok\">ลบแบบสำรวจ %s เรียบร้อยแล้ว</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">เซิร์ฟเวอร์</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">ส่วนต่อประสานกับผู้ใช้</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">แผนที่</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">จัดการแบบสำรวจ</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">การควบคุมการเข้าถึง</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">ตั้งค่าเซิร์ฟเวอร์</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL ของเซิร์ฟเวอร์</string>\n  <string name=\"url_error\">ขออภัย URL ไม่ถูกต้อง</string>\n  <string name=\"username\">ชื่อผู้ใช้</string>\n  <string name=\"change_username\">ชื่อผู้ใช้งาน ODK</string>\n  <string name=\"password\">รหัสผ่าน</string>\n  <string name=\"change_password\">รหัสผ่าน ODK</string>\n  <string name=\"password_error_whitespace\">ไม่สามารถใช้ช่องว่างนำหน้าและตามหลังรหัสผ่าน</string>\n  <string name=\"username_error_whitespace\">ไม่สามารถใช้ช่องว่างนำหน้าและตามหลังชื่อผู้ใช้</string>\n  <string name=\"language\">ภาษา</string>\n  <string name=\"font_size\">ขนาดตัวอักษร</string>\n  <string name=\"change_font_size\">ขนาดอักษร</string>\n  <string name=\"font_size_extra_large\">ขนาดใหญ่พิเศษ</string>\n  <string name=\"font_size_large\">ขนาดใหญ่</string>\n  <string name=\"font_size_medium\">ขนาดปานกลาง</string>\n  <string name=\"font_size_small\">ขนาดเล็ก</string>\n  <string name=\"font_size_extra_small\">ขนาดเล็กมาก</string>\n  <string name=\"navigation\">ค้นหา</string>\n  <string name=\"swipe_navigation\">ใช้การเลื่อนหน้าจอไปด้านข้าง</string>\n  <string name=\"buttons_navigation\">ใช่ปุ่มเลื่อนไปข้างหน้า/ย้อนกลับ</string>\n  <string name=\"swipe_buttons_navigation\">ใช้ทั้งการปัดเลื่อนหน้าจอและปุ่ม</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">ต่อไป</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">ย้อนกลับ</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"form_submission_category\">การส่งแบบสำรวจ</string>\n  <string name=\"autosend_selector_title\">ส่งอัตโนมัติ</string>\n  <string name=\"autosend\">ส่งอัตโนมัติ</string>\n  <string name=\"delete_after_send\">ลบหลังจากส่งแล้ว</string>\n  <string name=\"delete_after_send_summary\">ลบแบบสำรวจที่ตรวจสอบแล้วและไฟล์ภาพและเสียงหลังจากส่งไปที่เซิร์ฟเวอร์แล้ว</string>\n  <string name=\"constraint_behavior\">ลักษณะการประมวลผลแบบมีเงื่อนไข</string>\n  <string name=\"constraint_behavior_on_swipe\">ตรวจสอบเมื่อปัดเลื่อนไปข้างหน้า</string>\n  <string name=\"constraint_behavior_on_finalize\">เลื่อนการตรวจสอบไปจนกว่าการสำรวจสิ้นสุด</string>\n  <string name=\"high_resolution_summary\">เปิดใช้งานการบันทึกวีดีโอความละเอียดสูง</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"analytics_preferences\">ข้อมูลการใช้งาน</string>\n  <string name=\"analytics\">บันทึกค่าการใช้งานแบบไม่เจาะจงผู้ใช้</string>\n  <string name=\"form_metadata_title\">Metadata ของแบบสำรวจ</string>\n  <string name=\"form_metadata\">Form metadata</string>\n  <string name=\"form_metadata_summary\">ค่านี้จะถูกบันทึกในแบบสำรวจที่มีชื่อผู้ใช้ อีเมล์ และ/หรือ เบอร์โทรศัพท์ ที่โหลดไว้แล้วเพื่อระบุตัวผู้ส่งข้อมูล</string>\n  <string name=\"user_defined\">กำหนดโดยผู้ใช้งาน</string>\n  <string name=\"phone_number\">เบอร์โทรศัพท์</string>\n  <string name=\"email\">ที่อยู่อีเมล์</string>\n  <string name=\"device_defined\">กำหนดโดยโทรศัพท์</string>\n  <string name=\"device_id\">รหัสประจำเครื่อง</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">ใส่รหัสผ่านใหม่</string>\n  <string name=\"admin_password\">รหัสผ่านของผู้ดูแลระบบ</string>\n  <string name=\"admin_password_changed\">เปลี่ยนรหัสผ่านของผู้ควบคุมระบบเรียบร้อยแล้ว</string>\n  <string name=\"admin_password_disabled\">ขออภัย รหัสผ่านของผู้ควบคุมระบบยังไม่เปิดใช้งาน</string>\n  <string name=\"admin_password_incorrect\">ขออภัย รหัสผ่านไม่ถูกต้อง</string>\n  <string name=\"enter_admin_password\">ใส่รหัสผ่านสำหรับผู้ควบคุมระบบ</string>\n  <string name=\"change_admin_password\">เปลี่ยนรหัสผ่านของผู้ดูแลระบบ</string>\n  <string name=\"show_password\">แสดงรหัสผ่าน</string>\n  <string name=\"found_in_main\">ปล่อยว่างเพื่อซ่อนจากเมนูหลัก</string>\n  <string name=\"reset_project_settings_title\">ตั้งค่าใหม่</string>\n  <string name=\"select_all\">เลือกทั้งหมด</string>\n  <string name=\"clear_all\">ลบทั้งหมด</string>\n  <string name=\"reset_settings\">ค่าการติดตั้งทั้งหมด(การตั้งค่าภายใน, ค่าที่บันทึกไว้)</string>\n  <string name=\"reset_saved_forms\">แบบสำรวจที่บันทึกไว้ (แฟ้มInstances, ฐานข้อมูลinstance)</string>\n  <string name=\"reset_blank_forms\">แบบสำรวจเปล่า (แฟ้มแบบสำรวจ, ฐานข้อมูลแบบสำรวจ, ฐานข้อมูลitemsets)</string>\n  <string name=\"reset_layers\">ชั้นข้อมูลแผนที่ (แฟ้มชั้นข้อมูล)</string>\n  <string name=\"reset_settings_dialog_title\">เลือกสิ่งที่ต้องตั้งค่าใหม่</string>\n  <string name=\"reset_settings_button_reset\">ตั้งค่าใหม่</string>\n  <string name=\"reset_in_progress\">กำลังตั้งค่าเริ่มต้นใหม่</string>\n  <string name=\"reset_app_state_result\">ผลการตั้งค่าใหม่</string>\n  <string name=\"reset_app_state_warning\">ข้อมูลที่เลือกไว้ทั้งหมดจะถูกลบ และไม่สามารถย้อนกลับได้</string>\n  <string name=\"reset_settings_result\">การตั้งค่าทั้งหมด :: %s</string>\n  <string name=\"reset_saved_forms_result\">แบบสำรวจที่บันทึกไว้ :: %s</string>\n  <string name=\"reset_blank_forms_result\">แบบสำรวจเปล่า :: %s</string>\n  <string name=\"reset_layers_result\">ชั้นข้อมูลแผนที่ :: %s</string>\n  <string name=\"main_menu_settings\">ตั้งค่าเมนูหลัก</string>\n  <string name=\"user_settings\">ตั้งค่าผู้ใช้</string>\n  <string name=\"form_entry_setting\">ตั้งค่าการป้อนข้อมูลแบบสำรวจ</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">บันทึกเป็นฉบับร่าง</string>\n  <string name=\"save_mid_summary\">บันทึกเป็นฉบับร่าง</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">เกี่ยวกับ</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">ตกลง</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">ยกเลิก</string>\n  <string name=\"cancel_loading_form\">ยกเลิก</string>\n  <string name=\"cancel_location\">ยกเลิก</string>\n  <string name=\"do_not_change\">ยกเลิก</string>\n  <string name=\"canceling\">กำลังยกเลิก</string>\n  <string name=\"success\">สำเร็จ</string>\n  <string name=\"error_occured\">มีข้อผิดพลาดเกิดขึ้น</string>\n  <string name=\"please_wait\">โปรดรอสักครู่</string>\n  <string name=\"please_wait_long\">โปรดรอ อาจต้องใช้เวลาสักครู่</string>\n  <string name=\"fs_delete_media_path_if_file_error\">ไม่สามารถลบ \\'%s\\' กรุณาลบไฟล์ด้วยตนเองแล้วดาวน์โหลดแบบสำรวจใหม่</string>\n  <string name=\"fs_create_media_folder_error\">ไม่สามารถสร้างแฟ้มมีเดีย \\'%s\\'</string>\n  <string name=\"fs_file_copy_error\">ไม่สามารถคัดลอก \\'%1$s\\' บน \\'%2$s\\' เนื่องจาก %3$s</string>\n  <string name=\"select_odk_shortcut\">เลือก ODK Shortcut</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"settings\">การตั้งค่า</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">คุณดาวน์โหลดแบบสำรวจสองฉบับที่มีชื่อและรุ่นตรงกัน ซึ่งอาจเป็นแบบสำรวจเดียวกันที่ถูกอัพโหลดต่างเวลาหรือต่างเซิร์ฟเวอร์ อย่างไรก็แล้วแต่ ควรลบออกหนึ่งฉบับ</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">แบบสำรวจของท่านถูกบันทึกเป็นฉบับร่าง</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">จบแบบสำรวจฉบับร่างทั้งหมด</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">ยกเลิก</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">ค่าตำแหน่ง/พิกัด</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ti/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <!--Title of the end of form screen-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"ethiopian_month_1\">መስከረም</string>\n  <string name=\"ethiopian_month_2\">ጥቅምቲ</string>\n  <string name=\"ethiopian_month_3\">ሕዳር</string>\n  <string name=\"ethiopian_month_4\">ታኅሳስ</string>\n  <string name=\"ethiopian_month_5\">ጥሪ</string>\n  <string name=\"ethiopian_month_6\">ለካቲት</string>\n  <string name=\"ethiopian_month_7\">መጋቢት</string>\n  <string name=\"ethiopian_month_8\">ምያዝያ</string>\n  <string name=\"ethiopian_month_9\">ግንቦት</string>\n  <string name=\"ethiopian_month_10\">ሰነ</string>\n  <string name=\"ethiopian_month_11\">ሓምለ</string>\n  <string name=\"ethiopian_month_12\">ነሓሰ</string>\n  <string name=\"ethiopian_month_13\">ጳጐሜን</string>\n  <string name=\"persian_month_4\">ጥሪ</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Label for server type setting-->\n  <!--Title of dialog for configuring server URL-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-tl/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Magbukas ng bagong form </string>\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Handa nang ipadala</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Naipadala</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">I-download ang form</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Burahin ang form</string>\n  <string name=\"saved_forms\">I-save ang form</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Added on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Saved on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Niyari noong \\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Sent on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Sending failed on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Tapos na ang pag-scan. Naka-karga na ang lahat ng forms.</string>\n  <string name=\"loading_form\">Kina-karga ang Form</string>\n  <string name=\"parse_error\">Paumanhin, hindi ma-suri ang form.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Nabawi sa savepoint ang mga pagbabagong hindi na-Impukin!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Palitan ang Wika</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Pumunta sa Udyok</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Punta sa Simula</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Punta sa Dulo</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Punta Pataas</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Magdagdag pa</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Pamatnugutan ang Udyok</string>\n  <string name=\"clearanswer_confirm\">Tanggalin ang tugon sa \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Tanggalin ang tugon</string>\n  <string name=\"clear_answer_ask\">Tanggalin ang tugon na ito?</string>\n  <string name=\"clear_answer_no\">Urong</string>\n  <string name=\"discard_answer\">Tanggalin ang tugon</string>\n  <string name=\"discard_group\">Tanggalin ang pangkat</string>\n  <string name=\"delete_repeat\">Tanggalin ang pangkat</string>\n  <string name=\"delete_repeat_ask\">Tanggalin ang Pangkat na Ito?</string>\n  <string name=\"delete_repeat_confirm\">Tanggaling ang pangkat ng \\\"%s\\\" kasama ang pangkat sa ilalim nito?</string>\n  <string name=\"delete_repeat_no\">Urong</string>\n  <string name=\"invalid_answer_error\">Paumanhin, ang tugon na into ay hindi wasto!</string>\n  <string name=\"required_answer_error\">Paumanhin, ang tugon na ito ay kailangan!</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Ikaw ay nasa dulo ng \\\"%s\\\".</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Paumanhin, nabigo ang pag-impok ng form!</string>\n  <string name=\"data_saved_ok\">Matagumpay na inimpok ang Form!</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Impukin ang mga binago</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Ini-impok ang Form</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Ilunsad</string>\n  <string name=\"activity_not_found\">Hindi mapangasiwaan: %s</string>\n  <string name=\"no_app\">Ang hiniling na application ay nawawala. Paki-pasok ng mano-mano ang detalye.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"get_barcode\">Kunin ang Barcode</string>\n  <string name=\"replace_barcode\">Palitan ang Barcode</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Kumuha ng Larawan</string>\n  <string name=\"choose_image\">Pumili ng Imahe</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Impukin at Isara</string>\n  <string name=\"sign_button\">Kalapin ang Pirma</string>\n  <string name=\"markup_image\">Markahan ang Imahe</string>\n  <string name=\"draw_image\">Guhitan ang Imahe</string>\n  <string name=\"reset_image\">I-reset</string>\n  <string name=\"set_color\">I-set ang Kulay</string>\n  <string name=\"capture_video\">Mag-tala ng Bidyo</string>\n  <string name=\"choose_video\">Pumili ng Bidyo</string>\n  <string name=\"play_video\">I-play ang Bidyo</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Mag-tala ng Tunog</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Pumili ng Tunog</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Pumili ng Sagot</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Pinili: </string>\n  <string name=\"trigger\">Sige. Magpatuloy.</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Talaksa: %s ay walang kabuluhan.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Talaksa: %s ay nawawala.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Paumanhin, ang mga provider ng Kinalalagyan ay hindi pinagana!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Pinapadala ang Form</string>\n  <string name=\"noselect_error\">Paumanhin, walang form na pinili!</string>\n  <string name=\"no_connection\">Wala koneksyon sa network</string>\n  <string name=\"sending_items\">Pinapadala ang %1$s sa %2$s na (mga) form</string>\n  <string name=\"send_selected_data\">Ipadala ang mga pinili</string>\n  <string name=\"server_requires_auth\">Nangangailagan ng Authentication ang Server</string>\n  <string name=\"no_forms_uploaded\">Sorry, no form was uploaded.</string>\n  <string name=\"upload_results\">Upload Results</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Palitan ang Tanaw</string>\n  <string name=\"show_sent_and_unsent_forms\">Mga Napadala a at \\'di pa napapadalang Forms</string>\n  <string name=\"show_unsent_forms\">Mga hindi pa napapadalang Forms  lang</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Sariwain</string>\n  <string name=\"download\">I-karga ang Pinili</string>\n  <string name=\"downloading_data\">Kumakabit sa Server</string>\n  <string name=\"form_download_progress\">%1$s. Kinukuha ang talaksang media : %2$s of %3$s</string>\n  <string name=\"version\">Version:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Mga Inimpok na Form</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Mga Blangkong Form</string>\n  <string name=\"delete_confirm\">Burahin ang %s form(s)?</string>\n  <string name=\"delete_file\">Burahin ang mga napili</string>\n  <string name=\"delete_no\">Huwag burahin</string>\n  <string name=\"delete_yes\">Oo, Burahin</string>\n  <string name=\"file_deleted_ok\">%s na (mga) form ay matagumpay na binura!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Server</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Server</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">URL ng Server</string>\n  <string name=\"url_error\">Paumanhin, may mali sa URL!</string>\n  <string name=\"username\">Username</string>\n  <string name=\"change_username\">ODK Username</string>\n  <string name=\"password\">Kontrasenyas</string>\n  <string name=\"change_password\">Hudyat</string>\n  <string name=\"font_size\">Laki ng Teklado</string>\n  <string name=\"change_font_size\">Laki ng Tipo ng Teksto</string>\n  <string name=\"font_size_extra_large\">Labis na Malaki</string>\n  <string name=\"font_size_large\">Malaki</string>\n  <string name=\"font_size_medium\">Katamtaman</string>\n  <string name=\"font_size_small\">Maliit</string>\n  <string name=\"font_size_extra_small\">Labis na Maliit</string>\n  <string name=\"navigation\">Navigation</string>\n  <string name=\"swipe_navigation\">Use horizontal swipes</string>\n  <string name=\"buttons_navigation\">Use forward/backward buttons</string>\n  <string name=\"swipe_buttons_navigation\">Use swipes and buttons</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Next</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Enter new password</string>\n  <string name=\"admin_password_changed\">Admin password successfully changed</string>\n  <string name=\"admin_password_disabled\">Admin password disabled</string>\n  <string name=\"admin_password_incorrect\">Sorry, password is incorrect!</string>\n  <string name=\"enter_admin_password\">Enter Admin Password</string>\n  <string name=\"found_in_main\">Uncheck to hide from Main Menu</string>\n  <string name=\"reset_project_settings_title\">I-reset</string>\n  <string name=\"reset_settings_button_reset\">I-reset</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Urong</string>\n  <string name=\"cancel_loading_form\">Urong</string>\n  <string name=\"cancel_location\">Urong</string>\n  <string name=\"do_not_change\">Urong</string>\n  <string name=\"success\">Tagumpay</string>\n  <string name=\"error_occured\">May Maling Nangyari</string>\n  <string name=\"please_wait\">Mag-hintay ng ilang sandali.</string>\n  <string name=\"please_wait_long\">Mag-hintay. Ito ay maaring tumagal ng ilang minuto.</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-tl-rPH/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources>\n  <string name=\"activity_not_found\">Hindi mapangasiwaan: %s</string>\n  <string name=\"add_repeat\">Mag-dagdag ng Pangkat</string>\n    <string name=\"cancel\">Urong</string>\n  <string name=\"cancel_loading_form\">Urong</string>\n  <string name=\"cancel_location\">Urong</string>\n  <string name=\"capture_audio\">Mag-tala ng Tunog</string>\n  <string name=\"capture_image\">Kumuha ng Larawan</string>\n  <string name=\"capture_video\">Mag-tala ng Bidyo</string>\n  <string name=\"change_language\">Palitan ang Wika</string>\n  <string name=\"change_password\">Hudyat</string>\n  <string name=\"change_username\">ODK Username</string>\n  <string name=\"clearanswer_confirm\">Tanggalin ang tugon sa \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Tanggalin ang tugon</string>\n  <string name=\"clear_answer_ask\">Tanggalin ang tugon na ito?</string>\n  <string name=\"clear_answer_no\">Urong</string>\n  <string name=\"data\">Mga Inimpok na Form</string>\n  <string name=\"data_saved_error\">Paumanhin, nabigo ang pag-impok ng form!</string>\n  <string name=\"data_saved_ok\">Matagumpay na inimpok ang Form!</string>\n  <string name=\"delete_confirm\">Burahin ang %s form(s)?</string>\n  <string name=\"delete_file\">Burahin ang mga napili</string>\n  <string name=\"delete_no\">Huwag burahin</string>\n  <string name=\"delete_repeat\">Tanggalin ang pangkat</string>\n  <string name=\"delete_repeat_ask\">Tanggalin ang Pangkat na Ito?</string>\n  <string name=\"delete_repeat_confirm\">Tanggaling ang pangkat ng \\\"%s\\\" kasama ang pangkat sa ilalim nito?</string>\n  <string name=\"delete_repeat_no\">Urong</string>\n  <string name=\"delete_yes\">Oo, Burahin</string>\n  <string name=\"discard_answer\">Tanggalin ang tugon</string>\n  <string name=\"discard_group\">Tanggalin ang pangkat</string>\n  <string name=\"download\">I-karga ang Pinili</string>\n    <string name=\"downloading_data\">Kumakabit sa Server</string>\n  <string name=\"do_not_change\">Urong</string>\n  <string name=\"do_not_save\">Balewalain ang mga binago</string>\n  <string name=\"enter_data\">Punan ang Blangkong Form</string>\n  <string name=\"error_occured\">May Maling Nangyari</string>\n    <string name=\"file_deleted_ok\">%s na (mga) form ay matagumpay na binura!</string>\n  <string name=\"file_invalid\">Talaksa: %s ay walang kabuluhan.</string>\n  <string name=\"file_missing\">Talaksa: %s ay nawawala.</string>\n  <string name=\"finished_disk_scan\">Tapos na ang pag-scan. Naka-karga na ang lahat ng forms.</string>\n  <string name=\"forms\">Mga Blangkong Form</string>\n    <string name=\"get_barcode\">Kunin ang Barcode</string>\n  <string name=\"get_forms\">Kumuha ng Blangkong Form</string>\n  <string name=\"provider_disabled_error\">Paumanhin, ang mga provider ng Kinalalagyan ay hindi pinagana!</string>\n  <string name=\"invalid_answer_error\">Paumanhin, ang tugon na into ay hindi wasto!</string>\n  <string name=\"jump_to_beginning\">Punta sa Simula</string>\n  <string name=\"jump_to_end\">Punta sa Dulo</string>\n  <string name=\"jump_to_previous\">Punta Pataas</string>\n  <string name=\"loading_form\">Kina-karga ang Form</string>\n  <string name=\"load_remote_form_error\">May Mali sa pag-karga ng Listahan ng Form</string>\n    <string name=\"location_accuracy\">Ang kawastuhan ay %1$s m</string>\n    <string name=\"manage_files\">Burahin ang Inimpok na Form</string>\n  <string name=\"noselect_error\">Paumanhin, walang form na pinili!</string>\n  <string name=\"no_connection\">Wala koneksyon sa network</string>\n  <string name=\"ok\">OK</string>\n  <string name=\"parse_error\">Paumanhin, hindi ma-suri ang form.</string>\n  <string name=\"password\">Kontrasenyas</string>\n  <string name=\"play_video\">I-play ang Bidyo</string>\n  <string name=\"please_wait\">Mag-hintay ng ilang sandali.</string>\n  <string name=\"please_wait_long\">Mag-hintay. Ito ay maaring tumagal ng ilang minuto.</string>\n  <string name=\"refresh\">Sariwain</string>\n  <string name=\"replace_barcode\">Palitan ang Barcode</string>\n  <string name=\"required_answer_error\">Paumanhin, ang tugon na ito ay kailangan!</string>\n  <string name=\"review_data\">Patnugutan ang inimpok na Form</string>\n  <string name=\"save_all_answers\">Impukin ang mga sagot sa Form</string>\n  <string name=\"save_enter_data_description\">Ikaw ay nasa dulo ng \\\"%s\\\".</string>\n  <string name=\"saving_form\">Ini-impok ang Form</string>\n  <string name=\"sending_items\">Pinapadala ang %1$s sa %2$s na (mga) form</string>\n  <string name=\"send_data\">Ipadala ang mga Yaring Form</string>\n  <string name=\"send_selected_data\">Ipadala ang mga pinili</string>\n  <string name=\"server_requires_auth\">Nangangailagan ng Authentication ang Server</string>\n  <string name=\"server_url\">URL</string>\n  <string name=\"server_preferences\">Server</string>\n  <string name=\"trigger\">Sige. Magpatuloy.</string>\n  <string name=\"uploading_data\">Pinapadala ang Form</string>\n  <string name=\"url_error\">Paumanhin, may mali sa URL!</string>\n  <string name=\"username\">Username</string>\n  <string name=\"view_hierarchy\">Pumunta sa Udyok</string>\n  <string name=\"project_settings\">Palitan ang Settings</string>\n  <string name=\"client\">Client</string>\n  <string name=\"keep_changes\">Impukin ang mga binago</string>\n  <string name=\"font_size\">Laki ng Teklado</string>\n  <string name=\"change_font_size\">Laki ng Tipo ng Teksto</string>\n  <string name=\"form_download_progress\">%1$s. Kinukuha ang talaksang media : %2$s of %3$s</string>\n    <string name=\"success\">Tagumpay</string>\n  <string name=\"choose_sound\">Pumili ng Tunog</string>\n  <string name=\"choose_video\">Pumili ng Bidyo</string>\n  <string name=\"choose_image\">Pumili ng Imahe</string>\n  <string name=\"selected_title\">Pinili: </string>\n  <string name=\"select_answer\">Pumili ng Sagot</string>\n  <string name=\"edit_prompt\">Pamatnugutan ang Udyok</string>\n  <string name=\"font_size_extra_large\">Labis na Malaki</string>\n  <string name=\"font_size_large\">Malaki</string>\n  <string name=\"font_size_medium\">Katamtaman</string>\n  <string name=\"font_size_small\">Maliit</string>\n  <string name=\"font_size_extra_small\">Labis na Maliit</string>\n  <string name=\"change_view\">Palitan ang Tanaw</string>\n  <string name=\"show_sent_and_unsent_forms\">Mga Napadala a at \\'di pa napapadalang Forms</string>\n  <string name=\"show_unsent_forms\">Mga hindi pa napapadalang Forms  lang</string>\n  <!--http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Niyari noong \\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"launch_app\">Ilunsad</string>\n  <string name=\"no_app\">Ang hiniling na application ay nawawala. Paki-pasok ng mano-mano ang detalye.</string>\n  <string name=\"savepoint_used\">Nabawi sa savepoint ang mga pagbabagong hindi na-Impukin!</string>\n  <string name=\"save_and_close\">Impukin at Isara</string>\n  <string name=\"sign_button\">Kalapin ang Pirma</string>\n  <string name=\"markup_image\">Markahan ang Imahe</string>\n  <string name=\"draw_image\">Guhitan ang Imahe</string>\n  <string name=\"reset_image\">I-reset</string>\n  <string name=\"set_color\">I-set ang Kulay</string>\n    <string name=\"save_mid\">Impukin ang mga sagot sa Form</string>\n    <!--Geo string added in feature-geofeatures branch-->\n  <!--&lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-tr/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">Yeni form aç</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">Taslaklar</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">Gönderilmeye hazır</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">Gönderildi</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">Form indir</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">Form sil</string>\n  <string name=\"saved_forms\">Kayıtlı Formlar</string>\n  <string name=\"version_number\">Versiyon 1 %s</string>\n  <string name=\"id_number\">ID: 1%s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"submission_deleted\">Gönderim silindi</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"encrypted_form\">Şifrelenmiş form</string>\n  <string name=\"deleted_form\">Silinmiş form</string>\n  <string name=\"sort_the_list\">Listeyi sırala</string>\n  <string name=\"sort_by\">Sırala</string>\n  <string name=\"sort_by_name_asc\">İsim, A-Z</string>\n  <string name=\"sort_by_name_desc\">İsim, Z-A</string>\n  <string name=\"sort_by_date_desc\">Tarih, en yeni üstte</string>\n  <string name=\"sort_by_date_asc\">Tarih, en eski üstte</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">Son kaydedileni ilk göster</string>\n  <string name=\"filter_the_list\">Listeyi filtrele</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Arama</string>\n  <string name=\"storage_runtime_permission_denied_title\">Saklama izinleri</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Bu izinler olmadan Collect formlarınıza erişemez ya da cevapları saklayamaz. İzinleri vermek için hazır olduğunuzda uygulamayı tekrar açın.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Konum izinleri</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">Kamera izinleri</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Ses kaydetme izni</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">Form haritasını aç</string>\n  <string name=\"instance_scan_count\">%1$d adet form eklendi.</string>\n  <string name=\"finished_disk_scan\">Tarama bitti. Bütün Formlar Yüklendi.</string>\n  <string name=\"loading_form\">Form Yükleniyor</string>\n  <string name=\"survey_loading_reading_form_message\">Form tanımı okunuyor…</string>\n  <string name=\"survey_loading_reading_data_message\">Anket verisi okunuyor…</string>\n  <string name=\"survey_loading_reading_csv_message\">CSV dosyası okunuyor…</string>\n  <string name=\"parse_error\">Form ayrıştırılamadi.</string>\n  <string name=\"loading_form_failed\">Form yüklenirken bir hata oluştu. Lütfen tekrar deneyiniz.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Dil Değiştir</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">Taslak olarak kaydet</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Başlangıca Git</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Sona git</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Yukarı git</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Grup</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Tekrarlı grup</string>\n  <string name=\"exit\">Çıkış</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">Ekle %s ?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">Ekle</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Bir daha ekle</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"clearanswer_confirm\">Bu yanıt kaldırılsın mı \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Yanıtları kaldır</string>\n  <string name=\"clear_answer_ask\">Bu yanıt kaldırılsın mı ?</string>\n  <string name=\"clear_answer_no\">Vazgeç</string>\n  <string name=\"discard_answer\">Yanıtları kaldır</string>\n  <string name=\"discard_group\">Grubu sil</string>\n  <string name=\"delete_repeat\">Grubu Sil</string>\n  <string name=\"delete_repeat_ask\">Bu grubu sil ?</string>\n  <string name=\"delete_repeat_confirm\">Bu grupla beraber \\\"%s\\\" alt grubuda silinsin mi ?</string>\n  <string name=\"delete_repeat_no\">Vazgeç</string>\n  <string name=\"invalid_answer_error\">Üzgünüz, bu cevap geçersiz</string>\n  <string name=\"required_answer_error\">Üzgünüz, bu cevap gerekli</string>\n  <string name=\"success_form_validation\">Başarılı, Formunuzda hata bulunmuyor.</string>\n  <!--Title of the end of form screen-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">Taslak olarak kaydet</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">Sonlandır.</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">Gönder</string>\n  <string name=\"data_saved_error\">Form kaydedilirken hata oluştu</string>\n  <string name=\"data_saved_ok\">From başarılı bir şekilde saklandi</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">Formu iptal et.</string>\n  <string name=\"discard_changes\">Değişiklikleri iptal et</string>\n  <string name=\"keep_changes\">Değişiklikleri Kaydet</string>\n  <string name=\"keep_editing\">Düzenlemeye devam et</string>\n  <string name=\"quit_form_title\">Formu kaydetmek istiyor musunuz?</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"quit_form_continue_title\">Düzenlemeye devam etmek istiyor musunuz?</string>\n  <string name=\"discard_form_warning\">Formu iptal ederseniz, şu ana kadar yaptığınız değişiklikleri kaybedeceksiniz.</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Form Saklaniyor</string>\n  <string name=\"survey_saving_validating_message\">Cevaplar doğrulanıyor…</string>\n  <string name=\"survey_saving_collecting_message\">Veri toplanıyor…</string>\n  <string name=\"survey_saving_saving_message\">SD karta kaydediliyor…</string>\n  <string name=\"survey_saving_finalizing_message\">SD karta tamamlanıyor…</string>\n  <string name=\"survey_saving_encrypting_message\">Veri şifreleniyor…</string>\n  <string name=\"invalid_rsa_public_key\">Hatalı RSA açık anahtar.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">Kaydetmeye başlanamadı.</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">Mikrofon zaten kullanımda.</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Başla</string>\n  <string name=\"activity_not_found\">%s işleme alacak activite bulunamadı.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">URL Aç</string>\n  <string name=\"capture_osm\">OpenMapKit\\'i başlat</string>\n  <string name=\"recapture_osm\">OSM etiketini tekrar yap</string>\n  <string name=\"alert\">İkaz</string>\n  <string name=\"install_openmapkit\">Lütfen OpenMapKit\\'i yükleyiniz!</string>\n  <string name=\"get_barcode\">Barkod Al</string>\n  <string name=\"replace_barcode\">Barkod Değiştir</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Dosya aç</string>\n  <string name=\"capture_image\">Resim Çek</string>\n  <string name=\"choose_image\">Fotoğraf Seçimi</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Sakla ve Kapat</string>\n  <string name=\"capture_video\">Video Kayit</string>\n  <string name=\"choose_video\">Video Seçimi</string>\n  <string name=\"play_video\">Video Oynat</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Ses Kaydi</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Seskaydi Seçimi</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Cevabiniz </string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Seçilen:</string>\n  <string name=\"trigger\">Tamam. Devam edin. Bekleme yapmayin </string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Dosya : %s geçersiz</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Dosya : %s kayip</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Form Gönderiliyor</string>\n  <string name=\"noselect_error\">Herhangi bir form seçilmedi !</string>\n  <string name=\"no_connection\">Network bağlantisi yok</string>\n  <string name=\"sending_items\">%1$s ile %2$s form(s) gönderiliyor</string>\n  <string name=\"send_selected_data\">Seçili olanı gönder</string>\n  <string name=\"server_requires_auth\">Server kimlik doğrulaması gerektirir</string>\n  <string name=\"no_forms_uploaded\">Herhangi bir form gönderilmedi.</string>\n  <string name=\"upload_results\">Gönderme Sonucu</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"show_sent_and_unsent_forms\">Gönderilen ve Gönderilmeyen Formlari Göster</string>\n  <string name=\"show_unsent_forms\">Gönderilmeyen Formlari Gönder</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Yenile</string>\n  <string name=\"download\">Seçilenleri getir</string>\n  <string name=\"downloading_data\">Server\\'a bağlaniyor</string>\n  <string name=\"form_download_progress\">%1$s. Alinan medya dosyasi : %2$s of %3$s</string>\n  <string name=\"version\">Version:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Saklanan Formlar</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Boş Formlar</string>\n  <string name=\"delete_confirm\">silinecek %s form var. Silinsin mi ?</string>\n  <string name=\"delete_file\">Seçileni Sil</string>\n  <string name=\"delete_no\">Silme</string>\n  <string name=\"delete_yes\">Formlari Sil</string>\n  <string name=\"file_deleted_ok\">%s Form başarıyla silindi.</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Sunucu Ayarlari</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"url_error\">Geçersiz URL</string>\n  <string name=\"username\">Kullanici Adi</string>\n  <string name=\"change_username\">ODK Kullanici Adi</string>\n  <string name=\"password\">Şifre</string>\n  <string name=\"change_password\">ODK Şifresi</string>\n  <string name=\"font_size\">Text font büyüklüğü</string>\n  <string name=\"change_font_size\">Text font büyüklüğü</string>\n  <string name=\"font_size_extra_large\">Çok Büyük</string>\n  <string name=\"font_size_large\">Büyük</string>\n  <string name=\"font_size_medium\">Orta</string>\n  <string name=\"font_size_small\">Küçük</string>\n  <string name=\"font_size_extra_small\">Çok Küçük</string>\n  <string name=\"navigation\">navigasyon</string>\n  <string name=\"buttons_navigation\">ileri/geri butonlarini kullan </string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">İleri</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_no\">Hayır</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Yeni Şifreyi Giriniz</string>\n  <string name=\"admin_password_changed\">Yönetici Şifresi Başarili şekilde Değiştirildi</string>\n  <string name=\"admin_password_disabled\">Yönetici Şifresi Kapatılmış</string>\n  <string name=\"admin_password_incorrect\">Yanliş Şifre</string>\n  <string name=\"enter_admin_password\">Yönetici Şifresi Giriniz</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"yes\">Evet</string>\n  <string name=\"no\">Hayır</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">Taslak olarak kaydet</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">Tamam</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Vazgeç</string>\n  <string name=\"cancel_loading_form\">Vazgeç</string>\n  <string name=\"cancel_location\">Vazgeç</string>\n  <string name=\"do_not_change\">Vazgeç</string>\n  <string name=\"success\">Başarılı</string>\n  <string name=\"error_occured\">Hata Oluştu</string>\n  <string name=\"please_wait\">Biraz Bekleyin..</string>\n  <string name=\"please_wait_long\">Biraz sürecek gibi.. Az kaldi</string>\n  <string name=\"loading\">Yükleniyor…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">Projeler</string>\n  <string name=\"settings\">Ayarlar</string>\n  <string name=\"add_project\">Proje ekle</string>\n  <string name=\"add\">Ekle</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">Mükerrer proje</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">Projenin kopyasını ekle</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">Kayıtlı bir projeye geç</string>\n  <string name=\"project_name\">Proje adı</string>\n  <string name=\"project_icon\">Proje ikonu</string>\n  <string name=\"project_color\">Proje rengi</string>\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"or\">YA DA</string>\n  <string name=\"hex_color\">Hex renk kodu</string>\n  <string name=\"invalid_hex_code\">Hatalı hex kodu</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">QR kod ile kurulum yap</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">Proje detaylarını manuel ekle</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">Hala projeniz yok mu?</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">Demoyu dene</string>\n  <string name=\"scan_configuration_qr_code\">Ayar QR kodunu tarat</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">Detayları göster</string>\n  <string name=\"errors\">Hatalar</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Tanınmayan URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Hatalı URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">Doldurulmuş form silindi!</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">Ayarları aç</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">Hatalar</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-uk/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <string name=\"version_number\">Версія: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Додана\\' EEE, dd MMM, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Збережена\\' EEE, dd MMM, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Завершена\\' EEE, dd MMM, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Надіслана\\' EEE, dd MMM, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Помилка надсилання\\' EEE, dd MMM, yyyy \\'в\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'Видалена\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <string name=\"submission_deleted\">Надсилання видалене</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'Зімінено в\\' EEE, dd MMM, yyyy \\'at\\' HH:mm</string>\n  <string name=\"encrypted_form\">Зашифровані анкети</string>\n  <string name=\"deleted_form\">Видалені анкети</string>\n  <string name=\"sort_the_list\">Відсортувати список</string>\n  <string name=\"sort_by\">Сортувати за</string>\n  <string name=\"sort_by_name_asc\">За ім\\'ям, А-Я</string>\n  <string name=\"sort_by_name_desc\">За ім\\'ям, Я-А</string>\n  <string name=\"sort_by_date_desc\">За датою, найновіший перший</string>\n  <string name=\"sort_by_date_asc\">За датою, найстарший перший</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">Фільтрувати список</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">Пошук</string>\n  <string name=\"storage_runtime_permission_denied_title\">Дозволи сховища</string>\n  <string name=\"storage_runtime_permission_denied_desc\">Без цих дозволів Collect не матиме дотупу до ваших анкет (читання, запис відповідей). Відкрийте програму знову, ящо ви вже їх надали.</string>\n  <string name=\"location_runtime_permissions_denied_title\">Місцеві дозволи</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"camera_runtime_permission_denied_title\">Дозвіл використання камери</string>\n  <string name=\"camera_runtime_permission_denied_desc\">Без цього дозволу Collect не має доступу до камери. Будь ласка, спробуйте знову коли будете готові надати його.</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">Дозвіл на аудіозапис</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">Без цього дозволу Collect не має доступу до мікрофону. Будь ласка, спробуйте знову коли будете готові надати його.</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"instance_scan_count\">%1$d анкет додано.</string>\n  <string name=\"finished_disk_scan\">Сканування завершене. Всі анкети завантажені.</string>\n  <string name=\"xform_parse_error\">Помилка XForm парсингу в %1$s: %2$s пропущений або некоректний. </string>\n  <string name=\"loading_form\">Завантаження анкети</string>\n  <string name=\"survey_loading_reading_form_message\">Читання анкети…</string>\n  <string name=\"survey_loading_reading_data_message\">Читання даних опитування…</string>\n  <string name=\"survey_loading_reading_csv_message\">Читання CSV файлів…</string>\n  <string name=\"parse_error\">Вибачте, неможливо розібрати анкету.</string>\n  <string name=\"loading_form_failed\">Під час завантаження анкети виникла помилка. Будь ласка спробуйте знову.</string>\n  <string name=\"cannot_edit_completed_form\">Після позначення завершеною ця анкета не може бути відредагована. Може бути зашифрованою.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Незбережені зміни відновлені з місця збереження!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Змінити мову</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Перейти</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Перейти на початок</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Перейти в кінець</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Перейти назад</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">Група</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">Повторюєма Група</string>\n  <string name=\"exit\">Вийти</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">Додати</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Редагувати запит</string>\n  <string name=\"clearanswer_confirm\">Видалити відповідь на \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Видалити відповідь</string>\n  <string name=\"clear_answer_ask\">Видалити цю відповідь?</string>\n  <string name=\"clear_answer_no\">Скасувати</string>\n  <string name=\"discard_answer\">Видалити відповідь</string>\n  <string name=\"discard_group\">Видалити групу</string>\n  <string name=\"delete_repeat\">Видалити групу</string>\n  <string name=\"delete_repeat_ask\">Видалити цю групу?</string>\n  <string name=\"delete_repeat_confirm\">Видалити групу \\\"%s\\\" та всі її підгрупи?</string>\n  <string name=\"delete_repeat_no\">Скасувати</string>\n  <string name=\"invalid_answer_error\">Вибачте, ця відповідь некоректна!</string>\n  <string name=\"required_answer_error\">Вибачте, але ця відповідь обов`язкова!</string>\n  <string name=\"invalid_space_in_answer_singular\">Попередження: основне значення %s містить пробіли</string>\n  <string name=\"invalid_space_in_answer_plural\">Попередження: основні значення %s містять пробіли</string>\n  <string name=\"survey_internal_error\">Внутрішня помилка: невдалий перехід.</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Ви наприкінці %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Вибачте, не вдалось зберегти анкету!</string>\n  <string name=\"data_saved_ok\">Анкету успішно збережено!</string>\n  <string name=\"save_point_error\">Помилка при збереженні точки відновлення: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Зберегти зміни</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Зберігаю анкету</string>\n  <string name=\"survey_saving_validating_message\">Перевірка відповіді…</string>\n  <string name=\"survey_saving_collecting_message\">Збір даних…</string>\n  <string name=\"survey_saving_saving_message\">Збереження на SD картку…</string>\n  <string name=\"survey_saving_finalizing_message\">Завершення на SD картку…</string>\n  <string name=\"survey_saving_encrypting_message\">Шифрування даних…</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">Більше одного запису для цього екземпляру анкети!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">Більше однієї чистої анкети відповідають jr_form_id.</string>\n  <string name=\"no_form_id_specified\">Не зазначений FormId???</string>\n  <string name=\"phone_does_not_support_rsa\">Телефон не підтримує RSA кодування.</string>\n  <string name=\"invalid_rsa_public_key\">Некоректний відкритий ключ RSA.</string>\n  <string name=\"encryption_error_message\">%s анкета не була збережена як завершена.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Запустити</string>\n  <string name=\"activity_not_found\">Не знайдено жодного процесу для: %s</string>\n  <string name=\"no_app\">Потрібна програма відсутня. Будь ласка, введіть показники вручну.</string>\n  <string name=\"null_intent_value\">Зовнішня програма не надала інформацію, що очікувалась.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">Відкрити посилання</string>\n  <string name=\"capture_osm\">Запустити OpenMapKit</string>\n  <string name=\"recapture_osm\">Зробити наново OSM тегування</string>\n  <string name=\"edited_osm_file\">Відредагований OSM XML файл:</string>\n  <string name=\"invalid_osm_data\">Щось пішло не так. Ми не отримали коректні OSM дані.</string>\n  <string name=\"alert\">Тривога</string>\n  <string name=\"install_openmapkit\">Будь ласка встановіть OpenMapKit!</string>\n  <string name=\"get_barcode\">Зісканувати штрих-код</string>\n  <string name=\"replace_barcode\">Змінити штрих-код</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">Розмістіть штрихкод в прямокутнику</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">Відкрити файл</string>\n  <string name=\"rank_items\">Засоби оцінки</string>\n  <string name=\"invalid_range_widget\">Цей віджет має некоректні параметри!</string>\n  <string name=\"svg_file_does_not_exist\">SVG файл не існує!</string>\n  <string name=\"capture_image\">Сфотографувати</string>\n  <string name=\"choose_image\">Оберіть зображення</string>\n  <string name=\"selected_invalid_image\">Обраний файл не є коректним зображенням</string>\n  <string name=\"take_picture_instruction\">Щоб зробити знімок торкніться екрану</string>\n  <string name=\"error_front_camera_unavailable\">Фронтальна камера на цьому пристрої не доступна</string>\n  <string name=\"annotate_image\">Додати Анотацію</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Зберегти та закрити</string>\n  <string name=\"signature_capture\">Сфотографувати Підпис</string>\n  <string name=\"sign_button\">Отримати підпис</string>\n  <string name=\"markup_image\">Розмітка зображення</string>\n  <string name=\"draw_image\">Ескіз зображення</string>\n  <string name=\"reset_image\">Скинути</string>\n  <string name=\"set_color\">Колір</string>\n  <string name=\"capture_video\">Записати відео</string>\n  <string name=\"choose_video\">Оберіть відео</string>\n  <string name=\"play_video\">Відтворити відео</string>\n  <string name=\"choose_file\">Обрати файл</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Записати звук</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Оберіть аудіо</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">При додаванні медіафайлу: %s сталась помилка</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">Обрати заначення</string>\n  <string name=\"select_answer\">Обрати відповідь</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">Редагувати значення</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">Значення не обране</string>\n  <string name=\"selected_title\">Обрано:</string>\n  <string name=\"trigger\">Добре. Будь ласка, продовжуйте.</string>\n  <string name=\"ext_file_no_data_error\">Файл не містить даних!</string>\n  <string name=\"ext_conflicting_columns_error\">Стовбці %s співпадають!</string>\n  <string name=\"ext_sortBy_numeric_error\">Стобчик для сортування має містити тільки цифрові значення. Конфліктне значення \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">Синтаксична помилка в функції search(): Функція має містити 1, 4 або 6 аргументів.</string>\n  <string name=\"ext_search_wrong_function_error\">Синтаксична помилка в функції search(): Функція не розпізнана \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">Синтаксична помилка в функції search(): \\'%s\\' не функція.</string>\n  <string name=\"ext_search_return_error\">Пошук повернув об’єкт типу \\'%s\\'.</string>\n  <string name=\"ext_import_generic_error\">Не можливо імпортувати дані з %1$s. Причина: %2$s</string>\n  <string name=\"ext_import_progress_message\">Передзавантаження даних з \\'%1$s\\', будь ласка, зачекайте… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">Читання даних скасовано!</string>\n  <string name=\"ext_import_finalizing_message\">Завершення передзавантажених даних…</string>\n  <string name=\"ext_import_completed_message\">Читання даних завершене!</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager не ініціалізований.</string>\n  <string name=\"ext_import_csv_missing_error\">Зовнішні дані для %1$s не імпортовані. Можливо ви забули підключити в анкеті %2$s.csv файл?</string>\n  <string name=\"ext_search_generic_error\">Синтаксична помилка в функціі search(): %s</string>\n  <string name=\"ext_assign_value_error\">Неможливо присвоїти значення в \\'%s\\'.</string>\n  <string name=\"parser_exception\">XPathParser Exception: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">Записати пеленг</string>\n  <string name=\"replace_bearing\">Змінити пеленг</string>\n  <string name=\"getting_bearing\">Завантажити пеленг</string>\n  <string name=\"accept_bearing\">Записати пеленг</string>\n  <string name=\"bearing\">Пеленг: %.3f</string>\n  <string name=\"direction\">Напрямок: %s</string>\n  <string name=\"bearing_lack_of_sensors\">Неможливо записати пеленг: у пристрої відсутній акселерометр, сенсор магнітного поля чи обидва.</string>\n  <string name=\"select_date\">Обрати дату</string>\n  <string name=\"select_time\">Обрати час</string>\n  <string name=\"no_date_selected\">Дата не обрана</string>\n  <string name=\"no_time_selected\">Час не обраний</string>\n  <string name=\"ethiopian_month_1\">Мескерем</string>\n  <string name=\"ethiopian_month_2\">Тікімт</string>\n  <string name=\"ethiopian_month_3\">Хідар</string>\n  <string name=\"ethiopian_month_4\">Тасас</string>\n  <string name=\"ethiopian_month_5\">Тір</string>\n  <string name=\"ethiopian_month_6\">Йекатіт</string>\n  <string name=\"ethiopian_month_7\">Мегабіт</string>\n  <string name=\"ethiopian_month_8\">Міазіа</string>\n  <string name=\"ethiopian_month_9\">Гінбот</string>\n  <string name=\"ethiopian_month_10\">Сені</string>\n  <string name=\"ethiopian_month_11\">Хамлі</string>\n  <string name=\"ethiopian_month_12\">Нехасі</string>\n  <string name=\"ethiopian_month_13\">Пагумін</string>\n  <string name=\"coptic_month_1\">Тоут</string>\n  <string name=\"coptic_month_2\">Паопі</string>\n  <string name=\"coptic_month_3\">Хатор</string>\n  <string name=\"coptic_month_4\">Кояк</string>\n  <string name=\"coptic_month_5\">Тобі</string>\n  <string name=\"coptic_month_6\">Мешір</string>\n  <string name=\"coptic_month_7\">Паремат</string>\n  <string name=\"coptic_month_8\">Мармуті</string>\n  <string name=\"coptic_month_9\">Пашонс</string>\n  <string name=\"coptic_month_10\">Паоні</string>\n  <string name=\"coptic_month_11\">Епіп</string>\n  <string name=\"coptic_month_12\">Месорі</string>\n  <string name=\"coptic_month_13\">Пі Когі Енавот</string>\n  <string name=\"islamic_month_1\">Мухарам</string>\n  <string name=\"islamic_month_2\">Сафар</string>\n  <string name=\"islamic_month_3\">Рабі аль-аваль</string>\n  <string name=\"islamic_month_4\">Рабі аль-тані</string>\n  <string name=\"islamic_month_5\">Джумада аль-аваль</string>\n  <string name=\"islamic_month_6\">Джумада аль-тані</string>\n  <string name=\"islamic_month_7\">Раджаб</string>\n  <string name=\"islamic_month_8\">Сабан</string>\n  <string name=\"islamic_month_9\">Рамадан</string>\n  <string name=\"islamic_month_10\">Шаваль</string>\n  <string name=\"islamic_month_11\">Ду аль-кідах</string>\n  <string name=\"islamic_month_12\">Ду аль-хіджах</string>\n  <string name=\"persian_month_4\">Тір</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Файл: %s є невідповідним.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Файл: %s не знайдено.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS топографічна національна карта</string>\n  <string name=\"openmap_usgs_sat\">USGS Національна мапа Гібрид</string>\n  <string name=\"openmap_usgs_img\">USGS Національна мапа Картинки</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">Базова мапа</string>\n  <string name=\"streets\">Вулиці</string>\n  <string name=\"terrain\">Ландшафт</string>\n  <string name=\"hybrid\">Змішана</string>\n  <string name=\"satellite\">Супутник</string>\n  <string name=\"google_play_services_error_occured\">Нема доступу до Google Карт. Чи встановлено Google Play Services?</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">Записати точку</string>\n  <string name=\"provider_disabled_error\">Вибачте! Джерела місцезнаходження не доступні!</string>\n  <string name=\"google_play_services_not_available\">Ця анкета хоче відслідковувати ваше розташування але Сервіси Google Play не доступні.</string>\n  <string name=\"location_providers_disabled_dialog_message\">Ця анкета хоче відслідковувати ваше розташування але розташування вимкнене. Будь ласка, увімкніть його в налаштуваннях Android.</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">Перейти в налаштування</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"background_location_disabled\">Ця анкета хоче відслідковувати ваше розташування але відслідковування вимкнене. Будь ласка, увімкніть його в %1$s меню.</string>\n  <string name=\"background_location_enabled\">Ця анкета відслідковує ваше розташування. Ви можете вимкнути відслідковування в %1$s меню.</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">Захід %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">Схід %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">Південь %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">Північ %1$s%2$s%3$s</string>\n  <string name=\"input_method\">Метод введення</string>\n  <string name=\"start\">Почати</string>\n  <string name=\"enable_gps\">Увімкнути GPS</string>\n  <string name=\"geo_clear_warning\">Ознака вже створена. Бажаєте очистити ознаку?</string>\n  <string name=\"geo_exit_warning\">Скасувати зміни і повернутись до ODK?</string>\n  <string name=\"polygon_validator\">Для багатокутника необхідно мінімум 3 точки</string>\n  <string name=\"polyline_validator\">Для створення лінії необхідні принаймні дві точки</string>\n  <string name=\"clear\">Очистити</string>\n  <string name=\"placement_mode\">Розмішення тиком</string>\n  <string name=\"manual_mode\">Ручний запис розташування</string>\n  <string name=\"automatic_mode\">Автоматичний запис розташування</string>\n  <string name=\"recording_interval\">Інтервал запису</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d секунда</item>\n    <item quantity=\"few\">%d секунди</item>\n    <item quantity=\"many\">%d секунд</item>\n    <item quantity=\"other\">%d секунд</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d хвилина</item>\n    <item quantity=\"few\">%d хвилини</item>\n    <item quantity=\"many\">%d хвилин</item>\n    <item quantity=\"other\">%d хвилин</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">Вимога до точності</string>\n  <string name=\"none\">Немає</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\">%d метр</item>\n    <item quantity=\"few\">%d метри</item>\n    <item quantity=\"many\">%d метрів</item>\n    <item quantity=\"other\">%d метрів</item>\n  </plurals>\n  <string name=\"collection_status_paused\">Введених точок: %d</string>\n  <string name=\"collection_status_placement\">Введених точок: %d (ткніть для розміщення точок)</string>\n  <string name=\"collection_status_manual\">Введених точок: %d (ручний запис)</string>\n  <string name=\"collection_status_auto_seconds\">Введених точок: %1$d (запис кожні %2$d сек)</string>\n  <string name=\"collection_status_auto_minutes\">Введених точок: %1$d (запис кожні %2$d хв)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">Введених точок: %1$d (запис кожні %2$d сек, точність %3$d м)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">Введених точок: %1$d (запис кожні %2$d хв, точність %3$d м)</string>\n  <string name=\"geopoint_no_draggable_instruction\">Натисніть кнопку Додати позначку.</string>\n  <string name=\"discard\">Скинути</string>\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Надсилаю анкету</string>\n  <string name=\"noselect_error\">Вибачте, Ви не обрали жодної анкети!</string>\n  <string name=\"no_connection\">Мережеві з’єднання не доступні</string>\n  <string name=\"sending_items\">Надсилаю %1$s з %2$s анкет(и)</string>\n  <string name=\"send_selected_data\">Надіслати обране</string>\n  <string name=\"server_auth_credentials\">Неправильні ім\\'я користувача або пароль до сервера: %s</string>\n  <string name=\"server_requires_auth\">Для доступу до сервера необхідно авторизуватись</string>\n  <string name=\"no_forms_uploaded\">Вибачте, жодна анкета не була вивантажена.</string>\n  <string name=\"upload_results\">Вивантаження результатів</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Змінити вигляд</string>\n  <string name=\"show_sent_and_unsent_forms\">Показати надіслані та ненадіслані анкети</string>\n  <string name=\"show_unsent_forms\">Показати ненадіслані анкети</string>\n  <string name=\"send_in_progress\">Зпущений тіньовий процес відправки, будь ласка, спробуйте знову пізніше</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Оновити</string>\n  <string name=\"download\">Отримати обране</string>\n  <string name=\"downloading_data\">Під`єднуюсь до сервера</string>\n  <string name=\"fetching_file\">Отримую %1$s.\\n\\nанкету %2$s з %3$s анкет(и).</string>\n  <string name=\"form_download_progress\">%1$s. Отримання медіа файлу: %2$s з %3$s</string>\n  <string name=\"version\">Версія:</string>\n  <string name=\"form_updates_available\">Доступні оновлення анкет</string>\n  <string name=\"newer_version_of_a_form_info\">Це оновлення анкети яку ви вже маєте</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Збережені анкети</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Незаповнені анкети</string>\n  <string name=\"delete_confirm\">Видалити %s анкету(и)?</string>\n  <string name=\"delete_file\">Видалити обране</string>\n  <string name=\"delete_no\">Не видаляти</string>\n  <string name=\"delete_yes\">Видалити анкети</string>\n  <string name=\"form_delete_message\">Видалення обраних анкет</string>\n  <string name=\"file_deleted_ok\">%s анкет(у) успішно видалено!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Сервер</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"client\">Користувацький інтерфейс</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">Управління анкетою</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">Ідентифікація користувача та пристрою</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Налаштування сервера</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">Адреса URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Адреса серверу URL</string>\n  <string name=\"url_error\">Вибачте, посилання недійсне!</string>\n  <string name=\"username\">Ім\\'я користувача</string>\n  <string name=\"change_username\">Ім\\'я користувача ODK</string>\n  <string name=\"password\">Пароль</string>\n  <string name=\"change_password\">Пароль ODK</string>\n  <string name=\"password_error_whitespace\">в паролях заборонені початкові та кінцеві пробіли</string>\n  <string name=\"username_error_whitespace\">в імені користувача заборонені початкові та кінцеві пробіли</string>\n  <string name=\"app_theme\">Тема</string>\n  <string name=\"language\">Мова</string>\n  <string name=\"use_device_language\">Використовувати мову пристрою</string>\n  <string name=\"font_size\">Розмір шрифту</string>\n  <string name=\"change_font_size\">Розмір шрифту</string>\n  <string name=\"font_size_extra_large\">Дуже великий</string>\n  <string name=\"font_size_large\">Великий</string>\n  <string name=\"font_size_medium\">Середній</string>\n  <string name=\"font_size_small\">Маленький</string>\n  <string name=\"font_size_extra_small\">Дуже маленький</string>\n  <string name=\"navigation\">Навігація</string>\n  <string name=\"swipe_navigation\">Використовувати горизонтальну прокрутку</string>\n  <string name=\"buttons_navigation\">Використовувати кнопки вперед/назад</string>\n  <string name=\"swipe_buttons_navigation\">Використовувати прокрутку та кнопки</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Далі</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">Повернутись</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">Оновлення анкети</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">Кожних п\\'ятнадцять хвилин</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">Щогодини</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">Кожні шість годин</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">Кожні 24 години</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">Автоматичне завантаження</string>\n  <string name=\"automatic_download_summary\">Автоматично завантажувати нові версії анкет</string>\n  <string name=\"hide_old_form_versions_setting_title\">Приховати старі версії анкет</string>\n  <string name=\"hide_old_form_versions_setting_summary\">Тільки найновіша версія буде відображена в Заповнити анкету</string>\n  <string name=\"form_submission_category\">Відправка анкети</string>\n  <string name=\"autosend_selector_title\">Автоматичне надсилання</string>\n  <string name=\"autosend\">Автоматичне надсилання</string>\n  <string name=\"off\">Вимкнена</string>\n  <string name=\"wifi_autosend\">Тільки Wi-Fi</string>\n  <string name=\"cellular_autosend\">Тільки мобільна мережа</string>\n  <string name=\"wifi_cellular_autosend\">Wi-Fi або мобільна мережа</string>\n  <string name=\"delete_after_send\">Видалити після надсилання</string>\n  <string name=\"delete_after_send_summary\">Видаляє завершені анкети та медіа файли після надсилання на сервер</string>\n  <string name=\"form_filling_category\">Заповнення анкети</string>\n  <string name=\"constraint_behavior_title\">Обробка обмежень</string>\n  <string name=\"constraint_behavior\">Обмеження зумовлюють поведінку</string>\n  <string name=\"constraint_behavior_on_swipe\">Перевіряти підчас переходу вперед</string>\n  <string name=\"constraint_behavior_on_finalize\">Відкласти перевірку до завершення</string>\n  <string name=\"high_resolution_title\">Відео високої роздільної здатності</string>\n  <string name=\"high_resolution_summary\">Дозволяє запис відео високої якості</string>\n  <string name=\"image_size_title\">Розмір зображення</string>\n  <string name=\"image_size_dialog_title\">Максимальна кількість пікселів по довгій стороні малюнку</string>\n  <string name=\"image_size_large\">Великий (3072px)</string>\n  <string name=\"image_size_medium\">Середній (2048px)</string>\n  <string name=\"image_size_small\">Маленький (1024px)</string>\n  <string name=\"image_size_very_small\">Дуже маленький (640px)</string>\n  <string name=\"image_size_original\">Розмір визначений камерою (за замовчуванням)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">Відображати пояснення до питань</string>\n  <string name=\"guidance_no\">Ні</string>\n  <string name=\"guidance_yes\">Так - завжди відображуються</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">Так - згорнуті</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"form_import_category\">Імпорт анкети</string>\n  <string name=\"instance_sync\">Завершувати форми під час імпорту</string>\n  <string name=\"instance_sync_summary\">Брати до уваги форми додані безпосередньо до директорії екземплярів.</string>\n  <string name=\"analytics_preferences\">Дані про використання</string>\n  <string name=\"analytics\">Анонімно збирати дані про використання</string>\n  <string name=\"form_metadata_title\">Метаданні Анкети</string>\n  <string name=\"form_metadata\">Метаданні анкети</string>\n  <string name=\"form_metadata_summary\">Ці значення будуть додані до анкет, що мають поля ім\\'я користувача, електронна адреса та/або телефон для ідентифікації особи, що надсилає дані.</string>\n  <string name=\"user_defined\">Визначено користувачем</string>\n  <string name=\"phone_number\">Номер телефону</string>\n  <string name=\"email\">Електронна адреса</string>\n  <string name=\"invalid_email_address\">Некоректна електронна адреса!</string>\n  <string name=\"device_defined\">Визначено пристроєм</string>\n  <string name=\"device_id\">ID пристрою</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Введіть новий пароль</string>\n  <string name=\"admin_password\">Пароль адміністратора</string>\n  <string name=\"admin_password_changed\">Пароль адміністратора успішно змінено</string>\n  <string name=\"admin_password_disabled\">Пароль адміністратора відключено</string>\n  <string name=\"admin_password_incorrect\">Вибачте, пароль невірний!</string>\n  <string name=\"enter_admin_password\">Ввести пароль адміністратора</string>\n  <string name=\"change_admin_password\">Змінити пароль адміністратора</string>\n  <string name=\"show_password\">Показати пароль</string>\n  <string name=\"found_in_main\">Зніміть позначку щоб прибрати з Головного меню</string>\n  <string name=\"form_entry\">Зніміть позначку аби прибрати з Заповнити Анкету</string>\n  <string name=\"reset_project_settings_title\">Скинути</string>\n  <string name=\"select_all\">Обрати всі</string>\n  <string name=\"clear_all\">Зняти всі позначки</string>\n  <string name=\"reset_settings\">Всі налаштування (внутрішні налаштування, збережені налаштування)</string>\n  <string name=\"reset_saved_forms\">Збережені анкети (директорія instances, база даних instances)</string>\n  <string name=\"reset_blank_forms\">Чисті анкети (директорія forms, база даних forms, база даних itemsets)</string>\n  <string name=\"reset_layers\">Шари карт (директорія layers)</string>\n  <string name=\"reset_cache\">Кеш завантаження анкет (директорія .cache)</string>\n  <string name=\"reset_settings_dialog_title\">Оберіть що скинути</string>\n  <string name=\"reset_settings_button_reset\">Скинути</string>\n  <string name=\"reset_in_progress\">Скидання…</string>\n  <string name=\"reset_app_state_result\">Результати скидання</string>\n  <string name=\"reset_app_state_warning\">Всі обрані дані будуть остаточно видалені. Можливості повернутись не буде.</string>\n  <string name=\"reset_settings_result\">Всі налаштування :: %s</string>\n  <string name=\"reset_saved_forms_result\">Збережені анкети :: %s</string>\n  <string name=\"reset_blank_forms_result\">Незаповнені анкети :: %s</string>\n  <string name=\"reset_layers_result\">Шари карт :: %s</string>\n  <string name=\"reset_cache_result\">Кеш завантаження анкет :: %s</string>\n  <string name=\"main_menu_settings\">Налаштування Головного Меню</string>\n  <string name=\"user_settings\">Користувацькі Налаштування</string>\n  <string name=\"form_entry_setting\">Налашувати Заповнити Анкету</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">Переміщення назад</string>\n  <string name=\"moving_backwards_disabled_title\">Переміщення назад відключене</string>\n  <string name=\"yes\">Так</string>\n  <string name=\"no\">Ні</string>\n  <string name=\"moving_backwards_enabled_title\">Переміщення назад включене</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">Про нас</string>\n  <string name=\"tell_your_friends\">Поширити ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">Ваші колеги досі збирають дані через паперові акети? Поділіться з ними ODK Collect.</string>\n  <string name=\"leave_a_review\">Залишити відгук в Play Store</string>\n  <string name=\"leave_a_review_msg\">Ваш (сподіваємось позитивний) відгук покращить помітність програми в Play Store.</string>\n  <string name=\"odk_website\">Перейти на сайт ODK</string>\n  <string name=\"odk_website_summary\">ODK використовується для збору даних для суспільних потреб.</string>\n  <string name=\"odk_forum\">Долучитись до форуму ODK</string>\n  <string name=\"odk_forum_summary\">Долучайтесь до форуму аби отримати допомогу, запросити нові функції, допомогти в створенні коду чи перекладу!</string>\n  <string name=\"all_open_source_licenses\">Відкриті бібліотеки/ліцензії</string>\n  <string name=\"all_open_source_licenses_msg\">Ми стоїмо на плечах гігантів!</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"successfully_imported_settings\">Налаштування успішно імпортовані</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">Досягнуто максимальну кількість символів: QR код не може бути згенерований для всіх налаштувань</string>\n  <string name=\"share\">Поділитись</string>\n  <string name=\"turn_on_flashlight\">Увімкнути ліхтарик</string>\n  <string name=\"turn_off_flashlight\">Вимкнути ліхтарик</string>\n  <string name=\"server_password\">Пароль Сервера</string>\n  <string name=\"include_password_dialog\">Паролі Включені в Код</string>\n  <string name=\"generate\">Згенерувати</string>\n  <string name=\"invalid_qrcode\">QR код не містить коректних налаштувань</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">Добре</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Скасувати</string>\n  <string name=\"cancel_loading_form\">Скасувати</string>\n  <string name=\"cancel_location\">Скасувати</string>\n  <string name=\"do_not_change\">Скасувати</string>\n  <string name=\"canceling\">Скасування</string>\n  <string name=\"notification_channel_name\">Загальні сповіщення</string>\n  <string name=\"success\">Успішно!</string>\n  <string name=\"error_occured\">Сталась помилка</string>\n  <string name=\"please_wait\">Будь ласка, зачекайте.</string>\n  <string name=\"please_wait_long\">Будь ласка, зачекайте. Це може зайняти кілька хвилин.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">Не можу видалити\\'%s\\'. Будь ласка, видаліть файл вручну та завантажте анкету знову.</string>\n  <string name=\"fs_create_media_folder_error\">Неможливо створити медіа директорію \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">Неможливо скопіювати \\'%1$s\\' в \\'%2$s\\'. Причина: %3$s</string>\n  <string name=\"select_odk_shortcut\">Обрати посилання ODK</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">Нерозпізнаний URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">Поганий URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">Ви завантажили дві різні анкети однакової версії з однаковим ID. Можливо це та сама акета опублікована в різний час або на різних серверах. В будь-якому разі одну треба видалити.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">Скинути</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ur/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">نیا فارم بھرنا شروع کریں</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">ڈرافٹس</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">بھیجنے کے لیئے تیار</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">بھیجے جا چکے</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">فارم کو ڈاؤن لوڈ کریں</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">فارم کوڈیلیٹ کریں</string>\n  <string name=\"saved_forms\">محفوظ شدہ فارم</string>\n  <string name=\"version_number\">ورژن:%s</string>\n  <string name=\"id_number\">آئی ڈی: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">EEE, MMM dd, yyyy\\'کو\\' HH:mm \\'جمع کیا گیا\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">  EEE, MMM dd, yyyy \\'کو\\' HH:mm \\'پراپ ڈیٹ کیا گیا\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">  EEE, MMM dd, yyyy \\'کو\\' HH:mm \\'پرمحفوظ کیا گیا\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">EEE, MMM dd, yyyy\\'کو\\' HH:mm \\'پر حتمی کیا گیا\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">‎EEE, MMM dd, yyyy\\'کو\\' HH:mm \\'پربھیجا گیا\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">‎EEE, MMM dd, yyyy\\'کو\\' HH:mm \\'پرسینڈ فیل ہوگیا\\'</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\"> ‎EEE, MMM dd, yyyy\\'کو\\' HH:mm \\' پرڈیلیٹ کیا گیا\\'</string>\n  <string name=\"submission_deleted\">جمع شدہ فارم کو حذف کر دیا گیا</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\"> EEE, MMM dd, yyyy\\'کو\\' HH:mm \\'پر موڈیفائی کیا گیا\\'</string>\n  <string name=\"encrypted_form\">انکرپٹڈ فارم</string>\n  <string name=\"deleted_form\">ڈیلیٹڈ فارم</string>\n  <string name=\"sort_the_list\">فہرست ترتیب دیں</string>\n  <string name=\"sort_by\">بہ ترتیب</string>\n  <string name=\"sort_by_name_asc\">نام، اے-زیڈ </string>\n  <string name=\"sort_by_name_desc\">نام، زیڈ-اے </string>\n  <string name=\"sort_by_date_desc\">تاریخ، سب سے پہلےنئی</string>\n  <string name=\"sort_by_date_asc\">تاریخ، سب سے پہلے پرانی </string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">آخر میں جمع شدہ سب سے اوپر ہے</string>\n  <string name=\"filter_the_list\">فہرست فلٹر کریں</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">سرچ </string>\n  <string name=\"not_granted_permission\">آپ کو یہ کام کرنے کی اجازت نہیں ہے</string>\n  <string name=\"storage_runtime_permission_denied_title\">سٹوریج پرمیشن</string>\n  <string name=\"storage_runtime_permission_denied_desc\">اس اجازت کے بغیر کولیکٹ آپ کے فارم تک رسائی حاصل نہیں کرسکتایا جواب محفوظ نہیں کر سکتا جب آپ پرمیشن دینے کے لئے تیار ہو تو ایپلکیشن دوبارہ کھولیں.</string>\n  <string name=\"location_runtime_permissions_denied_title\">لوکیشن کی اجازت</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">اس <b>مخصوص</b> اجازت کے بغیر کولیکٹ آپ کی لوکیشن ریکارڈ نہیں کرسکتا۔ برائےمہربانی دوبارہ کوشش کریں جب آپ پرمیشن دینے کے لئے تیار ہو </string>\n  <string name=\"camera_runtime_permission_denied_title\">کمیرہ کی اجازت</string>\n  <string name=\"camera_runtime_permission_denied_desc\">اس اجازت کے بغیر کولیکٹ کو کیمرے تک رسائی نہیں مل سکتی. برائےمہربانی دوبارہ کوشش کریں جب آپ  پرمیشن دینے کے لئے تیار ہو</string>\n  <string name=\"record_audio_runtime_permission_denied_title\"> آواز ریکارڈ کرنے کی اجازت</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">اس اجازت کے بغیر کولیکٹ کو مائیکروفون تک رسائی نہیں مل سکتی. برائےمہربانی دوبارہ کوشش کریں جب آپ پرمیشن دینے کے لئے تیار ہو</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">فارم کے ساتھ کا نقشہ کھولیں</string>\n  <string name=\"instance_scan_count\">%1$dفارم شامل ہیں.</string>\n  <string name=\"finished_disk_scan\">سکیننگ ختم ہو گئ ۔ تمام فارمز لوڈ ہو گئے</string>\n  <string name=\"xform_parse_error\">%1$s میں ایکس فارم پارس کی خرابی: %2$s غائب یا غلط ہے</string>\n  <string name=\"loading_form\">فارم لوڈ ہو رہا ہے</string>\n  <string name=\"survey_loading_reading_form_message\">فارم کی تفصیلات دیکھی جا رہی ہیں</string>\n  <string name=\"survey_loading_reading_data_message\">سروے کا ڈیٹا دیکھا جا رہا ہے</string>\n  <string name=\"survey_loading_reading_csv_message\">CSV فائلز دیکھی جا رہی ہیں</string>\n  <string name=\"parse_error\">معذرت،مگر فارم صحیح طرح پڑھا نہیں جا سکا</string>\n  <string name=\"loading_form_failed\">فارم لوڈ کرتے ہوئے ایک خرابی واقع ہوئی. دوبارہ کوشش کریں.</string>\n  <string name=\"cannot_edit_completed_form\">اس فارم میں ترمیم نہیں کی جاسکتی اگر ایک مرتبہ اس کو حتمی شکل دےدی گئ ہو۔ اس کو انکرپٹ کیا جاسکتا ہے</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\"> غیر محفوظ شدہ تبدیلیاںسیو پوائنٹ سےبحال کرلی گئی</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">زبان تبدیل کریں</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">پرامپٹ پر جائیں</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">ڈرافٹ میں محفوظ کریں</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">سٹارٹ پر جائیں</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">اختتام پر جائیں</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">اوپر جائیں</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">گروپ</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">دہرایا جانے والا گروپ</string>\n  <string name=\"exit\">باہر نکلیں</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">ایک اور \\\"1 %s\\\"؟</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">ایک دفعہ مزید</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">ایک اور شامل کریں</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">پرامپٹ کی ترمیم کریں</string>\n  <string name=\"clearanswer_confirm\">‎%s‏ کو دیا گیا جواب ہٹائیں</string>\n  <string name=\"clear_answer\">جواب مٹا دیں</string>\n  <string name=\"clear_answer_ask\"> یہ جواب مٹا دیں؟</string>\n  <string name=\"clear_answer_no\">منسوخ</string>\n  <string name=\"discard_answer\">جواب کو ہٹا دیں</string>\n  <string name=\"discard_group\">گروپ کو ہٹا دیں</string>\n  <string name=\"delete_repeat\">گروپ کو ہٹا دیں</string>\n  <string name=\"delete_repeat_ask\">کیا اس گروپ کو ہٹا دیا جائے؟</string>\n  <string name=\"delete_repeat_confirm\">\\\"‎%s‏\\\" گروپ اور اس کے تمام ذیلی گروپوں کو ہٹا دیں</string>\n  <string name=\"delete_repeat_no\">منسوخ</string>\n  <string name=\"invalid_answer_error\">معذرت، یہ جواب غلط ہے</string>\n  <string name=\"required_answer_error\">معذرت، یہ جواب دینا ضروری ہے</string>\n  <string name=\"success_form_validation\">آپ کے فارم میں اب کوئی غلطی نہیں ہے۔</string>\n  <string name=\"invalid_space_in_answer_singular\">نیچے دی گئی ویلیو %sمیں سپیسز ہیں</string>\n  <string name=\"invalid_space_in_answer_plural\">انتباہ: نیچے دی گئی ویلیو%s میں خالی جگہیں ہیں</string>\n  <string name=\"too_complex_form\">یہ فارم اس فون کے لئے بہت پیچیدہ ہے۔ اس کو سادہ کرنے کی کوشش کریں، یا پھر فورم پہ مدد حاصل کریں۔</string>\n  <string name=\"survey_internal_error\">اندرونی خرابی: فوری طور پر سیٹ اپ ناکام ہوگیا</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">آپ ‎%s‏ کے آخر پر ہیں۔</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">اگر آپ فارم میں تبدیلی کرنا چاہتے ہیں تو فارم کو \\\"ڈرافٹ میں محفوط\\\" رکھے، جب تک فارم جمع کرانے کے لئے تیار نہیں۔</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">ڈرافٹ میں محفوظ کریں</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">فائنل کریں</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">بھیجیں</string>\n  <string name=\"data_saved_error\">معذرت, فارم محفوظ نہیں ہوسکا</string>\n  <string name=\"data_saved_ok\">فارم کامیابی سے محفوظ ہو گیا</string>\n  <string name=\"save_point_error\">ریکوری پوانٹ سیو کرتے وقت خرابی: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">فارم کو مسترد کر دیں</string>\n  <string name=\"discard_changes\">تبدیلیوں کو مسترد کر دیں</string>\n  <string name=\"keep_changes\">تبدیلیوں کو محفوظ کریں</string>\n  <string name=\"keep_editing\">ترامیم کرتے رہیں</string>\n  <string name=\"quit_form_title\">کیا فارم کو محفوظ کر لیا جائے؟</string>\n  <string name=\"save_explanation\">آواز کی ریکارڈنگ</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\"> یہ فارم آخری دفعہEEE, MMM dd, yyyy کو HH:mm بجے محفوظ کیا گیا تھا۔ نئی تبدیلیاں محفوظ کرنے کی لئے ڈرافٹ میں محفوظ کریں۔</string>\n  <string name=\"quit_form_continue_title\">کیا آپ ترامیم جاری رکھنا چاہتے ہیں؟</string>\n  <string name=\"discard_form_warning\">اگر آپ فارم مسترد کرتے ہیں تو آپ کی کی گئی تبدیلیاں ختم ہو جائیں گی۔</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">یہ فارم آخری دفعہEEE, MMM dd, yyyy کو HH:mm بجے محفوظ کیا گیا تھا۔ اگر آپ تبدیلیاں مسترد کرتے ہیں توآپ کی دئیے گئے آخری وقت کے بعد کی ساری تبدیلیاں ضائع ہو جائیں گی۔</string>\n  <string name=\"saving_form\">فارم محفوظ ہو رہا ہے</string>\n  <string name=\"survey_saving_validating_message\"> جوابات کی تصدیق کی جا رہی ہے</string>\n  <string name=\"survey_saving_collecting_message\">ڈیٹا جمع ہورہا ہے</string>\n  <string name=\"survey_saving_saving_message\">ایس ڈی کارڈ میںمحفوظ ہو رہا ہے </string>\n  <string name=\"survey_saving_finalizing_message\">ایسڈی کارڈ کو حتمی شکل دینا</string>\n  <string name=\"survey_saving_encrypting_message\">ڈیٹا انکرپٹ ہورہا ہے</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">اس مثال کے لئے بالکل ایک ریکارڈ نہیں ہے!</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">کوئی خالی فارم جے آر_فارم_آئی ڈی سے نہیں ملتا</string>\n  <string name=\"no_form_id_specified\">کوئی فارمڈ مقرر نہیں کیا ؟؟؟</string>\n  <string name=\"phone_does_not_support_rsa\">فون آر ایس اے انکرپشن سپورٹ نہیں کرتا.</string>\n  <string name=\"invalid_rsa_public_key\">غلط آر ایس اے  پبلک کی.</string>\n  <string name=\"encryption_error_message\">%sفارم کو حتمی طور پر محفوظ نہیں کیا گیا ہے.</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">ریکارڈنگ شروع نہیں ہو سکی۔</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">فون کا مائیک پہلے سے کہیں اور استعمال میں ہے۔</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">میڈیا اس فارم کے ساتھ اٹیچ نہیں ہو سکا، لیکن ابھی بھی %1$s پر موجود ہے۔</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">ریکارڈنگ بند ہے۔ 1%s میں دوبارہ سٹارٹ کریں۔</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">ڈیٹا اپ ڈیٹ نہیں ہو پا رہا۔ اس کی عمومی وجہ یہ ہو سکتی ہے کہ فارم ڈیزائن میں Calculations کو صحیح طریقے سے استعمال نہیں کیا گیا۔ \nاگر آپ کو بار بار یہی مسئلہ درپیش آ رہا ہے تو پراجیکٹ کے انچارج کو مطلع کریں۔</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\"> شروع کریں</string>\n  <string name=\"activity_not_found\">‎%s‏    کوئی سرگرمی نہی پا ئ گئ</string>\n  <string name=\"no_app\">درخواست کی گئی ایپلیکیشن موجود نہیں ہے۔ براو مہربانی اعداد و شمار کو خود درج کریں۔</string>\n  <string name=\"null_intent_value\">ایکسٹرنل اپلیکیشن  نے متوقع معلومات فراہم نہیں کی.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"open_url\">یوآرایل کھولیں </string>\n  <string name=\"capture_osm\">OpenMapKit شروع کریں</string>\n  <string name=\"recapture_osm\"> او ایس ایم ٹیگنگ دوبارہ کریں</string>\n  <string name=\"edited_osm_file\">ترمیم او ایس ایم ایکس ایم ال فائل:</string>\n  <string name=\"invalid_osm_data\">کچھ غلط ہو گیا. ہمیں درست  او ایس ایم ڈیٹا نہیں ملا.</string>\n  <string name=\"alert\">انتباہ</string>\n  <string name=\"install_openmapkit\">برائے مہربانی اوپن میپ کیٹ  انسٹال کریں!</string>\n  <string name=\"get_barcode\">بارکوڈ حاصل کریں</string>\n  <string name=\"replace_barcode\">بار کوڈ تبدیل کریں</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">بارکوڈ کو مستطیل کے اندر رکھیں</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">فائل کھولیں</string>\n  <string name=\"rank_items\"> رینک آئٹمز</string>\n  <string name=\"number_picker_title\">نمبر منتخب کرنے کی جگہ</string>\n  <string name=\"invalid_range_widget\">اس ویجیٹ میں غلط پیرامیٹرز ہیں!</string>\n  <string name=\"svg_file_does_not_exist\">SVG فائل موجود نہیں ہے </string>\n  <string name=\"capture_image\">تصویر لیں</string>\n  <string name=\"choose_image\">تصویر کا انتخاب کریں</string>\n  <string name=\"selected_invalid_image\">منتخب فائل ایک درست تصویر نہیں ہے</string>\n  <string name=\"take_picture_instruction\"> تصویر کھینچنے کے لئے اسکرین کو ٹیپ کریں</string>\n  <string name=\"error_front_camera_unavailable\">اس ڈیوائس پر فرنٹ کمیرہ دستیاب نہیں ہے</string>\n  <string name=\"camera_error\">تصویر لینے میں کوئی مسئلہ ہے۔</string>\n  <string name=\"camera_failed_to_initialize\">کیمرا سٹارٹ نہیں ہو سکا</string>\n  <string name=\"annotate_image\">تصویر کی تفصیل لکھیے </string>\n  <string name=\"gif_not_supported\">GIF فائلز کی سپورٹ ابھی میسر نہیں ہے۔</string>\n  <string name=\"invalid_file_type\">ایپ کو دی گئی فائل درست نہیں</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">محفوظ کرکے بند کردیں</string>\n  <string name=\"signature_capture\">دستخط محفوظ کریں </string>\n  <string name=\"sign_button\">دستخط حاصل کریں</string>\n  <string name=\"markup_image\">مارک اپ تصویر </string>\n  <string name=\"draw_image\">اسکیچ تصویر</string>\n  <string name=\"reset_image\">ریس سیٹ کریں</string>\n  <string name=\"set_color\">رنگ ترتیب دیں</string>\n  <string name=\"capture_video\">ویڈیو ریکارڈ کریں</string>\n  <string name=\"choose_video\">ویڈیو منتخب کریں </string>\n  <string name=\"play_video\">وڈیو لگا دیں</string>\n  <string name=\"choose_file\">فائل منتخب کریں</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">ختم کریں</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">آواز ریکارڈ کریں</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">آواز کا انتخاب کریں</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">عارضی وقفہ</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">وقفہ ختم کریں</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"background_audio_permission_explanation\">یہ فارم آواز کی ریکارڈنگ بیک گراونڈ میں کرتا ہے۔ آپ اگر اس کو مائک استعمال کرنے کی اجازت نہیں دیں گے تو آپ فارم نہیں کھول سکیں گے۔</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">یہ فارم آواز کی ریکارڈنگ بیک گراونڈ میں کرنے کی اجازت لیتا ہے۔ اگر آپ یہ روکیں گے تو نہ صرف آواز کی ریکارڈنگ رک جائے گی، بلکہ موجودہ ریکارڈنگ بھی ضائع ہو جائے گی۔ کیا آپ آگے جانا چاہتے ہیں؟</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">ریکارڈنگ کو روک دیں</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">ریکارڈنگ فورا شروع نہیں ہوگی۔ اس کے لئے آپ کو فارم دوبارہ کھولنا پڑے گا۔</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">فائل کوڈیلیٹ کردیں؟</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">ایک مرتبہ ڈیلیٹ ہونے کے بعد یہ فائل دوبارہ دستیاب نہیں ہوگی۔ کیا آپ یہ جانتے ہوئے آگے جانا چاہتے ہیں؟ </string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">روک دیں</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">ریکارڈنگ ہو رہی ہے…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">آواز کی ریکارڈنگ</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">آواز چلائیں</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">فائل محفوظ کی جا رہی ہے</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">اس سکرین کو چھوڑنے سے پہلے ریکارڈنگ کو روکنا پڑے گا۔</string>\n  <string name=\"error_attaching_binary_file\">میڈیا کو اٹیچ کرتے ہوئے ایک خرابی واقع ہوئی %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">سلیکٹ ویلیو </string>\n  <string name=\"select_answer\">جواب کا انتخاب کریں</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">ایڈٹ ویلیو </string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">کوئی ویلیو منتخب نہیں کی گئی </string>\n  <string name=\"selected_title\">منتخب شدہ</string>\n  <string name=\"trigger\">ٹھیک ہے،  برا ئے مہربانی جاری رکھیں</string>\n  <string name=\"ext_file_no_data_error\">اس فائل میں کوئی ڈیٹا نہیں ہے </string>\n  <string name=\"ext_conflicting_columns_error\">کالم%sمیچ!</string>\n  <string name=\"ext_sortBy_numeric_error\">آپ کی ترتیب کے کالم میں صرف نیومیرک ویلیوز شامل ہونی چاہییں. تنازعہ ویلیو\\'%s\\'. تھی</string>\n  <string name=\"ext_search_wrong_arguments_error\">سرچ() فنکشن میں خرابی . فنکشن کے لیے 1، 4 یا 6  کی ضرورت ہے</string>\n  <string name=\"ext_search_wrong_function_error\">سرچ() فنکشن میں سینٹکس کی خرابی: ان ریکگنائزڈ فنکشن\\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">سرچ() فنکشن میں سینٹکس کی خرابی: \\'%s\\' کو فنکشن کے طور پر نہیں پہچانا گیا</string>\n  <string name=\"ext_search_return_error\">سرچ ہینڈلر نے %s ٹائپ کا آبجیکٹ ریٹرن کیا ہے</string>\n  <string name=\"ext_import_generic_error\">%1$s سے ڈیٹا امپورٹ نہیں کیا جا سکا۔ وجہ:%2$s</string>\n  <string name=\"ext_import_progress_message\"> \\'%1$s\\' سے ڈیٹا پری لوڈ کیا جا رہا ہے، برائے مہربانی انتظار کریں۔۔۔ %2$s</string>\n  <string name=\"ext_import_cancelled_message\">ڈیٹاریڈنگ کو منسوخ کر دیا گیا!</string>\n  <string name=\"ext_import_finalizing_message\">پہلے سے لوڈ ہوئےڈیٹا کو حتمی شکل دی جا رہی ہے</string>\n  <string name=\"ext_import_completed_message\">ڈیٹاریڈنگ مکمل</string>\n  <string name=\"ext_not_initialized_error\">ایکسٹرنل ڈیٹا مینیجر کو شروع نہیں کیا گیا</string>\n  <string name=\"ext_import_csv_missing_error\">%1$sکےلیےایکسٹرنل ڈیٹاامپورٹ نہیں ہوا ہے۔ شاید آپ اپنے فارم کے ساتھ %2$s ۔سی ایس وی شامل کرنا بھول گئے ہیں </string>\n  <string name=\"ext_search_generic_error\">سرچ() فنکشن میں سینٹکس کی خرابی:%s</string>\n  <string name=\"ext_assign_value_error\">\\'%s\\'پر ویلیو اسائن نہیں کر سکتا۔</string>\n  <string name=\"parser_exception\"> ایکس پاتھ پارسر ایکسپشن:\\\"%s\\\"</string>\n  <string name=\"get_bearing\">ریکارڈ بیئرنگ </string>\n  <string name=\"replace_bearing\">بیئرنگ کو تبدیل کریں</string>\n  <string name=\"getting_bearing\">بیئرنگ لوڈہو رہی یے</string>\n  <string name=\"accept_bearing\">ریکارڈ بیئرنگ </string>\n  <string name=\"bearing\">بیؑرنگ:%.3f</string>\n  <string name=\"direction\">سمت:%s</string>\n  <string name=\"bearing_lack_of_sensors\">بیئرنگ جمع نہیں کیا جاسکتا ہے: ڈیوائس میں اکسلرومیٹر ، میگنیٹک فیلڈ  سینسر یا دونوں نہیں ہے.</string>\n  <string name=\"select_date\">تاریخ منتخب کریں</string>\n  <string name=\"select_time\">وقت منتخب کریں</string>\n  <string name=\"no_date_selected\">کوئی تاریخ منتخب نہیں کی گئی </string>\n  <string name=\"no_time_selected\">کوئی وقت نہیں منتخب کیا گیا </string>\n  <string name=\"ethiopian_month_1\">مسکرم</string>\n  <string name=\"ethiopian_month_2\">ٹیکیمٹ</string>\n  <string name=\"ethiopian_month_3\">حیدر</string>\n  <string name=\"ethiopian_month_4\">تہساس </string>\n  <string name=\"ethiopian_month_5\">تر </string>\n  <string name=\"ethiopian_month_6\">یکٹیٹ </string>\n  <string name=\"ethiopian_month_7\">میگابٹ </string>\n  <string name=\"ethiopian_month_8\">میازیہ </string>\n  <string name=\"ethiopian_month_9\">جنبوٹ </string>\n  <string name=\"ethiopian_month_10\">سینی </string>\n  <string name=\"ethiopian_month_11\">حملی</string>\n  <string name=\"ethiopian_month_12\">نیہسی</string>\n  <string name=\"ethiopian_month_13\">پیگومین </string>\n  <string name=\"coptic_month_1\">تھاؤٹ </string>\n  <string name=\"coptic_month_2\">پوپی </string>\n  <string name=\"coptic_month_3\">ہتھوڑ </string>\n  <string name=\"coptic_month_4\">کویک </string>\n  <string name=\"coptic_month_5\">ٹوبی</string>\n  <string name=\"coptic_month_6\">میشر</string>\n  <string name=\"coptic_month_7\">پریم ہیٹ</string>\n  <string name=\"coptic_month_8\">پرموتی </string>\n  <string name=\"coptic_month_9\">پسھونس </string>\n  <string name=\"coptic_month_10\">پونی </string>\n  <string name=\"coptic_month_11\">اپیپ </string>\n  <string name=\"coptic_month_12\">مسوری </string>\n  <string name=\"coptic_month_13\">پی کوگی ایناووٹ  </string>\n  <string name=\"islamic_month_1\">محرم </string>\n  <string name=\"islamic_month_2\">صفر </string>\n  <string name=\"islamic_month_3\">ربیع الاول </string>\n  <string name=\"islamic_month_4\">ربیع الثانی </string>\n  <string name=\"islamic_month_5\">جُمَادَىٰ الْأَوَّل </string>\n  <string name=\"islamic_month_6\">جمادی الثانی </string>\n  <string name=\"islamic_month_7\">رجب </string>\n  <string name=\"islamic_month_8\">شعبان </string>\n  <string name=\"islamic_month_9\">رمضان </string>\n  <string name=\"islamic_month_10\">شوال </string>\n  <string name=\"islamic_month_11\">ذو القعدة</string>\n  <string name=\"islamic_month_12\">ذو الحجة</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">تر </string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">فائل: ‎%s‏  درست نہیں ہے</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">فائل: ‎%s‏ غائب ہے</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">اپنی شناخت درج کریں</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">شناخت</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">اس فارم کے لیئے آپ کی شناخت ہونا ضروری ہے</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">بند کریں</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">تبدیلیوں کی وجہ</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">وجہ</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">براہ مہربانی اس فارم میں تبدیلیوں کی وجہ بیان کریں</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">محفوظ کریں</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">جگہ منتخب کریں</string>\n  <string name=\"choices\">انتخاب</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">یو ایس جی ایس نیشنل میپ ٹوپو </string>\n  <string name=\"openmap_usgs_sat\">یو ایس جی ایس نیشنل میپ ہائبرڈ </string>\n  <string name=\"openmap_usgs_img\">یو ایس جی ایس نیشنل میپ امیجری </string>\n  <string name=\"openmap_cartodb_positron\">کارٹو ڈی بی پوزٹران</string>\n  <string name=\"openmap_cartodb_darkmatter\">کارٹو ڈی ڈی ڈارک میٹر</string>\n  <string name=\"basemap\">بیس میپ</string>\n  <string name=\"basemap_source\">ذریعہ</string>\n  <string name=\"basemap_source_unavailable\">ہم معذرت خواہ ہیں کہ اس فون پہ basemaps %s موجود نہیں ہیں۔ براہ مہربانی سیٹنگز &gt; Maps میں سے کوئی اور basemap منتخب کریں۔</string>\n  <string name=\"map_style_label\">%s میپ کا سٹائل</string>\n  <string name=\"streets\">سٹریٹس </string>\n  <string name=\"terrain\">ٹیرین</string>\n  <string name=\"hybrid\">ہائبرڈ</string>\n  <string name=\"satellite\">سیٹیلائٹ</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">لائٹ/ہلکا رنگ</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">ڈارک/گہرا رنگ</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">باہر</string>\n  <string name=\"topographic\">جغرافیائی</string>\n  <string name=\"google_play_services_error_occured\">گوگل میپس تک رسائی حاصل کرنے میں ناکام۔ کیا گوگل پلے سروسز  انسٹالڈہے؟</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Dark Matter</string>\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"record_geopoint\">پوائنٹ کو ریکارڈ کریں</string>\n  <string name=\"provider_disabled_error\">معذرت، لوکیشن فراہم کرنے والے نااہل کئے گئے ہیں</string>\n  <string name=\"google_play_services_not_available\">یہ فارم آپ کی لوکیشن کو ریکارڈ کرنا چاہتا ہے، مگر Google Play Services دستیاب نہیں ہیں۔</string>\n  <string name=\"location_providers_disabled_dialog_message\">یہ فارم آپ کی لوکیشن حاصل کرنا چاہتا ہے، لیکن لوکیشن سیٹنگ بند ہے۔ براہ مہربانی اس کو اینڈرائڈ سیٹنگز سے کھولیں۔</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">سیٹنگز پہ جائیں</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"background_location_disabled\">یہ فارم آپ کی لوکیشن حاصل کرنا چاہتا ہے، لیکن لوکیشن سیٹنگ بند ہے۔ براہ مہربانی اس کو اوپر والے %1$s مینیو سے کھولیں۔</string>\n  <string name=\"background_location_enabled\">یہ فارم آپ کی لوکیشن ریکارڈ کرتا ہے۔ لوکیشن ریکارڈنگ کو روکنے کے لئے %1$s مینیو دبایئں۔</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">غلطیوں کو چیک کریں</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">کتنا ٹائم گزرا: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">پوائنٹ%1$s جگہ پر محفوظ کیا جائے گا۔</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">سیٹلائٹس: %1$s</string>\n  <string name=\"waiting_for_location\">لوکیشن ڈھونڈی جا رہی ہے۔ انتظار کریں</string>\n  <string name=\"poor_accuracy\">درستگی مناسب نہیں ہے۔ انتظار کریں۔</string>\n  <string name=\"unacceptable_accuracy\">درستگی صحیح نہیں ہے۔ انتظار کریں۔</string>\n  <string name=\"improving_accuracy\">درستگی کو زیادہ کیا جا رہا ہے۔ انتظار کریں۔</string>\n  <string name=\"accuracy_status_title\">لوکیشن حاصل کی جا رہی ہے۔</string>\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">ڈبلیو%1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">ای%1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">ایس%1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">این:%1$s%2$s%3$s</string>\n  <string name=\"input_method\">اینٹری کی طریقہ</string>\n  <string name=\"start\">شروع </string>\n  <string name=\"enable_gps\">جی پی ایس کو فعال  کریں</string>\n  <string name=\"gps_enable_message\">آپ کے فون کا GPS بند ہے۔ کیا آپ اس کو کھولنا چاھیں گے؟</string>\n  <string name=\"geo_clear_warning\">فیچر پہلے سے ہی بنائی گئی ہے. کیا آپ کو فیچرکو ختم کرنا چاہیں گے؟</string>\n  <string name=\"geo_exit_warning\">تبدیلیوں کو ختم کریں اور او ڈی کے  کے پاس واپس جائیں؟</string>\n  <string name=\"polygon_validator\">قطبون بنانے کے لئے کم از کم 3 پوائنٹس لازمی ہیں</string>\n  <string name=\"polyline_validator\">پولی لائن بنانے کے لئے کم سے کم 2 پوائنٹس لازمی ہیں</string>\n  <string name=\"clear\">صاف کریں </string>\n  <string name=\"placement_mode\">کلک کرکے اندراج کریں</string>\n  <string name=\"manual_mode\">دستی لوکیشن ریکارڈنگ</string>\n  <string name=\"automatic_mode\">لوکیشن کی خود بخود ریکارڈنگ</string>\n  <string name=\"recording_interval\">ریکارڈنگ کا وقفہ:</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"one\">%d سیکنڈز</item>\n    <item quantity=\"other\">%d سیکنڈز</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"one\">%d منٹس</item>\n    <item quantity=\"other\">%d منٹس</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">درستگی کی ضروریات</string>\n  <string name=\"none\">کوئی نہیں</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"one\"> %d میٹرز</item>\n    <item quantity=\"other\"> %d میٹرز</item>\n  </plurals>\n  <string name=\"collection_status_paused\">درج شدہ پوائنٹس: %d</string>\n  <string name=\"collection_status_placement\">درج شدہ پوائنٹس: %d (کلک کرکے پوائنٹس کو سیٹ کریں)</string>\n  <string name=\"collection_status_manual\">درج شدہ پوائنٹس: %d (دستی ریکارڈنگ)</string>\n  <string name=\"collection_status_auto_seconds\">درج شدہ پوائنٹس: %1$d (ہر %2$d سیکنڈز بعد ریکارڈنگ)</string>\n  <string name=\"collection_status_auto_minutes\">درج شدہ پوائنٹس: %1$d (ہر %2$d منٹ بعد ریکارڈنگ)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">درج شدہ پوائنٹس: %1$d (ہر %2$d سیکنڈز بعد ریکارڈنگ، درستگی %3$d میٹر)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">درج شدہ پوائنٹس: %1$d (ہر %2$d منٹ بعد ریکارڈنگ، درستگی %3$d میٹر)</string>\n  <string name=\"geopoint_no_draggable_instruction\"> مارکر بٹن پر کلک کریں.</string>\n  <string name=\"discard\">ختم کر دینا</string>\n  <string name=\"select_item_count\">%s: %d (%d میپ پہ موجود)</string>\n  <string name=\"select_item\">منتخب کریں</string>\n  <string name=\"new_item\">نیا آئٹم</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">ڈرافٹ میں ترمیم کریں</string>\n  <string name=\"view_data\">محفوظ شدہ فارم دیکھیں</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">لوکیشن کو ٹریک کریں</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">لوکیشن کا پتہ لگایا جا رہا ہے۔۔۔</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">میری لوکیشن دکھایئں</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">اس طرح زوم کریں کہ تمام پوائنٹ نظر آیئں</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">عارضی وقفہ</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">آخری پوائنٹ کو ختم کریں</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">آخری پوائنٹ کو ختم کریں</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">فارم بھیجا جا رہا ہے</string>\n  <string name=\"noselect_error\">معذرت، آپ نے کسی فارم کا انتخاب نہیں کیا!</string>\n  <string name=\"no_connection\">کوئی نیٹ ورک کنکشن دستیاب نہیں ہے</string>\n  <string name=\"sending_items\">فارم(ز)%1$s آف %2$s کو بھیجا جا رہا ہے‏ </string>\n  <string name=\"send_selected_data\">منتخب شدہ کو بھیجیں</string>\n  <string name=\"server_auth_credentials\">سرور کے لئے غلط صارف کا نام یا پاس ورڈ:%s</string>\n  <string name=\"server_requires_auth\">سرور کو تصدیق چاہیے</string>\n  <string name=\"no_forms_uploaded\">معذرت، کوئی فارم اپ لوڈ نہیں کیا گیا</string>\n  <string name=\"upload_results\">نتیجہ اپ لوڈ کریں</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">نظارہ بدلیں</string>\n  <string name=\"show_sent_and_unsent_forms\">بھیجے گئے اور نہ بھیجے گئے فارم دکھائیں</string>\n  <string name=\"show_unsent_forms\">نہ بھیجے گئے فارم دکھایٰیں</string>\n  <string name=\"send_in_progress\">پس منظر چل رہا ہے، براہ مہربانی دوبارہ کوشش کریں</string>\n  <string name=\"forms_upload_succeeded\">فارمز کا اپ لوڈ کامیابی سے مکمل ہو گیا ہے۔</string>\n  <string name=\"forms_upload_failed\">فارمز کا اپ لوڈ نہیں ہو سکا</string>\n  <string name=\"all_uploads_succeeded\">تمام اپ لوڈز کامیابی سے مکمل ہو گئے ہیں۔</string>\n  <string name=\"some_uploads_failed\">%2$s میں سے %1$s اپ لوڈ ناکام ہو گیا ہے۔</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">دوبارہ شروع کریں</string>\n  <string name=\"download\">منتخب کریں</string>\n  <string name=\"downloading_data\">سرور کے ساتھ رابطہ ہو رہا ہے</string>\n  <string name=\"fetching_file\">‎‎‏‏فارم %1$sکو حاصل کیا جا رہا ہے %2$s آف %3$sفارم (ز) ‎‏ </string>\n  <string name=\"form_download_progress\"> ‎‏ %1$s۔میڈیا فائل حاصل کی جا رہی ہے :‎‏  %2$s آف%3$s</string>\n  <string name=\"version\">ورژن</string>\n  <string name=\"form_updates_available\">فارم اپ ڈیٹس دستیاب ہیں</string>\n  <string name=\"newer_version_of_a_form_info\">یہ اس فارم کی اپ ڈیٹ ہے جو آپ کے پاس ہے</string>\n  <string name=\"form_update_error\">فارم اپ ڈیٹ نہیں ہو سکا</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">فارمز کی لسٹ ڈاون لوڈ نہیں ہو سکی</string>\n  <string name=\"forms_download_succeeded\">فارمز کا ڈاون لوڈ کامیابی سے مکمل ہو گیا ہے۔</string>\n  <string name=\"forms_download_failed\">فارمز کا ڈاون لوڈ نہیں ہو سکا</string>\n  <string name=\"all_downloads_succeeded\">تمام ڈاون لوڈز کامیابی سے مکمل ہو گئے ہیں۔</string>\n  <string name=\"some_downloads_failed\">%2$s میں سے %1$s ڈاون لوڈ ناکام ہو گیا ہے۔</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s Version: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">اگر آپ کو یہی مسئلہ بار بار آ رہا ہے تو پراجیکٹ کے انچارج سے رابطہ کریں</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">Collect کا %s پر سرور سے رابطہ نہیں ہو رہا۔ کیا آپ نے URL کا صحیح اندراج کیا تھا؟</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">Collect کا %s پر محفوظ طریقے سے سرور سے رابطہ نہیں ہو رہا۔ کیا آپ نے URL کا صحیح اندراج کیا تھا؟</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">سرور %1$s  نے جواب میں %2$s  status کوڈ بھیجا ہے۔</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">سرور %s  نے صحیح جواب نہیں بھیجا</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">سرور نے اس فارم کا hash مہیا نہیں کیا۔</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">اس فارم کو ڈاون لوڈ کرتے وقت فون کی ڈسک کا مسئلہ آ رہا ہے۔</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">اس فارم کی submission URL صحیح نہیں ہے۔</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">محفوظ شدہ فارم</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">خالی فارم(ز)</string>\n  <string name=\"delete_confirm\">‎%s‏ فارم کو مٹا دیں</string>\n  <string name=\"delete_file\">منتخب شدہ کو مٹادیں</string>\n  <string name=\"delete_no\">نہ مٹا ئیں</string>\n  <string name=\"delete_yes\">فارم کو مٹا دیں</string>\n  <string name=\"form_delete_message\">منتخب کردہ فارم کو ڈیلیٹ کیا جارہا ہے</string>\n  <string name=\"file_deleted_ok\">%s‏فارم (ز )‎‏ کامیابی سےڈیلیٹ کر دیے گئے</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">پراجیکٹ سیٹنگز</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">سرور </string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL, ، یوزر نیم، پاس ورڈ</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">پراجیکٹ ڈسپلے</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">نام، آئکون، رنگ</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">یوزر انٹرفیس</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">نقشہ جات</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">Basemap, styles, layers</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">فارم مینجمنٹ</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">خودکاراپ ڈیٹ، خودکارطریقے سے بھیجنا، خودکار طریقے سے ڈیلیٹ کرنا</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">صارف اور ڈیوائس کی شناخت</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">یوزر نیم، فون نمبر، ڈیوائس ID</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">تجرباتی</string>\n  <string name=\"protected_settings\">محفوظ</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">محفوظ سیٹنگز کو کھولیں</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">پاس ورڈ تبدیلی، Access کنٹرول کرنا</string>\n  <string name=\"set_admin_password\">ایڈمن کا پاس ورڈ سیٹ کریں</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">پراجیکٹ کا انتظام</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">سیٹنگ صحیح کرنا، دوبارہ سے سیٹنگ کرنا، ڈیلیٹ کرنا</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">استعمال کا کنٹرول</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">یوزر کی ایپ کو محدود کریں</string>\n  <string name=\"server_preferences\">سرور کی ترتیب</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">یو آر ایل</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">سروریو آر ایل</string>\n  <string name=\"url_error\">معذرت، URL صحیح نہیں ہے</string>\n  <string name=\"username\">استعمال کنندہ کا نام</string>\n  <string name=\"change_username\">ODK استعمال کرنے والے کا نام</string>\n  <string name=\"password\">پاس ورڈ</string>\n  <string name=\"change_password\">  ODK  کا پاس ورڈ</string>\n  <string name=\"password_error_whitespace\">پاس ورڈز میں شروع اور بعد میں سپیسس کی اجازت نہیں ہے</string>\n  <string name=\"username_error_whitespace\">صارف کےناموں میں شروع اور بعد میں سپیسس کی اجازت نہیں ہے</string>\n  <string name=\"app_theme\">تھیم </string>\n  <string name=\"language\">زبان </string>\n  <string name=\"use_device_language\">ڈیوائس کی زبان استعمال کریں</string>\n  <string name=\"font_size\">ٹیکسٹ فونٹ سائز</string>\n  <string name=\"change_font_size\">ٹیکسٹ فونٹ سائز</string>\n  <string name=\"font_size_extra_large\">زیادہ بڑا</string>\n  <string name=\"font_size_large\">بڑا</string>\n  <string name=\"font_size_medium\">درمیانہ</string>\n  <string name=\"font_size_small\">چھو ٹا</string>\n  <string name=\"font_size_extra_small\">زیادہ چھوٹا</string>\n  <string name=\"navigation\">نیوی گیشن</string>\n  <string name=\"swipe_navigation\">افقی سوائپ استعمال کریں</string>\n  <string name=\"buttons_navigation\">آگے / پیچھے جانے والے بٹن استعمال کریں۔</string>\n  <string name=\"swipe_buttons_navigation\">سوائپ اور بٹن کو استعمال کریں</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">اگلا</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">واپس</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">فارم اپ ڈیٹ</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">خالی فارم کواپ ڈیٹ کرنے کی سہولت</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">دستی</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">صرف پہلے سے ڈاون لوڈ کئے گئے فارمز</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">سرورسے مکمل میچنگ کریں</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">خودکار اپ ڈیٹ کی تعداد</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">ہر پندرہ منٹ</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">ہر گھنٹے </string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">ہر چھ گھنٹے </string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">ہر 24گھنٹے </string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">آٹومیٹک ڈاؤن لوڈ</string>\n  <string name=\"automatic_download_summary\">فارموں کے اپ ڈیٹڈ ورژن خود بخود ڈاؤن لوڈ کریں</string>\n  <string name=\"hide_old_form_versions_setting_title\">پرانے فارم ورژن چھپائیں</string>\n  <string name=\"hide_old_form_versions_setting_summary\">صرف نئے ورژن  خالی فارم پرکریں میں نظر آئیں گے</string>\n  <string name=\"form_submission_category\">فارم سبمیشن</string>\n  <string name=\"autosend_selector_title\">خود بیجھے </string>\n  <string name=\"autosend\">خودکار طریقے سے بھیجیں</string>\n  <string name=\"off\">بند </string>\n  <string name=\"wifi_autosend\">صرف وائی فائی </string>\n  <string name=\"cellular_autosend\">صرف سیلولر</string>\n  <string name=\"wifi_cellular_autosend\">وائی فائی یا سیلولر</string>\n  <string name=\"delete_after_send\">بھیجنے کے بعد ڈیلیٹ کریں</string>\n  <string name=\"delete_after_send_summary\">سرور کو بھیجنے کے بعد حتمی فارمزاور میڈیا ڈیلیٹ کردیتے ہیں</string>\n  <string name=\"form_filling_category\">فارم فلنگ</string>\n  <string name=\"constraint_behavior_title\">کونسٹراینٹ  پروسیسنگ</string>\n  <string name=\"constraint_behavior\">ترکیبی عمل کو روکیں</string>\n  <string name=\"constraint_behavior_on_swipe\">آگے سوائپ پر تصدیق کریں</string>\n  <string name=\"constraint_behavior_on_finalize\">توثیق کو حتمی ہونے تک مؤخر کردیں</string>\n  <string name=\"high_resolution_title\">ہائی کوالٹی ویڈیو </string>\n  <string name=\"high_resolution_summary\">اعلی کوالٹی ویڈیو ریکارڈنگ کو فعال کریں</string>\n  <string name=\"image_size_title\">تصویر کا سائز</string>\n  <string name=\"image_size_dialog_title\">تصویر کے طویل کنارے کی زیادہ سے زیادہ پکسلز</string>\n  <string name=\"image_size_large\">لارج (3072 پی ایکس)</string>\n  <string name=\"image_size_medium\">میڈیم (2048 پی ایکس)</string>\n  <string name=\"image_size_small\">سمال (1024 پی ایکس)</string>\n  <string name=\"image_size_very_small\">ویری سمال (640 پی ایکس)</string>\n  <string name=\"image_size_original\">کیمرے سے اصل سائز (پہلے سے طے شدہ)</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">سوالات کے لئے ہدایت دکھائیں</string>\n  <string name=\"guidance_no\">نہیں </string>\n  <string name=\"guidance_yes\">جی ہاں - ہمیشہ دکھایا</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">جی ہاں - منہدم ہو گیا</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">آواز کی ریکارڈنگ کے لئے کسی دوسری ایپ کا استعمال کریں</string>\n  <string name=\"form_import_category\">فارم امپورٹ </string>\n  <string name=\"instance_sync\">امپورٹ پر فارم کو حتمی شکل دیں</string>\n  <string name=\"instance_sync_summary\">افیکٹس فارمز براہ راست انسٹنسز فولڈر میں شامل کیا گئے.</string>\n  <string name=\"analytics_preferences\">استعمال کا ڈیٹا</string>\n  <string name=\"analytics\">انانمس ڈیٹا جمع کریں</string>\n  <string name=\"analytics_summary\">بغیر شناخت کے ڈیٹا سے ODK کی ٹیم کو ایپ کے مسائل حل کرنے اور نئی سہولیات بنانے میں مدد ملتی ہے</string>\n  <string name=\"form_metadata_title\">فارم میٹا ڈیٹا</string>\n  <string name=\"form_metadata\">فارم میٹا ڈیٹا</string>\n  <string name=\"form_metadata_summary\">ان ویلیوزکو فارمز میں شامل کیا جائے گا جس میں ڈیٹا جمع کرنے والے شخص کو شناخت کرنے کے لئے صارف کا نام، ای میل اور / یاٹیلی فون پہلے سے لوڈ شدہ فیلڈ موجود ہیں.</string>\n  <string name=\"user_defined\">یوزر-ڈیفائینڈ </string>\n  <string name=\"phone_number\">فون نمبر </string>\n  <string name=\"email\">ای میل ایڈریس </string>\n  <string name=\"invalid_email_address\">غلط ای میل ایڈریس!</string>\n  <string name=\"device_defined\">ڈیوائس-ڈیفائینڈ </string>\n  <string name=\"device_id\">ڈیوائس آئی ڈی </string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">موجود نھیں ہے</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">سیٹنگز کو کھولیں</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">سیٹنگز کھل چکی ہیں</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">نیا پاس ورڈ درج کریں</string>\n  <string name=\"admin_password\">ایڈمن پاس ورڈ </string>\n  <string name=\"admin_password_changed\">ایڈمن کا پاس ورڈ کامیابی سے بدلا گیا</string>\n  <string name=\"admin_password_disabled\">ایڈمن کا پاس ورڈ غیر فعال کر دیاگیا۔</string>\n  <string name=\"admin_password_incorrect\">معافی، پاس ورڈ ٹھیک نہیں ہے</string>\n  <string name=\"enter_admin_password\">ایڈمن کا پاس ورڈ درج کریں</string>\n  <string name=\"change_admin_password\">ایڈمن پاس ورڈ تبدیل کریں</string>\n  <string name=\"show_password\">پاس ورڈ دکھائیں</string>\n  <string name=\"found_in_main\">نشان ہٹا کرکے مرکزی فہرست سے ہٹا دیں</string>\n  <string name=\"found_in_settings\">Unprotected settings سے چھپانے کے لئے چیک مارک ہٹا دیں</string>\n  <string name=\"form_entry\">فارم اندراج سے چھپانے کے لئے انچیک کریں</string>\n  <string name=\"form_end\">Form End سے چھپانے کے لئے چیک مارک ہٹا دیں</string>\n  <string name=\"reset_project_settings_title\">ریس سیٹ کریں</string>\n  <string name=\"reset_project_settings_subtext\">سیٹنگز، فارمز یا ڈیٹا میں سے منتخب کریں</string>\n  <string name=\"delete_project\">ختم کریں</string>\n  <string name=\"select_all\">تمام منتخب کریں</string>\n  <string name=\"clear_all\">تمام کو صاف کریں</string>\n  <string name=\"reset_settings\">تمام سیٹنگز(اندرونی سیٹنگز، محفوظ کردہ سیٹنگز)</string>\n  <string name=\"reset_saved_forms\">محفوظ فارم (انسٹنس فولڈر، انسٹنس ڈیٹا بیس)</string>\n  <string name=\"reset_blank_forms\">خالی فارم (فولڈر فارم، فارم ڈیٹا بیس، آئٹم سیٹ ڈیٹا بیس)</string>\n  <string name=\"reset_layers\">میپ لئیرز (لئیرزفولڈر)</string>\n  <string name=\"reset_cache\">فارم لوڈ کیشے (.کیشے فولڈر)</string>\n  <string name=\"reset_settings_dialog_title\">ری سیٹ کرنے کے لئے منتخب کریں</string>\n  <string name=\"reset_settings_button_reset\">ری سیٹ </string>\n  <string name=\"reset_in_progress\">ریستٹنگ </string>\n  <string name=\"reset_app_state_result\">ری سیٹ رزلٹس </string>\n  <string name=\"reset_app_state_warning\">تمام منتخب کردہ ڈیٹا مستقل طور پر ڈیلیٹ ہو جائیں گے. اسکو واپس نہیں کیا جا سکتا ہے.</string>\n  <string name=\"reset_settings_result\">تمام سیٹنگز::%s</string>\n  <string name=\"reset_saved_forms_result\">محفوظ فارم :: %s</string>\n  <string name=\"reset_blank_forms_result\">خالی فارم  :: %s</string>\n  <string name=\"reset_layers_result\">میپ لیؑیرز:: %s</string>\n  <string name=\"reset_cache_result\">فارم لوڈ کیشے::%s</string>\n  <string name=\"main_menu_settings\">مین مینو سیٹنگز </string>\n  <string name=\"main_menu_settings_summary\">بٹن کو دکھائیں یا چھپائیں</string>\n  <string name=\"user_settings\">یوزر سیٹنگز </string>\n  <string name=\"user_setting_summary\">سیٹنگز کو دکھائیں یا چھپائیں</string>\n  <string name=\"form_entry_setting\">فارم انٹری سیٹنگز </string>\n  <string name=\"form_entry_settings_summary\">Actions کو دکھائیں یا چھپائیں</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">پیچھے کی طرف حرکت کرنا </string>\n  <string name=\"moving_backwards_disabled_title\">پیچھے کی طرف حرکت غیر فعال </string>\n  <string name=\"moving_backwards_disabled_message\">کیا آپ فون کو اس طرح سیٹ کرنا چاہتے ہیں کہ یوزر سیٹنگ کو خود تبدیل نہ کر سکے؟\nاس کے لئے یہ سیٹنگز استعمال ہوتی ہیں:\nڈرافٹ میں ترامیم کی اجازت بند کریں\nڈرافٹ میں محفوظ کرنے کی اجازت بند کر دیں\nGo to Prompt کی اجازت بند کر دیں\nConstraint processing کو forward swipe پہ validate کرنا لازمی کر دیں</string>\n  <string name=\"yes\">ہاں </string>\n  <string name=\"no\">نہیں </string>\n  <string name=\"moving_backwards_enabled_title\">پیچھے کی طرف حرکت  فعال</string>\n  <string name=\"moving_backwards_enabled_message\">آپ ان سیٹنگز کو غور سے دیکھ سکتے ہیں:\nڈرافٹ میں ترامیم\nڈرافٹ میں محفوظ کرنا\nGo to prompt\nConstraint processing</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">ڈرافٹ میں محفوظ کریں</string>\n  <string name=\"save_mid_summary\">محفوظ کرنے کا بٹن top bar میں ہے، جبکہ ڈرافٹ میں محفوظ کرنے کا طریقہ فارم ختم کرتے وقت آتا ہے۔</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">مزید معلومات</string>\n  <string name=\"tell_your_friends\"> او ڈی کے کوللیکٹ شیئرکریں </string>\n  <string name=\"tell_your_friends_msg\">کیا آپ کے ساتھی  ابھی بھی کاغذ پر ڈیٹا جمع کرتے ہیں؟  ان کے ساتھ او ڈی کے کوللیکٹ شیئرکریں        </string>\n  <string name=\"leave_a_review\">پلے اسٹور پر رائے دیجیے</string>\n  <string name=\"leave_a_review_msg\">آپ ( امید ہے مثبت) کی رائے سے پلے اسٹورمیں اپپ کی نمائش میں اضافہ ہوتا ہے </string>\n  <string name=\"odk_website\">او ڈی کے ویب سائٹ وزٹ کریں</string>\n  <string name=\"odk_website_summary\">ODK مشکل حالات میں معاشرتی بہتری کے لئے ڈیٹا اکٹھا کرنے میں استعمال ہوتا ہے۔</string>\n  <string name=\"odk_forum\">او ڈی کے فورم میں شمولیت اختیار کریں</string>\n  <string name=\"odk_forum_summary\">تعاون حاصل کرنے کے لئے فورم میں شمولیت اختیار کریں، خصوصیات کی درخواست کریں، کوڈ / ترجمہ کرنے میں اپنا حصہ ڈالیں  !</string>\n  <string name=\"all_open_source_licenses\">اوپن سورس لائبریری / لائسنسس</string>\n  <string name=\"all_open_source_licenses_msg\">ہم پرانےمشاہدات کو استعمال کر کے سچ کا پتا لگا رہے ہیں </string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">QR کوڈ سے دوبارہ سیٹنگ کریں</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">پہلے سے موجود تمام سیٹنگز کو تبدیل کر دیں</string>\n  <string name=\"qr_code\">QR کوڈ کی سیٹنگ</string>\n  <string name=\"scan_qr_code_fragment_title\">سکین کریں</string>\n  <string name=\"view_qr_code_fragment_title\">QR کوڈ</string>\n  <string name=\"import_qrcode_sd\"> QR کوڈ باہر سے لایئں</string>\n  <string name=\"successfully_imported_settings\">سیٹنگز کامیابی سےامپورٹ ہوگئیں</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">زیادہ سے زیادہ حروف کی حدپہنچ گئی:کیو آر کوڈ تمام سیٹنگنزکے لئے بنایا نہیں کیا جا سکتا</string>\n  <string name=\"share\">شیئر </string>\n  <string name=\"turn_on_flashlight\">فلیش لائٹ چالو کریں</string>\n  <string name=\"turn_off_flashlight\">فلیش لائٹ بند کریں</string>\n  <string name=\"qrcode_with_both_passwords\">اس میں مندرجہ ذیل حساس معلومات ہیں: <b>ایڈمن پاس ورڈ</b> اور <b>سرور پاس ورڈ</b></string>\n  <string name=\"qrcode_without_passwords\">QR کوڈ میں <b>ایڈمن</b> یا <b>سرور</b> کا پاس ورڈ نہیں ہے۔ اس کو ڈالنے کے لئے کلک کریں۔</string>\n  <string name=\"qrcode_with_admin_password\">اس میں مندرجہ ذیل حساس معلومات ہیں: <b>ایڈمن پاس ورڈ</b></string>\n  <string name=\"qrcode_with_server_password\">اس میں مندرجہ ذیل حساس معلومات ہیں: <b>سرور پاس ورڈ</b></string>\n  <string name=\"server_password\">سرور کا پاس ورڈ</string>\n  <string name=\"include_password_dialog\">کوڈ میں شامل پاس ورڈ</string>\n  <string name=\"generate\">جنریٹ </string>\n  <string name=\"invalid_qrcode\">QR کوڈ درست سیٹنگنز پر مشتمل نہیں ہے</string>\n  <string name=\"qr_code_not_found\">منتخب تصویر میں QR کوڈ موجود نھیں ہے</string>\n  <string name=\"corrupt_imported_preferences_error\">موجودہ سیٹنگز خراب ہو چکی ہیں۔ پراجیکٹ سیٹنگز سے دوبارہ ڈالیں، یا صحیح سیٹنگز دوبارہ امپورٹ کریں۔</string>\n  <string name=\"settings_with_gd_protocol\">Google drive/sheets کے پراجیکٹس اب مزید نہیں بنائے جا سکتے۔</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">ٹھیک ہے</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">منسوخ</string>\n  <string name=\"cancel_loading_form\">منسوخ</string>\n  <string name=\"cancel_location\">منسوخ</string>\n  <string name=\"do_not_change\">منسوخ</string>\n  <string name=\"canceling\">مسنوخ کیا جا رہا ہے </string>\n  <string name=\"notification_channel_name\">عمومی نوٹس</string>\n  <string name=\"success\">کامیابی</string>\n  <string name=\"error_occured\">غلطی ہوگئ</string>\n  <string name=\"please_wait\">براہ مہربانی تھوڑا انتظار کریں</string>\n  <string name=\"please_wait_long\">براہ مہربانی انتظار کریں۔ اسکو چند منٹ لگ سکتے ہیں</string>\n  <string name=\"fs_delete_media_path_if_file_error\">\\'%s\\'ڈیلیٹ نہیں ھو سکا۔ برائے مہربانی فائل کو خود سے ڈٰیلیٹ کریں اور فارم دوبارہ ڈاؤنلوڈ کریں،</string>\n  <string name=\"fs_create_media_folder_error\">میڈیا فولڈر %s نہیں بن سکا.</string>\n  <string name=\"fs_file_copy_error\">کاپی نہیں کر سکا \\'%1$s\\' اوور \\'%2$s\\'. وجہ:%3$s</string>\n  <string name=\"select_odk_shortcut\">او ڈی کے شارٹ کٹ منتخب کریں</string>\n  <string name=\"background_audio_help\">یہ فارم آوازکی ریکارڈنگ آپ کے فون کے مائک سے کرتا ہے۔\n\nآپ Volume Indicator  کے استعمال سے یہ ممکن بنا سکتے ہیں کہ مائک ریکارڈ کرنے والی آواز کے بالکل پاس موجود ہو۔\n\nریکارڈنگ روکنے کے لئے اس فارم کو بند کر دیں۔\n\nمزید معلومات کے لئے پراجیکٹ کے انچارج سے رابطہ کریں۔</string>\n  <string name=\"loading\">لوڈ ہو رہا ہے۔۔۔</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">پراجیکٹس</string>\n  <string name=\"settings\">سیٹنگز </string>\n  <string name=\"add_project\">پراجیکٹ شامل کریں</string>\n  <string name=\"add\">ایک دفعہ مزید</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">ڈپلیکیٹ پراجیکٹ</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">آپ کے پاس ان پراجیکٹ سیٹنگز والا ایک اور پراجیکٹ پہلے ہی موجود ہے۔ کیا آپ اس پراجیکٹ کو کھولنا چاہیں گے، یا ایک اور نیا پراجیکٹ شروع کرنا چاہیں گے؟</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">ڈپلیکیٹ پراجیکٹ شامل کریں</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">پہلے سے موجود پراجیکٹ پر جائیں</string>\n  <string name=\"project_name\">پراجیکٹ کا نام</string>\n  <string name=\"project_icon\">پراجیکٹ کا آئکون</string>\n  <string name=\"project_color\">پراجیکٹ کا رنگ</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">%s  میں تبدیلی ہو چکی ہے</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">%s  کا استعمال ہو رہا ہے</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">%s میں تبدیل کریں</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"or\">OR</string>\n  <string name=\"hex_color\">hex color</string>\n  <string name=\"invalid_hex_code\">hex code صحیح نہیں ہے</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">کہیں پر بھی ڈیٹا اکٹھا کریں</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">QR کوڈ سے سیٹنگ کریں</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">پراجیکٹ کی تفصیل خود ڈالیں</string>\n  <string name=\"project_config_tip\">اپنا پراجیکٹ شامل کرنے کے بعد آپ سیٹنگز سے اس کی تفصیل کو تبدیل کر سکتے ہیں</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">کیا ابھی کوئی پراجیکٹ نہیں ہے؟</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">ڈیمو دیکھیں</string>\n  <string name=\"scan_configuration_qr_code\">سیٹنگز کا QR کوڈ سکین کریں</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">تفصیلات دکھائیں</string>\n  <string name=\"errors\">غلطیاں</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">اس فارم کو کھولنے کے لئے پہلے ODK Collect کھولیں اور اس پراجیکٹ کا انتخاب کریں جس میں یہ موجود ہے۔</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">ODK Collect ابھی تک Configure نہیں کیا گیا۔ براہ مہربانی ODK Collect کو کھولیں اور سیٹنگ کریں۔\n\nاگر آپ نے کسی Shortcut پہ کلک کیا ہے، تو آپ کوODK Collect سیٹ کرنے کی بعد اس کو دوبارہ بنانا پڑے گا۔</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">ناقابل شناخت URI:%s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">خراب URI:%s </string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">بھرا ہوا فارم ختم کر دیا گیا ہے۔</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">اس ڈرافٹ میں ترمیم نہیں ہو رہی، کیونکہ اسکا مماثل خالی فارم موجود نہیں یا پھر ڈیلیٹ کر دیا گیا ہے، فارم کی شناخت: ‎%1$s‏</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">آپ نے ایک ہی  ID اور ورژن کے ساتھ دو مختلف فارم ڈاؤن لوڈ کیے ہیں. شاید وہ وہی وقت تھے جو مختلف اوقات میں یا مختلف سرورز میں اپ لوڈ کیے گئے ہیں. کسی بھی صورت میں، آپ کو ایک کو ڈیلیٹ کرنا چاہئے.</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">سیٹنگز کھولیں</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">پچھلی دفعہ یہ ایپ کریش ہو گئی تھی</string>\n  <string name=\"cant_start_app\">ایپ سٹارٹ نہیں ہو سکتی!</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"learn_more_button_text\">مزید جانیں</string>\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">ڈویلیپرز کے ٹولز</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">Crash app</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">Force an uncaught exception causing the app to crash</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"permission_dialog_title\">اجازت کی تفصیل</string>\n  <string name=\"permission_dialog_text\">آپ سے پوچھا جائے گا کہ ODK Collect نیچے دی گئی چیزوں کو استعمال کر سکتا ہے۔ اگر آپ مطمئن ہیں تو Allow کا بٹن دبایئں۔</string>\n  <string name=\"notifications\">نوٹس</string>\n  <string name=\"required_for_notifications\">Required for showing updates when forms are downloaded, updated and sent.</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">آپ کا فارم ایک ڈرافٹ کی شکل میں محفوظ ہو گیا ہے۔</string>\n  <string name=\"form_saved\">آپ کا فارم محفوظ ہو گیا ہے۔</string>\n  <string name=\"form_sending\">فارم بھیجا جا رہا ہے۔۔۔</string>\n  <string name=\"edit_form\">ترمیم کریں</string>\n  <string name=\"view_form\">دیکھیں</string>\n  <string name=\"close_snackbar\">snackbar کو بند کر دیں</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"one\">آخری فارم بھجوانے کا ٹائم: %d سیکنڈز پہلے</item>\n    <item quantity=\"other\">آخری فارم بھجوانے کا ٹائم: %d سیکنڈز پہلے </item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"one\">آخری فارم بھجوانے کا ٹائم: %d منٹ پہلے</item>\n    <item quantity=\"other\">آخری فارم بھجوانے کا ٹائم: %d منٹ پہلے</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"one\">آخری فارم بھجوانے کا ٹائم: %d گھنٹے پہلے</item>\n    <item quantity=\"other\">آخری فارم بھجوانے کا ٹائم: %d گھنٹے پہلے</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"one\">آخری فارم بھجوانے کا ٹائم: %d دن پہلے</item>\n    <item quantity=\"other\">آخری فارم بھجوانے کا ٹائم: %d دن پہلے </item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"one\">%d فارمز بھجوائے جانے کے لئے تیار ہیں</item>\n    <item quantity=\"other\">%d فارمز بھجوائے جانے کے لئے تیار ہیں</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">بعد کے versions میں finalized فارمز میں تبدیلی نہیں ہو سکے گی۔ فارم میں تبدیلی ممکن بنانے کے لئے فارم کو ڈرافٹ کے طور پہ محفوظ کریں۔\\n\\n ڈرافٹ فارم میں غلطیوں کو دیکھنے کے لئے سیٹنگز کے تین نقطوں (⋮) پر کلک کرکے ٗغلطیاں چیک کریں ٗ پر کلک کریں۔</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">بعد کے versions میں finalized فارمز میں تبدیلی کرنا ممکن نہیں ہوگا۔</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">تمام ڈرافٹس کو finalize کریں</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"one\">کیا آپ %d ڈرافٹس کو finalize کرنا چاہتے ہیں؟</item>\n    <item quantity=\"other\">کیا آپ %d ڈرافٹس کو finalize کرنا چاہتے ہیں؟</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">تمام ڈرافٹ فارمز کو finalize کرنے کے بعد وہ ٗ بھیجنے کے لئے تیار ٗ حالت میں آجایئں گے، اور آپ ان کوتبدیل نہیں کر سکیں گے۔ وہ تمام ڈرافٹس جن میں غلطیاں ہوں گی، وہ finalize نہیں ہوں گے۔\\n\\n آپ اس action کو واپس نہیں کر سکیں گے۔ </string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"one\">%d ڈرافٹس کامیابی سے finalize ہو گئے ہیں</item>\n    <item quantity=\"other\">%d ڈرافٹس کامیابی سے finalize ہو گئے ہیں</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"one\">%d ڈرافٹس میں کچھ غلطیاں ہیں، جو finalize کرنے سے پہلے دور کرنا ضروری ہیں</item>\n    <item quantity=\"other\">%d ڈرافٹس میں کچھ غلطیاں ہیں، جو finalize کرنے سے پہلے دور کرنا ضروری ہیں</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d ڈرافٹس finalize ہو چکے ہیں۔ %d ڈرافٹس میں کچھ غلطیاں ہیں جن کو finalize کرنے سے پہلے درست کرنا ضروری ہے۔ </string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d ڈرافٹس finalize ہو گئے ہیں۔ باقی رہ جانے والے ڈرافٹس کو باری باری finalize کرنا ہوگا۔</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">غلطیاں</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">کوئی غلطی نہیں ہے</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">ڈرافٹس میں سے چھپانے کے لئے uncheck کریں</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">یہاں دکھانے کو کچھ نہیں ہے</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">ڈرافٹس میں کچھ موجود نہیں</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">آپ کے ڈرافٹس میں محفوظ کئیے گئیے فارم یہاں نظر آیئں گے</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">بھیجنے کے لئے کچھ تیار نہیں</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">آپ کے Finalized کئے گئے ڈرافٹس یہاں نظر آیئں گے۔</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">بھیجے گئے فارمز میں کچھ نہیں ہے۔</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">آپ کے بھیجے گئے Finalized فارمز یہاں نظر آیئں گے۔</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">ختم کر دینا</string>\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-ur-rPK/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">‎\\'‏Added on‎\\' EEE, MMM dd, yyyy \\'‏at‎\\' HH:mm‏</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">‎\\'‏Saved on‎\\' EEE, MMM dd, yyyy \\'‏at‎\\' HH:mm‏</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">‎\\'‏Finalized on‎\\' EEE, MMM dd, yyyy \\'‏at‎\\' HH:mm‏</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">‎\\'‏Sent on‎\\' EEE, MMM dd, yyyy \\'‏at‎\\' HH:mm‏</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">‎\\'‏Sending failed on‎\\' EEE, MMM dd, yyyy \\'‏at‎\\' HH:mm‏</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"submission_deleted\">جمع کرانے کو حذف کر دیا گیا</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_form\">حذف شدہ فارم</string>\n  <string name=\"sort_the_list\">فہرست ترتیب دیں</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"filter_the_list\">فہرست کو فلٹر کریں۔</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">سکئننگ ختم ہو گئ ۔ تمام فائل لوڈ کئےگئے</string>\n  <string name=\"loading_form\">فارم لوڈ ہو رہا ہے</string>\n  <string name=\"parse_error\">معافی، فارم کا تجزیہ نہیں ہو سکا</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">سیو پوائنٹ سے حاصل شدہ غیر محفوظ شدہ تبدیلیاں</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">زبان تبدیل کریں</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">پرامپٹ (prompt) پر جایئں</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">شروع پر چلیں</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">آخر پر چلیں</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">اوپر چلیں</string>\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat\">مزید شامل کریں</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">پرامپٹ کی تدوین کریں</string>\n  <string name=\"clearanswer_confirm\">‎%s‏ کو دیا گیا جواب ہٹائیں</string>\n  <string name=\"clear_answer\">جواب مٹا دیں</string>\n  <string name=\"clear_answer_ask\"> یہ جواب مٹا دیں؟</string>\n  <string name=\"clear_answer_no\">منسوخ</string>\n  <string name=\"discard_answer\">جواب کو ہٹا دیں</string>\n  <string name=\"discard_group\">گروپ کو ہٹا دیں</string>\n  <string name=\"delete_repeat\">گروپ کو ہٹا دیں</string>\n  <string name=\"delete_repeat_ask\">اس گروپ کو پٹادیں</string>\n  <string name=\"delete_repeat_confirm\">\\\"‎%s‏\\\" گروپ اور اس کے تمام ذیلی گروپوں کو ہٹا دیں</string>\n  <string name=\"delete_repeat_no\">منسوخ</string>\n  <string name=\"invalid_answer_error\">معافی، یہ جواب ناقابل عمل ہے</string>\n  <string name=\"required_answer_error\">معافی، یہ جواب دینا ضروری ہے</string>\n  <string name=\"save_enter_data_description\">آپ ‎%s‏ کے آخر پر ہیں۔</string>\n  <string name=\"data_saved_error\">معافی, فارم محفوظ نہیں ہوسکا</string>\n  <string name=\"data_saved_ok\">فارم کامیابی سے محفوظ کیا گیا</string>\n  <string name=\"save_point_error\">ریکوری پوانٹ سیو کرتے وقت خرابی: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">تبدیلیوں کو محفوظ کریں</string>\n  <string name=\"saving_form\">فارم محفوظ ہو رہا ہے</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">آغاز</string>\n  <string name=\"activity_not_found\">‎%s‏  کو کر نے کیلیے کوئی سرگرمی نہی پا ئ گئ</string>\n  <string name=\"no_app\">درخواست کیا گیا ایپلیکیشن موجود نہیں ہے۔ براو مہربانی اعداد و شمار کو خود درج کریں۔</string>\n  <string name=\"get_barcode\">بارکوڈ حاصل کریں</string>\n  <string name=\"replace_barcode\">مقام کو بدل دیں</string>\n  <string name=\"capture_image\">تصویر لیں</string>\n  <string name=\"choose_image\">عکس کا انتخاب کریں</string>\n  <!--Dialog heading for saving annotated image-->\n  <string name=\"save_and_close\">محفوظ کرکے بند کردیں</string>\n  <string name=\"signature_capture\">دستخط حاصل کریں</string>\n  <string name=\"sign_button\">signature حاصل کریں</string>\n  <string name=\"markup_image\">تصویر کا انتخاب کریں</string>\n  <string name=\"draw_image\">تصویر کا خاکہ بنایئں</string>\n  <string name=\"reset_image\">دوبارہ ترتیب دیں</string>\n  <string name=\"set_color\">رنگ ترتیب دیں</string>\n  <string name=\"capture_video\">وڈیو ریکارڈ کریں</string>\n  <string name=\"play_video\">وڈیو لگا دیں</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">آوازریکارڈ کریں</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">اواز کا انتخاب کریں</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Text next to checkbox to allow the user to toggle audio recording on and off-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">جواب کا انتخاب کریں</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">منتخب شدہ</string>\n  <string name=\"trigger\">ٹھیک ہے، مہربانی کرکے جاری رکھیں</string>\n  <string name=\"ext_file_no_data_error\">اس فائل میں کوئ ڈیٹا موجود نھیں ہے</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">فائل: ‎%s‏  غیر کارآمد ہے</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">فائل: ‎%s‏ غائب ہے</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"provider_disabled_error\">معافی، مقام فراہم کرنے والے نااہل کئے گئے ہیں</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Label for a menu item that can be toggled to turn background location tracking on and off during form filling-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Text of button user can press to get help when setting up Reference Layers-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">فارم بھیجا جا رہا ہے</string>\n  <string name=\"noselect_error\">معافی، آپنے کسی فارم کا انتخاب نہیں کیا</string>\n  <string name=\"no_connection\">کوئی نیٹ ورک کنکشن دستیاب نہیں ہے</string>\n  <string name=\"sending_items\">فارم ‎%2$s‏ کے ‎%1$s‏ کو بھیجا جا رہا ہے</string>\n  <string name=\"send_selected_data\">منتخب شدہ کو بھیجیں</string>\n  <string name=\"server_requires_auth\">سرور کو تصدیق چاہیے</string>\n  <string name=\"no_forms_uploaded\">معافی، کوئی فارم اپ لوڈ نہیں کیا گیا</string>\n  <string name=\"upload_results\">نتیجہ اپ لوڈ کریں</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">نظارہ بدلیں</string>\n  <string name=\"show_sent_and_unsent_forms\">بھیجے گئے اور نہ بھیجے گئے فارم دکھائیں</string>\n  <string name=\"show_unsent_forms\">نہ بھیجے گئے فارم دکھایٰیں</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">دوبارہ شروع کریں</string>\n  <string name=\"download\">منتخب کریں</string>\n  <string name=\"downloading_data\">سرور کے ساتھ رابطہ ہو رہا ہے</string>\n  <string name=\"fetching_file\">‎%1$s‏ کو حاصل کیا جا رہا ہے؟؟ فارم (ز) ‎%3$s‏ کا فارم ‎%2$s‏</string>\n  <string name=\"form_download_progress\">غلطی: ‎%1$s‏ میڈیا فائل حاصل کیا جا رہا ہے: ‎%3$s‏ کا ‎%2$s‏</string>\n  <string name=\"version\">ورژن</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">محفوظ شدہ فارم</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">خالی فارم(ز)</string>\n  <string name=\"delete_confirm\">‎%s‏ فارم کو مٹا دیں</string>\n  <string name=\"delete_file\">منتخب شدہ کو مٹادیں</string>\n  <string name=\"delete_no\">نہ مٹا ئیں</string>\n  <string name=\"delete_yes\">فارم کو مٹا دیں</string>\n    <string name=\"file_deleted_ok\">فارم ‎%s‏ کامیابی سے مٹادئے گئے</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">سرور کی ترتیب</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">یو آر ایل (URL)</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">یو آر ایل (URL)</string>\n  <string name=\"url_error\">معافی، url صحیح نہیں ہے</string>\n  <string name=\"username\">استعمال کنندہ کا نام</string>\n  <string name=\"change_username\">ODK استعمال کرنے والے کا نام</string>\n  <string name=\"password\">شناختی لفظ (Password)</string>\n  <string name=\"change_password\">  ODK  کا شناختی لفظ (Password)</string>\n  <string name=\"use_device_language\">فون کی منتخب زبان استعمال کریں</string>\n  <string name=\"font_size\">ہجوں کے فانٹ کا ناپ</string>\n  <string name=\"change_font_size\">فانٹ کا سائز</string>\n  <string name=\"font_size_extra_large\">زیادہ بڑا</string>\n  <string name=\"font_size_large\">بڑا</string>\n  <string name=\"font_size_medium\">درمیانہ</string>\n  <string name=\"font_size_small\">چھو ٹا</string>\n  <string name=\"font_size_extra_small\">زیادہ چھوٹا</string>\n  <string name=\"navigation\">ہدف تلاش کرنا</string>\n  <string name=\"swipe_navigation\">آگے اور پیچھے جانے کے بٹن استعمال کرو</string>\n  <string name=\"buttons_navigation\">آگے / پیچھے جانے والے بٹن استعمال کریں۔</string>\n  <string name=\"swipe_buttons_navigation\">سوائپ اور بٹن کو استعمال کریں</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">اگلا</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"constraint_behavior\">ترکیبی عمل کو روکیں</string>\n  <string name=\"constraint_behavior_on_swipe\">آگے سوائپ پر توثیق کریں</string>\n  <string name=\"constraint_behavior_on_finalize\">توثیق کو حتمی ہونے تک موءخر کردیں</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"invalid_email_address\">ای میل ایڈریس صحیح نھیں ہے</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">موجود نھیں</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">نیا شناختی لفظ درج کریں</string>\n  <string name=\"admin_password_changed\">منتظم کا شناختی لفظ کامیابی سے بدلا گیا</string>\n  <string name=\"admin_password_disabled\">منتظم کا شناختے لفظ غیر موءثر کیا گیا۔</string>\n  <string name=\"admin_password_incorrect\">معافی، شناختی لفظ ٹھیک نہیں ہے</string>\n  <string name=\"enter_admin_password\">منتظم کا شناختی لفظ درج کریں</string>\n  <string name=\"show_password\">پاس ورڈ  دکھائیں</string>\n  <string name=\"found_in_main\">غیر نشان شدہ کرکے مرکزی فہرست میں چھپا لیں</string>\n  <string name=\"reset_project_settings_title\">دوبارہ ترتیب دیں</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">پیچھے جائیں</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">ٹھیک ہے</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">منسوخ</string>\n  <string name=\"cancel_loading_form\">منسوخ</string>\n  <string name=\"cancel_location\">منسوخ</string>\n  <string name=\"do_not_change\">منسوخ</string>\n  <string name=\"success\">کامیابی</string>\n  <string name=\"error_occured\">غلطی ہوگئ</string>\n  <string name=\"please_wait\">براہ مہربانی۔ چند منٹ انتظار کرلیں</string>\n  <string name=\"please_wait_long\">براہ مہربانی انتظار کریں۔ اسکو چند منٹ لگ سکتے ہیں</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"add\">مزید شامل کریں</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-vi/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'Được thêm vào\\' EEE, MMM dd, yyyy \\'lúc\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'Được lưu vào\\' EEE, MMM dd, yyyy \\'lúc\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'Hoàn thành vào\\' EEE, MMM dd, yyyy \\'lúc\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'Gửi vào\\' EEE, MMM dd, yyyy \\'lúc\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'Sending failed on\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Hoàn thành tìm kiếm. Tất cả các biểu mẫu đã được tải.</string>\n  <string name=\"loading_form\">Tải biểu mẫu</string>\n  <string name=\"parse_error\">Xin lỗi, không thể phân tích biểu mẫu.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Những thay đổi chưa ghi lại được phục hồi từ điểm đã lưu!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">Đổi ngôn ngữ</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Di chuyển đến</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Về đầu tiên</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Đến cuối cùng</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Về trước</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Sửa thông tin</string>\n  <string name=\"clearanswer_confirm\">Xoá câu trả lời \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Xoá câu trả lời</string>\n  <string name=\"clear_answer_ask\">Muốn xoá câu trả lời này?</string>\n  <string name=\"clear_answer_no\">Huỷ</string>\n  <string name=\"discard_answer\">Xoá nội dung trả lời</string>\n  <string name=\"discard_group\">Xoá nhóm</string>\n  <string name=\"delete_repeat\">Xoá nhóm</string>\n  <string name=\"delete_repeat_ask\">Xoá nhóm này?</string>\n  <string name=\"delete_repeat_confirm\">Xoá nhóm \\\"%s\\\" và tất cả các nhóm con liên quan?</string>\n  <string name=\"delete_repeat_no\">Huỷ</string>\n  <string name=\"invalid_answer_error\">Xin lỗi, phản hồi này không đúng!</string>\n  <string name=\"required_answer_error\">Xin lỗi, câu trả lời được yêu cầu điền!</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Bạn đang ở phần kết thúc của \\\"%s\\\".</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Xin lỗi, biểu mẫu không lưu được!</string>\n  <string name=\"data_saved_ok\">Biểu mẫu đã được lưu!</string>\n  <string name=\"save_point_error\">Lỗi lưu lại điểm khôi phục: %s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Lưu thay đổi</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Đang lưu biểu mẫu</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Chạy</string>\n  <string name=\"activity_not_found\">Không có hành động phù hợp với: %s</string>\n  <string name=\"no_app\">Không tìm thấy chương trình yêu cầu. Hãy chỉ rõ chương trình đọc.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"capture_osm\">Mở OpenMapKit</string>\n  <string name=\"recapture_osm\">Ghi lại Đánh dấu OSM</string>\n  <string name=\"get_barcode\">Lấy mã vạch</string>\n  <string name=\"replace_barcode\">Thay mã vạch</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Chụp ảnh</string>\n  <string name=\"choose_image\">Chọn ảnh</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Lưu và Đóng lại</string>\n  <string name=\"sign_button\">Xin Chữ kí</string>\n  <string name=\"markup_image\">Hình ảnh Đánh dấu</string>\n  <string name=\"draw_image\">Sketch Image</string>\n  <string name=\"reset_image\">Reset</string>\n  <string name=\"set_color\">Chọn màu</string>\n  <string name=\"capture_video\">Quay Video</string>\n  <string name=\"choose_video\">Chọn Video</string>\n  <string name=\"play_video\">Chạy Video</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Ghi âm</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Chọn âm thanh</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">Có lỗi xảy ra khi đính kèm định dạng media: %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Chọn câu trả lời</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Chọn: </string>\n  <string name=\"trigger\">OK. Hãy tiếp tục.</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">File: %s không đúng.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">File: %s bị thiếu.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Xin lỗi, Khả năng xác định vị trí không được kích hoạt!</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Gửi biểu mẫu</string>\n  <string name=\"noselect_error\">Xin lỗi, bạn không chọn biểu mẫu nào!</string>\n  <string name=\"no_connection\">Không có kết nối internet</string>\n  <string name=\"sending_items\">Gửi %1$s trong %2$s biểu mẫu</string>\n  <string name=\"send_selected_data\">Gửi lựa chọn</string>\n  <string name=\"server_requires_auth\">Máy chủ yêu cầu xác thực</string>\n  <string name=\"no_forms_uploaded\">Xin lỗi, biểu mẫu không tải lên được.</string>\n  <string name=\"upload_results\">Kết quả tải lên</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Đổi hiển thị</string>\n  <string name=\"show_sent_and_unsent_forms\">Biểu mẫu đã gửi và chưa gửi</string>\n  <string name=\"show_unsent_forms\">Chỉ các biểu mẫu chưa gửi</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Làm mới</string>\n  <string name=\"download\">Tải lựa chọn</string>\n  <string name=\"downloading_data\">Kết nối máy chủ</string>\n  <string name=\"fetching_file\">Tải %1$s.\\n\\nForm %2$s trong số %3$s biểu mẫu.</string>\n  <string name=\"form_download_progress\">%1$s. Lấy file đa phương tiện: %2$s of %3$s</string>\n  <string name=\"version\">Phiên bản:</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Lưu biểu mẫu</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Biểu mẫu trống</string>\n  <string name=\"delete_confirm\">Xoá biểu mẫu %s ?</string>\n  <string name=\"delete_file\">Xoá lựa chọn</string>\n  <string name=\"delete_no\">Không xoá</string>\n  <string name=\"delete_yes\">Xoá biểu mẫu</string>\n  <string name=\"file_deleted_ok\">Biểu mẫu %s đã bị xoá!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">Máy chủ</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">Máy chủ</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">Địa chỉ máy chủ</string>\n  <string name=\"url_error\">Xin lỗi URL không đúng!</string>\n  <string name=\"username\">Tên truy cập</string>\n  <string name=\"change_username\">Tên truy cập ODK</string>\n  <string name=\"password\">Mật khẩu</string>\n  <string name=\"change_password\">Mật khẩu</string>\n  <string name=\"font_size\">Kích thước chữ</string>\n  <string name=\"change_font_size\">Kích thước chữ</string>\n  <string name=\"font_size_extra_large\">Cực lớn</string>\n  <string name=\"font_size_large\">Lớn</string>\n  <string name=\"font_size_medium\">Vừa</string>\n  <string name=\"font_size_small\">Nhỏ</string>\n  <string name=\"font_size_extra_small\">Cực nhỏ</string>\n  <string name=\"navigation\">Navigation</string>\n  <string name=\"swipe_navigation\">Use horizontal swipes</string>\n  <string name=\"buttons_navigation\">Use forward/backward buttons</string>\n  <string name=\"swipe_buttons_navigation\">Use swipes and buttons</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Next</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Enter new password</string>\n  <string name=\"admin_password_changed\">Admin password successfully changed</string>\n  <string name=\"admin_password_disabled\">Admin password disabled</string>\n  <string name=\"admin_password_incorrect\">Sorry, password is incorrect!</string>\n  <string name=\"enter_admin_password\">Enter Admin Password</string>\n  <string name=\"found_in_main\">Uncheck to hide from Main Menu</string>\n  <string name=\"reset_project_settings_title\">Reset</string>\n  <string name=\"reset_settings_button_reset\">Reset</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">OK</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Huỷ</string>\n  <string name=\"cancel_loading_form\">Huỷ</string>\n  <string name=\"cancel_location\">Huỷ</string>\n  <string name=\"do_not_change\">Huỷ</string>\n  <string name=\"success\">Thành công</string>\n  <string name=\"error_occured\">Lỗi xuất hiện</string>\n  <string name=\"please_wait\">Hãy đợi 1 lúc.</string>\n  <string name=\"please_wait_long\">Hãy đợi trong ít phút.</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-zh/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">新建表单</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">草稿</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">准备上传</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">已上传</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">下载表单</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">删除表单</string>\n  <string name=\"saved_forms\">已保存的表单</string>\n  <string name=\"version_number\">版本: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'添加于\\' EEE, MMM dd, yyyy \\'在\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'更新于\\' EEE, MMM dd, yyyy \\'在\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'保存于\\' EEE, MMM dd, yyyy \\'在\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'完成于\\' EEE, MMM dd, yyyy \\'在\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'发送于\\' EEE, MMM dd, yyyy \\'在\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'发送失败于\\' EEE, MMM dd, yyyy \\'在\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">EEE, MMM dd, yyyy \\'的\\' HH:mm \\'已删除\\' </string>\n  <string name=\"submission_deleted\">提交已删除</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'已修改于\\' EEE, MMM dd, yyyy \\'，\\' HH:mm</string>\n  <string name=\"encrypted_form\">加密的表单</string>\n  <string name=\"deleted_form\">删除的表单</string>\n  <string name=\"sort_the_list\">排序列表</string>\n  <string name=\"sort_by\">排序</string>\n  <string name=\"sort_by_name_asc\">名称, A-Z</string>\n  <string name=\"sort_by_name_desc\">名称, Z-A</string>\n  <string name=\"sort_by_date_desc\">日期, 从新到旧</string>\n  <string name=\"sort_by_date_asc\">日期, 从旧到新</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">最新保存的</string>\n  <string name=\"filter_the_list\">筛选列表</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">搜索</string>\n  <string name=\"not_granted_permission\">您没有权限开启此活动。</string>\n  <string name=\"storage_runtime_permission_denied_title\">存储权限</string>\n  <string name=\"storage_runtime_permission_denied_desc\">没有这些权限，Collect应用将无法访问您的表格或保存答案。请重启应用以授予权限。</string>\n  <string name=\"location_runtime_permissions_denied_title\">位置权限</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">没有 <b>精确位置</b> 权限， Collect应用无法记录您的位置。请重新授予后重试。</string>\n  <string name=\"camera_runtime_permission_denied_title\">相机权限</string>\n  <string name=\"camera_runtime_permission_denied_desc\">没有此权限，Collect应用无法访问相机。请重新授予后重试。</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">录制音频权限</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">没有此权限，应用无法访问麦克风。请重新授予后重试。</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">打开表单地图</string>\n  <string name=\"instance_scan_count\">添加了%1$d 个表单.</string>\n  <string name=\"finished_disk_scan\">文件扫描结束. 所有表格已加载.</string>\n  <string name=\"xform_parse_error\"> %1$s解析出错: %2$s 缺少或无效。</string>\n  <string name=\"loading_form\">正在加载表格</string>\n  <string name=\"survey_loading_reading_form_message\">正在读取表单定义…</string>\n  <string name=\"survey_loading_reading_data_message\">正在读取调查数据…</string>\n  <string name=\"survey_loading_reading_csv_message\">正在读取CSV文件…</string>\n  <string name=\"parse_error\">无法读取表格.</string>\n  <string name=\"loading_form_failed\">加载表单时出错。请重试。</string>\n  <string name=\"cannot_edit_completed_form\">此表单在被标记为最终版本后将无法再编辑，它可能已被加密。</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">未保存的更改已从保存点恢复！</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">更改语言</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">跳到提示</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">保存为草稿</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">跳转到开始</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">跳转到结束</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">返回</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <string name=\"edit_finalized_form\">编辑最终版表单</string>\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">组</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">可重复组</string>\n  <string name=\"exit\">退出</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">添加 \\\"%s\\\"?</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">添加</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"do_not_add_repeat\">不加</string>\n  <string name=\"add_another_menu\">添加另一个</string>\n  <string name=\"add_another_question\">再增加一個？</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">修改提示</string>\n  <string name=\"clearanswer_confirm\">删除针对\\\"%s\\\"的回答?</string>\n  <string name=\"clear_answer\">删除回答</string>\n  <string name=\"clear_answer_ask\">删除这个回答?</string>\n  <string name=\"clear_answer_no\">取消</string>\n  <string name=\"discard_answer\">删除回答</string>\n  <string name=\"discard_group\">删除组</string>\n  <string name=\"delete_repeat\">删除组</string>\n  <string name=\"delete_repeat_ask\">删除这个组?</string>\n  <string name=\"delete_repeat_confirm\">删除组\\\"%s\\\"及其子组?</string>\n  <string name=\"delete_repeat_no\">取消</string>\n  <string name=\"invalid_answer_error\">抱歉，回答无效!</string>\n  <string name=\"required_answer_error\">抱歉，此项回答为必填！</string>\n  <string name=\"success_form_validation\">成功！表格中未发现错误。</string>\n  <string name=\"invalid_space_in_answer_singular\">警告：下划线的 %s 含有空格</string>\n  <string name=\"invalid_space_in_answer_plural\">警告：下划线的 %s 含有空格</string>\n  <string name=\"too_complex_form\">此表单对该设备来说过于复杂。请尝试简化表达式，或在论坛上寻求帮助。</string>\n  <string name=\"survey_internal_error\">内部错误：跳转到提示步骤失败。</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">您已到达 %s 的末尾.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <string name=\"form_editing_disabled_after_finalizing\">无法对最终版本进行编辑。</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <string name=\"form_editing_disabled_after_sending\">提交后将无法编辑。</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_finalizing\">仍可以对最终版本进行编辑</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <string name=\"form_editing_enabled_after_sending\">提交后仍可以编辑</string>\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">如果您需要对表格进行编辑，在准备提交前，请选择“另存为草稿”。</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <string name=\"form_editing_enabled_after_finalizing_hint\">若要对表单最终版进行编辑，请前往“待提交”，打开表单并点击编辑图标。</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <string name=\"form_editing_enabled_after_sending_hint\">若要对已提交的表单进行编辑，请前往“已提交”，打开表单并点击编辑图标。</string>\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">保存为草稿</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">最终确定</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">提交</string>\n  <string name=\"data_saved_error\">表单保存失败!</string>\n  <string name=\"data_saved_ok\">表单已成功保存!</string>\n  <string name=\"save_point_error\">保存恢复点时出错：%s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">放弃表单</string>\n  <string name=\"discard_changes\">放弃变更</string>\n  <string name=\"keep_changes\">保存修改</string>\n  <string name=\"keep_editing\">继续编辑</string>\n  <string name=\"quit_form_title\">储存表单？</string>\n  <string name=\"save_explanation\">您可以保存此表单并随时从草稿中存取它。</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'此表单最后储存于\\' EEE, MMM dd, yyyy\\'at\\'HH:mm\\'。另存为草稿以保留变更。\\'</string>\n  <string name=\"quit_form_continue_title\">继续编辑？</string>\n  <string name=\"discard_form_warning\">如果您丢弃表单，您将失去所有已做的更改。</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'此表单最后保存于 \\'EEE, MMM dd, yyyy HH:mm\\'。如果您放弃更改，您将失去此后所做的所有更改。\\'</string>\n  <string name=\"saving_form\">正在保存表单</string>\n  <string name=\"survey_saving_validating_message\">正在验证答案…</string>\n  <string name=\"survey_saving_collecting_message\">正在收集数据…</string>\n  <string name=\"survey_saving_saving_message\">正在保存至SD卡…</string>\n  <string name=\"survey_saving_finalizing_message\">正保存到SD卡…</string>\n  <string name=\"survey_saving_encrypting_message\">加密数据</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">此实例中没有一条记录！</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">并非只有一个空白表单与此jr_form_id匹配。</string>\n  <string name=\"no_form_id_specified\">未指定表单id???</string>\n  <string name=\"phone_does_not_support_rsa\">手机不支持RSA加密。</string>\n  <string name=\"invalid_rsa_public_key\">RSA公钥无效。</string>\n  <string name=\"encryption_error_message\">%s 表单尚未保存为完成状态。</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">表单无法使用</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">无法开始录制。</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">麦克风已在使用中。</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">无法将媒体附加到表单，但可以在 %1$s获取</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">录制已禁用。在 %s中启用</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">尝试更新值失败。这通常是因为表单设计中计算单元没有正确配置。如果您一直存在此问题，请向要求您收集数据的人报告。</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">运行</string>\n  <string name=\"activity_not_found\">没找到与 %s 对应的活动</string>\n  <string name=\"no_app\">请求的应用不可用. 请手动填入相应值.</string>\n  <string name=\"null_intent_value\">外部应用程序未提供预期信息。</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">列印</string>\n  <string name=\"open_url\">打开Url</string>\n  <string name=\"capture_osm\">启动 OpenMapKit</string>\n  <string name=\"recapture_osm\">重做OSM标记</string>\n  <string name=\"edited_osm_file\">已编辑的OSM XML文件：</string>\n  <string name=\"invalid_osm_data\">出了问题。我们没有获得有效的OSM数据。</string>\n  <string name=\"alert\">警告</string>\n  <string name=\"install_openmapkit\">请安装OpenMapKit！</string>\n  <string name=\"get_barcode\">获取条形码</string>\n  <string name=\"replace_barcode\">替换条形码</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">将条形码放在矩形内</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"rotate_device\">请旋转设备</string>\n  <string name=\"open_file\">打开文件</string>\n  <string name=\"rank_items\">排序元素</string>\n  <string name=\"number_picker_title\">数字选择器</string>\n  <string name=\"invalid_range_widget\">此部件的参数无效！</string>\n  <string name=\"svg_file_does_not_exist\">SVG文件不存在！</string>\n  <string name=\"capture_image\">照相</string>\n  <string name=\"choose_image\">选择图片</string>\n  <string name=\"selected_invalid_image\">所选文件不是有效图像</string>\n  <string name=\"take_picture_instruction\">点击屏幕拍照</string>\n  <string name=\"error_front_camera_unavailable\">前置摄像头在此设备上不可用</string>\n  <string name=\"camera_error\">拍照时出错</string>\n  <string name=\"camera_failed_to_initialize\">无法启动相机！</string>\n  <string name=\"annotate_image\">注释该图片</string>\n  <string name=\"gif_not_supported\">Gif 档案格式不支援</string>\n  <string name=\"invalid_file_type\">应用程序返回了无效的文件类型</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_title\">保存修改？</string>\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_drawing_explanation\">在您退出前，是否要保存修改？</string>\n  <string name=\"save_and_close\">保存并关闭</string>\n  <string name=\"signature_capture\">签名捕获</string>\n  <string name=\"sign_button\">收集签名</string>\n  <string name=\"markup_image\">修改图像</string>\n  <string name=\"draw_image\">生成图像</string>\n  <string name=\"reset_image\">重置</string>\n  <string name=\"set_color\">选择颜色</string>\n  <string name=\"capture_video\">拍摄视频</string>\n  <string name=\"choose_video\">选择视频</string>\n  <string name=\"play_video\">播放视频</string>\n  <string name=\"choose_file\">选择文件</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">删除</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">录音</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">选择音频</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">暂停</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">恢复</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">录音：开</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">录音：关闭</string>\n  <string name=\"background_audio_permission_explanation\">此表单在后台录制音频。您必须授予使用麦克风的权限。否则，您将无法打开此表单。</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">此表单请求后台音频录制。禁用它将停止录制并丢弃现有音频。是否确实要继续？</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">屏蔽录制</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">录制不会立即开始。您必须重新打开表单进行记录。</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">删除文件？</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">删除此文件后，您将无法恢复该文件。是否确实要继续？</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">停止</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">正在录制…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">录音</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">播放声音</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">保存文件</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">离开此屏幕前必须停止录制。</string>\n  <string name=\"error_attaching_binary_file\">在访问媒体文件：%s时发生了错误</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">选择值</string>\n  <string name=\"select_answer\">选择回答</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">编辑值</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">没有选定的值</string>\n  <string name=\"selected_title\">选定: </string>\n  <string name=\"trigger\">好的. 请继续.</string>\n  <string name=\"ext_file_no_data_error\">此文件不包含数据！</string>\n  <string name=\"ext_conflicting_columns_error\">列 %s 匹配!</string>\n  <string name=\"ext_sortBy_numeric_error\">sortby列应该只包含数值。冲突值为 \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">search() 函数语法错误：该函数需要1、4或6个参数。</string>\n  <string name=\"ext_search_wrong_function_error\">search() 函数中的语法错误：未识别的函数 \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">search()函数中的语法错误: \\'%s\\' 未作为函数计算.</string>\n  <string name=\"ext_search_return_error\">搜索处理程序返回了类型为 \\'%s\\'的对象.</string>\n  <string name=\"ext_import_generic_error\">无法从%1$s中导入数据，原因：%2$s</string>\n  <string name=\"ext_import_progress_message\">从中 \\'%1$s\\'预加载数据, 请稍候… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">读取数据已取消！</string>\n  <string name=\"ext_import_finalizing_message\">正在完成预加载的数据…</string>\n  <string name=\"ext_import_completed_message\">读取数据已完成！</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager尚未初始化。</string>\n  <string name=\"ext_import_csv_missing_error\">外部数据 %1$s 尚未导入. 也许您忘记在表单中包含 %2$s.csv 文件?</string>\n  <string name=\"ext_search_generic_error\">search() 函数中的语法错误: %s</string>\n  <string name=\"ext_assign_value_error\">无法给 \\'%s\\' 赋值.</string>\n  <string name=\"parser_exception\">XPathParser 异常: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">记录方位</string>\n  <string name=\"replace_bearing\">替代方位</string>\n  <string name=\"getting_bearing\">加载方位</string>\n  <string name=\"accept_bearing\">记录方位</string>\n  <string name=\"bearing\">方位: %.3f</string>\n  <string name=\"direction\">方向: %s</string>\n  <string name=\"bearing_lack_of_sensors\">无法收集方位：设备缺少加速计、磁场传感器或两者。</string>\n  <string name=\"select_date\">选择日期</string>\n  <string name=\"select_time\">选择时间</string>\n  <string name=\"no_date_selected\">未选择日期</string>\n  <string name=\"no_time_selected\">未选择时间</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <string name=\"buddhist_month_1\">一月</string>\n  <!--2nd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_2\">二月</string>\n  <!--3rd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_3\">三月</string>\n  <!--4th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_4\">四月</string>\n  <!--5th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_5\">五月</string>\n  <!--6th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_6\">六月</string>\n  <!--7th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_7\">七月</string>\n  <!--8th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_8\">八月</string>\n  <!--9th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_9\">九月</string>\n  <!--10th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_10\">十月</string>\n  <!--11th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_11\">十一月</string>\n  <!--12th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_12\">十二月</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">文件: %s 不是有效文件.</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">文件: %s 不能被找到.</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">输入身份</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">身份</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">此表单要求您确认身份</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">关闭</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">变更原因</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">理由</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">您需要解释更改此表单的原因</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">保存</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">选择地点</string>\n  <string name=\"choices\">选择</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS地形图</string>\n  <string name=\"openmap_usgs_sat\">USGS混合地图</string>\n  <string name=\"openmap_usgs_img\">USGS影像地图</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">底图</string>\n  <string name=\"basemap_source\">源</string>\n  <string name=\"basemap_source_unavailable\">抱歉, %s 此设备上没有底图。请在“设置”中选择另一个底图 &gt;  地图.</string>\n  <string name=\"map_style_label\">%s 地图样式</string>\n  <string name=\"streets\">街道</string>\n  <string name=\"terrain\">地形</string>\n  <string name=\"hybrid\">混合</string>\n  <string name=\"satellite\">卫星</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">浅色</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">深色</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">户外</string>\n  <string name=\"topographic\">地形图</string>\n  <string name=\"google_play_services_error_occured\">无法访问谷歌地图。是否安装了Google Play服务？</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Dark Matter</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">离线图层</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">图层</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">选择离线图层</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">选择要用于该项目中所有地图的离线图层。您可以通过从设备中选择 MBTiles 文件来将选项添加到列表中，也可以从列表中删除现有图层。</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">了解有关添加 MBTiles 的更多信息。</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">新增图层</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">删除图层</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <plurals name=\"non_mbtiles_files_selected_title\">\n    <item quantity=\"other\">%d 图层无法新增</item>\n  </plurals>\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">您选择的文件不是 MBTiles。您只能添加 MBTiles 文件。</string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">某些您选择的文件不是 MBTiles 格式。您只能添加 MBTiles 文件。</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">图层</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">选择访问图层</string>\n  <string name=\"select_layer_access_subtitle\">您希望图层在所有项目中使用，还是仅在当前项目中使用？</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">所有项目</string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">目前项目</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">您确定要删除 %1$s 离线图层吗？</string>\n  <string name=\"record_geopoint\">记录点</string>\n  <string name=\"location_accuracy\">准确率：%1$s</string>\n  <string name=\"location_accuracy_unacceptable\">准确率：%1$s（不合格）</string>\n  <string name=\"provider_disabled_error\">位置服务被关闭!</string>\n  <string name=\"gps_result\">纬度：%1$s\n经度：%2$s\n海拔：%3$s 米\n精度：%4$s 米</string>\n  <string name=\"google_play_services_not_available\">此表单希望跟踪您的位置，但Google Play服务不可用。</string>\n  <string name=\"location_providers_disabled_dialog_message\">此表单希望跟踪您的位置，但位置提供程序被禁用。请在Android设置中启用。</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">转到设置</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">追踪位置：开</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">追踪位置：关</string>\n  <string name=\"background_location_disabled\">此表单希望跟踪您的位置，但已禁用跟踪. 请在菜单 %1$s 上启用他</string>\n  <string name=\"background_location_enabled\">此表单跟踪您的位置. 您可以在上面的菜单 %1$s 中禁用跟踪。</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">检查错误</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">已用时间: %1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">点将保存在 %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">卫星: %1$s</string>\n  <string name=\"waiting_for_location\">正在尝试获取位置，请稍候。</string>\n  <string name=\"poor_accuracy\">精度差，请稍候。</string>\n  <string name=\"unacceptable_accuracy\">精度不可接受，请稍候。</string>\n  <string name=\"improving_accuracy\">提高准确性，请稍候。</string>\n  <string name=\"accuracy_status_title\">正在获取位置</string>\n  <!--The accuracy of a location fix in meters-->\n  <string name=\"accuracy_m\">%1$s米</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">W %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">输入方法</string>\n  <string name=\"start\">开始</string>\n  <string name=\"enable_gps\">启用GPS</string>\n  <string name=\"gps_enable_message\">设备上的GPS已禁用。是否要启用它？</string>\n  <string name=\"geo_clear_warning\">对象已创建。是否要清除该对象？</string>\n  <string name=\"geo_exit_warning\">放弃更改并返回应用？</string>\n  <string name=\"polygon_validator\">必须至少有3个点才能创建多边形</string>\n  <string name=\"polyline_validator\">必须至少有2个点才能创建多段线</string>\n  <string name=\"clear\">清除</string>\n  <string name=\"placement_mode\">轻敲放置</string>\n  <string name=\"manual_mode\">手动位置记录</string>\n  <string name=\"automatic_mode\">自动位置记录</string>\n  <string name=\"recording_interval\">记录间隔：</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"other\">%d 秒</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"other\">%d 分钟</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">精度要求：</string>\n  <string name=\"none\">没有</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"other\">%d 米</item>\n  </plurals>\n  <string name=\"collection_status_paused\">输入的点数: %d</string>\n  <string name=\"collection_status_placement\">输入的点数: %d (点击以放置点)</string>\n  <string name=\"collection_status_manual\">输入的点数: %d (手动记录)</string>\n  <string name=\"collection_status_auto_seconds\">输入的点数: %1$d (每隔%2$d 秒记录)</string>\n  <string name=\"collection_status_auto_minutes\">输入的点数: %1$d (每隔 %2$d 分钟记录)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">输入的点数: %1$d (每隔 %2$d 秒记录, 精度%3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">输入的点数: %1$d (每隔 %2$d 分钟记录, 精度 %3$d m)</string>\n  <string name=\"geopoint_instruction\">长按以放置标记，或点击“添加标记”按钮</string>\n  <string name=\"geopoint_no_draggable_instruction\">点击添加标记按钮。</string>\n  <string name=\"discard\">丢弃</string>\n  <string name=\"select_item_count\">%s: %d (%d 在图上)</string>\n  <string name=\"select_item\">选择</string>\n  <string name=\"new_item\">新元素</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">编辑草稿</string>\n  <string name=\"view_data\">查看已保存的表单</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">位置跟踪</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">跟踪位置…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">显示位置</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">缩放以适应所有</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">暂停</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">删除最后一点</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">删除最后一点</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <string name=\"done\">完成</string>\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">正在发送表单</string>\n  <string name=\"noselect_error\">没有选定表单!</string>\n  <string name=\"no_connection\">没有可用网络</string>\n  <string name=\"sending_items\">上传 %2$s 中的 %1$s 表单</string>\n  <string name=\"send_selected_data\">上传选定项</string>\n  <string name=\"server_auth_credentials\">对服务器：%s无效的用户名或者密码</string>\n  <string name=\"server_requires_auth\">服务器要求验证身份</string>\n  <string name=\"no_forms_uploaded\">没有表单被上传.</string>\n  <string name=\"upload_results\">上传结果</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">修改视图</string>\n  <string name=\"show_sent_and_unsent_forms\">显示已提交和未提交的表单</string>\n  <string name=\"show_unsent_forms\">只显示未发送的表单</string>\n  <string name=\"send_in_progress\">后台发送正在运行，请稍后重试</string>\n  <string name=\"forms_upload_succeeded\">表单上传成功</string>\n  <string name=\"forms_upload_failed\">表单上传失败</string>\n  <string name=\"all_uploads_succeeded\">所有上传成功！</string>\n  <string name=\"some_uploads_failed\">%1$s / %2$s 上载失败!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">刷新</string>\n  <string name=\"download\">下载选中项</string>\n  <string name=\"downloading_data\">连接到服务器</string>\n  <string name=\"fetching_file\">获取 %1$s表单。共 %2$s / %3$s 表单.</string>\n  <string name=\"form_download_progress\">%1$s. 加载媒体文件: %3$s 中的 %2$s</string>\n  <string name=\"version\">版本:</string>\n  <string name=\"form_updates_available\">表单更新可用</string>\n  <string name=\"newer_version_of_a_form_info\">这是对您的表单的更新</string>\n  <string name=\"form_update_error\">表单更新失败</string>\n  <string name=\"form_update_error_massage\">重启ODK Collect以重试</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"form_update_notification_title\">正在同步表单</string>\n  <string name=\"load_remote_form_error\">表单列表下载失败</string>\n  <string name=\"forms_download_succeeded\">表单下载成功</string>\n  <string name=\"forms_download_failed\">表单下载失败</string>\n  <string name=\"all_downloads_succeeded\">所有下载成功！</string>\n  <string name=\"some_downloads_failed\">%1$s / %2$s 下载失败!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s 版本: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">如果你一直有这个问题，请向要求你收集数据的人报告。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">无法在访问服务器 %s. 您输入的URL是否正确?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">无法安全连接到位于 %s的服务器. 您输入的URL是否正确?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">服务器%1$s 返回的状态 %2$s.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">服务器 %s 提供了无效的响应.</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">服务器未提供此表单的哈希值。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_parsing_error\">此表单无法处理：</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">下载此表单时，设备上出现磁盘错误。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">此表单的提交链接无效。</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">已保存的表单</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">空表单</string>\n  <string name=\"delete_confirm\">删除表单 %s ?</string>\n  <string name=\"delete_file\">删除选中</string>\n  <string name=\"delete_no\">不删除</string>\n  <string name=\"delete_yes\">删除表单</string>\n  <string name=\"form_delete_message\">删除所选表单</string>\n  <string name=\"file_deleted_ok\">%s 表单删除成功!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">项目设置</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">服务器</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL、用户名、密码</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">项目显示</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">名称、图标、颜色</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">用户界面</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_interface_settings_subtext\">應用程式語言、字體大小、導航</string>\n  <!--Title of a settings section-->\n  <string name=\"maps\">地图</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">底图、样式、图层</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">表单管理</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">自动更新、自动发送、自动删除</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">用户和设备标识</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">用户名、电话号码、设备ID</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">实验的</string>\n  <string name=\"protected_settings\">受保护的</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">解锁受保护的设置</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">更改密码，设置访问控制</string>\n  <string name=\"set_admin_password\">设置管理员密码</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">项目管理</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">重新配置、重置、删除</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">访问控制</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">有限用户界面</string>\n  <string name=\"server_preferences\">服务器设置</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">服務器 URL</string>\n  <string name=\"url_error\">不正确的URL!</string>\n  <string name=\"username\">用户名</string>\n  <string name=\"change_username\">用户名</string>\n  <string name=\"password\">密码</string>\n  <string name=\"change_password\">密码</string>\n  <string name=\"password_error_whitespace\">密码中不允许有前导或尾随空格</string>\n  <string name=\"username_error_whitespace\">用户名中不允许有前导或尾随空格</string>\n  <string name=\"app_theme\">主题</string>\n  <string name=\"language\">语言</string>\n  <string name=\"use_device_language\">使用设备语言</string>\n  <string name=\"font_size\">字体大小</string>\n  <string name=\"change_font_size\">字体大小</string>\n  <string name=\"font_size_extra_large\">极大</string>\n  <string name=\"font_size_large\">大</string>\n  <string name=\"font_size_medium\">中</string>\n  <string name=\"font_size_small\">小</string>\n  <string name=\"font_size_extra_small\">极小</string>\n  <string name=\"navigation\">导航</string>\n  <string name=\"swipe_navigation\">使用水平滑动</string>\n  <string name=\"buttons_navigation\">使用前进/后退按钮</string>\n  <string name=\"swipe_buttons_navigation\">使用滑动和按钮</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">下一个</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">上一个</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">表单更新</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">空白表单更新模式</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">手动</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">仅限曾下载的表单</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">完全匹配服务器</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">自动更新频率</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">每十五分钟</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">每小时</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">每6小时</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">每24小时</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">自动下载</string>\n  <string name=\"automatic_download_summary\">自动下载表单的更新版本</string>\n  <string name=\"hide_old_form_versions_setting_title\">隐藏旧版本表单</string>\n  <string name=\"hide_old_form_versions_setting_summary\">只有最新版本才会出现在“填写空白表单”中</string>\n  <string name=\"form_submission_category\">表单提交</string>\n  <string name=\"autosend_selector_title\">自动发送</string>\n  <string name=\"autosend\">自动发送</string>\n  <string name=\"off\">关</string>\n  <string name=\"wifi_autosend\">仅Wifi</string>\n  <string name=\"cellular_autosend\">仅蜂窝</string>\n  <string name=\"wifi_cellular_autosend\">Wifi或蜂窝</string>\n  <string name=\"delete_after_send\">发送后自动删除</string>\n  <string name=\"delete_after_send_summary\">发送到服务器后删除已完成的表单和媒体</string>\n  <string name=\"form_filling_category\">表单填写</string>\n  <string name=\"constraint_behavior_title\">约束处理</string>\n  <string name=\"constraint_behavior\">约束检查</string>\n  <string name=\"constraint_behavior_on_swipe\">向前滑动时验证</string>\n  <string name=\"constraint_behavior_on_finalize\">推迟检验直到完成</string>\n  <string name=\"high_resolution_title\">高分辨率视频</string>\n  <string name=\"high_resolution_summary\">启用高分辨率视频录制</string>\n  <string name=\"image_size_title\">图像大小</string>\n  <string name=\"image_size_dialog_title\">图像长边的最大像素</string>\n  <string name=\"image_size_large\">大（3072px）</string>\n  <string name=\"image_size_medium\">中（2048px）</string>\n  <string name=\"image_size_small\">小（1024px）</string>\n  <string name=\"image_size_very_small\">非常小（640px）</string>\n  <string name=\"image_size_original\">相机的原始大小（默认）</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">显示指南</string>\n  <string name=\"guidance_no\">否</string>\n  <string name=\"guidance_yes\">是-始终显示</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">是-已折叠</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">使用外部应用程序进行音频录制</string>\n  <string name=\"form_import_category\">表单导入</string>\n  <string name=\"instance_sync\">导入时将表单设为最终版本</string>\n  <string name=\"instance_sync_summary\">影响直接添加到实例文件夹的表单。</string>\n  <string name=\"analytics_preferences\">使用情况数据</string>\n  <string name=\"analytics\">收集匿名使用数据</string>\n  <string name=\"analytics_summary\">匿名使用数据帮助团队确定修复和功能的优先级。</string>\n  <string name=\"form_metadata_title\">表单元数据</string>\n  <string name=\"form_metadata\">表单元数据</string>\n  <string name=\"form_metadata_summary\">这些值将被添加到包含用户名、电子邮件和/或电话预加载字段的表单中，用于识别提交数据的人员。</string>\n  <string name=\"user_defined\">用户定义的</string>\n  <string name=\"phone_number\">电话号码</string>\n  <string name=\"email\">电子邮件地址</string>\n  <string name=\"invalid_email_address\">电子邮件地址无效！</string>\n  <string name=\"device_defined\">设备已定义</string>\n  <string name=\"device_id\">设备ID</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">不可用</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">解锁设置</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">设置已解锁</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <string name=\"setting_not_available_during_form_entry\">在填写表单时此设置不可用</string>\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">输入新密码</string>\n  <string name=\"admin_password\">管理员密码</string>\n  <string name=\"admin_password_changed\">管理员密码已成功修改</string>\n  <string name=\"admin_password_disabled\">管理员密码已禁用</string>\n  <string name=\"admin_password_incorrect\">对不起, 密码不正确!</string>\n  <string name=\"enter_admin_password\">输入管理员密码</string>\n  <string name=\"change_admin_password\">更改管理员密码</string>\n  <string name=\"show_password\">显示密码</string>\n  <string name=\"found_in_main\">不选中以从主菜单中隐藏</string>\n  <string name=\"found_in_settings\">取消选中以隐藏不受保护的设置</string>\n  <string name=\"form_entry\">取消勾选以在表单开头隐藏</string>\n  <string name=\"form_end\">取消勾选以在表单结尾隐藏</string>\n  <string name=\"reset_project_settings_title\">重置</string>\n  <string name=\"reset_project_settings_subtext\">从设置、表单和数据中进行选择</string>\n  <string name=\"delete_project\">删除</string>\n  <string name=\"select_all\">全选</string>\n  <string name=\"clear_all\">清空</string>\n  <string name=\"reset_settings\">所有设置（内部设置、保存的设置）</string>\n  <string name=\"reset_saved_forms\">已保存的表单（实例文件夹、实例数据库）</string>\n  <string name=\"reset_blank_forms\">空白表单（表单文件夹、表单数据库、元素集数据库）</string>\n  <string name=\"reset_layers\">地图图层（图层文件夹）</string>\n  <string name=\"reset_cache\">表单加载缓存（.cache文件夹）</string>\n  <string name=\"reset_settings_dialog_title\">选择要重置的内容</string>\n  <string name=\"reset_settings_button_reset\">重置</string>\n  <string name=\"reset_in_progress\">正在重置…</string>\n  <string name=\"reset_app_state_result\">重置结果</string>\n  <string name=\"reset_app_state_warning\">所有选定的数据将被永久删除。无法撤消。</string>\n  <string name=\"reset_settings_result\">所有设置 :: %s</string>\n  <string name=\"reset_saved_forms_result\">保存的表单 :: %s</string>\n  <string name=\"reset_blank_forms_result\">空白表单 :: %s</string>\n  <string name=\"reset_layers_result\">地图图层 :: %s</string>\n  <string name=\"reset_cache_result\">表单加载缓存 :: %s</string>\n  <string name=\"main_menu_settings\">主菜单设置</string>\n  <string name=\"main_menu_settings_summary\">显示或隐藏按钮</string>\n  <string name=\"user_settings\">用户设置</string>\n  <string name=\"user_setting_summary\">显示或隐藏设置</string>\n  <string name=\"form_entry_setting\">表单填写设置</string>\n  <string name=\"form_entry_settings_summary\">显示或隐藏操作</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">向后移动</string>\n  <string name=\"moving_backwards_disabled_title\">向后移动已禁用</string>\n  <string name=\"moving_backwards_disabled_message\">配置设备以防止用户绕过此设置？\\n\\n更改如下：\\n\\u2022 禁用“编辑草稿”\\n\\u2022 禁用“另存为草稿”\\n\\u2022 禁用“转到提示”\\n\\u2022 将约束处理设置为向前滑动时验证</string>\n  <string name=\"yes\">是</string>\n  <string name=\"no\">否</string>\n  <string name=\"moving_backwards_enabled_title\">已启用向后移动</string>\n  <string name=\"moving_backwards_enabled_message\">您可能希望检查以下设置:\\n\\n\\u2022 编辑草稿\\n\\u2022 另存为草稿\\n\\u2022 前往提示\\n\\u2022 约束处理</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">保存为草稿</string>\n  <string name=\"save_mid_summary\">退出表单前，请点击顶部栏中的保存图标以及“保存为草稿”按钮</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">关于</string>\n  <string name=\"tell_your_friends\">分享应用</string>\n  <string name=\"tell_your_friends_msg\">你的同事还在收集纸上的数据吗？与他们共享本应用。</string>\n  <string name=\"leave_a_review\">留下Play Store评论</string>\n  <string name=\"leave_a_review_msg\">你的（希望是正面的）评论提高了应用程序在应用商店中的知名度。</string>\n  <string name=\"odk_website\">访问网站</string>\n  <string name=\"odk_website_summary\">应用用于在具有挑战性的环境中收集社会数据。</string>\n  <string name=\"odk_forum\">加入论坛</string>\n  <string name=\"odk_forum_summary\">加入论坛以获得支持、请求功能、贡献代码/翻译！</string>\n  <string name=\"all_open_source_licenses\">开源库/许可证</string>\n  <string name=\"all_open_source_licenses_msg\">我们站在巨人的肩膀上！</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">使用二维码重新配置</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">替换所有设置</string>\n  <string name=\"qr_code\">二维码配置</string>\n  <string name=\"scan_qr_code_fragment_title\">扫描</string>\n  <string name=\"view_qr_code_fragment_title\">二维码</string>\n  <string name=\"import_qrcode_sd\">导入二维码</string>\n  <string name=\"successfully_imported_settings\">成功导入设置</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">已达到最大字符数限制：无法为所有设置生成二维码</string>\n  <string name=\"share\">分享</string>\n  <string name=\"turn_on_flashlight\">打开闪光灯</string>\n  <string name=\"turn_off_flashlight\">关闭闪光灯</string>\n  <string name=\"qrcode_with_both_passwords\">包含敏感信息: <b>管理员</b> 和 <b>服务器密码</b></string>\n  <string name=\"qrcode_without_passwords\">二维码不包含 <b>管理员</b> 或 <b>服务器</b> 密码. 点击以配置</string>\n  <string name=\"qrcode_with_admin_password\">包含敏感信息: <b>管理员</b> 密码</string>\n  <string name=\"qrcode_with_server_password\">包含敏感信息: <b>服务器</b> 密码</string>\n  <string name=\"server_password\">服务器密码</string>\n  <string name=\"include_password_dialog\">代码中包含的密码</string>\n  <string name=\"generate\">生成</string>\n  <string name=\"invalid_qrcode\">二维码不包含有效设置</string>\n  <string name=\"qr_code_not_found\">在所选图像中找不到二维码</string>\n  <string name=\"corrupt_imported_preferences_error\">当前设置已损坏。从项目管理设置中，重置设置或导入工作设置。</string>\n  <string name=\"settings_with_gd_protocol\">无法再创建 Google Drive/Sheets 项目</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">好</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">取消</string>\n  <string name=\"cancel_loading_form\">取消</string>\n  <string name=\"cancel_location\">取消</string>\n  <string name=\"do_not_change\">取消</string>\n  <string name=\"canceling\">正在取消</string>\n  <string name=\"notification_channel_name\">一般通知</string>\n  <string name=\"success\">成功</string>\n  <string name=\"error_occured\">错误</string>\n  <string name=\"please_wait\">请稍候.</string>\n  <string name=\"please_wait_long\">请稍候. 这可能需要几分钟.</string>\n  <string name=\"fs_delete_media_path_if_file_error\">无法删除 \\'%s\\'. 请手动删除文件并重新下载表单.</string>\n  <string name=\"fs_create_media_folder_error\">无法创建媒体文件夹 \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">不能拷贝 \\'%1$s\\' 到 \\'%2$s\\'. 原因: %3$s</string>\n  <string name=\"select_odk_shortcut\">选择快捷方式</string>\n  <string name=\"background_audio_help\">此表单会录制您设备麦克风的音频。\n\n您可以使用音量指示器来确保麦克风足够接近音源。\n\n要停止录音，请退出此表单。\n\n如需更多信息，请联系要求您收集数据的人员。</string>\n  <string name=\"loading\">正在加载…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">项目</string>\n  <string name=\"settings\">设置</string>\n  <string name=\"add_project\">添加项目</string>\n  <string name=\"add\">添加</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">重复项目</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">您已经有一个具有这些连接设置的项目。要切换到现有项目还是添加新项目？</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">添加重复项目</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">切换到现有项目</string>\n  <string name=\"project_name\">项目名称</string>\n  <string name=\"project_icon\">项目图标</string>\n  <string name=\"project_color\">项目颜色</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">已切换到 %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">使用 %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">切换到 %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <string name=\"delete_project_dialog_title\">删除%s</string>\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <string name=\"delete_project_message\">删除此项目后，您将永久删除:</string>\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <string name=\"delete_project_instructions\">输入%1$s以确定</string>\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <string name=\"delete_trigger\">删除</string>\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <string name=\"form_definitions_count\">表单定义：1%1$d</string>\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <string name=\"sent_count\">已上传：%1$d</string>\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <string name=\"unsent_count\">未上传：%1$d</string>\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <string name=\"drafts_count\">草稿：%1$d</string>\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"delete_project_confirm_button_text\">删除项目</string>\n  <string name=\"or\">或</string>\n  <string name=\"hex_color\">十六进制颜色</string>\n  <string name=\"invalid_hex_code\">无效的十六进制代码</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">收集数据\\n任何位置</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">使用二维码配置</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">手动输入项目详细信息</string>\n  <string name=\"project_config_tip\">添加项目后，可以在“设置”中对其进行配置</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">还没有项目？</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">尝试演示</string>\n  <string name=\"scan_configuration_qr_code\">扫描配置二维码</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">显示详细信息</string>\n  <string name=\"errors\">错误</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <string name=\"no_browser_installed\">没有可用的浏览器</string>\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">要打开此表单，必须首先打开ODK Collect应用并切换到包含它的项目。</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">尚未配置应用收集。尝试打开应用并对其进行配置。\\n\\n如果您点击了快捷方式，您可能需要在配置应用后重新创建它。</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">无法识别的URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">错误的URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">填写的表单已被删除！</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">无法编辑此草稿，因为相应的空白表格不存在或已被删除。\\n\\n表格ID: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">您已下载了两个具有相同表单 ID 和版本的不同表单。它们可能是在不同时间或上传到不同服务器的同一个表单。您应删除其中一个。</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <string name=\"cannot_open_form_because_of_forms_update\">该表单正在更新，无法打开。请稍后重试。</string>\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">打开设置</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <string name=\"entities_title\">實體列表</string>\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <string name=\"entity_browser_button\">查看实体列表</string>\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">离线</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">此表单不受当前版本的 ODK Collect。请升级 Collect应用。\n如果问题仍然存在，请告知收集您数据的人员。\n\n表单实体规范版本：1%s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">应用程序上次运行时发生了崩溃！</string>\n  <string name=\"cant_start_app\">无法启动应用程序！</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">该项目之前已连接到 Google Drive 帐户。Google Drive 支持已被删除。您可以配置服务器或将提交内容传至电脑。\n\n</string>\n  <string name=\"learn_more_button_text\">了解更多</string>\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">开发人员工具</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">崩溃应用程序</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">强制执行导致应用程序崩溃的未捕获异常</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <string name=\"debug_filters\">调试筛选器</string>\n  <string name=\"debug_filters_summary\">在表单填写过程中显示筛选表达式执行的调试信息</string>\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"use_zxing_for_barcode_scanning\">转换为旧款二维码扫描器</string>\n  <string name=\"permission_dialog_title\">关于权限</string>\n  <string name=\"permission_dialog_text\">您将被要求允许 ODK Collect 访问以下功能，如果您想使用它们，请选择“允许”。</string>\n  <string name=\"notifications\">提醒事项</string>\n  <string name=\"required_for_notifications\"> 在下载、更新和发送表单时需要显示更新。</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">您的表单已保存为草稿。</string>\n  <string name=\"form_saved\">你的表单已被保存。</string>\n  <string name=\"form_sending\">上传表单中…</string>\n  <string name=\"edit_form\">编辑</string>\n  <string name=\"view_form\">检视</string>\n  <string name=\"close_snackbar\">关闭通知</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"other\">最后发送的表单：%d 秒前</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"other\">最后发送的表单：%d 分钟前</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"other\">最后发送的表单：%d 小时前</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"other\">最后发送的表单：%d 天前</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"other\">%d 表单已准备上传</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">在后续版本中，已完成的表单将不再可编辑。请将表单保存为草稿以便之后编辑。\n\n您可以通过点击右上角的三个点（⋮），然后选择“检查错误”来检查草稿表单中的错误。</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">在后续版本中，最终确定的表单将不再可编辑。</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">完成所有草稿</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"other\">您要完成 %d 草稿?</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">一旦您完成所有草稿，它们将处于“准备发送”状态，并且您将无法进行编辑。任何有错误的草稿都不会被最终确定。\\n\\n您将无法撤消此操作。</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"other\">成功！%d 份草稿已完成。</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"other\">%d 个草稿有错误，必须在最终确定之前解决。</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d 份草稿已完成。%d 个草稿有错误，必须在最终确定之前解决。</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d 份草稿已完成。剩下的草稿需要手动完成。</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">错误</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">无错误</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">取消选中即可在草稿中隐藏</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">没有可显示的内容</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">无空白表单</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">下载表单以开始使用</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">無草稿</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">当您保存为草稿时，表单将显示在此处</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">尚无准备发送的表單</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">当你完成草稿时，它们将显示在这里</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">沒有任何已發送的表單</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">当您上传最终表单后，它们将在这里显示</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">无下载表单</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">下载表单以开始使用</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">没有要删除的表单</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">下载空白表单后，它们将显示在这里</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">当您保存表单后，它们将显示在此处</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">恢复您的作业？</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"savepoint_recovery_dialog_message\">\\'Collect 于 \\'EEE, MMM dd, yyyy HH:mm\\' 意外关闭并保存了您的工作。\\n\\n您想恢复还是放弃它？\\'</string>\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">恢复</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">放弃</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">取得定位点</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">检视或查看定位点</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">查看定位点</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">变更定位点</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">取得线段</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">查看或变更线段</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">检视线段</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">获取多边形</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">检视或变更多边形</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">检视多边形</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <string name=\"settings_json\">设置JSON</string>\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <string name=\"preparing_form_edit\">正在准备编辑的表单…</string>\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <string name=\"user_visible_instance_name\">%s (设置%d)</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_title\">继续编辑草稿？</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_draft_edit_found_dialog_message\">“草稿编辑已保存于” EEE, MMM dd, yyyy “的” HH:mm\n\n您要继续此编辑吗？</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <string name=\"newer_draft_edit_found_dialog_positive_button\">继续编辑</string>\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_title\">编辑最新的版本？</string>\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"newer_finalized_edit_found_dialog_message\">‘此版本已被编辑过。\n\n您要编辑最新的最终版本，保存于’ EEE, MMM dd, yyyy ’的‘ HH:mm吗？’</string>\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <string name=\"newer_finalized_edit_found_dialog_positive_button\">编辑最新的表单</string>\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n  <string name=\"finalized_form_edit_started\">您正在编辑此表单，点击一个问题以开始编辑。</string>\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <string name=\"enter_data\">新建表格</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"review_data\">草稿</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"send_data\">準備傳送</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"view_sent_forms\">已傳送</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"get_forms\">下載表格</string>\n  <!--Text for an action button on the main screen.-->\n  <string name=\"manage_files\">刪除表格</string>\n  <string name=\"saved_forms\">已保存的表格</string>\n  <string name=\"version_number\">版本: %s</string>\n  <string name=\"id_number\">ID: %s</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'添加於\\' EEE, MMM dd, yyyy \\' \\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"updated_on_date_at_time\">\\'更新於\\' EEE, MMM dd, yyyy \\' \\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'保存於\\' EEE, MMM dd, yyyy \\' \\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'完成於\\' EEE, MMM dd, yyyy \\' \\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'傳送於\\' EEE, MMM dd, yyyy \\' \\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'傳送失敗於\\' EEE, MMM dd, yyyy \\' \\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"deleted_on_date_at_time\">\\'於\\' EEE, MMM dd, yyyy \\' \\' HH:mm刪除</string>\n  <string name=\"submission_deleted\">提交已刪除</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"modified_on_date_at_time\">\\'修改日期\\' EEE, MMM dd, yyyy \\' \\' HH:mm</string>\n  <string name=\"encrypted_form\">加密的表單</string>\n  <string name=\"deleted_form\">刪除的表單</string>\n  <string name=\"sort_the_list\">排序列表</string>\n  <string name=\"sort_by\">排序</string>\n  <string name=\"sort_by_name_asc\">依名稱：A-&gt;Z</string>\n  <string name=\"sort_by_name_desc\">依名稱：Z-&gt;A</string>\n  <string name=\"sort_by_date_desc\">依日期：由新至舊</string>\n  <string name=\"sort_by_date_asc\">依日期：由舊至新</string>\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <string name=\"sort_by_last_saved\">依保存時間：由新至舊</string>\n  <string name=\"filter_the_list\">篩選列表</string>\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <string name=\"search\">搜索</string>\n  <string name=\"not_granted_permission\">您嘗試啟動的活動需要未授予的權限。</string>\n  <string name=\"storage_runtime_permission_denied_title\">存儲權限</string>\n  <string name=\"storage_runtime_permission_denied_desc\">沒有這些權限，程式無法存取您的表單或填寫的資料。請授與權限後，再重新開啟此程式。</string>\n  <string name=\"location_runtime_permissions_denied_title\">位置權限</string>\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <string name=\"location_runtime_permissions_denied_desc\">沒有 <b>精確位置</b> 權限，Collect 無法記錄您的位置。請授與權限後再重試。</string>\n  <string name=\"camera_runtime_permission_denied_title\">相機權限</string>\n  <string name=\"camera_runtime_permission_denied_desc\">沒有此權限，Collect 無法存取相機。請授與權限後再重試。</string>\n  <string name=\"record_audio_runtime_permission_denied_title\">錄製音頻權限</string>\n  <string name=\"record_audio_runtime_permission_denied_desc\">沒有此權限，Collect 無法存取麥克風。請在準備好授予時重試。</string>\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"open_form_map\">打開表單地圖</string>\n  <string name=\"instance_scan_count\">添加了 %1$d 個表單.</string>\n  <string name=\"finished_disk_scan\">文件掃瞄結束，所有表格已經被載入。</string>\n  <string name=\"xform_parse_error\"> %1$s解析出錯: %2$s 缺少或無效。</string>\n  <string name=\"loading_form\">正在載入表格</string>\n  <string name=\"survey_loading_reading_form_message\">正在讀取表單定義…</string>\n  <string name=\"survey_loading_reading_data_message\">正在讀取調查資料…</string>\n  <string name=\"survey_loading_reading_csv_message\">正在讀取CSV文件…</string>\n  <string name=\"parse_error\">無法讀取表格。</string>\n  <string name=\"loading_form_failed\">載入表單時出錯。請重試。</string>\n  <string name=\"cannot_edit_completed_form\">一旦此表格被標記為「最終版」，它便不能再次編輯，因為它可能已被加密。</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">已從自動儲存點恢復未儲存的修改！</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">更改語言</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">前往提示</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <string name=\"save_all_answers\">儲存為草稿</string>\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">開始</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">結束</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">返回</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <string name=\"group_label\">群組</string>\n  <!--Displayed below repeat names in a form's table of contents-->\n  <string name=\"repeatable_group_label\">可重複群組</string>\n  <string name=\"exit\">退出</string>\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <string name=\"add_repeat_question\">增加\\\"%s\\\"項目？</string>\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <string name=\"add_repeat\">增加</string>\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <string name=\"add_another_menu\">增加其他項目</string>\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">修改提示</string>\n  <string name=\"clearanswer_confirm\">刪除針對 \\\"%s\\\" 的回答?</string>\n  <string name=\"clear_answer\">刪除回答</string>\n  <string name=\"clear_answer_ask\">刪除這個回答?</string>\n  <string name=\"clear_answer_no\">取消</string>\n  <string name=\"discard_answer\">刪除回答</string>\n  <string name=\"discard_group\">刪除群組</string>\n  <string name=\"delete_repeat\">刪除群組</string>\n  <string name=\"delete_repeat_ask\">刪除這個群組?</string>\n  <string name=\"delete_repeat_confirm\">刪除群組 \\\"%s\\\" 及其子群組?</string>\n  <string name=\"delete_repeat_no\">取消</string>\n  <string name=\"invalid_answer_error\">回答無效!</string>\n  <string name=\"required_answer_error\">此問題為必答題!</string>\n  <string name=\"success_form_validation\">成功！表格中未發現錯誤。</string>\n  <string name=\"invalid_space_in_answer_singular\">警告：內在值 %s 有空白符號</string>\n  <string name=\"invalid_space_in_answer_plural\">警告：多個內在值 %s 有空白符號</string>\n  <string name=\"too_complex_form\">此表單對於此設備太複雜。嘗試簡化表達或在論壇上尋求幫助。</string>\n  <string name=\"survey_internal_error\">內部錯誤：提示步驟失敗。</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">您已在 %s 的結尾。</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <string name=\"form_editing_disabled_hint\">如果您需要對表格進行編輯，請點選「儲存為草稿」，直到您準備好傳送為止。</string>\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <string name=\"save_as_draft\">儲存為草稿</string>\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <string name=\"finalize\">確定完成</string>\n  <!--Button text for sending a submission.-->\n  <string name=\"send\">傳送</string>\n  <string name=\"data_saved_error\">表格儲存失敗!</string>\n  <string name=\"data_saved_ok\">表格已成功儲存!</string>\n  <string name=\"save_point_error\">錯誤！儲存還原點文件：%s</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"do_not_save\">放棄表格</string>\n  <string name=\"discard_changes\">放棄變更</string>\n  <string name=\"keep_changes\">儲存修改</string>\n  <string name=\"keep_editing\">繼續編輯</string>\n  <string name=\"quit_form_title\">儲存表格？</string>\n  <string name=\"save_explanation\">您可以儲存此表單，並隨時從草稿中存取它。</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"save_explanation_with_last_saved\">\\'此表格最後儲存於\\' EEE, MMM dd, yyyy\\' \\'HH:mm\\'。另存為草稿以保留變更。\\'</string>\n  <string name=\"quit_form_continue_title\">繼續編輯？</string>\n  <string name=\"discard_form_warning\">如果您放棄儲存表單，您將失去所有已做的修改。</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"discard_changes_warning\">\\'此表格最後儲存於\\' EEE, MMM dd, yyyy HH:mm \\'，如果您放棄變更，將失去此後所做的更改。\\'</string>\n  <string name=\"saving_form\">正在儲存表格</string>\n  <string name=\"survey_saving_validating_message\">正在驗證答案…</string>\n  <string name=\"survey_saving_collecting_message\">正在收集資料…</string>\n  <string name=\"survey_saving_saving_message\">正在儲存至SD卡…</string>\n  <string name=\"survey_saving_finalizing_message\">正儲存到SD卡…</string>\n  <string name=\"survey_saving_encrypting_message\">加密資料</string>\n  <string name=\"not_exactly_one_record_for_this_instance\">此實例中沒有一條記錄！</string>\n  <string name=\"not_exactly_one_blank_form_for_this_form_id\">並非只有一個空白表單與此 jr_form_id 匹配。</string>\n  <string name=\"no_form_id_specified\">未指定表單ID???</string>\n  <string name=\"phone_does_not_support_rsa\">手機不支持RSA加密。</string>\n  <string name=\"invalid_rsa_public_key\">RSA公鑰無效。</string>\n  <string name=\"encryption_error_message\">%s 表單尚未儲存為完成狀態。</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <string name=\"form_cannot_be_used\">表單無法被使用</string>\n  <!--Text displayed when audio recording fails to start-->\n  <string name=\"start_recording_failed\">無法開始錄製。</string>\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <string name=\"mic_in_use\">麥克風已在使用中。</string>\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <string name=\"answer_file_copy_failed_message\">無法將媒體附加到表單，但可以在 %1$s 取得</string>\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <string name=\"recording_disabled\">錄製已禁用。在 %s 中啟用</string>\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <string name=\"update_widgets_error\">嘗試更新數值失敗，這通常是因為表單設計的 calculation 欄位使用不正確所引起。\\n\\n如果您一直存在此問題，請向要求您收集資料的人報告。</string>\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">執行</string>\n  <string name=\"activity_not_found\">沒找到與 %s 對應的活動</string>\n  <string name=\"no_app\">所請求的應用程式無法使用，請手動填入對應值。</string>\n  <string name=\"null_intent_value\">外部應用程式未提供預期訊息。</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"print\">列印</string>\n  <string name=\"open_url\">打開Url</string>\n  <string name=\"capture_osm\">啟動 OpenMapKit</string>\n  <string name=\"recapture_osm\">重做OSM標記</string>\n  <string name=\"edited_osm_file\">已編輯的OSM XML文件：</string>\n  <string name=\"invalid_osm_data\">出了問題，我們沒有獲得有效的OSM資料。</string>\n  <string name=\"alert\">警告</string>\n  <string name=\"install_openmapkit\">請安裝OpenMapKit！</string>\n  <string name=\"get_barcode\">取得條碼</string>\n  <string name=\"replace_barcode\">替換條碼</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <string name=\"barcode_scanner_prompt\">請將條碼放於矩形內</string>\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"open_file\">打開文件</string>\n  <string name=\"rank_items\">排序項目</string>\n  <string name=\"number_picker_title\">數字選擇器</string>\n  <string name=\"invalid_range_widget\">此部件的參數無效！</string>\n  <string name=\"svg_file_does_not_exist\">SVG文件不存在！</string>\n  <string name=\"capture_image\">拍照</string>\n  <string name=\"choose_image\">選擇圖片</string>\n  <string name=\"selected_invalid_image\">所選文件不是有效影像檔案</string>\n  <string name=\"take_picture_instruction\">點擊螢幕拍照</string>\n  <string name=\"error_front_camera_unavailable\">前置鏡頭在此設備上不可用</string>\n  <string name=\"camera_error\">拍照時出錯</string>\n  <string name=\"camera_failed_to_initialize\">無法啟動相機！</string>\n  <string name=\"annotate_image\">註釋該圖片</string>\n  <string name=\"gif_not_supported\">Gif 檔案格式不支援</string>\n  <string name=\"invalid_file_type\">應用程式返回了無效的文件類型</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">儲存並關閉</string>\n  <string name=\"signature_capture\">取得簽名</string>\n  <string name=\"sign_button\">收集簽名</string>\n  <string name=\"markup_image\">修改影像</string>\n  <string name=\"draw_image\">繪製圖像</string>\n  <string name=\"reset_image\">重置</string>\n  <string name=\"set_color\">選擇顏色</string>\n  <string name=\"capture_video\">拍攝影片</string>\n  <string name=\"choose_video\">選擇影片</string>\n  <string name=\"play_video\">播放影片</string>\n  <string name=\"choose_file\">選擇文件</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <string name=\"delete_answer_file\">刪除</string>\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">錄音</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">選擇錄音檔</string>\n  <!--Description text for button that that pauses recording sound-->\n  <string name=\"pause_recording\">暫停</string>\n  <!--Description text for button that that resumes recording sound-->\n  <string name=\"resume_recording\">恢復</string>\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <string name=\"record_audio_on\">錄音：開啟</string>\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <string name=\"record_audio_off\">錄音：關閉</string>\n  <string name=\"background_audio_permission_explanation\">此表單在背景錄音。您必須授予使用麥克風的權限，否則無法開啟此表單。</string>\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <string name=\"stop_recording_confirmation\">此表單要求背景錄音，停用它將停止錄製，並放棄現有錄音，請您確定是否繼續？</string>\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <string name=\"disable_recording\">停用錄音</string>\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <string name=\"background_audio_recording_enabled_explanation\">錄音不會立即開始，您必須重新開啟表單進行記錄。</string>\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <string name=\"delete_answer_file_question\">刪除文件？</string>\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <string name=\"answer_file_delete_warning\">刪除此文件後，您將無法恢復該文件，請您確定是否繼續？</string>\n  <!--Text for button that stops audio recording-->\n  <string name=\"stop_recording\">停止</string>\n  <!--Text to let the user know a recording is in progress-->\n  <string name=\"recording\">正在錄製…</string>\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <string name=\"recording_channel\">錄音</string>\n  <!--Text for button that plays audio clip-->\n  <string name=\"play_audio\">播放聲音</string>\n  <!--Text to let the user know a file is being saved-->\n  <string name=\"saving_file\">儲存文件</string>\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"recording_warning\">離開此螢幕前必須停止錄音。</string>\n  <string name=\"error_attaching_binary_file\">在存取媒體文件： %s 時發生了錯誤</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_value\">選擇數值</string>\n  <string name=\"select_answer\">選擇回答</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"edit_value\">編輯數值</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"no_value_selected\">沒有選定的數值</string>\n  <string name=\"selected_title\">選定: </string>\n  <string name=\"trigger\">好的，請繼續。</string>\n  <string name=\"ext_file_no_data_error\">此文件不包含資料！</string>\n  <string name=\"ext_conflicting_columns_error\">列 %s 相符!</string>\n  <string name=\"ext_sortBy_numeric_error\">sortby列應該只包含數值。衝突值為 \\'%s\\'.</string>\n  <string name=\"ext_search_wrong_arguments_error\">search() 函數語法錯誤：該函數需要1、4或6個參數。</string>\n  <string name=\"ext_search_wrong_function_error\">search() 函數中的語法錯誤：未識別的函數 \\'%s\\'.</string>\n  <string name=\"ext_search_bad_function_error\">search()函數中的語法錯誤: \\'%s\\' 未作為函數計算.</string>\n  <string name=\"ext_search_return_error\">搜索處理程式返回了類型為 \\'%s\\'的對象.</string>\n  <string name=\"ext_import_generic_error\">無法從 %1$s 中導入資料，原因：%2$s</string>\n  <string name=\"ext_import_progress_message\">從中 \\'%1$s\\'預載入資料, 請稍候… %2$s</string>\n  <string name=\"ext_import_cancelled_message\">讀取資料已取消！</string>\n  <string name=\"ext_import_finalizing_message\">正在完成預載入的資料…</string>\n  <string name=\"ext_import_completed_message\">讀取資料已完成！</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager尚未初始化。</string>\n  <string name=\"ext_import_csv_missing_error\">外部資料 %1$s 尚未導入. 也許您忘記在表單中包含 %2$s.csv 文件?</string>\n  <string name=\"ext_search_generic_error\">search() 函數中的語法錯誤: %s</string>\n  <string name=\"ext_assign_value_error\"> \\'%s\\' 無法賦予值。</string>\n  <string name=\"parser_exception\">XPathParser 異常: \\\"%s\\\"</string>\n  <string name=\"get_bearing\">記錄方位</string>\n  <string name=\"replace_bearing\">替代方位</string>\n  <string name=\"getting_bearing\">載入方位</string>\n  <string name=\"accept_bearing\">記錄方位</string>\n  <string name=\"bearing\">方位: %.3f</string>\n  <string name=\"direction\">方向: %s</string>\n  <string name=\"bearing_lack_of_sensors\">無法收集方位資訊：缺少加速計或磁場傳感器設備，或兩者都缺。</string>\n  <string name=\"select_date\">選擇日期</string>\n  <string name=\"select_time\">選擇時間</string>\n  <string name=\"no_date_selected\">未選擇日期</string>\n  <string name=\"no_time_selected\">未選擇時間</string>\n  <string name=\"ethiopian_month_1\">Meskerem</string>\n  <string name=\"ethiopian_month_2\">Tikimt</string>\n  <string name=\"ethiopian_month_3\">Hidar</string>\n  <string name=\"ethiopian_month_4\">Tahsas</string>\n  <string name=\"ethiopian_month_5\">Tir</string>\n  <string name=\"ethiopian_month_6\">Yekatit</string>\n  <string name=\"ethiopian_month_7\">Megabit</string>\n  <string name=\"ethiopian_month_8\">Miazia</string>\n  <string name=\"ethiopian_month_9\">Ginbot</string>\n  <string name=\"ethiopian_month_10\">Senie</string>\n  <string name=\"ethiopian_month_11\">Hamlie</string>\n  <string name=\"ethiopian_month_12\">Nehasie</string>\n  <string name=\"ethiopian_month_13\">Pagumien</string>\n  <string name=\"coptic_month_1\">Thout</string>\n  <string name=\"coptic_month_2\">Paopi</string>\n  <string name=\"coptic_month_3\">Hathor</string>\n  <string name=\"coptic_month_4\">Koiak</string>\n  <string name=\"coptic_month_5\">Tobi</string>\n  <string name=\"coptic_month_6\">Meshir</string>\n  <string name=\"coptic_month_7\">Paremhat</string>\n  <string name=\"coptic_month_8\">Parmouti</string>\n  <string name=\"coptic_month_9\">Pashons</string>\n  <string name=\"coptic_month_10\">Paoni</string>\n  <string name=\"coptic_month_11\">Epip</string>\n  <string name=\"coptic_month_12\">Mesori</string>\n  <string name=\"coptic_month_13\">Pi Kogi Enavot</string>\n  <string name=\"islamic_month_1\">Muharram</string>\n  <string name=\"islamic_month_2\">Safar</string>\n  <string name=\"islamic_month_3\">Rabi\\' al-awwal</string>\n  <string name=\"islamic_month_4\">Rabi\\' al-thani</string>\n  <string name=\"islamic_month_5\">Jumada al-awwal</string>\n  <string name=\"islamic_month_6\">Jumada al-thani</string>\n  <string name=\"islamic_month_7\">Rajab</string>\n  <string name=\"islamic_month_8\">Sha\\'ban</string>\n  <string name=\"islamic_month_9\">Ramadan</string>\n  <string name=\"islamic_month_10\">Shawwal</string>\n  <string name=\"islamic_month_11\">Dhu al-Qidah</string>\n  <string name=\"islamic_month_12\">Dhu al-Hijjah</string>\n  <string name=\"persian_month_1\">Farvardin</string>\n  <string name=\"persian_month_2\">Ordibehesht</string>\n  <string name=\"persian_month_3\">Khordad</string>\n  <string name=\"persian_month_4\">Tir</string>\n  <string name=\"persian_month_5\">Mordad</string>\n  <string name=\"persian_month_6\">Shahrivar</string>\n  <string name=\"persian_month_7\">Mehr</string>\n  <string name=\"persian_month_8\">Aban</string>\n  <string name=\"persian_month_9\">Azar</string>\n  <string name=\"persian_month_10\">Dey</string>\n  <string name=\"persian_month_11\">Bahman</string>\n  <string name=\"persian_month_12\">Esfand</string>\n  <!--1st month in the Buddhist calendar-->\n  <string name=\"buddhist_month_1\">มกราคม</string>\n  <!--2nd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_2\">กุมภาพันธ์</string>\n  <!--3rd month in the Buddhist calendar-->\n  <string name=\"buddhist_month_3\">มีนาคม</string>\n  <!--4th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_4\">เมษายน</string>\n  <!--5th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_5\">พฤษภาคม</string>\n  <!--6th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_6\">มิถุนายน</string>\n  <!--7th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_7\">กรกฎาคม</string>\n  <!--8th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_8\">สิงหาคม</string>\n  <!--9th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_9\">กันยายน</string>\n  <!--10th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_10\">ตุลาคม</string>\n  <!--11th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_11\">พฤศจิกายน</string>\n  <!--12th month in the Buddhist calendar-->\n  <string name=\"buddhist_month_12\">ธันวาคม</string>\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <string name=\"custom_date\">%1$s (%2$s)</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">文件: %s 不是有效文件。</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">文件: %s 不能被找到。</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity\">輸入身份</string>\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"identity\">身份</string>\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <string name=\"enter_identity_description\">此表單要求您確認身份</string>\n  <!--Action to close a full screen dialog-->\n  <string name=\"close\">關閉</string>\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes\">變更原因</string>\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason\">理由</string>\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <string name=\"reason_for_changes_description\">您需要解釋更改此表單的原因</string>\n  <!--Action to save the contents of a full screen dialog-->\n  <string name=\"save\">儲存</string>\n  <!--Button text to select a place from a list presented on a map-->\n  <string name=\"select_place\">選擇地點</string>\n  <string name=\"choices\">選擇</string>\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <string name=\"openmap_usgs_topo\">USGS地形圖</string>\n  <string name=\"openmap_usgs_sat\">USGS混合地圖</string>\n  <string name=\"openmap_usgs_img\">USGS影像地圖</string>\n  <string name=\"openmap_cartodb_positron\">CartoDB Positron</string>\n  <string name=\"openmap_cartodb_darkmatter\">CartoDB Dark Matter</string>\n  <string name=\"basemap\">底圖</string>\n  <string name=\"basemap_source\">來源</string>\n  <string name=\"basemap_source_unavailable\">抱歉, %s 此設備上沒有底圖。請在「設置」中選擇另一個底圖 &gt;  地圖.</string>\n  <string name=\"map_style_label\">%s 地圖樣式</string>\n  <string name=\"streets\">街道</string>\n  <string name=\"terrain\">地形</string>\n  <string name=\"hybrid\">混合</string>\n  <string name=\"satellite\">衛星</string>\n  <string name=\"light\" tools:ignore=\"UnusedResources\">淺色</string>\n  <string name=\"dark\" tools:ignore=\"UnusedResources\">深色</string>\n  <string name=\"outdoors\" tools:ignore=\"UnusedResources\">戶外</string>\n  <string name=\"topographic\">地形圖</string>\n  <string name=\"google_play_services_error_occured\">無法存取谷歌地圖。是否安裝了Google Play服務？</string>\n  <string name=\"carto_map_style_positron\">Positron</string>\n  <string name=\"carto_map_style_dark_matter\">Dark Matter</string>\n  <!--Title for the offline layer section of settings-->\n  <string name=\"offline_layer\">離線圖層</string>\n  <!--Label for the offline layer setting-->\n  <string name=\"layer_data_file\">圖層</string>\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <string name=\"offline_layer_dialog_title\">選擇離線圖層</string>\n  <!--Description of how to add offline layers to the project-->\n  <string name=\"offline_layer_dialog_description\">選擇要用於該項目中所有地圖的離線圖層。您可從設備中選取 MBTiles 文件，添加選項到列表中，也可以從列表刪除現有圖層。</string>\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <string name=\"get_help_with_offline_layers\">瞭解有關添加 MBTiles 的更多訊息。</string>\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <string name=\"add_layer\">新增圖層</string>\n  <!--Text for button that lets the user delete a offline layer-->\n  <string name=\"delete_layer\">刪除圖層</string>\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <plurals name=\"non_mbtiles_files_selected_title\">\n    <item quantity=\"other\">%d 圖層無法新增</item>\n  </plurals>\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <string name=\"all_non_mbtiles_files_selected_message\">您選擇的文件不是 MBTiles。您只能添加 MBTiles 文件。</string>\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <string name=\"some_non_mbtiles_files_selected_message\">某些您選擇的文件不是 MBTiles 格式，您只能添加 MBTiles 文件。</string>\n  <!--Text for title above list of layers-->\n  <string name=\"layers_list_title\">圖層</string>\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <string name=\"select_layer_access_title\">選擇存取圖層</string>\n  <string name=\"select_layer_access_subtitle\">您希望圖層在所有項目中使用，還是僅在當前項目中使用？</string>\n  <!--Text for option where layer should be available to all projects-->\n  <string name=\"all_projects_option\">所有項目</string>\n  <!--Text for option where layer should be available to the current project only-->\n  <string name=\"current_project_option\">目前項目</string>\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"delete_layer_confirmation_message\">您確定要刪除 %1$s 離線圖層嗎？</string>\n  <string name=\"record_geopoint\">記錄點</string>\n  <string name=\"provider_disabled_error\">位置服務被關閉！</string>\n  <string name=\"google_play_services_not_available\">此表單希望跟蹤您的位置，但Google Play服務不可用。</string>\n  <string name=\"location_providers_disabled_dialog_message\">此表單希望跟蹤您的位置，但位置提供程式被禁用，請在Android設置中啟用。</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <string name=\"go_to_settings\">轉到設置</string>\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <string name=\"track_location_on\">追蹤位置：開啟</string>\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <string name=\"track_location_off\">追蹤位置：關閉</string>\n  <string name=\"background_location_disabled\">此表單希望跟蹤您的位置，但已禁用跟蹤，請在選單 %1$s 上啟用</string>\n  <string name=\"background_location_enabled\">此表單跟蹤您的位置，您可以選單上 %1$s 中禁用跟蹤。</string>\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <string name=\"validate\">檢查錯誤</string>\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <string name=\"time_elapsed\">已用時間：%1$s</string>\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <string name=\"point_will_be_saved\">點將儲存在 %1$s</string>\n  <!--%1$s is the number of satellites being used to get location-->\n  <string name=\"satellites\">衛星：%1$s</string>\n  <string name=\"waiting_for_location\">正在嘗試取得位置，請稍候..</string>\n  <string name=\"poor_accuracy\">精度差，請稍候。</string>\n  <string name=\"unacceptable_accuracy\">精度不可接受，請稍候。</string>\n  <string name=\"improving_accuracy\">提高準確性，請稍候。</string>\n  <string name=\"accuracy_status_title\">正在取得位置</string>\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <string name=\"west\">W %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <string name=\"east\">E %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <string name=\"south\">S %1$s%2$s%3$s</string>\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <string name=\"north\">N %1$s%2$s%3$s</string>\n  <string name=\"input_method\">輸入方法</string>\n  <string name=\"start\">開始</string>\n  <string name=\"enable_gps\">啟用GPS</string>\n  <string name=\"gps_enable_message\">設備上的GPS已禁用，是否要啟用它？</string>\n  <string name=\"geo_clear_warning\">對像已建立，是否要清除該對像？</string>\n  <string name=\"geo_exit_warning\">放棄更改並返回 ODK？</string>\n  <string name=\"polygon_validator\">必須至少有3個點才能建立多邊形</string>\n  <string name=\"polyline_validator\">必須至少有2個點才能建立多段線</string>\n  <string name=\"clear\">清除</string>\n  <string name=\"placement_mode\">輕敲放置</string>\n  <string name=\"manual_mode\">手動位置記錄</string>\n  <string name=\"automatic_mode\">自動位置記錄</string>\n  <string name=\"recording_interval\">記錄間隔：</string>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_seconds\">\n    <item quantity=\"other\">%d 秒</item>\n  </plurals>\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <plurals name=\"number_of_minutes\">\n    <item quantity=\"other\">%d 分鐘</item>\n  </plurals>\n  <string name=\"accuracy_requirement\">精度要求：</string>\n  <string name=\"none\">沒有</string>\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <plurals name=\"number_of_meters\">\n    <item quantity=\"other\">%d 米</item>\n  </plurals>\n  <string name=\"collection_status_paused\">輸入的點數: %d</string>\n  <string name=\"collection_status_placement\">輸入的點數: %d (點擊以放置點)</string>\n  <string name=\"collection_status_manual\">輸入的點數: %d (手動記錄)</string>\n  <string name=\"collection_status_auto_seconds\">輸入的點數: %1$d (每隔%2$d 秒記錄)</string>\n  <string name=\"collection_status_auto_minutes\">輸入的點數: %1$d (每隔 %2$d 分鐘記錄)</string>\n  <string name=\"collection_status_auto_seconds_accuracy\">輸入的點數: %1$d (每隔 %2$d 秒記錄, 精度%3$d m)</string>\n  <string name=\"collection_status_auto_minutes_accuracy\">輸入的點數: %1$d (每隔 %2$d 分鐘記錄, 精度 %3$d m)</string>\n  <string name=\"geopoint_no_draggable_instruction\">點擊添加標記按鈕。</string>\n  <string name=\"discard\">放棄</string>\n  <string name=\"select_item_count\">%s: %d (%d 在圖上)</string>\n  <string name=\"select_item\">選擇</string>\n  <string name=\"new_item\">新項目</string>\n  <!--Name of an action button-->\n  <string name=\"edit_data\">編輯草稿</string>\n  <string name=\"view_data\">查看已儲存的表格</string>\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <string name=\"location_tracking_notification_channel_name\">位置跟蹤</string>\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <string name=\"location_tracking_notification_title\">跟蹤位置…</string>\n  <!--Description for button that zooms to the user's current location on a map-->\n  <string name=\"show_my_location\">顯示位置</string>\n  <!--Description for button that zooms a map so that it shows all items-->\n  <string name=\"zoom_to_fit_all\">縮放至適合所有</string>\n  <!--Description for button that pauses automatic location recording-->\n  <string name=\"pause_location_recording\">暫停</string>\n  <!--Description for button that removes the last recorded point-->\n  <string name=\"remove_last_point\">移除最後一點</string>\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <string name=\"remove_last_point_info_item\">移除最後一點</string>\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">正在傳送表格</string>\n  <string name=\"noselect_error\">沒有選定表格！</string>\n  <string name=\"no_connection\">沒有可用網路</string>\n  <string name=\"sending_items\">上傳 %2$s 中的 %1$s 個表格</string>\n  <string name=\"send_selected_data\">上傳選定項</string>\n  <string name=\"server_auth_credentials\">伺服器：%s無效的使用者名稱或密碼</string>\n  <string name=\"server_requires_auth\">伺服器要求驗證身份</string>\n  <string name=\"no_forms_uploaded\">沒有表格被上傳。</string>\n  <string name=\"upload_results\">上傳結果</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">修改視圖</string>\n  <string name=\"show_sent_and_unsent_forms\">顯示已傳送和未傳送的表格</string>\n  <string name=\"show_unsent_forms\">只顯示未傳送的表格</string>\n  <string name=\"send_in_progress\">後台傳送正在運行，請稍後重試</string>\n  <string name=\"forms_upload_succeeded\">表單上載成功</string>\n  <string name=\"forms_upload_failed\">表單上載失敗</string>\n  <string name=\"all_uploads_succeeded\">所有上傳成功！</string>\n  <string name=\"some_uploads_failed\">%1$s / %2$s 上載失敗!</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">更新</string>\n  <string name=\"download\">下載選取項目</string>\n  <string name=\"downloading_data\">連接到伺服器</string>\n  <string name=\"fetching_file\">正在下載 %1$s.\\n\\n表單 %2$s / %3$s 表單..</string>\n  <string name=\"form_download_progress\">%1$s. 載入媒體文件: %3$s 中的 %2$s</string>\n  <string name=\"version\">版本：</string>\n  <string name=\"form_updates_available\">表單更新可用</string>\n  <string name=\"newer_version_of_a_form_info\">這是對您的表單的更新</string>\n  <string name=\"form_update_error\">表單更新失敗</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <string name=\"load_remote_form_error\">表單列表下載失敗</string>\n  <string name=\"forms_download_succeeded\">表單下載成功</string>\n  <string name=\"forms_download_failed\">表單下載失敗</string>\n  <string name=\"all_downloads_succeeded\">所有下載成功！</string>\n  <string name=\"some_downloads_failed\">%1$s / %2$s 下載失敗!</string>\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <string name=\"form_details\">ID: %1$s 版本: %2$s</string>\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <string name=\"report_to_project_lead\">如果你一直有這個問題，請向要求你收集資料的人報告。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"unreachable_error\">無法在存取伺服器 %s，您輸入的URL是否正確?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"security_error\">無法安全連接到位於 %s 的伺服器. 您輸入的URL是否正確?</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"server_error\">伺服器 %1$s 返回的狀態 %2$s。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"invalid_response\">伺服器 %s 提供了無效的響應。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_no_hash_error\">伺服器未為此表單提供哈希值。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_save_disk_error\">下載此表單時，設備上出現磁碟錯誤。</string>\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <string name=\"form_with_invalid_submission_error\">此表單的提交URL無效。</string>\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">已儲存的表格</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">空表格</string>\n  <string name=\"delete_confirm\">刪除表格 %s ?</string>\n  <string name=\"delete_file\">刪除選擇</string>\n  <string name=\"delete_no\">不刪除</string>\n  <string name=\"delete_yes\">刪除表格</string>\n  <string name=\"form_delete_message\">刪除所選表單</string>\n  <string name=\"file_deleted_ok\">%s 表格被成功刪除!</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <string name=\"project_settings\">項目設置</string>\n  <!--Title of a settings section-->\n  <string name=\"server_settings_title\">伺服器</string>\n  <!--Subtext of a settings section-->\n  <string name=\"server_settings_subtext\">URL、使用者名稱、密碼</string>\n  <!--Title of a settings section-->\n  <string name=\"project_display_title\">項目顯示</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_display_subtext\">名稱、圖示、顏色</string>\n  <!--Title of a settings section-->\n  <string name=\"client\">使用者介面</string>\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <string name=\"maps\">地圖</string>\n  <!--Subtext of a settings section-->\n  <string name=\"maps_settings_subtext\">底圖、樣式、圖層</string>\n  <!--Title of a settings section-->\n  <string name=\"form_management_preferences\">表單管理</string>\n  <!--Subtext of a settings section-->\n  <string name=\"form_management_settings_subtext\">自動更新、自動傳送、自動刪除</string>\n  <!--Title of a settings section-->\n  <string name=\"user_and_device_identity_title\">使用者和設備標識</string>\n  <!--Subtext of a settings section-->\n  <string name=\"user_and_device_identity_settings_subtext\">使用者名稱、電話號碼、設備ID</string>\n  <!--Title of a settings section only available in beta or development builds.-->\n  <string name=\"experimental\">實驗項目</string>\n  <string name=\"protected_settings\">受保護的設置</string>\n  <!--Title of a settings section-->\n  <string name=\"unlock_protected_settings_section_title\">解鎖受保護的設置</string>\n  <!--Subtext of a settings section-->\n  <string name=\"unlock_protected_settings_section_subtext\">更改密碼，設置存取控制</string>\n  <string name=\"set_admin_password\">設置管理員密碼</string>\n  <!--Title of a settings section-->\n  <string name=\"project_management_section_title\">項目管理</string>\n  <!--Subtext of a settings section-->\n  <string name=\"project_management_section_subtext\">重新配置、重置、刪除</string>\n  <!--Title of a settings section-->\n  <string name=\"access_control_section_title\">存取控制</string>\n  <!--Subtext of a settings section-->\n  <string name=\"access_control_section_subtext\">有限使用者界面</string>\n  <string name=\"server_preferences\">伺服器設置</string>\n  <!--Label for server type setting-->\n  <string name=\"server_url\">URL</string>\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"change_server_url\">伺服器 URL</string>\n  <string name=\"url_error\">不正確的URL！</string>\n  <string name=\"username\">使用者名稱</string>\n  <string name=\"change_username\">使用者名稱</string>\n  <string name=\"password\">密碼</string>\n  <string name=\"change_password\">密碼</string>\n  <string name=\"password_error_whitespace\">密碼字串前後不能有空格</string>\n  <string name=\"username_error_whitespace\">使用者名稱字串前後不能有空格</string>\n  <string name=\"app_theme\">主題</string>\n  <string name=\"language\">語言</string>\n  <string name=\"use_device_language\">使用設備語言</string>\n  <string name=\"font_size\">字體大小</string>\n  <string name=\"change_font_size\">字體大小</string>\n  <string name=\"font_size_extra_large\">極大</string>\n  <string name=\"font_size_large\">大</string>\n  <string name=\"font_size_medium\">中</string>\n  <string name=\"font_size_small\">小</string>\n  <string name=\"font_size_extra_small\">極小</string>\n  <string name=\"navigation\">操作方式</string>\n  <string name=\"swipe_navigation\">使用螢幕左右滑動</string>\n  <string name=\"buttons_navigation\">使用前進/後退按鈕</string>\n  <string name=\"swipe_buttons_navigation\">使用滑動和按鈕</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">下一個</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_backward\">上一個</string>\n  <!--Heading for the settings category about updating blank forms-->\n  <string name=\"form_update_category\">表單更新</string>\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <string name=\"form_update_mode_title\">空白表單更新模式</string>\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <string name=\"manual\">手動</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <string name=\"previously_downloaded_only\">僅限以前下載的表單</string>\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <string name=\"match_exactly\">與伺服器完全相符</string>\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <string name=\"form_update_frequency_title\">自動更新頻率</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_fifteen_minutes\">每十五分鐘</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_one_hour\">每小時</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_six_hours\">每6小時</string>\n  <!--Frequency for requesting blank form updates from the server-->\n  <string name=\"every_24_hours\">每24小時</string>\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"automatic_download\">自動下載</string>\n  <string name=\"automatic_download_summary\">自動下載表單的更新版本</string>\n  <string name=\"hide_old_form_versions_setting_title\">隱藏舊版本表單</string>\n  <string name=\"hide_old_form_versions_setting_summary\">只有最新版本才會出現在「填寫空白表單」中</string>\n  <string name=\"form_submission_category\">表單提交</string>\n  <string name=\"autosend_selector_title\">自動傳送</string>\n  <string name=\"autosend\">自動傳送</string>\n  <string name=\"off\">關閉</string>\n  <string name=\"wifi_autosend\">僅Wi-Fi</string>\n  <string name=\"cellular_autosend\">僅行動網路</string>\n  <string name=\"wifi_cellular_autosend\">Wi-Fi或行動網路</string>\n  <string name=\"delete_after_send\">傳送後自動刪除</string>\n  <string name=\"delete_after_send_summary\">傳送到伺服器後刪除已完成的表單和媒體</string>\n  <string name=\"form_filling_category\">表單填寫</string>\n  <string name=\"constraint_behavior_title\">約束處理</string>\n  <string name=\"constraint_behavior\">約束檢查</string>\n  <string name=\"constraint_behavior_on_swipe\">向前滑動時驗證</string>\n  <string name=\"constraint_behavior_on_finalize\">推遲檢驗直到完成</string>\n  <string name=\"high_resolution_title\">高解析度影片</string>\n  <string name=\"high_resolution_summary\">啟用高解析度影片錄製</string>\n  <string name=\"image_size_title\">相片大小</string>\n  <string name=\"image_size_dialog_title\">相片長的最高像素值</string>\n  <string name=\"image_size_large\">大（3072px）</string>\n  <string name=\"image_size_medium\">中（2048px）</string>\n  <string name=\"image_size_small\">小（1024px）</string>\n  <string name=\"image_size_very_small\">非常小（640px）</string>\n  <string name=\"image_size_original\">相機的原始大小（默認）</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <string name=\"guidance_hint_title\">顯示指南</string>\n  <string name=\"guidance_no\">否</string>\n  <string name=\"guidance_yes\">是-始終顯示</string>\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <string name=\"guidance_yes_collapsed\">是-已折疊</string>\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <string name=\"external_app_recording\">使用外部應用程式進行音頻錄製</string>\n  <string name=\"form_import_category\">表單導入</string>\n  <string name=\"instance_sync\">導入時完成表單</string>\n  <string name=\"instance_sync_summary\">影響直接添加到實例文件夾的表單。</string>\n  <string name=\"analytics_preferences\">使用情況資料</string>\n  <string name=\"analytics\">收集匿名使用資料</string>\n  <string name=\"analytics_summary\">匿名使用資料幫助團隊確定修復和功能的優先級。</string>\n  <string name=\"form_metadata_title\">表單元資料</string>\n  <string name=\"form_metadata\">表單元資料</string>\n  <string name=\"form_metadata_summary\">這些值將添加到預先載入了使用者名稱、電子郵件和/或電話字段的表單中，以標識提交資料的人。</string>\n  <string name=\"user_defined\">使用者定義的</string>\n  <string name=\"phone_number\">電話號碼</string>\n  <string name=\"email\">電子郵件地址</string>\n  <string name=\"invalid_email_address\">電子郵件地址無效！</string>\n  <string name=\"device_defined\">設備已定義</string>\n  <string name=\"device_id\">設備ID</string>\n  <!--Displayed when a device property such as the phone number is not available-->\n  <string name=\"preference_not_available\">不可用</string>\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <string name=\"unlock_settings\">解鎖設置</string>\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <string name=\"settings_unlocked\">設置已解鎖</string>\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">輸入新密碼</string>\n  <string name=\"admin_password\">管理員密碼</string>\n  <string name=\"admin_password_changed\">管理員密碼已成功修改</string>\n  <string name=\"admin_password_disabled\">管理員密碼已禁用</string>\n  <string name=\"admin_password_incorrect\">對不起, 密碼不正確!</string>\n  <string name=\"enter_admin_password\">輸入管理員密碼</string>\n  <string name=\"change_admin_password\">更改管理員密碼</string>\n  <string name=\"show_password\">顯示密碼</string>\n  <string name=\"found_in_main\">不勾選可於主選單中隱藏該項目</string>\n  <string name=\"found_in_settings\">取消選擇可隱藏不受保護的設置項目</string>\n  <string name=\"form_entry\">取消選中可隱藏表單輸入時的項目</string>\n  <string name=\"form_end\">取消選擇可隱藏表單結束時的項目</string>\n  <string name=\"reset_project_settings_title\">重置</string>\n  <string name=\"reset_project_settings_subtext\">從設置、表單和資料中進行選擇</string>\n  <string name=\"delete_project\">刪除</string>\n  <string name=\"select_all\">全選</string>\n  <string name=\"clear_all\">清空</string>\n  <string name=\"reset_settings\">所有設置（內部設置、儲存的設置）</string>\n  <string name=\"reset_saved_forms\">儲存的表單（實例文件夾、實例資料庫）</string>\n  <string name=\"reset_blank_forms\">空白表單（表單文件夾、表單資料庫、元素集資料庫）</string>\n  <string name=\"reset_layers\">地圖圖層（圖層文件夾）</string>\n  <string name=\"reset_cache\">表單載入緩存（.cache文件夾）</string>\n  <string name=\"reset_settings_dialog_title\">選擇要重置的內容</string>\n  <string name=\"reset_settings_button_reset\">重置</string>\n  <string name=\"reset_in_progress\">正在重置…</string>\n  <string name=\"reset_app_state_result\">重置結果</string>\n  <string name=\"reset_app_state_warning\">所有選定的資料將被永久刪除。無法撤消。</string>\n  <string name=\"reset_settings_result\">所有設置 :: %s</string>\n  <string name=\"reset_saved_forms_result\">儲存的表單 :: %s</string>\n  <string name=\"reset_blank_forms_result\">空白表單 :: %s</string>\n  <string name=\"reset_layers_result\">地圖圖層 :: %s</string>\n  <string name=\"reset_cache_result\">表單載入緩存 :: %s</string>\n  <string name=\"main_menu_settings\">主選單設置</string>\n  <string name=\"main_menu_settings_summary\">顯示或隱藏按鈕</string>\n  <string name=\"user_settings\">使用者設置</string>\n  <string name=\"user_setting_summary\">顯示或隱藏設置</string>\n  <string name=\"form_entry_setting\">表單條目設置</string>\n  <string name=\"form_entry_settings_summary\">顯示或隱藏操作</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <string name=\"moving_backwards_title\">向後移動</string>\n  <string name=\"moving_backwards_disabled_title\">向後移動已禁用</string>\n  <string name=\"moving_backwards_disabled_message\">配置設備以防止使用者繞過此設置？\\n\\n更改如下：\\n\\u2022 禁用「編輯草稿」\\n\\u2022 禁用「另存為草稿」\\n\\u2022 禁用「轉到提示」\\n\\u2022 將約束處理設置為向前滑動時驗證</string>\n  <string name=\"yes\">是</string>\n  <string name=\"no\">否</string>\n  <string name=\"moving_backwards_enabled_title\">已啟用向後移動</string>\n  <string name=\"moving_backwards_enabled_message\">您可能希望檢查以下設置:\\n\\n\\u2022 編輯草稿\\n\\u2022 另存為草稿\\n\\u2022 前往提示\\n\\u2022 約束處理</string>\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <string name=\"save_mid\">儲存為草稿</string>\n  <string name=\"save_mid_summary\">退出表單時，頂部欄中的儲存圖標和「另存為草稿」按鈕</string>\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <string name=\"about_preferences\">關於</string>\n  <string name=\"tell_your_friends\">分享ODK Collect</string>\n  <string name=\"tell_your_friends_msg\">您的同事還在用紙本收集資料嗎？與他們分享 ODK Collect 應用程式。</string>\n  <string name=\"leave_a_review\">留下 Play Store 評論</string>\n  <string name=\"leave_a_review_msg\">你的評論（希望是正面的）提高了應用程式在應用商店中的知名度。</string>\n  <string name=\"odk_website\">造訪 ODK 網站</string>\n  <string name=\"odk_website_summary\">ODK 用於具有挑戰性的環境中收集社會資料。</string>\n  <string name=\"odk_forum\">加入論壇</string>\n  <string name=\"odk_forum_summary\">加入 ODK 論壇以獲得支持、請求功能、貢獻代碼/翻譯！</string>\n  <string name=\"all_open_source_licenses\">開源庫/許可證</string>\n  <string name=\"all_open_source_licenses_msg\">我們站在巨人的肩膀上！</string>\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <string name=\"reconfigure_with_qr_code_settings_title\">使用 QR code 重新配置</string>\n  <string name=\"reconfigure_with_qr_code_settings_subtext\">替換所有設置</string>\n  <string name=\"qr_code\">QR code 配置</string>\n  <string name=\"scan_qr_code_fragment_title\">掃瞄</string>\n  <string name=\"view_qr_code_fragment_title\">QR code</string>\n  <string name=\"import_qrcode_sd\">導入 QR code</string>\n  <string name=\"successfully_imported_settings\">成功導入設置</string>\n  <string name=\"encoding_max_limit\" tools:ignore=\"UnusedResources\">已達到最大字符數限制：無法為所有設置生成 QR code</string>\n  <string name=\"share\">分享</string>\n  <string name=\"turn_on_flashlight\">打開閃光燈</string>\n  <string name=\"turn_off_flashlight\">關閉閃光燈</string>\n  <string name=\"qrcode_with_both_passwords\">包含敏感資訊：<b>管理員</b> 和 <b>伺服器密碼</b></string>\n  <string name=\"qrcode_without_passwords\">QR code 不包含 <b>管理員</b> 或 <b>伺服器</b> 密碼。點擊以配置</string>\n  <string name=\"qrcode_with_admin_password\">包含敏感資訊：<b>管理員</b> 密碼</string>\n  <string name=\"qrcode_with_server_password\">包含敏感資訊：<b>伺服器</b> 密碼</string>\n  <string name=\"server_password\">伺服器密碼</string>\n  <string name=\"include_password_dialog\">代碼中包含的密碼</string>\n  <string name=\"generate\">生成</string>\n  <string name=\"invalid_qrcode\">QR code 不包含有效設置</string>\n  <string name=\"qr_code_not_found\">在所選圖像中找不到 QR code</string>\n  <string name=\"corrupt_imported_preferences_error\">當前設置已損壞。從項目管理設置中，重置設置或導入工作設置。</string>\n  <string name=\"settings_with_gd_protocol\">無法再建立 Google Drive/Sheets 項目</string>\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">確定</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">取消</string>\n  <string name=\"cancel_loading_form\">取消</string>\n  <string name=\"cancel_location\">取消</string>\n  <string name=\"do_not_change\">取消</string>\n  <string name=\"canceling\">正在取消</string>\n  <string name=\"notification_channel_name\">一般通知</string>\n  <string name=\"success\">成功</string>\n  <string name=\"error_occured\">錯誤</string>\n  <string name=\"please_wait\">請稍候。</string>\n  <string name=\"please_wait_long\">請稍候。這可能需要幾分鐘。</string>\n  <string name=\"fs_delete_media_path_if_file_error\">無法刪除 \\'%s\\'. 請手動刪除文件並重新下載表單。</string>\n  <string name=\"fs_create_media_folder_error\">無法建立媒體文件夾 \\'%s\\'.</string>\n  <string name=\"fs_file_copy_error\">不能複製 \\'%1$s\\' 到 \\'%2$s\\'，原因： %3$s</string>\n  <string name=\"select_odk_shortcut\">選擇 ODK 快捷方式</string>\n  <string name=\"background_audio_help\">此表單記錄設備麥克風的音頻。\\n\\n您可以使用音量指示器來確保麥克風足夠接近您需要錄製的聲音。\\n\\n若要停止錄製，請退出此表單。\\n\\n有關詳細資訊，請與要求您收集資料的人員聯繫。</string>\n  <string name=\"loading\">正在載入…</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <string name=\"projects\">項目</string>\n  <string name=\"settings\">設置</string>\n  <string name=\"add_project\">添加項目</string>\n  <string name=\"add\">添加</string>\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project\">重複項目</string>\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <string name=\"duplicate_project_details\">您已經有此連結設置的項目。要切換到現有項目還是添加新項目？</string>\n  <!--Button text for adding a duplicate project-->\n  <string name=\"add_duplicate_project\">添加重複項目</string>\n  <!--Buton text for switching to an existing project-->\n  <string name=\"switch_to_existing\">切換到現有項目</string>\n  <string name=\"project_name\">項目名稱</string>\n  <string name=\"project_icon\">項目圖示</string>\n  <string name=\"project_color\">項目顏色</string>\n  <!--Toast text shown when the current project is changed-->\n  <string name=\"switched_project\">已切換到 %s</string>\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <string name=\"using_project\">使用 %s</string>\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <string name=\"switch_to_project\">切換到 %s</string>\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <string name=\"or\">或</string>\n  <string name=\"hex_color\">十六進位顏色</string>\n  <string name=\"invalid_hex_code\">無效的十六進位代碼</string>\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <string name=\"tagline\">收集資料\\n任何位置</string>\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <string name=\"configure_with_qr_code\">使用 QR code 配置</string>\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <string name=\"configure_manually\">手動輸入項目詳細訊息</string>\n  <string name=\"project_config_tip\">在新增項目後，可以於「設置」對項目進行設定</string>\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <string name=\"dont_have_project\">還沒有項目？</string>\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <string name=\"try_demo\">嘗試示範項目</string>\n  <string name=\"scan_configuration_qr_code\">掃瞄配置 QR code</string>\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <string name=\"show_details\">顯示詳細訊息</string>\n  <string name=\"errors\">錯誤</string>\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <string name=\"wrong_project_selected_for_form\">要打開此表單，必須首先開啟 ODK Collect 並切換到包含它的項目。</string>\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <string name=\"app_not_configured\">尚未配置 ODK Collect 。嘗試開啟 ODK Collect 進行設定。\\n\\n如果您點選了快捷方式，您可能需要在配置 ODK Collect 後重新建立它。</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <string name=\"unrecognized_uri\">無法識別的URI: %s</string>\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <string name=\"bad_uri\">錯誤的URI: %s</string>\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <string name=\"instance_deleted_message\">填寫的表單已被刪除！</string>\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <string name=\"parent_form_not_present\">無法編輯此草稿，因為相應的空白表格不存在或已被刪除。\\n\\n表格ID: %1$s</string>\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <string name=\"survey_multiple_forms_error\">您下載了兩個具有相同表單ID和版本的不同表單。也許它們是在不同的時間或不同的伺服器上傳的相同表單。無論如何，您都應該刪除一個。</string>\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <string name=\"open_settings\">打開設置</string>\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <string name=\"entities_title\">實體清單</string>\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <string name=\"entity_browser_button\">查看實體列表</string>\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <string name=\"offline\">離線</string>\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <string name=\"unrecognized_entity_version\">此版本的 ODK Collect 不支援此表格。請升級Collect。如果您仍然遇到此問題，請將其報告給要求您收集資料的人員。\\n\\nForm Entities spec version: %s</string>\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <string name=\"crash_last_run\">應用程式上次運行時發生了崩潰！</string>\n  <string name=\"cant_start_app\">無法啟動應用程式！</string>\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <string name=\"google_drive_removed_message\">該項目之前已連接到 Google Drive 帳戶。Google Drive 支持已被刪除。您可以配置伺服器或將提交內容傳至電腦。\n\n</string>\n  <string name=\"learn_more_button_text\">瞭解更多</string>\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <string name=\"dev_tools\">開發人員工具</string>\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <string name=\"crash_app\">崩潰應用程式</string>\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <string name=\"crash_app_summary\">強制執行導致應用程式崩潰的未捕獲異常</string>\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <string name=\"permission_dialog_title\">關於權限</string>\n  <string name=\"permission_dialog_text\">您將被要求允許 ODK Collect 存取以下功能，如果您想使用它們，請選擇「允許」。</string>\n  <string name=\"notifications\">提醒事項</string>\n  <string name=\"required_for_notifications\"> 在下載、更新和傳送表單時需要顯示更新。</string>\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <string name=\"form_saved_as_draft\">您的表單已儲存為草稿。</string>\n  <string name=\"form_saved\">你的表格已被儲存。</string>\n  <string name=\"form_sending\">傳送表格中…</string>\n  <string name=\"edit_form\">編輯</string>\n  <string name=\"view_form\">檢視</string>\n  <string name=\"close_snackbar\">關閉通知</string>\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <plurals name=\"last_form_sent_seconds_ago\">\n    <item quantity=\"other\">最後傳送的表格：%d 秒前</item>\n  </plurals>\n  <plurals name=\"last_form_sent_minutes_ago\">\n    <item quantity=\"other\">最後傳送的表格：%d 分鐘前</item>\n  </plurals>\n  <plurals name=\"last_form_sent_hours_ago\">\n    <item quantity=\"other\">最後傳送的表格：%d 小時前</item>\n  </plurals>\n  <plurals name=\"last_form_sent_days_ago\">\n    <item quantity=\"other\">最後傳送的表格：%d 天前</item>\n  </plurals>\n  <plurals name=\"forms_ready_to_send\">\n    <item quantity=\"other\">%d 表格已準備傳送</item>\n  </plurals>\n  <string name=\"edit_finalized_form_warning\" tools:ignore=\"UnusedResources\">在後續版本中，已完成的表格將不再可編輯。將表單儲存為草稿以便稍後編輯。\\n\\n您可以通過點擊三個點 (⋮) 然後檢查錯誤來檢查草稿表單中的錯誤。</string>\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <string name=\"edit_finalized_form_deprecation_message\" tools:ignore=\"UnusedResources\">在後續版本中，已完成的表格將不再可編輯。</string>\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <string name=\"finalize_all_drafts\">完成所有草稿</string>\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <plurals name=\"bulk_finalize_confirmation\">\n    <item quantity=\"other\">您要完成 %d 份草稿?</item>\n  </plurals>\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <string name=\"bulk_finalize_explanation\">一旦您完成所有草稿，它們將處於「準備傳送」狀態，並且您將無法進行編輯。任何有錯誤的草稿都不會被完成。\\n\\n您將無法復原此操作。</string>\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <plurals name=\"bulk_finalize_success\">\n    <item quantity=\"other\">成功！%d 份草稿已完成。</item>\n  </plurals>\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <plurals name=\"bulk_finalize_failure\">\n    <item quantity=\"other\">%d 份草稿有錯誤，必須在確認完成前解決。</item>\n  </plurals>\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <string name=\"bulk_finalize_partial_success\">%d 份草稿已完成。%d 份草稿有錯誤，必須在確認完成前解決。</string>\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <string name=\"bulk_finalize_unsupported\">%d 份草稿已完成。剩下的草稿需要手動完成。</string>\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_errors\">錯誤</string>\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <string name=\"draft_no_errors\">無錯誤</string>\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <string name=\"uncheck_to_hide_from_drafts\">取消選擇可隱藏在草稿中的項目</string>\n  <!--Message displayed when the list of questions is empty-->\n  <string name=\"empty_list_of_questions\">沒有可顯示的內容</string>\n  <!--Message displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_title\">無空白表格</string>\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <string name=\"empty_list_of_blank_forms_subtitle\">下載表格以開始使用</string>\n  <!--Message displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_title\">無草稿</string>\n  <!--Subtext displayed when the list of drafts is empty-->\n  <string name=\"empty_list_of_drafts_subtitle\">當您儲存為草稿時，表格將顯示在此處</string>\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_title\">尚無準備傳送的表單</string>\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <string name=\"empty_list_of_finalized_forms_subtitle\">當你完成草稿時，它們將顯示在這裡</string>\n  <!--Message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_title\">沒有任何已傳送的表單</string>\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <string name=\"empty_list_of_sent_forms_subtitle\">當您傳送最終表單後，它們將在這裡顯示</string>\n  <!--Message displayed when the list of forms to download is empty-->\n  <string name=\"empty_list_of_forms_to_download_title\">無下載表單</string>\n  <string name=\"empty_list_of_forms_to_download_subtitle\">下載表格以開始使用</string>\n  <!--Message displayed when the list of forms to delete is empty-->\n  <string name=\"empty_list_of_forms_to_delete_title\">沒有要刪除的表單</string>\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <string name=\"empty_list_of_blank_forms_to_delete_subtitle\">下載空白表單後，它們將顯示在這裡</string>\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <string name=\"empty_list_of_saved_forms_to_delete_subtitle\">當您儲存表單後，它們將顯示在此處</string>\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <string name=\"savepoint_recovery_dialog_title\">恢復您的作業？</string>\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"savepoint_recovery_dialog_message\">\\'Collect 於 \\'EEE, MMM dd, yyyy HH:mm\\' 意外關閉並儲存了您的工作。\\n\\n您想恢復還是放棄它？\\'</string>\n  <!--Text for the button that accepts the recovery of the work-->\n  <string name=\"recover\">恢復</string>\n  <!--Text for the button that declines the recovery of the work-->\n  <string name=\"do_not_recover\">放棄</string>\n  <!--Text for the button that initiates getting user location-->\n  <string name=\"get_point\">取得定位點</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <string name=\"view_or_change_point\">檢視或查看定位點</string>\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <string name=\"view_point\">查看定位點</string>\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <string name=\"change_point\">變更定位點</string>\n  <!--Text for the button that displays a map for getting a new line-->\n  <string name=\"get_line\">取得線段</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <string name=\"view_or_change_line\">查看或變更線段</string>\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <string name=\"view_line\">檢視線段</string>\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <string name=\"get_polygon\">獲取多邊形</string>\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <string name=\"view_or_change_polygon\">檢視或變更多邊形</string>\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <string name=\"view_polygon\">檢視多邊形</string>\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/main/res/values-zu/strings.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" xmlns:oat=\"http://schema.getodk.org/odkAndroidTools\">\n  <!--Text for the topmost button on the main screen. It should be short and action-oriented.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Text for an action button on the main screen.-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"added_on_date_at_time\">\\'okwandisiwe\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saved_on_date_at_time\">\\'okugciniwe\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"finalized_on_date_at_time\">\\'okugciniwe\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sent_on_date_at_time\">\\'okuthunyelwe\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"sending_failed_on_date_at_time\">\\'okuthunyelwe akuphumelelanga\\' EEE, MMM dd, yyyy \\'at\\' HH:mm</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--A sort option for blank forms. When this option is selected, the form for which a filled instance was last filled will go first.-->\n  <!--Displayed in search boxes for form lists or question types that involve search-->\n  <!--Starting in Android 12, Android makes it possible to give precise or approximate location permissions to an application. ODK Collect only captures location if the precise location permission is granted.-->\n  <!--##############################################\n    # FILL BLANK FORM\n    ##############################################-->\n  <!--Accessibility description of the form map button (e.g. used by screen readers). This button is shown next to every form definition in Fill Blank Form. When tapped, a map of all filled forms for that form definition is shown.-->\n  <string name=\"finished_disk_scan\">Iqedile ukuskena. Onke amafomu afakiwe.</string>\n  <string name=\"loading_form\">Kufakwa ifomu</string>\n  <string name=\"survey_loading_reading_form_message\">Ufunda encazelweni</string>\n  <string name=\"survey_loading_reading_data_message\">ufunda ulwazi lwenhlovo…</string>\n  <string name=\"survey_loading_reading_csv_message\">ufunda amafayili e CSV…</string>\n  <string name=\"parse_error\">Uxolo, akukwazi ukuncozulula ifomu.</string>\n  <!--Displayed when loading a form if that form was previously filled but closed because of a crash or system failure-->\n  <string name=\"savepoint_used\">Izinguquko ezingagciniwe zitholakele from savepoint!</string>\n  <!--Displayed in the form filling menu when a form definition specifies multiple languages-->\n  <string name=\"change_language\">shintsha ulimi</string>\n  <!--Text for button displayed in the form filling menu to see the form's table of contents-->\n  <string name=\"view_hierarchy\">Iya kubangela</string>\n  <!--Text for button displayed in the form filling menu to save the form-->\n  <!--Text of button from the form's table of contents to jump to the beginning of the form-->\n  <string name=\"jump_to_beginning\">Yana ekuqaleni</string>\n  <!--Text of button from the form's table of contents to jump to the end of the form-->\n  <string name=\"jump_to_end\">Yana ekugcineni</string>\n  <!--Text of button from the form's table of contents to navigate out of a group or a repeat-->\n  <string name=\"jump_to_previous\">Iya phezulu</string>\n  <!--Text of button from the form's table of contents to edit a finalized form-->\n  <!--Displayed below group names in a form's table of contents-->\n  <!--Displayed below repeat names in a form's table of contents-->\n  <!--This sentence is used in the dialog for adding repeats and will be completed by a singular noun usually. Examples would be ‘Add “observation”?’ or ‘Add “Reading”?’. If the phrase doesn’t make sense in your language you can add a word like “new”, being careful of gender/plural.-->\n  <!--Text of button from the 'Add new repeat' dialog to confirm adding a repeat-->\n  <!--Text of button from the 'Add new repeat' dialog to cancel adding a repeat-->\n  <!--Title of overlay menu displayed when long pressing on a question in a form-->\n  <string name=\"edit_prompt\">Hlela ngokushesha</string>\n  <string name=\"clearanswer_confirm\">Susa impendulo yoku \\\"%s\\\"?</string>\n  <string name=\"clear_answer\">Susa Impendulo</string>\n  <string name=\"clear_answer_ask\">Susa lempendulo?</string>\n  <string name=\"clear_answer_no\">Khansela</string>\n  <string name=\"discard_answer\">Susa impendulo</string>\n  <string name=\"discard_group\">Susa iqembu</string>\n  <string name=\"delete_repeat\">Susa iqembu</string>\n  <string name=\"delete_repeat_ask\">Susa leliqembu?</string>\n  <string name=\"delete_repeat_confirm\">Susa iqembu \\\"%s\\\" nawowonke amaqenjana alo?</string>\n  <string name=\"delete_repeat_no\">Khansela</string>\n  <string name=\"invalid_answer_error\">Uxolo, lempendulo ayilungile</string>\n  <string name=\"required_answer_error\">Uxolo, lempendulo iyadingeka</string>\n  <!--Title of the end of form screen-->\n  <string name=\"save_enter_data_description\">Ususekupheleni kuka %s.</string>\n  <!--Text shown in an information box on the end of form screen when auto-send is off or the device is offline-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the device is online-->\n  <!--Text shown in an information box on the end of form screen when auto-send is off and the form allows edits after finalize/send-->\n  <!--Text shown in an information box on the end of form screen when auto-send is on and the form allows edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form does not allow edits after finalize/send-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after finalization-->\n  <!--Details shown in an information box on the end of form screen when the form allows edits after sending-->\n  <!--Button text for saving as draft-->\n  <!--Button text for finalizing a submission. Displayed instead of \"Send\" when auto-send is off or the device is offline-->\n  <!--Button text for sending a submission.-->\n  <string name=\"data_saved_error\">Uxolo ifomu alilondolozekanga</string>\n  <string name=\"data_saved_ok\">Ifomu lilondolozeke ngempumelelo</string>\n  <!--Used when exiting a form filling session in which a form was not properly loaded. &lt; and &gt; surrounds the text so it will appear as <no form loaded>-->\n  <string name=\"keep_changes\">Gcina ushintsho</string>\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <string name=\"saving_form\">Liyagcinwa ifomu</string>\n  <string name=\"survey_saving_validating_message\">Uqinisekisa izimpendulo…</string>\n  <string name=\"survey_saving_collecting_message\">Ukuqoqa ulwazi kwenziwe lula…</string>\n  <string name=\"survey_saving_saving_message\">ukulondoloza esiqhebhazaneni se SD…</string>\n  <string name=\"survey_saving_finalizing_message\">Ukuqedela esiqgebhazaneni se SD…</string>\n  <string name=\"survey_saving_encrypting_message\">Uqopha ulwazi…</string>\n  <!--Displayed when an attempt is made to start form filling, but the form is broken-->\n  <!--Text displayed when audio recording fails to start-->\n  <!--Text displayed when user tries to start recording while the mic is already in use-->\n  <!--Error message displayed when copying media from a temporary directory fails-->\n  <!--Text displayed in the audio bar when background audio recording is explicitly disabled from the options menu. Should be as short as possible. The inserted string is ⋮ which indicates the options menu.-->\n  <!--Error displayed when something bad happens during updating widgets in a field-list group-->\n  <!--##############################################\n    # FORM FEATURES\n    ##############################################-->\n  <!--Name of button displayed when a screen contains multiple questions populated by an external app: https://docs.getodk.org/launch-apps-from-collect/#launching-external-apps-to-populate-multiple-fields-->\n  <string name=\"launch_app\">Yethula</string>\n  <string name=\"activity_not_found\">Awukho umsebenzi otholakalayo ukusingatha: %s</string>\n  <string name=\"no_app\">Isicelo asiphumelelanga. Sicela ufake imibhalo ngesandla.</string>\n  <!--Text on button that is part of print questions. Tapping the button prints the value of the question.-->\n  <string name=\"capture_osm\">ukwethula i-OpenMapKit</string>\n  <string name=\"recapture_osm\">phinda i-OSM Tagging</string>\n  <string name=\"get_barcode\">Thola ibhakhodi</string>\n  <string name=\"replace_barcode\">Shintsha ibhakhodi</string>\n  <!--Prompt under view finder to describe where to place the camera to scan a barcode-->\n  <!--Label for button that rotates camera orientation when scanning barcodes-->\n  <string name=\"capture_image\">Thwebula isithombe</string>\n  <string name=\"choose_image\">Khetha isithombe</string>\n  <!--Title displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <!--Explanation message displayed in a dialog for saving a signature, drawing, or annotated image-->\n  <string name=\"save_and_close\">Londoloza bese uyavala</string>\n  <string name=\"sign_button\">Thola isithupha</string>\n  <string name=\"markup_image\">Isithombe sokukhombisa</string>\n  <string name=\"draw_image\">Isithombe somdwebo</string>\n  <string name=\"reset_image\">Hlela kabusha</string>\n  <string name=\"set_color\">Hlela ummbala</string>\n  <string name=\"capture_video\">Thwebula ividiyo</string>\n  <string name=\"choose_video\">Khetha ummboniso</string>\n  <string name=\"play_video\">Dlala Umbukiso</string>\n  <!--Text for button that deletes a file that is the answer to a question-->\n  <!--Text for button that starts recording sound-->\n  <string name=\"capture_audio\">Qopha umsindo</string>\n  <!--Text for button that lets the user choose a sound file on their device-->\n  <string name=\"choose_sound\">Khetha umsindo</string>\n  <!--Description text for button that that pauses recording sound-->\n  <!--Description text for button that that resumes recording sound-->\n  <!--Displayed in the form filling menu when a form has background audio recording enabled-->\n  <!--Displayed in the form filling menu when a form has background audio recording disabled-->\n  <!--Text asking the user to confirm whether they want to stop recording or not (which deletes the recording file)-->\n  <!--Button text for the dialog confirming whether the user wants to disable background recording-->\n  <!--Text warning that user after they're reenabled background audio recording that it won't start again until they re-open the form-->\n  <!--Title for dialog asking the user if they want to delete a file-->\n  <!--Dialog text warning the user they won't be able to recover a file once deleted-->\n  <!--Text for button that stops audio recording-->\n  <!--Text to let the user know a recording is in progress-->\n  <!--Title of Android Notification Channel for audio recording notifications-->\n  <!--Text for button that plays audio clip-->\n  <!--Text to let the user know a file is being saved-->\n  <!--Text in dialog that instructs the user to stop recording audio before leaving the form-->\n  <string name=\"error_attaching_binary_file\">Iphutha lenzekile ngenkathi kunafakwa : %s</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"select_answer\">Khetha impendulo</string>\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <!--Displayed in form questions for which the user needs to select a value-->\n  <string name=\"selected_title\">Okukhethiwe:</string>\n  <string name=\"trigger\">Kulungile. Ucelwa ukuba uqhubeke.</string>\n  <string name=\"ext_file_no_data_error\">Lomqulu awunalwazi</string>\n  <string name=\"ext_import_finalizing_message\">Kuqedelwa ulwazi obeluvele lukhona…</string>\n  <string name=\"ext_import_completed_message\">Ukufunda ulwazi kuqediwe</string>\n  <string name=\"ext_not_initialized_error\">ExternalDataManager wolwazi uqalisiwe</string>\n  <!--1st month in the Buddhist calendar-->\n  <!--2nd month in the Buddhist calendar-->\n  <!--3rd month in the Buddhist calendar-->\n  <!--4th month in the Buddhist calendar-->\n  <!--5th month in the Buddhist calendar-->\n  <!--6th month in the Buddhist calendar-->\n  <!--7th month in the Buddhist calendar-->\n  <!--8th month in the Buddhist calendar-->\n  <!--9th month in the Buddhist calendar-->\n  <!--10th month in the Buddhist calendar-->\n  <!--11th month in the Buddhist calendar-->\n  <!--12th month in the Buddhist calendar-->\n  <!--Displays dates in specialized calendars with the Gregorian dates in parentheses. This is translated so right-to-left languages can specify the correct order-->\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is invalid-->\n  <string name=\"file_invalid\">Ifayela: %s alilungile</string>\n  <!--Displayed when a form media file such as an image for a question label or an audio file for a choice is missing-->\n  <string name=\"file_missing\">Ifayela: %s iyantuleka</string>\n  <!--Heading for a dialog which prompts the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Label for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Explanation for prompt for the user to identify themselves when making changes to data. See https://docs.getodk.org/form-audit-log/#enumerator-identification.-->\n  <!--Action to close a full screen dialog-->\n  <!--Heading for a dialog which prompts the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Label for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Explanation for prompt for the user to describe why a change was made to data. See https://docs.getodk.org/form-audit-log/#change-tracking.-->\n  <!--Action to save the contents of a full screen dialog-->\n  <!--Button text to select a place from a list presented on a map-->\n  <!--##############################################\n    # GEO\n    ##############################################-->\n  <!--TODO(ping): Delete these openmap_* strings when we're ready to move on from OSMDroid.\n   These strings are only being used to preserve compatibility with pre-existing\n    tile caches, and are not shown in the UI.-->\n  <!--Title for the offline layer section of settings-->\n  <!--Label for the offline layer setting-->\n  <!--Title for the screen that allows the user to select a offline layer for maps-->\n  <!--Description of how to add offline layers to the project-->\n  <!--Text of button user can press to get help when setting up Offline Layers-->\n  <!--Text for button that lets the user select a offline layer on their device to add-->\n  <!--Text for button that lets the user delete a offline layer-->\n  <!--Text for title displayed in a dialog to inform that some/all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that all the selected files are not MBTiles and they can't be imported-->\n  <!--Text for message displayed in a dialog to inform that some of the selected files are not MBTiles and they can't be imported-->\n  <!--Text for title above list of layers-->\n  <!--Text for title and subtitle above options for how the layer can be access-->\n  <!--Text for option where layer should be available to all projects-->\n  <!--Text for option where layer should be available to the current project only-->\n  <!--Text for message displayed in a dialog to confirm that the layer should be deleted. The placeholder is the name of the layer as displayed in the list-->\n  <string name=\"provider_disabled_error\">Uxolo, indawo enikeziwe ayisebenzi</string>\n  <!--Action to go to Android settings. Displayed when location providers are disabled.-->\n  <!--Displayed in the form filling menu when a form has background location enabled-->\n  <!--Displayed in the form filling menu when a form has background location disabled-->\n  <!--Text of an option available when filling out a form. Tapping this option checks the form for constraint or required violations and navigates the user back to the first field with an error.-->\n  <!--%1$s is the time taken so far in hh:mm:ss-->\n  <!--%1$s is the accuracy required for a point to be automatically saved-->\n  <!--%1$s is the number of satellites being used to get location-->\n  <!--The accuracy of a location fix in meters-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in W 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in E 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in S 18°48'8\"-->\n  <!--%1$s represents degrees, %2$s represents minutes and %3$s represents seconds as in N 18°48'8\"-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for location recording interval in geotrace and geoshape questions-->\n  <!--Used to display choices for accuracy requirements in geotrace and geoshape questions-->\n  <!--Name of an action button-->\n  <!--Name for the location tracking notifications. Will be seen in Android settings.-->\n  <!--Title for notification that lets user know their location is currently being tracked-->\n  <!--Description for button that zooms to the user's current location on a map-->\n  <!--Description for button that zooms a map so that it shows all items-->\n  <!--Description for button that pauses automatic location recording-->\n  <!--Description for button that removes the last recorded point-->\n  <!--Message shown when an error with the trace/shape has been fixed-->\n  <!--Description for button that explains how to modify the map-->\n  <!--Title for instructions on how to modify map features-->\n  <!--Instruction in a list on how to move a point-->\n  <!--Instruction in a list on how to remove the most recently added point-->\n  <!--Instruction in a list on how to delete the current shape to start over-->\n  <!--Instruction in a list on how to add a new point-->\n  <!--Instruction in a list on how to tap to add a point-->\n  <!--Instruction in a list on how to delete the entire shape-->\n  <!--Button text to close the instructions dialog-->\n  <!--Button text to open the instructions dialog from the error snackbar-->\n  <!--##############################################\n    # SEND FINALIZED FORM\n    ##############################################-->\n  <string name=\"uploading_data\">Kuthunyelwa ifomu</string>\n  <string name=\"noselect_error\">Uxolo, alikho ifomu olikhethile</string>\n  <string name=\"no_connection\">Akukho ukuxhumana kwinethiwekhi</string>\n  <string name=\"sending_items\">Liyathunyelwa %1$s of %2$s ifomu.</string>\n  <string name=\"send_selected_data\">Thumela okukhethiwe</string>\n  <string name=\"server_requires_auth\">Iseva idinga isikhombosigxivizo</string>\n  <string name=\"no_forms_uploaded\">Siyaxolisa, akukho fomu alayishiwe .</string>\n  <string name=\"upload_results\">Layisha imiphumela</string>\n  <!--Name of option in Send Finalized Form menu to show only unsent forms (default) or show sent and unsent forms-->\n  <string name=\"change_view\">Shintsha indlela okubukakala ngayo</string>\n  <string name=\"show_sent_and_unsent_forms\">Bonisa amaFomu asethunyeliwe kanye namaFomu angakathunyelwa</string>\n  <string name=\"show_unsent_forms\">Bonisa amaFomu angakathunyelwa</string>\n  <!--##############################################\n    # GET BLANK FORM\n    ##############################################-->\n  <string name=\"refresh\">Vuselela</string>\n  <string name=\"download\">Thola okukhethiwe</string>\n  <string name=\"downloading_data\">Ingena kuseva</string>\n  <string name=\"fetching_file\">Ukuthola %1$s.\\n\\nifomu %2$s of %3$s ifomu.</string>\n  <string name=\"form_download_progress\">%1$s. Ukuthola amafayela emidya: %2$s of %3$s</string>\n  <!--Text for notification shown while syncing forms with the server-->\n  <!--Part of form download dialog. Clicking it displays additional information about a form that failed-->\n  <!--##############################################\n    # GET BLANK FORM - ERROR MESSAGES\n    ##############################################-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--Displayed in error dialog for form downloads. Followed by instructions to talk to person who asked to collect data-->\n  <!--##############################################\n    # DELETE SAVED FORM\n    ##############################################-->\n  <!--Title of a section in \"Delete Saved Forms\" for deleting filled/saved forms (as opposed to blank forms)-->\n  <string name=\"data\">Amafomu agciniwe</string>\n  <!--Title of a section in \"Delete Saved Forms\" for deleting blank forms (as opposed to filled/saved forms)-->\n  <string name=\"forms\">Amafomu angenalutho</string>\n  <string name=\"delete_confirm\">Ukususa %s ifomu?</string>\n  <string name=\"delete_file\">Susa okukhethiwe</string>\n  <string name=\"delete_no\">Ungasusi</string>\n  <string name=\"delete_yes\">Susa amafomu</string>\n  <string name=\"file_deleted_ok\">%s ifomu lisuswe ngempumelelo</string>\n  <!--##############################################\n    # UNPROTECTED SETTINGS\n    ##############################################-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section only available in beta or development builds.-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <!--Title of a settings section-->\n  <!--Subtext of a settings section-->\n  <string name=\"server_preferences\">ukuhlelwa kweSeva</string>\n  <!--Label for server type setting-->\n  <!--Title of dialog for configuring server URL-->\n  <string name=\"url_error\">Uxolo, alilungile URL</string>\n  <string name=\"username\">Igamamfihlo </string>\n  <string name=\"change_username\">igamamfihlo le ODK</string>\n  <string name=\"password\">Inombolomfihlo</string>\n  <string name=\"change_password\">Inombolomfihlo ye ODK</string>\n  <string name=\"font_size\">Usayizi wombhalo (Font)</string>\n  <string name=\"change_font_size\">Usayizi wombhalo (Font)</string>\n  <string name=\"font_size_extra_large\">Okukhulu kakhulu</string>\n  <string name=\"font_size_large\">Okukhulu </string>\n  <string name=\"font_size_medium\">Okuphakathi</string>\n  <string name=\"font_size_small\">Okuncane</string>\n  <string name=\"font_size_extra_small\">Okuncane kakhulu</string>\n  <string name=\"navigation\">Ukubhekabheka</string>\n  <string name=\"swipe_navigation\">Hambisa isandla unqumise/uvundle</string>\n  <string name=\"buttons_navigation\">Sebenzisa iphambili noma amabhathini asemuva</string>\n  <string name=\"swipe_buttons_navigation\">Sebenzisa ukuhambisa isandla noma amabhawoti</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <string name=\"form_forward\">Okulandelayo</string>\n  <!--Text for navigation button showed at the bottom of a form that's being filled. Try to use a short word common for navigation.-->\n  <!--Heading for the settings category about updating blank forms-->\n  <!--Name of the setting that defines how blank forms are updated from the server.-->\n  <!--One of the possible settings values for \"Blank form update mode\". Manual update mode means forms have to be explicitly downloaded from Get Blank Forms.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Previously downloaded\" update mode means forms that have been manually downloaded will be kept up to date.-->\n  <!--One of the possible settings values for \"Blank form update mode\". \"Exactly match server\" update mode means that new forms or form updates on the server will be downloaded and forms that the server doesn't know about will be removed.-->\n  <!--Name of the setting that defines how often blank forms are updated from the server.-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Frequency for requesting blank form updates from the server-->\n  <!--Name of setting that defines whether form updates should be automatically downloaded when in \"previously downloaded forms only\" download mode-->\n  <string name=\"delete_after_send\">Susa emva kokuthumela</string>\n  <string name=\"delete_after_send_summary\">Cima amafomu nezekuxhumana emuva kokuthumela ku seva</string>\n  <string name=\"high_resolution_summary\">Vumela ukuqobha ngesithwebuli sevidiyo sezinga eliphezulu</string>\n  <!--Name of setting for enabling guidance hints while filling forms. Guidance hints are meant for training or other specialized contexts.-->\n  <!--Guidance hints can be displayed behind an icon that must be tapped to reveal the hint.-->\n  <!--Title for setting to let the user switch between using Collect or another app (on the device) for recording audio-->\n  <!--Displayed when a device property such as the phone number is not available-->\n  <!--Label for an item displayed in the project settings menu to unlock settings-->\n  <!--Label for an item displayed in the project settings menu to indicate that settings are unlocked-->\n  <!--Summary on setting that's currently not available because the user has entered settings from form entry-->\n  <!--##############################################\n     # PROTECTED SETTINGS\n     ##############################################-->\n  <string name=\"enter_new_password\">Faka inombolomfihlo entsha</string>\n  <string name=\"admin_password_changed\">Inombolomfihlo yokusingathwa komsebenzi ishintshwe ngempumelelo</string>\n  <string name=\"admin_password_disabled\">Inombolomfihlo yokusingathwa komsebenzi ivinjiwe</string>\n  <string name=\"admin_password_incorrect\">Uxolo, inombolomfihlo akuyona</string>\n  <string name=\"enter_admin_password\">Faka inombolomfihlo yokusingathwa komsebenzi</string>\n  <string name=\"found_in_main\">Susa uphawu ukufihla ohlwini oluwumongo</string>\n  <string name=\"reset_project_settings_title\">Hlela kabusha</string>\n  <string name=\"reset_settings_button_reset\">Hlela kabusha</string>\n  <!--Name of a setting that determines whether users can navigate backwards in a form-->\n  <!--Protected setting. When enabled, users are allowed to save forms while filling them. When disabled, users are only allowed to save forms when they reach their end-->\n  <!--##############################################\n     # ABOUT\n     ##############################################-->\n  <!--##############################################\n     # SETTINGS IMPORT\n     ##############################################-->\n  <!--##############################################\n     # MISC\n     ##############################################-->\n  <string name=\"ok\">Kulungile</string>\n  <!--Used in many dialogs to dismiss the dialog without taking any action-->\n  <string name=\"cancel\">Khansela</string>\n  <string name=\"cancel_loading_form\">Khansela</string>\n  <string name=\"cancel_location\">Khansela</string>\n  <string name=\"do_not_change\">Khansela</string>\n  <string name=\"success\">Impumelelo</string>\n  <string name=\"error_occured\">Iphutha lenzekile</string>\n  <string name=\"please_wait\">Sicela ulinde imizuzu emibalwa.</string>\n  <string name=\"please_wait_long\">Uyacelwa ulinde. Lokhu kungathatha imizuzu embalwa.</string>\n  <string name=\"fs_create_media_folder_error\">Awenzeki umqulu wezokuxhumana \\'%s\\'.</string>\n  <!--##############################################\n     # Projects\n     ##############################################-->\n  <!--Title of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Detail text of the dialog shown when attempting to create a project with connection settings that match an existing project-->\n  <!--Button text for adding a duplicate project-->\n  <!--Buton text for switching to an existing project-->\n  <!--Toast text shown when the current project is changed-->\n  <!--Description for screen readers of which project is in use. This is part of the project selection dialog-->\n  <!--Description for screen readers of the action to switch to a different existing project. This is part of the project selection dialog-->\n  <!--Text for the dialog title displayed when a user attempts to delete the current project-->\n  <!--Text that appears above the list of counts of different kinds of forms shown before deleting a project-->\n  <!--Instructions telling the user they need to type a specific word to confirm deletion of the project-->\n  <!--The word that needs to be typed to confirm deletion of the form. Should be all lowercase to allow for case insensitive matching-->\n  <!--The list item for form definition count in the project delete confirmation dialog-->\n  <!--The list item for sent count in the project delete confirmation dialog-->\n  <!--The list item for unsent count in the project delete confirmation dialog-->\n  <!--The list item for draft count in the project delete confirmation dialog-->\n  <!--Text for the button that confirms deleting the project-->\n  <!--##############################################\n     # First launch and initial configuration\n     ##############################################-->\n  <!--Tag line displayed on the first launch screen. The text should be split into two lines with \\n. Please\n    select the wording and where to put the \\n for maximum impact in your language.-->\n  <!--Text for the button that allows for configuring a project by scanning a QR code. Should be short-->\n  <!--Text for the button that allows for manually entering project details. Should be short-->\n  <!--Immediately followed by \"Try Collect\" which is a link to create a demo project-->\n  <!--Preceded by \"Don't have a project yet?\" question-->\n  <!--##############################################\n    # Errors\n    ##############################################-->\n  <!--Error shown when the user tries to open a web page from the app, but they don't have a browser installed-->\n  <!--##############################################\n    # Form opening errors\n    ##############################################-->\n  <!--Error shown when a user tries to open a form that's in a different project-->\n  <!--Error shown when a user tries to open a form when there are no projects-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI mime type is unknown-->\n  <!--Displayed when form filling is attempted to be launched by intent but the intent URI is incorrect-->\n  <!--Error displayed when opened filled form has been deleted on disk-->\n  <!--Error shown when a user tries to edit a form but there is no corresponding blank form-->\n  <!--Error shown when a user tries to open a form but there is more than one form with the same form ID-->\n  <!--Error shown when a user tries to open a form when forms update is being applied in the background-->\n  <!--##############################################\n    # Permissions\n    ##############################################-->\n  <!--An entity is any person, place or thing that a form can be about. This title appears on an experimental settings screen to view entities managed in the current project.-->\n  <!--Text for button that leads to browser for the user to view and manage entity lists-->\n  <!--Label for entity that has been created offline and is not on the server yet-->\n  <!--Error message displayed when a user opens a form with an unrecognized entity version-->\n  <!--##############################################\n    # Crash handling\n    ##############################################-->\n  <!--##############################################\n    # Google Drive deprecation\n    ##############################################-->\n  <!--##############################################\n    # Min SDK deprecation banner\n    ##############################################-->\n  <!--First part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Second part of the message shown in the banner, informing the user that their device will soon be unsupported-->\n  <!--Button text that opens a forum post with more information about upcoming device support changes-->\n  <!--Button text to dismiss the banner-->\n  <!--##############################################\n    # Developer tools\n    ##############################################-->\n  <!--Section title in settings-->\n  <!--Option provided in the Developer tools settings to force an application crash-->\n  <!--An uncaught exception is an exception that is not handled by ODK Collect and so will force the application to close-->\n  <!--Option provided in the Developer tools settings to allow debugging filters during form entry-->\n  <!--Option provided in the Experimental settings to revert to the old ZXing based barcode scanner-->\n  <!--##############################################\n    # Form saved Snackbar\n    ##############################################-->\n  <!--##############################################\n    # Forms ready to send banner\n    ##############################################-->\n  <!--Message showed on main menu to inform users that the ability to edit finalized forms is going away-->\n  <!--Label for Drafts list option that attempts to finalize all the drafts the user is currently looking at. Finalizing a draft locks it so it can't be edited and makes it eligible for autosend-->\n  <!--Dialog title that asks if the user wants to finalize all drafts-->\n  <!--Explanation for how bulk finalize works shown in a dialog before the user confirms if they want to proceed or cancel-->\n  <!--Message shown after bulk finalization when finalizing all drafts has succeeded-->\n  <!--Message shown after bulk finalization when no drafts finalized successfully-->\n  <!--Message shown after bulk finalization when some drafts finalize successfully and others fail-->\n  <!--Message shown when some drafts finalize successfully but others need to be finalized manually-->\n  <!--Displayed above a draft that has constraints that aren't met or required questions that aren't answered or both. Should match the wording for the 'Check for errors' feature.-->\n  <!--Displayed above a draft that has no constraint violations and in which all required questions have been answered. Should match the wording for the 'Check for errors' feature.-->\n  <!--Title of section of features that the user can uncheck to hide from the Drafts screen-->\n  <!--Message displayed when the list of questions is empty-->\n  <!--Message displayed when the list of blank forms is empty-->\n  <!--Subtext displayed when the list of blank forms is empty-->\n  <!--Message displayed when the list of drafts is empty-->\n  <!--Subtext displayed when the list of drafts is empty-->\n  <!--Message displayed when the list of ready to send forms is empty-->\n  <!--Subtext displayed when the list of ready to send forms is empty-->\n  <!--Message displayed when the list of sent forms is empty-->\n  <!--Subtext of message displayed when the list of sent forms is empty-->\n  <!--Message displayed when the list of forms to download is empty-->\n  <!--Message displayed when the list of forms to delete is empty-->\n  <!--Subtext displayed when the list of blank forms to delete is empty-->\n  <!--Subtext displayed when the list of filled forms to delete is empty-->\n  <!--##############################################\n    # Savepoint recovery dialog\n    ##############################################-->\n  <!--Title displayed in a dialog before starting a form if there is a work to recover-->\n  <!--Message displayed in a dialog before starting a form if there is a work to recover. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the button that accepts the recovery of the work-->\n  <!--Text for the button that declines the recovery of the work-->\n  <!--Text for the button that initiates getting user location-->\n  <!--Text for the button that displays a map for viewing or changing an already existing location-->\n  <!--Text for the button that displays a map for viewing an already existing location-->\n  <!--Text for the button that initiates getting user location when one is already present-->\n  <!--Text for the button that displays a map for getting a new line-->\n  <!--Text for the button that displays a map for viewing or changing an already existing line-->\n  <!--Text for the button that displays a map for viewing an already existing line-->\n  <!--Text for the button that displays a map for getting a new polygon-->\n  <!--Text for the button that displays a map for viewing or changing an already existing polygon-->\n  <!--Text for the button that displays a map for viewing an already existing polygon-->\n  <!--Text to describe a field for setting settings JSON in a mobile device management platform (MDM)-->\n  <!--Text for the dialog that is displayed while the form is being prepared for editing-->\n  <!--Instance name with a suffix indicating the edit number, if this is an edited instance-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer draft version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer draft version exists-->\n  <!--Text for the dialog title displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the dialog message displayed when a user attempts to edit a finalized form and a newer finalized version exists. Please format date appropriately for your language: http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html-->\n  <!--Text for the dialog positive button displayed when a user attempts to edit a finalized form and a newer finalized version exists-->\n  <!--Text for the snackbar shown when editing of a finalized form begins-->\n</resources>\n"
  },
  {
    "path": "strings/src/test/java/org/odk/collect/strings/format/DateFormatsTest.kt",
    "content": "package org.odk.collect.strings.format\n\nimport android.app.Application\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.odk.collect.strings.R\nimport org.odk.collect.strings.localization.getLocalizedResources\nimport java.text.SimpleDateFormat\nimport java.util.Locale\n\n@RunWith(AndroidJUnit4::class)\nclass DateFormatsTest {\n\n    private val application = ApplicationProvider.getApplicationContext<Application>()\n\n    @Test\n    fun `all date formats parse in all available locales`() {\n        formats.forEach { format ->\n            application.resources.assets.locales.forEach { localeCode ->\n                val locale = Locale.forLanguageTag(localeCode)\n                val resources = application.getLocalizedResources(locale)\n                val string = resources.getString(format)\n                try {\n                    SimpleDateFormat(string, locale)\n                } catch (e: Throwable) {\n                    throw IllegalArgumentException(\"Illegal format translation: $string\", e)\n                }\n            }\n        }\n    }\n}\n\nprivate val formats = setOf(\n    R.string.save_explanation_with_last_saved,\n    R.string.discard_changes_warning,\n    R.string.added_on_date_at_time,\n    R.string.updated_on_date_at_time,\n    R.string.saved_on_date_at_time,\n    R.string.finalized_on_date_at_time,\n    R.string.sent_on_date_at_time,\n    R.string.sending_failed_on_date_at_time,\n    R.string.deleted_on_date_at_time,\n    R.string.modified_on_date_at_time,\n    R.string.savepoint_recovery_dialog_message,\n    R.string.newer_draft_edit_found_dialog_message,\n    R.string.newer_finalized_edit_found_dialog_message\n)\n"
  },
  {
    "path": "strings/src/test/java/org/odk/collect/strings/format/LengthFormatterTest.kt",
    "content": "package org.odk.collect.strings.format\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\n\nclass LengthFormatterTest {\n\n    @Test\n    fun formatsToMinutesAndSeconds() {\n        assertThat(formatLength(1000), equalTo(\"00:01\"))\n        assertThat(formatLength(52000), equalTo(\"00:52\"))\n        assertThat(formatLength(64000), equalTo(\"01:04\"))\n    }\n\n    @Test\n    fun whenHours_formatsToHoursMinutesAndSeconds() {\n        assertThat(formatLength(60 * 60 * 1000 + 64000), equalTo(\"01:01:04\"))\n    }\n\n    @Test\n    fun when100Hours_showsDurationInHoursMinutesAndSeconds() {\n        assertThat(formatLength(100 * 60 * 60 * 1000), equalTo(\"100:00:00\"))\n    }\n}\n"
  },
  {
    "path": "strings/src/test/resources/robolectric.properties",
    "content": "sdk=33"
  },
  {
    "path": "test-forms/.gitignore",
    "content": "/build"
  },
  {
    "path": "test-forms/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.javaLibrary)\n    alias(libs.plugins.kotlinLibrary)\n}\n\njava {\n    sourceCompatibility = JavaVersion.VERSION_17\n    targetCompatibility = JavaVersion.VERSION_17\n}\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/Empty First Repeat.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>Empty First Repeat</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"empty_first_repeat\" version=\"1\">\n                    <repeat jr:template=\"\">\n                        <text />\n                    </repeat>\n                    <repeat>\n                        <text />\n                    </repeat>\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/repeat/text\" relevant=\"position(..) != 1\" type=\"string\" />\n            <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\"\n                type=\"string\" />\n        </model>\n    </h:head>\n    <h:body>\n        <group ref=\"/data/repeat\">\n            <label>Repeat</label>\n            <repeat nodeset=\"/data/repeat\">\n                <input ref=\"/data/repeat/text\">\n                    <label>Question in repeat</label>\n                    <hint>Will never be relevant for first repeat instance</hint>\n                </input>\n            </repeat>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/RepeatGroupAndGroup.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\r\n  <h:head>\r\n    <h:title>RepeatGroupAndGroup</h:title>\r\n    <model>\r\n      <instance>\r\n        <data id=\"build_OneGroup-One_1518698685\">\r\n          <meta>\r\n            <instanceID/>\r\n          </meta>\r\n          <G1 jr:template=\"\">\r\n            <Text1/>\r\n          </G1>\r\n          <G3>\r\n            <Text3/>\r\n          </G3>\r\n        </data>\r\n      </instance>\r\n      <itext>\r\n        <translation lang=\"English\">\r\n          <text id=\"/data/G1:label\">\r\n            <value>G1</value>\r\n          </text>\r\n          <text id=\"/data/G1/Text1:label\">\r\n            <value>Text1</value>\r\n          </text>\r\n          <text id=\"/data/G3:label\">\r\n            <value>G3</value>\r\n          </text>\r\n          <text id=\"/data/G3/Text3:label\">\r\n            <value>Text3</value>\r\n          </text>\r\n        </translation>\r\n      </itext>\r\n      <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\r\n      <bind nodeset=\"/data/G1/Text1\" type=\"string\"/>\r\n      <bind nodeset=\"/data/G3/Text3\" type=\"string\"/>\r\n    </model>\r\n  </h:head>\r\n  <h:body>\r\n    <group>\r\n      <label ref=\"jr:itext('/data/G1:label')\"/>\r\n      <repeat nodeset=\"/data/G1\">\r\n        <input ref=\"/data/G1/Text1\">\r\n          <label ref=\"jr:itext('/data/G1/Text1:label')\"/>\r\n        </input>\r\n      </repeat>\r\n    </group>\r\n    <group>\r\n      <label ref=\"jr:itext('/data/G3:label')\"/>\r\n      <input ref=\"/data/G3/Text3\">\r\n        <label ref=\"jr:itext('/data/G3/Text3:label')\"/>\r\n      </input>\r\n    </group>\r\n  </h:body>\r\n</h:html>\r\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/RepeatTitles_1648.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\r\n  <h:head>\r\n    <h:title>Repeat titles 1648</h:title>\r\n    <model>\r\n      <instance>\r\n        <data id=\"build_1648_1511357420\">\r\n          <meta>\r\n            <instanceID/>\r\n          </meta>\r\n          <Text/>\r\n          <gr1>\r\n            <gr2>\r\n              <name/>\r\n              <age/>\r\n              <photo/>\r\n            </gr2>\r\n          </gr1>\r\n          <part1>\r\n            <part2>\r\n              <partText/>\r\n              <date/>\r\n              <multi/>\r\n            </part2>\r\n          </part1>\r\n        </data>\r\n      </instance>\r\n      <itext>\r\n        <translation lang=\"English\">\r\n          <text id=\"/data/Text:label\">\r\n            <value>Text</value>\r\n          </text>\r\n          <text id=\"/data/gr1:label\">\r\n            <value>gr1</value>\r\n          </text>\r\n          <text id=\"/data/gr1/gr2:label\">\r\n            <value>gr2</value>\r\n          </text>\r\n          <text id=\"/data/gr1/gr2/name:label\">\r\n            <value>Name</value>\r\n          </text>\r\n          <text id=\"/data/gr1/gr2/age:label\">\r\n            <value>Age</value>\r\n          </text>\r\n          <text id=\"/data/gr1/gr2/photo:label\">\r\n            <value>Photo</value>\r\n          </text>\r\n          <text id=\"/data/part1:label\">\r\n            <value>Part1</value>\r\n          </text>\r\n          <text id=\"/data/part1/part2:label\">\r\n            <value>Part2</value>\r\n          </text>\r\n          <text id=\"/data/part1/part2/partText:label\">\r\n            <value>Part text</value>\r\n          </text>\r\n          <text id=\"/data/part1/part2/date:label\">\r\n            <value>Date</value>\r\n          </text>\r\n          <text id=\"/data/part1/part2/multi:label\">\r\n            <value>Multi Select</value>\r\n          </text>\r\n          <text id=\"/data/part1/part2/multi:hint\">\r\n            <value></value>\r\n          </text>\r\n          <text id=\"/data/part1/part2/multi:option0\">\r\n            <value>a</value>\r\n          </text>\r\n          <text id=\"/data/part1/part2/multi:option1\">\r\n            <value>b</value>\r\n          </text>\r\n          <text id=\"/data/part1/part2/multi:option2\">\r\n            <value>c</value>\r\n          </text>\r\n        </translation>\r\n      </itext>\r\n      <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\r\n      <bind nodeset=\"/data/Text\" type=\"string\"/>\r\n      <bind nodeset=\"/data/gr1/gr2/name\" type=\"string\"/>\r\n      <bind nodeset=\"/data/gr1/gr2/age\" type=\"int\"/>\r\n      <bind nodeset=\"/data/gr1/gr2/photo\" type=\"binary\"/>\r\n      <bind nodeset=\"/data/part1/part2/partText\" type=\"string\"/>\r\n      <bind nodeset=\"/data/part1/part2/date\" type=\"date\"/>\r\n      <bind nodeset=\"/data/part1/part2/multi\" type=\"select\"/>\r\n    </model>\r\n  </h:head>\r\n  <h:body>\r\n    <input ref=\"/data/Text\">\r\n      <label ref=\"jr:itext('/data/Text:label')\"/>\r\n    </input>\r\n    <group>\r\n      <label ref=\"jr:itext('/data/gr1:label')\"/>\r\n      <repeat nodeset=\"/data/gr1\">\r\n      <group ref=\"/data/gr1/gr2\">\r\n        <label> Person: <output value=\"/data/gr1/gr2/age\"/></label>\r\n        <input ref=\"/data/gr1/gr2/name\">\r\n          <label ref=\"jr:itext('/data/gr1/gr2/name:label')\"/>\r\n        </input>\r\n        <input ref=\"/data/gr1/gr2/age\">\r\n          <label ref=\"jr:itext('/data/gr1/gr2/age:label')\"/>\r\n        </input>\r\n        <upload ref=\"/data/gr1/gr2/photo\" mediatype=\"image/*\">\r\n          <label ref=\"jr:itext('/data/gr1/gr2/photo:label')\"/>\r\n        </upload>\r\n      </group>\r\n     </repeat>\r\n    </group>\r\n    <group>\r\n      <label ref=\"jr:itext('/data/part1:label')\"/>\r\n      <repeat nodeset=\"/data/part1\">\r\n      <group ref=\"/data/part1/part2\">\r\n        <label> Xxx: <output value=\"/data/part1/part2/partText\"/></label>\r\n        <input ref=\"/data/part1/part2/partText\">\r\n          <label ref=\"jr:itext('/data/part1/part2/partText:label')\"/>\r\n        </input>\r\n        <input ref=\"/data/part1/part2/date\">\r\n          <label ref=\"jr:itext('/data/part1/part2/date:label')\"/>\r\n        </input>\r\n        <select ref=\"/data/part1/part2/multi\">\r\n          <label ref=\"jr:itext('/data/part1/part2/multi:label')\"/>\r\n          <hint ref=\"jr:itext('/data/part1/part2/multi:hint')\"/>\r\n          <item>\r\n            <label ref=\"jr:itext('/data/part1/part2/multi:option0')\"/>\r\n            <value>a</value>\r\n          </item>\r\n          <item>\r\n            <label ref=\"jr:itext('/data/part1/part2/multi:option1')\"/>\r\n            <value>b</value>\r\n          </item>\r\n          <item>\r\n            <label ref=\"jr:itext('/data/part1/part2/multi:option2')\"/>\r\n            <value>c</value>\r\n          </item>\r\n        </select>\r\n      </group>\r\n     </repeat>\r\n    </group>\r\n  </h:body>\r\n</h:html>\r\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/TestRepeat.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head>\n    <h:title>TestRepeat</h:title>\n    <model>\n      <itext>\n        <translation default=\"true()\" lang=\"language\">\n          <text id=\"/TestRepeat/q2:label\">\n            <value>Repeat # 2</value>\n          </text>\n          <text id=\"/TestRepeat/q0:label\">\n            <value>This form behaves different if navigation is set as horizontal wipes (OK) vs. buttons (infinite cycle). Please choose DO NOT ADD both &quot;Repeat # 1&quot; group and &quot;Repeat # 1&quot;</value>\n          </text>\n          <text id=\"/TestRepeat/q2/q2.1:label\">\n            <value>Question of Repeat # 2</value>\n          </text>\n          <text id=\"/TestRepeat/q1:label\">\n            <value>Repeat # 1</value>\n          </text>\n          <text id=\"/TestRepeat/q1/q1.1:label\">\n            <value>Question of Repeat # 1</value>\n          </text>\n          <text id=\"/TestRepeat/q3:label\">\n            <value>If it was set horizontal wipes should be displayed once, if not, it would be displayed indefinitely</value>\n          </text>\n        </translation>\n      </itext>\n      <instance>\n        <TestRepeat id=\"TestRepeat\">\n          <q0/>\n          <q1 jr:template=\"\">\n            <q1.1/>\n          </q1>\n          <q2 jr:template=\"\">\n            <q2.1/>\n          </q2>\n          <q3/>\n          <meta>\n            <instanceID/>\n          </meta>\n        </TestRepeat>\n      </instance>\n      <bind nodeset=\"/TestRepeat/q0\" readonly=\"true()\" required=\"false()\" type=\"string\"/>\n      <bind nodeset=\"/TestRepeat/q1\" readonly=\"false()\" required=\"false()\"/>\n      <bind nodeset=\"/TestRepeat/q1/q1.1\" readonly=\"false()\" required=\"false()\" type=\"string\"/>\n      <bind nodeset=\"/TestRepeat/q2\" readonly=\"false()\" required=\"false()\"/>\n      <bind nodeset=\"/TestRepeat/q2/q2.1\" readonly=\"false()\" required=\"false()\" type=\"string\"/>\n      <bind nodeset=\"/TestRepeat/q3\" readonly=\"true()\" required=\"false()\" type=\"string\"/>\n      <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/TestRepeat/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n    </model>\n  </h:head>\n  <h:body>\n    <input ref=\"/TestRepeat/q0\">\n      <label ref=\"jr:itext('/TestRepeat/q0:label')\"/>\n    </input>\n    <group ref=\"/TestRepeat/q1\">\n      <label ref=\"jr:itext('/TestRepeat/q1:label')\"/>\n      <repeat nodeset=\"/TestRepeat/q1\">\n        <input ref=\"/TestRepeat/q1/q1.1\">\n          <label ref=\"jr:itext('/TestRepeat/q1/q1.1:label')\"/>\n        </input>\n      </repeat>\n    </group>\n    <group ref=\"/TestRepeat/q2\">\n      <label ref=\"jr:itext('/TestRepeat/q2:label')\"/>\n      <repeat nodeset=\"/TestRepeat/q2\">\n        <input ref=\"/TestRepeat/q2/q2.1\">\n          <label ref=\"jr:itext('/TestRepeat/q2/q2.1:label')\"/>\n        </input>\n      </repeat>\n    </group>\n    <input ref=\"/TestRepeat/q3\">\n      <label ref=\"jr:itext('/TestRepeat/q3:label')\"/>\n    </input>\n  </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/all-widgets.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head><!-- ODK Aggregate upload time: 2019-12-11T17:25:50.080+0000 on https://opendatakit.appspot.com -->\n    <h:title>All widgets</h:title>\n    <model>\n      <itext>\n        <translation default=\"true()\" lang=\"default\">\n          <text id=\"/data/select_one_widgets/grid_widget/a:label\">\n            <value>A</value>\n            <value form=\"image\">jr://images/a.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget/b:label\">\n            <value>B</value>\n            <value form=\"image\">jr://images/b.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget/c:label\">\n            <value>C</value>\n            <value form=\"image\">jr://images/c.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget/d:label\">\n            <value>D</value>\n            <value form=\"image\">jr://images/d.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact/a:label\">\n            <value>A</value>\n            <value form=\"image\">jr://images/a.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact/b:label\">\n            <value>B</value>\n            <value form=\"image\">jr://images/b.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact/c:label\">\n            <value>C</value>\n            <value form=\"image\">jr://images/c.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact/d:label\">\n            <value>D</value>\n            <value form=\"image\">jr://images/d.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact2/a:label\">\n            <value>A</value>\n            <value form=\"image\">jr://images/a.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact2/b:label\">\n            <value>B</value>\n            <value form=\"image\">jr://images/b.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact2/c:label\">\n            <value>C</value>\n            <value form=\"image\">jr://images/c.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact2/d:label\">\n            <value>D</value>\n            <value form=\"image\">jr://images/d.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact/a:label\">\n            <value>A</value>\n            <value form=\"image\">jr://images/a.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact/b:label\">\n            <value>B</value>\n            <value form=\"image\">jr://images/b.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact/c:label\">\n            <value>C</value>\n            <value form=\"image\">jr://images/c.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact/d:label\">\n            <value>D</value>\n            <value form=\"image\">jr://images/d.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact2/a:label\">\n            <value>A</value>\n            <value form=\"image\">jr://images/a.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact2/b:label\">\n            <value>B</value>\n            <value form=\"image\">jr://images/b.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact2/c:label\">\n            <value>C</value>\n            <value form=\"image\">jr://images/c.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact2/d:label\">\n            <value>D</value>\n            <value form=\"image\">jr://images/d.jpg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/select_one_image_map:label\">\n            <value>Image select one widget</value>\n            <value form=\"image\">jr://images/body.svg</value>\n          </text>\n          <text id=\"/data/select_one_widgets/likert_widget/strongly_disagree:label\">\n            <value>Strongly Disagree</value>\n            <value form=\"image\">jr://images/angry.png</value>\n          </text>\n          <text id=\"/data/select_one_widgets/likert_widget/disagree:label\">\n            <value>Disagree</value>\n            <value form=\"image\">jr://images/sad.png</value>\n          </text>\n          <text id=\"/data/select_one_widgets/likert_widget/neutral:label\">\n            <value>Neutral</value>\n            <value form=\"image\">jr://images/neutral.png</value>\n          </text>\n          <text id=\"/data/select_one_widgets/likert_widget/agree:label\">\n            <value>Agree</value>\n            <value form=\"image\">jr://images/smile.png</value>\n          </text>\n          <text id=\"/data/select_one_widgets/likert_widget/strongly_agree:label\">\n            <value>Strongly Agree</value>\n            <value form=\"image\">jr://images/famous.png</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact/a:label\">\n            <value>A</value>\n            <value form=\"image\">jr://images/a.jpg</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact/b:label\">\n            <value>B</value>\n            <value form=\"image\">jr://images/b.jpg</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact/c:label\">\n            <value>C</value>\n            <value form=\"image\">jr://images/c.jpg</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact/d:label\">\n            <value>D</value>\n            <value form=\"image\">jr://images/d.jpg</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact2/a:label\">\n            <value>A</value>\n            <value form=\"image\">jr://images/a.jpg</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact2/b:label\">\n            <value>B</value>\n            <value form=\"image\">jr://images/b.jpg</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact2/c:label\">\n            <value>C</value>\n            <value form=\"image\">jr://images/c.jpg</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact2/d:label\">\n            <value>D</value>\n            <value form=\"image\">jr://images/d.jpg</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/select_multi_image_map:label\">\n            <value>Image select multiple widget</value>\n            <value form=\"image\">jr://images/body.svg</value>\n          </text>\n        </translation>\n        <translation lang=\"French (fr)\">\n          <text id=\"/data/select_one_widgets/select_one_image_map:label\">\n            <value form=\"image\">jr://images/body.svg</value>\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/select_multi_image_map:label\">\n            <value form=\"image\">jr://images/body.svg</value>\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget/a:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget/b:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget/c:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget/d:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact/a:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact/b:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact/c:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact/d:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact2/a:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact2/b:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact2/c:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_compact2/d:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact/a:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact/b:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact/c:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact/d:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact2/a:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact2/b:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact2/c:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/grid_widget_quickcompact2/d:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/likert_widget/strongly_disagree:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/likert_widget/disagree:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/likert_widget/neutral:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/likert_widget/agree:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_one_widgets/likert_widget/strongly_agree:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact/a:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact/b:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact/c:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact/d:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact2/a:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact2/b:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact2/c:label\">\n            <value>-</value>\n          </text>\n          <text id=\"/data/select_multi_widgets/grid_multi_widget_compact2/d:label\">\n            <value>-</value>\n          </text>\n        </translation>\n      </itext>\n      <instance>\n        <data id=\"all-widgets\" version=\"2019121101\">\n          <intro/>\n          <text_widgets>\n            <string_widget/>\n            <string_number_widget/>\n            <url_widget>http://opendatakit.org/</url_widget>\n            <ex_string_widget/>\n            <ex_printer_widget/>\n          </text_widgets>\n          <number_widgets>\n            <integer_widget/>\n            <integer_thousands_sep_widget/>\n            <ex_integer_widget/>\n            <decimal_widget/>\n            <ex_decimal_widget/>\n            <bearing_widget/>\n          </number_widgets>\n          <range_widgets>\n            <range_integer_widget/>\n            <range_decimal_widget/>\n            <range_integer_widget_vertical/>\n            <range_integer_widget_picker/>\n            <range_integer_widget_rating/>\n          </range_widgets>\n          <image_widgets>\n            <image_widget/>\n            <image_widget_no_choose/>\n            <selfie_image_widget/>\n            <draw_image_widget/>\n            <annotate_image_widget/>\n            <signature_widget/>\n          </image_widgets>\n          <media_widgets>\n            <barcode_widget/>\n            <audio_widget/>\n            <video_widget/>\n            <file_widget/>\n          </media_widgets>\n          <date_time_widgets>\n            <date_widget/>\n            <date_widget_nocalendar/>\n            <date_widget_month_year/>\n            <date_widget_year/>\n            <time_widget/>\n            <date_time_widget/>\n            <date_time_widget_nocalendar/>\n            <ethiopian_date_widget/>\n            <coptic_date_widget/>\n            <islamic_date_widget/>\n            <bikram_sambat_date_widget/>\n            <myanmar_date_widget/>\n            <persian_date_widget/>\n          </date_time_widgets>\n          <geopoint_widgets>\n            <geopoint_widget/>\n            <geopoint_widget_placementmap/>\n            <geopoint_widget_maps/>\n            <geotrace_widget/>\n            <geoshape_widget/>\n            <osm_building/>\n          </geopoint_widgets>\n          <select_one_widgets>\n            <select_one_widget/>\n            <spinner_widget/>\n            <select_one_autoadvance_widget/>\n            <select_one_search_widget/>\n            <select_one_autocomplete_widget/>\n            <grid_widget/>\n            <grid_widget_compact/>\n            <grid_widget_compact2/>\n            <grid_widget_quickcompact/>\n            <grid_widget_quickcompact2/>\n            <select_one_image_map/>\n            <likert_widget/>\n            <select_one_from_map_widget />\n          </select_one_widgets>\n          <select_multi_widgets>\n            <select_multi_widget/>\n            <select_multi_widget_autocomplete/>\n            <grid_multi_widget_compact/>\n            <grid_multi_widget_compact2/>\n            <spinner_multi_widget/>\n            <select_multi_image_map/>\n          </select_multi_widgets>\n          <table_list_test>\n            <table_list_test_label/>\n            <table_list_test_label_2/>\n            <table_list_1/>\n            <table_list_2/>\n            <list_widget/>\n            <list_multi_widget/>\n          </table_list_test>\n          <rank_widget/>\n          <my_trigger/>\n          <meta>\n            <instanceID/>\n          </meta>\n        </data>\n      </instance>\n      <instance id=\"places\">\n        <root>\n          <item>\n            <geometry>46.5841618 7.0801379 0 0</geometry>\n            <label>A</label>\n            <name>a</name>\n          </item>\n          <item>\n            <geometry>35.880508260751576 76.51505715762721 0 0</geometry>\n            <label>B</label>\n            <name>b</name>\n          </item>\n          <item>\n            <geometry>41.3737384 2.1482904 0 0; 59.8937806 10.6450356 0 0</geometry>\n            <label>C</label>\n            <name>c</name>\n          </item>\n          <item>\n            <geometry>53.40903732686215 -2.991905620523312 0 0; 51.505094241512666 -0.1321803079139272 0 0; 53.8052722823615 -1.5363611594108022 0 0; 53.40903732686215 -2.991905620523312 0 0</geometry>\n            <label>D</label>\n            <name>d</name>\n          </item>\n        </root>\n      </instance>\n      <bind nodeset=\"/data/intro\" readonly=\"true()\" type=\"string\"/>\n      <bind nodeset=\"/data/text_widgets/string_widget\" type=\"string\"/>\n      <bind nodeset=\"/data/text_widgets/string_number_widget\" type=\"string\"/>\n      <bind nodeset=\"/data/text_widgets/url_widget\" type=\"string\"/>\n      <bind nodeset=\"/data/text_widgets/ex_string_widget\" type=\"string\"/>\n      <bind calculate=\"concat('123456789','&lt;br&gt;','QRCODE','&lt;br&gt;','Text')\" nodeset=\"/data/text_widgets/ex_printer_widget\" type=\"string\"/>\n      <bind nodeset=\"/data/number_widgets/integer_widget\" type=\"int\"/>\n      <bind nodeset=\"/data/number_widgets/integer_thousands_sep_widget\" type=\"int\"/>\n      <bind nodeset=\"/data/number_widgets/ex_integer_widget\" type=\"int\"/>\n      <bind nodeset=\"/data/number_widgets/decimal_widget\" type=\"decimal\"/>\n      <bind nodeset=\"/data/number_widgets/ex_decimal_widget\" type=\"decimal\"/>\n      <bind nodeset=\"/data/number_widgets/bearing_widget\" type=\"decimal\"/>\n      <bind nodeset=\"/data/range_widgets/range_integer_widget\" type=\"int\"/>\n      <bind nodeset=\"/data/range_widgets/range_decimal_widget\" type=\"decimal\"/>\n      <bind nodeset=\"/data/range_widgets/range_integer_widget_vertical\" type=\"int\"/>\n      <bind nodeset=\"/data/range_widgets/range_integer_widget_picker\" type=\"int\"/>\n      <bind nodeset=\"/data/range_widgets/range_integer_widget_rating\" type=\"int\"/>\n      <bind nodeset=\"/data/image_widgets/image_widget\" type=\"binary\"/>\n      <bind nodeset=\"/data/image_widgets/image_widget_no_choose\" type=\"binary\"/>\n      <bind nodeset=\"/data/image_widgets/selfie_image_widget\" type=\"binary\"/>\n      <bind nodeset=\"/data/image_widgets/draw_image_widget\" type=\"binary\"/>\n      <bind nodeset=\"/data/image_widgets/annotate_image_widget\" type=\"binary\"/>\n      <bind nodeset=\"/data/image_widgets/signature_widget\" type=\"binary\"/>\n      <bind nodeset=\"/data/media_widgets/barcode_widget\" type=\"barcode\"/>\n      <bind nodeset=\"/data/media_widgets/audio_widget\" type=\"binary\"/>\n      <bind nodeset=\"/data/media_widgets/video_widget\" type=\"binary\"/>\n      <bind nodeset=\"/data/media_widgets/file_widget\" type=\"binary\"/>\n      <bind nodeset=\"/data/date_time_widgets/date_widget\" type=\"date\"/>\n      <bind nodeset=\"/data/date_time_widgets/date_widget_nocalendar\" type=\"date\"/>\n      <bind nodeset=\"/data/date_time_widgets/date_widget_month_year\" type=\"date\"/>\n      <bind nodeset=\"/data/date_time_widgets/date_widget_year\" type=\"date\"/>\n      <bind nodeset=\"/data/date_time_widgets/time_widget\" type=\"time\"/>\n      <bind nodeset=\"/data/date_time_widgets/date_time_widget\" type=\"dateTime\"/>\n      <bind nodeset=\"/data/date_time_widgets/date_time_widget_nocalendar\" type=\"dateTime\"/>\n      <bind nodeset=\"/data/date_time_widgets/ethiopian_date_widget\" type=\"date\"/>\n      <bind nodeset=\"/data/date_time_widgets/coptic_date_widget\" type=\"date\"/>\n      <bind nodeset=\"/data/date_time_widgets/islamic_date_widget\" type=\"date\"/>\n      <bind nodeset=\"/data/date_time_widgets/bikram_sambat_date_widget\" type=\"date\"/>\n      <bind nodeset=\"/data/date_time_widgets/myanmar_date_widget\" type=\"date\"/>\n      <bind nodeset=\"/data/date_time_widgets/persian_date_widget\" type=\"date\"/>\n      <bind nodeset=\"/data/geopoint_widgets/geopoint_widget\" type=\"geopoint\"/>\n      <bind nodeset=\"/data/geopoint_widgets/geopoint_widget_placementmap\" type=\"geopoint\"/>\n      <bind nodeset=\"/data/geopoint_widgets/geopoint_widget_maps\" type=\"geopoint\"/>\n      <bind nodeset=\"/data/geopoint_widgets/geotrace_widget\" type=\"geotrace\"/>\n      <bind nodeset=\"/data/geopoint_widgets/geoshape_widget\" type=\"geoshape\"/>\n      <bind nodeset=\"/data/geopoint_widgets/osm_building\" type=\"binary\"/>\n      <bind nodeset=\"/data/select_one_widgets/select_one_widget\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/spinner_widget\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/select_one_autoadvance_widget\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/select_one_search_widget\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/select_one_autocomplete_widget\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/grid_widget\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/grid_widget_compact\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/grid_widget_compact2\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/grid_widget_quickcompact\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/grid_widget_quickcompact2\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/select_one_image_map\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/likert_widget\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_one_widgets/select_one_from_map_widget\" type=\"select1\"/>\n      <bind nodeset=\"/data/select_multi_widgets/select_multi_widget\" type=\"select\"/>\n      <bind nodeset=\"/data/select_multi_widgets/select_multi_widget_autocomplete\" type=\"select\"/>\n      <bind nodeset=\"/data/select_multi_widgets/grid_multi_widget_compact\" type=\"select\"/>\n      <bind nodeset=\"/data/select_multi_widgets/grid_multi_widget_compact2\" type=\"select\"/>\n      <bind nodeset=\"/data/select_multi_widgets/spinner_multi_widget\" type=\"select\"/>\n      <bind nodeset=\"/data/select_multi_widgets/select_multi_image_map\" type=\"select\"/>\n      <bind nodeset=\"/data/table_list_test/table_list_test_label\" type=\"select1\"/>\n      <bind nodeset=\"/data/table_list_test/table_list_test_label_2\" type=\"select\"/>\n      <bind nodeset=\"/data/table_list_test/table_list_1\" type=\"select1\"/>\n      <bind nodeset=\"/data/table_list_test/table_list_2\" type=\"select\"/>\n      <bind nodeset=\"/data/table_list_test/list_widget\" type=\"select1\"/>\n      <bind nodeset=\"/data/table_list_test/list_multi_widget\" type=\"select\"/>\n      <bind nodeset=\"/data/rank_widget\" type=\"odk:rank\"/>\n      <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n    </model>\n  </h:head>\n  <h:body>\n    <input ref=\"/data/intro\">\n      <label>Welcome to ODK Collect! This form showcases the different available question types (&lt;em&gt;widgets&lt;/em&gt;).</label>\n      <hint>This is a sample for introductory and debugging purposes. It is updated periodically so check \"Get Blank Form\" to get the latest!\n\n        Version 2019120201 adds:\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Likert widget\n\n        Version 2019061302 adds:\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Myanmar calendar\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Persian calendar\n\n        Version 2019020201 adds:\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Appearance rating for range type\n\n        Version 2018102901 adds:\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Bikram Sambat calendar\n\n        Version 2018081601 adds:\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Rank widget\n\n        Version 2018051401 adds:\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- File widget\n\n        Version 2018032801 adds:\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Appearance new-front (selfie) for video type\n\n        Version 2018022001 adds:\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Appearance image-map for select\\_one and select\\_multiple types\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Appearance new for image, audio, video types\n\n        Version 2017121301 added:\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Appearance thousands-sep for numeric types\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Range questions\n        &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Ethiopian, coptic and islamic calendars</hint>\n    </input>\n    <group ref=\"/data/text_widgets\">\n      <label>Text widgets</label>\n      <input ref=\"/data/text_widgets/string_widget\">\n        <label>String widget</label>\n      </input>\n      <input appearance=\"numbers\" ref=\"/data/text_widgets/string_number_widget\">\n        <label>String number widget</label>\n        <hint>text type with numbers appearance</hint>\n      </input>\n      <input appearance=\"url\" ref=\"/data/text_widgets/url_widget\">\n        <label>URL widget</label>\n        <hint>text type with url appearance and default value of http://opendatakit.org/</hint>\n      </input>\n      <input appearance=\"ex:change.uw.android.BREATHCOUNT\" ref=\"/data/text_widgets/ex_string_widget\">\n        <label>Ex string widget</label>\n        <hint>text type with ex:change.uw.android.BREATHCOUNT appearance (can use other external apps)</hint>\n      </input>\n      <input appearance=\"printer:org.opendatakit.sensors.ZebraPrinter\" ref=\"/data/text_widgets/ex_printer_widget\">\n        <label>Ex printer widget</label>\n        <hint>text type with printer:org.opendatakit.sensors.ZebraPrinter</hint>\n      </input>\n    </group>\n    <group ref=\"/data/number_widgets\">\n      <label>Numerical widgets</label>\n      <input ref=\"/data/number_widgets/integer_widget\">\n        <label>Integer widget</label>\n        <hint>integer type with no appearance</hint>\n      </input>\n      <input appearance=\"thousands-sep\" ref=\"/data/number_widgets/integer_thousands_sep_widget\">\n        <label>Integer widget with thousands separators</label>\n        <hint>integer type with thousands-sep appearance. This appearance can also be applied to decimal and string numbers widgets</hint>\n      </input>\n      <input appearance=\"ex:change.uw.android.BREATHCOUNT\" ref=\"/data/number_widgets/ex_integer_widget\">\n        <label>Ex integer widget</label>\n        <hint>integer type with ex:change.uw.android.BREATHCOUNT appearance (can use other external apps)</hint>\n      </input>\n      <input ref=\"/data/number_widgets/decimal_widget\">\n        <label>Decimal widget</label>\n        <hint>decimal type with no appearance</hint>\n      </input>\n      <input appearance=\"ex:change.uw.android.BREATHCOUNT\" ref=\"/data/number_widgets/ex_decimal_widget\">\n        <label>Ex decimal widget</label>\n        <hint>decimal type with ex:change.uw.android.BREATHCOUNT appearance (can use other external apps)</hint>\n      </input>\n      <input appearance=\"bearing\" ref=\"/data/number_widgets/bearing_widget\">\n        <label>Bearing widget</label>\n        <hint>decimal type with bearing appearance</hint>\n      </input>\n    </group>\n    <group ref=\"/data/range_widgets\">\n      <label>Range widgets</label>\n      <range end=\"10\" ref=\"/data/range_widgets/range_integer_widget\" start=\"1\" step=\"1\">\n        <label>Range integer widget</label>\n        <hint>range integer widget with no appearance</hint>\n      </range>\n      <range end=\"5.5\" ref=\"/data/range_widgets/range_decimal_widget\" start=\"1.5\" step=\"0.5\">\n        <label>Range decimal widget</label>\n        <hint>range decimal widget with no appearance</hint>\n      </range>\n      <range appearance=\"vertical\" end=\"10\" ref=\"/data/range_widgets/range_integer_widget_vertical\" start=\"1\" step=\"1\">\n        <label>Range vertical integer widget</label>\n        <hint>range integer widget with vertical appearance. This appearance can also be applied to a decimal range.</hint>\n      </range>\n      <range appearance=\"picker\" end=\"10\" ref=\"/data/range_widgets/range_integer_widget_picker\" start=\"1\" step=\"1\">\n        <label>Range picker integer widget</label>\n        <hint>range integer widget with picker appearance. This appearance can also be applied to a decimal range.</hint>\n      </range>\n      <range appearance=\"rating\" end=\"5\" ref=\"/data/range_widgets/range_integer_widget_rating\" start=\"1\" step=\"1\">\n        <label>Range rating integer widget</label>\n        <hint>range integer widget with rating appearance. This appearance can also be applied to a decimal range.</hint>\n      </range>\n    </group>\n    <group ref=\"/data/image_widgets\">\n      <label>Image widgets</label>\n      <upload mediatype=\"image/*\" ref=\"/data/image_widgets/image_widget\">\n        <label>Image widget</label>\n        <hint>image type with no appearance</hint>\n      </upload>\n      <upload appearance=\"new\" mediatype=\"image/*\" ref=\"/data/image_widgets/image_widget_no_choose\">\n        <label>Image widget without Choose button</label>\n        <hint>image type with new appearance (can also be added with annotate appearance and on audio and video types)</hint>\n      </upload>\n      <upload appearance=\"selfie\" mediatype=\"image/*\" ref=\"/data/image_widgets/selfie_image_widget\">\n        <label>Selfie widget</label>\n        <hint>image type with selfie appearance</hint>\n      </upload>\n      <upload appearance=\"draw\" mediatype=\"image/*\" ref=\"/data/image_widgets/draw_image_widget\">\n        <label>Draw widget</label>\n        <hint>image type with draw appearance</hint>\n      </upload>\n      <upload appearance=\"annotate\" mediatype=\"image/*\" ref=\"/data/image_widgets/annotate_image_widget\">\n        <label>Annotate widget</label>\n        <hint>image type with annotate appearance</hint>\n      </upload>\n      <upload appearance=\"signature\" mediatype=\"image/*\" ref=\"/data/image_widgets/signature_widget\">\n        <label>Signature widget</label>\n        <hint>image type with signature appearance</hint>\n      </upload>\n    </group>\n    <group ref=\"/data/media_widgets\">\n      <label>Media widgets</label>\n      <input ref=\"/data/media_widgets/barcode_widget\">\n        <label>Barcode widget</label>\n        <hint>barcode type with no appearance</hint>\n      </input>\n      <upload mediatype=\"audio/*\" ref=\"/data/media_widgets/audio_widget\">\n        <label>Audio widget</label>\n        <hint>audio type with no appearance</hint>\n      </upload>\n      <upload mediatype=\"video/*\" ref=\"/data/media_widgets/video_widget\">\n        <label>Video widget</label>\n        <hint>video type with no appearance</hint>\n      </upload>\n      <upload mediatype=\"application/*\" ref=\"/data/media_widgets/file_widget\">\n        <label>File widget</label>\n        <hint>file type with no appearance &lt;br/&gt; WARNING: any kind of file could be uploaded including files that contain viruses or other malware. Be sure to take proper precautions when downloading files from server.</hint>\n      </upload>\n    </group>\n    <group ref=\"/data/date_time_widgets\">\n      <label>Date and time widgets</label>\n      <input ref=\"/data/date_time_widgets/date_widget\">\n        <label>Date widget</label>\n        <hint>date type with no appearance</hint>\n      </input>\n      <input appearance=\"no-calendar\" ref=\"/data/date_time_widgets/date_widget_nocalendar\">\n        <label>Date Widget</label>\n        <hint>date type with no-calendar appearance</hint>\n      </input>\n      <input appearance=\"month-year\" ref=\"/data/date_time_widgets/date_widget_month_year\">\n        <label>Date widget</label>\n        <hint>date type with month-year appearance</hint>\n      </input>\n      <input appearance=\"year\" ref=\"/data/date_time_widgets/date_widget_year\">\n        <label>Date widget</label>\n        <hint>date type with year appearance</hint>\n      </input>\n      <input ref=\"/data/date_time_widgets/time_widget\">\n        <label>Time widget</label>\n        <hint>time type with no appearance</hint>\n      </input>\n      <input ref=\"/data/date_time_widgets/date_time_widget\">\n        <label>Date time widget</label>\n        <hint>dateTime type with no appearance</hint>\n      </input>\n      <input appearance=\"no-calendar\" ref=\"/data/date_time_widgets/date_time_widget_nocalendar\">\n        <label>Date time widget</label>\n        <hint>dateTime type with no-calendar appearance</hint>\n      </input>\n      <input appearance=\"ethiopian\" ref=\"/data/date_time_widgets/ethiopian_date_widget\">\n        <label>Ethiopian date widget</label>\n        <hint>date type ethiopian appearance</hint>\n      </input>\n      <input appearance=\"coptic\" ref=\"/data/date_time_widgets/coptic_date_widget\">\n        <label>Coptic date widget</label>\n        <hint>date type coptic appearance</hint>\n      </input>\n      <input appearance=\"islamic\" ref=\"/data/date_time_widgets/islamic_date_widget\">\n        <label>Islamic date widget</label>\n        <hint>date type islamic appearance</hint>\n      </input>\n      <input appearance=\"bikram-sambat\" ref=\"/data/date_time_widgets/bikram_sambat_date_widget\">\n        <label>Bikram Sambat date widget</label>\n        <hint>date type bikram-sambat appearance</hint>\n      </input>\n      <input appearance=\"myanmar\" ref=\"/data/date_time_widgets/myanmar_date_widget\">\n        <label>Myanmar date widget</label>\n        <hint>date type myanmar appearance</hint>\n      </input>\n      <input appearance=\"persian\" ref=\"/data/date_time_widgets/persian_date_widget\">\n        <label>Persian date widget</label>\n        <hint>date type persian appearance</hint>\n      </input>\n    </group>\n    <group ref=\"/data/geopoint_widgets\">\n      <label>Geo widgets</label>\n      <input ref=\"/data/geopoint_widgets/geopoint_widget\">\n        <label>Geopoint widget</label>\n        <hint>geopoint type with no appearance</hint>\n      </input>\n      <input appearance=\"placement-map\" ref=\"/data/geopoint_widgets/geopoint_widget_placementmap\">\n        <label>Geopoint widget</label>\n        <hint>geopoint type with placement-map appearance</hint>\n      </input>\n      <input appearance=\"maps\" ref=\"/data/geopoint_widgets/geopoint_widget_maps\">\n        <label>Geopoint widget</label>\n        <hint>geopoint type with maps appearance</hint>\n      </input>\n      <input ref=\"/data/geopoint_widgets/geotrace_widget\">\n        <label>Geotrace widget</label>\n        <hint>geotrace type with no appearance</hint>\n      </input>\n      <input ref=\"/data/geopoint_widgets/geoshape_widget\">\n        <label>Geoshape widget</label>\n        <hint>geoshape type with no appearance</hint>\n      </input>\n      <upload mediatype=\"osm/*\" ref=\"/data/geopoint_widgets/osm_building\">\n        <label>OSM integration</label>\n        <hint>osm type with required tags defined in osm list osm_building</hint>\n        <tag key=\"\">\n          <label>Nom en Francais</label>\n        </tag>\n        <tag key=\"\">\n          <label>House Number</label>\n        </tag>\n        <tag key=\"\">\n          <label>Street Name</label>\n        </tag>\n        <tag key=\"\">\n          <label>City</label>\n        </tag>\n        <tag key=\"\">\n          <label>Zip Code</label>\n        </tag>\n        <tag key=\"\">\n          <label>Building</label>\n        </tag>\n        <tag key=\"\">\n          <label>Building Type</label>\n        </tag>\n      </upload>\n    </group>\n    <group ref=\"/data/select_one_widgets\">\n      <label>Select one widgets</label>\n      <select1 ref=\"/data/select_one_widgets/select_one_widget\">\n        <label>Select one widget</label>\n        <hint>select_one type with no appearance, 4 text choices</hint>\n        <item>\n          <label>A</label>\n          <value>a</value>\n        </item>\n        <item>\n          <label>B</label>\n          <value>b</value>\n        </item>\n        <item>\n          <label>C</label>\n          <value>c</value>\n        </item>\n        <item>\n          <label>D</label>\n          <value>d</value>\n        </item>\n      </select1>\n      <select1 appearance=\"minimal\" ref=\"/data/select_one_widgets/spinner_widget\">\n        <label>Spinner widget</label>\n        <hint>select_one type with minimal appearance, 4 text choices</hint>\n        <item>\n          <label>A</label>\n          <value>a</value>\n        </item>\n        <item>\n          <label>B</label>\n          <value>b</value>\n        </item>\n        <item>\n          <label>C</label>\n          <value>c</value>\n        </item>\n        <item>\n          <label>D</label>\n          <value>d</value>\n        </item>\n      </select1>\n      <select1 appearance=\"quick\" ref=\"/data/select_one_widgets/select_one_autoadvance_widget\">\n        <label>Select one autoadvance widget</label>\n        <hint>select_one type with quick appearance, 4 text choices</hint>\n        <item>\n          <label>A</label>\n          <value>a</value>\n        </item>\n        <item>\n          <label>B</label>\n          <value>b</value>\n        </item>\n        <item>\n          <label>C</label>\n          <value>c</value>\n        </item>\n        <item>\n          <label>D</label>\n          <value>d</value>\n        </item>\n      </select1>\n      <select1 appearance=\"search\" ref=\"/data/select_one_widgets/select_one_search_widget\">\n        <label>Select one search widget</label>\n        <hint>select_one type with search appearance, 4 text choices</hint>\n        <item>\n          <label>A</label>\n          <value>a</value>\n        </item>\n        <item>\n          <label>B</label>\n          <value>b</value>\n        </item>\n        <item>\n          <label>C</label>\n          <value>c</value>\n        </item>\n        <item>\n          <label>D</label>\n          <value>d</value>\n        </item>\n      </select1>\n      <select1 appearance=\"autocomplete\" ref=\"/data/select_one_widgets/select_one_autocomplete_widget\">\n        <label>Select one search widget</label>\n        <hint>select_one type with autocomplete appearance, 4 text choices</hint>\n        <item>\n          <label>A</label>\n          <value>a</value>\n        </item>\n        <item>\n          <label>B</label>\n          <value>b</value>\n        </item>\n        <item>\n          <label>C</label>\n          <value>c</value>\n        </item>\n        <item>\n          <label>D</label>\n          <value>d</value>\n        </item>\n      </select1>\n      <select1 ref=\"/data/select_one_widgets/grid_widget\">\n        <label>Grid select one widget</label>\n        <hint>select_one type with no appearance, 4 image choices (a.jpg, b.jpg, c.jpg, d.jpg)</hint>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget/a:label')\"/>\n          <value>a</value>\n        </item><item>\n        <label ref=\"jr:itext('/data/select_one_widgets/grid_widget/b:label')\"/>\n        <value>b</value>\n      </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget/c:label')\"/>\n          <value>c</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget/d:label')\"/>\n          <value>d</value>\n        </item>\n      </select1>\n      <select1 appearance=\"compact\" ref=\"/data/select_one_widgets/grid_widget_compact\">\n        <label>Grid select one widget</label>\n        <hint>select_one type with compact appearance, 4 image choices (a.jpg, b.jpg, c.jpg, d.jpg)</hint>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_compact/a:label')\"/>\n          <value>a</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_compact/b:label')\"/>\n          <value>b</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_compact/c:label')\"/>\n          <value>c</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_compact/d:label')\"/>\n          <value>d</value>\n        </item>\n      </select1>\n      <select1 appearance=\"compact-2\" ref=\"/data/select_one_widgets/grid_widget_compact2\">\n        <label>Grid select one widget</label>\n        <hint>select_one type with compact-2 appearance, 4 image choices (a.jpg, b.jpg, c.jpg, d.jpg)</hint>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_compact2/a:label')\"/>\n          <value>a</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_compact2/b:label')\"/>\n          <value>b</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_compact2/c:label')\"/>\n          <value>c</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_compact2/d:label')\"/>\n          <value>d</value>\n        </item>\n      </select1>\n      <select1 appearance=\"quickcompact\" ref=\"/data/select_one_widgets/grid_widget_quickcompact\">\n        <label>Grid select one widget</label>\n        <hint>select_one type with quickcompact appearance, 4 image choices (a.jpg, b.jpg, c.jpg, d.jpg)</hint>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_quickcompact/a:label')\"/>\n          <value>a</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_quickcompact/b:label')\"/>\n          <value>b</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_quickcompact/c:label')\"/>\n          <value>c</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_quickcompact/d:label')\"/>\n          <value>d</value>\n        </item>\n      </select1>\n      <select1 appearance=\"quickcompact-2\" ref=\"/data/select_one_widgets/grid_widget_quickcompact2\">\n        <label>Grid select one widget</label>\n        <hint>select_one type with quickcompact-2 appearance, 4 image choices (a.jpg, b.jpg, c.jpg, d.jpg)</hint>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_quickcompact2/a:label')\"/>\n          <value>a</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_quickcompact2/b:label')\"/>\n          <value>b</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_quickcompact2/c:label')\"/>\n          <value>c</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/grid_widget_quickcompact2/d:label')\"/>\n          <value>d</value>\n        </item>\n      </select1>\n      <select1 appearance=\"image-map\" ref=\"/data/select_one_widgets/select_one_image_map\">\n        <label ref=\"jr:itext('/data/select_one_widgets/select_one_image_map:label')\"/>\n        <hint>select_one with type image-map appearance and image body.svg</hint>\n        <item>\n          <label>Head</label>\n          <value>head</value>\n        </item>\n        <item>\n          <label>Neck</label>\n          <value>neck</value>\n        </item>\n        <item>\n          <label>Lungs</label>\n          <value>lungs</value>\n        </item>\n        <item>\n          <label>Left proximal arm</label>\n          <value>left_proximal_arm</value>\n        </item>\n        <item>\n          <label>Left distal arm</label>\n          <value>left_distal_arm</value>\n        </item>\n        <item>\n          <label>Right proximal arm</label>\n          <value>right_proximal_arm</value>\n        </item>\n        <item>\n          <label>Right distal arm</label>\n          <value>right_distal_arm</value>\n        </item>\n        <item>\n          <label>Left proximal leg</label>\n          <value>left_proximal_leg</value>\n        </item>\n        <item>\n          <label>Left distal leg</label>\n          <value>left_distal_leg</value>\n        </item>\n        <item>\n          <label>Right proximal leg</label>\n          <value>right_proximal_leg</value>\n        </item>\n        <item>\n          <label>Right distal leg</label>\n          <value>right_distal_leg</value>\n        </item>\n      </select1>\n      <select1 appearance=\"likert\" ref=\"/data/select_one_widgets/likert_widget\">\n        <label>Likert widget</label>\n        <hint>Images made by Pixel Perfect and downloaded from flaticon.com</hint>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/likert_widget/strongly_disagree:label')\"/>\n          <value>strongly_disagree</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/likert_widget/disagree:label')\"/>\n          <value>disagree</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/likert_widget/neutral:label')\"/>\n          <value>neutral</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/likert_widget/agree:label')\"/>\n          <value>agree</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_one_widgets/likert_widget/strongly_agree:label')\"/>\n          <value>strongly_agree</value>\n        </item>\n      </select1>\n      <select1 appearance=\"map\" ref=\"/data/select_one_widgets/select_one_from_map_widget\">\n        <label>Select one from map widget</label>\n        <hint>select_one type with map appearance</hint>\n        <itemset nodeset=\"instance('places')/root/item[true()]\">\n          <value ref=\"name\" />\n          <label ref=\"label\" />\n        </itemset>\n      </select1>\n    </group>\n    <group ref=\"/data/select_multi_widgets\">\n      <label>Select multi widgets</label>\n      <select ref=\"/data/select_multi_widgets/select_multi_widget\">\n        <label>Multi select widget</label>\n        <hint>select_multiple type with no appearance, 4 text choices</hint>\n        <item>\n          <label>A</label>\n          <value>a</value>\n        </item>\n        <item>\n          <label>B</label>\n          <value>b</value>\n        </item>\n        <item>\n          <label>C</label>\n          <value>c</value>\n        </item>\n        <item>\n          <label>D</label>\n          <value>d</value>\n        </item>\n      </select>\n      <select appearance=\"autocomplete\" ref=\"/data/select_multi_widgets/select_multi_widget_autocomplete\">\n        <label>Multi select autocomplete widget</label>\n        <hint>select_multiple type with autocomplete appearance, 4 text choices</hint>\n        <item>\n          <label>A</label>\n          <value>a</value>\n        </item>\n        <item>\n          <label>B</label>\n          <value>b</value>\n        </item>\n        <item>\n          <label>C</label>\n          <value>c</value>\n        </item>\n        <item>\n          <label>D</label>\n          <value>d</value>\n        </item>\n      </select>\n      <select appearance=\"compact\" ref=\"/data/select_multi_widgets/grid_multi_widget_compact\">\n        <label>Grid select multiple widget</label>\n        <hint>select_multiple type with compact appearance, 4 image choices</hint>\n        <item>\n          <label ref=\"jr:itext('/data/select_multi_widgets/grid_multi_widget_compact/a:label')\"/>\n          <value>a</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_multi_widgets/grid_multi_widget_compact/b:label')\"/>\n          <value>b</value>\n        </item><item>\n        <label ref=\"jr:itext('/data/select_multi_widgets/grid_multi_widget_compact/c:label')\"/>\n        <value>c</value>\n      </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_multi_widgets/grid_multi_widget_compact/d:label')\"/>\n          <value>d</value>\n        </item>\n      </select>\n      <select appearance=\"compact-2\" ref=\"/data/select_multi_widgets/grid_multi_widget_compact2\">\n        <label>Grid select multiple widget</label>\n        <hint>select_multiple type with compact-2 appearance, 4 image choices</hint>\n        <item>\n          <label ref=\"jr:itext('/data/select_multi_widgets/grid_multi_widget_compact2/a:label')\"/>\n          <value>a</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_multi_widgets/grid_multi_widget_compact2/b:label')\"/>\n          <value>b</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_multi_widgets/grid_multi_widget_compact2/c:label')\"/>\n          <value>c</value>\n        </item>\n        <item>\n          <label ref=\"jr:itext('/data/select_multi_widgets/grid_multi_widget_compact2/d:label')\"/>\n          <value>d</value>\n        </item>\n      </select>\n      <select appearance=\"minimal\" ref=\"/data/select_multi_widgets/spinner_multi_widget\">\n        <label>Spinner widget: select multiple</label>\n        <hint>select_multiple type with minimal appearance, 4 image choices</hint>\n        <item>\n          <label>A</label>\n          <value>a</value>\n        </item>\n        <item>\n          <label>B</label>\n          <value>b</value>\n        </item>\n        <item>\n          <label>C</label>\n          <value>c</value>\n        </item>\n        <item>\n          <label>D</label>\n          <value>d</value>\n        </item>\n      </select>\n      <select appearance=\"image-map\" ref=\"/data/select_multi_widgets/select_multi_image_map\">\n        <label ref=\"jr:itext('/data/select_multi_widgets/select_multi_image_map:label')\"/>\n        <hint>select_multiple type with image-map appearance and image body.svg</hint>\n        <item>\n          <label>Head</label>\n          <value>head</value>\n        </item>\n        <item>\n          <label>Neck</label>\n          <value>neck</value>\n        </item>\n        <item>\n          <label>Lungs</label>\n          <value>lungs</value>\n        </item>\n        <item>\n          <label>Left proximal arm</label>\n          <value>left_proximal_arm</value>\n        </item>\n        <item>\n          <label>Left distal arm</label>\n          <value>left_distal_arm</value>\n        </item>\n        <item>\n          <label>Right proximal arm</label>\n          <value>right_proximal_arm</value>\n        </item>\n        <item>\n          <label>Right distal arm</label>\n          <value>right_distal_arm</value>\n        </item>\n        <item>\n          <label>Left proximal leg</label>\n          <value>left_proximal_leg</value>\n        </item>\n        <item>\n          <label>Left distal leg</label>\n          <value>left_distal_leg</value>\n        </item>\n        <item>\n          <label>Right proximal leg</label>\n          <value>right_proximal_leg</value>\n        </item>\n        <item>\n          <label>Right distal leg</label>\n          <value>right_distal_leg</value>\n        </item>\n      </select>\n    </group>\n    <group appearance=\"field-list\" ref=\"/data/table_list_test\">\n      <label>List group</label>\n      <select1 appearance=\"label\" ref=\"/data/table_list_test/table_list_test_label\">\n        <label>Label widget</label>\n        <hint>Show only the labels of these options and not the inputs (type=select_one yes_no, appearance=label)</hint>\n        <item>\n          <label>Yes</label>\n          <value>yes</value>\n        </item>\n        <item>\n          <label>No</label>\n          <value>no</value>\n        </item>\n        <item>\n          <label>Don't Know</label>\n          <value>dk</value>\n        </item>\n        <item>\n          <label>Not Applicable</label>\n          <value>na</value>\n        </item>\n      </select1>\n      <select appearance=\"label\" ref=\"/data/table_list_test/table_list_test_label_2\">\n        <label>Label multi widget</label>\n        <hint>Show only the labels of these options and not the inputs (type=select_multiple yes_no, appearance=label)</hint>\n        <item>\n          <label>Yes</label>\n          <value>yes</value>\n        </item>\n        <item>\n          <label>No</label>\n          <value>no</value>\n        </item>\n        <item>\n          <label>Don't Know</label>\n          <value>dk</value>\n        </item>\n        <item>\n          <label>Not Applicable</label>\n          <value>na</value>\n        </item>\n      </select>\n      <select1 appearance=\"list-nolabel\" ref=\"/data/table_list_test/table_list_1\">\n        <label>List widget</label>\n        <hint>Show only the inputs of these options and not the labels (type=select_one yes_no, appearance=list-nolabel)</hint>\n        <item>\n          <label>Yes</label>\n          <value>yes</value>\n        </item>\n        <item>\n          <label>No</label>\n          <value>no</value>\n        </item>\n        <item>\n          <label>Don't Know</label>\n          <value>dk</value>\n        </item>\n        <item>\n          <label>Not Applicable</label>\n          <value>na</value>\n        </item>\n      </select1>\n      <select appearance=\"list-nolabel\" ref=\"/data/table_list_test/table_list_2\">\n        <label>List multi widget</label>\n        <hint>Show only the inputs of these options and not the labels (type=select_multiple yes_no, appearance=list-nolabel)</hint>\n        <item>\n          <label>Yes</label>\n          <value>yes</value>\n        </item>\n        <item>\n          <label>No</label>\n          <value>no</value>\n        </item>\n        <item>\n          <label>Don't Know</label>\n          <value>dk</value>\n        </item>\n        <item>\n          <label>Not Applicable</label>\n          <value>na</value>\n        </item>\n      </select>\n      <select1 appearance=\"list\" ref=\"/data/table_list_test/list_widget\">\n        <label>List widget</label>\n        <hint>This is a normal list widget with (type = select_one, appearance = list)</hint>\n        <item>\n          <label>Yes</label>\n          <value>yes</value>\n        </item>\n        <item>\n          <label>No</label>\n          <value>no</value>\n        </item>\n        <item>\n          <label>Don't Know</label>\n          <value>dk</value>\n        </item>\n        <item>\n          <label>Not Applicable</label>\n          <value>na</value>\n        </item>\n      </select1>\n      <select appearance=\"list\" ref=\"/data/table_list_test/list_multi_widget\">\n        <label>List multi widget</label>\n        <hint>This is a normal list widget with (type = select_multiple, appearance = list)</hint>\n        <item>\n          <label>Yes</label>\n          <value>yes</value>\n        </item>\n        <item>\n          <label>No</label>\n          <value>no</value>\n        </item>\n        <item>\n          <label>Don't Know</label>\n          <value>dk</value>\n        </item>\n        <item>\n          <label>Not Applicable</label>\n          <value>na</value>\n        </item>\n      </select>\n    </group>\n    <odk:rank ref=\"/data/rank_widget\">\n      <label>Rank widget</label>\n      <hint>rank type with no appearance, 4 text choices. Long press on a choice and drag it to change its position.</hint>\n      <item>\n        <label>A</label>\n        <value>a</value>\n      </item>\n      <item>\n        <label>B</label>\n        <value>b</value>\n      </item>\n      <item>\n        <label>C</label>\n        <value>c</value>\n      </item>\n      <item>\n        <label>D</label>\n        <value>d</value>\n      </item>\n    </odk:rank>\n    <trigger ref=\"/data/my_trigger\">\n      <label>Trigger widget</label>\n      <hint>Prompts for confirmation. Useful to combine with required or relevant. (type=trigger)</hint>\n    </trigger>\n  </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/audio-question.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>Audio Question</h:title>\n        <model>\n            <instance>\n                <data id=\"audio_question\" orx:version=\"1\">\n                    <sound/>\n                </data>\n            </instance>\n            <bind nodeset=\"sound\" type=\"binary\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <upload mediatype=\"audio/*\" ref=\"/data/sound\">\n            <label>What does it sound like?</label>\n        </upload>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/basic.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head>\n    <h:title>basic</h:title>\n    <model>\n      <instance>\n        <basic id=\"basic\">\n          <name>\n            <first/>\n            </name>\n          <age/>\n        </basic>\n      </instance>\n      <bind nodeset=\"name/first\" type=\"int\"/>\n      <bind nodeset=\"age\" type=\"int\"/>\n    </model>\n  </h:head>\n  <h:body>\n    <input ref=\"name/first\">\n      <label>what is your name Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\n      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,\n      quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\n      consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse\n      cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non\n      proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\n      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,\n      quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\n      consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse\n      cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non\n      proident, sunt in culpa qui officia deserunt mollit anim id est laborum. LoremLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\n      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,\n      quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\n      consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse\n      cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non\n      proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem</label>\n    </input>\n    <input ref=\"age\">\n      <label>what is your age</label>\n    </input>\n  </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/different-search-appearances.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>different-search-appearances</h:title>\n        <model>\n            <instance>\n                <different-search-appearances id=\"different-search-appearances\">\n                    <fruit1 />\n                    <note_fruit />\n                    <animal1 />\n                    <animal2 />\n                    <animal3 />\n                    <fruit2 />\n                    <fruit3 />\n                    <fruit4 />\n                    <animal4 />\n                    <animal5 />\n                    <animal6 />\n                    <fruit5 />\n                    <fruit6 />\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </different-search-appearances>\n            </instance>\n            <bind nodeset=\"/different-search-appearances/fruit1\" type=\"select1\" />\n            <bind nodeset=\"/different-search-appearances/note_fruit\" readonly=\"true()\" type=\"string\" />\n            <bind nodeset=\"/different-search-appearances/animal1\" type=\"select1\" />\n            <bind nodeset=\"/different-search-appearances/animal2\" type=\"select1\" />\n            <bind nodeset=\"/different-search-appearances/animal3\" type=\"select1\" />\n            <bind nodeset=\"/different-search-appearances/fruit2\" type=\"select1\" />\n            <bind nodeset=\"/different-search-appearances/fruit3\" type=\"select1\" />\n            <bind nodeset=\"/different-search-appearances/fruit4\" type=\"select\" />\n            <bind nodeset=\"/different-search-appearances/animal4\" type=\"select\" />\n            <bind nodeset=\"/different-search-appearances/animal5\" type=\"select\" />\n            <bind nodeset=\"/different-search-appearances/animal6\" type=\"select\" />\n            <bind nodeset=\"/different-search-appearances/fruit5\" type=\"select\" />\n            <bind nodeset=\"/different-search-appearances/fruit6\" type=\"select\" />\n            <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/different-search-appearances/meta/instanceID\" readonly=\"true()\" type=\"string\" />\n        </model>\n    </h:head>\n    <h:body>\n        <select1 appearance=\"search('fruits')\" ref=\"/different-search-appearances/fruit1\">\n            <label>Select one from a CSV using search() appearance/function</label>\n            <item>\n                <label>name</label>\n                <value>name_key</value>\n            </item>\n        </select1>\n        <input ref=\"/different-search-appearances/note_fruit\">\n            <label>\n                The fruit <output value=\" /different-search-appearances/fruit1 \" /> pulled from csv\n            </label>\n        </input>\n        <select1 ref=\"/different-search-appearances/animal1\">\n            <label>Static select one with no appearance</label>\n            <item>\n                <label>Wolf</label>\n                <value>wolf</value>\n            </item>\n            <item>\n                <label>Warthog</label>\n                <value>warthog</value>\n            </item>\n            <item>\n                <label>Raccoon</label>\n                <value>raccoon</value>\n            </item>\n            <item>\n                <label>Rabbit</label>\n                <value>rabbit</value>\n            </item>\n        </select1>\n        <select1 appearance=\"search\" ref=\"/different-search-appearances/animal2\">\n            <label>Static select one with search appearance</label>\n            <item>\n                <label>Wolf</label>\n                <value>wolf</value>\n            </item>\n            <item>\n                <label>Warthog</label>\n                <value>warthog</value>\n            </item>\n            <item>\n                <label>Raccoon</label>\n                <value>raccoon</value>\n            </item>\n            <item>\n                <label>Rabbit</label>\n                <value>rabbit</value>\n            </item>\n        </select1>\n        <select1 appearance=\"autocomplete\" ref=\"/different-search-appearances/animal3\">\n            <label>Static select one with autocomplete appearance</label>\n            <item>\n                <label>Wolf</label>\n                <value>wolf</value>\n            </item>\n            <item>\n                <label>Warthog</label>\n                <value>warthog</value>\n            </item>\n            <item>\n                <label>Raccoon</label>\n                <value>raccoon</value>\n            </item>\n            <item>\n                <label>Rabbit</label>\n                <value>rabbit</value>\n            </item>\n        </select1>\n        <select1 appearance=\"search search('fruits')\" ref=\"/different-search-appearances/fruit2\">\n            <label>Select one from a CSV using search() appearance/function and search appearance</label>\n            <item>\n                <label>name</label>\n                <value>name_key</value>\n            </item>\n        </select1>\n        <select1 appearance=\"autocomplete search('fruits')\" ref=\"/different-search-appearances/fruit3\">\n            <label>Select one from a CSV using search() appearance/function and autocomplete appearance</label>\n            <item>\n                <label>name</label>\n                <value>name_key</value>\n            </item>\n        </select1>\n        <select appearance=\"search('fruits')\" ref=\"/different-search-appearances/fruit4\">\n            <label>Select multiple from a CSV using search() appearance/function</label>\n            <item>\n                <label>name</label>\n                <value>name_key</value>\n            </item>\n        </select>\n        <select ref=\"/different-search-appearances/animal4\">\n            <label>Static select multiple with no appearance</label>\n            <item>\n                <label>Wolf</label>\n                <value>wolf</value>\n            </item>\n            <item>\n                <label>Warthog</label>\n                <value>warthog</value>\n            </item>\n            <item>\n                <label>Raccoon</label>\n                <value>raccoon</value>\n            </item>\n            <item>\n                <label>Rabbit</label>\n                <value>rabbit</value>\n            </item>\n        </select>\n        <select appearance=\"search\" ref=\"/different-search-appearances/animal5\">\n            <label>Static select multiple with search appearance</label>\n            <item>\n                <label>Wolf</label>\n                <value>wolf</value>\n            </item>\n            <item>\n                <label>Warthog</label>\n                <value>warthog</value>\n            </item>\n            <item>\n                <label>Raccoon</label>\n                <value>raccoon</value>\n            </item>\n            <item>\n                <label>Rabbit</label>\n                <value>rabbit</value>\n            </item>\n        </select>\n        <select appearance=\"autocomplete\" ref=\"/different-search-appearances/animal6\">\n            <label>Static select multiple with autocomplete appearance</label>\n            <item>\n                <label>Wolf</label>\n                <value>wolf</value>\n            </item>\n            <item>\n                <label>Warthog</label>\n                <value>warthog</value>\n            </item>\n            <item>\n                <label>Raccoon</label>\n                <value>raccoon</value>\n            </item>\n            <item>\n                <label>Rabbit</label>\n                <value>rabbit</value>\n            </item>\n        </select>\n        <select appearance=\"search search('fruits')\" ref=\"/different-search-appearances/fruit5\">\n            <label>Select multiple from a CSV using search() appearance/function and search appearance</label>\n            <item>\n                <label>name</label>\n                <value>name_key</value>\n            </item>\n        </select>\n        <select appearance=\"autocomplete search('fruits')\" ref=\"/different-search-appearances/fruit6\">\n            <label>Select multiple from a CSV using search() appearance/function and autocomplete appearance</label>\n            <item>\n                <label>name</label>\n                <value>name_key</value>\n            </item>\n        </select>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/dynamic_and_static_choices.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:ev=\"http://www.w3.org/2001/xml-events\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>dynamic_and_static_choices</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"dynamic_and_static_choices\">\n                    <fruits/>\n                    <numbers/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/fruits\" type=\"string\"/>\n            <bind nodeset=\"/data/numbers\" type=\"string\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <select1 ref=\"/data/fruits\" appearance=\"search('fruits')\">\n            <label>Choose a fruit</label>\n            <item>\n                <label>name</label>\n                <value>name_key</value>\n            </item>\n            <item>\n                <label>None of the above</label>\n                <value>0</value>\n            </item>\n        </select1>\n        <select1 ref=\"/data/numbers\" appearance=\"search('numbers')\">\n            <label>Choose a number</label>\n            <item>\n                <label>0</label>\n                <value>0</value>\n            </item>\n            <item>\n                <label>1</label>\n                <value>1</value>\n            </item>\n        </select1>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/dynamic_required_question.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:ev=\"http://www.w3.org/2001/xml-events\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>dynamic_required_question</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"dynamic_required_question\">\n                    <group>\n                        <source/>\n                        <target/>\n                    </group>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/group/source\" type=\"string\"/>\n            <bind nodeset=\"/data/group/target\" type=\"string\" required=\" /data/group/source !=&quot;&quot;\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\" ref=\"/data/group\">\n            <input ref=\"/data/group/source\">\n                <label>Source</label>\n            </input>\n            <input ref=\"/data/group/target\">\n                <label>Target</label>\n            </input>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/emptyGroupFieldList.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\r\n  <h:head>\r\n    <h:title>emptyGroupFieldList</h:title>\r\n    <model>\r\n      <instance>\r\n        <data id=\"build_emptyGroupFieldList_1538592237\">\r\n          <meta>\r\n            <instanceID/>\r\n          </meta>\r\n          <g>\r\n          </g>\r\n        </data>\r\n      </instance>\r\n      <itext>\r\n        <translation lang=\"English\">\r\n          <text id=\"/data/g:label\">\r\n            <value>g</value>\r\n          </text>\r\n        </translation>\r\n      </itext>\r\n      <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\r\n    </model>\r\n  </h:head>\r\n  <h:body>\r\n    <group appearance=\"field-list\">\r\n      <label ref=\"jr:itext('/data/g:label')\"/>\r\n    </group>\r\n  </h:body>\r\n</h:html>\r\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/encrypted-no-instanceID.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>encrypted-no-instanceID</h:title>\n        <model>\n            <submission base64RsaPublicKey=\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsrq8xrFswX/Ht1X4UVeLjupqz8hCMo+GR3hwqZdx7BQjvw9KBcCL+J6CI/2yyTqprVmeWZZu/i9qjuYcyWir3ZonZIDaVvClP8LN7e0+0SgQgEV+v9bjGVTDMQIKY2vq2ZNEbuy4UAHFLJCwGaUE370w76r/Da4YbAgfGVQn1sHarJ8Zp1o/6RE1IckxA8L2spo8oSU23KnttLIaR2qIS7mY+BkZPItyyNjulpJUZlxf4AgO7T8S4grmOC5TW4laB25vjbPw4KzB3L8bm+oK5JjlocazOiyUVDz8UwYMQke4ybEwSJbu3gl7DJzlwwQ1u3AbtjZk2T7LKUotrkVzAQIDAQAB\"/>\n            <instance>\n                <encrypted-no-instanceID id=\"encrypted-no-instanceID\">\n                    <question/>\n                </encrypted-no-instanceID>\n            </instance>\n            <bind nodeset=\"/encrypted-no-instanceID/question\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/encrypted-no-instanceID/question\">\n            <label>Question 1</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/encrypted.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>encrypted</h:title>\n        <model>\n            <submission base64RsaPublicKey=\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsrq8xrFswX/Ht1X4UVeLjupqz8hCMo+GR3hwqZdx7BQjvw9KBcCL+J6CI/2yyTqprVmeWZZu/i9qjuYcyWir3ZonZIDaVvClP8LN7e0+0SgQgEV+v9bjGVTDMQIKY2vq2ZNEbuy4UAHFLJCwGaUE370w76r/Da4YbAgfGVQn1sHarJ8Zp1o/6RE1IckxA8L2spo8oSU23KnttLIaR2qIS7mY+BkZPItyyNjulpJUZlxf4AgO7T8S4grmOC5TW4laB25vjbPw4KzB3L8bm+oK5JjlocazOiyUVDz8UwYMQke4ybEwSJbu3gl7DJzlwwQ1u3AbtjZk2T7LKUotrkVzAQIDAQAB\"/>\n            <instance>\n                <encrypted id=\"encrypted\">\n                    <question/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </encrypted>\n            </instance>\n            <bind nodeset=\"/encrypted/question\" type=\"string\"/>\n            <bind jr:preload=\"uid\" nodeset=\"/encrypted/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/encrypted/question\">\n            <label>Question 1</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/entity-update-pulldata.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n\t<h:head>\n\t\t<h:title>Entity Update Pull Data</h:title>\n\t\t<model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2024.1.0\">\n\t\t\t<instance>\n\t\t\t\t<data id=\"entity_update_pull_data\" version=\"1\">\n\t\t\t\t\t<selected_person/>\n\t\t\t\t\t<person/>\n\t\t\t\t\t<name/>\n\t\t\t\t\t<meta>\n\t\t\t\t\t\t<instanceID/>\n\t\t\t\t\t\t<instanceName/>\n\t\t\t\t\t\t<entity dataset=\"people\" update=\"true\" id=\"\" baseVersion=\"\" trunkVersion=\"\" branchId=\"\">\n\t\t\t\t\t\t\t<label/>\n\t\t\t\t\t\t</entity>\n\t\t\t\t\t</meta>\n\t\t\t\t</data>\n\t\t\t</instance>\n\n\t\t\t<instance id=\"people\" src=\"jr://file-csv/people.csv\"/>\n\n\t\t\t<bind nodeset=\"/data/selected_person\" type=\"string\" calculate=\"pulldata('people', 'full_name', 'name', /data/person)\"/>\n\t\t\t<bind nodeset=\"/data/person\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/name\" type=\"string\" calculate=\"pulldata('people', 'full_name', 'name', /data/person)\" entities:saveto=\"full_name\"/>\n\n\t\t\t<bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n\t\t\t<bind nodeset=\"/data/meta/entity/@id\" type=\"string\" calculate=\"/data/person\"/>\n\t\t\t<bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/meta/entity/@baseVersion\" calculate=\"instance('people')/root/item[name=/data/person]/__version\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/meta/entity/@trunkVersion\" calculate=\"instance('people')/root/item[name=/data/person]/__trunkVersion\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/meta/entity/@branchId\" calculate=\"instance('people')/root/item[name=/data/person]/__branchId\" type=\"string\"/>\n\t\t</model>\n\t</h:head>\n\t<h:body>\n\t\t<select1 ref=\"/data/person\">\n\t\t\t<label>Select person</label>\n\t\t\t<itemset nodeset=\"instance('people')/root/item\">\n\t\t\t\t<value ref=\"name\"/>\n\t\t\t\t<label ref=\"label\"/>\n\t\t\t</itemset>\n\t\t\t<setvalue event=\"xforms-value-changed\" ref=\"/data/name\" value=\"/data/selected_person\" />\n\t\t</select1>\n\t\t<input ref=\"/data/name\">\n\t\t\t<label>Name</label>\n\t\t</input>\n\t</h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/external-audio-question.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>External Audio Question</h:title>\n        <model>\n            <instance>\n                <data id=\"audio_question\" orx:version=\"1\">\n                    <sound/>\n                </data>\n            </instance>\n            <bind nodeset=\"sound\" type=\"binary\" odk:quality=\"external\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <upload mediatype=\"audio/*\" ref=\"/data/sound\">\n            <label>What does it sound like?</label>\n        </upload>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/external-csv-search-broken.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head>\n    <h:title>external-csv-search</h:title>\n    <model>\n      <instance>\n        <external-csv-search id=\"external-csv-search\">\n          <produce_search/>\n          <produce/>\n          <meta>\n            <instanceID/>\n          </meta>\n        </external-csv-search>\n      </instance>\n      <bind nodeset=\"/external-csv-search/produce_search\" type=\"string\"/>\n      <bind nodeset=\"/external-csv-search/produce\" type=\"select1\"/>\n      <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/external-csv-search/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n    </model>\n  </h:head>\n  <h:body>\n    <input ref=\"/external-csv-search/produce_search\">\n      <label>Produce search</label>\n    </input>\n    <select1 appearance=\"search('external-csv-search-produce', 'contains', 'wat',  /external-csv-search/produce_search )\" ref=\"/external-csv-search/produce\">\n      <label>Produce</label>\n      <item>\n        <label>label</label>\n        <value>name</value>\n      </item>\n    </select1>\n  </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/external-csv-search.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head>\n    <h:title>external-csv-search</h:title>\n    <model>\n      <instance>\n        <external-csv-search id=\"external-csv-search\">\n          <multi_produce/>\n          <produce_search/>\n          <produce/>\n          <meta>\n            <instanceID/>\n          </meta>\n        </external-csv-search>\n      </instance>\n      <bind nodeset=\"/external-csv-search/multi_produce\" type=\"select\"/>\n      <bind nodeset=\"/external-csv-search/produce_search\" type=\"string\"/>\n      <bind nodeset=\"/external-csv-search/produce\" type=\"select1\"/>\n      <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/external-csv-search/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n    </model>\n  </h:head>\n  <h:body>\n    <select appearance=\"search('external-csv-search-produce')\" ref=\"/external-csv-search/multi_produce\">\n      <label>Multiple produce</label>\n      <item>\n        <label>label</label>\n        <value>name</value>\n      </item>\n    </select>\n    <input ref=\"/external-csv-search/produce_search\">\n      <label>Produce search</label>\n    </input>\n    <select1 appearance=\"search('external-csv-search-produce', 'contains', 'name',  /external-csv-search/produce_search )\" ref=\"/external-csv-search/produce\">\n      <label>Produce</label>\n      <item>\n        <label>label</label>\n        <value>name</value>\n      </item>\n    </select1>\n  </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/external_csv_form.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head><!-- ODK Aggregate upload time: 2015-04-09T23:26:16.409+0000 on https://enketo-aggregate.appspot.com -->\n    <h:title>Form with external CSV files</h:title>\n    <model>\n      <instance>\n        <ecsv id=\"ecsv\" version=\"\">\n          <country/>\n          <city/>\n          <neighborhood/>\n          <meta>\n            <instanceID/>\n          </meta>\n        </ecsv>\n      </instance>\n      <instance id=\"cities\" src=\"jr://file-csv/external_csv_cities.csv\" />\n      <instance id=\"neighborhoods\" src=\"jr://file-csv/external_csv_neighbourhoods.csv\" />\n      <instance id=\"countries\" src=\"jr://file-csv/external_csv_countries.csv\" />\n      <bind nodeset=\"/ecsv/country\" type=\"select1\"/>\n      <bind nodeset=\"/ecsv/city\" type=\"select1\"/>\n      <bind nodeset=\"/ecsv/neighborhood\" type=\"select1\"/>\n      <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/ecsv/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n    </model>\n  </h:head>\n  <h:body>\n    <select1 ref=\"/ecsv/country\">\n      <label>Country</label>\n      <itemset nodeset=\"instance('countries')/root/item\">\n          <value ref=\"name\"/>\n          <label ref=\"label\"/>\n      </itemset>\n    </select1>\n    <select1 ref=\"/ecsv/city\">\n      <label>City</label>\n      <itemset nodeset=\"instance('cities')/root/item[country= /ecsv/country ]\">\n        <value ref=\"name\"/>\n        <label ref=\"label\"/>\n      </itemset>\n    </select1>\n    <select1 ref=\"/ecsv/neighborhood\">\n      <label>Neighborhood</label>\n      <itemset nodeset=\"instance('neighborhoods')/root/item[country= /ecsv/country  and city= /ecsv/city ]\">\n        <value ref=\"name\"/>\n        <label ref=\"label\"/>\n      </itemset>\n    </select1>\n  </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/external_data_questions.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:ev=\"http://www.w3.org/2001/xml-events\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>externalDataQuestions</h:title>\n        <model>\n            <instance>\n                <externalDataQuestions id=\"externalDataQuestions\">\n                    <q1/>\n                    <q2/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </externalDataQuestions>\n            </instance>\n            <bind nodeset=\"/externalDataQuestions/q1\" type=\"select1\"/>\n            <bind nodeset=\"/externalDataQuestions/q2\" type=\"string\"/>\n            <bind jr:preload=\"uid\" nodeset=\"/externalDataQuestions/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <select1 appearance=\"search('fruits')\" ref=\"/externalDataQuestions/q1\">\n            <label>Search func</label>\n            <item>\n                <label>name</label>\n                <value>name_key</value>\n            </item>\n        </select1>\n        <input query=\"instance('counties')/root/item[county= &quot;King&quot;]\" ref=\"/externalDataQuestions/q2\">\n            <label>External csv</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/external_select.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>external select</h:title>\n        <model>\n            <instance>\n                <external_select id=\"external_select\">\n                    <first/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </external_select>\n            </instance>\n            <instance id=\"xml\" src=\"jr://file/external_data.xml\"/>\n            <bind nodeset=\"/external_select/first\" type=\"select1\"/>\n            <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/external_select/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <select1 ref=\"/external_select/first\">\n            <label>First</label>\n            <itemset nodeset=\"instance('xml')/root/item\">\n                <value ref=\"name\"/>\n                <label ref=\"label\"/>\n            </itemset>\n        </select1>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/external_select_10.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>external select 10</h:title>\n        <model>\n            <itext>\n                <translation default=\"true()\" lang=\"default\">\n                    <text id=\"static_instance-first-0\">\n                        <value>a</value>\n                    </text>\n                    <text id=\"static_instance-first-1\">\n                        <value>b</value>\n                    </text>\n                    <text id=\"static_instance-first-2\">\n                        <value>c</value>\n                    </text>\n                    <text id=\"static_instance-second-0\">\n                        <value>aa</value>\n                    </text>\n                    <text id=\"static_instance-second-1\">\n                        <value>ab</value>\n                    </text>\n                    <text id=\"static_instance-second-2\">\n                        <value>ac</value>\n                    </text>\n                    <text id=\"static_instance-second-3\">\n                        <value>ba</value>\n                    </text>\n                    <text id=\"static_instance-second-4\">\n                        <value>bb</value>\n                    </text>\n                    <text id=\"static_instance-second-5\">\n                        <value>bc</value>\n                    </text>\n                    <text id=\"static_instance-second-6\">\n                        <value>ca</value>\n                    </text>\n                    <text id=\"static_instance-second-7\">\n                        <value>cb</value>\n                    </text>\n                    <text id=\"static_instance-second-8\">\n                        <value>cc</value>\n                    </text>\n                </translation>\n            </itext>\n            <instance>\n                <external_select_10 id=\"external_select_10\">\n                    <first/>\n                    <second/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </external_select_10>\n            </instance>\n            <instance id=\"first\">\n                <root>\n                    <item>\n                        <itextId>static_instance-first-0</itextId>\n                        <name>a</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-first-1</itextId>\n                        <name>b</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-first-2</itextId>\n                        <name>c</name>\n                    </item>\n                </root>\n            </instance>\n            <instance id=\"second\" src=\"jr://file/external_data_10.xml\">\n            </instance>\n            <bind nodeset=\"/external_select_10/first\" type=\"select1\"/>\n            <bind nodeset=\"/external_select_10/second\" type=\"select1\"/>\n            <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/external_select_10/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <select1 ref=\"/external_select_10/first\">\n            <label>First</label>\n            <item>\n                <label>a</label>\n                <value>a</value>\n            </item>\n            <item>\n                <label>b</label>\n                <value>b</value>\n            </item>\n            <item>\n                <label>c</label>\n                <value>c</value>\n            </item>\n        </select1>\n        <select1 ref=\"/external_select_10/second\">\n            <label>Second</label>\n            <itemset nodeset=\"instance('second')/root/item[first= /external_select_10/first ]\">\n                <value ref=\"name\"/>\n                <label ref=\"jr:itext(itextId)\"/>\n            </itemset>\n        </select1>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/external_select_csv.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>external select</h:title>\n        <model>\n            <instance>\n                <external_select id=\"external_select\">\n                    <first/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </external_select>\n            </instance>\n            <instance id=\"csv\" src=\"jr://file-csv/external_data.csv\"/>\n            <bind nodeset=\"/external_select/first\" type=\"select1\"/>\n            <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/external_select/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <select1 ref=\"/external_select/first\">\n            <label>First</label>\n            <itemset nodeset=\"instance('csv')/root/item\">\n                <value ref=\"name\"/>\n                <label ref=\"label\"/>\n            </itemset>\n        </select1>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/field-list-repeat.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>Field-List Repeat</h:title>\n        <model>\n            <instance>\n                <data id=\"field_list_repeat\">\n                    <person>\n                        <age/>\n                        <name/>\n                    </person>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/person/age\" type=\"int\"/>\n            <bind nodeset=\"/data/person/name\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group ref=\"/data/person\">\n            <label>Person</label>\n            <repeat nodeset=\"/data/person\" appearance=\"field-list\">\n                <input ref=\"/data/person/age\">\n                    <label>What is their age?</label>\n                </input>\n                <input ref=\"/data/person/name\">\n                    <label>What is their name?</label>\n                </input>\n            </repeat>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/fieldlist-updates.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head>\n    <h:title>fieldlist-updates</h:title>\n    <model>\n      <itext>\n        <translation default=\"true()\" lang=\"default\">\n          <text id=\"/fieldlist-updates/guidance/target11:hint\">\n            <value form=\"guidance\"><output value=\" /fieldlist-updates/guidance/calculate11 \"/></value>\n          </text>\n          <text id=\"static_instance-level1-0\">\n            <value>A</value>\n          </text>\n          <text id=\"static_instance-level1-1\">\n            <value>B</value>\n          </text>\n          <text id=\"static_instance-level1-2\">\n            <value>C</value>\n          </text>\n          <text id=\"static_instance-level2-0\">\n            <value>A1</value>\n          </text>\n          <text id=\"static_instance-level2-1\">\n            <value>A2</value>\n          </text>\n          <text id=\"static_instance-level2-2\">\n            <value>A3</value>\n          </text>\n          <text id=\"static_instance-level2-3\">\n            <value>B1</value>\n          </text>\n          <text id=\"static_instance-level2-4\">\n            <value>B2</value>\n          </text>\n          <text id=\"static_instance-level2-5\">\n            <value>B3</value>\n          </text>\n          <text id=\"static_instance-level2-6\">\n            <value>C1</value>\n          </text>\n          <text id=\"static_instance-level2-7\">\n            <value>C2</value>\n          </text>\n          <text id=\"static_instance-level2-8\">\n            <value>C3</value>\n          </text>\n          <text id=\"static_instance-level2-9\">\n            <value>C4</value>\n          </text>\n          <text id=\"static_instance-level3-0\">\n            <value>A1A</value>\n          </text>\n          <text id=\"static_instance-level3-1\">\n            <value>A1B</value>\n          </text>\n          <text id=\"static_instance-level3-2\">\n            <value>B1A</value>\n          </text>\n          <text id=\"static_instance-level3-3\">\n            <value>B1B</value>\n          </text>\n        </translation>\n      </itext>\n      <instance>\n        <fieldlist-updates id=\"fieldlist-updates\">\n          <single_relevance_at_end>\n            <source1/>\n            <target1/>\n          </single_relevance_at_end>\n          <single_relevance_at_beginning>\n            <target2/>\n            <source2/>\n          </single_relevance_at_beginning>\n          <single_relevance_in_middle>\n            <source3/>\n            <target3/>\n            <filler3/>\n          </single_relevance_in_middle>\n          <label_change>\n            <target4/>\n            <source4/>\n          </label_change>\n          <hint_change>\n            <source5/>\n            <target5/>\n          </hint_change>\n          <value_change>\n            <source6/>\n            <target6/>\n            <target6_1/>\n          </value_change>\n          <cascading_select>\n            <source7/>\n            <target7/>\n            <target7-1/>\n          </cascading_select>\n          <cascading_select_minimal>\n            <source8/>\n            <target8/>\n            <target8-1/>\n          </cascading_select_minimal>\n          <push_off_screen>\n            <relevance_group>\n              <target9-1/>\n              <target9-2/>\n              <target9-3/>\n              <target9-4/>\n              <target9-5/>\n              <target9-6/>\n              <target9-7/>\n              <target9-8/>\n              <target9-9/>\n              <target9-10/>\n              <target9-11/>\n              <target9-12/>\n              <target9-13/>\n              <target9-14/>\n              <target9-15/>\n            </relevance_group>\n            <source9/>\n          </push_off_screen>\n          <push_off_screen_binary>\n            <relevance_group2>\n              <target10-1/>\n              <target10-2/>\n              <target10-3/>\n              <target10-4/>\n              <target10-5/>\n              <target10-6/>\n              <target10-7/>\n              <target10-8/>\n              <target10-9/>\n              <target10-10/>\n              <target10-11/>\n              <target10-12/>\n              <target10-13/>\n              <target10-14/>\n              <target10-15/>\n            </relevance_group2>\n            <source10/>\n          </push_off_screen_binary>\n          <guidance>\n            <source11/>\n            <calculate11/>\n            <target11/>\n          </guidance>\n          <date-time>\n            <source12/>\n            <target12/>\n          </date-time>\n          <rating>\n            <source13/>\n            <target13/>\n          </rating>\n          <external_app>\n            <source14/>\n            <target14/>\n          </external_app>\n          <search_in_field_list_group>\n            <source15/>\n            <target15/>\n          </search_in_field_list_group>\n          <long_list_of_questions>\n            <question1/>\n            <question2/>\n            <question3/>\n            <question4/>\n            <question5/>\n            <question6/>\n            <question7/>\n            <question8/>\n            <question9/>\n            <question10/>\n            <question11/>\n            <question12/>\n            <question13/>\n            <question14/>\n            <question15/>\n            <question16/>\n            <question17/>\n            <question18/>\n            <question19/>\n            <question20/>\n          </long_list_of_questions>\n          <audio>\n            <source16/>\n            <target16/>\n          </audio>\n          <meta>\n            <instanceID/>\n          </meta>\n        </fieldlist-updates>\n      </instance>\n      <instance id=\"level1\">\n        <root>\n          <item>\n            <itextId>static_instance-level1-0</itextId>\n            <name>a</name>\n          </item>\n          <item>\n            <itextId>static_instance-level1-1</itextId>\n            <name>b</name>\n          </item>\n          <item>\n            <itextId>static_instance-level1-2</itextId>\n            <name>c</name>\n          </item>\n        </root>\n      </instance>\n      <instance id=\"level2\">\n        <root>\n          <item>\n            <itextId>static_instance-level2-0</itextId>\n            <level1>a</level1>\n            <name>a1</name>\n          </item>\n          <item>\n            <itextId>static_instance-level2-1</itextId>\n            <level1>a</level1>\n            <name>a2</name>\n          </item>\n          <item>\n            <itextId>static_instance-level2-2</itextId>\n            <level1>a</level1>\n            <name>a3</name>\n          </item>\n          <item>\n            <itextId>static_instance-level2-3</itextId>\n            <level1>b</level1>\n            <name>b1</name>\n          </item>\n          <item>\n            <itextId>static_instance-level2-4</itextId>\n            <level1>b</level1>\n            <name>b2</name>\n          </item>\n          <item>\n            <itextId>static_instance-level2-5</itextId>\n            <level1>b</level1>\n            <name>b3</name>\n          </item>\n          <item>\n            <itextId>static_instance-level2-6</itextId>\n            <level1>c</level1>\n            <name>c1</name>\n          </item>\n          <item>\n            <itextId>static_instance-level2-7</itextId>\n            <level1>c</level1>\n            <name>c2</name>\n          </item>\n          <item>\n            <itextId>static_instance-level2-8</itextId>\n            <level1>c</level1>\n            <name>c3</name>\n          </item>\n          <item>\n            <itextId>static_instance-level2-9</itextId>\n            <level1>c</level1>\n            <name>c4</name>\n          </item>\n        </root>\n      </instance>\n      <instance id=\"level3\">\n        <root>\n          <item>\n            <itextId>static_instance-level3-0</itextId>\n            <name>a1a</name>\n            <level2>a1</level2>\n          </item>\n          <item>\n            <itextId>static_instance-level3-1</itextId>\n            <name>a1b</name>\n            <level2>a1</level2>\n          </item>\n          <item>\n            <itextId>static_instance-level3-2</itextId>\n            <name>b1a</name>\n            <level2>b1</level2>\n          </item>\n          <item>\n            <itextId>static_instance-level3-3</itextId>\n            <name>b1b</name>\n            <level2>b1</level2>\n          </item>\n        </root>\n      </instance>\n      <bind nodeset=\"/fieldlist-updates/single_relevance_at_end/source1\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/single_relevance_at_end/target1\" relevant=\"string-length( /fieldlist-updates/single_relevance_at_end/source1 ) &gt; 0\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/single_relevance_at_beginning/target2\" relevant=\"string-length( /fieldlist-updates/single_relevance_at_beginning/source2 ) &gt; 0\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/single_relevance_at_beginning/source2\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/single_relevance_in_middle/source3\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/single_relevance_in_middle/target3\" relevant=\"string-length( /fieldlist-updates/single_relevance_in_middle/source3 ) &gt; 0\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/single_relevance_in_middle/filler3\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/label_change/target4\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/label_change/source4\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/hint_change/source5\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/hint_change/target5\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/value_change/source6\" type=\"string\"/>\n      <bind calculate=\"string-length( /fieldlist-updates/value_change/source6 )\" nodeset=\"/fieldlist-updates/value_change/target6\" type=\"int\"/>\n      <bind calculate=\"substr( /fieldlist-updates/value_change/source6, 0, 1 )\" nodeset=\"/fieldlist-updates/value_change/target6_1\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/cascading_select/source7\" type=\"select1\"/>\n      <bind nodeset=\"/fieldlist-updates/cascading_select/target7\" type=\"select1\"/>\n      <bind nodeset=\"/fieldlist-updates/cascading_select/target7-1\" type=\"select1\"/>\n      <bind nodeset=\"/fieldlist-updates/cascading_select_minimal/source8\" type=\"select1\"/>\n      <bind nodeset=\"/fieldlist-updates/cascading_select_minimal/target8\" type=\"select1\"/>\n      <bind nodeset=\"/fieldlist-updates/cascading_select_minimal/target8-1\" type=\"select1\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group\" relevant=\"string-length( /fieldlist-updates/push_off_screen/source9 ) &gt; 0\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-1\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-2\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-3\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-4\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-5\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-6\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-7\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-8\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-9\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-10\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-11\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-12\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-13\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-14\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/relevance_group/target9-15\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen/source9\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2\" relevant=\" /fieldlist-updates/push_off_screen_binary/source10  != ''\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-1\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-2\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-3\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-4\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-5\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-6\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-7\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-8\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-9\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-10\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-11\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-12\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-13\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-14\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-15\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/push_off_screen_binary/source10\" type=\"binary\"/>\n      <bind nodeset=\"/fieldlist-updates/guidance/source11\" type=\"int\"/>\n      <bind calculate=\" /fieldlist-updates/guidance/source11  * 2\" nodeset=\"/fieldlist-updates/guidance/calculate11\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/guidance/target11\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/date-time/source12\" type=\"dateTime\"/>\n      <bind nodeset=\"/fieldlist-updates/date-time/target12\" relevant=\" /fieldlist-updates/date-time/source12  != ''\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/rating/source13\" type=\"int\"/>\n      <bind nodeset=\"/fieldlist-updates/rating/target13\" relevant=\" /fieldlist-updates/rating/source13  != ''\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/external_app/source14\" type=\"int\"/>\n      <bind nodeset=\"/fieldlist-updates/external_app/target14\" relevant=\" /fieldlist-updates/external_app/source14  != ''\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/search_in_field_list_group/source15\" type=\"select1\"/>\n      <bind nodeset=\"/fieldlist-updates/search_in_field_list_group/target15\" relevant=\" /fieldlist-updates/search_in_field_list_group/source15  != ''\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question1\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question2\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question3\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question4\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question5\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question6\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question7\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question8\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question9\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question10\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question11\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question12\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question13\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question14\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question15\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question16\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question17\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question18\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question19\" type=\"string\"/>\n      <bind nodeset=\"/fieldlist-updates/long_list_of_questions/question20\" type=\"integer\"/>\n      <bind nodeset=\"/fieldlist-updates/audio/source16\" type=\"binary\"/>\n      <bind nodeset=\"/fieldlist-updates/audio/target16\" relevant=\" /fieldlist-updates/audio/source16  != ''\" type=\"string\"/>\n      <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/fieldlist-updates/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n    </model>\n  </h:head>\n  <h:body>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/single_relevance_at_end\">\n      <label>Single relevance at end</label>\n      <input ref=\"/fieldlist-updates/single_relevance_at_end/source1\">\n        <label>Source1</label>\n      </input>\n      <input ref=\"/fieldlist-updates/single_relevance_at_end/target1\">\n        <label>Target1</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/single_relevance_at_beginning\">\n      <label>Single relevance at beginning</label>\n      <input ref=\"/fieldlist-updates/single_relevance_at_beginning/target2\">\n        <label>Target2</label>\n      </input>\n      <input ref=\"/fieldlist-updates/single_relevance_at_beginning/source2\">\n        <label>Source2</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/single_relevance_in_middle\">\n      <label>Single relevance in middle</label>\n      <input ref=\"/fieldlist-updates/single_relevance_in_middle/source3\">\n        <label>Source3</label>\n      </input>\n      <input ref=\"/fieldlist-updates/single_relevance_in_middle/target3\">\n        <label>Target3</label>\n      </input>\n      <input ref=\"/fieldlist-updates/single_relevance_in_middle/filler3\">\n        <label>Filler3</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/label_change\">\n      <label>Label change</label>\n      <input ref=\"/fieldlist-updates/label_change/target4\">\n        <label> Hello, <output value=\" /fieldlist-updates/label_change/source4 \"/>, how are you today? </label>\n      </input>\n      <input ref=\"/fieldlist-updates/label_change/source4\">\n        <label>What is your name?</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/hint_change\">\n      <label>Hint change</label>\n      <input ref=\"/fieldlist-updates/hint_change/source5\">\n        <label>What is your name?</label>\n      </input>\n      <input ref=\"/fieldlist-updates/hint_change/target5\">\n        <label>What is 2 + 2?</label>\n        <hint> Please don't use your calculator, <output value=\" /fieldlist-updates/hint_change/source5 \"/>! </hint>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/value_change\">\n      <label>Value change</label>\n      <input ref=\"/fieldlist-updates/value_change/source6\">\n        <label>What is your name?</label>\n      </input>\n      <input ref=\"/fieldlist-updates/value_change/target6\">\n        <label>Name length</label>\n      </input>\n      <input ref=\"/fieldlist-updates/value_change/target6_1\">\n        <label>First name letter</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/cascading_select\">\n      <label>Cascading select</label>\n      <select1 ref=\"/fieldlist-updates/cascading_select/source7\">\n        <label>Level1</label>\n        <item>\n          <label>A</label>\n          <value>a</value>\n        </item>\n        <item>\n          <label>B</label>\n          <value>b</value>\n        </item>\n        <item>\n          <label>C</label>\n          <value>c</value>\n        </item>\n      </select1>\n      <select1 ref=\"/fieldlist-updates/cascading_select/target7\">\n        <label>Level2</label>\n        <itemset nodeset=\"instance('level2')/root/item[level1 =  /fieldlist-updates/cascading_select/source7 ]\">\n          <value ref=\"name\"/>\n          <label ref=\"jr:itext(itextId)\"/>\n        </itemset>\n      </select1>\n      <select1 ref=\"/fieldlist-updates/cascading_select/target7-1\">\n        <label>Level3</label>\n        <itemset nodeset=\"instance('level3')/root/item[level2 =  /fieldlist-updates/cascading_select/target7 ]\">\n          <value ref=\"name\"/>\n          <label ref=\"jr:itext(itextId)\"/>\n        </itemset>\n      </select1>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/cascading_select_minimal\">\n      <label>Cascading select minimal</label>\n      <select1 appearance=\"minimal\" ref=\"/fieldlist-updates/cascading_select_minimal/source8\">\n        <label>Level1</label>\n        <item>\n          <label>A</label>\n          <value>a</value>\n        </item>\n        <item>\n          <label>B</label>\n          <value>b</value>\n        </item>\n        <item>\n          <label>C</label>\n          <value>c</value>\n        </item>\n      </select1>\n      <select1 appearance=\"minimal\" ref=\"/fieldlist-updates/cascading_select_minimal/target8\">\n        <label>Level2</label>\n        <itemset nodeset=\"instance('level2')/root/item[level1 =  /fieldlist-updates/cascading_select_minimal/source8 ]\">\n          <value ref=\"name\"/>\n          <label ref=\"jr:itext(itextId)\"/>\n        </itemset>\n      </select1>\n      <select1 appearance=\"minimal\" ref=\"/fieldlist-updates/cascading_select_minimal/target8-1\">\n        <label>Level3</label>\n        <itemset nodeset=\"instance('level3')/root/item[level2 =  /fieldlist-updates/cascading_select_minimal/target8 ]\">\n          <value ref=\"name\"/>\n          <label ref=\"jr:itext(itextId)\"/>\n        </itemset>\n      </select1>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/push_off_screen\">\n      <label>Push off screen</label>\n      <group ref=\"/fieldlist-updates/push_off_screen/relevance_group\">\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-1\">\n          <label>Target9-1</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-2\">\n          <label>Target9-2</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-3\">\n          <label>Target9-3</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-4\">\n          <label>Target9-4</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-5\">\n          <label>Target9-5</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-6\">\n          <label>Target9-6</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-7\">\n          <label>Target9-7</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-8\">\n          <label>Target9-8</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-9\">\n          <label>Target9-9</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-10\">\n          <label>Target9-10</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-11\">\n          <label>Target9-11</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-12\">\n          <label>Target9-12</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-13\">\n          <label>Target9-13</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-14\">\n          <label>Target9-14</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen/relevance_group/target9-15\">\n          <label>Target9-15</label>\n        </input>\n      </group>\n      <input ref=\"/fieldlist-updates/push_off_screen/source9\">\n        <label>Source9</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/push_off_screen_binary\">\n      <label>Push off screen binary</label>\n      <group ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2\">\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-1\">\n          <label>Target10-1</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-2\">\n          <label>Target10-2</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-3\">\n          <label>Target10-3</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-4\">\n          <label>Target10-4</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-5\">\n          <label>Target10-5</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-6\">\n          <label>Target10-6</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-7\">\n          <label>Target10-7</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-8\">\n          <label>Target10-8</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-9\">\n          <label>Target10-9</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-10\">\n          <label>Target10-10</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-11\">\n          <label>Target10-11</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-12\">\n          <label>Target10-12</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-13\">\n          <label>Target10-13</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-14\">\n          <label>Target10-14</label>\n        </input>\n        <input ref=\"/fieldlist-updates/push_off_screen_binary/relevance_group2/target10-15\">\n          <label>Target10-15</label>\n        </input>\n      </group>\n      <upload mediatype=\"image/*\" ref=\"/fieldlist-updates/push_off_screen_binary/source10\">\n        <label>Source10</label>\n      </upload>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/guidance\">\n      <label>Guidance hint</label>\n      <input ref=\"/fieldlist-updates/guidance/source11\">\n        <label>Source11</label>\n      </input>\n      <input ref=\"/fieldlist-updates/guidance/target11\">\n        <label>Target11</label>\n        <hint ref=\"jr:itext('/fieldlist-updates/guidance/target11:hint')\"/>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/date-time\">\n      <label>Date time</label>\n      <input ref=\"/fieldlist-updates/date-time/source12\">\n        <label>Source12</label>\n      </input>\n      <input ref=\"/fieldlist-updates/date-time/target12\">\n        <label>Target12</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/rating\">\n      <label>Rating</label>\n      <range appearance=\"rating\" end=\"10\" ref=\"/fieldlist-updates/rating/source13\" start=\"1\" step=\"1\">\n        <label>Source13</label>\n      </range>\n      <input ref=\"/fieldlist-updates/rating/target13\">\n        <label>Target13</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/external_app\">\n      <label>External app</label>\n      <input appearance=\"ex:org.fake.app(fake=&quot;fake&quot;)\" ref=\"/fieldlist-updates/external_app/source14\">\n        <label>Source14</label>\n      </input>\n      <input ref=\"/fieldlist-updates/external_app/target14\">\n        <label>Target14</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/search_in_field_list_group\">\n      <label>Search in field-list</label>\n      <select1 appearance=\"minimal search('fruits')\" ref=\"/fieldlist-updates/search_in_field_list_group/source15\">\n        <label>Source15</label>\n        <item>\n          <label>name</label>\n          <value>name_key</value>\n        </item>\n      </select1>\n      <input ref=\"/fieldlist-updates/search_in_field_list_group/target15\">\n        <label>Target15</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/long_list_of_questions\">\n      <label>Long list of questions</label>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question1\">\n        <label>Question1</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question2\">\n        <label>Question2</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question3\">\n        <label>Question3</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question4\">\n        <label>Question4</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question5\">\n        <label>Question5</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question6\">\n        <label>Question6</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question7\">\n        <label>Question7</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question8\">\n        <label>Question8</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question9\">\n        <label>Question9</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question10\">\n        <label>Question10</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question11\">\n        <label>Question11</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question12\">\n        <label>Question12</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question13\">\n        <label>Question13</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question14\">\n        <label>Question14</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question15\">\n        <label>Question15</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question16\">\n        <label>Question16</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question17\">\n        <label>Question17</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question18\">\n        <label>Question18</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question19\">\n        <label>Question19</label>\n      </input>\n      <input ref=\"/fieldlist-updates/long_list_of_questions/question20\">\n        <label>Question20</label>\n      </input>\n    </group>\n    <group appearance=\"field-list\" ref=\"/fieldlist-updates/audio\">\n      <label>Audio</label>\n      <upload mediatype=\"audio/*\" ref=\"/fieldlist-updates/audio/source16\">\n        <label>Source16</label>\n      </upload>\n      <input ref=\"/fieldlist-updates/audio/target16\">\n        <label>Target16</label>\n      </input>\n    </group>\n  </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/fixed-count-repeat.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\"\n    xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>Fixed Count Repeat</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <itext>\n                <translation lang=\"English (en)\">\n                    <text id=\"/data/person:label\">\n                        <value>Person</value>\n                    </text>\n                    <text id=\"/data/person/age:label\">\n                        <value>What is their age?</value>\n                    </text>\n                </translation>\n            </itext>\n            <instance>\n                <data id=\"Fixed Repeat\">\n                    <person_count />\n                    <person jr:template=\"\">\n                        <age />\n                    </person>\n                    <person>\n                        <age />\n                    </person>\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </data>\n            </instance>\n            <bind calculate=\"2\" nodeset=\"/data/person_count\" readonly=\"true()\" type=\"string\" />\n            <bind nodeset=\"/data/person/age\" type=\"int\" />\n            <bind nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"\n                jr:preload=\"uid\" />\n        </model>\n    </h:head>\n    <h:body>\n        <group ref=\"/data/person\">\n            <label ref=\"jr:itext('/data/person:label')\" />\n            <repeat nodeset=\"/data/person\" jr:count=\" /data/person_count \">\n                <input ref=\"/data/person/age\">\n                    <label ref=\"jr:itext('/data/person/age:label')\" />\n                </input>\n            </repeat>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/form1.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>form1</h:title>\n        <model>\n            <instance>\n                <data id=\"build_form1_1517264936\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <G1>\n                        <G2>\n                            <T1/>\n                            <T2/>\n                            <T3/>\n                        </G2>\n                    </G1>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/G1:label\">\n                        <value>G1</value>\n                    </text>\n                    <text id=\"/data/G1/G2:label\">\n                        <value>G2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T1:label\">\n                        <value>T1</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T2:label\">\n                        <value>T2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T3:label\">\n                        <value>T3</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/G1/G2/T1\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T2\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T3\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\">\n            <label ref=\"jr:itext('/data/G1:label')\"/>\n            <group>\n                <label ref=\"jr:itext('/data/G1/G2:label')\"/>\n                <input ref=\"/data/G1/G2/T1\">\n                    <label ref=\"jr:itext('/data/G1/G2/T1:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T2\">\n                    <label ref=\"jr:itext('/data/G1/G2/T2:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T3\">\n                    <label ref=\"jr:itext('/data/G1/G2/T3:label')\"/>\n                </input>\n            </group>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/form2.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>form2</h:title>\n        <model>\n            <instance>\n                <data id=\"build_form2_1517303824\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <G1>\n                        <G2>\n                            <T1/>\n                            <T2/>\n                            <T3/>\n                        </G2>\n                    </G1>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/G1:label\">\n                        <value>G1</value>\n                    </text>\n                    <text id=\"/data/G1/G2:label\">\n                        <value>G2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T1:label\">\n                        <value>T1</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T2:label\">\n                        <value>T2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T3:label\">\n                        <value>T3</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/G1/G2/T1\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T2\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T3\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\">\n            <label ref=\"jr:itext('/data/G1:label')\"/>\n            <group appearance=\"field-list\">\n                <label ref=\"jr:itext('/data/G1/G2:label')\"/>\n                <input ref=\"/data/G1/G2/T1\">\n                    <label ref=\"jr:itext('/data/G1/G2/T1:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T2\">\n                    <label ref=\"jr:itext('/data/G1/G2/T2:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T3\">\n                    <label ref=\"jr:itext('/data/G1/G2/T3:label')\"/>\n                </input>\n            </group>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/form3.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>form3</h:title>\n        <model>\n            <instance>\n                <data id=\"build_form3_1517303930\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <G1>\n                        <G2>\n                            <T1/>\n                            <T2/>\n                            <T3/>\n                        </G2>\n                    </G1>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/G1:label\">\n                        <value>G1</value>\n                    </text>\n                    <text id=\"/data/G1/G2:label\">\n                        <value>G2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T1:label\">\n                        <value>T1</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T2:label\">\n                        <value>T2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T3:label\">\n                        <value>T3</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/G1/G2/T1\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T2\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T3\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group>\n            <label ref=\"jr:itext('/data/G1:label')\"/>\n            <group appearance=\"field-list\">\n                <label ref=\"jr:itext('/data/G1/G2:label')\"/>\n                <input ref=\"/data/G1/G2/T1\">\n                    <label ref=\"jr:itext('/data/G1/G2/T1:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T2\">\n                    <label ref=\"jr:itext('/data/G1/G2/T2:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T3\">\n                    <label ref=\"jr:itext('/data/G1/G2/T3:label')\"/>\n                </input>\n            </group>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/form4.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>form4</h:title>\n        <model>\n            <instance>\n                <data id=\"build_form4_1517307219\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <G1>\n                        <G2>\n                            <T1/>\n                            <T2/>\n                            <T3/>\n                        </G2>\n                    </G1>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/G1:label\">\n                        <value>G1</value>\n                    </text>\n                    <text id=\"/data/G1/G2:label\">\n                        <value>G2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T1:label\">\n                        <value>T1</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T2:label\">\n                        <value>T2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T3:label\">\n                        <value>T3</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/G1/G2/T1\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T2\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T3\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group>\n            <label ref=\"jr:itext('/data/G1:label')\"/>\n            <group>\n                <label ref=\"jr:itext('/data/G1/G2:label')\"/>\n                <input ref=\"/data/G1/G2/T1\">\n                    <label ref=\"jr:itext('/data/G1/G2/T1:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T2\">\n                    <label ref=\"jr:itext('/data/G1/G2/T2:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T3\">\n                    <label ref=\"jr:itext('/data/G1/G2/T3:label')\"/>\n                </input>\n            </group>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/form5.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>form5</h:title>\n        <model>\n            <instance>\n                <data id=\"build_form4_1517307426\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <G1>\n                        <T1/>\n                        <G2>\n                            <T2/>\n                            <T3/>\n                        </G2>\n                    </G1>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/G1:label\">\n                        <value>G1</value>\n                    </text>\n                    <text id=\"/data/G1/T1:label\">\n                        <value>T1</value>\n                    </text>\n                    <text id=\"/data/G1/G2:label\">\n                        <value>G2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T2:label\">\n                        <value>T2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T3:label\">\n                        <value>T3</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/G1/T1\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T2\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T3\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group>\n            <label ref=\"jr:itext('/data/G1:label')\"/>\n            <input ref=\"/data/G1/T1\">\n                <label ref=\"jr:itext('/data/G1/T1:label')\"/>\n            </input>\n            <group>\n                <label ref=\"jr:itext('/data/G1/G2:label')\"/>\n                <input ref=\"/data/G1/G2/T2\">\n                    <label ref=\"jr:itext('/data/G1/G2/T2:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T3\">\n                    <label ref=\"jr:itext('/data/G1/G2/T3:label')\"/>\n                </input>\n            </group>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/form6.xml",
    "content": "\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>form6</h:title>\n        <model>\n            <instance>\n                <data id=\"build_form6_1517307617\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <G1>\n                        <T1/>\n                        <G2>\n                            <T2/>\n                        </G2>\n                        <T3/>\n                    </G1>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/G1:label\">\n                        <value>G1</value>\n                    </text>\n                    <text id=\"/data/G1/T1:label\">\n                        <value>T1</value>\n                    </text>\n                    <text id=\"/data/G1/G2:label\">\n                        <value>G2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T2:label\">\n                        <value>T2</value>\n                    </text>\n                    <text id=\"/data/G1/T3:label\">\n                        <value>T3</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/G1/T1\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T2\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/T3\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group>\n            <label ref=\"jr:itext('/data/G1:label')\"/>\n            <input ref=\"/data/G1/T1\">\n                <label ref=\"jr:itext('/data/G1/T1:label')\"/>\n            </input>\n            <group>\n                <label ref=\"jr:itext('/data/G1/G2:label')\"/>\n                <input ref=\"/data/G1/G2/T2\">\n                    <label ref=\"jr:itext('/data/G1/G2/T2:label')\"/>\n                </input>\n            </group>\n            <input ref=\"/data/G1/T3\">\n                <label ref=\"jr:itext('/data/G1/T3:label')\"/>\n            </input>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/form7.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>form7</h:title>\n        <model>\n            <instance>\n                <data id=\"build_form7_1517307808\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <G1>\n                        <T1/>\n                        <G2>\n                            <T2/>\n                            <T3/>\n                        </G2>\n                    </G1>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/G1:label\">\n                        <value>G1</value>\n                    </text>\n                    <text id=\"/data/G1/T1:label\">\n                        <value>T1</value>\n                    </text>\n                    <text id=\"/data/G1/G2:label\">\n                        <value>G2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T2:label\">\n                        <value>T2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T3:label\">\n                        <value>T3</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/G1/T1\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T2\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T3\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\">\n            <label ref=\"jr:itext('/data/G1:label')\"/>\n            <input ref=\"/data/G1/T1\">\n                <label ref=\"jr:itext('/data/G1/T1:label')\"/>\n            </input>\n            <group>\n                <label ref=\"jr:itext('/data/G1/G2:label')\"/>\n                <input ref=\"/data/G1/G2/T2\">\n                    <label ref=\"jr:itext('/data/G1/G2/T2:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T3\">\n                    <label ref=\"jr:itext('/data/G1/G2/T3:label')\"/>\n                </input>\n            </group>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/form8.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>form8</h:title>\n        <model>\n            <instance>\n                <data id=\"build_form8_1517310417\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <G1>\n                        <T1/>\n                        <G2>\n                            <T2/>\n                            <T3/>\n                        </G2>\n                        <T4/>\n                        <G3>\n                            <T5/>\n                            <T6/>\n                        </G3>\n                        <T7/>\n                    </G1>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/G1:label\">\n                        <value>G1</value>\n                    </text>\n                    <text id=\"/data/G1/T1:label\">\n                        <value>T1</value>\n                    </text>\n                    <text id=\"/data/G1/G2:label\">\n                        <value>G2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T2:label\">\n                        <value>T2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T3:label\">\n                        <value>T3</value>\n                    </text>\n                    <text id=\"/data/G1/T4:label\">\n                        <value>T4</value>\n                    </text>\n                    <text id=\"/data/G1/G3:label\">\n                        <value>G3</value>\n                    </text>\n                    <text id=\"/data/G1/G3/T5:label\">\n                        <value>T5</value>\n                    </text>\n                    <text id=\"/data/G1/G3/T6:label\">\n                        <value>T6</value>\n                    </text>\n                    <text id=\"/data/G1/T7:label\">\n                        <value>T7</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/G1/T1\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T2\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T3\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/T4\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G3/T5\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G3/T6\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/T7\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\">\n            <label ref=\"jr:itext('/data/G1:label')\"/>\n            <input ref=\"/data/G1/T1\">\n                <label ref=\"jr:itext('/data/G1/T1:label')\"/>\n            </input>\n            <group>\n                <label ref=\"jr:itext('/data/G1/G2:label')\"/>\n                <input ref=\"/data/G1/G2/T2\">\n                    <label ref=\"jr:itext('/data/G1/G2/T2:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T3\">\n                    <label ref=\"jr:itext('/data/G1/G2/T3:label')\"/>\n                </input>\n            </group>\n            <input ref=\"/data/G1/T4\">\n                <label ref=\"jr:itext('/data/G1/T4:label')\"/>\n            </input>\n            <group>\n                <label ref=\"jr:itext('/data/G1/G3:label')\"/>\n                <input ref=\"/data/G1/G3/T5\">\n                    <label ref=\"jr:itext('/data/G1/G3/T5:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G3/T6\">\n                    <label ref=\"jr:itext('/data/G1/G3/T6:label')\"/>\n                </input>\n            </group>\n            <input ref=\"/data/G1/T7\">\n                <label ref=\"jr:itext('/data/G1/T7:label')\"/>\n            </input>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/form9.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>form9</h:title>\n        <model>\n            <instance>\n                <data id=\"build_form9_1517311390\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <G1>\n                        <T1/>\n                        <G2>\n                            <T2/>\n                            <T3/>\n                            <G3>\n                                <T5/>\n                                <T6/>\n                            </G3>\n                        </G2>\n                        <T4/>\n                        <T7/>\n                    </G1>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/G1:label\">\n                        <value>G1</value>\n                    </text>\n                    <text id=\"/data/G1/T1:label\">\n                        <value>T1</value>\n                    </text>\n                    <text id=\"/data/G1/G2:label\">\n                        <value>G2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T2:label\">\n                        <value>T2</value>\n                    </text>\n                    <text id=\"/data/G1/G2/T3:label\">\n                        <value>T3</value>\n                    </text>\n                    <text id=\"/data/G1/G2/G3:label\">\n                        <value>G3</value>\n                    </text>\n                    <text id=\"/data/G1/G2/G3/T5:label\">\n                        <value>T5</value>\n                    </text>\n                    <text id=\"/data/G1/G2/G3/T6:label\">\n                        <value>T6</value>\n                    </text>\n                    <text id=\"/data/G1/T4:label\">\n                        <value>T4</value>\n                    </text>\n                    <text id=\"/data/G1/T7:label\">\n                        <value>T7</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/G1/T1\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T2\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/T3\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/G3/T5\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/G2/G3/T6\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/T4\" type=\"string\"/>\n            <bind nodeset=\"/data/G1/T7\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\">\n            <label ref=\"jr:itext('/data/G1:label')\"/>\n            <input ref=\"/data/G1/T1\">\n                <label ref=\"jr:itext('/data/G1/T1:label')\"/>\n            </input>\n            <group appearance=\"field-list\">\n                <label ref=\"jr:itext('/data/G1/G2:label')\"/>\n                <input ref=\"/data/G1/G2/T2\">\n                    <label ref=\"jr:itext('/data/G1/G2/T2:label')\"/>\n                </input>\n                <input ref=\"/data/G1/G2/T3\">\n                    <label ref=\"jr:itext('/data/G1/G2/T3:label')\"/>\n                </input>\n                <group appearance=\"field-list\">\n                    <label ref=\"jr:itext('/data/G1/G2/G3:label')\"/>\n                    <input ref=\"/data/G1/G2/G3/T5\">\n                        <label ref=\"jr:itext('/data/G1/G2/G3/T5:label')\"/>\n                    </input>\n                    <input ref=\"/data/G1/G2/G3/T6\">\n                        <label ref=\"jr:itext('/data/G1/G2/G3/T6:label')\"/>\n                    </input>\n                </group>\n            </group>\n            <input ref=\"/data/G1/T4\">\n                <label ref=\"jr:itext('/data/G1/T4:label')\"/>\n            </input>\n            <input ref=\"/data/G1/T7\">\n                <label ref=\"jr:itext('/data/G1/T7:label')\"/>\n            </input>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/formHierarchy1.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>formHierarchy1</h:title>\n        <model>\n            <instance>\n                <formHierarchy1 id=\"formHierarchy1\">\n                    <name>\n                        <first/>\n                    </name>\n                    <age/>\n                </formHierarchy1>\n            </instance>\n            <bind nodeset=\"name/first\" type=\"int\"/>\n            <bind nodeset=\"age\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"name/first\">\n            <label>what is your name?</label>\n        </input>\n        <input ref=\"age\">\n            <label>what is your age?</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/formHierarchy2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>formHierarchy2</h:title>\n        <model>\n            <instance>\n                <formHierarchy2 id=\"formHierarchy2\">\n                    <number_in_party />\n                    <party_names_note />\n                    <guest_details_count />\n                    <guest_details jr:template=\"\">\n                        <guest_details_gr>\n                            <guest_name />\n                            <guest_dietary />\n                        </guest_details_gr>\n                    </guest_details>\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </formHierarchy2>\n            </instance>\n            <bind constraint=\". &lt;= 8\" nodeset=\"/formHierarchy2/number_in_party\" type=\"int\" />\n            <bind nodeset=\"/formHierarchy2/party_names_note\" readonly=\"true()\" type=\"string\" />\n            <bind calculate=\" /formHierarchy2/number_in_party \" nodeset=\"/formHierarchy2/guest_details_count\" readonly=\"true()\" type=\"string\" />\n            <bind nodeset=\"/formHierarchy2/guest_details/guest_details_gr\" relevant=\"position(..)&lt;= /formHierarchy2/number_in_party \" />\n            <bind nodeset=\"/formHierarchy2/guest_details/guest_details_gr/guest_name\" type=\"string\" />\n            <bind nodeset=\"/formHierarchy2/guest_details/guest_details_gr/guest_dietary\" type=\"string\" />\n            <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/formHierarchy2/meta/instanceID\" readonly=\"true()\" type=\"string\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/formHierarchy2/number_in_party\">\n            <label>How many guests are in your party?</label>\n        </input>\n        <input ref=\"/formHierarchy2/party_names_note\">\n            <label>Please provide details for each guest.</label>\n        </input>\n        <group ref=\"/formHierarchy2/guest_details\">\n            <label>Guest details</label>\n            <repeat jr:count=\" /formHierarchy2/guest_details_count \" nodeset=\"/formHierarchy2/guest_details\">\n                <group ref=\"/formHierarchy2/guest_details/guest_details_gr\">\n                    <input ref=\"/formHierarchy2/guest_details/guest_details_gr/guest_name\">\n                        <label>Guest's name</label>\n                    </input>\n                    <input ref=\"/formHierarchy2/guest_details/guest_details_gr/guest_dietary\">\n                        <label>Does this guest have any dietary restrictions?</label>\n                    </input>\n                </group>\n            </repeat>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/formHierarchy3.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>formHierarchy3</h:title>\n        <model>\n            <instance>\n                <formHierarchy3 id=\"formHierarchy3\">\n                    <intro />\n                    <group1>\n                        <text />\n                        <group1_1>\n                            <intereg1_1 />\n                            <integer1_2 />\n                        </group1_1>\n                        <group1_2>\n                            <integer2_1 />\n                            <integer2_2 />\n                        </group1_2>\n                    </group1>\n                    <repeatGroup1 jr:template=\"\">\n                        <barcode />\n                        <repeatGroup1_1 jr:template=\"\">\n                            <date />\n                        </repeatGroup1_1>\n                    </repeatGroup1>\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </formHierarchy3>\n            </instance>\n            <bind nodeset=\"/formHierarchy3/intro\" readonly=\"true()\" type=\"string\" />\n            <bind nodeset=\"/formHierarchy3/group1/text\" type=\"string\" />\n            <bind nodeset=\"/formHierarchy3/group1/group1_1/intereg1_1\" type=\"int\" />\n            <bind nodeset=\"/formHierarchy3/group1/group1_1/integer1_2\" type=\"int\" />\n            <bind nodeset=\"/formHierarchy3/group1/group1_2/integer2_1\" type=\"int\" />\n            <bind nodeset=\"/formHierarchy3/group1/group1_2/integer2_2\" type=\"int\" />\n            <bind nodeset=\"/formHierarchy3/repeatGroup1/barcode\" type=\"barcode\" />\n            <bind nodeset=\"/formHierarchy3/repeatGroup1/repeatGroup1_1/date\" type=\"date\" />\n            <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/formHierarchy3/meta/instanceID\" readonly=\"true()\" type=\"string\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/formHierarchy3/intro\">\n            <label>Intro</label>\n        </input>\n        <group ref=\"/formHierarchy3/group1\">\n            <label>Group 1</label>\n            <input ref=\"/formHierarchy3/group1/text\">\n                <label>Text</label>\n            </input>\n            <group ref=\"/formHierarchy3/group1/group1_1\">\n                <label>Group 1_1</label>\n                <input ref=\"/formHierarchy3/group1/group1_1/intereg1_1\">\n                    <label>Integer 1_1</label>\n                </input>\n                <input ref=\"/formHierarchy3/group1/group1_1/integer1_2\">\n                    <label>Integer 1_2</label>\n                </input>\n            </group>\n            <group ref=\"/formHierarchy3/group1/group1_2\">\n                <label>Group 1_2</label>\n                <input ref=\"/formHierarchy3/group1/group1_2/integer2_1\">\n                    <label>Integer 2_1</label>\n                </input>\n                <input ref=\"/formHierarchy3/group1/group1_2/integer2_2\">\n                    <label>Integer 2_2</label>\n                </input>\n            </group>\n        </group>\n        <group ref=\"/formHierarchy3/repeatGroup1\">\n            <label>Repeat Group 1</label>\n            <repeat nodeset=\"/formHierarchy3/repeatGroup1\">\n                <input ref=\"/formHierarchy3/repeatGroup1/barcode\">\n                    <label>Barcode</label>\n                </input>\n                <group ref=\"/formHierarchy3/repeatGroup1/repeatGroup1_1\">\n                    <label>Repeat Group 1_1</label>\n                    <repeat nodeset=\"/formHierarchy3/repeatGroup1/repeatGroup1_1\">\n                        <input ref=\"/formHierarchy3/repeatGroup1/repeatGroup1_1/date\">\n                            <label>Date</label>\n                        </input>\n                    </repeat>\n                </group>\n            </repeat>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/form_design_error.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>Relevance and calculate loop</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"form-list-relevance-issue\">\n                    <group>\n                        <name>A</name>\n                        <fullName/>\n                        <middleName>third</middleName>\n                    </group>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/group/name\" type=\"string\"/>\n            <!-- The form design issue is that calculates can't be used as dynamic defaults because they're reevaluated continuously -->\n            <bind calculate=\" /data/group/name \" nodeset=\"/data/group/fullName\" type=\"string\"/>\n            <bind nodeset=\"/data/group/middleName\" relevant=\" /data/group/name != /data/group/fullName \" type=\"string\"/>\n            <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\" ref=\"/data/group\">\n            <input ref=\"/data/group/name\">\n                <label>What is your first name</label>\n            </input>\n            <input ref=\"/data/group/fullName\">\n                <label>Please add your second name to have your full name</label>\n            </input>\n            <input ref=\"/data/group/middleName\">\n                <label>Please add your middle name if you have one</label>\n                <hint>Display if full name is different than first name</hint>\n            </input>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/form_styling.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>Form styling</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <itext>\n                <translation default=\"true()\" lang=\"default\">\n                    <text id=\"/data/q0:hint\">\n                        <value>&lt;span style=\"color:blue\"&gt;Hint text&lt;/span&gt;</value>\n                        <value form=\"guidance\">&lt;span style=\"color:blue\"&gt;Guidance text&lt;/span&gt;</value>\n                    </text>\n                </translation>\n            </itext>\n            <instance>\n                <data id=\"form_styling\">\n                    <q0 />\n                    <q1 />\n                    <selectOneQuestions>\n                        <q2 />\n                        <q3>one</q3>\n                        <q4>one</q4>\n                        <q5 />\n                        <q6 />\n                        <q7 />\n                    </selectOneQuestions>\n                    <selectMultipleQuestions>\n                        <q8 />\n                        <q9>one</q9>\n                        <q10>one</q10>\n                        <q11 />\n                        <q12 />\n                    </selectMultipleQuestions>\n                    <q13>three, two, one</q13>\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/q0\" readonly=\"true()\" type=\"string\" />\n            <bind nodeset=\"/data/q1\" type=\"string\" required=\"true()\"/>\n            <bind nodeset=\"/data/selectOneQuestions/q2\" type=\"string\" />\n            <bind nodeset=\"/data/selectOneQuestions/q3\" type=\"string\" />\n            <bind nodeset=\"/data/selectOneQuestions/q4\" type=\"string\" />\n            <bind nodeset=\"/data/selectOneQuestions/q5\" type=\"string\" />\n            <bind nodeset=\"/data/selectOneQuestions/q6\" type=\"string\" />\n            <bind nodeset=\"/data/selectOneQuestions/q7\" type=\"string\" />\n            <bind nodeset=\"/data/selectMultipleQuestions/q8\" type=\"string\" />\n            <bind nodeset=\"/data/selectMultipleQuestions/q9\" type=\"string\" />\n            <bind nodeset=\"/data/selectMultipleQuestions/q10\" type=\"string\" />\n            <bind nodeset=\"/data/selectMultipleQuestions/q11\" type=\"string\" />\n            <bind nodeset=\"/data/selectMultipleQuestions/q12\" type=\"string\" />\n            <bind nodeset=\"/data/q13\" type=\"odk:rank\" />\n            <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/q0\">\n            <label>&lt;span style=\"color:blue\"&gt;Note text&lt;/span&gt;</label>\n            <hint ref=\"jr:itext('/data/q0:hint')\" />\n        </input>\n        <input ref=\"/data/q1\">\n            <label>#### &lt;span style=\"color:blue\"&gt;Required text question with header style&lt;/span&gt;</label>\n        </input>\n        <group ref=\"/data/selectOneQuestions\">\n            <label>&lt;span style=\"color:blue\"&gt;selectOneQuestions&lt;/span&gt;</label>\n            <select1 ref=\"/data/selectOneQuestions/q2\">\n                <label>Select one widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select1>\n            <select1 appearance=\"minimal\" ref=\"/data/selectOneQuestions/q3\">\n                <label>Select one minimal widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select1>\n            <select1 appearance=\"Image-map\" ref=\"/data/selectOneQuestions/q4\">\n                <label>Select one image-map widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select1>\n            <select1 appearance=\"label\" ref=\"/data/selectOneQuestions/q5\">\n                <label>Select one label widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select1>\n            <select1 appearance=\"list\" ref=\"/data/selectOneQuestions/q6\">\n                <label>Select one list widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select1>\n            <select1 appearance=\"likert\" ref=\"/data/selectOneQuestions/q7\">\n                <label>Select one likert widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select1>\n        </group>\n        <group ref=\"/data/selectMultipleQuestions\">\n            <label>&lt;span style=\"color:blue\"&gt;selectMultipleQuestions&lt;/span&gt;</label>\n            <select ref=\"/data/selectMultipleQuestions/q8\">\n                <label>Select multiple widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select>\n            <select appearance=\"minimal\" ref=\"/data/selectMultipleQuestions/q9\">\n                <label>Select multiple minimal widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select>\n            <select appearance=\"Image-map\" ref=\"/data/selectMultipleQuestions/q10\">\n                <label>Select multiple image-map widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select>\n            <select appearance=\"label\" ref=\"/data/selectMultipleQuestions/q11\">\n                <label>Select multiple label widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select>\n            <select appearance=\"list\" ref=\"/data/selectMultipleQuestions/q12\">\n                <label>Select multiple list widget</label>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                    <value>one</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                    <value>two</value>\n                </item>\n                <item>\n                    <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                    <value>three</value>\n                </item>\n            </select>\n        </group>\n        <odk:rank ref=\"/data/q13\">\n            <label>Rank widget</label>\n            <item>\n                <label>&lt;span style=\"color:blue\"&gt;One&lt;/span&gt;</label>\n                <value>one</value>\n            </item>\n            <item>\n                <label>&lt;span style=\"color:blue\"&gt;Two&lt;/span&gt;</label>\n                <value>two</value>\n            </item>\n            <item>\n                <label>&lt;span style=\"color:blue\"&gt;Three&lt;/span&gt;</label>\n                <value>three</value>\n            </item>\n        </odk:rank>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/form_with_images.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:ev=\"http://www.w3.org/2001/xml-events\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>form_with_images</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <itext>\n                <translation lang=\"default\" default=\"true()\">\n                    <text id=\"/data/img1:label\">\n                        <value>JPG</value>\n                        <value form=\"image\">jr://images/rect.jpg</value>\n                    </text>\n                    <text id=\"/data/img2:label\">\n                        <value>SVG</value>\n                        <value form=\"image\">jr://images/rect.svg</value>\n                    </text>\n                </translation>\n            </itext>\n            <instance>\n                <data id=\"form_with_images\">\n                    <img1/>\n                    <img2/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/img1\" type=\"string\"/>\n            <bind nodeset=\"/data/img2\" type=\"string\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/img1\">\n            <label ref=\"jr:itext('/data/img1:label')\"/>\n        </input>\n        <input ref=\"/data/img2\">\n            <label ref=\"jr:itext('/data/img2:label')\"/>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/g6Error.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>g6Error</h:title>\n        <model>\n            <instance>\n                <data id=\"build_g6Error_1517568408\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <g1>\n                        <text1/>\n                        <g2 jr:template=\"\">\n                            <text2/>\n                        </g2>\n                    </g1>\n                    <text3/>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/g1:label\">\n                        <value>g1</value>\n                    </text>\n                    <text id=\"/data/g1/text1:label\">\n                        <value>text1</value>\n                    </text>\n                    <text id=\"/data/g1/g2:label\">\n                        <value>g2</value>\n                    </text>\n                    <text id=\"/data/g1/g2/text2:label\">\n                        <value>text2</value>\n                    </text>\n                    <text id=\"/data/text3:label\">\n                        <value>text3</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/g1/text1\" type=\"string\"/>\n            <bind nodeset=\"/data/g1/g2/text2\" type=\"string\"/>\n            <bind nodeset=\"/data/text3\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\">\n            <label ref=\"jr:itext('/data/g1:label')\"/>\n            <input ref=\"/data/g1/text1\">\n                <label ref=\"jr:itext('/data/g1/text1:label')\"/>\n            </input>\n            <group>\n                <label ref=\"jr:itext('/data/g1/g2:label')\"/>\n                <repeat nodeset=\"/data/g1/g2\">\n                    <input ref=\"/data/g1/g2/text2\">\n                        <label ref=\"jr:itext('/data/g1/g2/text2:label')\"/>\n                    </input>\n                </repeat>\n            </group>\n        </group>\n        <input ref=\"/data/text3\">\n            <label ref=\"jr:itext('/data/text3:label')\"/>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/hints_textq.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\r\n  <h:head>\r\n    <h:title>hints textq</h:title>\r\n    <model>\r\n      <instance>\r\n        <data id=\"hints_textq\">\r\n          <meta>\r\n            <instanceID/>\r\n          </meta>\r\n          <one-page>\r\n            <text/>\r\n          </one-page>\r\n        </data>\r\n      </instance>\r\n      <itext>\r\n        <translation lang=\"English\">\r\n          <text id=\"/data/one-page:label\">\r\n            <value>One page</value>\r\n          </text>\r\n          <text id=\"/data/one-page/text:label\">\r\n            <value>Text</value>\r\n          </text>\r\n          <text id=\"/data/one-page/text:hint\">\r\n            <value>Hint 1</value>\r\n\t\t\t\t<value form=\"guidance\">1 very very very very very very very very very very long text</value>\r\n          </text>\r\n        </translation>\r\n      </itext>\r\n      <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\r\n      <bind nodeset=\"/data/one-page/text\" type=\"string\" constraint=\"(regex(., &quot;^.{5,15}$&quot;))\" jr:constraintMsg=\"Response length must be between 5 and 15\"/>\r\n    </model>\r\n  </h:head>\r\n  <h:body>\r\n    <group appearance=\"field-list\">\r\n      <label ref=\"jr:itext('/data/one-page:label')\"/>\r\n      <input ref=\"/data/one-page/text\">\r\n        <label ref=\"jr:itext('/data/one-page/text:label')\"/>\r\n        <hint ref=\"jr:itext('/data/one-page/text:hint')\"/>\r\n      </input>\r\n    </group>\r\n  </h:body>\r\n</h:html>\r\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/identify-user-audit-false.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\"\n    xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>Identify User False</h:title>\n        <model>\n            <instance>\n                <data id=\"identify-user-false\">\n                    <what_up />\n                    <meta>\n                        <audit />\n                        <instanceID />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/what_up\" type=\"string\" />\n            <bind nodeset=\"/data/meta/audit\" type=\"binary\" odk:identify-user=\"false\" />\n            <bind nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"\n                jr:preload=\"uid\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/what_up\">\n            <label>What up?</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/identify-user-audit.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\"\n    xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>Identify User</h:title>\n        <model>\n            <instance>\n                <data id=\"identify-user\">\n                    <what_up />\n                    <meta>\n                        <audit />\n                        <instanceID />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/what_up\" type=\"string\" />\n            <bind nodeset=\"/data/meta/audit\" type=\"binary\" odk:identify-user=\"true\" />\n            <bind nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"\n                jr:preload=\"uid\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/what_up\">\n            <label>What up?</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/intent-group.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head>\n    <h:title>intent-group</h:title>\n    <model odk:xforms-version=\"1.0.0\">\n      <itext>\n        <translation lang=\"English\">\n          <text id=\"/data/group:label\">\n            <value>Fields to populate</value>\n            <value form=\"noAppErrorString\">This is noAppErrorString</value>\n            <value form=\"buttonText\">This is buttonText</value>\n          </text>\n          <text id=\"/data/group/questionInteger:label\">\n            <value>Integer external</value>\n          </text>\n          <text id=\"/data/group/questionDecimal:label\">\n            <value>Decimal external</value>\n          </text>\n          <text id=\"/data/group/questionText:label\">\n            <value>Text external</value>\n          </text>\n          <text id=\"/data/group/questionImage:label\">\n            <value>Image external</value>\n          </text>\n          <text id=\"/data/group/questionAudio:label\">\n            <value>Audio external</value>\n          </text>\n          <text id=\"/data/group/questionVideo:label\">\n            <value>Video external</value>\n          </text>\n          <text id=\"/data/group/questionFile:label\">\n            <value>File external</value>\n          </text>\n        </translation>\n      </itext>\n      <instance>\n        <data id=\"intent-group\">\n          <group>\n            <questionInteger />\n            <questionDecimal />\n            <questionText />\n            <questionImage />\n            <questionAudio />\n            <questionVideo />\n            <questionFile />\n          </group>\n          <meta>\n            <instanceID />\n          </meta>\n        </data>\n      </instance>\n      <bind nodeset=\"/data/group/questionInteger\" type=\"int\" />\n      <bind nodeset=\"/data/group/questionDecimal\" type=\"decimal\" />\n      <bind nodeset=\"/data/group/questionText\" type=\"string\" />\n      <bind nodeset=\"/data/group/questionImage\" type=\"binary\" />\n      <bind nodeset=\"/data/group/questionAudio\" type=\"binary\" />\n      <bind nodeset=\"/data/group/questionVideo\" type=\"binary\" />\n      <bind nodeset=\"/data/group/questionFile\" type=\"binary\" />\n      <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\" />\n    </model>\n  </h:head>\n  <h:body>\n    <group appearance=\"field-list\" intent=\"org.mycompany.myapp(sometext='Some text', uuid=/intent-group/meta/instanceID)\" ref=\"/data/group\">\n      <label ref=\"jr:itext('/data/group:label')\" />\n      <input ref=\"/data/group/questionInteger\">\n        <label ref=\"jr:itext('/data/group/questionInteger:label')\" />\n      </input>\n      <input ref=\"/data/group/questionDecimal\">\n        <label ref=\"jr:itext('/data/group/questionDecimal:label')\" />\n      </input>\n      <input ref=\"/data/group/questionText\">\n        <label ref=\"jr:itext('/data/group/questionText:label')\" />\n      </input>\n      <upload mediatype=\"image/*\" ref=\"/data/group/questionImage\">\n        <label ref=\"jr:itext('/data/group/questionImage:label')\" />\n      </upload>\n      <upload mediatype=\"audio/*\" ref=\"/data/group/questionAudio\">\n        <label ref=\"jr:itext('/data/group/questionAudio:label')\" />\n      </upload>\n      <upload mediatype=\"video/*\" ref=\"/data/group/questionVideo\">\n        <label ref=\"jr:itext('/data/group/questionVideo:label')\" />\n      </upload>\n      <upload mediatype=\"application/*\" ref=\"/data/group/questionFile\">\n        <label ref=\"jr:itext('/data/group/questionFile:label')\" />\n      </upload>\n    </group>\n  </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/internal-audio-question.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>Audio Question</h:title>\n        <model>\n            <instance>\n                <data id=\"audio_question\" orx:version=\"1\">\n                    <sound/>\n                </data>\n            </instance>\n            <bind nodeset=\"sound\" type=\"binary\" odk:quality=\"normal\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <upload mediatype=\"audio/*\" ref=\"/data/sound\">\n            <label>What does it sound like?</label>\n        </upload>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/invalid-form.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n   <h:head>\n      <h:title>invalid-form</h:title>\n      <model>\n         <itext>\n            <translation lang=\"English\">\n               <text id=\"/asasas/q2:label\">\n                  <value>label::English</value>\n               </text>\n               <text id=\"/asasas/q1:label\">\n                  <value>label::English</value>\n               </text>\n            </translation>\n         </itext>\n         <instance>\n            <asasas id=\"asasas\">\n               <q1 />\n               <q2 />\n               <meta>\n                  <instanceID />\n               </meta>\n            </asasas>\n         </instance>\n         <bind nodeset=\"/asasas/q1\" type=\"int\" />\n         <bind nodeset=\"/asasas/q2\" relevant=\" /asasas/q2 =1\" type=\"int\" />\n         <bind jr:preload=\"uid\" nodeset=\"/asasas/meta/instanceID\" readonly=\"true()\" type=\"string\" />\n      </model>\n   </h:head>\n   <h:body>\n      <input ref=\"/asasas/q1\">\n         <label ref=\"jr:itext('/asasas/q1:label')\" />\n      </input>\n      <input ref=\"/asasas/q2\">\n         <label ref=\"jr:itext('/asasas/q2:label')\" />\n      </input>\n   </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/likert_test.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\"\n    xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>All widgets likert icon</h:title>\n        <model>\n            <itext>\n                <translation default=\"true()\" lang=\"default\">\n                    <text id=\"/likert_test/likert_image_widget/strongly_agree:label\">\n                        <value>Strongly Agree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget_error2/strongly_disagree:label\">\n                        <value>Strongly Disagree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget/agree:label\">\n                        <value>Agree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget_error2/agree:label\">\n                        <value>Agree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget_error/strongly_disagree:label\">\n                        <value>Strongly Disagree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget_error/agree:label\">\n                        <value>Agree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget_error2/strongly_agree:label\">\n                        <value>Strongly Agree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget/disagree:label\">\n                        <value>Disagree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget_error/strongly_agree:label\">\n                        <value>Strongly Agree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget_error2/neutral:label\">\n                        <value>Neutral</value>\n                        <value form=\"image\">jr://images/not_Found.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget_error2/disagree:label\">\n                        <value>Disagree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget/strongly_disagree:label\">\n                        <value>Strongly Disagree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget_error/disagree:label\">\n                        <value>Disagree</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"/likert_test/likert_image_widget/neutral:label\">\n                        <value>Neutral</value>\n                        <value form=\"image\">jr://images/famous.jpg</value>\n                    </text>\n                    <text id=\"static_instance-level1-0\">\n                        <value>A</value>\n                    </text>\n                    <text id=\"static_instance-level1-1\">\n                        <value>B</value>\n                    </text>\n                    <text id=\"static_instance-level1-2\">\n                        <value>C</value>\n                    </text>\n                    <text id=\"static_instance-level2-0\">\n                        <value>A1</value>\n                    </text>\n                    <text id=\"static_instance-level2-1\">\n                        <value>A2</value>\n                    </text>\n                    <text id=\"static_instance-level2-2\">\n                        <value>A3</value>\n                    </text>\n                    <text id=\"static_instance-level2-3\">\n                        <value>B1</value>\n                    </text>\n                    <text id=\"static_instance-level2-4\">\n                        <value>B2</value>\n                    </text>\n                    <text id=\"static_instance-level2-5\">\n                        <value>B3</value>\n                    </text>\n                    <text id=\"static_instance-level2-6\">\n                        <value>C1</value>\n                    </text>\n                    <text id=\"static_instance-level2-7\">\n                        <value>C2</value>\n                    </text>\n                    <text id=\"static_instance-level2-8\">\n                        <value>C3</value>\n                    </text>\n                    <text id=\"static_instance-level2-9\">\n                        <value>C4</value>\n                    </text>\n                    <text id=\"static_instance-level3-0\">\n                        <value>A1A</value>\n                    </text>\n                    <text id=\"static_instance-level3-1\">\n                        <value>A1B</value>\n                    </text>\n                    <text id=\"static_instance-level3-2\">\n                        <value>B1A</value>\n                    </text>\n                    <text id=\"static_instance-level3-3\">\n                        <value>B1B</value>\n                    </text>\n                </translation>\n            </itext>\n            <instance>\n                <likert_test id=\"all-widgets-icon\" version=\"2019051305\">\n                    <likert_widget />\n                    <likert_image_widget />\n                    <likert_widget_error />\n                    <likert_image_widget_error />\n                    <likert_image_widget_error2 />\n                    <likert_error_missing_text />\n                    <cascading_select>\n                        <source1/>\n                        <target1/>\n                        <target1-1/>\n                    </cascading_select>\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </likert_test>\n            </instance>\n            <instance id=\"level1\">\n                <root>\n                    <item>\n                        <itextId>static_instance-level1-0</itextId>\n                        <name>a</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level1-1</itextId>\n                        <name>b</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level1-2</itextId>\n                        <name>c</name>\n                    </item>\n                </root>\n            </instance>\n            <instance id=\"level2\">\n                <root>\n                    <item>\n                        <itextId>static_instance-level2-0</itextId>\n                        <level1>a</level1>\n                        <name>a1</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level2-1</itextId>\n                        <level1>a</level1>\n                        <name>a2</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level2-2</itextId>\n                        <level1>a</level1>\n                        <name>a3</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level2-3</itextId>\n                        <level1>b</level1>\n                        <name>b1</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level2-4</itextId>\n                        <level1>b</level1>\n                        <name>b2</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level2-5</itextId>\n                        <level1>b</level1>\n                        <name>b3</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level2-6</itextId>\n                        <level1>c</level1>\n                        <name>c1</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level2-7</itextId>\n                        <level1>c</level1>\n                        <name>c2</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level2-8</itextId>\n                        <level1>c</level1>\n                        <name>c3</name>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level2-9</itextId>\n                        <level1>c</level1>\n                        <name>c4</name>\n                    </item>\n                </root>\n            </instance>\n            <instance id=\"level3\">\n                <root>\n                    <item>\n                        <itextId>static_instance-level3-0</itextId>\n                        <name>a1a</name>\n                        <level2>a1</level2>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level3-1</itextId>\n                        <name>a1b</name>\n                        <level2>a1</level2>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level3-2</itextId>\n                        <name>b1a</name>\n                        <level2>b1</level2>\n                    </item>\n                    <item>\n                        <itextId>static_instance-level3-3</itextId>\n                        <name>b1b</name>\n                        <level2>b1</level2>\n                    </item>\n                </root>\n            </instance>\n            <bind nodeset=\"/likert_test/likert_widget\" type=\"select1\" />\n            <bind nodeset=\"/likert_test/likert_image_widget\" type=\"select1\" />\n            <bind nodeset=\"/likert_test/likert_widget_error\" type=\"select1\" />\n            <bind nodeset=\"/likert_test/likert_image_widget_error\" type=\"select1\" />\n            <bind nodeset=\"/likert_test/likert_image_widget_error2\" type=\"select1\" />\n            <bind nodeset=\"/likert_test/likert_error_missing_text\" type=\"select1\" />\n            <bind nodeset=\"/likert_test/cascading_select/source1\" type=\"select1\"/>\n            <bind nodeset=\"/likert_test/cascading_select/target1\" type=\"select1\"/>\n            <bind nodeset=\"/likert_test/cascading_select/target1-1\" type=\"select1\"/>\n            <bind nodeset=\"/likert_test/meta/instanceID\" readonly=\"true()\" type=\"string\"\n                jr:preload=\"uid\" />\n        </model>\n    </h:head>\n    <h:body>\n        <select1 appearance=\"likert\" ref=\"/likert_test/likert_widget\">\n            <label>Likert Widget</label>\n            <hint>Likert type widget (happy case)</hint>\n            <item>\n                <label>0</label>\n                <value>choice_0</value>\n            </item>\n            <item>\n                <label>1</label>\n                <value>choice_1</value>\n            </item>\n            <item>\n                <label>2</label>\n                <value>choice_2</value>\n            </item>\n            <item>\n                <label>3</label>\n                <value>choice_3</value>\n            </item>\n            <item>\n                <label>4</label>\n                <value>choice_4</value>\n            </item>\n        </select1>\n        <select1 appearance=\"likert\" ref=\"/likert_test/likert_image_widget\">\n            <label>Likert Image Widget</label>\n            <hint>Likert type widget with images (happy case)</hint>\n            <item>\n                <label ref=\"jr:itext('/likert_test/likert_image_widget/strongly_disagree:label')\" />\n                <value>strongly_disagree</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/likert_test/likert_image_widget/disagree:label')\" />\n                <value>disagree</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/likert_test/likert_image_widget/neutral:label')\" />\n                <value>neutral</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/likert_test/likert_image_widget/agree:label')\" />\n                <value>agree</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/likert_test/likert_image_widget/strongly_agree:label')\" />\n                <value>strongly_agree</value>\n            </item>\n        </select1>\n        <select1 appearance=\"likert\" ref=\"/likert_test/likert_widget_error\">\n            <label>Likert Widget Error</label>\n            <hint>Insufficient text provided</hint>\n            <item>\n                <label>0</label>\n                <value>choice_0</value>\n            </item>\n            <item>\n                <label>1</label>\n                <value>choice_1</value>\n            </item>\n            <item>\n                <label>2</label>\n                <value>choice_2</value>\n            </item>\n            <item>\n                <label>3</label>\n                <value>choice_3</value>\n            </item>\n        </select1>\n        <select1 appearance=\"likert\" ref=\"/likert_test/likert_image_widget_error\">\n            <label>Likert Image Widget Error</label>\n            <hint>Insufficient images provided</hint>\n            <item>\n                <label\n                    ref=\"jr:itext('/likert_test/likert_image_widget_error/strongly_disagree:label')\" />\n                <value>strongly_disagree</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/likert_test/likert_image_widget_error/disagree:label')\" />\n                <value>disagree</value>\n            </item>\n            <item>\n                <label>Neutral</label>\n                <value>neutral</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/likert_test/likert_image_widget_error/agree:label')\" />\n                <value>agree</value>\n            </item>\n            <item>\n                <label\n                    ref=\"jr:itext('/likert_test/likert_image_widget_error/strongly_agree:label')\" />\n                <value>strongly_agree</value>\n            </item>\n        </select1>\n        <select1 appearance=\"likert\" ref=\"/likert_test/likert_image_widget_error2\">\n            <label>Likert Image Widget Error2</label>\n            <hint>Image cannot be found</hint>\n            <item>\n                <label\n                    ref=\"jr:itext('/likert_test/likert_image_widget_error2/strongly_disagree:label')\" />\n                <value>strongly_disagree</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/likert_test/likert_image_widget_error2/disagree:label')\" />\n                <value>disagree</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/likert_test/likert_image_widget_error2/neutral:label')\" />\n                <value>neutral</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/likert_test/likert_image_widget_error2/agree:label')\" />\n                <value>agree</value>\n            </item>\n            <item>\n                <label\n                    ref=\"jr:itext('/likert_test/likert_image_widget_error2/strongly_agree:label')\" />\n                <value>strongly_agree</value>\n            </item>\n        </select1>\n        <select1 appearance=\"likert\" ref=\"/likert_test/likert_error_missing_text\">\n            <label>Likert Missing text Error</label>\n            <hint>When there is a missing Text</hint>\n            <item>\n                <label>0</label>\n                <value>choice_0</value>\n            </item>\n            <item>\n                <label></label>\n                <value>choice_1</value>\n            </item>\n            <item>\n                <label>2</label>\n                <value>choice_2</value>\n            </item>\n            <item>\n                <label>3</label>\n                <value>choice_3</value>\n            </item>\n        </select1>\n        <group appearance=\"field-list\" ref=\"/likert_test/cascading_select\">\n            <label>Cascading likert</label>\n            <select1 appearance=\"likert\" ref=\"/likert_test/cascading_select/source1\">\n                <label>Level1</label>\n                <item>\n                    <label>A</label>\n                    <value>a</value>\n                </item>\n                <item>\n                    <label>B</label>\n                    <value>b</value>\n                </item>\n                <item>\n                    <label>C</label>\n                    <value>c</value>\n                </item>\n            </select1>\n            <select1 appearance=\"likert\" ref=\"/likert_test/cascading_select/target1\">\n                <label>Level2</label>\n                <itemset nodeset=\"instance('level2')/root/item[level1 =  /likert_test/cascading_select/source1 ]\">\n                    <value ref=\"name\"/>\n                    <label ref=\"jr:itext(itextId)\"/>\n                </itemset>\n            </select1>\n            <select1 appearance=\"likert\" ref=\"/likert_test/cascading_select/target1-1\">\n                <label>Level3</label>\n                <itemset nodeset=\"instance('level3')/root/item[level2 =  /likert_test/cascading_select/target1 ]\">\n                    <value ref=\"name\"/>\n                    <label ref=\"jr:itext(itextId)\"/>\n                </itemset>\n            </select1>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/location-audit.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:odk=\"http://www.opendatakit.org/xforms\">\r\n  <h:head>\r\n    <h:title>Audit with Location</h:title>\r\n    <model>\r\n      <instance>\r\n        <data id=\"location_audit\">\r\n          <orx:meta>\r\n            <instanceID/>\r\n            <orx:audit/>\r\n          </orx:meta>\r\n          <text1/>\r\n          <text2/>\r\n        </data>\r\n      </instance>\r\n      <itext>\r\n        <translation lang=\"English\">\r\n          <text id=\"/data/text1:label\">\r\n            <value>Text1</value>\r\n          </text>\r\n          <text id=\"/data/text2:label\">\r\n            <value>Text2</value>\r\n          </text>\r\n        </translation>\r\n      </itext>\r\n      <bind nodeset=\"/data/orx:meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\r\n      <bind nodeset=\"/data/orx:meta/audit\" type=\"binary\" odk:location-priority=\"high_priority\" odk:location-min-interval=\"10\" odk:location-max-age=\"60\" />\r\n      <bind nodeset=\"/data/text1\" type=\"string\"/>\r\n      <bind nodeset=\"/data/text2\" type=\"string\"/>\r\n    </model>\r\n  </h:head>\r\n  <h:body>\r\n    <input ref=\"/data/text1\">\r\n      <label ref=\"jr:itext('/data/text1:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/text2\">\r\n      <label ref=\"jr:itext('/data/text2:label')\"/>\r\n    </input>\r\n  </h:body>\r\n</h:html>\r\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/manyQ.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\r\n  <h:head>\r\n    <h:title>manyQ</h:title>\r\n    <model>\r\n      <instance>\r\n        <data id=\"manyQ\" version=\"1\">\r\n          <meta>\r\n            <instanceID/>\r\n          </meta>\r\n          <t1/>\r\n          <t2/>\r\n          <g1>\r\n            <g2>\r\n              <n1/>\r\n              <n2/>\r\n              <n3/>\r\n              <n4/>\r\n            </g2>\r\n          </g1>\r\n          <t3/>\r\n          <t4/>\r\n          <t5/>\r\n          <t6/>\r\n          <t7/>\r\n          <t8/>\r\n          <t9/>\r\n          <t10/>\r\n          <t11/>\r\n          <t12/>\r\n          <t13/>\r\n          <t14/>\r\n          <t15/>\r\n        </data>\r\n      </instance>\r\n      <itext>\r\n        <translation lang=\"English\">\r\n          <text id=\"/data/t1:label\">\r\n            <value>t1</value>\r\n          </text>\r\n          <text id=\"/data/t2:label\">\r\n            <value>t2</value>\r\n          </text>\r\n          <text id=\"/data/g1:label\">\r\n            <value>g1</value>\r\n          </text>\r\n          <text id=\"/data/g1/g2:label\">\r\n            <value>g2</value>\r\n          </text>\r\n          <text id=\"/data/g1/g2/n1:label\">\r\n            <value>n1</value>\r\n          </text>\r\n          <text id=\"/data/g1/g2/n2:label\">\r\n            <value>n2</value>\r\n          </text>\r\n          <text id=\"/data/g1/g2/n3:label\">\r\n            <value>n3</value>\r\n          </text>\r\n          <text id=\"/data/g1/g2/n4:label\">\r\n            <value>n4</value>\r\n          </text>\r\n          <text id=\"/data/t3:label\">\r\n            <value>t3</value>\r\n          </text>\r\n          <text id=\"/data/t4:label\">\r\n            <value>t4</value>\r\n          </text>\r\n          <text id=\"/data/t5:label\">\r\n            <value>t5</value>\r\n          </text>\r\n          <text id=\"/data/t6:label\">\r\n            <value>t6</value>\r\n          </text>\r\n          <text id=\"/data/t7:label\">\r\n            <value>t7</value>\r\n          </text>\r\n          <text id=\"/data/t8:label\">\r\n            <value>t8</value>\r\n          </text>\r\n          <text id=\"/data/t9:label\">\r\n            <value>t9</value>\r\n          </text>\r\n          <text id=\"/data/t10:label\">\r\n            <value>t10</value>\r\n          </text>\r\n          <text id=\"/data/t11:label\">\r\n            <value>t11</value>\r\n          </text>\r\n          <text id=\"/data/t12:label\">\r\n            <value>t12</value>\r\n          </text>\r\n          <text id=\"/data/t13:label\">\r\n            <value>t13</value>\r\n          </text>\r\n          <text id=\"/data/t14:label\">\r\n            <value>t14</value>\r\n          </text>\r\n          <text id=\"/data/t15:label\">\r\n            <value>t15</value>\r\n          </text>\r\n        </translation>\r\n      </itext>\r\n      <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\r\n      <bind nodeset=\"/data/t1\" type=\"string\"/>\r\n      <bind nodeset=\"/data/t2\" type=\"string\"/>\r\n      <bind nodeset=\"/data/g1/g2/n1\" type=\"int\"/>\r\n      <bind nodeset=\"/data/g1/g2/n2\" type=\"int\"/>\r\n      <bind nodeset=\"/data/g1/g2/n3\" type=\"int\"/>\r\n      <bind nodeset=\"/data/g1/g2/n4\" type=\"int\"/>\r\n      <bind nodeset=\"/data/t3\" type=\"string\"/>\r\n      <bind nodeset=\"/data/t4\" type=\"string\"/>\r\n      <bind nodeset=\"/data/t5\" type=\"string\"/>\r\n      <bind nodeset=\"/data/t6\" type=\"string\"/>\r\n      <bind nodeset=\"/data/t7\" type=\"string\"/>\r\n      <bind nodeset=\"/data/t8\" type=\"string\"/>\r\n      <bind nodeset=\"/data/t9\" type=\"string\"/>\r\n      <bind nodeset=\"/data/t10\" type=\"string\"/>\r\n      <bind nodeset=\"/data/t11\" type=\"string\"/>\r\n      <bind nodeset=\"/data/t12\" type=\"string\"/>\r\n    </model>\r\n  </h:head>\r\n  <h:body>\r\n    <input ref=\"/data/t1\">\r\n      <label ref=\"jr:itext('/data/t1:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t2\">\r\n      <label ref=\"jr:itext('/data/t2:label')\"/>\r\n    </input>\r\n    <group appearance=\"field-list\">\r\n      <label ref=\"jr:itext('/data/g1:label')\"/>\r\n      <group>\r\n        <label ref=\"jr:itext('/data/g1/g2:label')\"/>\r\n        <input ref=\"/data/g1/g2/n1\">\r\n          <label ref=\"jr:itext('/data/g1/g2/n1:label')\"/>\r\n        </input>\r\n        <input ref=\"/data/g1/g2/n2\">\r\n          <label ref=\"jr:itext('/data/g1/g2/n2:label')\"/>\r\n        </input>\r\n        <input ref=\"/data/g1/g2/n3\">\r\n          <label ref=\"jr:itext('/data/g1/g2/n3:label')\"/>\r\n        </input>\r\n        <input ref=\"/data/g1/g2/n4\">\r\n          <label ref=\"jr:itext('/data/g1/g2/n4:label')\"/>\r\n        </input>\r\n      </group>\r\n    </group>\r\n    <input ref=\"/data/t3\">\r\n      <label ref=\"jr:itext('/data/t3:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t4\">\r\n      <label ref=\"jr:itext('/data/t4:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t5\">\r\n      <label ref=\"jr:itext('/data/t5:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t6\">\r\n      <label ref=\"jr:itext('/data/t6:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t7\">\r\n      <label ref=\"jr:itext('/data/t7:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t8\">\r\n      <label ref=\"jr:itext('/data/t8:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t9\">\r\n      <label ref=\"jr:itext('/data/t9:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t10\">\r\n      <label ref=\"jr:itext('/data/t10:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t11\">\r\n      <label ref=\"jr:itext('/data/t11:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t12\">\r\n      <label ref=\"jr:itext('/data/t12:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t13\">\r\n      <label ref=\"jr:itext('/data/t13:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t14\">\r\n      <label ref=\"jr:itext('/data/t14:label')\"/>\r\n    </input>\r\n    <input ref=\"/data/t15\">\r\n      <label ref=\"jr:itext('/data/t15:label')\"/>\r\n    </input>\r\n  </h:body>\r\n</h:html>\r\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/metadata.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head>\n    <h:title>Metadata</h:title>\n    <model>\n      <instance>\n        <preload id=\"preload\" version=\"2017061601\">\n          <username/>\n          <email/>\n          <phonenumber/>\n          <deviceID/>\n          <subscriberID/>\n          <simSerial/>\n          <meta>\n            <instanceID/>\n          </meta>\n        </preload>\n      </instance>\n      <bind nodeset=\"/preload/username\" type=\"string\" jr:preload=\"property\" jr:preloadParams=\"username\" readonly=\"true()\"/>\n      <bind nodeset=\"/preload/email\" type=\"string\" jr:preload=\"property\" jr:preloadParams=\"email\" readonly=\"true()\"/>\n      <bind nodeset=\"/preload/phonenumber\" type=\"string\" jr:preload=\"property\" jr:preloadParams=\"phonenumber\" readonly=\"true()\"/>\n      <bind nodeset=\"/preload/deviceID\" type=\"string\" jr:preload=\"property\" jr:preloadParams=\"deviceID\" readonly=\"true()\"/>\n      <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/preload/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n    </model>\n  </h:head>\n  <h:body>\n    <group appearance=\"field-list\">\n      <input ref=\"/preload/username\">\n        <label>Username</label>\n      </input>\n      <input ref=\"/preload/email\">\n        <label>Email</label>\n      </input>\n      <input ref=\"/preload/phonenumber\">\n        <label>Phone number</label>\n      </input>\n      <input ref=\"/preload/deviceID\">\n        <label>Device ID</label>\n      </input>\n      <input ref=\"/preload/subscriberID\">\n        <label>Subscriber ID</label>\n      </input>\n      <input ref=\"/preload/simSerial\">\n        <label>SIM serial number</label>\n      </input>\n    </group>\n  </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/nested-intent-group.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>nested-intent-group</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"person_registration\" version=\"20250411164720\">\n                    <container>\n                        <skip/>\n                        <Right_Thumb>\n                            <right_thumb_Registration_NFIQ/>\n                            <right_thumb_Registration_template/>\n                        </Right_Thumb>\n                        <outside>\n                            <template_length/>\n                            <note/>\n                            <text/>\n                        </outside>\n                    </container>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <instance id=\"yes_no\">\n                <root>\n                    <item>\n                        <name>yes</name>\n                        <label>Yes</label>\n                    </item>\n                    <item>\n                        <name>no</name>\n                        <label>No</label>\n                    </item>\n                </root>\n            </instance>\n            <bind nodeset=\"/data/container/skip\" type=\"string\"/>\n            <bind nodeset=\"/data/container/Right_Thumb\" relevant=\" /data/container/skip  != 'yes'\"/>\n            <bind constraint=\".&lt;=3\" jr:constraintMsg=\"Please rescan until NFIQ is 1,2 or 3\" nodeset=\"/data/container/Right_Thumb/right_thumb_Registration_NFIQ\" required=\"true()\" type=\"int\"/>\n            <bind nodeset=\"/data/container/Right_Thumb/right_thumb_Registration_template\" required=\"true()\" type=\"string\"/>\n            <bind calculate=\"string-length( /data/container/Right_Thumb/right_thumb_Registration_template )\" nodeset=\"/data/container/outside/template_length\" type=\"string\"/>\n            <bind nodeset=\"/data/container/outside/note\" readonly=\"true()\" type=\"string\"/>\n            <bind nodeset=\"/data/container/outside/text\" type=\"string\"/>\n            <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\" ref=\"/data/container\">\n            <select1 ref=\"/data/container/skip\">\n                <label>Skip?</label>\n                <itemset nodeset=\"instance('yes_no')/root/item\">\n                    <value ref=\"name\"/>\n                    <label ref=\"label\"/>\n                </itemset>\n            </select1>\n            <group intent=\"uk.ac.lshtm.keppel.android.SCAN( uk.ac.lshtm.keppel.android.return_nfiq=&quot;right_thumb_Registration_NFIQ&quot;, uk.ac.lshtm.keppel.android.return_iso_template=&quot;right_thumb_Registration_template&quot;,  uk.ac.lshtm.keppel.android.fast='true' )\" ref=\"/data/container/Right_Thumb\">\n                <label>Scan the Right Thumb</label>\n                <input ref=\"/data/container/Right_Thumb/right_thumb_Registration_NFIQ\">\n                    <label>NFIQ Score</label>\n                </input>\n                <input appearance=\"masked\" ref=\"/data/container/Right_Thumb/right_thumb_Registration_template\">\n                    <label>Template</label>\n                </input>\n            </group>\n            <group ref=\"/data/container/outside\" intent=\"some.app.ACTION()\">\n                <label>Outside</label>\n                <input ref=\"/data/container/outside/note\">\n                    <label>Length of template: <output value=\" /data/container/outside/template_length \"/></label>\n                </input>\n                <input ref=\"/data/container/outside/text\">\n                    <label>Some text</label>\n                </input>\n            </group>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/numberInCSV.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html\n    xmlns:h=\"http://www.w3.org/1999/xhtml\">\n    <h:head>\n        <h:title>numberInCSV</h:title>\n        <model>\n            <instance>\n                <data id=\"a0e1C00000DA61ZQAT\">\n                    <new_section>\n                        <number>\n                            <number_static />\n                            <a0R1C00000anTbn />\n                            <a0R1C00000anTbo />\n                            <a0R1C00000anTbp />\n                        </number>\n                    </new_section>\n                    <closemessage />\n                </data>\n            </instance>\n            <bind nodeset=\"/data/new_section\" />\n            <bind nodeset=\"/data/new_section/number\" />\n            <bind nodeset=\"/data/new_section/number/number_static\" readonly=\"true()\" />\n            <bind nodeset=\"/data/new_section/number/a0R1C00000anTbn\" type=\"select1\" />\n            <bind nodeset=\"/data/new_section/number/a0R1C00000anTbo\" type=\"string\" />\n            <bind nodeset=\"/data/new_section/number/a0R1C00000anTbp\" type=\"string\" />\n            <bind nodeset=\"/data/closemessage\" type=\"string\" readonly=\"true()\" relevant=\"not(null)\" />\n        </model>\n    </h:head>\n    <h:body>\n        <group nodeset=\"/data/new_section\">\n            <label>New section</label>\n            <group nodeset=\"/data/new_section/number\">\n                <label>number</label>\n                <input ref=\"/data/new_section/number/number_static\">\n                    <label>number</label>\n                </input>\n                <select1 ref=\"/data/new_section/number/a0R1C00000anTbn\">\n                    <label>﻿1a</label>\n                    <item>\n                        <label>Vendas agrícolas</label>\n                        <value>Vendas agrícolas</value>\n                    </item>\n                    <item>\n                        <label>Venda de animais</label>\n                        <value>Venda de animais</value>\n                    </item>\n                    <item>\n                        <label>Salário</label>\n                        <value>Salário</value>\n                    </item>\n                    <item>\n                        <label>Pensão</label>\n                        <value>Pensão</value>\n                    </item>\n                    <item>\n                        <label>Enviu de dinheiro de algum familiar/amigo que não vive na casa</label>\n                        <value>Enviu de dinheiro de algum familiar/amigo que não vive na casa</value>\n                    </item>\n                    <item>\n                        <label>Cobrança de rendas</label>\n                        <value>Cobrança de rendas</value>\n                    </item>\n                    <item>\n                        <label>Dinheiro do esposo/esposa ou outro familiar que vive na casa</label>\n                        <value>Dinheiro do esposo/esposa ou outro familiar que vive na casa</value>\n                    </item>\n                    <item>\n                        <label>Outro</label>\n                        <value>Outro</value>\n                    </item>\n                </select1>\n                <input ref=\"/data/new_section/number/a0R1C00000anTbo\" query=\"instance('2a')/root/item[1a=/data/new_section/number/a0R1C00000anTbn]\">\n                    <label>2a</label>\n                </input>\n                <input ref=\"/data/new_section/number/a0R1C00000anTbp\" query=\"instance('3a')/root/item[1a=/data/new_section/number/a0R1C00000anTbn and 2a=/data/new_section/number/a0R1C00000anTbo]\">\n                    <label>3a</label>\n                </input>\n            </group>\n        </group>\n        <input ref=\"/data/closemessage\">\n            <label>Thank you for taking the time to complete this form!</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-audit.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question Audit</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question_audit\" orx:version=\"1\">\n                    <age/>\n                    <meta>\n                        <audit />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"age\" type=\"int\"/>\n            <bind nodeset=\"/data/meta/audit\" type=\"binary\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-autoplay.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question Autoplay</h:title>\n        <model>\n            <itext>\n                <translation lang=\"default\" default=\"true()\">\n                    <text id=\"/data/age:label\">\n                        <value>what is your age</value>\n                        <value form=\"audio\">jr://audio/sampleAudio.wav</value>\n                    </text>\n                </translation>\n            </itext>\n            <instance>\n                <data id=\"one_question_autoplay\" orx:version=\"1\">\n                    <age/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"age\" type=\"int\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\" autoplay=\"audio\">\n            <label ref=\"jr:itext('/data/age:label')\"/>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-autosend-disabled.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question Autosend Disabled</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question\" orx:version=\"1\">\n                    <age/>\n                </data>\n            </instance>\n            <submission orx:auto-send=\"false\" />\n            <bind nodeset=\"age\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-autosend.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question Autosend</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question\" orx:version=\"1\">\n                    <age/>\n                </data>\n            </instance>\n            <submission orx:auto-send=\"true\" />\n            <bind nodeset=\"age\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-background-audio-audit.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>One Question Background Audio And Audit</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question\" orx:version=\"1\">\n                    <recording/>\n                    <age/>\n                    <meta>\n                        <audit/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"age\" type=\"int\"/>\n            <bind nodeset=\"/data/meta/audit\" type=\"binary\"/>\n            <odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-background-audio-multiple.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>One Question</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question\" orx:version=\"1\">\n                    <recording1/>\n                    <recording2/>\n                    <age/>\n                </data>\n            </instance>\n            <bind nodeset=\"age\" type=\"int\"/>\n            <odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording1\"/>\n            <odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording2\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-background-audio.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>One Question</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question\" orx:version=\"1\">\n                    <recording/>\n                    <age/>\n                </data>\n            </instance>\n            <bind nodeset=\"age\" type=\"int\"/>\n            <odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-editable.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>One Question Editable</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question_editable\" orx:version=\"1\">\n                    <age/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <submission odk:client-editable=\"true\" />\n            <bind nodeset=\"age\" type=\"int\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-encrypted-unicode.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question Encrypted Unicode</h:title>\n        <model>\n            <submission base64RsaPublicKey=\"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAysbTy+pI99GTbLBeBfbEKO+mQJDoL0moKh+r0ECz/yAmUkZOBumwI0o9il0tIygtEKbnUBkc9iwWs0FAvTBK63NpVqfXrtyt64+2UYMWuhXatwFbnqz+5D+C6qlmv5qsHEjJLK4LRUvNcxRY1pTtTQ9EYHpYSiqwcEsHwX18GKAueaki0ZnfU1fT5x1rVU+cJ2GgdHUMW81eXUNATYQyDqy4Ehqmo9zL19gT4AVb5ODWMG2bCKUpZdxkBtkkoLqZM2jiYzdedvOMjrYGx8INE4uumF02sbnJ1KF9GhXyLoTv0mSvCVhZrOL/jgmSBlQRKtacAv9Rbj1RtnedhG9ylQIDAQAB\"/>\n            <instance>\n                <data id=\"one_questión_encrypted_unicode\" orx:version=\"versión 4\">\n                    <age/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"age\" type=\"int\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-create-and-update.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n\t<h:head>\n\t\t<h:title>One Question Entity Registration</h:title>\n\t\t<model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2024.1.0\">\n\t\t\t<instance>\n\t\t\t\t<data id=\"one_question_entity\" version=\"1\">\n\t\t\t\t\t<name/>\n\t\t\t\t\t<meta>\n\t\t\t\t\t\t<instanceID/>\n\t\t\t\t\t\t<instanceName/>\n\t\t\t\t\t\t<entity dataset=\"people\" create=\"1\" update=\"1\" id=\"\">\n\t\t\t\t\t\t\t<label/>\n\t\t\t\t\t\t</entity>\n\t\t\t\t\t</meta>\n\t\t\t\t</data>\n\t\t\t</instance>\n\t\t\t<bind nodeset=\"/data/name\" type=\"string\" entities:saveto=\"full_name\"/>\n\n\t\t\t<bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n\t\t\t<bind nodeset=\"/data/meta/entity/@id\" type=\"string\"/>\n\t\t\t<setvalue event=\"odk-instance-first-load\" ref=\"/data/meta/entity/@id\" value=\"uuid()\"/>\n\t\t\t<bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n\t\t</model>\n\t</h:head>\n\t<h:body>\n\t\t<input ref=\"/data/name\">\n\t\t\t<label>Name</label>\n\t\t</input>\n\t</h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-follow-up.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n\t<h:head>\n\t\t<h:title>One Question Entity Follow Up</h:title>\n\t\t<model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2024.1.0\">\n\t\t\t<instance>\n\t\t\t\t<data id=\"one_question_entity_follow_up\" version=\"1\">\n\t\t\t\t\t<person/>\n\t\t\t\t\t<meta>\n\t\t\t\t\t\t<instanceID/>\n\t\t\t\t\t</meta>\n\t\t\t\t</data>\n\t\t\t</instance>\n\n\t\t\t<instance id=\"people\" src=\"jr://file-csv/people.csv\"/>\n\n\t\t\t<bind nodeset=\"/data/person\" type=\"string\"/>\n\t\t\t<bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\t\t</model>\n\t</h:head>\n\t<h:body>\n\t\t<select1 ref=\"/data/person\">\n\t\t\t<label>Select person</label>\n\t\t\t<itemset nodeset=\"instance('people')/root/item\">\n\t\t\t\t<value ref=\"name\"/>\n\t\t\t\t<label ref=\"label\"/>\n\t\t\t</itemset>\n\t\t</select1>\n\t</h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-registration-broken.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n    <h:head>\n        <h:title>One Question Entity Registration Broken</h:title>\n        <model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2452.2.0\">\n            <instance>\n                <data id=\"one_question_entity_registration_broken\" version=\"1\">\n                    <name/>\n                    <meta>\n                        <instanceID/>\n                        <instanceName/>\n                        <entity dataset=\"people\" create=\"1\" id=\"\">\n                            <label/>\n                        </entity>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/name\" type=\"string\" entities:saveto=\"full_name\"/>\n\n            <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n            <bind nodeset=\"/data/meta/entity/@id\" type=\"string\"/>\n            <setvalue event=\"odk-instance-first-load\" ref=\"/data/meta/entity/@id\" value=\"uuid()\"/>\n            <bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>Name</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-registration-editable.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n    <h:head>\n        <h:title>One Question Entity Registration Editable</h:title>\n        <model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2024.1.0\">\n            <instance>\n                <data id=\"one_question_entity_registration_editable\" version=\"1\">\n                    <name/>\n                    <meta>\n                        <instanceID/>\n                        <instanceName/>\n                        <entity dataset=\"people\" create=\"1\" id=\"\">\n                            <label/>\n                        </entity>\n                    </meta>\n                </data>\n            </instance>\n            <submission odk:client-editable=\"true\" />\n            <bind nodeset=\"/data/name\" type=\"string\" entities:saveto=\"full_name\"/>\n\n            <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n            <bind nodeset=\"/data/meta/entity/@id\" type=\"string\"/>\n            <setvalue event=\"odk-instance-first-load\" ref=\"/data/meta/entity/@id\" value=\"uuid()\"/>\n            <bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>Name</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-registration-id.xml",
    "content": "<?xml version=\"1.0\"?>\n<!--\nLike \"one-question-entity-registration.xml except that the entity ID uses the name field so that\nit can be used in tests (as opposed to having the standard UUID generated ID).\n -->\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n\t<h:head>\n\t\t<h:title>One Question Entity Registration</h:title>\n\t\t<model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2024.1.0\">\n\t\t\t<instance>\n\t\t\t\t<data id=\"one_question_entity\" version=\"1\">\n\t\t\t\t\t<name/>\n\t\t\t\t\t<meta>\n\t\t\t\t\t\t<instanceID/>\n\t\t\t\t\t\t<instanceName/>\n\t\t\t\t\t\t<entity dataset=\"people\" create=\"1\" id=\"\">\n\t\t\t\t\t\t\t<label/>\n\t\t\t\t\t\t</entity>\n\t\t\t\t\t</meta>\n\t\t\t\t</data>\n\t\t\t</instance>\n\t\t\t<bind nodeset=\"/data/name\" type=\"string\" entities:saveto=\"full_name\"/>\n\n\t\t\t<bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n\t\t\t<bind nodeset=\"/data/meta/entity/@id\" type=\"string\" calculate=\"/data/name\"/>\n\t\t\t<bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n\t\t</model>\n\t</h:head>\n\t<h:body>\n\t\t<input ref=\"/data/name\">\n\t\t\t<label>Name</label>\n\t\t</input>\n\t</h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-registration-v2020.1.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n    <h:head>\n        <h:title>One Question Entity Registration</h:title>\n        <model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2020.1.0\">\n            <instance>\n                <data id=\"one_question_entity\" version=\"1\">\n                    <name/>\n                    <meta>\n                        <instanceID/>\n                        <instanceName/>\n                        <entity dataset=\"people\" create=\"1\" id=\"\">\n                            <label/>\n                        </entity>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/name\" type=\"string\" entities:saveto=\"full_name\"/>\n\n            <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n            <bind nodeset=\"/data/meta/entity/@id\" type=\"string\"/>\n            <setvalue event=\"odk-instance-first-load\" ref=\"/data/meta/entity/@id\" value=\"uuid()\"/>\n            <bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>Name</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-registration-v2023.1.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n\t<h:head>\n\t\t<h:title>One Question Entity Registration</h:title>\n\t\t<model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2023.1.0\">\n\t\t\t<instance>\n\t\t\t\t<data id=\"one_question_entity\" version=\"1\">\n\t\t\t\t\t<name/>\n\t\t\t\t\t<meta>\n\t\t\t\t\t\t<instanceID/>\n\t\t\t\t\t\t<instanceName/>\n\t\t\t\t\t\t<entity dataset=\"people\" create=\"1\" id=\"\">\n\t\t\t\t\t\t\t<label/>\n\t\t\t\t\t\t</entity>\n\t\t\t\t\t</meta>\n\t\t\t\t</data>\n\t\t\t</instance>\n\t\t\t<bind nodeset=\"/data/name\" type=\"string\" entities:saveto=\"full_name\"/>\n\n\t\t\t<bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n\t\t\t<bind nodeset=\"/data/meta/entity/@id\" type=\"string\"/>\n\t\t\t<setvalue event=\"odk-instance-first-load\" ref=\"/data/meta/entity/@id\" value=\"uuid()\"/>\n\t\t\t<bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n\t\t</model>\n\t</h:head>\n\t<h:body>\n\t\t<input ref=\"/data/name\">\n\t\t\t<label>Name</label>\n\t\t</input>\n\t</h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-registration.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n\t<h:head>\n\t\t<h:title>One Question Entity Registration</h:title>\n\t\t<model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2024.1.0\">\n\t\t\t<instance>\n\t\t\t\t<data id=\"one_question_entity\" version=\"1\">\n\t\t\t\t\t<name/>\n\t\t\t\t\t<meta>\n\t\t\t\t\t\t<instanceID/>\n\t\t\t\t\t\t<instanceName/>\n\t\t\t\t\t\t<entity dataset=\"people\" create=\"1\" id=\"\">\n\t\t\t\t\t\t\t<label/>\n\t\t\t\t\t\t</entity>\n\t\t\t\t\t</meta>\n\t\t\t\t</data>\n\t\t\t</instance>\n\t\t\t<bind nodeset=\"/data/name\" type=\"string\" entities:saveto=\"full_name\"/>\n\n\t\t\t<bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n\t\t\t<bind nodeset=\"/data/meta/entity/@id\" type=\"string\"/>\n\t\t\t<setvalue event=\"odk-instance-first-load\" ref=\"/data/meta/entity/@id\" value=\"uuid()\"/>\n\t\t\t<bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n\t\t</model>\n\t</h:head>\n\t<h:body>\n\t\t<input ref=\"/data/name\">\n\t\t\t<label>Name</label>\n\t\t</input>\n\t</h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-update-and-create.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n\t<h:head>\n\t\t<h:title>One Question Entity Update</h:title>\n\t\t<model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2024.1.0\">\n\t\t\t<instance>\n\t\t\t\t<data id=\"one_question_entity_update\" version=\"1\">\n\t\t\t\t\t<person/>\n\t\t\t\t\t<name/>\n\t\t\t\t\t<meta>\n\t\t\t\t\t\t<instanceID/>\n\t\t\t\t\t\t<instanceName/>\n\t\t\t\t\t\t<entity dataset=\"people\" create=\"true\" update=\"true\" id=\"\" baseVersion=\"\" trunkVersion=\"\" branchId=\"\">\n\t\t\t\t\t\t\t<label/>\n\t\t\t\t\t\t</entity>\n\t\t\t\t\t</meta>\n\t\t\t\t</data>\n\t\t\t</instance>\n\n\t\t\t<instance id=\"people\" src=\"jr://file-csv/people.csv\"/>\n\n\t\t\t<bind nodeset=\"/data/person\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/name\" type=\"string\" entities:saveto=\"full_name\"/>\n\n\t\t\t<bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n\t\t\t<bind nodeset=\"/data/meta/entity/@id\" type=\"string\" calculate=\"/data/person\"/>\n\t\t\t<bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/meta/entity/@baseVersion\" calculate=\"instance('people')/root/item[name=/data/person]/__version\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/meta/entity/@trunkVersion\" calculate=\"instance('people')/root/item[name=/data/person]/__trunkVersion\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/meta/entity/@branchId\" calculate=\"instance('people')/root/item[name=/data/person]/__branchId\" type=\"string\"/>\n\t\t</model>\n\t</h:head>\n\t<h:body>\n\t\t<select1 ref=\"/data/person\">\n\t\t\t<label>Select person</label>\n\t\t\t<itemset nodeset=\"instance('people')/root/item\">\n\t\t\t\t<value ref=\"name\"/>\n\t\t\t\t<label ref=\"label\"/>\n\t\t\t</itemset>\n\t\t</select1>\n\t\t<input ref=\"/data/name\">\n\t\t\t<label>Name</label>\n\t\t</input>\n\t</h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-update-editable.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n    <h:head>\n        <h:title>One Question Entity Update Editable</h:title>\n        <model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2024.1.0\">\n            <instance>\n                <data id=\"one_question_entity_update-editable\" version=\"1\">\n                    <person/>\n                    <name/>\n                    <meta>\n                        <instanceID/>\n                        <instanceName/>\n                        <entity dataset=\"people\" update=\"true\" id=\"\" baseVersion=\"\" trunkVersion=\"\" branchId=\"\">\n                            <label/>\n                        </entity>\n                    </meta>\n                </data>\n            </instance>\n            <submission odk:client-editable=\"true\" />\n\n            <instance id=\"people\" src=\"jr://file-csv/people.csv\"/>\n\n            <bind nodeset=\"/data/person\" type=\"string\"/>\n            <bind nodeset=\"/data/name\" type=\"string\" entities:saveto=\"full_name\"/>\n\n            <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n            <bind nodeset=\"/data/meta/entity/@id\" type=\"string\" calculate=\"/data/person\"/>\n            <bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n            <bind nodeset=\"/data/meta/entity/@baseVersion\" calculate=\"instance('people')/root/item[name=/data/person]/__version\" type=\"string\"/>\n            <bind nodeset=\"/data/meta/entity/@trunkVersion\" calculate=\"instance('people')/root/item[name=/data/person]/__trunkVersion\" type=\"string\"/>\n            <bind nodeset=\"/data/meta/entity/@branchId\" calculate=\"instance('people')/root/item[name=/data/person]/__branchId\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <select1 ref=\"/data/person\">\n            <label>Select person</label>\n            <itemset nodeset=\"instance('people')/root/item\">\n                <value ref=\"name\"/>\n                <label ref=\"label\"/>\n            </itemset>\n        </select1>\n        <input ref=\"/data/name\">\n            <label>Name</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-entity-update.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:entities=\"http://www.opendatakit.org/xforms/entities\">\n\t<h:head>\n\t\t<h:title>One Question Entity Update</h:title>\n\t\t<model odk:xforms-version=\"1.0.0\" entities:entities-version=\"2024.1.0\">\n\t\t\t<instance>\n\t\t\t\t<data id=\"one_question_entity_update\" version=\"1\">\n\t\t\t\t\t<person/>\n\t\t\t\t\t<name/>\n\t\t\t\t\t<meta>\n\t\t\t\t\t\t<instanceID/>\n\t\t\t\t\t\t<instanceName/>\n\t\t\t\t\t\t<entity dataset=\"people\" update=\"true\" id=\"\" baseVersion=\"\" trunkVersion=\"\" branchId=\"\">\n\t\t\t\t\t\t\t<label/>\n\t\t\t\t\t\t</entity>\n\t\t\t\t\t</meta>\n\t\t\t\t</data>\n\t\t\t</instance>\n\n\t\t\t<instance id=\"people\" src=\"jr://file-csv/people.csv\"/>\n\n\t\t\t<bind nodeset=\"/data/person\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/name\" type=\"string\" entities:saveto=\"full_name\"/>\n\n\t\t\t<bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\n\t\t\t<bind nodeset=\"/data/meta/entity/@id\" type=\"string\" calculate=\"/data/person\"/>\n\t\t\t<bind calculate=\"/data/name\" nodeset=\"/data/meta/entity/label\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/meta/entity/@baseVersion\" calculate=\"instance('people')/root/item[name=/data/person]/__version\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/meta/entity/@trunkVersion\" calculate=\"instance('people')/root/item[name=/data/person]/__trunkVersion\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/data/meta/entity/@branchId\" calculate=\"instance('people')/root/item[name=/data/person]/__branchId\" type=\"string\"/>\n\t\t</model>\n\t</h:head>\n\t<h:body>\n\t\t<select1 ref=\"/data/person\">\n\t\t\t<label>Select person</label>\n\t\t\t<itemset nodeset=\"instance('people')/root/item\">\n\t\t\t\t<value ref=\"name\"/>\n\t\t\t\t<label ref=\"label\"/>\n\t\t\t</itemset>\n\t\t</select1>\n\t\t<input ref=\"/data/name\">\n\t\t\t<label>Name</label>\n\t\t</input>\n\t</h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-last-saved-updated.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question Last Saved</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question_last_saved\" orx:version=\"2\">\n                    <age/>\n                </data>\n            </instance>\n            <instance id=\"__last-saved\" src=\"jr://instance/last-saved\" />\n            <bind nodeset=\"age\" type=\"int\"/>\n            <setvalue event=\"odk-instance-first-load\" ref=\"/data/age\" value=\" instance('__last-saved')/data/age \" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age?</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-last-saved.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question Last Saved</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question_last_saved\" orx:version=\"1\">\n                    <age/>\n                </data>\n            </instance>\n            <instance id=\"__last-saved\" src=\"jr://instance/last-saved\" />\n            <bind nodeset=\"age\" type=\"int\"/>\n            <setvalue event=\"odk-instance-first-load\" ref=\"/data/age\" value=\" instance('__last-saved')/data/age \" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-partial.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question\" orx:version=\"1\">\n                    <age/>\n                </data>\n            </instance>\n            <submission ref=\"/data/age\" />\n            <bind nodeset=\"age\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-repeat.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question Repeat</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question_repeat\">\n                    <person>\n                        <age/>\n                    </person>\n                </data>\n            </instance>\n            <bind nodeset=\"age\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group ref=\"/data/person\">\n            <label>Person</label>\n            <repeat nodeset=\"/data/person\">\n                <input ref=\"/data/person/age\">\n                    <label>What is their age?</label>\n                </input>\n            </repeat>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-translation.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>One Question</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question\" orx:version=\"1\">\n                    <age />\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English (en)\">\n                    <text id=\"/data/age:label\">\n                        <value>what is your age</value>\n                    </text>\n                </translation>\n\n                <translation lang=\"French (fr)\">\n                    <text id=\"/data/age:label\">\n                        <value>quel âge as-tu</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"age\" type=\"int\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label ref=\"jr:itext('/data/age:label')\" />\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-updated.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question Updated</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question\" orx:version=\"2\">\n                    <age/>\n                </data>\n            </instance>\n            <bind nodeset=\"age\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-uuid-instance-name.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question\" orx:version=\"1\">\n                    <meta>\n                        <instanceName/>\n                    </meta>\n                    <age/>\n                </data>\n            </instance>\n            <bind nodeset=\"age\" type=\"int\"/>\n            <bind nodeset=\"/data/meta/instanceName\" type=\"string\" calculate=\"uuid()\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question-with-constraint.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:ev=\"http://www.w3.org/2001/xml-events\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>One Question With Constraint</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"one-question-with-constraint\">\n                    <age/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/age\" type=\"int\" constraint=\".&gt;=18\" jr:constraintMsg=\"Age must be at least 18\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>What is your age?</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/one-question.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>One Question</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question\" orx:version=\"1\">\n                    <age/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"age\" type=\"int\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>what is your age</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/pull_data.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\"\n    xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>pull_data</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"pull_data\">\n                    <fruit />\n                    <note_country />\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </data>\n            </instance>\n            <instance id=\"fruits\" src=\"jr://file-csv/fruits.csv\" />\n            <bind calculate=\"pulldata('fruits', 'name', 'name_key', 'mango')\" nodeset=\"/data/fruit\"\n                type=\"string\" />\n            <bind nodeset=\"/data/note_country\" readonly=\"true()\" type=\"string\" />\n            <bind nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"\n                jr:preload=\"uid\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/note_country\">\n            <label>The fruit <output value=\" /data/fruit \" /> is pulled csv data.\n            </label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/random.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n\t<h:head>\n\t\t<h:title>random</h:title>\n\t\t<model>\n\t\t\t<instance>\n\t\t\t\t<random id=\"random\">\n\t\t\t\t\t<rand/>\n\t\t\t\t\t<rand_note/>\n\t\t\t\t\t<rand_once/>\n\t\t\t\t\t<rand_once_note/>\n\t\t\t\t\t<meta>\n\t\t\t\t\t\t<instanceID/>\n\t\t\t\t\t</meta>\n\t\t\t\t</random>\n\t\t\t</instance>\n\t\t\t<bind calculate=\"random()\" nodeset=\"/random/rand\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/random/rand_note\" readonly=\"true()\" type=\"string\"/>\n\t\t\t<bind calculate=\"once(random())\" nodeset=\"/random/rand_once\" type=\"string\"/>\n\t\t\t<bind nodeset=\"/random/rand_once_note\" readonly=\"true()\" type=\"string\"/>\n\t\t\t<bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/random/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\t\t</model>\n\t</h:head>\n\t<h:body>\n\t\t<input ref=\"/random/rand_note\">\n\t\t\t<label>Your random value: \n\t\t\t\t<output value=\" /random/rand \"/></label>\n\t\t</input>\n\t\t<input ref=\"/random/rand_once_note\">\n\t\t\t<label>Your random once value: \n\t\t\t\t<output value=\" /random/rand_once \"/></label>\n\t\t</input>\n\t</h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/randomTest_broken.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>random test</h:title>\n        <model>\n            <itext>\n                <translation default=\"true()\" lang=\"English\">\n                    <text id=\"/randomTest_broken/selectedNote:label\">\n                        <value>\n                            Your random number was\n                            <output value=\" /randomTest_broken/randNum \" />\n                            . For this survey, please interview mother\n                            <output value=\" /randomTest_broken/selected \" />\n                            . Thank the other eligible women (if any) for their time and tell them you will only need to speak to mother\n                            <output value=\" /randomTest_broken/selected \" />\n                            .\n                        </value>\n                    </text>\n                    <text id=\"/randomTest_broken/cGroup1/c2:label\">\n                        <value>How many women in the household are pregnant or have young children?</value>\n                    </text>\n                    <text id=\"/randomTest_broken/cGroup1/c2:jr:constraintMsg\">\n                        <value>Number must between 0 and 10</value>\n                    </text>\n                </translation>\n            </itext>\n            <instance>\n                <randomTest_broken id=\"randomTest\" version=\"2018041801\">\n                    <cGroup1>\n                        <c2 />\n                    </cGroup1>\n                    <randNum />\n                    <selected />\n                    <selectedNote />\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </randomTest_broken>\n            </instance>\n            <bind jr:preload=\"property\" jr:preloadParams=\"deviceid\" nodeset=\"/randomTest_broken/deviceid\" type=\"string\" />\n            <bind jr:preload=\"property\" jr:preloadParams=\"username\" nodeset=\"/randomTest_broken/username\" type=\"string\" />\n            <bind jr:preload=\"timestamp\" jr:preloadParams=\"start\" nodeset=\"/randomTest_broken/start\" type=\"dateTime\" />\n            <bind jr:preload=\"timestamp\" jr:preloadParams=\"end\" nodeset=\"/randomTest_broken/end\" type=\"dateTime\" />\n            <bind jr:preload=\"date\" jr:preloadParams=\"today\" nodeset=\"/randomTest_broken/today\" type=\"date\" />\n            <bind constraint=\".&gt;=0 and .&lt;=10\" jr:constraintMsg=\"jr:itext('/randomTest_broken/cGroup1/c2:jr:constraintMsg')\" nodeset=\"/randomTest_broken/cGroup1/c2\" required=\"true()\" type=\"int\" />\n            <bind calculate=\"once(random())\" nodeset=\"/randomTest_broken/randNum\" type=\"string\" />\n            <bind calculate=\"int( /randomTest_broken/randNum * /randomTest_broken/cGroup1/c2 )+1\" nodeset=\"/randomTest_broken/selected\" type=\"string\" />\n            <bind nodeset=\"/randomTest_broken/selectedNote\" readonly=\"true()\" type=\"string\" />\n            <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/randomTest_broken/meta/instanceID\" readonly=\"true()\" type=\"string\" />\n        </model>\n    </h:head>\n    <h:body class=\"pages\">\n        <group appearance=\"field-list\" ref=\"/randomTest_broken/cGroup1\">\n            <input ref=\"/randomTest_broken/cGroup1/c2\">\n                <label ref=\"jr:itext('/randomTest_broken/cGroup1/c2:label')\" />\n            </input>\n        </group>\n        <input ref=\"/randomTest_broken/selectedNote\">\n            <label ref=\"jr:itext('/randomTest_broken/selectedNote:label')\" />\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/ranking_widget.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n\txmlns=\"http://www.w3.org/2002/xforms\"\n\txmlns:ev=\"http://www.w3.org/2001/xml-events\"\n\txmlns:h=\"http://www.w3.org/1999/xhtml\"\n\txmlns:jr=\"http://openrosa.org/javarosa\"\n\txmlns:odk=\"http://www.opendatakit.org/xforms\"\n\txmlns:orx=\"http://openrosa.org/xforms\"\n\txmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n\t<h:head>\n\t\t<h:title>ranking_widget</h:title>\n\t\t<model>\n\t\t\t<instance>\n\t\t\t\t<ranking_widget id=\"ranking_widget\">\n\t\t\t\t\t<q1/>\n\t\t\t\t\t<meta>\n\t\t\t\t\t\t<instanceID/>\n\t\t\t\t\t</meta>\n\t\t\t\t</ranking_widget>\n\t\t\t</instance>\n\t\t\t<bind nodeset=\"/ranking_widget/q1\" type=\"odk:rank\"/>\n\t\t\t<bind jr:preload=\"uid\" nodeset=\"/ranking_widget/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n\t\t</model>\n\t</h:head>\n\t<h:body>\n\t\t<odk:rank appearance=\"search('fruits')\" ref=\"/ranking_widget/q1\">\n\t\t\t<label>Items from an external csv file</label>\n\t\t\t<item>\n\t\t\t\t<label>name</label>\n\t\t\t\t<value>name_key</value>\n\t\t\t</item>\n\t\t</odk:rank>\n\t</h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/repeat_group_form.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head>\n    <h:title>Repeat Group</h:title>\n    <model>\n      <instance>\n        <data id=\"328b5fbf-dfe6-4c1d-9561-d82e1d65b77e\" version=\"mrc\">\n          <grp09d86f73-7b3c-44e5-9f49-bdae8ab50132 jr:template=\"\">\n            <header/>\n            <qingacd1f34e-8f48-49cd-aaa0-24575878ead0/>\n            <qing1507542f-3919-4e6f-b305-32ed0fa29565/>\n            <qing204bedfc-ec15-4247-8457-e307db11f040/>\n          </grp09d86f73-7b3c-44e5-9f49-bdae8ab50132>\n          <grp2ea471c2-9316-4166-9749-566f2ab4b45a>\n            <header/>\n            <qinga12441e2-79c0-4584-90ce-349923583726/>\n            <qing281f13e2-94d3-4be7-992e-1d02f4259b34/>\n          </grp2ea471c2-9316-4166-9749-566f2ab4b45a>\n          <grpa337aa68-0152-44b6-aae3-1fe91b55890e jr:template=\"\">\n            <header/>\n            <qing7bd3d9e8-4fbb-4b7f-9a35-e117a34c8ccc/>\n            <qingc26f3f0f-eb3e-4375-aafa-e2ea0cd683a4/>\n          </grpa337aa68-0152-44b6-aae3-1fe91b55890e>\n        </data>\n      </instance>\n      <itext>\n        <translation lang=\"English\">\n          <text id=\"grp09d86f73-7b3c-44e5-9f49-bdae8ab50132:label\">\n            <value>Grp1</value>\n          </text>\n          <text id=\"grp09d86f73-7b3c-44e5-9f49-bdae8ab50132:hint\">\n            <value>Grp1</value>\n          </text>\n          <text id=\"grp09d86f73-7b3c-44e5-9f49-bdae8ab50132:itemname\">\n            <value>Hi’ \"<output value=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132/qingacd1f34e-8f48-49cd-aaa0-24575878ead0\"/>\"</value>\n          </text>\n          <text id=\"grp2ea471c2-9316-4166-9749-566f2ab4b45a:label\">\n            <value>Group Name</value>\n          </text>\n          <text id=\"grp2ea471c2-9316-4166-9749-566f2ab4b45a:hint\">\n            <value>Group Hint</value>\n          </text>\n          <text id=\"grpa337aa68-0152-44b6-aae3-1fe91b55890e:label\">\n            <value>Grp2</value>\n          </text>\n          <text id=\"grpa337aa68-0152-44b6-aae3-1fe91b55890e:hint\">\n            <value>Grp2</value>\n          </text>\n          <text id=\"grpa337aa68-0152-44b6-aae3-1fe91b55890e:itemname\">\n            <value>\n              <output value=\"if((/data/grpa337aa68-0152-44b6-aae3-1fe91b55890e/qing7bd3d9e8-4fbb-4b7f-9a35-e117a34c8ccc) &gt; 18, 'A’&quot;yeah&quot;', 'C')\"/>\n            </value>\n          </text>\n          <text id=\"qingacd1f34e-8f48-49cd-aaa0-24575878ead0:label\">\n            <value>Text Question Title 1</value>\n          </text>\n          <text id=\"qingacd1f34e-8f48-49cd-aaa0-24575878ead0:hint\">\n            <value>Question Hint 1</value>\n          </text>\n          <text id=\"qing1507542f-3919-4e6f-b305-32ed0fa29565:label\">\n            <value>Text Question Title 2</value>\n          </text>\n          <text id=\"qing1507542f-3919-4e6f-b305-32ed0fa29565:hint\">\n            <value>Question Hint 2</value>\n          </text>\n          <text id=\"qing204bedfc-ec15-4247-8457-e307db11f040:label\">\n            <value>Text Question Title 3</value>\n          </text>\n          <text id=\"qing204bedfc-ec15-4247-8457-e307db11f040:hint\">\n            <value>Question Hint 3</value>\n          </text>\n          <text id=\"qinga12441e2-79c0-4584-90ce-349923583726:label\">\n            <value>Text Question Title 4</value>\n          </text>\n          <text id=\"qinga12441e2-79c0-4584-90ce-349923583726:hint\">\n            <value>Question Hint 4</value>\n          </text>\n          <text id=\"qing281f13e2-94d3-4be7-992e-1d02f4259b34:label\">\n            <value>Text Question Title 5</value>\n          </text>\n          <text id=\"qing281f13e2-94d3-4be7-992e-1d02f4259b34:hint\">\n            <value>Question Hint 5</value>\n          </text>\n          <text id=\"qing7bd3d9e8-4fbb-4b7f-9a35-e117a34c8ccc:label\">\n            <value>Integer Question Title 6</value>\n          </text>\n          <text id=\"qing7bd3d9e8-4fbb-4b7f-9a35-e117a34c8ccc:hint\">\n            <value>Question Hint 6</value>\n          </text>\n          <text id=\"qingc26f3f0f-eb3e-4375-aafa-e2ea0cd683a4:label\">\n            <value>Text Question Title 7</value>\n          </text>\n          <text id=\"qingc26f3f0f-eb3e-4375-aafa-e2ea0cd683a4:hint\">\n            <value>Question Hint 7</value>\n          </text>\n          <text id=\"BLANK\">\n            <value/>\n          </text>\n        </translation>\n      </itext>\n      <bind nodeset=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132\"/>\n      <bind nodeset=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132/header\" readonly=\"true()\" type=\"string\"/>\n      <bind nodeset=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132/qingacd1f34e-8f48-49cd-aaa0-24575878ead0\" type=\"string\"/>\n      <bind nodeset=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132/qing1507542f-3919-4e6f-b305-32ed0fa29565\" type=\"string\"/>\n      <bind nodeset=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132/qing204bedfc-ec15-4247-8457-e307db11f040\" type=\"string\"/>\n      <bind nodeset=\"/data/grp2ea471c2-9316-4166-9749-566f2ab4b45a\"/>\n      <bind nodeset=\"/data/grp2ea471c2-9316-4166-9749-566f2ab4b45a/header\" readonly=\"true()\" type=\"string\"/>\n      <bind nodeset=\"/data/grp2ea471c2-9316-4166-9749-566f2ab4b45a/qinga12441e2-79c0-4584-90ce-349923583726\" type=\"string\"/>\n      <bind nodeset=\"/data/grp2ea471c2-9316-4166-9749-566f2ab4b45a/qing281f13e2-94d3-4be7-992e-1d02f4259b34\" type=\"string\"/>\n      <bind nodeset=\"/data/grpa337aa68-0152-44b6-aae3-1fe91b55890e\"/>\n      <bind nodeset=\"/data/grpa337aa68-0152-44b6-aae3-1fe91b55890e/header\" readonly=\"true()\" type=\"string\"/>\n      <bind nodeset=\"/data/grpa337aa68-0152-44b6-aae3-1fe91b55890e/qing7bd3d9e8-4fbb-4b7f-9a35-e117a34c8ccc\" type=\"int\"/>\n      <bind nodeset=\"/data/grpa337aa68-0152-44b6-aae3-1fe91b55890e/qingc26f3f0f-eb3e-4375-aafa-e2ea0cd683a4\" type=\"string\"/>\n    </model>\n  </h:head>\n  <h:body>\n    <group ref=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132\">\n      <label ref=\"jr:itext('grp09d86f73-7b3c-44e5-9f49-bdae8ab50132:label')\"/>\n      <repeat nodeset=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132\">\n        <group appearance=\"field-list\">\n          <label ref=\"jr:itext('grp09d86f73-7b3c-44e5-9f49-bdae8ab50132:itemname')\"/>\n          <input ref=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132/header\">\n            <hint ref=\"jr:itext('grp09d86f73-7b3c-44e5-9f49-bdae8ab50132:hint')\"/>\n          </input>\n          <input ref=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132/qingacd1f34e-8f48-49cd-aaa0-24575878ead0\">\n            <label ref=\"jr:itext('qingacd1f34e-8f48-49cd-aaa0-24575878ead0:label')\"/>\n            <hint ref=\"jr:itext('qingacd1f34e-8f48-49cd-aaa0-24575878ead0:hint')\"/>\n          </input>\n          <input ref=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132/qing1507542f-3919-4e6f-b305-32ed0fa29565\">\n            <label ref=\"jr:itext('qing1507542f-3919-4e6f-b305-32ed0fa29565:label')\"/>\n            <hint ref=\"jr:itext('qing1507542f-3919-4e6f-b305-32ed0fa29565:hint')\"/>\n          </input>\n          <input ref=\"/data/grp09d86f73-7b3c-44e5-9f49-bdae8ab50132/qing204bedfc-ec15-4247-8457-e307db11f040\">\n            <label ref=\"jr:itext('qing204bedfc-ec15-4247-8457-e307db11f040:label')\"/>\n            <hint ref=\"jr:itext('qing204bedfc-ec15-4247-8457-e307db11f040:hint')\"/>\n          </input>\n        </group>\n      </repeat>\n    </group>\n    <group ref=\"/data/grp2ea471c2-9316-4166-9749-566f2ab4b45a\">\n      <label ref=\"jr:itext('grp2ea471c2-9316-4166-9749-566f2ab4b45a:label')\"/>\n      <group appearance=\"field-list\">\n        <input ref=\"/data/grp2ea471c2-9316-4166-9749-566f2ab4b45a/header\">\n          <hint ref=\"jr:itext('grp2ea471c2-9316-4166-9749-566f2ab4b45a:hint')\"/>\n        </input>\n        <input ref=\"/data/grp2ea471c2-9316-4166-9749-566f2ab4b45a/qinga12441e2-79c0-4584-90ce-349923583726\">\n          <label ref=\"jr:itext('qinga12441e2-79c0-4584-90ce-349923583726:label')\"/>\n          <hint ref=\"jr:itext('qinga12441e2-79c0-4584-90ce-349923583726:hint')\"/>\n        </input>\n        <input ref=\"/data/grp2ea471c2-9316-4166-9749-566f2ab4b45a/qing281f13e2-94d3-4be7-992e-1d02f4259b34\">\n          <label ref=\"jr:itext('qing281f13e2-94d3-4be7-992e-1d02f4259b34:label')\"/>\n          <hint ref=\"jr:itext('qing281f13e2-94d3-4be7-992e-1d02f4259b34:hint')\"/>\n        </input>\n      </group>\n    </group>\n    <group ref=\"grpa337aa68-0152-44b6-aae3-1fe91b55890e\">\n      <label ref=\"jr:itext('grpa337aa68-0152-44b6-aae3-1fe91b55890e:label')\"/>\n      <repeat nodeset=\"/data/grpa337aa68-0152-44b6-aae3-1fe91b55890e\">\n        <group>\n          <label ref=\"jr:itext('grpa337aa68-0152-44b6-aae3-1fe91b55890e:itemname')\"/>\n          <input ref=\"/data/grpa337aa68-0152-44b6-aae3-1fe91b55890e/header\">\n            <hint ref=\"jr:itext('grpa337aa68-0152-44b6-aae3-1fe91b55890e:hint')\"/>\n          </input>\n          <input ref=\"/data/grpa337aa68-0152-44b6-aae3-1fe91b55890e/qing7bd3d9e8-4fbb-4b7f-9a35-e117a34c8ccc\">\n            <label ref=\"jr:itext('qing7bd3d9e8-4fbb-4b7f-9a35-e117a34c8ccc:label')\"/>\n            <hint ref=\"jr:itext('qing7bd3d9e8-4fbb-4b7f-9a35-e117a34c8ccc:hint')\"/>\n          </input>\n          <input ref=\"/data/grpa337aa68-0152-44b6-aae3-1fe91b55890e/qingc26f3f0f-eb3e-4375-aafa-e2ea0cd683a4\">\n            <label ref=\"jr:itext('qingc26f3f0f-eb3e-4375-aafa-e2ea0cd683a4:label')\"/>\n            <hint ref=\"jr:itext('qingc26f3f0f-eb3e-4375-aafa-e2ea0cd683a4:hint')\"/>\n          </input>\n        </group>\n      </repeat>\n    </group>\n  </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/repeat_group_new.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n  <h:head>\n    <h:title>RepeatGroupNew</h:title>\n    <model>\n      <instance>\n        <data id=\"build_Untitled-Form_150757354889\">\n          <meta>\n            <instanceID/>\n          </meta>\n          <RepeatGroup jr:template=\"\">\n            <Name/>\n            <Age/>\n          </RepeatGroup>\n        </data>\n      </instance>\n      <itext>\n        <translation lang=\"English\">\n          <text id=\"/data/RepeatGroup:label\">\n            <value>RepeatGroup</value>\n          </text>\n          <text id=\"/data/RepeatGroup/Name:label\">\n            <value>Name</value>\n          </text>\n          <text id=\"/data/RepeatGroup/Age:label\">\n            <value>Age</value>\n          </text>\n        </translation>\n      </itext>\n      <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n      <bind nodeset=\"/data/RepeatGroup/Name\" type=\"string\"/>\n      <bind nodeset=\"/data/RepeatGroup/Age\" type=\"int\"/>\n    </model>\n  </h:head>\n  <h:body>\n    <group>\n      <label>People</label>\n      <repeat nodeset=\"/data/RepeatGroup\">\n\t<group>\n        <label>Person: <output value=\"/data/RepeatGroup/Name\"/></label>\n        <input ref=\"/data/RepeatGroup/Name\">\n          <label ref=\"jr:itext('/data/RepeatGroup/Name:label')\"/>\n        </input>\n        <input ref=\"/data/RepeatGroup/Age\">\n          <label ref=\"jr:itext('/data/RepeatGroup/Age:label')\"/>\n        </input>\n\t</group>\n      </repeat>\n    </group>\n\n  </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/repeat_group_wrapped_with_a_regular_group.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:ev=\"http://www.w3.org/2001/xml-events\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>Repeat group wrapped with a regular group</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"groups_crash\">\n                    <outer>\n                        <inner jr:template=\"\">\n                            <name/>\n                        </inner>\n                        <inner>\n                            <name/>\n                        </inner>\n                    </outer>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/outer/inner/name\" type=\"string\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group ref=\"/data/outer\">\n            <label>Outer</label>\n            <group ref=\"/data/outer/inner\">\n                <label>Inner</label>\n                <repeat nodeset=\"/data/outer/inner\">\n                    <input ref=\"/data/outer/inner/name\">\n                        <label>Name</label>\n                    </input>\n                </repeat>\n            </group>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/repeat_groups.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\n    <h:head>\n        <h:title>repeatGroups</h:title>\n        <model>\n            <instance>\n                <data id=\"build_repeatGroups_1567117575\">\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                    <text0>0</text0>\n                    <repeatGroup>\n                        <text1>1</text1>\n                    </repeatGroup>\n                    <repeatGroup>\n                        <text1>2</text1>\n                    </repeatGroup>\n                    <repeatGroup>\n                        <text1>3</text1>\n                    </repeatGroup>\n                    <repeatGroup>\n                        <text1>4</text1>\n                    </repeatGroup>\n                    <repeatGroupFieldList>\n                        <number1>1</number1>\n                    </repeatGroupFieldList>\n                    <repeatGroupFieldList>\n                        <number1>2</number1>\n                    </repeatGroupFieldList>\n                    <repeatGroupFieldList>\n                        <number1>3</number1>\n                    </repeatGroupFieldList>\n                    <repeatGroupFieldList>\n                        <number1>4</number1>\n                    </repeatGroupFieldList>\n                </data>\n            </instance>\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/text0\">\n                        <value>text0</value>\n                    </text>\n                    <text id=\"/data/repeatGroup:label\">\n                        <value>repeatGroup</value>\n                    </text>\n                    <text id=\"/data/repeatGroup/text1:label\">\n                        <value>text1</value>\n                    </text>\n                    <text id=\"/data/repeatGroupFieldList:label\">\n                        <value>repeatGroupFieldList</value>\n                    </text>\n                    <text id=\"/data/repeatGroupFieldList/number1:label\">\n                        <value>number1</value>\n                    </text>\n                </translation>\n            </itext>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\n            <bind nodeset=\"/data/repeatGroup/text1\" type=\"string\"/>\n            <bind nodeset=\"/data/repeatGroupFieldList/number1\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/text0\">\n            <label ref=\"jr:itext('/data/text0')\"/>\n        </input>\n        <group ref=\"/data/repeatGroup\">\n            <label ref=\"jr:itext('/data/repeatGroup:label')\"/>\n            <repeat nodeset=\"/data/repeatGroup\">\n                <input ref=\"/data/repeatGroup/text1\">\n                    <label ref=\"jr:itext('/data/repeatGroup/text1:label')\"/>\n                </input>\n            </repeat>\n        </group>\n        <group ref=\"/data/repeatGroupFieldList\">\n            <label ref=\"jr:itext('/data/repeatGroupFieldList:label')\"/>\n            <repeat appearance=\"field-list\" nodeset=\"/data/repeatGroupFieldList\">\n                <input ref=\"/data/repeatGroupFieldList/number1\">\n                    <label ref=\"jr:itext('/data/repeatGroupFieldList/number1:label')\"/>\n                </input>\n            </repeat>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/repeat_in_field_list.xml",
    "content": "<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:jr=\"http://openrosa.org/javarosa\">\r\n  <h:head>\r\n    <h:title>repeat_in_field_list</h:title>\r\n    <model>\r\n      <instance>\r\n        <data id=\"repeat_in_field_list\">\r\n          <meta>\r\n            <instanceID/>\r\n          </meta>\r\n          <g1>\r\n            <text1/>\r\n            <g2 jr:template=\"\">\r\n              <text2/>\r\n            </g2>\r\n          </g1>\r\n          <text3/>\r\n        </data>\r\n      </instance>\r\n      <itext>\r\n        <translation lang=\"English\">\r\n          <text id=\"/data/g1:label\">\r\n            <value>g1</value>\r\n          </text>\r\n          <text id=\"/data/g1/text1:label\">\r\n            <value>text1</value>\r\n          </text>\r\n          <text id=\"/data/g1/g2:label\">\r\n            <value>g2</value>\r\n          </text>\r\n          <text id=\"/data/g1/g2/text2:label\">\r\n            <value>text2</value>\r\n          </text>\r\n          <text id=\"/data/text3:label\">\r\n            <value>text3</value>\r\n          </text>\r\n        </translation>\r\n      </itext>\r\n      <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" calculate=\"concat('uuid:', uuid())\"/>\r\n      <bind nodeset=\"/data/g1/text1\" type=\"string\"/>\r\n      <bind nodeset=\"/data/g1/g2/text2\" type=\"string\"/>\r\n      <bind nodeset=\"/data/text3\" type=\"string\"/>\r\n    </model>\r\n  </h:head>\r\n  <h:body>\r\n    <group appearance=\"field-list\">\r\n      <label ref=\"jr:itext('/data/g1:label')\"/>\r\n      <input ref=\"/data/g1/text1\">\r\n        <label ref=\"jr:itext('/data/g1/text1:label')\"/>\r\n      </input>\r\n      <group>\r\n        <label ref=\"jr:itext('/data/g1/g2:label')\"/>\r\n        <repeat nodeset=\"/data/g1/g2\">\r\n          <input ref=\"/data/g1/g2/text2\">\r\n            <label ref=\"jr:itext('/data/g1/g2/text2:label')\"/>\r\n          </input>\r\n        </repeat>\r\n      </group>\r\n    </group>\r\n    <input ref=\"/data/text3\">\r\n      <label ref=\"jr:itext('/data/text3:label')\"/>\r\n    </input>\r\n  </h:body>\r\n</h:html>\r\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/repeat_without_label.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:ev=\"http://www.w3.org/2001/xml-events\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>Repeat without label</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"repeat_without_label\">\n                    <repeat_no_label jr:template=\"\">\n                        <name/>\n                    </repeat_no_label>\n                    <repeat_no_label>\n                        <name/>\n                    </repeat_no_label>\n                    <repeat_blank_label jr:template=\"\">\n                        <name/>\n                    </repeat_blank_label>\n                    <repeat_blank_label>\n                        <name/>\n                    </repeat_blank_label>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/repeat_no_label/name\" type=\"string\"/>\n            <bind nodeset=\"/data/repeat_blank_label/name\" type=\"string\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group ref=\"/data/repeat_no_label\">\n            <label/>\n            <repeat nodeset=\"/data/repeat_no_label\">\n                <input ref=\"/data/repeat_no_label/name\">\n                    <label>Name</label>\n                </input>\n            </repeat>\n        </group>\n        <group ref=\"/data/repeat_blank_label\">\n            <label></label>\n            <repeat nodeset=\"/data/repeat_blank_label\">\n                <input ref=\"/data/repeat_blank_label/name\">\n                    <label>Name</label>\n                </input>\n            </repeat>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/requiredQuestionInFieldList.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>requiredQuestionInFieldList</h:title>\n        <model>\n            <instance>\n                <required id=\"requiredQuestionInFieldList\">\n                    <group>\n                        <text/>\n                        <text2/>\n                    </group>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </required>\n            </instance>\n            <bind jr:requiredMsg=\"Custom required message\" nodeset=\"/required/group/text\" required=\"true()\" type=\"string\"/>\n            <bind jr:requiredMsg=\"Custom required message2\" nodeset=\"/required/group/text2\" required=\"true()\" type=\"string\"/>\n            <bind calculate=\"concat('uuid:', uuid())\" nodeset=\"/required/meta/instanceID\" readonly=\"true()\" type=\"string\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\" ref=\"/required/group\">\n            <input ref=\"/required/group/text\">\n                <label>Foo</label>\n            </input>\n            <input ref=\"/required/group/text2\">\n                <label>Bar</label>\n            </input>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/required_question_with_audio.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:ev=\"http://www.w3.org/2001/xml-events\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>required_question_with_audio</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <itext>\n                <translation lang=\"default\" default=\"true()\">\n                    <text id=\"/data/q1:label\">\n                        <value>Required question with audio</value>\n                        <value form=\"audio\">jr://audio/blah.mp3</value>\n                    </text>\n                </translation>\n            </itext>\n            <instance>\n                <data id=\"required_question_with_audio\">\n                    <q1/>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/q1\" type=\"string\" required=\"true()\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/q1\">\n            <label ref=\"jr:itext('/data/q1:label')\"/>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/required_question_with_custom_error_message.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:ev=\"http://www.w3.org/2001/xml-events\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>required_question_with_custom_error_message</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"required_question_with_custom_error_message\">\n                    <q1/>\n                    <meta>\n                        <instanceID/>\n                        <orx:audit/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/orx:meta/audit\" type=\"binary\" />\n            <bind nodeset=\"/data/q1\" type=\"string\" required=\"true()\" jr:requiredMsg=\"Custom message\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/q1\">\n            <label>Required question</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/search_and_select.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\"\n    xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>search_and_select</h:title>\n        <model>\n            <instance>\n                <search_and_select id=\"search_and_select\" version=\"103\">\n                    <encabezado>\n                        <nombre />\n                        <nombre2 />\n                        <note_fruit />\n                    </encabezado>\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </search_and_select>\n            </instance>\n            <bind nodeset=\"/search_and_select/encabezado/nombre\" type=\"select1\" />\n            <bind nodeset=\"/search_and_select/encabezado/nombre2\" type=\"select1\" />\n            <bind nodeset=\"/search_and_select/encabezado/note_fruit\" readonly=\"true()\"\n                type=\"string\" />\n            <bind nodeset=\"/search_and_select/meta/instanceID\" readonly=\"true()\" type=\"string\"\n                jr:preload=\"uid\" />\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\" ref=\"/search_and_select/encabezado\">\n            <label>Registro de sondeo</label>\n            <select1 appearance=\"minimal search('nombre')\"\n                ref=\"/search_and_select/encabezado/nombre\">\n                <label>seleccione un nombre</label>\n                <item>\n                    <label>name</label>\n                    <value>name_key</value>\n                </item>\n            </select1>\n            <select1 appearance=\"minimal search('nombre2')\"\n                ref=\"/search_and_select/encabezado/nombre2\">\n                <label>seleccione un nombre</label>\n                <item>\n                    <label>name</label>\n                    <value>name_key</value>\n                </item>\n            </select1>\n            <input ref=\"/search_and_select/encabezado/note_fruit\">\n                <label>The fruit\n                    <output value=\" /search_and_select/encabezado/nombre \" />\n                    pulled from csv\n                </label>\n            </input>\n        </group>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/selectOneExternal.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>selectOneExternal</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <itext>\n                <translation lang=\"English\">\n                    <text id=\"/data/state:label\">\n                        <value>state</value>\n                    </text>\n                    <text id=\"/data/state/a1:label\">\n                        <value>Texas</value>\n                    </text>\n                    <text id=\"/data/state/a2:label\">\n                        <value>Washington</value>\n                    </text>\n                    <text id=\"/data/county:label\">\n                        <value>county</value>\n                    </text>\n                    <text id=\"/data/city:label\">\n                        <value>city</value>\n                    </text>\n                    <text id=\"/data/state2:label\">\n                        <value>state</value>\n                    </text>\n                    <text id=\"/data/state2/a1:label\">\n                        <value>Texas</value>\n                    </text>\n                    <text id=\"/data/state2/a2:label\">\n                        <value>Washington</value>\n                    </text>\n                    <text id=\"/data/county2:label\">\n                        <value>county</value>\n                    </text>\n                    <text id=\"/data/city2:label\">\n                        <value>city</value>\n                    </text>\n                </translation>\n                <translation lang=\"French\">\n                    <text id=\"/data/state:label\">\n                        <value>Le state</value>\n                    </text>\n                    <text id=\"/data/state/a1:label\">\n                        <value>Le Texas</value>\n                    </text>\n                    <text id=\"/data/state/a2:label\">\n                        <value>La Washington</value>\n                    </text>\n                    <text id=\"/data/county:label\">\n                        <value>Pays</value>\n                    </text>\n                    <text id=\"/data/city:label\">\n                        <value>Ville</value>\n                    </text>\n                    <text id=\"/data/state2:label\">\n                        <value>Le state</value>\n                    </text>\n                    <text id=\"/data/state2/a1:label\">\n                        <value>Le Texas</value>\n                    </text>\n                    <text id=\"/data/state2/a2:label\">\n                        <value>La Washington</value>\n                    </text>\n                    <text id=\"/data/county2:label\">\n                        <value>Pays</value>\n                    </text>\n                    <text id=\"/data/city2:label\">\n                        <value>Ville</value>\n                    </text>\n                </translation>\n            </itext>\n            <instance>\n                <data id=\"cascading_select_test\">\n                    <state />\n                    <county />\n                    <city />\n                    <state2 />\n                    <county2 />\n                    <city2 />\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/state\" type=\"string\" />\n            <bind nodeset=\"/data/county\" type=\"string\" />\n            <bind nodeset=\"/data/city\" type=\"string\" />\n            <bind nodeset=\"/data/state2\" type=\"string\" />\n            <bind nodeset=\"/data/county2\" type=\"string\" />\n            <bind nodeset=\"/data/city2\" type=\"string\" />\n            <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\" />\n        </model>\n    </h:head>\n    <h:body>\n        <select1 ref=\"/data/state\">\n            <label ref=\"jr:itext('/data/state:label')\" />\n            <item>\n                <label ref=\"jr:itext('/data/state/a1:label')\" />\n                <value>a1</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/data/state/a2:label')\" />\n                <value>a2</value>\n            </item>\n        </select1>\n        <input query=\"instance('counties')/root/item[state= /data/state ]\" ref=\"/data/county\">\n            <label ref=\"jr:itext('/data/county:label')\" />\n        </input>\n        <input query=\"instance('cities')/root/item[state= /data/state  and county= /data/county ]\" ref=\"/data/city\">\n            <label ref=\"jr:itext('/data/city:label')\" />\n        </input>\n        <select1 appearance=\"minimal\" ref=\"/data/state2\">\n            <label ref=\"jr:itext('/data/state2:label')\" />\n            <hint>minimal</hint>\n            <item>\n                <label ref=\"jr:itext('/data/state2/a1:label')\" />\n                <value>a1</value>\n            </item>\n            <item>\n                <label ref=\"jr:itext('/data/state2/a2:label')\" />\n                <value>a2</value>\n            </item>\n        </select1>\n        <input appearance=\"minimal\" query=\"instance('counties')/root/item[state= /data/state2 ]\" ref=\"/data/county2\">\n            <label ref=\"jr:itext('/data/county2:label')\" />\n            <hint>minimal</hint>\n        </input>\n        <input appearance=\"minimal autocomplete\" query=\"instance('cities')/root/item[state= /data/state2  and county= /data/county2 ]\" ref=\"/data/city2\">\n            <label ref=\"jr:itext('/data/city2:label')\" />\n            <hint>minimal autocomplete</hint>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/select_one_external.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <h:head>\n    <h:title>cascading select test</h:title>\n    <model odk:xforms-version=\"1.0.0\">\n      <itext>\n        <translation lang=\"English\">\n          <text id=\"/data/state:label\">\n            <value>state</value>\n          </text>\n          <text id=\"/data/state/a1:label\">\n            <value>Texas</value>\n          </text>\n          <text id=\"/data/state/a2:label\">\n            <value>Washington</value>\n          </text>\n          <text id=\"/data/county:label\">\n            <value>county</value>\n          </text>\n          <text id=\"/data/city:label\">\n            <value>city</value>\n          </text>\n        </translation>\n      </itext>\n      <instance>\n        <data id=\"cascading_select_test\">\n          <state />\n          <county />\n          <city />\n          <meta>\n            <instanceID />\n          </meta>\n        </data>\n      </instance>\n      <bind nodeset=\"/data/state\" type=\"string\" />\n      <bind nodeset=\"/data/county\" type=\"string\" />\n      <bind nodeset=\"/data/city\" type=\"string\" />\n      <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\" />\n    </model>\n  </h:head>\n  <h:body>\n    <select1 ref=\"/data/state\">\n      <label ref=\"jr:itext('/data/state:label')\" />\n      <item>\n        <label ref=\"jr:itext('/data/state/a1:label')\" />\n        <value>a1</value>\n      </item>\n      <item>\n        <label ref=\"jr:itext('/data/state/a2:label')\" />\n        <value>a2</value>\n      </item>\n    </select1>\n    <input query=\"instance('counties')/root/item[state= /data/state ]\" ref=\"/data/county\">\n      <label ref=\"jr:itext('/data/county:label')\" />\n    </input>\n    <input query=\"instance('cities')/root/item[state= /data/state  and county= /data/county ]\" ref=\"/data/city\">\n      <label ref=\"jr:itext('/data/city:label')\" />\n    </input>\n  </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/setgeopoint-action.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\"\n        xmlns:h=\"http://www.w3.org/1999/xhtml\"\n        xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>setgeopoint-action-instance-load</h:title>\n        <model>\n            <instance>\n                <data id=\"setgeopoint-action-instance-load\">\n                    <location/>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/location\" type=\"string\" />\n            <odk:setgeopoint ref=\"/data/location\" event=\"odk-instance-first-load\" />\n        </model>\n    </h:head>\n    <h:body />\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/simple-search-external-csv.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>simple-search-external-csv</h:title>\n        <model>\n            <instance>\n                <simple-search-external-csv id=\"simple-search-external-csv\">\n                    <fruit1 />\n                    <note_fruit />\n                </simple-search-external-csv>\n            </instance>\n            <bind nodeset=\"/simple-search-external-csv/fruit1\" type=\"select1\" />\n            <bind nodeset=\"/simple-search-external-csv/note_fruit\" readonly=\"true()\" type=\"string\" />\n        </model>\n    </h:head>\n    <h:body>\n        <select1 appearance=\"search('simple-search-external-csv-fruits')\" ref=\"/simple-search-external-csv/fruit1\">\n            <label>Select from a CSV using search() appearance/function</label>\n            <item>\n                <label>name</label>\n                <value>name_key</value>\n            </item>\n        </select1>\n        <input ref=\"/simple-search-external-csv/note_fruit\">\n            <label>\n                The fruit <output value=\" /simple-search-external-csv/fruit1 \" /> pulled from csv\n            </label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/single-geopoint.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>Single geopoint</h:title>\n        <model>\n            <instance>\n                <data id=\"single-geopoint\">\n                    <name />\n                    <location />\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/name\" type=\"string\" />\n            <bind nodeset=\"/data/location\" type=\"geopoint\" />\n            <bind nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"\n                jr:preload=\"uid\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>Name</label>\n        </input>\n\n        <input ref=\"/data/location\">\n            <label>Location</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/start-geopoint.xml",
    "content": "<?xml version=\"1.0\"?><h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><h:head><h:title>start-geopoint</h:title><model odk:xforms-version=\"1.0.0\"><instance><data id=\"start-geopoint\"><start-geopoint/><meta><instanceID/></meta></data></instance><bind nodeset=\"/data/start-geopoint\" type=\"geopoint\"/><bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"/><odk:setgeopoint event=\"odk-instance-first-load\" ref=\"/data/start-geopoint\"/></model></h:head><h:body/></h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/string_widgets_in_field_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<h:html xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>fl</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"stringwidgets\">\n                    <my-group>\n                        <question1 />\n                        <question2 />\n                    </my-group>\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/my-group/question1\" type=\"string\" />\n            <bind nodeset=\"/data/my-group/question2\" type=\"int\" />\n            <bind jr:preload=\"uid\" nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\" />\n        </model>\n    </h:head>\n    <h:body>\n        <group appearance=\"field-list\" ref=\"/data/my-group\">\n            <label>My group</label>\n            <input ref=\"/data/my-group/question1\">\n                <label>Question1</label>\n            </input>\n            <input ref=\"/data/my-group/question2\">\n                <label>Question2</label>\n            </input>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/track-changes-reason-on-edit.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\"\n    xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>Track Changes Reason</h:title>\n        <model>\n            <instance>\n                <data id=\"track-changes-reason\">\n                    <what_up />\n                    <meta>\n                        <audit />\n                        <instanceID />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/what_up\" type=\"string\" />\n            <bind nodeset=\"/data/meta/audit\" type=\"binary\" odk:track-changes=\"true\" />\n            <bind nodeset=\"/data/meta/audit\" type=\"binary\" odk:track-changes-reasons=\"on-form-edit\" />\n            <bind nodeset=\"/data/meta/instanceID\" readonly=\"true()\" type=\"string\"\n                jr:preload=\"uid\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/what_up\">\n            <label>What up?</label>\n        </input>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/two-question-audit-track-changes.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\"\n    xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>One Question Audit Track Changes</h:title>\n        <model>\n            <instance>\n                <data id=\"one_question_audit\" orx:version=\"1\">\n                    <age />\n                    <name />\n                    <meta>\n                        <audit />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/meta/audit\" type=\"binary\" odk:track-changes=\"true\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/age\">\n            <label>What is your age?</label>\n        </input>\n        <input ref=\"/data/name\">\n            <label>What is your name?</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/two-question-audit.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>Two Question</h:title>\n        <model>\n            <instance>\n                <data id=\"two_question\">\n                    <name/>\n                    <age/>\n                    <meta>\n                        <audit />\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"name\" type=\"string\"/>\n            <bind nodeset=\"age\" type=\"int\"/>\n            <bind nodeset=\"/data/meta/audit\" type=\"binary\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>What is your name?</label>\n        </input>\n        <input ref=\"/data/age\">\n            <label>What is your age?</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/two-question-external.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>Two Question</h:title>\n        <model>\n            <instance>\n                <data id=\"two_question\">\n                    <name/>\n                    <age/>\n                </data>\n            </instance>\n            <bind nodeset=\"name\" type=\"string\"/>\n            <bind nodeset=\"age\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>What is your name?</label>\n        </input>\n        <input ref=\"/data/age\" appearance=\"ex:com.example.EXAMPLE\">\n            <label>What is your age?</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/two-question-required.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>Two Question Required</h:title>\n        <model>\n            <instance>\n                <data id=\"two_question\">\n                    <name/>\n                    <age/>\n                </data>\n            </instance>\n            <bind nodeset=\"name\" type=\"string\"/>\n            <bind nodeset=\"age\" required=\"true()\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>What is your name?</label>\n        </input>\n        <input ref=\"/data/age\">\n            <label>What is your age?</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/two-question-save-incomplete-required.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>Two Question Save Incomplete Required</h:title>\n        <model>\n            <instance>\n                <data id=\"two_question\">\n                    <name/>\n                    <age/>\n                </data>\n            </instance>\n            <bind nodeset=\"name\" type=\"string\"/>\n            <bind nodeset=\"age\" type=\"int\" saveIncomplete=\"true()\" required=\"true()\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>What is your name?</label>\n        </input>\n        <input ref=\"/data/age\">\n            <label>[saveIncomplete] What is your age?</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/two-question-save-incomplete.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>Two Question Save Incomplete</h:title>\n        <model>\n            <instance>\n                <data id=\"two_question\">\n                    <name/>\n                    <age/>\n                </data>\n            </instance>\n            <bind nodeset=\"name\" type=\"string\"/>\n            <bind nodeset=\"age\" type=\"int\" saveIncomplete=\"true()\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>What is your name?</label>\n        </input>\n        <input ref=\"/data/age\">\n            <label>[saveIncomplete] What is your age?</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/two-question-updated.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>Two Question Updated</h:title>\n        <model>\n            <instance>\n                <data id=\"two_question\">\n                    <name/>\n                    <age/>\n                </data>\n            </instance>\n            <bind nodeset=\"name\" type=\"string\"/>\n            <bind nodeset=\"age\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>What is your name?</label>\n        </input>\n        <input ref=\"/data/age\">\n            <label>What is your age?</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/two-question.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns=\"http://www.w3.org/2002/xforms\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\" xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n    <h:head>\n        <h:title>Two Question</h:title>\n        <model>\n            <instance>\n                <data id=\"two_question\">\n                    <name/>\n                    <age/>\n                </data>\n            </instance>\n            <bind nodeset=\"name\" type=\"string\"/>\n            <bind nodeset=\"age\" type=\"int\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/data/name\">\n            <label>What is your name?</label>\n        </input>\n        <input ref=\"/data/age\">\n            <label>What is your age?</label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/forms/two-questions-in-group.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html\n    xmlns=\"http://www.w3.org/2002/xforms\"\n    xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:ev=\"http://www.w3.org/2001/xml-events\"\n    xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns:jr=\"http://openrosa.org/javarosa\"\n    xmlns:orx=\"http://openrosa.org/xforms\"\n    xmlns:odk=\"http://www.opendatakit.org/xforms\">\n    <h:head>\n        <h:title>two-questions-in-group</h:title>\n        <model odk:xforms-version=\"1.0.0\">\n            <instance>\n                <data id=\"two-questions-in-group\">\n                    <group>\n                        <first_name/>\n                        <last_name/>\n                    </group>\n                    <meta>\n                        <instanceID/>\n                    </meta>\n                </data>\n            </instance>\n            <bind nodeset=\"/data/group/first_name\" type=\"string\"/>\n            <bind nodeset=\"/data/group/last_name\" type=\"string\"/>\n            <bind nodeset=\"/data/meta/instanceID\" type=\"string\" readonly=\"true()\" jr:preload=\"uid\"/>\n        </model>\n    </h:head>\n    <h:body>\n        <group ref=\"/data/group\">\n            <label>Name</label>\n            <input ref=\"/data/group/first_name\">\n                <label>First name</label>\n            </input>\n            <input ref=\"/data/group/last_name\">\n                <label>Last name</label>\n            </input>\n        </group>\n    </h:body>\n</h:html>"
  },
  {
    "path": "test-forms/src/main/resources/forms/validate.xml",
    "content": "<?xml version=\"1.0\"?>\n<h:html xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:h=\"http://www.w3.org/1999/xhtml\"\n    xmlns:jr=\"http://openrosa.org/javarosa\" xmlns:odk=\"http://www.opendatakit.org/xforms\"\n    xmlns:orx=\"http://openrosa.org/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n    xmlns=\"http://www.w3.org/2002/xforms\">\n    <h:head>\n        <h:title>validate</h:title>\n        <model>\n            <instance>\n                <validate id=\"validate\">\n                    <year>2019</year>\n                    <result />\n                    <showresult />\n                    <meta>\n                        <instanceID />\n                    </meta>\n                </validate>\n            </instance>\n            <bind nodeset=\"/validate/year\" type=\"int\" />\n            <bind calculate=\"decimal-date-time(concat( /validate/year ,&quot;-01-01&quot;))\"\n                nodeset=\"/validate/result\" type=\"string\" />\n            <bind nodeset=\"/validate/showresult\" readonly=\"true()\" type=\"string\" />\n            <bind nodeset=\"/validate/meta/instanceID\" readonly=\"true()\" type=\"string\"\n                jr:preload=\"uid\" />\n        </model>\n    </h:head>\n    <h:body>\n        <input ref=\"/validate/year\">\n            <label>year</label>\n        </input>\n        <input ref=\"/validate/showresult\">\n            <label>result = <output value=\" /validate/result \" /></label>\n        </input>\n    </h:body>\n</h:html>\n"
  },
  {
    "path": "test-forms/src/main/resources/media/external-csv-search-produce.csv",
    "content": "name,label\nartichoke,Artichoke\napple,Apple\nbanana,Banana\nblueberry,Blueberry\ncherimoya,Cherimoya\ncarrot,Carrot"
  },
  {
    "path": "test-forms/src/main/resources/media/external_csv_cities.csv",
    "content": "name,label,country\nams,Amsterdam,nl\nden,Denver,usa\nnyc,New York City,usa\nla,Los Angeles,usa\nrot,Rotterdam,nl\ndro,Dronten,nl\n"
  },
  {
    "path": "test-forms/src/main/resources/media/external_csv_countries.csv",
    "content": "name,label\nnl,The Netherlands\nusa,United States"
  },
  {
    "path": "test-forms/src/main/resources/media/external_csv_neighbourhoods.csv",
    "content": "name,label,country,city\nbronx,Bronx,usa,nyc\nharlem,Harlem,usa,nyc\nbelair,Bel Air,usa,la\nwes,Westerpark,nl,ams\nparkhill,Park Hill,usa,den\nhaven,Harbor,nl,rot\ndam,Dam,nl,ams\ncentrum,Downtown,nl,rot\nhaven,Harbor,nl,dro"
  },
  {
    "path": "test-forms/src/main/resources/media/external_data.csv",
    "content": "name,label\none,One\ntwo,Two\nthree,Three\n"
  },
  {
    "path": "test-forms/src/main/resources/media/external_data.xml",
    "content": "<root>\n    <item>\n        <name>one</name>\n        <label>One</label>\n    </item>\n    <item>\n        <name>two</name>\n        <label>Two</label>\n    </item>\n    <item>\n        <name>three</name>\n        <label>Three</label>\n    </item>\n</root>\n"
  },
  {
    "path": "test-forms/src/main/resources/media/external_data_10.xml",
    "content": "<root>\n    <item>\n        <itextId>static_instance-second-0</itextId>\n        <name>aa</name>\n        <first>a</first>\n    </item>\n    <item>\n        <itextId>static_instance-second-1</itextId>\n        <name>ab</name>\n        <first>a</first>\n    </item>\n    <item>\n        <itextId>static_instance-second-2</itextId>\n        <name>ac</name>\n        <first>a</first>\n    </item>\n    <item>\n        <itextId>static_instance-second-3</itextId>\n        <name>ba</name>\n        <first>b</first>\n    </item>\n    <item>\n        <itextId>static_instance-second-4</itextId>\n        <name>bb</name>\n        <first>b</first>\n    </item>\n    <item>\n        <itextId>static_instance-second-5</itextId>\n        <name>bc</name>\n        <first>b</first>\n    </item>\n    <item>\n        <itextId>static_instance-second-6</itextId>\n        <name>ca</name>\n        <first>c</first>\n    </item>\n    <item>\n        <itextId>static_instance-second-7</itextId>\n        <name>cb</name>\n        <first>c</first>\n    </item>\n    <item>\n        <itextId>static_instance-second-8</itextId>\n        <name>cc</name>\n        <first>c</first>\n    </item>\n</root>\n"
  },
  {
    "path": "test-forms/src/main/resources/media/external_data_broken.csv",
    "content": "name,label\none,\"One\ntwo,Two\nthree,Three\n"
  },
  {
    "path": "test-forms/src/main/resources/media/fruits.csv",
    "content": "name_key,name\nmango,Mango\noranges,Oranges\nstrawberries,Strawberries"
  },
  {
    "path": "test-forms/src/main/resources/media/itemSets.csv",
    "content": "list_name,name,label,1a,2a\n1a,Vendas agrícolas,Vendas agrícolas\n1a,Venda de animais,Venda de animais\n1a,Salário,Salário\n1a,Pensão,Pensão\n1a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n1a,Cobrança de rendas,Cobrança de rendas\n1a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n1a,Outro,Outro\n2a,Venda de animais,Venda de animais,Vendas agrícolas\n2a,Salário,Salário,Vendas agrícolas\n2a,Pensão,Pensão,Vendas agrícolas\n2a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas\n2a,Cobrança de rendas,Cobrança de rendas,Vendas agrícolas\n2a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas\n2a,Outro,Outro,Vendas agrícolas\n2a,Vendas agrícolas,Vendas agrícolas,Venda de animais\n2a,Salário,Salário,Venda de animais\n2a,Pensão,Pensão,Venda de animais\n2a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais\n2a,Cobrança de rendas,Cobrança de rendas,Venda de animais\n2a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais\n2a,Outro,Outro,Venda de animais\n2a,Vendas agrícolas,Vendas agrícolas,Salário\n2a,Venda de animais,Venda de animais,Salário\n2a,Pensão,Pensão,Salário\n2a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário\n2a,Cobrança de rendas,Cobrança de rendas,Salário\n2a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário\n2a,Outro,Outro,Salário\n2a,Vendas agrícolas,Vendas agrícolas,Pensão\n2a,Venda de animais,Venda de animais,Pensão\n2a,Salário,Salário,Pensão\n2a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão\n2a,Cobrança de rendas,Cobrança de rendas,Pensão\n2a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão\n2a,Outro,Outro,Pensão\n2a,Vendas agrícolas,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n2a,Venda de animais,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n2a,Salário,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n2a,Pensão,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n2a,Cobrança de rendas,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n2a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n2a,Outro,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n2a,Vendas agrícolas,Vendas agrícolas,Cobrança de rendas\n2a,Venda de animais,Venda de animais,Cobrança de rendas\n2a,Salário,Salário,Cobrança de rendas\n2a,Pensão,Pensão,Cobrança de rendas\n2a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas\n2a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas\n2a,Outro,Outro,Cobrança de rendas\n2a,Vendas agrícolas,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n2a,Venda de animais,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n2a,Salário,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n2a,Pensão,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n2a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n2a,Cobrança de rendas,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n2a,Outro,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n2a,Vendas agrícolas,Vendas agrícolas,Outro\n2a,Venda de animais,Venda de animais,Outro\n2a,Salário,Salário,Outro\n2a,Pensão,Pensão,Outro\n2a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro\n2a,Cobrança de rendas,Cobrança de rendas,Outro\n2a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro\n3a,Salário,Salário,Vendas agrícolas,Venda de animais\n3a,Pensão,Pensão,Vendas agrícolas,Venda de animais\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas,Venda de animais\n3a,Cobrança de rendas,Cobrança de rendas,Vendas agrícolas,Venda de animais\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas,Venda de animais\n3a,Outro,Outro,Vendas agrícolas,Venda de animais\n3a,Venda de animais,Venda de animais,Vendas agrícolas,Salário\n3a,Pensão,Pensão,Vendas agrícolas,Salário\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas,Salário\n3a,Cobrança de rendas,Cobrança de rendas,Vendas agrícolas,Salário\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas,Salário\n3a,Outro,Outro,Vendas agrícolas,Salário\n3a,Venda de animais,Venda de animais,Vendas agrícolas,Pensão\n3a,Salário,Salário,Vendas agrícolas,Pensão\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas,Pensão\n3a,Cobrança de rendas,Cobrança de rendas,Vendas agrícolas,Pensão\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas,Pensão\n3a,Outro,Outro,Vendas agrícolas,Pensão\n3a,Venda de animais,Venda de animais,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Salário,Salário,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Pensão,Pensão,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Outro,Outro,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Venda de animais,Venda de animais,Vendas agrícolas,Cobrança de rendas\n3a,Salário,Salário,Vendas agrícolas,Cobrança de rendas\n3a,Pensão,Pensão,Vendas agrícolas,Cobrança de rendas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas,Cobrança de rendas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas,Cobrança de rendas\n3a,Outro,Outro,Vendas agrícolas,Cobrança de rendas\n3a,Venda de animais,Venda de animais,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Salário,Salário,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Pensão,Pensão,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Outro,Outro,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Venda de animais,Venda de animais,Vendas agrícolas,Outro\n3a,Salário,Salário,Vendas agrícolas,Outro\n3a,Pensão,Pensão,Vendas agrícolas,Outro\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas,Outro\n3a,Cobrança de rendas,Cobrança de rendas,Vendas agrícolas,Outro\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas,Outro\n3a,Salário,Salário,Venda de animais,Vendas agrícolas\n3a,Pensão,Pensão,Venda de animais,Vendas agrícolas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais,Vendas agrícolas\n3a,Cobrança de rendas,Cobrança de rendas,Venda de animais,Vendas agrícolas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais,Vendas agrícolas\n3a,Outro,Outro,Venda de animais,Vendas agrícolas\n3a,Vendas agrícolas,Vendas agrícolas,Venda de animais,Salário\n3a,Pensão,Pensão,Venda de animais,Salário\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais,Salário\n3a,Cobrança de rendas,Cobrança de rendas,Venda de animais,Salário\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais,Salário\n3a,Outro,Outro,Venda de animais,Salário\n3a,Vendas agrícolas,Vendas agrícolas,Venda de animais,Pensão\n3a,Salário,Salário,Venda de animais,Pensão\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais,Pensão\n3a,Cobrança de rendas,Cobrança de rendas,Venda de animais,Pensão\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais,Pensão\n3a,Outro,Outro,Venda de animais,Pensão\n3a,Vendas agrícolas,Vendas agrícolas,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Salário,Salário,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Pensão,Pensão,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Outro,Outro,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Venda de animais,Cobrança de rendas\n3a,Salário,Salário,Venda de animais,Cobrança de rendas\n3a,Pensão,Pensão,Venda de animais,Cobrança de rendas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais,Cobrança de rendas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais,Cobrança de rendas\n3a,Outro,Outro,Venda de animais,Cobrança de rendas\n3a,Vendas agrícolas,Vendas agrícolas,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Salário,Salário,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Pensão,Pensão,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Outro,Outro,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Venda de animais,Outro\n3a,Salário,Salário,Venda de animais,Outro\n3a,Pensão,Pensão,Venda de animais,Outro\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais,Outro\n3a,Cobrança de rendas,Cobrança de rendas,Venda de animais,Outro\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais,Outro\n3a,Venda de animais,Venda de animais,Salário,Vendas agrícolas\n3a,Pensão,Pensão,Salário,Vendas agrícolas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário,Vendas agrícolas\n3a,Cobrança de rendas,Cobrança de rendas,Salário,Vendas agrícolas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário,Vendas agrícolas\n3a,Outro,Outro,Salário,Vendas agrícolas\n3a,Vendas agrícolas,Vendas agrícolas,Salário,Venda de animais\n3a,Pensão,Pensão,Salário,Venda de animais\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário,Venda de animais\n3a,Cobrança de rendas,Cobrança de rendas,Salário,Venda de animais\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário,Venda de animais\n3a,Outro,Outro,Salário,Venda de animais\n3a,Vendas agrícolas,Vendas agrícolas,Salário,Pensão\n3a,Venda de animais,Venda de animais,Salário,Pensão\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário,Pensão\n3a,Cobrança de rendas,Cobrança de rendas,Salário,Pensão\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário,Pensão\n3a,Outro,Outro,Salário,Pensão\n3a,Vendas agrícolas,Vendas agrícolas,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Venda de animais,Venda de animais,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Pensão,Pensão,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Outro,Outro,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Salário,Cobrança de rendas\n3a,Venda de animais,Venda de animais,Salário,Cobrança de rendas\n3a,Pensão,Pensão,Salário,Cobrança de rendas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário,Cobrança de rendas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário,Cobrança de rendas\n3a,Outro,Outro,Salário,Cobrança de rendas\n3a,Vendas agrícolas,Vendas agrícolas,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Venda de animais,Venda de animais,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Pensão,Pensão,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Outro,Outro,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Salário,Outro\n3a,Venda de animais,Venda de animais,Salário,Outro\n3a,Pensão,Pensão,Salário,Outro\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário,Outro\n3a,Cobrança de rendas,Cobrança de rendas,Salário,Outro\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário,Outro\n3a,Venda de animais,Venda de animais,Pensão,Vendas agrícolas\n3a,Salário,Salário,Pensão,Vendas agrícolas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão,Vendas agrícolas\n3a,Cobrança de rendas,Cobrança de rendas,Pensão,Vendas agrícolas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão,Vendas agrícolas\n3a,Outro,Outro,Pensão,Vendas agrícolas\n3a,Vendas agrícolas,Vendas agrícolas,Pensão,Venda de animais\n3a,Salário,Salário,Pensão,Venda de animais\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão,Venda de animais\n3a,Cobrança de rendas,Cobrança de rendas,Pensão,Venda de animais\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão,Venda de animais\n3a,Outro,Outro,Pensão,Venda de animais\n3a,Vendas agrícolas,Vendas agrícolas,Pensão,Salário\n3a,Venda de animais,Venda de animais,Pensão,Salário\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão,Salário\n3a,Cobrança de rendas,Cobrança de rendas,Pensão,Salário\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão,Salário\n3a,Outro,Outro,Pensão,Salário\n3a,Vendas agrícolas,Vendas agrícolas,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Venda de animais,Venda de animais,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Salário,Salário,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Outro,Outro,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Pensão,Cobrança de rendas\n3a,Venda de animais,Venda de animais,Pensão,Cobrança de rendas\n3a,Salário,Salário,Pensão,Cobrança de rendas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão,Cobrança de rendas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão,Cobrança de rendas\n3a,Outro,Outro,Pensão,Cobrança de rendas\n3a,Vendas agrícolas,Vendas agrícolas,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Venda de animais,Venda de animais,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Salário,Salário,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Outro,Outro,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Pensão,Outro\n3a,Venda de animais,Venda de animais,Pensão,Outro\n3a,Salário,Salário,Pensão,Outro\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão,Outro\n3a,Cobrança de rendas,Cobrança de rendas,Pensão,Outro\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão,Outro\n3a,Venda de animais,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas\n3a,Salário,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas\n3a,Pensão,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas\n3a,Cobrança de rendas,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas\n3a,Outro,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Vendas agrícolas\n3a,Vendas agrícolas,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais\n3a,Salário,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais\n3a,Pensão,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais\n3a,Cobrança de rendas,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais\n3a,Outro,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Venda de animais\n3a,Vendas agrícolas,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário\n3a,Venda de animais,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário\n3a,Pensão,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário\n3a,Cobrança de rendas,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário\n3a,Outro,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Salário\n3a,Vendas agrícolas,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão\n3a,Venda de animais,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão\n3a,Salário,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão\n3a,Cobrança de rendas,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão\n3a,Outro,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Pensão\n3a,Vendas agrícolas,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas\n3a,Venda de animais,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas\n3a,Salário,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas\n3a,Pensão,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas\n3a,Outro,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas\n3a,Vendas agrícolas,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Venda de animais,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Salário,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Pensão,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Outro,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro\n3a,Venda de animais,Venda de animais,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro\n3a,Salário,Salário,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro\n3a,Pensão,Pensão,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro\n3a,Cobrança de rendas,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro\n3a,Venda de animais,Venda de animais,Cobrança de rendas,Vendas agrícolas\n3a,Salário,Salário,Cobrança de rendas,Vendas agrícolas\n3a,Pensão,Pensão,Cobrança de rendas,Vendas agrícolas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas,Vendas agrícolas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas,Vendas agrícolas\n3a,Outro,Outro,Cobrança de rendas,Vendas agrícolas\n3a,Vendas agrícolas,Vendas agrícolas,Cobrança de rendas,Venda de animais\n3a,Salário,Salário,Cobrança de rendas,Venda de animais\n3a,Pensão,Pensão,Cobrança de rendas,Venda de animais\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas,Venda de animais\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas,Venda de animais\n3a,Outro,Outro,Cobrança de rendas,Venda de animais\n3a,Vendas agrícolas,Vendas agrícolas,Cobrança de rendas,Salário\n3a,Venda de animais,Venda de animais,Cobrança de rendas,Salário\n3a,Pensão,Pensão,Cobrança de rendas,Salário\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas,Salário\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas,Salário\n3a,Outro,Outro,Cobrança de rendas,Salário\n3a,Vendas agrícolas,Vendas agrícolas,Cobrança de rendas,Pensão\n3a,Venda de animais,Venda de animais,Cobrança de rendas,Pensão\n3a,Salário,Salário,Cobrança de rendas,Pensão\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas,Pensão\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas,Pensão\n3a,Outro,Outro,Cobrança de rendas,Pensão\n3a,Vendas agrícolas,Vendas agrícolas,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Venda de animais,Venda de animais,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Salário,Salário,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Pensão,Pensão,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Outro,Outro,Cobrança de rendas,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Venda de animais,Venda de animais,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Salário,Salário,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Pensão,Pensão,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Outro,Outro,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Cobrança de rendas,Outro\n3a,Venda de animais,Venda de animais,Cobrança de rendas,Outro\n3a,Salário,Salário,Cobrança de rendas,Outro\n3a,Pensão,Pensão,Cobrança de rendas,Outro\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Cobrança de rendas,Outro\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas,Outro\n3a,Venda de animais,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas\n3a,Salário,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas\n3a,Pensão,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas\n3a,Cobrança de rendas,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas\n3a,Outro,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Vendas agrícolas\n3a,Vendas agrícolas,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais\n3a,Salário,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais\n3a,Pensão,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais\n3a,Cobrança de rendas,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais\n3a,Outro,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Venda de animais\n3a,Vendas agrícolas,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário\n3a,Venda de animais,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário\n3a,Pensão,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário\n3a,Cobrança de rendas,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário\n3a,Outro,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Salário\n3a,Vendas agrícolas,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão\n3a,Venda de animais,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão\n3a,Salário,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão\n3a,Cobrança de rendas,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão\n3a,Outro,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Pensão\n3a,Vendas agrícolas,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Venda de animais,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Salário,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Pensão,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Outro,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas\n3a,Venda de animais,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas\n3a,Salário,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas\n3a,Pensão,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas\n3a,Outro,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Cobrança de rendas\n3a,Vendas agrícolas,Vendas agrícolas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro\n3a,Venda de animais,Venda de animais,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro\n3a,Salário,Salário,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro\n3a,Pensão,Pensão,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro\n3a,Cobrança de rendas,Cobrança de rendas,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro\n3a,Venda de animais,Venda de animais,Outro,Vendas agrícolas\n3a,Salário,Salário,Outro,Vendas agrícolas\n3a,Pensão,Pensão,Outro,Vendas agrícolas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro,Vendas agrícolas\n3a,Cobrança de rendas,Cobrança de rendas,Outro,Vendas agrícolas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro,Vendas agrícolas\n3a,Vendas agrícolas,Vendas agrícolas,Outro,Venda de animais\n3a,Salário,Salário,Outro,Venda de animais\n3a,Pensão,Pensão,Outro,Venda de animais\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro,Venda de animais\n3a,Cobrança de rendas,Cobrança de rendas,Outro,Venda de animais\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro,Venda de animais\n3a,Vendas agrícolas,Vendas agrícolas,Outro,Salário\n3a,Venda de animais,Venda de animais,Outro,Salário\n3a,Pensão,Pensão,Outro,Salário\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro,Salário\n3a,Cobrança de rendas,Cobrança de rendas,Outro,Salário\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro,Salário\n3a,Vendas agrícolas,Vendas agrícolas,Outro,Pensão\n3a,Venda de animais,Venda de animais,Outro,Pensão\n3a,Salário,Salário,Outro,Pensão\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro,Pensão\n3a,Cobrança de rendas,Cobrança de rendas,Outro,Pensão\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro,Pensão\n3a,Vendas agrícolas,Vendas agrícolas,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Venda de animais,Venda de animais,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Salário,Salário,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Pensão,Pensão,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro,Enviu de dinheiro de algum familiar/amigo que não vive na casa\n3a,Vendas agrícolas,Vendas agrícolas,Outro,Cobrança de rendas\n3a,Venda de animais,Venda de animais,Outro,Cobrança de rendas\n3a,Salário,Salário,Outro,Cobrança de rendas\n3a,Pensão,Pensão,Outro,Cobrança de rendas\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro,Cobrança de rendas\n3a,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Dinheiro do esposo/esposa ou outro familiar que vive na casa,Outro,Cobrança de rendas\n3a,Vendas agrícolas,Vendas agrícolas,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Venda de animais,Venda de animais,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Salário,Salário,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Pensão,Pensão,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Enviu de dinheiro de algum familiar/amigo que não vive na casa,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa\n3a,Cobrança de rendas,Cobrança de rendas,Outro,Dinheiro do esposo/esposa ou outro familiar que vive na casa"
  },
  {
    "path": "test-forms/src/main/resources/media/people.csv",
    "content": "name,label,full_name,__version\n93a179a0-71b3-42f8-b78e-4c182eaf2ae7,Roman Roy,Roman Roy,1\n"
  },
  {
    "path": "test-forms/src/main/resources/media/selectOneExternal-media/itemsets.csv",
    "content": "\"list_name\",\"name\",\"label::English\",\"state\",\"county\",\"label::French\"\r\n\"states\",\"a1\",\"Texas\",\"\",\"\",\"Le Texas\"\r\n\"states\",\"a2\",\"Washington\",\"\",\"\",\"La Washington\"\r\n\"counties\",\"b1\",\"King\",\"a2\",\"\",\"Le King\"\r\n\"counties\",\"b2\",\"Pierce\",\"a2\",\"\",\"La Pierce\"\r\n\"counties\",\"b3\",\"King\",\"a1\",\"\",\"Le King\"\r\n\"counties\",\"b4\",\"Cameron\",\"a1\",\"\",\"La Cameron\"\r\n\"cities\",\"dumont\",\"Dumont\",\"a1\",\"b3\",\"Le Dumont\"\r\n\"cities\",\"finney\",\"Finney\",\"a1\",\"b3\",\"La Finney\"\r\n\"cities\",\"brownsville\",\"brownsville\",\"a1\",\"b4\",\"Le brownsville\"\r\n\"cities\",\"harlingen\",\"harlingen\",\"a1\",\"b4\",\"La harlingen\"\r\n\"cities\",\"seattle\",\"Seattle\",\"a2\",\"b3\",\"Le Seattle\"\r\n\"cities\",\"redmond\",\"Redmond\",\"a2\",\"b3\",\"La Redmond\"\r\n\"cities\",\"tacoma\",\"Tacoma\",\"a2\",\"b2\",\"Le Tacoma\"\r\n\"cities\",\"puyallup\",\"Puyallup\",\"a2\",\"b2\",\"La Puyallup\"\r\n"
  },
  {
    "path": "test-forms/src/main/resources/media/simple-search-external-csv-fruits.csv",
    "content": "name_key,name\nmango,Mango\noranges,Oranges\nstrawberries,Strawberries"
  },
  {
    "path": "test-forms/src/main/resources/media/updated-people.csv",
    "content": "name,label,full_name,__version\n93a179a0-71b3-42f8-b78e-4c182eaf2ae7,Ro-Ro Roy,Ro-Ro Roy,3\n"
  },
  {
    "path": "test-shared/.gitignore",
    "content": "/build"
  },
  {
    "path": "test-shared/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    namespace = \"org.odk.collect.testshared\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(fileTree(mapOf(\"dir\" to \"libs\", \"include\" to listOf(\"*.jar\"))))\n    implementation(project(\":async\"))\n    implementation(project(\":audio-clips\"))\n    implementation(project(\":shared\"))\n    implementation(project(\":androidshared\"))\n    implementation(project(\":androidtest\"))\n    implementation(project(\":service-test\"))\n    implementation(project(\":qr-code\"))\n    implementation(project(\":web-page\"))\n    implementation(libs.androidxRecyclerview)\n    implementation(libs.kotlinStdlib)\n    implementation(libs.androidxCoreKtx)\n    implementation(libs.androidxWorkRuntime)\n    implementation(libs.androidxAppcompat)\n    implementation(libs.robolectric)\n    implementation(libs.junit)\n    implementation(libs.androidxTestEspressoIntents)\n    implementation(libs.androidxTestEspressoContrib)\n    implementation(libs.androidMaterial)\n    implementation(libs.danlewAndroidJoda)\n    implementation(libs.androidXComposeUiTestJunit4)\n    implementation(libs.androidXComposeUiTest)\n    implementation(libs.androidxFragmentTesting) {\n        exclude(group = \"androidx.test\", module = \"monitor\") // fixes issue https://github.com/android/android-test/issues/731\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    /\n    <application>\n        <activity android:name=\"org.odk.collect.testshared.DummyActivity\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/ActivityControllerRule.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.app.Activity\nimport android.content.Intent\nimport org.junit.rules.ExternalResource\nimport org.robolectric.Robolectric\nimport org.robolectric.android.controller.ActivityController\n\nclass ActivityControllerRule : ExternalResource() {\n\n    private val controllers = mutableListOf<ActivityController<*>>()\n\n    override fun after() {\n        controllers.forEach { it.close() }\n        controllers.clear()\n    }\n\n    fun <A : Activity> build(activityClass: Class<A>, intent: Intent): ActivityController<A> {\n        return Robolectric.buildActivity(activityClass, intent).also {\n            controllers.add(it)\n        }\n    }\n\n    fun <A : Activity> add(supplier: () -> ActivityController<A>): ActivityController<A> {\n        return supplier().also {\n            controllers.add(it)\n        }\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/ActivityExt.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.app.Activity\nimport android.view.View\nimport android.view.ViewGroup\n\nobject ActivityExt {\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <T : View> Activity.getContextView(): T {\n        return this.findViewById<ViewGroup>(android.R.id.content).getChildAt(0) as T\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/AssertIntentsHelper.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.content.Intent\nimport androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent\nimport org.hamcrest.Matcher\nimport org.odk.collect.testshared.EspressoAssertions.assertIntents\nimport kotlin.reflect.KClass\n\nclass AssertIntentsHelper {\n\n    private val matchers = mutableListOf<Matcher<Intent>>()\n\n    fun assertNewIntent(matcher: Matcher<Intent>) {\n        matchers.add(matcher)\n        assertIntents(*matchers.toTypedArray())\n    }\n\n    fun assertNewIntent(activityClass: KClass<*>) {\n        matchers.add(hasComponent(activityClass.java.name))\n        assertIntents(*matchers.toTypedArray())\n    }\n\n    fun assertNoNewIntent() {\n        assertIntents(*matchers.toTypedArray())\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/AssertionFramework.kt",
    "content": "package org.odk.collect.testshared\n\nenum class AssertionFramework {\n    ESPRESSO,\n    COMPOSE\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/ComposeAssertions.kt",
    "content": "package org.odk.collect.testshared\n\nimport androidx.compose.ui.test.ExperimentalTestApi\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.hasText\nimport androidx.compose.ui.test.junit4.ComposeTestRule\n\n@OptIn(ExperimentalTestApi::class)\nobject ComposeAssertions {\n    fun assertVisible(composeRule: ComposeTestRule, text: String) {\n        assertVisible(composeRule, hasText(text))\n    }\n\n    fun assertVisible(composeRule: ComposeTestRule, matcher: SemanticsMatcher) {\n        composeRule.run {\n            waitUntilAtLeastOneExists(matcher)\n            onNode(matcher).assertIsDisplayed()\n        }\n    }\n\n    fun assertNotVisible(composeRule: ComposeTestRule, matcher: SemanticsMatcher) {\n        composeRule.waitUntilDoesNotExist(matcher)\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/ComposeInteractions.kt",
    "content": "package org.odk.collect.testshared\n\nimport androidx.compose.ui.test.SemanticsMatcher\nimport androidx.compose.ui.test.junit4.ComposeTestRule\nimport androidx.compose.ui.test.performClick\n\nobject ComposeInteractions {\n    fun clickOn(composeRule: ComposeTestRule, matcher: SemanticsMatcher, assertion: () -> Unit) {\n        composeRule.onNode(matcher).performClick()\n        assertion()\n    }\n}"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/DummyActivity.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.app.Activity\nimport android.os.Bundle\nimport android.widget.TextView\n\nclass DummyActivity : Activity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(\n            TextView(this).also {\n                it.text = TEXT\n            }\n        )\n    }\n\n    companion object {\n        const val TEXT = \"I AM DUMMY\"\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/ErrorIntentLauncher.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport androidx.activity.result.ActivityResultLauncher\nimport org.odk.collect.androidshared.system.IntentLauncher\n\nclass ErrorIntentLauncher : IntentLauncher {\n    override fun launch(context: Context, intent: Intent?, onError: () -> Unit) {\n        onError()\n    }\n\n    override fun launchForResult(\n        activity: Activity,\n        intent: Intent?,\n        requestCode: Int,\n        onError: () -> Unit\n    ) {\n        onError()\n    }\n\n    override fun launchForResult(\n        resultLauncher: ActivityResultLauncher<Intent>,\n        intent: Intent?,\n        onError: () -> Unit\n    ) {\n        onError()\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/EspressoAssertions.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.content.Intent\nimport android.view.View\nimport androidx.test.espresso.Espresso\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.Root\nimport androidx.test.espresso.assertion.PositionAssertions.isCompletelyBelow\nimport androidx.test.espresso.assertion.ViewAssertions.doesNotExist\nimport androidx.test.espresso.assertion.ViewAssertions.matches\nimport androidx.test.espresso.intent.Intents\nimport androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE\nimport androidx.test.espresso.matcher.ViewMatchers.hasSibling\nimport androidx.test.espresso.matcher.ViewMatchers.isEnabled\nimport androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility\nimport org.hamcrest.CoreMatchers.not\nimport org.hamcrest.Matcher\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.allOf\nimport org.hamcrest.Matchers.equalTo\nimport org.odk.collect.androidshared.ui.AlertStore\nimport org.odk.collect.testshared.WaitFor.waitFor\n\nobject EspressoAssertions {\n\n    fun assertVisible(\n        view: Matcher<View>,\n        root: Matcher<Root>? = null,\n        sibling: Matcher<View>? = null\n    ) {\n        val baseMatcher = allOf(view, withEffectiveVisibility(VISIBLE))\n\n        val withSibling = if (sibling != null) {\n            allOf(baseMatcher, hasSibling(sibling))\n        } else {\n            baseMatcher\n        }\n\n        val onView = if (root != null) {\n            onView(withSibling).inRoot(root)\n        } else {\n            onView(withSibling)\n        }\n\n        onView.check(matches(not(doesNotExist())))\n    }\n\n    fun assertEnabled(\n        view: Matcher<View>,\n        root: Matcher<Root>? = null,\n        sibling: Matcher<View>? = null\n    ) {\n        assertVisible(allOf(view, isEnabled()), root, sibling)\n    }\n\n    fun assertDisabled(\n        view: Matcher<View>,\n        root: Matcher<Root>? = null,\n        sibling: Matcher<View>? = null,\n    ) {\n        assertVisible(allOf(view, not(isEnabled())), root, sibling)\n    }\n\n    fun assertNotVisible(view: Matcher<View>, root: Matcher<Root>? = null) {\n        val onView = if (root != null) {\n            onView(allOf(view, withEffectiveVisibility(VISIBLE))).inRoot(root)\n        } else {\n            onView(allOf(view, withEffectiveVisibility(VISIBLE)))\n        }\n\n        onView.check(doesNotExist())\n    }\n\n    fun assertIntents(vararg intentMatchers: Matcher<Intent>) {\n        val intents = Intents.getIntents()\n        assertThat(intentMatchers.size, equalTo(intents.size))\n\n        intentMatchers.forEachIndexed { index, matcher ->\n            assertThat(intents[index], matcher)\n        }\n    }\n\n    fun assertBelow(below: Matcher<View>, above: Matcher<View>) {\n        onView(below).check(isCompletelyBelow(above))\n    }\n\n    fun assertAlert(alertStore: AlertStore, alert: String, failureMessage: String) {\n        Espresso.onIdle()\n        waitFor {\n            if (alertStore.popAll().none { it == alert }) {\n                throw RuntimeException(failureMessage)\n            }\n        }\n    }\n\n    fun assertNoAlert(alertStore: AlertStore, alert: String, failureMessage: String) {\n        Espresso.onIdle()\n        if (alertStore.popAll().any { it == alert }) {\n            throw RuntimeException(failureMessage)\n        }\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/EspressoInteractions.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.view.View\nimport androidx.test.espresso.Espresso.closeSoftKeyboard\nimport androidx.test.espresso.Espresso.onView\nimport androidx.test.espresso.Root\nimport androidx.test.espresso.action.ViewActions\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.action.ViewActions.scrollTo\nimport org.hamcrest.Matcher\nimport org.odk.collect.testshared.WaitFor.tryAgainOnFail\n\nobject EspressoInteractions {\n\n    /**\n     * Click on the view matched by [view]. The root to use can optionally be specified with\n     * [root] (otherwise Espresso will use heuristics to determine the most likely root). If\n     * initially clicking on the view fails, this will then attempt to scroll to the view and\n     * retry the click.\n     */\n    @JvmStatic\n    @JvmOverloads\n    fun clickOn(view: Matcher<View>, root: Matcher<Root>? = null) {\n        val onView = if (root != null) {\n            onView(view).inRoot(root)\n        } else {\n            onView(view)\n        }\n\n        try {\n            onView.perform(click())\n        } catch (e: Exception) {\n            onView.perform(scrollTo(), click())\n        }\n    }\n\n    /**\n     * Like [clickOn], but an [assertion] can be made after the click. If this fails, the click\n     * action will be reattempted.\n     *\n     * This can be useful in cases where [clickOn] itself appears to succeed, but the test fails\n     * because the click never actually occurs (most likely due to some flakiness in\n     * [androidx.test.espresso.action.ViewActions.click]).\n     */\n    fun clickOn(view: Matcher<View>, root: Matcher<Root>? = null, assertion: () -> Unit) {\n        tryAgainOnFail {\n            clickOn(view, root)\n            assertion()\n        }\n    }\n\n    /**\n     * Replaces text in the view matched by [view] and then closes the keyboard.\n     */\n    @JvmStatic\n    fun replaceText(view: Matcher<View>, text: String) {\n        onView(view).perform(ViewActions.replaceText(text))\n        closeSoftKeyboard()\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/FakeAudioPlayer.kt",
    "content": "package org.odk.collect.testshared\n\nimport androidx.activity.ComponentActivity\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\nimport org.odk.collect.audioclips.AudioPlayer\nimport org.odk.collect.audioclips.AudioPlayerFactory\nimport org.odk.collect.audioclips.Clip\nimport java.util.function.Consumer\n\nclass FakeAudioPlayer : AudioPlayer {\n    private val playingChangedListeners: MutableMap<String, Consumer<Boolean>> = HashMap()\n    private val positionChangedListeners: MutableMap<String, Consumer<Int>> = HashMap()\n    private val positions: MutableMap<String, Int> = HashMap()\n\n    var playedClips: Int = 0\n        private set\n    var isPlaying: Boolean = false\n        private set\n    var currentClip: Clip? = null\n        private set\n\n    override fun isLoading(): LiveData<Boolean> {\n        return MutableLiveData(false)\n    }\n\n    override fun play(clip: Clip) {\n        this.currentClip = clip\n        playedClips++\n        isPlaying = true\n        playingChangedListeners[clip.clipID]!!.accept(true)\n    }\n\n    override fun pause() {\n        isPlaying = false\n        playingChangedListeners[currentClip!!.clipID]!!.accept(false)\n    }\n\n    override fun setPosition(clipId: String, position: Int) {\n        positions[clipId] = position\n        positionChangedListeners[clipId]!!.accept(position)\n    }\n\n    override fun onPlayingChanged(clipID: String, playingConsumer: Consumer<Boolean>) {\n        playingChangedListeners[clipID] = playingConsumer\n    }\n\n    override fun onPositionChanged(clipID: String, positionConsumer: Consumer<Int>) {\n        positionChangedListeners[clipID] = positionConsumer\n    }\n\n    override fun onPlaybackError(error: Consumer<Exception>) {\n    }\n\n    override fun stop() {\n        currentClip?.also {\n            playingChangedListeners[it.clipID]?.accept(false)\n        }\n\n        isPlaying = false\n        currentClip = null\n    }\n\n    override fun playInOrder(clips: List<Clip>) {\n        playedClips += clips.size\n        isPlaying = true\n    }\n}\n\nclass FakeAudioPlayerFactory : AudioPlayerFactory {\n    lateinit var audioPlayer: FakeAudioPlayer\n        private set\n\n    override fun create(\n        activity: ComponentActivity,\n        lifecycleOwner: LifecycleOwner\n    ): AudioPlayer {\n        return FakeAudioPlayer().also {\n            audioPlayer = it\n        }\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/FakeBarcodeScannerView.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.app.Activity\nimport android.content.Context\nimport android.os.Looper\nimport androidx.lifecycle.DefaultLifecycleObserver\nimport androidx.lifecycle.LifecycleOwner\nimport org.odk.collect.androidshared.utils.CompressionUtils\nimport org.odk.collect.qrcode.BarcodeScannerView\nimport org.odk.collect.qrcode.BarcodeScannerViewContainer\n\nclass FakeBarcodeScannerView(context: Context) : BarcodeScannerView(context) {\n\n    var isScanning = false\n        private set\n\n    private var callback: ((String) -> Unit)? = null\n\n    override fun scan(callback: (String) -> Unit) {\n        isScanning = true\n        this.callback = callback\n    }\n\n    override fun setTorchOn(on: Boolean) = Unit\n    override fun setTorchListener(torchListener: TorchListener) = Unit\n    override fun supportsFullScreenViewFinder(): Boolean {\n        return false\n    }\n\n    override fun setFullScreenViewFinder(fullScannerViewFinder: Boolean) = Unit\n\n    fun scan(result: String) {\n        isScanning = false\n\n        if (Looper.myLooper() == Looper.getMainLooper()) {\n            callback?.invoke(result)\n        } else {\n            post {\n                callback?.invoke(result)\n            }\n        }\n    }\n}\n\nclass FakeBarcodeScannerViewFactory : BarcodeScannerViewContainer.Factory {\n\n    private val views = mutableListOf<FakeBarcodeScannerView>()\n\n    val isScanning: Boolean\n        get() = views.any { it.isScanning }\n\n    override fun create(\n        activity: Activity,\n        lifecycleOwner: LifecycleOwner,\n        qrOnly: Boolean,\n        useFrontCamera: Boolean\n    ): BarcodeScannerView {\n        return FakeBarcodeScannerView(activity).also {\n            views.add(it)\n            lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {\n                override fun onDestroy(owner: LifecycleOwner) {\n                    views.remove(it)\n                }\n            })\n        }\n    }\n\n    fun scan(result: String) {\n        val compressedResult = CompressionUtils.compress(result)\n        views.forEach {\n            if (it.isScanning) {\n                it.scan(compressedResult)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/FakeBroadcastReceiverRegister.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport org.odk.collect.androidshared.system.BroadcastReceiverRegister\n\nclass FakeBroadcastReceiverRegister : BroadcastReceiverRegister {\n    val registeredReceivers = mutableListOf<Pair<String, BroadcastReceiver>>()\n\n    override fun registerReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {\n        registeredReceivers.add(Pair(filter.getAction(0), receiver))\n    }\n\n    override fun unregisterReceiver(receiver: BroadcastReceiver) {\n        registeredReceivers.removeIf { it.second == receiver }\n    }\n\n    fun broadcast(context: Context, intent: Intent) {\n        registeredReceivers\n            .filter { it.first == intent.action }\n            .forEach { it.second.onReceive(context, intent) }\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/FakeScheduler.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.content.Context\nimport androidx.lifecycle.LiveData\nimport androidx.test.core.app.ApplicationProvider\nimport kotlinx.coroutines.CoroutineDispatcher\nimport kotlinx.coroutines.Runnable\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flowOn\nimport org.odk.collect.androidtest.getOrAwaitValue\nimport org.odk.collect.async.Cancellable\nimport org.odk.collect.async.NotificationInfo\nimport org.odk.collect.async.Scheduler\nimport org.odk.collect.async.TaskSpec\nimport java.util.LinkedList\nimport java.util.function.Consumer\nimport java.util.function.Supplier\nimport kotlin.coroutines.CoroutineContext\n\nclass FakeScheduler : Scheduler {\n\n    private val backgroundDispatcher = object : CoroutineDispatcher() {\n        override fun dispatch(context: CoroutineContext, block: Runnable) {\n            backgroundTasks.add(block)\n        }\n    }\n\n    private var foregroundTasks = LinkedList<Runnable>()\n    private var backgroundTasks = LinkedList<Runnable>()\n    private var repeatTasks = ArrayList<RepeatTask>()\n\n    override fun <T> immediate(background: Supplier<T>, foreground: Consumer<T>) {\n        backgroundTasks.addLast(\n            Runnable {\n                val result = background.get()\n                foregroundTasks.add(Runnable { foreground.accept(result) })\n            }\n        )\n    }\n\n    override fun immediate(foreground: Boolean, delay: Long?, runnable: Runnable) {\n        if (!foreground) {\n            backgroundTasks.push(runnable)\n        } else {\n            foregroundTasks.push(runnable)\n        }\n    }\n\n    override fun immediate(\n        tag: String,\n        spec: TaskSpec,\n        inputData: Map<String, String>,\n        notificationInfo: NotificationInfo\n    ) {\n        val context = ApplicationProvider.getApplicationContext<Context>()\n        backgroundTasks.push(spec.getTask(context, inputData, true) { false }::get)\n    }\n\n    override fun networkDeferred(\n        tag: String,\n        spec: TaskSpec,\n        inputData: Map<String, String>,\n        networkConstraint: Scheduler.NetworkType?\n    ) {\n    }\n\n    override fun networkDeferredRepeat(\n        tag: String,\n        taskSpec: TaskSpec,\n        repeatPeriod: Long,\n        inputData: Map<String, String>\n    ) {\n    }\n\n    override fun repeat(foreground: Runnable, repeatPeriod: Long): Cancellable {\n        val task = RepeatTask(repeatPeriod, foreground, null)\n        repeatTasks.add(task)\n\n        return object : Cancellable {\n            override fun cancel(): Boolean {\n                repeatTasks.remove(task)\n                return true\n            }\n        }\n    }\n\n    override fun cancelAllDeferred() {}\n\n    override fun <T> flowOnBackground(flow: Flow<T>): Flow<T> {\n        return flow.flowOn(backgroundDispatcher)\n    }\n\n    fun runFirstForeground() {\n        if (foregroundTasks.isNotEmpty()) {\n            foregroundTasks.removeFirst().run()\n        }\n    }\n\n    fun runForeground() {\n        while (foregroundTasks.isNotEmpty()) {\n            foregroundTasks.remove().run()\n        }\n\n        repeatTasks.forEach { it.runnable.run() }\n    }\n\n    fun runForeground(currentTime: Long) {\n        while (foregroundTasks.isNotEmpty()) {\n            foregroundTasks.remove().run()\n        }\n\n        repeatTasks.forEach {\n            it.lastRun.let { lastRun ->\n                if (lastRun == null) {\n                    it.runnable.run()\n                    it.lastRun = currentTime\n                } else if ((currentTime - lastRun) >= it.interval) {\n                    it.runnable.run()\n                    it.lastRun = currentTime\n                }\n            }\n        }\n    }\n\n    fun runFirstBackground() {\n        if (backgroundTasks.isNotEmpty()) {\n            backgroundTasks.removeFirst().run()\n        }\n    }\n\n    fun runBackground() {\n        while (backgroundTasks.isNotEmpty()) {\n            backgroundTasks.remove().run()\n        }\n    }\n\n    @JvmOverloads\n    fun flush(enforce: Boolean = false) {\n        if (enforce && backgroundTasks.isEmpty() && foregroundTasks.isEmpty()) {\n            throw IllegalStateException(\"No tasks to run!\")\n        }\n\n        while (backgroundTasks.isNotEmpty() || foregroundTasks.isNotEmpty()) {\n            runBackground()\n            runForeground()\n        }\n    }\n\n    fun isRepeatRunning(): Boolean {\n        return repeatTasks.isNotEmpty()\n    }\n\n    override fun isDeferredRunning(tag: String): Boolean {\n        return false\n    }\n\n    override fun cancelDeferred(tag: String) {}\n}\n\nfun <T> LiveData<T>.getOrAwaitValue(\n    scheduler: FakeScheduler? = null\n): T {\n    return this.getOrAwaitValue { scheduler?.flush() }\n}\n\nprivate data class RepeatTask(val interval: Long, val runnable: Runnable, var lastRun: Long?)\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/FragmentResultRecorder.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.os.Bundle\nimport androidx.fragment.app.FragmentResultListener\n\nclass FragmentResultRecorder : FragmentResultListener {\n\n    private val results = mutableListOf<Pair<String, Bundle>>()\n\n    fun getAll(): List<Pair<String, Bundle>> {\n        return results\n    }\n\n    fun clear() {\n        results.clear()\n    }\n\n    override fun onFragmentResult(requestKey: String, result: Bundle) {\n        results.add(Pair(requestKey, result))\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/LocationTestUtils.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.location.Location\n\nobject LocationTestUtils {\n\n    @JvmStatic\n    fun createLocation(provider: String, lat: Double, lon: Double, alt: Double): FakeLocation {\n        val location = FakeLocation(provider)\n        location.latitude = lat\n        location.longitude = lon\n        location.altitude = alt\n        return location\n    }\n\n    @JvmStatic\n    fun createLocation(\n        provider: String,\n        lat: Double,\n        lon: Double,\n        alt: Double,\n        sd: Float\n    ): FakeLocation {\n        val location = createLocation(provider, lat, lon, alt)\n        location.accuracy = sd\n        return location\n    }\n\n    @JvmStatic\n    fun createLocation(\n        provider: String,\n        lat: Double,\n        lon: Double,\n        alt: Double,\n        sd: Float,\n        isLocationMocked: Boolean\n    ): FakeLocation {\n        val location = createLocation(provider, lat, lon, alt, sd)\n        location.setIsFromMockProvider(isLocationMocked)\n        return location\n    }\n}\n\n/**\n * [Location] throws \"Method ... not mocked\" errors if you attempt to use it local tests. This fake\n * intercepts set/get calls and uses its own fields (in a similar way to Roboletric's \"Shadows\").\n *\n * @see [Method ... not mocked](https://tools.android.com/tech-docs/unit-testing-support#TOC-Method-...-not-mocked.-)\n */\nclass FakeLocation(provider: String?) : Location(provider) {\n\n    private var _isFromMockProvider = false\n    private var _provider: String? = provider\n    private var _latitude: Double = 0.0\n    private var _longitude: Double = 0.0\n    private var _accuracy: Float? = null\n    private var _altitude: Double = 0.0\n    private var _time = System.currentTimeMillis()\n\n    fun setIsFromMockProvider(isFromMockProvider: Boolean) {\n        _isFromMockProvider = isFromMockProvider\n    }\n\n    override fun isFromMockProvider(): Boolean {\n        return _isFromMockProvider\n    }\n\n    override fun getAltitude(): Double {\n        return _altitude\n    }\n\n    override fun getAccuracy(): Float {\n        return _accuracy ?: 0.0f\n    }\n\n    override fun getLatitude(): Double {\n        return _latitude\n    }\n\n    override fun getLongitude(): Double {\n        return _longitude\n    }\n\n    override fun getProvider(): String? {\n        return _provider\n    }\n\n    override fun hasAccuracy(): Boolean {\n        return _accuracy != null\n    }\n\n    override fun setLatitude(latitude: Double) {\n        _latitude = latitude\n    }\n\n    override fun setLongitude(longitude: Double) {\n        _longitude = longitude\n    }\n\n    override fun setProvider(provider: String?) {\n        _provider = provider\n    }\n\n    override fun setAltitude(altitude: Double) {\n        _altitude = altitude\n    }\n\n    override fun setAccuracy(horizontalAccuracy: Float) {\n        _accuracy = horizontalAccuracy\n    }\n\n    override fun getTime(): Long {\n        return _time\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) {\n            return true\n        }\n        if (other == null || javaClass != other.javaClass) {\n            return false\n        }\n\n        val location = other as FakeLocation\n        return this._latitude == location._latitude &&\n            this._longitude == location._longitude &&\n            this._altitude == location._altitude &&\n            this._accuracy == location._accuracy &&\n            this._provider == location._provider\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/MockFragmentFactory.kt",
    "content": "package org.odk.collect.testshared\n\nimport androidx.fragment.app.DialogFragment\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentFactory\n\n/**\n * Instead of instantiate requested Fragments, will instantiate a [MockFragment] (or\n * [MockDialogFragment]) that wraps the Fragment class. This allows a test to check that the right\n * Fragments are setup without needing to deal with their actual creation/dependencies.\n */\nclass MockFragmentFactory : FragmentFactory() {\n    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {\n        val fragmentClass = loadFragmentClass(classLoader, className)\n        return if (DialogFragment::class.java.isAssignableFrom(fragmentClass)) {\n            MockDialogFragment(fragmentClass)\n        } else {\n            MockFragment(fragmentClass)\n        }\n    }\n}\n\nclass MockFragment(val fragmentClass: Class<out Fragment>) : Fragment()\nclass MockDialogFragment(val fragmentClass: Class<out Fragment>) : DialogFragment()\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/MockWebPageService.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.app.Activity\nimport android.net.Uri\nimport org.odk.collect.webpage.WebPageService\n\nclass MockWebPageService : WebPageService {\n\n    private val _openedPages = mutableListOf<Uri>()\n    val openedPages: List<Uri> = _openedPages\n\n    override fun openWebPage(activity: Activity, uri: Uri) {\n        _openedPages += uri\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/RecyclerViewMatcher.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.content.res.Resources\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\nimport org.hamcrest.TypeSafeMatcher\n\n// https://github.com/dannyroa/espresso-samples/blob/master/RecyclerView/app/src/androidTest/java/com/dannyroa/espresso_samples/recyclerview/RecyclerViewMatcher.java\nclass RecyclerViewMatcher private constructor(private val recyclerViewId: Int) {\n\n    fun atPositionOnView(position: Int, targetViewId: Int): Matcher<View> {\n        return object : TypeSafeMatcher<View>() {\n            var resources: Resources? = null\n            var childView: View? = null\n\n            override fun describeTo(description: Description) {\n                var idDescription = recyclerViewId.toString()\n                if (resources != null) {\n                    idDescription = try {\n                        resources!!.getResourceName(recyclerViewId)\n                    } catch (e: Resources.NotFoundException) {\n                        String.format(\"%s (resource name not found)\", recyclerViewId)\n                    }\n                }\n                description.appendText(\"RecyclerView with id: $idDescription at position: $position\")\n            }\n\n            public override fun matchesSafely(view: View): Boolean {\n                resources = view.resources\n                if (childView == null) {\n                    val recyclerView: RecyclerView = view.rootView.findViewById(\n                        recyclerViewId\n                    )\n                    if (recyclerView.id == recyclerViewId) {\n                        val viewHolder = recyclerView.findViewHolderForAdapterPosition(position)\n                        if (viewHolder != null) {\n                            childView = viewHolder.itemView\n                        }\n                    } else {\n                        return false\n                    }\n                }\n                return if (targetViewId == -1) {\n                    view === childView\n                } else {\n                    val targetView = childView!!.findViewById<View>(targetViewId)\n                    view === targetView\n                }\n            }\n        }\n    }\n\n    companion object {\n        @JvmStatic\n        fun withRecyclerView(recyclerViewId: Int): RecyclerViewMatcher {\n            return RecyclerViewMatcher(recyclerViewId)\n        }\n\n        @JvmStatic\n        fun withListSize(size: Int): Matcher<View> {\n            return object : TypeSafeMatcher<View>() {\n                public override fun matchesSafely(view: View): Boolean {\n                    return (view as RecyclerView).adapter!!.itemCount == size\n                }\n\n                override fun describeTo(description: Description) {\n                    description.appendText(\"RecyclerView should have $size items\")\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/RobolectricHelpers.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.app.Activity\nimport android.app.Application\nimport android.app.Service\nimport android.content.Intent\nimport android.graphics.drawable.Drawable\nimport android.media.MediaMetadataRetriever\nimport android.os.Bundle\nimport android.os.Environment\nimport android.os.Looper\nimport android.view.ViewGroup\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentActivity\nimport androidx.fragment.app.FragmentManager\nimport androidx.lifecycle.Lifecycle\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.core.app.ApplicationProvider\nimport org.odk.collect.servicetest.ServiceScenario\nimport org.odk.collect.servicetest.ServiceScenario.Companion.launch\nimport org.robolectric.Robolectric\nimport org.robolectric.Shadows\nimport org.robolectric.Shadows.shadowOf\nimport org.robolectric.android.controller.ActivityController\nimport org.robolectric.shadows.ShadowEnvironment\nimport org.robolectric.shadows.ShadowMediaMetadataRetriever\nimport org.robolectric.shadows.ShadowMediaPlayer\nimport org.robolectric.shadows.ShadowMediaPlayer.MediaInfo\nimport org.robolectric.shadows.util.DataSource\n\nobject RobolectricHelpers {\n\n    var services: MutableMap<Class<*>, ServiceScenario<*>> = HashMap()\n\n    @JvmStatic\n    @JvmOverloads\n    fun <T : FragmentActivity> createThemedActivity(\n        clazz: Class<T>,\n        theme: Int = com.google.android.material.R.style.Theme_MaterialComponents\n    ): T {\n        val activity = Robolectric.buildActivity(clazz)\n        activity.get()!!.setTheme(theme)\n        return activity.setup().get()\n    }\n\n    @JvmStatic\n    fun getCreatedFromResId(drawable: Drawable): Int {\n        return Shadows.shadowOf(drawable).createdFromResId\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun setupMediaPlayerDataSource(testFile: String, duration: Int = 322450): DataSource {\n        val dataSource = DataSource.toDataSource(testFile)\n        ShadowMediaMetadataRetriever.addMetadata(\n            dataSource,\n            MediaMetadataRetriever.METADATA_KEY_DURATION,\n            duration.toString()\n        )\n        ShadowMediaPlayer.addMediaInfo(dataSource, MediaInfo(duration, 0))\n        return dataSource\n    }\n\n    fun mountExternalStorage() {\n        ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED)\n    }\n\n    @JvmStatic\n    fun <T : ViewGroup> populateRecyclerView(view: T): T {\n        for (i in 0 until view.childCount) {\n            val child = view.getChildAt(i)\n            if (child is RecyclerView) {\n                child.measure(0, 0)\n                child.layout(0, 0, 100, 10000)\n                break\n            } else if (child is ViewGroup) {\n                populateRecyclerView(child)\n            }\n        }\n        return view\n    }\n\n    @JvmStatic\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <F : Fragment?> getFragmentByClass(\n        fragmentManager: FragmentManager,\n        fragmentClass: Class<F>\n    ): F? {\n        val fragments = fragmentManager.fragments\n        for (fragment in fragments) {\n            if (fragment.javaClass.isAssignableFrom(fragmentClass)) {\n                return fragment as F\n            }\n        }\n        return null\n    }\n\n    @JvmStatic\n    fun runLooper() {\n        Shadows.shadowOf(Looper.getMainLooper()).idle()\n    }\n\n    fun clearServices() {\n        services.clear()\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun runServices(keepServices: Boolean) {\n        val application = ApplicationProvider.getApplicationContext<Application>()\n\n        // Run pending start commands\n        while (Shadows.shadowOf(application).peekNextStartedService() != null) {\n            val intent = Shadows.shadowOf(application).nextStartedService\n            val serviceClass: Class<*> = try {\n                Class.forName(intent.component!!.className)\n            } catch (e: ClassNotFoundException) {\n                throw RuntimeException(e)\n            }\n            if (keepServices) {\n                if (services.containsKey(serviceClass)) {\n                    services[serviceClass]!!.startWithNewIntent(intent)\n                } else {\n                    val serviceController: ServiceScenario<*> =\n                        launch(serviceClass as Class<Service>, intent)\n                    services[serviceClass] = serviceController\n                }\n            } else {\n                launch(serviceClass as Class<Service>, intent)\n            }\n        }\n\n        // Run pending stops - only need to stop previously started services\n        if (keepServices) {\n            while (true) {\n                val intent = Shadows.shadowOf(application).nextStoppedService ?: break\n                val serviceClass: Class<*> = try {\n                    Class.forName(intent.component!!.className)\n                } catch (e: ClassNotFoundException) {\n                    throw RuntimeException(e)\n                }\n                if (services.containsKey(serviceClass)) {\n                    services[serviceClass]!!.moveToState(Lifecycle.State.DESTROYED)\n                    services.remove(serviceClass)\n                }\n            }\n        }\n    }\n\n    inline fun <reified A : Activity> ActivityController<A>.recreateWithProcessRestore(\n        resultCode: Int? = null,\n        result: Intent? = null,\n        resetProcess: () -> Unit\n    ): ActivityController<A> {\n        // Destroy activity with saved instance state\n        val outState = Bundle()\n        this.saveInstanceState(outState).pause().stop().destroy()\n\n        // Reset process\n        resetProcess()\n\n        // Recreate with saved instance state\n        val recreated = Robolectric.buildActivity(A::class.java, this.intent).create(outState)\n            .start()\n            .restoreInstanceState(outState)\n            .postCreate(outState)\n\n        // Return result\n        if (resultCode != null) {\n            val startedActivityForResult = shadowOf(this.get()).nextStartedActivityForResult\n            shadowOf(recreated.get()).receiveResult(\n                startedActivityForResult.intent,\n                resultCode,\n                result\n            )\n        }\n\n        // Resume activity\n        return recreated.resume()\n            .visible()\n            .topActivityResumed(true)\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/SliderExt.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.view.MotionEvent\nimport com.google.android.material.slider.Slider\n\nfun Slider.clickOnMinValue() {\n    clickOnPosition(0f)\n}\n\nfun Slider.clickOnMaxValue() {\n    clickOnPosition(width.toFloat())\n}\n\nfun Slider.clickOnPosition(xPosition: Float) {\n    val currentTime = System.currentTimeMillis()\n\n    val downEvent = MotionEvent.obtain(\n        currentTime,\n        currentTime,\n        MotionEvent.ACTION_DOWN,\n        xPosition,\n        0f,\n        0\n    )\n    dispatchTouchEvent(downEvent)\n\n    val upEvent = MotionEvent.obtain(\n        currentTime,\n        currentTime,\n        MotionEvent.ACTION_UP,\n        xPosition,\n        0f,\n        0\n    )\n    dispatchTouchEvent(upEvent)\n\n    downEvent.recycle()\n    upEvent.recycle()\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/TimeZoneSetter.kt",
    "content": "package org.odk.collect.testshared\n\nimport org.joda.time.DateTimeZone\nimport java.util.TimeZone\n\nobject TimeZoneSetter {\n    /**\n     * Always update both java.util.TimeZone and org.joda.time.DateTimeZone to avoid weird bugs in\n     * tests that depend on time zones.\n     */\n    @JvmStatic\n    fun setTimezone(timezone: TimeZone) {\n        TimeZone.setDefault(timezone)\n        DateTimeZone.setDefault(DateTimeZone.forID(timezone.id))\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/ViewActions.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.content.Context\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.NumberPicker\nimport android.widget.RatingBar\nimport androidx.annotation.StringRes\nimport androidx.core.view.allViews\nimport androidx.recyclerview.widget.RecyclerView.ViewHolder\nimport androidx.test.espresso.UiController\nimport androidx.test.espresso.ViewAction\nimport androidx.test.espresso.action.GeneralLocation\nimport androidx.test.espresso.action.GeneralSwipeAction\nimport androidx.test.espresso.action.Press\nimport androidx.test.espresso.action.Swipe\nimport androidx.test.espresso.action.ViewActions.click\nimport androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem\nimport androidx.test.espresso.matcher.ViewMatchers\nimport androidx.test.espresso.matcher.ViewMatchers.hasDescendant\nimport org.hamcrest.Matcher\n\nobject ViewActions {\n\n    @JvmStatic\n    fun clickOnViewContentDescription(@StringRes stringId: Int, context: Context) = object : ViewAction {\n        override fun getConstraints() = null\n\n        override fun getDescription() = \"Click on a child view with specified content description.\"\n\n        override fun perform(uiController: UiController, view: View) {\n            for (child in (view as ViewGroup).allViews) {\n                if (child.contentDescription == context.getString(stringId)) {\n                    child.performClick()\n                    break\n                }\n            }\n        }\n    }\n\n    @JvmStatic\n    fun clickOnItemWith(matcher: Matcher<View>): ViewAction {\n        return actionOnItem<ViewHolder>(hasDescendant(matcher), click())\n    }\n\n    fun scrollNumberPickerToValue(targetValue: Int): ViewAction {\n        return object : ViewAction {\n            override fun getConstraints(): Matcher<View> {\n                return ViewMatchers.isAssignableFrom(NumberPicker::class.java)\n            }\n\n            override fun getDescription(): String {\n                return \"Scroll the NumberPicker to a specific value\"\n            }\n\n            override fun perform(uiController: UiController, view: View) {\n                val numberPicker = view as NumberPicker\n\n                while (targetValue != numberPicker.value) {\n                    if (targetValue < numberPicker.value) {\n                        swipeDown(uiController, view)\n                    } else {\n                        swipeUp(uiController, view)\n                    }\n                }\n            }\n\n            private fun swipeUp(uiController: UiController, view: View) {\n                GeneralSwipeAction(\n                    Swipe.SLOW,\n                    GeneralLocation.CENTER,\n                    GeneralLocation.TOP_CENTER,\n                    Press.FINGER\n                ).perform(uiController, view)\n            }\n\n            private fun swipeDown(uiController: UiController, view: View) {\n                GeneralSwipeAction(\n                    Swipe.SLOW,\n                    GeneralLocation.CENTER,\n                    GeneralLocation.BOTTOM_CENTER,\n                    Press.FINGER\n                ).perform(uiController, view)\n            }\n        }\n    }\n\n    @JvmStatic\n    fun setRating(rating: Float): ViewAction {\n        return object : ViewAction {\n            override fun getConstraints(): Matcher<View> {\n                return ViewMatchers.isAssignableFrom(RatingBar::class.java)\n            }\n\n            override fun getDescription(): String {\n                return \"Custom view action to set rating on RatingBar\"\n            }\n\n            override fun perform(uiController: UiController, view: View) {\n                val ratingBar = view as RatingBar\n                ratingBar.rating = rating\n            }\n        }\n    }\n\n    fun setNumber(number: String): ViewAction {\n        return object : ViewAction {\n            override fun getConstraints(): Matcher<View?>? {\n                return ViewMatchers.isAssignableFrom(NumberPicker::class.java)\n            }\n\n            override fun getDescription(): String {\n                return \"\"\n            }\n\n            override fun perform(uiController: UiController, view: View) {\n                val numberPicker = view as NumberPicker\n                val index = numberPicker.displayedValues.indexOfFirst { it == number }\n                numberPicker.value = index\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/ViewMatchers.kt",
    "content": "package org.odk.collect.testshared\n\nimport android.view.View\nimport android.widget.NumberPicker\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.test.espresso.matcher.ViewMatchers\nimport org.hamcrest.Description\nimport org.hamcrest.Matcher\nimport org.hamcrest.TypeSafeMatcher\nimport org.odk.collect.testshared.RecyclerViewMatcher.Companion.withRecyclerView\n\nobject ViewMatchers {\n\n    @JvmStatic\n    fun recyclerView(): Matcher<View> {\n        return ViewMatchers.isAssignableFrom(RecyclerView::class.java)\n    }\n\n    fun atPositionInRecyclerView(listId: Int, position: Int, childViewId: Int): Matcher<View> {\n        return withRecyclerView(listId)\n            .atPositionOnView(\n                position,\n                childViewId\n            )\n    }\n\n    fun hasPicked(number: String): Matcher<View> {\n        return object : TypeSafeMatcher<View>() {\n            override fun matchesSafely(view: View): Boolean {\n                return view is NumberPicker && view.displayedValues[view.value] == number\n            }\n\n            override fun describeTo(description: Description) {\n                description.appendText(\"is NumberPicker with picked value: \")\n                description.appendValue(number)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test-shared/src/main/java/org/odk/collect/testshared/WaitFor.kt",
    "content": "package org.odk.collect.testshared\n\nimport junit.framework.AssertionFailedError\nimport org.odk.collect.shared.TimeInMs\nimport java.util.concurrent.Callable\n\nobject WaitFor {\n\n    @JvmStatic\n    fun <T> waitFor(callable: Callable<T>): T {\n        var failure: Throwable? = null\n        val startTime = System.currentTimeMillis()\n\n        // Try for 5 seconds\n        while ((System.currentTimeMillis() - startTime) < (5 * TimeInMs.ONE_SECOND)) {\n            failure = try {\n                return callable.call()\n            } catch (throwable: Exception) {\n                throwable\n            } catch (throwable: AssertionFailedError) {\n                throwable\n            } catch (throwable: AssertionError) {\n                throwable\n            }\n\n            Thread.sleep(10)\n        }\n\n        throw failure!!\n    }\n\n    @JvmStatic\n    @JvmOverloads\n    fun tryAgainOnFail(maxTimes: Int = 2, action: Runnable) {\n        var failure: Throwable? = null\n        for (i in 0 until maxTimes) {\n            try {\n                action.run()\n                return\n            } catch (e: Throwable) {\n                failure = e\n                Thread.sleep(250)\n            }\n        }\n\n        throw RuntimeException(\"tryAgainOnFail failed\", failure)\n    }\n}\n"
  },
  {
    "path": "timedgrid/.gitignore",
    "content": "/build"
  },
  {
    "path": "timedgrid/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.timedgrid\"\n\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(getDefaultProguardFile(\"proguard-android-optimize.txt\"))\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n        buildConfig = true\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":androidshared\"))\n    implementation(project(\":strings\"))\n\n    implementation(libs.androidxFragmentKtx)\n    implementation(libs.androidMaterial)\n    implementation(libs.androidxAppcompat)\n    implementation(libs.androidFlexbox)\n    implementation(libs.androidxLifecycleViewmodelKtx)\n    implementation(libs.javarosa) {\n        exclude(group = \"joda-time\")\n        exclude(group = \"org.hamcrest\", module = \"hamcrest-all\")\n    }\n}\n"
  },
  {
    "path": "timedgrid/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/AssessmentType.kt",
    "content": "package org.odk.collect.timedgrid\n\nenum class AssessmentType(\n    private val rendererFactory: () -> TimedGridRenderer\n) {\n    LETTERS(::CommonTimedGridRenderer),\n    WORDS(::CommonTimedGridRenderer),\n    NUMBERS(::CommonTimedGridRenderer),\n    ARITHMETIC(::CommonTimedGridRenderer),\n    READING({ CommonTimedGridRenderer(showRowNumbers = false) });\n\n    fun createRenderer(): TimedGridRenderer = rendererFactory()\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/CommonTimedGridRenderer.kt",
    "content": "package org.odk.collect.timedgrid\n\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport org.odk.collect.timedgrid.databinding.TimedGridBinding\n\n/**\n * Common renderer for timed grid modes that share the same UI and interaction pattern:\n * LETTERS, WORDS, NUMBERS, ARITHMETIC, READING, X.\n */\nclass CommonTimedGridRenderer(\n    private val showRowNumbers: Boolean = true\n) : TimedGridRenderer {\n\n    private lateinit var binding: TimedGridBinding\n    private lateinit var grid: TimedGridWidgetLayout\n    private var derivedState: TimedGridState = TimedGridState.NEW\n\n    override fun inflateView(inflater: LayoutInflater, parent: ViewGroup): View {\n        binding = TimedGridBinding.inflate(inflater, parent, false)\n        return binding.root\n    }\n\n    override fun bind(\n        root: View,\n        configuration: TimedGridWidgetConfiguration,\n        items: List<GridItem>,\n        onStart: () -> Unit,\n        onComplete: () -> Unit,\n        onFinish: () -> Unit,\n        onTogglePause: () -> Unit,\n    ) {\n        binding.buttonStart.setOnClickListener { onStart() }\n        binding.buttonComplete.setOnClickListener { onComplete() }\n        binding.buttonTimer.setOnClickListener { onTogglePause() }\n        binding.buttonFinish.setOnClickListener { onFinish() }\n\n        grid = TimedGridWidgetLayout(\n            layoutInflater = LayoutInflater.from(binding.root.context),\n            containerView = binding.containerRows,\n            columns = configuration.columns,\n            rowsPerPage = configuration.rowsPerPage,\n            type = configuration.type,\n            showRowNumbers = showRowNumbers,\n            strictMode = configuration.strict,\n            endAfterConsecutive = configuration.endAfterConsecutive,\n            onComplete = onComplete,\n            allItems = items\n        )\n\n        // Pagination buttons\n        binding.buttonPrev.setOnClickListener {\n            grid.prevPage()\n            updatePaginationButtons()\n        }\n        binding.buttonNext.setOnClickListener {\n            grid.nextPage()\n            updatePaginationButtons()\n        }\n\n        // Enable Finish when assessor picks last item.\n        grid.setLastItemSelectionListener { _ ->\n            binding.buttonFinish.isEnabled = true\n        }\n\n        updatePaginationButtons()\n    }\n\n    private fun updatePaginationButtons() {\n        binding.buttonPrev.visibility = if (grid.isFirstPage()) View.GONE else View.VISIBLE\n        binding.buttonNext.visibility = if (grid.isLastPage()) View.GONE else View.VISIBLE\n        binding.buttonComplete.visibility = if (grid.isLastPage() && derivedState == TimedGridState.IN_PROGRESS) View.VISIBLE else View.GONE\n    }\n\n    override fun updateUIForState(state: TimedGridState) {\n        derivedState = state\n\n        when (state) {\n            TimedGridState.NEW -> {\n                binding.buttonStart.visibility = View.VISIBLE\n                binding.buttonTimer.visibility = View.GONE\n                binding.buttonFinish.visibility = View.GONE\n                binding.buttonComplete.visibility = View.GONE\n\n                grid.setGridEnabled(false)\n            }\n\n            TimedGridState.IN_PROGRESS -> {\n                binding.buttonStart.visibility = View.GONE\n                binding.buttonTimer.visibility = View.VISIBLE\n                binding.buttonFinish.visibility = View.GONE\n                binding.buttonComplete.visibility = View.VISIBLE\n\n                grid.setGridEnabled(true)\n                updatePaginationButtons()\n            }\n\n            TimedGridState.PAUSED -> {\n                binding.buttonStart.visibility = View.GONE\n                binding.buttonTimer.visibility = View.VISIBLE\n                binding.buttonFinish.visibility = View.GONE\n                binding.buttonComplete.visibility = View.GONE\n\n                grid.setGridEnabled(false)\n                updatePaginationButtons()\n            }\n\n            TimedGridState.COMPLETED_NO_LAST_ITEM -> {\n                binding.buttonStart.visibility = View.GONE\n                binding.buttonTimer.visibility = View.GONE\n                binding.buttonFinish.visibility = View.VISIBLE\n                binding.buttonComplete.visibility = View.GONE\n\n                val lastAttempted = getLastSelectedLastItemValue()\n\n                if (lastAttempted == null) {\n                    // Disable Finish until last-item selected\n                    binding.buttonFinish.isEnabled = false\n\n                    // Allow selecting last-item after the last toggled (or any if none)\n                    val lastToggled = grid.getLastToggledValue()\n                    grid.setEnabledFrom(lastToggled)\n                }\n            }\n\n            TimedGridState.COMPLETED -> {\n                binding.buttonStart.visibility = View.GONE\n                binding.buttonTimer.visibility = View.GONE\n                binding.buttonFinish.visibility = View.GONE\n                binding.buttonComplete.visibility = View.GONE\n\n                // Final visuals: wrong answers + highlight last item\n                val wrongAnswers = grid.getToggledAnswers()\n                val lastSelected = grid.getLastSelectedLastItemValue()\n                grid.revealFinalSelection(wrongAnswers, lastSelected)\n            }\n        }\n    }\n\n    override fun updateTimer(timeString: String) {\n        binding.buttonTimer.text = timeString\n    }\n\n    override fun getLastSelectedLastItemValue(): String? =\n        grid.getLastSelectedLastItemValue()\n\n    override fun getToggledAnswers(): Set<String> =\n        grid.getToggledAnswers()\n\n    override fun firstLineAllIncorrect(): Boolean = grid.firstLineSelected()\n\n    override fun restoreAnswers(toggled: Set<String>, last: String?) {\n        grid.setToggledAnswers(toggled)\n        grid.setLastSelectedLastItemValue(last)\n    }\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/FinishType.kt",
    "content": "package org.odk.collect.timedgrid\n\nenum class FinishType(val code: Int) {\n    /** User confirms and picks last attempted item manually */\n    CONFIRM_AND_PICK(1),\n\n    /** User confirms, auto-pick last item in list */\n    CONFIRM_AND_AUTO_PICK(2),\n\n    /** No confirm, auto-pick last item in list */\n    AUTO_PICK_NO_CONFIRM(3);\n\n    companion object {\n        fun fromInt(value: Int): FinishType =\n            entries.find { it.code == value } ?: CONFIRM_AND_PICK\n    }\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/NavigationAwareWidget.kt",
    "content": "package org.odk.collect.timedgrid\n\nimport androidx.annotation.StringRes\n\ninterface NavigationAwareWidget {\n\n    /**\n     * Returns NavigationBlock if navigation should be blocked, or null if navigation is allowed.\n     */\n    fun shouldBlockNavigation(): NavigationWarning?\n}\n\ndata class NavigationWarning(\n    @StringRes val titleRes: Int,\n    @StringRes val messageRes: Int\n)\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/PausableCountDownTimer.kt",
    "content": "package org.odk.collect.timedgrid\n\nimport android.os.CountDownTimer\n\nclass PausableCountDownTimer {\n    private var millisRemaining: Long = 0\n    private lateinit var onTick: (millisUntilFinished: Long) -> Unit\n    private lateinit var onFinish: () -> Unit\n\n    private var timer: CountDownTimer? = null\n    private var isPaused: Boolean = true\n\n    fun setUpListeners(onTick: (millisUntilFinished: Long) -> Unit, onFinish: () -> Unit) {\n        this.onTick = onTick\n        this.onFinish = onFinish\n    }\n\n    fun setUpDuration(millisRemaining: Long) {\n        this.millisRemaining = millisRemaining\n    }\n\n    /**\n     * Starts or resumes the countdown.\n     * @return This PausableCountDownTimer.\n     */\n    @Synchronized\n    fun start(): PausableCountDownTimer {\n        if (isPaused) {\n            isPaused = false\n            timer = object : CountDownTimer(millisRemaining, 1000) {\n                override fun onTick(millisUntilFinished: Long) {\n                    millisRemaining = millisUntilFinished\n                    this@PausableCountDownTimer.onTick(millisRemaining)\n                }\n\n                override fun onFinish() {\n                    millisRemaining = 0\n                    this@PausableCountDownTimer.onFinish()\n                }\n            }.start()\n        }\n        return this\n    }\n\n    /**\n     * Pauses the countdown.\n     */\n    @Synchronized\n    fun pause() {\n        if (!isPaused) {\n            timer?.cancel()\n            timer = null\n        }\n        isPaused = true\n    }\n\n    /**\n     * Cancels the countdown and resets the timer.\n     */\n    @Synchronized\n    fun cancel() {\n        timer?.cancel()\n        timer = null\n        isPaused = true\n    }\n\n    fun getMillisRemaining(): Long {\n        return millisRemaining\n    }\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/TimedGridRenderer.kt",
    "content": "package org.odk.collect.timedgrid\n\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\n\ninterface TimedGridRenderer {\n    fun inflateView(inflater: LayoutInflater, parent: ViewGroup): View\n\n    fun bind(\n        root: View,\n        configuration: TimedGridWidgetConfiguration,\n        items: List<GridItem>,\n        onStart: () -> Unit,\n        onComplete: () -> Unit,\n        onFinish: () -> Unit,\n        onTogglePause: () -> Unit\n    )\n\n    fun updateUIForState(state: TimedGridState)\n\n    fun updateTimer(timeString: String)\n\n    fun getLastSelectedLastItemValue(): String?\n\n    fun getToggledAnswers(): Set<String>\n\n    fun firstLineAllIncorrect(): Boolean\n\n    fun restoreAnswers(toggled: Set<String>, last: String?) {}\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/TimedGridState.kt",
    "content": "package org.odk.collect.timedgrid\n\n// Lifecycle states for the timed grid widget.\nenum class TimedGridState {\n    NEW, IN_PROGRESS, PAUSED, COMPLETED_NO_LAST_ITEM, COMPLETED\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/TimedGridSummary.kt",
    "content": "package org.odk.collect.timedgrid\n\ndata class TimedGridSummary(\n    val secondsRemaining: Int,\n    val attemptedCount: Int,\n    val incorrectCount: Int,\n    val correctCount: Int,\n    val firstLineAllIncorrect: Boolean,\n    val sentencesPassed: Int,\n    val correctItems: String,\n    val unansweredItems: String,\n    val punctuationCount: Int,\n) {\n    data class Builder(\n        var secondsRemaining: Int = 0,\n        var attemptedCount: Int = 0,\n        var incorrectCount: Int = 0,\n        var correctCount: Int = 0,\n        var firstLineAllIncorrect: Boolean = false,\n        var sentencesPassed: Int = 0,\n        var correctItems: String = \"\",\n        var unansweredItems: String = \"\",\n        var punctuationCount: Int = 0,\n    ) {\n        fun secondsRemaining(secondsRemaining: Int) = apply { this.secondsRemaining = secondsRemaining }\n        fun attemptedCount(attemptedCount: Int) = apply { this.attemptedCount = attemptedCount }\n        fun incorrectCount(incorrectCount: Int) = apply { this.incorrectCount = incorrectCount }\n        fun correctCount(correctCount: Int) = apply { this.correctCount = correctCount }\n        fun firstLineAllIncorrect(firstLineAllIncorrect: Boolean) = apply { this.firstLineAllIncorrect = firstLineAllIncorrect }\n        fun sentencesPassed(sentencesPassed: Int) = apply { this.sentencesPassed = sentencesPassed }\n        fun correctItems(correctItems: String) = apply { this.correctItems = correctItems }\n        fun unansweredItems(unansweredItems: String) = apply { this.unansweredItems = unansweredItems }\n        fun punctuationCount(punctuationCount: Int) = apply { this.punctuationCount = punctuationCount }\n        fun build() = TimedGridSummary(\n            secondsRemaining,\n            attemptedCount,\n            incorrectCount,\n            correctCount,\n            firstLineAllIncorrect,\n            sentencesPassed,\n            correctItems,\n            unansweredItems,\n            punctuationCount\n        )\n    }\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/TimedGridSummaryAnswerCreator.kt",
    "content": "package org.odk.collect.timedgrid\n\nimport org.javarosa.core.model.FormIndex\nimport org.javarosa.core.model.GroupDef\nimport org.javarosa.core.model.IFormElement\nimport org.javarosa.core.model.QuestionDef\nimport org.javarosa.core.model.SelectChoice\nimport org.javarosa.core.model.data.BooleanData\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.core.model.data.IntegerData\nimport org.javarosa.core.model.data.StringData\nimport org.javarosa.core.model.instance.TreeReference\nimport org.javarosa.form.api.FormEntryPrompt\n\nclass TimedGridSummaryAnswerCreator(\n    private val formEntryPrompt: FormEntryPrompt,\n    private val formControllerFacade: FormControllerFacade,\n    private val formAnswerRefresher: FormAnswerRefresher\n) {\n    companion object {\n        val SUMMARY_QUESTION_APPEARANCE_REGEX = Regex(\"\"\"x-timed-grid-answer\\((.+),(.+)\\)\"\"\")\n    }\n\n    fun answerSummaryQuestions(summary: TimedGridSummary) {\n        val timedGridQuestionId = formEntryPrompt.index.reference.toString(false)\n\n        forEachFormQuestionDef(formControllerFacade.getFormElements()) { questionIndex, questionDef ->\n            val summaryQuestionMatch =\n                SUMMARY_QUESTION_APPEARANCE_REGEX.find(questionDef.appearanceAttr ?: \"\")\n\n            if (summaryQuestionMatch != null) {\n                val referencedQuestion = summaryQuestionMatch.groupValues[1].trim()\n                val metadataName = summaryQuestionMatch.groupValues[2].trim()\n\n                if (referencedQuestion == timedGridQuestionId) {\n                    val answer = getSummaryAnswer(metadataName, summary)\n                    formControllerFacade.saveAnswer(questionIndex, answer)\n                    formAnswerRefresher.refreshAnswer(questionIndex)\n                }\n            }\n        }\n    }\n\n    private fun forEachFormQuestionDef(\n        iFormElements: Iterable<IFormElement>?,\n        currentFormIndex: FormIndex? = null,\n        action: (formIndex: FormIndex, IFormElement) -> Unit\n    ) {\n        iFormElements?.forEachIndexed { index, formElement ->\n            val nextLevelIndex = FormIndex(index, formElement.bind.reference as TreeReference)\n            if (formElement is GroupDef) {\n                forEachFormQuestionDef(formElement.children, nextLevelIndex, action)\n            } else if (formElement is QuestionDef) {\n                action(FormIndex(nextLevelIndex, currentFormIndex), formElement)\n            }\n        }\n    }\n\n    private fun getSummaryAnswer(metadataName: String, summary: TimedGridSummary): IAnswerData {\n        return when (metadataName) {\n            \"time-remaining\" -> IntegerData(summary.secondsRemaining)\n            \"attempted-count\" -> IntegerData(summary.attemptedCount)\n            \"incorrect-count\" -> IntegerData(summary.incorrectCount)\n            \"correct-count\" -> IntegerData(summary.correctCount)\n            \"first-line-all-incorrect\" -> BooleanData(summary.firstLineAllIncorrect)\n            \"sentences-passed\" -> IntegerData(summary.sentencesPassed)\n            \"correct-items\" -> StringData(summary.correctItems)\n            \"unanswered-items\" -> StringData(summary.unansweredItems)\n            \"punctuation-count\" -> IntegerData(summary.punctuationCount)\n            else -> throw IllegalArgumentException(\"Unknown metadata name: $metadataName\")\n        }\n    }\n}\n\ninterface FormControllerFacade {\n    fun getFormElements(): List<IFormElement>?\n    fun getItems(): List<SelectChoice>\n    fun saveAnswer(index: FormIndex, answer: IAnswerData)\n}\n\ninterface FormAnswerRefresher {\n    fun refreshAnswer(index: FormIndex)\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/TimedGridViewModel.kt",
    "content": "package org.odk.collect.timedgrid\n\nimport androidx.lifecycle.ViewModel\nimport org.javarosa.core.model.FormIndex\n\nclass TimedGridViewModel : ViewModel() {\n\n    data class TimedGridTimerState(\n        val millisRemaining: Long,\n        val state: TimedGridState,\n        val toggledAnswers: Set<String> = emptySet(),\n        val lastAttempted: String? = null\n    )\n\n    private val timedGridStates = mutableMapOf<FormIndex, TimedGridTimerState>()\n\n    fun saveTimedGridState(index: FormIndex, state: TimedGridTimerState) {\n        timedGridStates[index] = state\n    }\n\n    fun getTimedGridState(index: FormIndex): TimedGridTimerState? {\n        return timedGridStates[index]\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n        timedGridStates.clear()\n    }\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/TimedGridWidgetConfiguration.kt",
    "content": "package org.odk.collect.timedgrid\n\nimport androidx.core.text.isDigitsOnly\nimport org.javarosa.form.api.FormEntryPrompt\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\n\ndata class TimedGridWidgetConfiguration(\n    /** The type of assessment being conducted (e.g., LETTERS, WORDS). */\n    val type: AssessmentType,\n    /** The number of columns to display in the grid. */\n    val columns: Int,\n    /** The number of rows to display per page in the grid. */\n    val rowsPerPage: Int,\n    /** The total duration of the timed assessment. */\n    val duration: Duration,\n    /** The number of consecutive incorrect answers after which the assessment should end early. */\n    val endAfterConsecutive: Int,\n    /** Enable strict mode that disables selection after time expires and enforces early-end behavior. */\n    val strict: Boolean,\n    /** Allows to pause timer during assessment. */\n    val allowPause: Boolean,\n    /** A special value stored when all answers are marked correctly. */\n    val allAnsweredCorrectly: String,\n    /** Determines how the task finishes */\n    val finish: FinishType\n) {\n    class Builder {\n        private var type: AssessmentType = AssessmentType.LETTERS\n        private var columns: Int = 10\n        private var rowsPerPage: Int = 5\n        private var duration: Duration = 120.seconds\n        private var endAfterConsecutive: Int = 5\n        private var strict: Boolean = false\n        private var allowPause: Boolean = true\n        private var allAnsweredCorrectly: String = \"999\"\n        private var finish: FinishType = FinishType.CONFIRM_AND_PICK\n\n        fun type(type: AssessmentType) = apply { this.type = type }\n        fun columns(columns: Int) = apply { this.columns = columns }\n        fun rowsPerPage(rowsPerPage: Int) = apply { this.rowsPerPage = rowsPerPage }\n        fun duration(duration: Duration) = apply { this.duration = duration }\n        fun endAfterConsecutive(endAfterConsecutive: Int) = apply { this.endAfterConsecutive = endAfterConsecutive }\n        fun strict(strict: Boolean) = apply { this.strict = strict }\n        fun allowPause(allowPause: Boolean) = apply { this.allowPause = allowPause }\n        fun allAnsweredCorrectly(allAnsweredCorrectly: String) = apply { this.allAnsweredCorrectly = allAnsweredCorrectly }\n        fun finish(finish: FinishType) = apply { this.finish = finish }\n\n        fun build(): TimedGridWidgetConfiguration {\n            return TimedGridWidgetConfiguration(\n                type,\n                columns,\n                rowsPerPage,\n                duration,\n                endAfterConsecutive,\n                strict,\n                allowPause,\n                allAnsweredCorrectly,\n                finish\n            )\n        }\n    }\n\n    companion object {\n        private object Keys {\n            const val TYPE = \"type\"\n            const val COLUMNS = \"columns\"\n            const val PAGE_ROWS = \"page-rows\"\n            const val DURATION = \"duration\"\n            const val END_AFTER = \"end-after\"\n            const val STRICT = \"strict\"\n            const val PAUSE = \"pause\"\n            const val ALL_ANSWERED = \"all-answered\"\n            const val FINISH = \"finish\"\n        }\n\n        fun fromPrompt(prompt: FormEntryPrompt): TimedGridWidgetConfiguration {\n            val params = extractParams(prompt.appearanceHint)\n\n            val detectedType: AssessmentType\n            var columnsOverride: Int? = null\n\n            val typeParam = params[Keys.TYPE]\n            if (typeParam != null) {\n                if (typeParam.isDigitsOnly()) {\n                    detectedType = AssessmentType.LETTERS\n                    columnsOverride = typeParam.toInt()\n                } else {\n                    detectedType = try {\n                        AssessmentType.valueOf(typeParam.uppercase())\n                    } catch (e: IllegalArgumentException) {\n                        AssessmentType.LETTERS\n                    }\n                }\n            } else {\n                detectedType = AssessmentType.LETTERS\n            }\n\n            val configBuilder = Builder().apply {\n                type(detectedType)\n                when (detectedType) {\n                    AssessmentType.LETTERS -> {\n                        columns(10)\n                        rowsPerPage(5)\n                        duration(120.seconds)\n                        endAfterConsecutive(5)\n                        strict(false)\n                        allowPause(false)\n                        allAnsweredCorrectly(\"999\")\n                        finish(FinishType.CONFIRM_AND_PICK)\n                    }\n                    AssessmentType.WORDS -> {\n                        columns(5)\n                        rowsPerPage(5)\n                        duration(120.seconds)\n                        endAfterConsecutive(5)\n                        strict(false)\n                        allowPause(false)\n                        allAnsweredCorrectly(\"999\")\n                        finish(FinishType.CONFIRM_AND_PICK)\n                    }\n                    AssessmentType.READING -> {\n                        columns(1)\n                        rowsPerPage(1)\n                        duration(120.seconds)\n                        endAfterConsecutive(5)\n                        strict(false)\n                        allowPause(false)\n                        allAnsweredCorrectly(\"999\")\n                        finish(FinishType.CONFIRM_AND_PICK)\n                    }\n                    AssessmentType.NUMBERS -> {\n                        columns(5)\n                        rowsPerPage(5)\n                        duration(120.seconds)\n                        endAfterConsecutive(5)\n                        strict(false)\n                        allowPause(false)\n                        allAnsweredCorrectly(\"999\")\n                        finish(FinishType.CONFIRM_AND_PICK)\n                    }\n                    AssessmentType.ARITHMETIC -> {\n                        columns(2)\n                        rowsPerPage(5)\n                        duration(120.seconds)\n                        endAfterConsecutive(5)\n                        strict(false)\n                        allowPause(false)\n                        allAnsweredCorrectly(\"999\")\n                        finish(FinishType.CONFIRM_AND_PICK)\n                    }\n                }\n            }\n\n            columnsOverride?.let { configBuilder.columns(it) }\n\n            if (columnsOverride == null) {\n                params[Keys.COLUMNS]?.let { configBuilder.columns(it.toInt()) }\n            }\n            params[Keys.FINISH]?.let {\n                val finishValue = it.toIntOrNull()\n                if (finishValue != null) {\n                    configBuilder.finish(FinishType.fromInt(finishValue))\n                }\n            }\n            params[Keys.PAGE_ROWS]?.let { configBuilder.rowsPerPage(it.toInt()) }\n            params[Keys.DURATION]?.let { configBuilder.duration(it.toInt().seconds) }\n            params[Keys.END_AFTER]?.let { configBuilder.endAfterConsecutive(it.toInt()) }\n            params[Keys.STRICT]?.let { configBuilder.strict(it.toBoolean()) }\n            params[Keys.PAUSE]?.let {\n                val normalized = it.trim().lowercase()\n                val allowPause = normalized == \"true\" || normalized == \"1\"\n                configBuilder.allowPause(allowPause)\n            }\n            params[Keys.ALL_ANSWERED]?.let { configBuilder.allAnsweredCorrectly(it) }\n\n            return configBuilder.build()\n        }\n    }\n}\n\nprivate fun extractParams(appearance: String): Map<String, String> {\n    val params = mutableMapOf<String, String>()\n    // Allow for optional whitespace around '=' and support hyphenated keys\n    val regex = Regex(\"([\\\\w-]+)\\\\s*=\\\\s*([^,)]+)\")\n    val matches = regex.findAll(appearance)\n\n    for (matchResult in matches) {\n        val (key, value) = matchResult.destructured\n        params[key.trim()] = value.trim()\n    }\n    return params\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/TimedGridWidgetDelegate.kt",
    "content": "package org.odk.collect.timedgrid\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.ViewModelStoreOwner\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.javarosa.core.model.data.IAnswerData\nimport org.javarosa.core.model.data.MultipleItemsData\nimport org.javarosa.core.model.data.helper.Selection\nimport org.javarosa.form.api.FormEntryPrompt\nimport kotlin.collections.ifEmpty\nimport kotlin.collections.toMutableList\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass TimedGridWidgetDelegate(\n    private val context: Context,\n    private val formEntryPrompt: FormEntryPrompt,\n    formControllerFacade: FormControllerFacade,\n    formAnswerRefresher: FormAnswerRefresher,\n    private val widgetValueChanged: () -> Unit\n) {\n    private val viewModel = ViewModelProvider(context as ViewModelStoreOwner)[TimedGridViewModel::class.java]\n    private val items = formControllerFacade.getItems()\n    private val timer = PausableCountDownTimer()\n\n    // Parsed prompt configuration (type, duration, etc.).\n    private val config = TimedGridWidgetConfiguration.fromPrompt(\n        formEntryPrompt\n    ).also {\n        timer.setUpDuration(it.duration.inWholeMilliseconds)\n    }\n\n    private val summaryAnswerCreator = TimedGridSummaryAnswerCreator(formEntryPrompt, formControllerFacade, formAnswerRefresher)\n\n    // Filtered items to display in the grid.\n    private val gridItems = items\n        .filter { it.value != config.allAnsweredCorrectly }\n        .map { GridItem(it.value, formEntryPrompt.getSelectChoiceText(it)) }\n\n    // Renderer chosen by the assessment type.\n    private val renderer = config.type.createRenderer()\n\n    // Current widget state and timer reference.\n    private var state = TimedGridState.NEW\n\n    private val summaryBuilder = TimedGridSummary.Builder()\n\n    fun onCreateWidgetView(parent: ViewGroup): View {\n        val inflater = LayoutInflater.from(context)\n        val root = renderer.inflateView(inflater, parent)\n        renderer.bind(\n            root,\n            config,\n            gridItems,\n            onStart = { startAssessment() },\n            onComplete = { onEarlyFinishPress() },\n            onFinish = { finishAssessment() },\n            onTogglePause = { if (state == TimedGridState.PAUSED) resumeAssessment() else pauseAssessment() }\n        )\n\n        timer.setUpListeners(\n            this::onTimerTick,\n            this::onTimerFinish\n        )\n\n        viewModel.getTimedGridState(formEntryPrompt.index)?.let { saved ->\n            state = saved.state\n            timer.cancel()\n            timer.setUpDuration(saved.millisRemaining)\n\n            renderer.restoreAnswers(saved.toggledAnswers, saved.lastAttempted)\n\n            if (state == TimedGridState.IN_PROGRESS) {\n                timer.start()\n            } else if (state == TimedGridState.PAUSED) {\n                val secondsRemaining = saved.millisRemaining.milliseconds.inWholeSeconds\n                val timeLeftText = context.getString(\n                    R.string.timed_grid_time_left,\n                    secondsRemaining\n                )\n                renderer.updateTimer(timeLeftText)\n                summaryBuilder.secondsRemaining(secondsRemaining.toInt())\n            }\n\n            renderer.updateUIForState(state)\n        } ?: run {\n            readSavedItems()\n            renderer.updateUIForState(state)\n        }\n\n        return root\n    }\n\n    /**\n     * Returns all selected (wrong) answers followed by last attempted answer.\n     * If no answers ware selected, a special value is used instead.\n     * [selected answers, last attempted answer] where selected answers can be real selected answers or special value for \"all correct\".\n     */\n    fun getAnswer(): IAnswerData {\n        val selectedValues = renderer.getToggledAnswers().ifEmpty {\n            setOf(config.allAnsweredCorrectly)\n        }.toMutableList()\n\n        renderer.getLastSelectedLastItemValue()?.let {\n            selectedValues.add(it)\n        }\n\n        val choicesByValue = items.associateBy { it.value }\n        return MultipleItemsData(selectedValues.map { selectedValue -> Selection(choicesByValue[selectedValue]) })\n    }\n\n    fun onDetachedFromWindow() {\n        // Ensure state is saved when widget is removed\n        saveTimerState()\n        timer.cancel()\n    }\n\n    private fun onTimerTick(millisUntilFinished: Long) {\n        val secondsRemaining = millisUntilFinished.milliseconds.inWholeSeconds\n        val timeLeftText = context.getString(\n            R.string.timed_grid_time_left,\n            secondsRemaining\n        )\n        renderer.updateTimer(timeLeftText)\n        summaryBuilder.secondsRemaining(secondsRemaining.toInt())\n    }\n\n    private fun onTimerFinish() {\n        val timeLeftText = context.getString(\n            R.string.timed_grid_time_left,\n            0\n        )\n        renderer.updateTimer(timeLeftText)\n        if (config.strict) {\n            completeAssessment()\n        }\n    }\n\n    private fun startAssessment() {\n        renderer.restoreAnswers(emptySet(), null)\n        state = TimedGridState.IN_PROGRESS\n        renderer.updateUIForState(state)\n        timer.start()\n    }\n\n    private fun pauseAssessment() {\n        if (!config.allowPause) {\n            return\n        }\n\n        timer.pause()\n        state = TimedGridState.PAUSED\n        saveTimerState()\n        renderer.updateUIForState(state)\n    }\n\n    private fun resumeAssessment() {\n        if (!config.allowPause) {\n            return\n        }\n\n        timer.start()\n        state = TimedGridState.IN_PROGRESS\n        renderer.updateUIForState(state)\n    }\n\n    private fun onEarlyFinishPress() {\n        when (config.finish) {\n            FinishType.CONFIRM_AND_PICK -> {\n                showConfirmFinishDialog { completeAssessment() }\n            }\n            FinishType.CONFIRM_AND_AUTO_PICK -> {\n                showConfirmFinishDialog {\n                    completeAssessment(true)\n                }\n            }\n            FinishType.AUTO_PICK_NO_CONFIRM -> {\n                completeAssessment(true)\n            }\n        }\n    }\n\n    private fun showConfirmFinishDialog(onConfirm: () -> Unit) {\n        MaterialAlertDialogBuilder(context)\n            .setTitle(R.string.early_finish_title)\n            .setMessage(R.string.early_finish_message)\n            .setPositiveButton(R.string.end_test) { _, _ ->\n                onConfirm()\n            }\n            .setNegativeButton(R.string.continue_test, null)\n            .show()\n    }\n\n    private fun readSavedItems() {\n        val selections = (formEntryPrompt.answerValue?.value as? List<Selection>)\n            ?: emptyList()\n\n        val choiceByValue = items.associateBy { it.value }\n        val mapped = selections.mapNotNull { choiceByValue[it.value] }\n\n        if (mapped.isNotEmpty()) {\n            val toggledSet = mapped.take(mapped.size - 1)\n                .mapNotNull { it.value }\n                .toSet()\n\n            val lastValue = mapped.last().value\n            renderer.restoreAnswers(\n                toggledSet,\n                lastValue.takeIf { it != config.allAnsweredCorrectly }\n            )\n        }\n    }\n\n    private fun completeAssessment(forceAutopick: Boolean = false) {\n        timer.cancel()\n        state = TimedGridState.COMPLETED_NO_LAST_ITEM\n        saveTimerState()\n        renderer.updateUIForState(state)\n\n        // If the last item is toggled, auto-pick it as last attempted\n        val lastItemValue = gridItems.lastOrNull()?.value\n        if (forceAutopick ||\n            (lastItemValue != null && renderer.getToggledAnswers().contains(lastItemValue))\n        ) {\n            autoPickLastItem()\n            finishAssessment()\n        }\n    }\n\n    private fun finishAssessment() {\n        state = TimedGridState.COMPLETED\n        saveTimerState()\n        renderer.updateUIForState(state)\n\n        // summaryBuilder.secondsRemaining updated by timer\n        summaryBuilder.attemptedCount(gridItems.indexOfFirst { gridItem -> gridItem.value == renderer.getLastSelectedLastItemValue() } + 1)\n        summaryBuilder.incorrectCount(renderer.getToggledAnswers().size)\n        summaryBuilder.correctCount(summaryBuilder.attemptedCount - summaryBuilder.incorrectCount)\n        summaryBuilder.firstLineAllIncorrect(renderer.firstLineAllIncorrect())\n        summaryBuilder.sentencesPassed(gridItems.subList(0, summaryBuilder.attemptedCount)\n            .count { gridItem -> Regex(\"^\\\\p{Punct}+$\").matches(gridItem.text) })\n        summaryBuilder.correctItems(gridItems.subList(0, summaryBuilder.attemptedCount)\n            .filter { !renderer.getToggledAnswers().contains(it.value) }\n            .joinToString { it.text })\n        summaryBuilder.unansweredItems(gridItems.subList(\n            summaryBuilder.attemptedCount, gridItems.size\n        ).joinToString { it.text })\n        summaryBuilder.punctuationCount(gridItems.count { Regex(\"^\\\\p{Punct}+$\").matches(it.text) })\n        summaryAnswerCreator.answerSummaryQuestions(summaryBuilder.build())\n\n        widgetValueChanged()\n    }\n\n    private fun autoPickLastItem() {\n        val lastItemValue = gridItems.lastOrNull()?.value\n        if (lastItemValue != null) {\n            renderer.restoreAnswers(renderer.getToggledAnswers(), lastItemValue)\n        }\n    }\n\n    private fun saveTimerState() {\n        viewModel.saveTimedGridState(\n            formEntryPrompt.index,\n            TimedGridViewModel.TimedGridTimerState(\n                millisRemaining = timer.getMillisRemaining(),\n                state = state,\n                toggledAnswers = renderer.getToggledAnswers(),\n                lastAttempted = renderer.getLastSelectedLastItemValue()\n            )\n        )\n    }\n\n    fun shouldBlockNavigation(): NavigationWarning? {\n        val shouldNavigationBeBlocked = state == TimedGridState.IN_PROGRESS ||\n            state == TimedGridState.PAUSED ||\n            state == TimedGridState.COMPLETED_NO_LAST_ITEM\n\n        return if (shouldNavigationBeBlocked) {\n            NavigationWarning(\n                titleRes = R.string.assessment,\n                messageRes = R.string.assessment_warning\n            )\n        } else {\n            null\n        }\n    }\n}\n"
  },
  {
    "path": "timedgrid/src/main/java/org/odk/collect/timedgrid/TimedGridWidgetLayout.kt",
    "content": "package org.odk.collect.timedgrid\n\nimport android.content.res.ColorStateList\nimport android.graphics.Paint\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.core.content.ContextCompat\nimport com.google.android.material.button.MaterialButton\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport org.odk.collect.androidshared.system.ContextExt.getThemeAttributeValue\nimport org.odk.collect.timedgrid.databinding.TimedGridItemButtonBinding\nimport org.odk.collect.timedgrid.databinding.TimedGridItemRowBinding\n\ndata class GridItem(val value: String, val text: String)\n\nprivate enum class ButtonFinalState { DEFAULT, WRONG, LAST }\n\n/**\n * Compact grid used by the timed-grid widget.\n *\n * Responsibilities:\n *  - render items as MaterialButtons\n *  - track toggled (attempted/wrong) answers\n *  - support enabling a suffix of buttons for \"last-item\" selection\n *  - notify on last-item selection\n *  - reveal final visuals (wrong + last)\n */\nclass TimedGridWidgetLayout(\n    private val layoutInflater: LayoutInflater,\n    private val containerView: ViewGroup,\n    private val columns: Int,\n    private val rowsPerPage: Int,\n    private val type: AssessmentType,\n    private val showRowNumbers: Boolean,\n    private val strictMode: Boolean,\n    private val endAfterConsecutive: Int,\n    private val onComplete: () -> Unit,\n    private val allItems: List<GridItem>\n) {\n    private val toggledAnswers = mutableSetOf<String>()\n    private val buttons = mutableListOf<MaterialButton>()\n    private val valueForButton = mutableMapOf<MaterialButton, String>()\n\n    // original visuals so we can restore them\n    private val originalTextColor = mutableMapOf<MaterialButton, Int>()\n    private val originalBackgroundTint = mutableMapOf<MaterialButton, ColorStateList?>()\n\n    private var lastItemSelectionListener: ((String) -> Unit)? = null\n\n    // marker for last-item mode (start index from setEnabledFrom). null = not in mode\n    private var lastItemModeIndex: Int? = null\n\n    private var lastSelectedLastItemValue: String? = null\n\n    // Render all items in a single row container instead of chunking into multiple numbered rows\n    private val shouldRenderAsSingleRow = columns == 1 && type == AssessmentType.READING\n\n    private val orderedValues = mutableListOf<String>()\n    private val valueToIndexMap = mutableMapOf<String, Int>()\n\n    /**\n     * Determines whether the user has chosen to continue the assessment\n     * after exceeding the `end-after` consecutive mistakes limit.\n     */\n    private var userChoseToContinueAfterLimit = false\n\n    // Pagination\n    private var currentPage = 0\n    private var totalRows = 0\n\n    private companion object {\n        const val MARGIN_PERCENT_PER_BUTTON = 0.0025f\n        const val SMALL_SCREEN_DP_THRESHOLD = 600\n        const val MULTIPLE_ROWS_COLUMN_THRESHOLD = 5\n        val ONLY_PUNCTUATION_REGEX = Regex(\"^\\\\p{Punct}+$\")\n    }\n\n    init {\n        allItems.forEachIndexed { index, item ->\n            orderedValues.add(item.value)\n            valueToIndexMap[item.value] = index\n        }\n\n        buildAllRows()\n\n        showPage(0)\n        setGridEnabled(false)\n    }\n\n    private fun buildAllRows() {\n        val rows = if (shouldRenderAsSingleRow) {\n            listOf(allItems)\n        } else {\n            allItems.chunked(columns)\n        }\n        totalRows = rows.size\n\n        rows.forEachIndexed { rowIndex, rowItems ->\n            val rowBinding = TimedGridItemRowBinding.inflate(layoutInflater, containerView, false)\n\n            if (showRowNumbers) {\n                rowBinding.textviewRowNumber.text = containerView.context.getString(\n                    R.string.timed_grid_row_number,\n                    rowIndex + 1\n                )\n            } else {\n                rowBinding.textviewRowNumber.visibility = View.GONE\n            }\n\n            populateRow(layoutInflater, rowBinding, rowItems)\n            containerView.addView(rowBinding.root)\n        }\n    }\n\n    private fun populateRow(\n        layoutInflater: LayoutInflater,\n        rowBinding: TimedGridItemRowBinding,\n        rowItems: List<GridItem>\n    ) {\n        val flexBasisPercent = calculateFlexBasisPercent()\n\n        for (item in rowItems) {\n            val button = TimedGridItemButtonBinding.inflate(\n                layoutInflater, rowBinding.containerWordButtons, false\n            ).root\n\n            button.text = item.text\n            buttons.add(button)\n            valueForButton[button] = item.value\n\n            originalTextColor[button] = button.currentTextColor\n            originalBackgroundTint[button] = button.backgroundTintList\n\n            button.setOnClickListener { handleButtonClick(button, item) }\n\n            val lp = button.layoutParams as com.google.android.flexbox.FlexboxLayout.LayoutParams\n            if (shouldRenderAsSingleRow) {\n                lp.flexGrow = 1f\n                button.setPadding(24, 0, 24, 0)\n            } else {\n                lp.flexBasisPercent = flexBasisPercent\n                button.layoutParams = lp\n            }\n\n            rowBinding.containerWordButtons.addView(button)\n        }\n    }\n\n    private fun showPage(pageIndex: Int) {\n        currentPage = pageIndex\n        val startRow = pageIndex * rowsPerPage\n        val endRow = startRow + rowsPerPage\n\n        for (i in 0 until containerView.childCount) {\n            val rowView = containerView.getChildAt(i)\n            rowView.visibility = if (i in startRow until endRow) View.VISIBLE else View.GONE\n        }\n    }\n\n    fun nextPage() {\n        if (currentPage < getTotalPages() - 1) {\n            showPage(currentPage + 1)\n        }\n    }\n\n    fun prevPage() {\n        if (currentPage > 0) {\n            showPage(currentPage - 1)\n        }\n    }\n\n    fun isFirstPage() = currentPage == 0\n    fun isLastPage() = currentPage == getTotalPages() - 1\n    private fun getTotalPages() = (totalRows + rowsPerPage - 1) / rowsPerPage\n\n    fun setGridEnabled(isEnabled: Boolean) {\n        lastItemModeIndex = null\n        buttons.forEach { button ->\n            val isPunctuationInReading = type == AssessmentType.READING && ONLY_PUNCTUATION_REGEX.matches(button.text)\n            button.isEnabled = isEnabled && !isPunctuationInReading\n        }\n    }\n\n    fun setEnabledFrom(valueFrom: String?) {\n        val idx = buttons.indexOfFirst { valueForButton[it] == valueFrom }\n        val startIndex = idx + 1\n        lastItemModeIndex = startIndex.coerceAtMost(buttons.size)\n        buttons.forEachIndexed { i, btn -> btn.isEnabled = i >= startIndex }\n    }\n\n    fun setLastItemSelectionListener(listener: ((String) -> Unit)?) {\n        lastItemSelectionListener = listener\n    }\n\n    fun getLastToggledValue(): String? {\n        for (i in buttons.size - 1 downTo 0) {\n            val value = valueForButton[buttons[i]]\n            if (value != null && value in toggledAnswers) return value\n        }\n        return null\n    }\n\n    fun getLastSelectedLastItemValue(): String? = lastSelectedLastItemValue\n    fun getToggledAnswers(): Set<String> = toggledAnswers.toSet()\n\n    fun revealFinalSelection(wrongAnswers: Set<String>, lastItemValue: String?) {\n        buttons.forEach { btn ->\n            val value = valueForButton[btn]\n            val state = when {\n                value != null && value == lastItemValue -> ButtonFinalState.LAST\n                value != null && value in wrongAnswers -> ButtonFinalState.WRONG\n                else -> ButtonFinalState.DEFAULT\n            }\n            applyFinalVisual(btn, state)\n            btn.isEnabled = false\n        }\n        lastSelectedLastItemValue = lastItemValue\n    }\n\n    private fun calculateFlexBasisPercent(): Float {\n        var desiredCols = columns\n        if (isSmallScreen() && type == AssessmentType.LETTERS && columns > MULTIPLE_ROWS_COLUMN_THRESHOLD) {\n            desiredCols /= 2\n        }\n\n        if (showRowNumbers) {\n            desiredCols += 1\n        }\n\n        return (1f - desiredCols * MARGIN_PERCENT_PER_BUTTON) / desiredCols\n    }\n\n    private fun isSmallScreen(): Boolean {\n        val displayMetrics = containerView.context.resources.displayMetrics\n        val dpWidth = displayMetrics.widthPixels / displayMetrics.density\n        return dpWidth < SMALL_SCREEN_DP_THRESHOLD\n    }\n\n    private fun handleButtonClick(button: MaterialButton, item: GridItem) {\n        val inLastItemMode = lastItemModeIndex != null && button.isEnabled\n\n        if (inLastItemMode) {\n            MaterialAlertDialogBuilder(button.context)\n                .setTitle(R.string.last_attempted_item)\n                .setMessage(R.string.confirm_last_attempted_item)\n                .setPositiveButton(org.odk.collect.strings.R.string.yes) { _, _ ->\n                    // User confirmed -> pick last item: keep other visuals intact\n                    lastSelectedLastItemValue = item.value\n                    applyFinalVisual(button, ButtonFinalState.LAST)\n                    lastItemSelectionListener?.invoke(item.value)\n                    lastItemModeIndex = null\n                    buttons.forEach { it.isEnabled = false }\n                }\n                .setNegativeButton(org.odk.collect.strings.R.string.no) { _, _ ->\n                    // User declined -> remain in last-item mode so they can pick a different one.\n                }\n                .show()\n\n            return\n        }\n\n        // Normal toggle\n        val isSelected = item.value in toggledAnswers\n\n        if (isSelected) {\n            toggledAnswers.remove(item.value)\n        } else {\n            toggledAnswers.add(item.value)\n\n            val currentIndex = valueToIndexMap[item.value] ?: return\n\n            val backwardCount = countConsecutiveWrongFrom(currentIndex - 1, -1)\n            val forwardCount = countConsecutiveWrongFrom(currentIndex + 1, 1)\n\n            val totalStreak = backwardCount + 1 + forwardCount\n\n            if (endAfterConsecutive in 1..totalStreak) {\n                updateButtonVisuals(button, !isSelected)\n\n                if (strictMode) {\n                    showLimitExceededDialog(button)\n                } else {\n                    if (userChoseToContinueAfterLimit) {\n                        return\n                    }\n                    showConsecutiveMistakesChoiceDialog(button)\n                }\n                return\n            }\n        }\n\n        updateButtonVisuals(button, !isSelected)\n    }\n\n    private fun showLimitExceededDialog(button: MaterialButton) {\n        MaterialAlertDialogBuilder(button.context)\n            .setTitle(R.string.test_ended_early_title)\n            .setMessage(\n                button.context.getString(\n                    R.string.test_ended_early_message,\n                    endAfterConsecutive\n                )\n            )\n            .setPositiveButton(org.odk.collect.strings.R.string.ok) { _, _ ->\n                onComplete.invoke()\n            }\n            .setCancelable(false)\n            .show()\n    }\n\n    private fun showConsecutiveMistakesChoiceDialog(button: MaterialButton) {\n        MaterialAlertDialogBuilder(button.context)\n            .setTitle(R.string.consecutive_mistakes_title)\n            .setMessage(\n                button.context.getString(\n                    R.string.consecutive_mistakes_message,\n                    endAfterConsecutive\n                )\n            )\n            .setPositiveButton(R.string.end_test) { _, _ ->\n                onComplete.invoke()\n            }\n            .setNegativeButton(R.string.continue_test) { _, _ ->\n                userChoseToContinueAfterLimit = true\n            }\n            .show()\n    }\n\n    private fun countConsecutiveWrongFrom(startIndex: Int, step: Int): Int {\n        var count = 0\n        var index = startIndex\n\n        while (true) {\n            val value = orderedValues.getOrNull(index) ?: break\n            val item = allItems.getOrNull(index) ?: break\n\n            // Skip punctuation items (don’t count them, don’t break streak) in READING assessments\n            if (type == AssessmentType.READING && ONLY_PUNCTUATION_REGEX.matches(item.text)) {\n                index += step\n                continue\n            }\n\n            if (value in toggledAnswers) {\n                count++\n                index += step\n            } else {\n                break\n            }\n        }\n\n        return count\n    }\n\n    private fun updateButtonVisuals(button: MaterialButton, isSelected: Boolean) {\n        button.isSelected = isSelected\n        if (isSelected) {\n            button.paintFlags = button.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG\n        } else {\n            button.paintFlags = button.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()\n        }\n    }\n\n    fun setToggledAnswers(values: Set<String>) {\n        toggledAnswers.clear()\n        toggledAnswers.addAll(values)\n\n        // Refresh visuals for currently rendered buttons\n        for (btn in buttons) {\n            val v = valueForButton[btn]\n            val selected = v != null && v in toggledAnswers\n            btn.isSelected = selected\n            btn.paintFlags = if (selected) {\n                btn.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG\n            } else {\n                btn.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()\n            }\n        }\n    }\n\n    /**\n     * Set last-selected last-item value (visual only). Pass null to clear.\n     */\n    fun setLastSelectedLastItemValue(value: String?) {\n        // Clear previous\n        lastSelectedLastItemValue?.let { prev ->\n            val prevBtn = buttons.find { valueForButton[it] == prev }\n            prevBtn?.let { applyFinalVisual(it, ButtonFinalState.DEFAULT) }\n        }\n\n        lastSelectedLastItemValue = value\n        value?.let { v ->\n            val btn = buttons.find { valueForButton[it] == v }\n            btn?.let { applyFinalVisual(it, ButtonFinalState.LAST) }\n        }\n    }\n\n    fun firstLineSelected(): Boolean {\n        if (buttons.size < columns || type == AssessmentType.READING) {\n            return false\n        }\n        val firstLineButtons = buttons.take(columns)\n        return firstLineButtons.all { it.isSelected }\n    }\n\n    /**\n     * Apply final look for a button:\n     *  - DEFAULT: restore originals\n     *  - WRONG: selected + strike-through + error text color\n     *  - LAST: selected + no strike-through + green background tint\n     */\n    private fun applyFinalVisual(button: MaterialButton, state: ButtonFinalState) {\n        when (state) {\n            ButtonFinalState.DEFAULT -> {\n                originalBackgroundTint[button]?.let { button.backgroundTintList = it }\n                originalTextColor[button]?.let { button.setTextColor(it) }\n                button.paintFlags = button.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()\n                button.isSelected = false\n            }\n            ButtonFinalState.WRONG -> {\n                button.isSelected = true\n                button.paintFlags = button.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG\n                button.setTextColor(getThemeAttributeValue(button.context, androidx.appcompat.R.attr.colorError))\n            }\n            ButtonFinalState.LAST -> {\n                button.isSelected = true\n                button.paintFlags = button.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()\n                button.backgroundTintList = ContextCompat.getColorStateList(\n                    button.context,\n                    R.color.timedGridButtonGreenLastAnswer\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "timedgrid/src/main/res/color/timed_grid_button_tint_selector.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <!--\n    RULE 1: SUMMARY MODE - SELECTED\n    Condition: Disabled AND Selected.\n    Visual: Yellow background.\n    -->\n    <item android:color=\"@color/timedGridButtonYellowSummary\"\n        android:state_enabled=\"false\"\n        android:state_selected=\"true\" />\n\n    <!--\n    RULE 2: SUMMARY / PRE-TEST MODE - NOT SELECTED\n    Condition: Disabled (and not caught by Rule 1).\n    Visual: Gray background.\n    -->\n    <item android:color=\"@color/timedGridButtonGray\"\n        android:state_enabled=\"false\" />\n\n    <!--\n    RULE 3: ACTIVE MODE - SELECTED\n    Condition: Enabled AND Selected.\n    Visual: Orange background.\n    -->\n    <item android:color=\"@color/timedGridButtonOrangeSelected\"\n        android:state_selected=\"true\" />\n\n    <!--\n    RULE 4: ACTIVE MODE - NOT SELECTED (DEFAULT)\n    Condition: Enabled (and not caught by Rule 3).\n    Visual: Blue background.\n    -->\n    <item android:color=\"@color/timedGridButtonBlueDefault\" />\n\n</selector>"
  },
  {
    "path": "timedgrid/src/main/res/drawable/row_number_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/timedGridButtonGray\" />\n    <corners android:radius=\"8dp\" />\n</shape>"
  },
  {
    "path": "timedgrid/src/main/res/layout/timed_grid.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/timed_grid_root_container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <com.google.android.material.button.MaterialButton\n        android:id=\"@+id/button_start\"\n        style=\"@style/TimedGridStateButton\"\n        android:text=\"@string/timed_grid_start\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <com.google.android.material.button.MaterialButton\n        android:id=\"@+id/button_timer\"\n        style=\"@style/TimedGridStateButton.Red\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <com.google.android.material.button.MaterialButton\n        android:id=\"@+id/button_finish\"\n        style=\"@style/TimedGridStateButton\"\n        android:text=\"@string/timed_grid_test_complete\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n    <androidx.constraintlayout.widget.Barrier\n        android:id=\"@+id/top_toolbar_barrier\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:barrierDirection=\"bottom\"\n        app:constraint_referenced_ids=\"button_start,button_timer,button_finish\" />\n\n    <LinearLayout\n        android:id=\"@+id/container_rows\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:orientation=\"vertical\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/top_toolbar_barrier\" />\n\n    <com.google.android.material.button.MaterialButton\n        android:id=\"@+id/button_prev\"\n        style=\"@style/TimedGridStateButton\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:text=\"@string/timed_grid_prev_page\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/container_rows\" />\n\n    <com.google.android.material.button.MaterialButton\n        android:id=\"@+id/button_next\"\n        style=\"@style/TimedGridStateButton\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:text=\"@string/timed_grid_next_page\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/container_rows\" />\n\n    <com.google.android.material.button.MaterialButton\n        android:id=\"@+id/button_complete\"\n        style=\"@style/TimedGridStateButton.Red\"\n        android:layout_marginTop=\"@dimen/margin_extra_small\"\n        android:text=\"@string/timed_grid_early_finish\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@id/container_rows\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "timedgrid/src/main/res/layout/timed_grid_item_button.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.button.MaterialButton\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"60dp\"\n    android:layout_marginEnd=\"@dimen/margin_extra_extra_small\"\n    android:layout_marginStart=\"@dimen/margin_extra_extra_small\"\n    android:minWidth=\"0dp\"\n    android:padding=\"0dp\"\n\n    android:textColor=\"@android:color/black\"\n    android:gravity=\"center\"\n    android:textSize=\"16sp\"\n\n    app:backgroundTint=\"@color/timed_grid_button_tint_selector\"\n    app:cornerRadius=\"@dimen/corner_radius_extra_extra_small\" />"
  },
  {
    "path": "timedgrid/src/main/res/layout/timed_grid_item_row.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_vertical\"\n    android:orientation=\"horizontal\"\n    android:paddingBottom=\"@dimen/margin_extra_small\">\n\n    <!-- Row number indicator -->\n    <TextView\n        android:id=\"@+id/textview_row_number\"\n        android:layout_width=\"@dimen/margin_extra_extra_large\"\n        android:layout_height=\"@dimen/margin_extra_extra_large\"\n        android:layout_marginEnd=\"@dimen/margin_extra_small\"\n        android:background=\"@drawable/row_number_background\"\n        android:gravity=\"center\"\n        android:textColor=\"#333\" />\n\n    <!-- Container for buttons -->\n    <com.google.android.flexbox.FlexboxLayout\n        android:id=\"@+id/container_word_buttons\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        app:flexDirection=\"row\"\n        app:flexWrap=\"wrap\"\n        app:justifyContent=\"flex_start\"\n        app:alignItems=\"center\"\n        app:alignContent=\"flex_start\" />\n\n</LinearLayout>"
  },
  {
    "path": "timedgrid/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"timedGridButtonBlueDefault\">#2196F3</color>\n    <color name=\"timedGridButtonOrangeSelected\">#FF9800</color>\n    <color name=\"timedGridButtonGray\">#E0E0E0</color>\n    <color name=\"timedGridButtonYellowSummary\">#FFEB3B</color>\n    <color name=\"timedGridButtonGreenLastAnswer\">#4CAF50</color>\n</resources>"
  },
  {
    "path": "timedgrid/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <dimen name=\"corner_radius_extra_extra_small\">4dp</dimen>\n    <dimen name=\"corner_radius_extra_small\">8dp</dimen>\n</resources>"
  },
  {
    "path": "timedgrid/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!--\n     ##############################################\n     # Timed Grid Test\n     ##############################################\n    -->\n    <string name=\"last_attempted_item\">Last Attempted Item</string>\n    <string name=\"confirm_last_attempted_item\">Do you want to confirm the last attempted item?</string>\n\n    <string name=\"test_ended_early_title\">Test Ended Early</string>\n    <string name=\"test_ended_early_message\">\n    You have reached the limit of %d consecutive wrong answers.\n    </string>\n\n    <!-- Warning shown when user reaches the consecutive mistakes limit in non-strict mode -->\n    <string name=\"consecutive_mistakes_title\">Consecutive Mistakes</string>\n    <string name=\"consecutive_mistakes_message\">\n    You have made %d consecutive mistakes. Do you want to end the test or continue?\n    </string>\n\n    <!-- Buttons for the choice dialog -->\n    <string name=\"end_test\">End Test</string>\n    <string name=\"continue_test\">Continue</string>\n\n    <!-- Confirmation that user wants to stop the end -->\n    <string name=\"early_finish_title\">Early Finish</string>\n    <string name=\"early_finish_message\">Do you want to end the test now?</string>\n\n    <string name=\"timed_grid_start\">Start</string>\n    <string name=\"timed_grid_time_left\">Time Left: %d</string>\n    <string name=\"timed_grid_test_complete\">Test Complete</string>\n    <string name=\"timed_grid_prev_page\">Prev Page</string>\n    <string name=\"timed_grid_next_page\">Next Page</string>\n    <string name=\"timed_grid_early_finish\">Early Finish</string>\n    <string name=\"timed_grid_row_number\">(%d)</string>\n\n    <string name=\"assessment\">Assessment…</string>\n    <string name=\"assessment_warning\">You must finish assessment before leaving this screen.</string>\n</resources>"
  },
  {
    "path": "timedgrid/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"TimedGridStateButton\" parent=\"Widget.MaterialComponents.Button\">\n        <item name=\"android:layout_width\">wrap_content</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:textColor\">@android:color/white</item>\n        <item name=\"android:paddingStart\">@dimen/margin_large</item>\n        <item name=\"android:paddingEnd\">@dimen/margin_large</item>\n        <item name=\"android:minWidth\">180dp</item>\n        <item name=\"android:insetTop\">0dp</item>\n        <item name=\"android:insetBottom\">0dp</item>\n        <item name=\"cornerRadius\">@dimen/corner_radius_extra_small</item>\n    </style>\n\n    <style name=\"TimedGridStateButton.Red\" parent=\"TimedGridStateButton\">\n        <item name=\"backgroundTint\">?colorError</item>\n    </style>\n</resources>"
  },
  {
    "path": "upgrade/.gitignore",
    "content": "/build"
  },
  {
    "path": "upgrade/README.md",
    "content": "# upgrade\n\nMicro framework for handling app upgrades.\n"
  },
  {
    "path": "upgrade/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n    alias(libs.plugins.kotlinKsp)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    namespace = \"org.odk.collect.upgrade\"\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(libs.androidxCoreKtx)\n    implementation(project(\":shared\"))\n\n    testImplementation(project(\":test-shared\"))\n    testImplementation(libs.mockitoKotlin)\n    testImplementation(libs.junit)\n    testImplementation(libs.hamcrest)\n}\n"
  },
  {
    "path": "upgrade/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>\n"
  },
  {
    "path": "upgrade/src/main/java/org/odk/collect/upgrade/AppUpgrader.kt",
    "content": "package org.odk.collect.upgrade\n\nimport android.app.Application\nimport org.odk.collect.shared.settings.Settings\n\nclass AppUpgrader internal constructor(\n    private val settings: Settings,\n    private val launchState: LaunchState,\n    private val upgrades: List<Upgrade>\n) {\n    constructor(\n        key: String,\n        settings: Settings,\n        versionCode: Int,\n        installDetector: InstallDetector,\n        upgrades: List<Upgrade>\n    ) : this(\n        settings,\n        VersionCodeLaunchState(key, settings, versionCode, installDetector),\n        upgrades\n    )\n\n    /**\n     * Runs the list of passed [Upgrade] implementations in order if this is the first launch of a\n     * new version of the app (an \"app upgrade\"). This should be called in (or from somewhere called\n     * from) [Application.onCreate].\n     *\n     * [Upgrade]s that return non-null values from [Upgrade.key] will only be run if they have not\n     * been run before.\n     */\n    fun upgradeIfNeeded() {\n        if (launchState.isUpgradedFirstLaunch()) {\n            upgrades.forEach {\n                val key = it.key()\n\n                if (key == null) {\n                    it.run()\n                } else if (!settings.getBoolean(key)) {\n                    it.run()\n                    settings.save(key, true)\n                }\n            }\n        } else {\n            upgrades.forEach {\n                it.key()?.let { key ->\n                    settings.save(key, true)\n                }\n            }\n        }\n\n        launchState.appLaunched()\n    }\n}\n"
  },
  {
    "path": "upgrade/src/main/java/org/odk/collect/upgrade/LaunchState.kt",
    "content": "package org.odk.collect.upgrade\n\nimport org.odk.collect.shared.settings.Settings\n\ninternal interface LaunchState {\n    fun isUpgradedFirstLaunch(): Boolean\n    fun appLaunched()\n}\n\ninternal class VersionCodeLaunchState(\n    private val key: String,\n    private val settings: Settings,\n    private val currentVersion: Int,\n    private val installDetector: InstallDetector\n) : LaunchState {\n\n    override fun isUpgradedFirstLaunch(): Boolean {\n        return if (settings.contains(key)) {\n            settings.getInt(key) < currentVersion\n        } else {\n            return installDetector.installDetected()\n        }\n    }\n\n    override fun appLaunched() {\n        settings.save(key, currentVersion)\n    }\n}\n\ninterface InstallDetector {\n    fun installDetected(): Boolean\n}\n"
  },
  {
    "path": "upgrade/src/main/java/org/odk/collect/upgrade/Upgrade.kt",
    "content": "package org.odk.collect.upgrade\n\ninterface Upgrade {\n\n    /**\n     * If this returns a non-`null` value, the [Upgrade] will only be run once (the next app\n     * upgrade). [Upgrade] implementations that return `null` from [Upgrade.key] will be run on\n     * every app upgrade.\n     */\n    fun key(): String?\n\n    fun run()\n}\n"
  },
  {
    "path": "upgrade/src/test/java/org/odk/collect/upgrade/AppUpgraderTest.kt",
    "content": "package org.odk.collect.upgrade\n\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.mockito.kotlin.times\nimport org.mockito.kotlin.verify\nimport org.mockito.kotlin.whenever\nimport org.odk.collect.shared.settings.InMemSettings\n\nclass AppUpgraderTest {\n\n    @Test\n    fun `upgradeIfNeeded() runs all upgrades`() {\n        val upgrade1 = mock<Upgrade>()\n        val upgrade2 = mock<Upgrade>()\n\n        val appUpgrader = AppUpgrader(\n            InMemSettings(),\n            mock { on { isUpgradedFirstLaunch() } doReturn true },\n            listOf(upgrade1, upgrade2)\n        )\n\n        appUpgrader.upgradeIfNeeded()\n\n        verify(upgrade1).run()\n        verify(upgrade2).run()\n    }\n\n    @Test\n    fun `upgradeIfNeeded() skips upgrades with a key the second time`() {\n        val upgrade1 = mock<Upgrade> {\n            on { key() } doReturn \"blah\"\n        }\n\n        val upgrade2 = mock<Upgrade> {\n            on { key() } doReturn null\n        }\n\n        val appUpgrader = AppUpgrader(\n            InMemSettings(),\n            mock { on { isUpgradedFirstLaunch() } doReturn true },\n            listOf(upgrade1, upgrade2)\n        )\n\n        appUpgrader.upgradeIfNeeded()\n        appUpgrader.upgradeIfNeeded()\n\n        verify(upgrade1, times(1)).run()\n        verify(upgrade2, times(2)).run()\n    }\n\n    @Test\n    fun `upgradeIfNeeded() just marks upgrades with a key as run if not upgrading`() {\n        val upgrade1 = mock<Upgrade> {\n            on { key() } doReturn \"blah\"\n        }\n\n        val upgrade2 = mock<Upgrade> {\n            on { key() } doReturn null\n        }\n\n        val launchState = mock<LaunchState> { on { isUpgradedFirstLaunch() } doReturn false }\n        val appUpgrader = AppUpgrader(\n            InMemSettings(),\n            launchState,\n            listOf(upgrade1, upgrade2)\n        )\n\n        appUpgrader.upgradeIfNeeded()\n        verify(upgrade1, times(0)).run()\n        verify(upgrade2, times(0)).run()\n\n        whenever(launchState.isUpgradedFirstLaunch()).thenReturn(true)\n        appUpgrader.upgradeIfNeeded()\n        verify(upgrade1, times(0)).run()\n        verify(upgrade2, times(1)).run()\n    }\n\n    @Test\n    fun `upgradeIfNeeded() calls appLaunched()`() {\n        val launchState = mock<LaunchState> { on { isUpgradedFirstLaunch() } doReturn true }\n        val appUpgrader = AppUpgrader(\n            InMemSettings(),\n            launchState,\n            emptyList()\n        )\n\n        appUpgrader.upgradeIfNeeded()\n        verify(launchState).appLaunched()\n    }\n}\n"
  },
  {
    "path": "upgrade/src/test/java/org/odk/collect/upgrade/VersionCodeLaunchStateTest.kt",
    "content": "package org.odk.collect.upgrade\n\nimport org.hamcrest.MatcherAssert.assertThat\nimport org.hamcrest.Matchers.equalTo\nimport org.junit.Test\nimport org.mockito.kotlin.doReturn\nimport org.mockito.kotlin.mock\nimport org.odk.collect.shared.settings.InMemSettings\n\nclass VersionCodeLaunchStateTest {\n\n    @Test\n    fun `isUpgradedFirstLaunch() returns false for empty settings`() {\n        val appStateProvider = VersionCodeLaunchState(\"key\", InMemSettings(), 1, mock())\n        assertThat(appStateProvider.isUpgradedFirstLaunch(), equalTo(false))\n    }\n\n    @Test\n    fun `isUpgradedFirstLaunch() returns false if last launched is equal to current version`() {\n        val inMemSettings = InMemSettings()\n        inMemSettings.save(\"key\", 1)\n\n        val appStateProvider = VersionCodeLaunchState(\"key\", inMemSettings, 1, mock())\n        assertThat(appStateProvider.isUpgradedFirstLaunch(), equalTo(false))\n    }\n\n    @Test\n    fun `isUpgradedFirstLaunch() returns true if last launched is less than current version`() {\n        val inMemSettings = InMemSettings()\n        inMemSettings.save(\"key\", 1)\n\n        val appStateProvider = VersionCodeLaunchState(\"key\", inMemSettings, 2, mock())\n        assertThat(appStateProvider.isUpgradedFirstLaunch(), equalTo(true))\n    }\n\n    @Test\n    fun `isUpgradedFirstLaunch() returns false if last launched is less than current version after appLaunched()`() {\n        val inMemSettings = InMemSettings()\n        inMemSettings.save(\"key\", 1)\n\n        val appStateProvider = VersionCodeLaunchState(\"key\", inMemSettings, 2, mock())\n        appStateProvider.appLaunched()\n        assertThat(appStateProvider.isUpgradedFirstLaunch(), equalTo(false))\n    }\n\n    @Test\n    fun `isUpgradedFirstLaunch() returns true for empty settings when legacy install detected`() {\n        val appStateProvider = VersionCodeLaunchState(\n            \"key\",\n            InMemSettings(),\n            1,\n            mock { on { installDetected() } doReturn true }\n        )\n\n        assertThat(appStateProvider.isUpgradedFirstLaunch(), equalTo(true))\n    }\n\n    @Test\n    fun `isUpgradedFirstLaunch() returns false for empty settings when legacy install not detected`() {\n        val appStateProvider = VersionCodeLaunchState(\n            \"key\",\n            InMemSettings(),\n            1,\n            mock { on { installDetected() } doReturn false }\n        )\n\n        assertThat(appStateProvider.isUpgradedFirstLaunch(), equalTo(false))\n    }\n}\n"
  },
  {
    "path": "web-page/.gitignore",
    "content": "/build"
  },
  {
    "path": "web-page/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.androidLibrary)\n}\n\napply(from = \"../config/quality.gradle\")\n\nandroid {\n    namespace = \"org.odk.collect.webpage\"\n\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    testOptions {\n        unitTests {\n            isIncludeAndroidResources = true\n        }\n    }\n\n    buildFeatures {\n        viewBinding = true\n    }\n}\n\ndependencies {\n    coreLibraryDesugaring(libs.desugar)\n\n    implementation(project(\":androidshared\"))\n    implementation(project(\":icons\"))\n    implementation(project(\":strings\"))\n    implementation(libs.androidMaterial)\n    implementation(libs.androidxBrowser)\n\n    testImplementation(libs.androidxTestExtJunit)\n    testImplementation(libs.hamcrest)\n    testImplementation(libs.robolectric)\n}\n"
  },
  {
    "path": "web-page/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "web-page/src/main/java/org/odk/collect/webpage/CustomTabsWebPageService.kt",
    "content": "package org.odk.collect.webpage\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.browser.customtabs.CustomTabsIntent\nimport org.odk.collect.androidshared.ui.ToastUtils.showLongToast\n\nobject CustomTabsWebPageService : WebPageService {\n\n    /**\n     * Opens web page using Android Custom Tabs. If the user's browser doesn't support Custom Tabs,\n     * the Uri will just be opened in their device's default browser.\n     */\n    override fun openWebPage(activity: Activity, uri: Uri) {\n        val normalizedUri = uri.normalizeScheme()\n\n        try {\n            openUriInCustomTab(activity, normalizedUri)\n        } catch (_: Throwable) {\n            openWebPageInBrowser(activity, normalizedUri)\n        }\n    }\n\n    private fun openWebPageInBrowser(activity: Activity, uri: Uri) {\n        val normalizedUri = uri.normalizeScheme()\n\n        try {\n            activity.startActivity(Intent(Intent.ACTION_VIEW, normalizedUri))\n        } catch (_: Throwable) {\n            showLongToast(org.odk.collect.strings.R.string.no_browser_installed)\n        }\n    }\n\n    private fun openUriInCustomTab(context: Context, uri: Uri) {\n        CustomTabsIntent.Builder().build().launchUrl(context, uri)\n    }\n}\n"
  },
  {
    "path": "web-page/src/main/java/org/odk/collect/webpage/WebPageService.kt",
    "content": "package org.odk.collect.webpage\n\nimport android.app.Activity\nimport android.net.Uri\n\ninterface WebPageService {\n    fun openWebPage(activity: Activity, uri: Uri)\n}\n"
  },
  {
    "path": "web-page/src/test/java/org/odk/collect/webpage/CustomTabsWebPageServiceTest.java",
    "content": "package org.odk.collect.webpage;\n\nimport android.app.Activity;\nimport android.net.Uri;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.Robolectric;\n\nimport static android.net.Uri.parse;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.robolectric.Shadows.shadowOf;\n\n@RunWith(AndroidJUnit4.class)\npublic class CustomTabsWebPageServiceTest {\n\n    @Test\n    public void uriShouldBeNormalized() {\n        Activity activity = Robolectric.buildActivity(Activity.class).get();\n\n        CustomTabsWebPageService.INSTANCE.openWebPage(activity, parse(\"HTTP://example.com\"));\n\n        Uri uri = shadowOf(activity).getNextStartedActivity().getData();\n        assertThat(uri, equalTo(Uri.parse(\"http://example.com\")));\n    }\n}\n"
  }
]